@jskit-ai/agent-docs 0.1.244 → 0.1.246

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/guide/agent/app-setup/multi-homing.md +189 -1
  2. package/package.json +1 -1
  3. package/patterns/feature-package/example/booking-engine/package.json +1 -1
  4. package/patterns/minimal-foundation/example/package.json +3 -3
  5. package/patterns/shell-foundation/example/package.json +4 -4
  6. package/patterns/shell-foundation/example/packages/main/package.json +1 -1
  7. package/reference/autogen/PATTERN_INDEX.md +37 -37
  8. package/reference/autogen/README.md +1 -0
  9. package/reference/autogen/packages/architecture-inspector.md +85 -0
  10. package/reference/autogen/packages/assistant-core.md +1 -0
  11. package/reference/autogen/packages/shell-web.md +1 -62
  12. package/reference/autogen/packages/workspaces-core.md +4 -3
  13. package/reference/autogen/packages/workspaces-web.md +25 -2
  14. package/skills/jskit/references/multi-homing.md +189 -1
  15. package/skills/jskit/references/pattern-index.md +37 -37
  16. package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +3 -3
  17. package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +4 -4
  18. package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/package.json +1 -1
  19. package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +2 -2
  20. package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
  21. package/skills/jskit/references/patterns/auth/supabase-auth/example/package.json +1 -1
  22. package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +4 -4
  23. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/package.json +2 -2
  24. package/skills/jskit/references/patterns/database/mysql-application/example/package.json +1 -1
  25. package/skills/jskit/references/patterns/database/postgres-application/example/package.json +1 -1
  26. package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
  27. package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +1 -1
  28. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +2 -2
  29. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +3 -3
  30. package/skills/jskit/references/patterns/workspaces/workspace-server/PATTERN.md +6 -0
  31. package/skills/jskit/references/patterns/workspaces/workspace-surfaces/PATTERN.md +10 -2
  32. package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/src/pages/home/workspaces/index.vue +7 -0
