Consolidate KittyCAD API token environment variables (#7665)

* Consolidate KittyCAD API token environment variables

* Remove duplicate variable in type definition

* Remove unnecessary intermediate steps

* Keep base label for concatenation functions
This commit is contained in:
Jace Browning
2025-07-03 13:15:21 -04:00
committed by GitHub
parent df6256266c
commit 34494f3bba
18 changed files with 46 additions and 58 deletions

View File

@ -3,17 +3,17 @@
NODE_ENV=development NODE_ENV=development
DEV=true DEV=true
# App
VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.dev.zoo.dev VITE_KITTYCAD_API_BASE_URL=https://api.dev.zoo.dev
VITE_KC_SITE_BASE_URL=https://dev.zoo.dev VITE_KC_SITE_BASE_URL=https://dev.zoo.dev
VITE_KC_SITE_APP_URL=https://app.dev.zoo.dev VITE_KC_SITE_APP_URL=https://app.dev.zoo.dev
VITE_KC_CONNECTION_TIMEOUT_MS=5000 VITE_KC_CONNECTION_TIMEOUT_MS=5000
#VITE_WASM_URL="optional way of overriding the wasm url, particular for unit tests which need this if you running not on the default 3000 port" #VITE_WASM_URL="optional override of Wasm URL if not on default port 3000"
#VITE_KC_DEV_TOKEN="optional token to skip auth in the app" #VITE_KITTYCAD_API_TOKEN="required for testing, optional to skip auth in the app"
#token="required token for playwright. TODO: clean up env vars in #3973" FAIL_ON_CONSOLE_ERRORS=true
# KCL
RUST_BACKTRACE=1 RUST_BACKTRACE=1
PYO3_PYTHON=/usr/local/bin/python3 PYO3_PYTHON=/usr/local/bin/python3
#KITTYCAD_API_TOKEN="required token for engine testing" #KITTYCAD_API_TOKEN=$VITE_KITTYCAD_API_TOKEN
FAIL_ON_CONSOLE_ERRORS=true

View File

@ -1,6 +1,8 @@
NODE_ENV=production NODE_ENV=production
# App
VITE_KC_API_WS_MODELING_URL=wss://api.zoo.dev/ws/modeling/commands VITE_KC_API_WS_MODELING_URL=wss://api.zoo.dev/ws/modeling/commands
VITE_KC_API_BASE_URL=https://api.zoo.dev VITE_KITTYCAD_API_BASE_URL=https://api.zoo.dev
VITE_KC_SITE_BASE_URL=https://zoo.dev VITE_KC_SITE_BASE_URL=https://zoo.dev
VITE_KC_SITE_APP_URL=https://app.zoo.dev VITE_KC_SITE_APP_URL=https://app.zoo.dev
VITE_KC_CONNECTION_TIMEOUT_MS=15000 VITE_KC_CONNECTION_TIMEOUT_MS=15000

View File

@ -157,7 +157,7 @@ jobs:
timeout_minutes: 5 timeout_minutes: 5
max_attempts: 5 max_attempts: 5
env: env:
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} VITE_KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
TAB_API_URL: ${{ secrets.TAB_API_URL }} TAB_API_URL: ${{ secrets.TAB_API_URL }}
TAB_API_KEY: ${{ secrets.TAB_API_KEY }} TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }} CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
@ -169,7 +169,7 @@ jobs:
if: always() if: always()
run: npm run test:snapshots -- --last-failed --update-snapshots run: npm run test:snapshots -- --last-failed --update-snapshots
env: env:
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} VITE_KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
TAB_API_URL: ${{ secrets.TAB_API_URL }} TAB_API_URL: ${{ secrets.TAB_API_URL }}
TAB_API_KEY: ${{ secrets.TAB_API_KEY }} TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }} CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
@ -284,7 +284,7 @@ jobs:
timeout_minutes: 5 timeout_minutes: 5
max_attempts: 5 max_attempts: 5
env: env:
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} VITE_KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
TAB_API_URL: ${{ secrets.TAB_API_URL }} TAB_API_URL: ${{ secrets.TAB_API_URL }}
TAB_API_KEY: ${{ secrets.TAB_API_KEY }} TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }} CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
@ -410,7 +410,7 @@ jobs:
max_attempts: 9 max_attempts: 9
env: env:
FAIL_ON_CONSOLE_ERRORS: true FAIL_ON_CONSOLE_ERRORS: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} VITE_KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
TAB_API_URL: ${{ secrets.TAB_API_URL }} TAB_API_URL: ${{ secrets.TAB_API_URL }}
TAB_API_KEY: ${{ secrets.TAB_API_KEY }} TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }} CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}

