@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.
- package/guide/agent/app-setup/multi-homing.md +189 -1
- package/package.json +1 -1
- package/patterns/feature-package/example/booking-engine/package.json +1 -1
- package/patterns/minimal-foundation/example/package.json +3 -3
- package/patterns/shell-foundation/example/package.json +4 -4
- package/patterns/shell-foundation/example/packages/main/package.json +1 -1
- package/reference/autogen/PATTERN_INDEX.md +37 -37
- package/reference/autogen/README.md +1 -0
- package/reference/autogen/packages/architecture-inspector.md +85 -0
- package/reference/autogen/packages/assistant-core.md +1 -0
- package/reference/autogen/packages/shell-web.md +1 -62
- package/reference/autogen/packages/workspaces-core.md +4 -3
- package/reference/autogen/packages/workspaces-web.md +25 -2
- package/skills/jskit/references/multi-homing.md +189 -1
- package/skills/jskit/references/pattern-index.md +37 -37
- package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +3 -3
- package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +4 -4
- package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/package.json +1 -1
- package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +2 -2
- package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
- package/skills/jskit/references/patterns/auth/supabase-auth/example/package.json +1 -1
- package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +4 -4
- package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/package.json +2 -2
- package/skills/jskit/references/patterns/database/mysql-application/example/package.json +1 -1
- package/skills/jskit/references/patterns/database/postgres-application/example/package.json +1 -1
- package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
- package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +1 -1
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +2 -2
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +3 -3
- package/skills/jskit/references/patterns/workspaces/workspace-server/PATTERN.md +6 -0
- package/skills/jskit/references/patterns/workspaces/workspace-surfaces/PATTERN.md +10 -2
- 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
|
|
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
|
-
- `
|
|
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.
|