@@ -0,0 +1,85 @@
1
+ # packages/architecture-inspector
2
+
3
+ Generated by `npm run agent-docs:build`.
4
+ Do not edit manually.
5
+
6
+ Generated inventory for `packages/architecture-inspector`.
7
+ Use this on demand; do not load the full index at startup.
8
+
9
+ ## Scope
10
+ - Source: `packages/architecture-inspector/**/*{.js,.mjs,.cjs,.vue}`
11
+ - Excludes: `.build/`, `.git/`, `.jscpd/`, `.jskit/`, `coverage/`, `dist/`, `docs/`, `node_modules/`, `test/`, `tests/`, `__tests__/`, `.vitepress/cache/`, `.vitepress/dist/`, `skills/jskit/references/patterns/`, `*.test.*`, `*.spec.*`, `*.vitest.*`
12
+
13
+ ## Sections
14
+
15
+ ### src
16
+
17
+ ### `src/cli.js`
18
+ Exports
19
+ - None
20
+
21
+ ### `src/inspectVueArchitecture.js`
22
+ Exports
23
+ - `inspectVueArchitecture({ projectRoot, input, aliases = {}, httpScope = "", apiBasePath = "" })`
24
+ Local functions
25
+ - `hashBytes(source)`
26
+ - `isCanonicalPath(value)`
27
+ - `propertyKey(entry)`
28
+ - `isStaticObjectProperty(entry)`
29
+ - `objectProperty(node, key)`
30
+ - `hasOnlyStaticProperties(node)`
31
+ - `isFunctionNode(node)`
32
+ - `directoryPrefixes(filePath)`
33
+ - `collectNodes(root)`
34
+ - `staticValue(node, bindings, seen = new Set())`
35
+ - `staticObjectValue(node, bindings, seen)`
36
+ - `bindingNode(node, bindings, seen = new Set())`
37
+ - `isShadowed({ ancestors }, name)`
38
+ - `isLocalCall(record, names)`
39
+ - `importedNames(module, source, imported)`
40
+ - `findRecord(module, node)`
41
+ - `kebabCase(name)`
42
+ - `templateTagImport(module, tag)`
43
+ - `sourcePoint(module, index)`
44
+ - `sourceEvidence(module, node, offset = 0)`
45
+ - `pageTitle(file)`
46
+ - `validateInput({ projectRoot, input, aliases })`
47
+ - `createOutput({ input, httpScope })`
48
+ - `warn(inspection, code, message, source)`
49
+ - `loadModule(inspection, filePath)`
50
+ - `scriptUnits(inspection, module)`
51
+ - `indexScriptUnit(inspection, module, unit)`
52
+ - `indexDeclaration(module, node)`
53
+ - `resolveImport(inspection, from, specifier)`
54
+ - `resolvePackageEntryFile(inspection, from, specifier)`
55
+ - `resolvePackageImport(inspection, from, specifier)`
56
+ - `resolveCrudResource(inspection, module, node, seen = new Set())`
57
+ - `resolveExportedCrudResource(inspection, module, name, seen)`
58
+ - `crudOperationKey(inspection, surface, namespace, operation, apiSuffix)`
59
+ - `recordPage(inspection, { file, discovery, identity, registrationModule, registrationNode, parentId = null })`
60
+ - `isSupportedFilesystemOptions(options)`
61
+ - `discoverFilesystemPages(inspection, discovery, module, registrationPath)`
62
+ - `byCodeUnits(left, right)`
63
+ - `recordFilesystemRoutes(inspection, { tree, discovery, module, node })`
64
+ - `discoverRegisteredPages(inspection, discovery, module, registrationPath)`
65
+ - `routeComponentSpecifier(module, component)`
66
+ - `recordRegisteredRoutes(inspection, { routes, discovery, module, registrationPath })`
67
+ - `discoverPages(inspection, discoveries)`
68
+ - `readTemplate(module)`
69
+ - `discoverComponents(inspection)`
70
+ - `recordComponentUse(inspection, { module, file, parentId, tag, pending })`
71
+ - `recordPackageComponent(inspection, { module, file, tag, imported })`
72
+ - `crudScreenKind(component)`
73
+ - `crudScreenCall(module, tag, list)`
74
+ - `hasDynamicScreenTransport(module, config)`
75
+ - `hasCustomScreenClient(options)`
76
+ - `recordCrudScreenUse(inspection, { module, file, parentId, tag })`
77
+ - `isGlobalFetchCall(module, { node, ancestors })`
78
+ - `recordFetchUses(inspection, { module, parentId, eventHandlers })`
79
+ - `discoverEntryOperations(inspection)`
80
+ - `crudControllerOperations(module, config, resource)`
81
+ - `recordCrudController(inspection, { module, file, record })`
82
+ - `recordFeatureRoute(inspection, { module, file, record, featureFactories })`
83
+ - `importSpecifierName(specifier)`
84
+ - `isTypeOnlyImport(node)`
85
+ - `collectImports(inspection)`
@@ -326,6 +326,7 @@ Local functions
326
326
  - `claudeUserText(content)`
327
327
  - `retainedClaudeFrame(frame)`
328
328
  - `claudeHistoryFrames(file)`
329
+ - `completeHistoryOffset(handle, start, size)`
329
330
  - `readClaudeHistoryFrames(file)`
330
331
 
331
332
  ### `src/server/conversation/claudeProcess.js`
@@ -20,68 +20,7 @@ Exports
20
20
 
21
21
  ### `src/architecture/inspectVueArchitecture.js`
22
22
  Exports