View File

@ -62,7 +62,7 @@ jobs:
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }} if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: xvfb-run -a npm run test:unit run: xvfb-run -a npm run test:unit
env: env:
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} VITE_KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- name: Check for changes - name: Check for changes
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }} if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}

View File

@ -65,7 +65,7 @@ If you're not a Zoo employee you won't be able to access the dev environment, yo
### Development environment variables ### Development environment variables
The Copilot LSP plugin in the editor requires a Zoo API token to run. In production, we authenticate this with a token via cookie in the browser and device auth token in the desktop environment, but this token is inaccessible in the dev browser version because the cookie is considered "cross-site" (from `localhost` to `zoo.dev`). There is an optional environment variable called `VITE_KC_DEV_TOKEN` that you can populate with a dev token in a `.env.development.local` file to not check it into Git, which will use that token instead of other methods for the LSP service. The Copilot LSP plugin in the editor requires a Zoo API token to run. In production, we authenticate this with a token via cookie in the browser and device auth token in the desktop environment, but this token is inaccessible in the dev browser version because the cookie is considered "cross-site" (from `localhost` to `zoo.dev`). There is an optional environment variable called `VITE_KITTYCAD_API_TOKEN` that you can populate with a dev token in a `.env.development.local` file to not check it into Git, which will use that token instead of other methods for the LSP service.
### Developing in Chrome ### Developing in Chrome
@ -96,7 +96,7 @@ To package the app for your platform with electron-builder, run `npm run tronb:p
Prepare these system dependencies: Prepare these system dependencies:
- Set $token from https://zoo.dev/account/api-tokens - Set `$VITE_KITTYCAD_API_TOKEN` from https://zoo.dev/account/api-tokens
#### Snapshot tests (Google Chrome on Ubuntu only) #### Snapshot tests (Google Chrome on Ubuntu only)
@ -259,7 +259,7 @@ If the application needs to overwrite the known file on disk use this pattern. T
- `npm run circular-deps:overwrite` - `npm run circular-deps:overwrite`
- `npm run url-checker:overwrite` - `npm run url-checker:overwrite`
#### Diff baseline and current #### Diff baseline and current
These commands will write a /tmp/ file on disk and compare it to the known file in the repository. This command will also be used in the CI CD pipeline for automated checks These commands will write a /tmp/ file on disk and compare it to the known file in the repository. This command will also be used in the CI CD pipeline for automated checks

View File

@ -1,6 +1,5 @@
import { expect, test } from '@e2e/playwright/zoo-test' import { expect, test } from '@e2e/playwright/zoo-test'
// test file is for testing auth functionality
test.describe('Authentication tests', () => { test.describe('Authentication tests', () => {
test( test(
`The user can sign out and back in`, `The user can sign out and back in`,
@ -13,22 +12,12 @@ test.describe('Authentication tests', () => {
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.projectSection.waitFor() await homePage.projectSection.waitFor()
// This is only needed as an override to test-utils' setup() for this test
await page.addInitScript(() => {
localStorage.setItem('TOKEN_PERSIST_KEY', '')
})
await test.step('Click on sign out and expect sign in page', async () => { await test.step('Click on sign out and expect sign in page', async () => {
await toolbar.userSidebarButton.click() await toolbar.userSidebarButton.click()
await toolbar.signOutButton.click() await toolbar.signOutButton.click()
await expect(signInPage.signInButton).toBeVisible() await expect(signInPage.signInButton).toBeVisible()
}) })
await test.step("Refresh doesn't log the user back in", async () => {
await page.reload()
await expect(signInPage.signInButton).toBeVisible()
})
await test.step('Click on sign in and cancel, click again and expect different code', async () => { await test.step('Click on sign in and cancel, click again and expect different code', async () => {
await signInPage.signInButton.click() await signInPage.signInButton.click()
await expect(signInPage.userCode).toBeVisible() await expect(signInPage.userCode).toBeVisible()

View File

@ -17,7 +17,7 @@ import dotenv from 'dotenv'
const NODE_ENV = process.env.NODE_ENV || 'development' const NODE_ENV = process.env.NODE_ENV || 'development'
dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] }) dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] })
export const token = process.env.token || '' export const token = process.env.VITE_KITTYCAD_API_TOKEN || ''
import type { ProjectConfiguration } from '@rust/kcl-lib/bindings/ProjectConfiguration' import type { ProjectConfiguration } from '@rust/kcl-lib/bindings/ProjectConfiguration'

5
interface.d.ts vendored
View File

@ -73,13 +73,12 @@ export interface IElectronAPI {
process: { process: {
env: { env: {
IS_PLAYWRIGHT: string IS_PLAYWRIGHT: string
VITE_KC_DEV_TOKEN: string VITE_KITTYCAD_API_TOKEN: string
VITE_KC_API_WS_MODELING_URL: string VITE_KC_API_WS_MODELING_URL: string
VITE_KC_API_BASE_URL: string VITE_KITTYCAD_API_BASE_URL: string
VITE_KC_SITE_BASE_URL: string VITE_KC_SITE_BASE_URL: string
VITE_KC_SITE_APP_URL: string VITE_KC_SITE_APP_URL: string
VITE_KC_CONNECTION_TIMEOUT_MS: string VITE_KC_CONNECTION_TIMEOUT_MS: string
VITE_KC_DEV_TOKEN: string
NODE_ENV: string NODE_ENV: string
PROD: string PROD: string
DEV: string DEV: string

View File

@ -8,12 +8,14 @@ export const NODE_ENV = env.NODE_ENV as string | undefined
export const VITE_KC_API_WS_MODELING_URL = env.VITE_KC_API_WS_MODELING_URL as export const VITE_KC_API_WS_MODELING_URL = env.VITE_KC_API_WS_MODELING_URL as
| string | string
| undefined | undefined
export const VITE_KC_API_BASE_URL = env.VITE_KC_API_BASE_URL export const VITE_KITTYCAD_API_BASE_URL = env.VITE_KITTYCAD_API_BASE_URL
export const VITE_KC_SITE_BASE_URL = env.VITE_KC_SITE_BASE_URL export const VITE_KC_SITE_BASE_URL = env.VITE_KC_SITE_BASE_URL
export const VITE_KC_SITE_APP_URL = env.VITE_KC_SITE_APP_URL export const VITE_KC_SITE_APP_URL = env.VITE_KC_SITE_APP_URL
export const VITE_KC_CONNECTION_TIMEOUT_MS = export const VITE_KC_CONNECTION_TIMEOUT_MS =
env.VITE_KC_CONNECTION_TIMEOUT_MS as string | undefined env.VITE_KC_CONNECTION_TIMEOUT_MS as string | undefined
export const VITE_KC_DEV_TOKEN = env.VITE_KC_DEV_TOKEN as string | undefined export const VITE_KITTYCAD_API_TOKEN = env.VITE_KITTYCAD_API_TOKEN as
| string
| undefined
export const PROD = env.PROD as string | undefined export const PROD = env.PROD as string | undefined
export const TEST = env.TEST as string | undefined export const TEST = env.TEST as string | undefined
export const DEV = env.DEV as string | undefined export const DEV = env.DEV as string | undefined

View File

@ -1,4 +1,4 @@
import { VITE_KC_DEV_TOKEN } from '@src/env' import { VITE_KITTYCAD_API_TOKEN } from '@src/env'
import { createLiteral } from '@src/lang/create' import { createLiteral } from '@src/lang/create'
import type { import type {
@ -40,10 +40,9 @@ import { isOverlap } from '@src/lib/utils'
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
await new Promise((resolve) => { await new Promise((resolve) => {
engineCommandManager.start({ engineCommandManager.start({
token: VITE_KC_DEV_TOKEN, token: VITE_KITTYCAD_API_TOKEN,
width: 256, width: 256,
height: 256, height: 256,
setMediaStream: () => {}, setMediaStream: () => {},

View File

@ -4,16 +4,15 @@ import { initPromise } from '@src/lang/wasmUtils'
import { err } from '@src/lib/trap' import { err } from '@src/lib/trap'
import type { Selection } from '@src/lib/selections' import type { Selection } from '@src/lib/selections'
import { engineCommandManager, kclManager } from '@src/lib/singletons' import { engineCommandManager, kclManager } from '@src/lib/singletons'
import { VITE_KC_DEV_TOKEN } from '@src/env' import { VITE_KITTYCAD_API_TOKEN } from '@src/env'
import { modifyAstWithTagsForSelection } from '@src/lang/modifyAst/tagManagement' import { modifyAstWithTagsForSelection } from '@src/lang/modifyAst/tagManagement'
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
await new Promise((resolve) => { await new Promise((resolve) => {
engineCommandManager.start({ engineCommandManager.start({
token: VITE_KC_DEV_TOKEN, token: VITE_KITTYCAD_API_TOKEN,
width: 256, width: 256,
height: 256, height: 256,
setMediaStream: () => {}, setMediaStream: () => {},

View File

@ -1,5 +1,5 @@
import type { Models } from '@kittycad/lib' import type { Models } from '@kittycad/lib'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from '@src/env' import { VITE_KC_API_WS_MODELING_URL, VITE_KITTYCAD_API_TOKEN } from '@src/env'
import { jsAppSettings } from '@src/lib/settings/settingsUtils' import { jsAppSettings } from '@src/lib/settings/settingsUtils'
import { BSON } from 'bson' import { BSON } from 'bson'
@ -400,7 +400,7 @@ class EngineConnection extends EventTarget {
this.send({ this.send({
type: 'headers', type: 'headers',
headers: { headers: {
Authorization: `Bearer ${VITE_KC_DEV_TOKEN}`, Authorization: `Bearer ${VITE_KITTYCAD_API_TOKEN}`,
}, },
}) })
} }

View File

@ -1,5 +1,5 @@
import { VITE_KC_API_BASE_URL } from '@src/env' import { VITE_KITTYCAD_API_BASE_URL } from '@src/env'
export function withAPIBaseURL(path: string): string { export function withAPIBaseURL(path: string): string {
return VITE_KC_API_BASE_URL + path return VITE_KITTYCAD_API_BASE_URL + path
} }

View File

@ -1,5 +1,5 @@
import type { Models } from '@kittycad/lib' import type { Models } from '@kittycad/lib'
import { VITE_KC_DEV_TOKEN } from '@src/env' import { VITE_KITTYCAD_API_TOKEN } from '@src/env'
import { assign, fromPromise, setup } from 'xstate' import { assign, fromPromise, setup } from 'xstate'
import { COOKIE_NAME, OAUTH2_DEVICE_CLIENT_ID } from '@src/lib/constants' import { COOKIE_NAME, OAUTH2_DEVICE_CLIENT_ID } from '@src/lib/constants'
@ -34,7 +34,7 @@ export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
*/ */
const persistedCookie = getCookie(COOKIE_NAME) const persistedCookie = getCookie(COOKIE_NAME)
const persistedLocalStorage = localStorage?.getItem(TOKEN_PERSIST_KEY) || '' const persistedLocalStorage = localStorage?.getItem(TOKEN_PERSIST_KEY) || ''
const persistedDevToken = VITE_KC_DEV_TOKEN const persistedDevToken = VITE_KITTYCAD_API_TOKEN
export const persistedToken = export const persistedToken =
persistedDevToken || persistedCookie || persistedLocalStorage persistedDevToken || persistedCookie || persistedLocalStorage
console.log('Initial persisted token') console.log('Initial persisted token')
@ -197,10 +197,10 @@ async function getAndSyncStoredToken(input: {
token?: string token?: string
}): Promise<string> { }): Promise<string> {
// dev mode // dev mode
if (VITE_KC_DEV_TOKEN) { if (VITE_KITTYCAD_API_TOKEN) {
console.log('Token used for authentication') console.log('Token used for authentication')
console.table([['api token', !!VITE_KC_DEV_TOKEN]]) console.table([['api token', !!VITE_KITTYCAD_API_TOKEN]])
return VITE_KC_DEV_TOKEN return VITE_KITTYCAD_API_TOKEN
} }
const inputToken = input.token && input.token !== '' ? input.token : '' const inputToken = input.token && input.token !== '' ? input.token : ''
@ -213,7 +213,7 @@ async function getAndSyncStoredToken(input: {
['persisted token', !!inputToken], ['persisted token', !!inputToken],
['cookie', !!cookieToken], ['cookie', !!cookieToken],
['local storage', !!localStorageToken], ['local storage', !!localStorageToken],
['api token', !!VITE_KC_DEV_TOKEN], ['api token', !!VITE_KITTYCAD_API_TOKEN],
]) ])
if (token) { if (token) {
// has just logged in, update storage // has just logged in, update storage

View File

@ -11,7 +11,7 @@ import {
engineCommandManager, engineCommandManager,
kclManager, kclManager,
} from '@src/lib/singletons' } from '@src/lib/singletons'
import { VITE_KC_DEV_TOKEN } from '@src/env' import { VITE_KITTYCAD_API_TOKEN } from '@src/env'
import { getConstraintInfoKw } from '@src/lang/std/sketch' import { getConstraintInfoKw } from '@src/lang/std/sketch'
import { getNodeFromPath } from '@src/lang/queryAst' import { getNodeFromPath } from '@src/lang/queryAst'
import type { Node } from '@rust/kcl-lib/bindings/Node' import type { Node } from '@rust/kcl-lib/bindings/Node'
@ -29,10 +29,9 @@ import { removeSingleConstraintInfo } from '@src/lang/modifyAst'
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
await new Promise((resolve) => { await new Promise((resolve) => {
engineCommandManager.start({ engineCommandManager.start({
token: VITE_KC_DEV_TOKEN, token: VITE_KITTYCAD_API_TOKEN,
width: 256, width: 256,
height: 256, height: 256,
setMediaStream: () => {}, setMediaStream: () => {},

View File

@ -1,5 +1,5 @@
import { engineCommandManager, kclManager } from '@src/lib/singletons' import { engineCommandManager, kclManager } from '@src/lib/singletons'
import { VITE_KC_DEV_TOKEN } from '@src/env' import { VITE_KITTYCAD_API_TOKEN } from '@src/env'
import { getModuleIdByFileName, isArray } from '@src/lib/utils' import { getModuleIdByFileName, isArray } from '@src/lib/utils'
import { vi, inject } from 'vitest' import { vi, inject } from 'vitest'
import { assertParse } from '@src/lang/wasm' import { assertParse } from '@src/lang/wasm'
@ -355,10 +355,9 @@ cases.push(
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
await new Promise((resolve) => { await new Promise((resolve) => {
engineCommandManager.start({ engineCommandManager.start({
token: VITE_KC_DEV_TOKEN, token: VITE_KITTYCAD_API_TOKEN,
width: 256, width: 256,
height: 256, height: 256,
setMediaStream: () => {}, setMediaStream: () => {},

View File

@ -71,7 +71,7 @@ dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] })
// default vite values based on mode // default vite values based on mode
process.env.NODE_ENV ??= viteEnv.MODE process.env.NODE_ENV ??= viteEnv.MODE
process.env.VITE_KC_API_WS_MODELING_URL ??= viteEnv.VITE_KC_API_WS_MODELING_URL process.env.VITE_KC_API_WS_MODELING_URL ??= viteEnv.VITE_KC_API_WS_MODELING_URL
process.env.VITE_KC_API_BASE_URL ??= viteEnv.VITE_KC_API_BASE_URL process.env.VITE_KITTYCAD_API_BASE_URL ??= viteEnv.VITE_KITTYCAD_API_BASE_URL
process.env.VITE_KC_SITE_BASE_URL ??= viteEnv.VITE_KC_SITE_BASE_URL process.env.VITE_KC_SITE_BASE_URL ??= viteEnv.VITE_KC_SITE_BASE_URL
process.env.VITE_KC_SITE_APP_URL ??= viteEnv.VITE_KC_SITE_APP_URL process.env.VITE_KC_SITE_APP_URL ??= viteEnv.VITE_KC_SITE_APP_URL
process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??= process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??=

View File

@ -289,11 +289,11 @@ contextBridge.exposeInMainWorld('electron', {
exposeProcessEnvs([ exposeProcessEnvs([
'NODE_ENV', 'NODE_ENV',
'VITE_KC_API_WS_MODELING_URL', 'VITE_KC_API_WS_MODELING_URL',
'VITE_KC_API_BASE_URL', 'VITE_KITTYCAD_API_BASE_URL',
'VITE_KC_SITE_BASE_URL', 'VITE_KC_SITE_BASE_URL',
'VITE_KC_SITE_APP_URL', 'VITE_KC_SITE_APP_URL',
'VITE_KC_CONNECTION_TIMEOUT_MS', 'VITE_KC_CONNECTION_TIMEOUT_MS',
'VITE_KC_DEV_TOKEN', 'VITE_KITTYCAD_API_TOKEN',
'IS_PLAYWRIGHT', 'IS_PLAYWRIGHT',