23
- - `inspectVueArchitecture({ projectRoot, input, aliases = {}, httpScope = "", apiBasePath = "" })`
24
- Local functions
25
- - `hashBytes(source)`
26
- - `isCanonicalPath(value)`
27
- - `propertyKey(entry)`
28
- - `isStaticObjectProperty(entry)`
29
- - `objectProperty(node, key)`
30
- - `hasOnlyStaticProperties(node)`
31
- - `isFunctionNode(node)`
32
- - `directoryPrefixes(filePath)`
33
- - `collectNodes(root)`
34
- - `staticValue(node, bindings, seen = new Set())`
35
- - `staticObjectValue(node, bindings, seen)`
36
- - `bindingNode(node, bindings, seen = new Set())`
37
- - `isShadowed({ ancestors }, name)`
38
- - `isLocalCall(record, names)`
39
- - `importedNames(module, source, imported)`
40
- - `findRecord(module, node)`
41
- - `kebabCase(name)`
42
- - `templateTagImport(module, tag)`
43
- - `sourcePoint(module, index)`
44
- - `sourceEvidence(module, node, offset = 0)`
45
- - `pageTitle(file)`
46
- - `validateInput({ projectRoot, input, aliases })`
47
- - `createOutput({ input, httpScope })`
48
- - `warn(inspection, code, message, source)`
49
- - `loadModule(inspection, filePath)`
50
- - `scriptUnits(inspection, module)`
51
- - `indexScriptUnit(inspection, module, unit)`
52
- - `indexDeclaration(module, node)`
53
- - `resolveImport(inspection, from, specifier)`
54
- - `resolvePackageEntryFile(inspection, from, specifier)`
55
- - `resolvePackageImport(inspection, from, specifier)`
56
- - `resolveCrudResource(inspection, module, node, seen = new Set())`
57
- - `resolveExportedCrudResource(inspection, module, name, seen)`
58
- - `crudOperationKey(inspection, surface, namespace, operation, apiSuffix)`
59
- - `recordPage(inspection, { file, discovery, identity, registrationModule, registrationNode, parentId = null })`
60
- - `isSupportedFilesystemOptions(options)`
61
- - `discoverFilesystemPages(inspection, discovery, module, registrationPath)`
62
- - `recordFilesystemRoutes(inspection, { tree, discovery, module, node })`
63
- - `discoverRegisteredPages(inspection, discovery, module, registrationPath)`
64
- - `routeComponentSpecifier(module, component)`
65
- - `recordRegisteredRoutes(inspection, { routes, discovery, module, registrationPath })`
66
- - `discoverPages(inspection, discoveries)`
67
- - `readTemplate(module)`
68
- - `discoverComponents(inspection)`
69
- - `recordComponentUse(inspection, { module, file, parentId, tag, pending })`
70
- - `recordPackageComponent(inspection, { module, file, tag, imported })`
71
- - `crudScreenKind(component)`
72
- - `crudScreenCall(module, tag, list)`
73
- - `hasDynamicScreenTransport(module, config)`
74
- - `hasCustomScreenClient(options)`
75
- - `recordCrudScreenUse(inspection, { module, file, parentId, tag })`
76
- - `isGlobalFetchCall(module, { node, ancestors })`
77
- - `recordFetchUses(inspection, { module, parentId, eventHandlers })`
78
- - `discoverEntryOperations(inspection)`
79
- - `crudControllerOperations(module, config, resource)`
80
- - `recordCrudController(inspection, { module, file, record })`
81
- - `recordFeatureRoute(inspection, { module, file, record, featureFactories })`
82
- - `importSpecifierName(specifier)`
83
- - `isTypeOnlyImport(node)`
84
- - `collectImports(inspection)`
23
+ - `inspectVueArchitecture`
85
24
 
86
25
  ### `src/client/asyncModuleRecovery/index.js`
87
26
  Exports
@@ -222,10 +222,9 @@ Exports
222
222
 
223
223
  ### `src/server/workspaceSettings/workspaceSettingsRepository.js`
224
224
  Exports
225
- - `createRepository({ api, defaultInvitesEnabled = true } = {})`
225
+ - `createRepository({ api, defaultInvitesEnabled = true, defaultColorSettings = {} } = {})`
226
226
  Local functions
227
- - `pickPatchFields(source = {})`
228
- - `createDefaultWorkspaceSettingsCreatePayload(workspaceId, defaultInvitesEnabled)`
227
+ - `createDefaultWorkspaceSettingsCreatePayload(workspaceId, defaultInvitesEnabled, defaultColorSettings = {})`
229
228
 
230
229
  ### `src/server/workspaceSettings/workspaceSettingsService.js`
231
230
  Exports
@@ -239,6 +238,7 @@ Exports
239
238
  Local functions
240
239
  - `requirePositiveInteger(value, label)`
241
240
  - `requireBoolean(value, label)`
241
+ - `requireWorkspaceColorDefaults(defaults)`
242
242
  - `resolveInviteEmailTemplate(config)`
243
243
 
244
244
  ### `src/server/WorkspacesIntegrationsProvider.js`
@@ -339,6 +339,7 @@ Exports
339
339
  - `normalizeWorkspaceThemeMode(value = "")`
340
340
  - `resolveWorkspaceThemeDefaultPalette(mode = WORKSPACE_THEME_MODE_LIGHT)`
341
341
  - `resolveWorkspaceThemePalettes(input = {})`
342
+ - `WORKSPACE_SETTINGS_COLOR_FIELDS`
342
343
  - `WORKSPACE_THEME_MODE_DARK`
343
344
  - `WORKSPACE_THEME_MODE_LIGHT`
344
345
  - `resolveWorkspaceThemePalette(input = {}, { mode = WORKSPACE_THEME_MODE_LIGHT } = {})`
@@ -97,8 +97,10 @@ Local functions
97
97
  - `normalizePendingInvite(entry)`
98
98
  - `reportFeedback({ message, severity = "error", channel = "", dedupeKey = "" } = {})`
99
99
  - `workspaceInitials(workspace)`
100
- - `workspaceAvatarStyle(workspace)`
101
100
  - `workspaceHomePath(workspaceSlug)`
101
+ - `resolveLandingSlug()`
102
+ - `workspaceOpenLabel(workspace)`
103
+ - `isRespondingToInvite(invite, decision)`
102
104
  - `openWorkspace(workspaceSlug)`
103
105
  - `respondToInvite(invite, decision)`
104
106
  - `acceptInvite(invite)`
@@ -112,7 +114,6 @@ Exports
112
114
  Local functions
113
115
  - `resolveBrowserPath()`
114
116
  - `navigateToWorkspace(slug)`
115
- - `workspaceAvatarStyle(workspace)`
116
117
 
117
118
  ### `src/client/components/WorkspaceSettingsMenuItem.vue`
118
119
  Exports
@@ -137,6 +138,7 @@ Exports
137
138
  - `WorkspacesWebClientProvider`
138
139
  - `WorkspaceMembersClientElement`
139
140
  - `WorkspaceInviteLanding`
141
+ - `WorkspacesClientElement`
140
142
 
141
143
  ### `src/client/lib/bootstrap.js`
142
144
  Exports
@@ -147,6 +149,15 @@ Exports
147
149
  Local functions
148
150
  - `normalizeWorkspaceSlug(value = "")`
149
151
 
152
+ ### `src/client/lib/lastWorkspace.js`
153
+ Exports
154
+ - `readLastWorkspaceSlug(userId, { storage } = {})`
155
+ - `resolveWorkspaceLandingSlug({ workspaces = [], pendingInvites = [], lastWorkspaceSlug = "" } = {})`
156
+ - `writeLastWorkspaceSlug(userId, workspaceSlug, { storage } = {})`
157
+ Local functions
158
+ - `resolveStorage(storage)`
159
+ - `lastWorkspaceStorageKey(userId)`
160
+
150
161
  ### `src/client/lib/profileSurfaceMenuLinks.js`
151
162
  Exports
152
163
  - `resolveProfileSurfaceMenuLinks({ context, surface } = {})`
@@ -310,6 +321,14 @@ Exports
310
321
  Exports
311
322
  - None
312
323
 
324
+ ### `fixtures/workspaces-landing/main.js`
325
+ Exports
326
+ - None
327
+
328
+ ### `fixtures/workspaces-landing/vite.config.mjs`
329
+ Exports
330
+ - None
331
+
313
332
  ### patterns
314
333
 
315
334
  ### `patterns/workspace-surfaces/example/packages/main/src/client/components/AccountPendingInvitesCue.vue`
@@ -344,6 +363,10 @@ Exports
344
363
  Exports
345
364
  - None
346
365
 
366
+ ### `patterns/workspace-surfaces/example/src/pages/home/workspaces/index.vue`
367
+ Exports
368
+ - None
369
+
347
370
  ### `patterns/workspace-surfaces/example/src/pages/invite/[token].vue`
348
371
  Exports
349
372
  - None
@@ -26,6 +26,70 @@ Choose whether the product is:
26
26
  Do not add workspace machinery to a product that only needs an owner id on one
27
27
  domain record.
28
28
 
29
+ Set the model with `tenancyMode` and `tenancyPolicy` in `config/public.js`:
30
+
31
+ | `tenancyMode` | Workspaces | First workspace | Creating more |
32
+ | --- | --- | --- | --- |
33
+ | `none` | None | None | No |
34
+ | `personal` | One personal workspace per user | Always provisioned | No |
35
+ | `workspaces` | Named workspaces with membership, invitations and switching | Only with `tenancyPolicy.workspace.autoProvision: true` | With `tenancyPolicy.workspace.allowSelfCreate: true` |
36
+
37
+ In `workspaces` mode the product chooses one of two styles:
38
+
39
+ - **Team-first** (the default, `autoProvision` unset or `false`): a new account
40
+ has no workspace until it creates one or accepts an invitation. Mount the
41
+ **Your workspaces** page (see [Browser composition](#browser-composition)) so
42
+ these people can do either.
43
+ - **Personal workspace plus shared workspaces** (`autoProvision: true`): every
44
+ user, including people who signed up through an invitation, owns one personal
45
+ workspace and can also join or create others.
46
+
47
+ ```js
48
+ // config/public.js
49
+ config.tenancyMode = "workspaces";
50
+ config.tenancyPolicy = {
51
+ workspace: {
52
+ allowSelfCreate: true,
53
+ autoProvision: true
54
+ }
55
+ };
56
+ ```
57
+
58
+ Provisioning runs in the users profile-sync lifecycle after registration and
59
+ sign-in. The first run creates the personal workspace, its owner membership and
60
+ its settings; later runs reuse it. The default name is the user's display name,
61
+ or email, followed by "'s Workspace". Set `personalWorkspaceName` in
62
+ `config/server.js` to choose another; it receives the synchronized profile
63
+ (`id`, `displayName`, `email`, `username`), and an empty result keeps the default:
64
+
65
+ ```js
66
+ // config/server.js
67
+ config.personalWorkspaceName = (user) => `${user.displayName || user.email}'s home`;
68
+ ```
69
+
70
+ ### Development sign-in
71
+
72
+ Development sign-in (`POST /api/dev-auth/login-as`, used by the Playwright
73
+ `loginAsExistingUser()` helper and managed previews) signs in as an existing
74
+ user without changing data. It does not run the profile-sync lifecycle, so
75
+ workspace provisioning and other lifecycle contributors do not run. Create the
76
+ workspaces that seeded users need in seed data, or sign in normally once.
77
+
78
+ ## Data model
79
+
80
+ The package migrations create and own these tables:
81
+
82
+ | Table | Columns that applications use |
83
+ | --- | --- |
84
+ | `workspaces` | `id`, `slug` (unique), `name`, `owner_user_id`, `is_personal`, `avatar_url`, `deleted_at` |
85
+ | `workspace_memberships` | `workspace_id`, `user_id`, `role_sid`, `status`; one row per workspace and user |
86
+ | `workspace_settings` | One row per workspace keyed by `workspace_id`: the eight palette colours and `invites_enabled` |
87
+ | `workspace_invites` | `workspace_id`, `email`, `role_sid`, `status`, `token_hash`, `invited_by_user_id`, `expires_at`, `accepted_at`, `revoked_at` |
88
+
89
+ Application records that belong to a workspace use a non-null `workspace_id`
90
+ foreign key to `workspaces.id`. Read and change these tables through the
91
+ `workspaces.core` repositories and services, not directly.
92
+
29
93
  ## Server composition
30
94
 
31
95
  Use `workspaces/workspace-server` for role vocabulary, creation policy,
@@ -35,6 +99,103 @@ Workspace scope must reach repository queries. Parsing a workspace slug or
35
99
  hiding an admin link is not authorization. Membership and role checks happen
36
100
  before an operation discloses records or record existence.
37
101
 
102
+ ### Workspace-scoped features and routes
103
+
104
+ For a standard CRUD resource owned by a workspace, give its table the
105
+ `workspace_id` column, set the resource's `autofilter: "workspace"`, and bind
106
+ `defineCrudJsonApiFeature()` with `ownershipFilter: "workspace"` on a workspace
107
+ surface. JSKIT then filters `workspaceId` by the workspace in the route.
108
+ `workspace_user` also filters by the signed-in user. See the CRUD operations
109
+ guide.
110
+
111
+ A hand-written route registers under `/api/w/:workspaceSlug/...` with
112
+ `contextPolicy: "required"`:
113
+
114
+ ```js
115
+ router.register(
116
+ "GET",
117
+ "/api/w/:workspaceSlug/projects",
118
+ {
119
+ auth: "required",
120
+ surface: "app",
121
+ contextPolicy: "required",
122
+ permission: "projects.view" // optional
123
+ },
124
+ async function (request, reply) {
125
+ const projects = await projectsService.listForWorkspace(request.workspace.id);
126
+ // ...
127
+ }
128
+ );
129
+ ```
130
+
131
+ Before the handler runs, the auth plugin resolves the slug for the signed-in
132
+ user and sets `request.workspace`, `request.membership` (including `roleSid`
133
+ and `status`) and `request.permissions`. An unknown slug returns 404 and a
134
+ non-member receives 403, unless the route's `surface` uses an access policy with
135
+ `requireWorkspaceMembership: false`. A route with `permission` returns 403 when
136
+ the membership lacks it. Scope queries with `request.workspace.id`; never take a
137
+ workspace id from request input.
138
+
139
+ ### Workspace services from another feature
140
+
141
+ Declare `workspaces: "workspaces.core"` in the feature's `requires`. The
142
+ capability provides:
143
+
144
+ | Member | Use |
145
+ | --- | --- |
146
+ | `workspaces.services.directory` | Workspace lists, context by slug, creation and provisioning |
147
+ | `workspaces.services.members` | Members, roles and invitations |
148
+ | `workspaces.services.pendingInvitations` | Invitation resolution, redemption and acceptance participants |
149
+ | `workspaces.services.settings` | Workspace settings |
150
+ | `workspaces.repositories.workspaces`, `.workspaceMemberships`, `.workspaceSettings`, `.workspaceInvites` | Repository access inside the application's own transactions |
151
+ | `workspaces.tenancyProfile` | The resolved tenancy mode and workspace policy |
152
+
153
+ ### Invitation email
154
+
155
+ To send invitation email, set a mailer in `config/server.js`. Without one,
156
+ invitations are still created and delivery reports `mailer_unconfigured`.
157
+ `workspaceInviteMailer` is an object whose `sendWorkspaceInvite(message,
158
+ options)` receives `{ email, inviteUrl, workspace, inviter, roleSid, expiresAt,
159
+ message }`, where the inner `message` is the rendered `{ subject, text, html }`.
160
+ `workspaceInviteEmailTemplate` is an optional function that renders that inner
161
+ message; it receives `{ email, inviteUrl, workspace, inviter, roleSid,
162
+ roleLabel, expiresAt }` (`roleLabel` only when configured). Invitation links use `/invite/:token` unless
163
+ `workspaceInvitations.invitePath` names another path. The
164
+ `workspaces/workspace-server` pattern includes an example renderer.
165
+
166
+ ### Workspace colours
167
+
168
+ Each workspace stores a light and a dark palette in `workspace_settings`. On
169
+ workspace routes, `workspaces-web` switches Vuetify to the `workspace-light` or
170
+ `workspace-dark` theme, built from the application's own theme with `primary`,
171
+ `secondary`, `surface` and `surface-variant` replaced by the workspace's
172
+ colours. Other routes use the application's themes.
173
+
174
+ New workspaces start from JSKIT's blue palette unless `config/server.js` sets
175
+ the application's own. Missing fields keep JSKIT's values; an invalid colour
176
+ stops the server at startup:
177
+
178
+ ```js
179
+ // config/server.js
180
+ config.workspaceSettings = {
181
+ defaults: {
182
+ invitesEnabled: true,
183
+ lightPrimaryColor: "#1C1C1C",
184
+ lightSecondaryColor: "#5C5C5C",
185
+ lightSurfaceColor: "#FFFFFF",
186
+ lightSurfaceVariantColor: "#424242",
187
+ darkPrimaryColor: "#F5F5F5",
188
+ darkSecondaryColor: "#BDBDBD",
189
+ darkSurfaceColor: "#121212",
190
+ darkSurfaceVariantColor: "#C8C8C8"
191
+ }
192
+ };
193
+ ```
194
+
195
+ Defaults apply when a workspace is created. Existing workspaces keep their
196
+ stored colours; change them with `PATCH /api/w/:workspaceSlug/settings` or
197
+ `workspaces.services.settings.updateWorkspaceSettings(workspace, patch)`.
198
+
38
199
  `workspace_authenticated` and similar policies must implement their documented
39
200
  membership semantics exactly; do not synthesize memberships to compensate for
40
201
  a policy/runtime mismatch.
@@ -265,7 +426,34 @@ placements.
265
426
 
266
427
  The route slug, selected workspace, query keys, and server scope stay aligned.
267
428
  Invalid workspace URLs show a deliberate not-found/forbidden state instead of
268
- silently selecting another workspace.
429
+ silently selecting another workspace. The browser waits for the server's answer
430
+ about an unknown slug before deciding, so the surface can show that state.
431
+
432
+ ### Surfaces and where `/` lands
433
+
434
+ Workspace surfaces put the slug in their pages root, such as
435
+ `pagesRoot: "w/[workspaceSlug]"` with `requiresWorkspace: true`, and
436
+ `w/[workspaceSlug]/admin` for administration. Keep the default surface
437
+ (`surfaceDefaultId`, usually `home`) free of a workspace requirement: `/`,
438
+ invitation links and other unscoped pages belong to it. With
439
+ `webRootAllowed: "no"`, `/` opens that surface's root.
440
+
441
+ `workspaces-web` remembers the last workspace each person opened in this
442
+ browser. The **Your workspaces** page, `WorkspacesClientElement` from
443
+ `@jskit-ai/workspaces-web/client/components/WorkspacesClientElement`, uses it:
444
+
445
+ 1. With pending invitations, it shows them so the person can join or refuse.
446
+ 2. Otherwise it opens the last workspace used in this browser, if the person is
447
+ still a member, or their only workspace.
448
+ 3. Otherwise it lists their workspaces and, when they have none and
449
+ `allowSelfCreate` is on, offers to create the first one.
450
+
451
+ The page sends signed-out visitors to sign in. The
452
+ `workspaces/workspace-surfaces` pattern mounts it at `/home/workspaces`. The
453
+ product decides where people land: keep a public landing page at `/home` that
454
+ links signed-in people there, or mount the page at `/home` itself so everyone
455
+ goes straight to sign-in or into their workspace. Applications may replace it
456
+ with their own page.
269
457
 
270
458
  Loading uses skeletons; mutation errors use toasts. Warm-cache and browser
271
459
  back/forward navigation must restore the correct workspace context.