@jskit-ai/workspaces-core 0.1.138-oldline.1 → 0.1.138

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 (50) hide show
  1. package/package.json +154 -10
  2. package/patterns/workspace-server/PATTERN.md +60 -0
  3. package/src/server/WorkspacesFeature.js +226 -0
  4. package/src/server/WorkspacesIntegrationsProvider.js +74 -0
  5. package/src/server/common/contributors/workspaceActionContextContributor.js +5 -9
  6. package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +16 -2
  7. package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +22 -4
  8. package/src/server/common/support/realtimeServiceEvents.js +102 -61
  9. package/src/server/support/workspaceActionSurfaces.js +1 -13
  10. package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +5 -11
  11. package/src/server/workspaceDirectory/workspaceDirectoryActions.js +24 -14
  12. package/src/server/workspaceMembers/bootWorkspaceMembers.js +5 -12
  13. package/src/server/workspaceMembers/workspaceMembersActions.js +63 -23
  14. package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +4 -6
  15. package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +31 -12
  16. package/src/server/workspaceSettings/bootWorkspaceSettings.js +5 -8
  17. package/src/server/workspaceSettings/workspaceSettingsActions.js +26 -8
  18. package/src/server/workspaceSettings/workspaceSettingsRepository.js +4 -4
  19. package/test/exportsContract.test.js +2 -1
  20. package/test/featureRuntime.test.js +116 -0
  21. package/test/packageMetadata.test.js +59 -0
  22. package/test/usersRouteResources.test.js +1 -1
  23. package/test/workspaceActionContextContributor.test.js +7 -12
  24. package/test/workspaceAuthPolicyContextResolver.test.js +30 -0
  25. package/test/workspaceRouteVisibilityResolver.test.js +31 -0
  26. package/test/workspaceService.test.js +2 -2
  27. package/test/workspaceSettingsActions.test.js +103 -17
  28. package/test/workspacesRouteRequestInputValidator.test.js +14 -65
  29. package/package.descriptor.mjs +0 -352
  30. package/src/server/WorkspacesCoreServiceProvider.js +0 -64
  31. package/src/server/registerWorkspaceBootstrap.js +0 -27
  32. package/src/server/registerWorkspaceCore.js +0 -103
  33. package/src/server/registerWorkspaceRepositories.js +0 -30
  34. package/src/server/support/workspaceServerScopeSupport.js +0 -18
  35. package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +0 -19
  36. package/src/server/workspaceMembers/registerWorkspaceMembers.js +0 -137
  37. package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +0 -130
  38. package/src/server/workspaceSettings/registerWorkspaceSettings.js +0 -66
  39. package/test/packageDescriptor.test.js +0 -91
  40. package/test/registerServiceRealtimeEvents.test.js +0 -116
  41. package/test/registerWorkspaceBootstrap.test.js +0 -86
  42. package/test/registerWorkspaceDirectory.test.js +0 -31
  43. package/test/registerWorkspaceMembers.test.js +0 -140
  44. package/test/registerWorkspaceSettings.test.js +0 -40
  45. package/test/workspaceServerScopeSupport.test.js +0 -131
  46. /package/{templates/migrations → migrations}/workspaces_core_initial.cjs +0 -0
  47. /package/{templates/migrations → migrations}/workspaces_core_workspace_settings_single_name_source.cjs +0 -0
  48. /package/{templates/migrations → migrations}/workspaces_core_workspaces_drop_color.cjs +0 -0
  49. /package/{templates → patterns/workspace-server/example}/config/roles.js +0 -0
  50. /package/{templates → patterns/workspace-server/example}/packages/main/src/server/email/workspaceInviteEmail.js +0 -0
@@ -1,26 +1,26 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import { workspaceDirectoryActions } from "../src/server/workspaceDirectory/workspaceDirectoryActions.js";
4
- import { workspacePendingInvitationsActions } from "../src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js";
5
- import { workspaceMembersActions } from "../src/server/workspaceMembers/workspaceMembersActions.js";
6
- import { workspaceSettingsActions } from "../src/server/workspaceSettings/workspaceSettingsActions.js";
3
+ import { workspaceDirectoryActionSpecifications } from "../src/server/workspaceDirectory/workspaceDirectoryActions.js";
4
+ import { workspacePendingInvitationsActionSpecifications } from "../src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js";
5
+ import { workspaceMembersActionSpecifications } from "../src/server/workspaceMembers/workspaceMembersActions.js";
6
+ import { workspaceSettingsActionSpecifications } from "../src/server/workspaceSettings/workspaceSettingsActions.js";
7
7
 
8
- test("workspace settings actions live in their own action array", () => {
8
+ test("workspace settings action specifications stay explicit", () => {
9
9
  assert.deepEqual(
10
- workspaceSettingsActions.map((action) => action.id),
10
+ workspaceSettingsActionSpecifications.map((action) => action.id),
11
11
  ["workspace.settings.read", "workspace.settings.update"]
12
12
  );
13
- assert.equal(workspaceSettingsActions[0].surfacesFrom, "workspace");
14
- assert.equal(workspaceSettingsActions[1].surfacesFrom, "workspace");
15
- assert.deepEqual(workspaceSettingsActions[1].channels, ["api", "assistant_tool", "automation", "internal"]);
16
- assert.equal(workspaceSettingsActions[1].extensions?.assistant?.description, "Update workspace settings.");
13
+ assert.deepEqual(workspaceSettingsActionSpecifications[0].surfaces, ["*"]);
14
+ assert.deepEqual(workspaceSettingsActionSpecifications[1].surfaces, ["*"]);
15
+ assert.deepEqual(workspaceSettingsActionSpecifications[1].channels, ["api", "assistant_tool", "automation", "internal"]);
16
+ assert.equal(workspaceSettingsActionSpecifications[1].extensions?.assistant?.description, "Update workspace settings.");
17
17
  });
18
18
 
19
- test("workspace actions array no longer owns workspace settings actions", () => {
19
+ test("workspace actions array excludes workspace settings actions", () => {
20
20
  const otherWorkspaceActionIds = [
21
- ...workspaceDirectoryActions,
22
- ...workspacePendingInvitationsActions,
23
- ...workspaceMembersActions
21
+ ...workspaceDirectoryActionSpecifications,
22
+ ...workspacePendingInvitationsActionSpecifications,
23
+ ...workspaceMembersActionSpecifications
24
24
  ].map((action) => action.id);
25
25
 
26
26
  assert.equal(
@@ -34,17 +34,103 @@ test("workspace actions array no longer owns workspace settings actions", () =>
34
34
  });
35
35
 
36
36
  test("workspace directory actions stay thin and defer output validation to routes", () => {
37
- const listAction = workspaceDirectoryActions.find((action) => action.id === "workspace.workspaces.list");
37
+ const listAction = workspaceDirectoryActionSpecifications.find((action) => action.id === "workspace.workspaces.list");
38
38
  assert.ok(listAction);
39
39
  assert.equal(listAction.output, null);
40
40
  });
41
41
 
42
42
  test("workspace directory read/update actions stay thin and defer output validation to routes", () => {
43
- const readAction = workspaceDirectoryActions.find((action) => action.id === "workspace.workspaces.read");
44
- const updateAction = workspaceDirectoryActions.find((action) => action.id === "workspace.workspaces.update");
43
+ const readAction = workspaceDirectoryActionSpecifications.find((action) => action.id === "workspace.workspaces.read");
44
+ const updateAction = workspaceDirectoryActionSpecifications.find((action) => action.id === "workspace.workspaces.update");
45
45
 
46
46
  assert.ok(readAction);
47
47
  assert.ok(updateAction);
48
48
  assert.equal(readAction.output, null);
49
49
  assert.equal(updateAction.output, null);
50
50
  });
51
+
52
+ test("workspace mutation actions declare their domain and bootstrap changes explicitly", async () => {
53
+ const context = {
54
+ actor: { id: 7 },
55
+ visibilityContext: { visibility: "workspace", scopeKind: "workspace", scopeOwnerId: 11 },
56
+ requestMeta: {
57
+ resolvedWorkspaceContext: {
58
+ workspace: { id: 11, slug: "acme" }
59
+ }
60
+ }
61
+ };
62
+ const settingsAction = workspaceSettingsActionSpecifications.find(
63
+ (action) => action.id === "workspace.settings.update"
64
+ );
65
+ const events = await Promise.all(settingsAction.events.map((builder) => builder({
66
+ input: { workspaceSlug: "acme", invitesEnabled: true },
67
+ result: { value: { invitesEnabled: true } },
68
+ context
69
+ })));
70
+
71
+ assert.deepEqual(events.map((event) => event.realtime.event), [
72
+ "workspace.settings.changed",
73
+ "users.bootstrap.changed"
74
+ ]);
75
+ assert.deepEqual(events[0].scope, { kind: "workspace", id: "11" });
76
+ assert.equal(events[0].entityId, "11");
77
+ assert.deepEqual(events[0].realtime.payload, { workspaceSlug: "acme" });
78
+ });
79
+
80
+ test("workspace invite actions carry recipient lookup as delivery policy, not service metadata", async () => {
81
+ const action = workspaceMembersActionSpecifications.find((entry) => entry.id === "workspace.invite.create");
82
+ const context = {
83
+ actor: { id: 7 },
84
+ visibilityContext: { visibility: "workspace", scopeKind: "workspace", scopeOwnerId: 11 },
85
+ requestMeta: { resolvedWorkspaceContext: { workspace: { id: 11, slug: "acme" } } }
86
+ };
87
+ const events = await Promise.all(action.events.map((builder) => builder({
88
+ input: { workspaceSlug: "acme", email: "reader@example.test", roleSid: "member" },
89
+ result: { value: { createdInviteId: 91 } },
90
+ context
91
+ })));
92
+ assert.equal(events[0].operation, "created");
93
+ assert.equal(events[1].entityId, "91");
94
+ assert.equal(typeof events[1].realtime.audience.userQuery, "function");
95
+ const rows = await events[1].realtime.audience.userQuery({
96
+ event: events[1],
97
+ knex() {
98
+ return {
99
+ join() { return this; },
100
+ where(field, value) {
101
+ assert.equal(field, "wi.id");
102
+ assert.equal(value, "91");
103
+ return this;
104
+ },
105
+ async first() { return { user_id: 55 }; }
106
+ };
107
+ }
108
+ });
109
+ assert.deepEqual(rows, [{ userId: "55" }]);
110
+ });
111
+
112
+ test("accepting an invitation declares directory/member refresh while refusal does not", async () => {
113
+ const action = workspacePendingInvitationsActionSpecifications.find(
114
+ (entry) => entry.id === "workspace.invite.redeem"
115
+ );
116
+ async function emittedNames(decision) {
117
+ const built = await Promise.all(action.events.map((builder) => builder({
118
+ input: { decision, token: "opaque" },
119
+ result: { value: { workspaceId: 11 } },
120
+ context: { actor: { id: 7 } }
121
+ })));
122
+ return built.filter(Boolean).map((event) => event.realtime.event);
123
+ }
124
+ assert.deepEqual(await emittedNames("accept"), [
125
+ "workspace.invitations.pending.changed",
126
+ "users.bootstrap.changed",
127
+ "workspaces.changed",
128
+ "workspace.members.changed",
129
+ "workspace.invites.changed"
130
+ ]);
131
+ assert.deepEqual(await emittedNames("refuse"), [
132
+ "workspace.invitations.pending.changed",
133
+ "users.bootstrap.changed",
134
+ "workspace.invites.changed"
135
+ ]);
136
+ });
@@ -1,9 +1,10 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import { UsersCoreServiceProvider } from "../../users-core/src/server/UsersCoreServiceProvider.js";
4
- import { INTERNAL_JSON_REST_API } from "../../json-rest-api-core/src/server/jsonRestApiHost.js";
5
3
  import { resolveTenancyProfile } from "../src/shared/tenancyProfile.js";
6
- import { WorkspacesCoreServiceProvider } from "../src/server/WorkspacesCoreServiceProvider.js";
4
+ import { registerWorkspaceDirectoryRoutes } from "../src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js";
5
+ import { registerWorkspaceMembersRoutes } from "../src/server/workspaceMembers/bootWorkspaceMembers.js";
6
+ import { registerWorkspacePendingInvitationsRoutes } from "../src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js";
7
+ import { registerWorkspaceSettingsRoutes } from "../src/server/workspaceSettings/bootWorkspaceSettings.js";
7
8
 
8
9
  function createReplyDouble() {
9
10
  return {
@@ -30,20 +31,11 @@ function findRoute(routes, { method, path }) {
30
31
  }
31
32
 
32
33
  async function registerRoutes({
33
- authService = {},
34
- consoleService = null,
35
34
  workspaceEnabled = true,
36
- workspaceTenancyEnabled = true,
37
35
  workspaceInvitationsEnabled = true,
38
- workspaceSelfCreateEnabled = true
36
+ workspaceSelfCreateEnabled = true,
37
+ config = {}
39
38
  } = {}) {
40
- const internalApi = {
41
- resources: {},
42
- async addResource(scopeName) {
43
- this.resources[scopeName] = {};
44
- return this.resources[scopeName];
45
- }
46
- };
47
39
  const registeredRoutes = [];
48
40
  const router = {
49
41
  register(method, path, route, handler) {
@@ -55,53 +47,14 @@ async function registerRoutes({
55
47
  });
56
48
  }
57
49
  };
58
-
59
- const bindings = new Map([
60
- ["jskit.http.router", router],
61
- ["authService", authService],
62
- [INTERNAL_JSON_REST_API, internalApi],
63
- [
64
- "users.accountProfile.service",
65
- {
66
- async readAvatar() {
67
- return {
68
- mimeType: "image/png",
69
- buffer: Buffer.from([])
70
- };
71
- }
72
- }
73
- ],
74
- ["actionExecutor", {}],
75
- ["workspaces.enabled", workspaceEnabled],
76
- ["workspaces.tenancy.enabled", workspaceTenancyEnabled],
77
- ["workspaces.invitations.enabled", workspaceInvitationsEnabled],
78
- ["workspaces.self-create.enabled", workspaceSelfCreateEnabled]
79
- ]);
80
-
81
- if (consoleService) {
82
- bindings.set("consoleService", consoleService);
83
- }
84
-
85
- const app = {
86
- has(token) {
87
- return bindings.has(token);
88
- },
89
- instance(token, value) {
90
- bindings.set(token, value);
91
- return this;
92
- },
93
- make(token) {
94
- if (!bindings.has(token)) {
95
- throw new Error(`Missing test binding for token: ${String(token)}`);
96
- }
97
- return bindings.get(token);
50
+ if (workspaceEnabled) {
51
+ registerWorkspaceDirectoryRoutes(router, { config, workspaceSelfCreateEnabled });
52
+ registerWorkspaceSettingsRoutes(router, { config });
53
+ registerWorkspaceMembersRoutes(router, { config, workspaceInvitationsEnabled });
54
+ if (workspaceInvitationsEnabled) {
55
+ registerWorkspacePendingInvitationsRoutes(router);
98
56
  }
99
- };
100
-
101
- const usersCoreProvider = new UsersCoreServiceProvider();
102
- const workspacesCoreProvider = new WorkspacesCoreServiceProvider();
103
- await usersCoreProvider.boot(app);
104
- await workspacesCoreProvider.boot(app);
57
+ }
105
58
 
106
59
  return registeredRoutes;
107
60
  }
@@ -115,8 +68,8 @@ async function registerRoutesForMode({
115
68
  tenancyPolicy
116
69
  });
117
70
  return registerRoutes({
71
+ config: { tenancyMode, tenancyPolicy },
118
72
  workspaceEnabled: tenancyProfile.workspace.enabled === true,
119
- workspaceTenancyEnabled: tenancyProfile.mode === "workspaces",
120
73
  workspaceInvitationsEnabled:
121
74
  tenancyProfile.workspace.enabled === true && tenancyProfile.mode !== "none",
122
75
  workspaceSelfCreateEnabled: tenancyProfile.workspace.allowSelfCreate === true
@@ -220,7 +173,6 @@ test("workspace core/settings routes mount one canonical workspace endpoint", as
220
173
  test("workspaces-core boot skips workspace routes when workspace policy is disabled", async () => {
221
174
  const routes = await registerRoutes({
222
175
  workspaceEnabled: false,
223
- workspaceTenancyEnabled: false,
224
176
  workspaceInvitationsEnabled: false,
225
177
  workspaceSelfCreateEnabled: false
226
178
  });
@@ -230,13 +182,11 @@ test("workspaces-core boot skips workspace routes when workspace policy is disab
230
182
  assert.equal(findRoute(routes, { method: "GET", path: "/api/w/:workspaceSlug" }), null);
231
183
  assert.equal(findRoute(routes, { method: "PATCH", path: "/api/w/:workspaceSlug" }), null);
232
184
  assert.equal(findRoute(routes, { method: "GET", path: "/api/w/:workspaceSlug/settings" }), null);
233
- assert.equal(findRoute(routes, { method: "GET", path: "/api/settings" })?.path, "/api/settings");
234
185
  });
235
186
 
236
187
  test("workspaces-core boot skips workspace create route when self-create policy is disabled", async () => {
237
188
  const routes = await registerRoutes({
238
189
  workspaceEnabled: true,
239
- workspaceTenancyEnabled: true,
240
190
  workspaceInvitationsEnabled: true,
241
191
  workspaceSelfCreateEnabled: false
242
192
  });
@@ -327,7 +277,6 @@ test("workspaces-core route registration follows tenancy mode matrix", async ()
327
277
  test("workspaces-core boot skips invitation redeem/list routes when workspace invitations are disabled", async () => {
328
278
  const routes = await registerRoutes({
329
279
  workspaceEnabled: true,
330
- workspaceTenancyEnabled: true,
331
280
  workspaceInvitationsEnabled: false,
332
281
  workspaceSelfCreateEnabled: false
333
282
  });
@@ -1,352 +0,0 @@
1
- export default Object.freeze({
2
- packageVersion: 1,
3
- packageId: "@jskit-ai/workspaces-core",
4
- version: "0.1.138-oldline.1",
5
- kind: "runtime",
6
- description: "Workspace tenancy runtime plus HTTP routes, role catalog, and workspace config scaffolding.",
7
- dependsOn: [
8
- "@jskit-ai/json-rest-api-core",
9
- "@jskit-ai/resource-core",
10
- "@jskit-ai/resource-crud-core",
11
- "@jskit-ai/users-core"
12
- ],
13
- capabilities: {
14
- provides: [
15
- "workspaces.core",
16
- "workspaces.server-routes"
17
- ],
18
- requires: [
19
- "users.core",
20
- "json-rest-api.core"
21
- ]
22
- },
23
- runtime: {
24
- server: {
25
- providers: [
26
- {
27
- entrypoint: "src/server/WorkspacesCoreServiceProvider.js",
28
- export: "WorkspacesCoreServiceProvider"
29
- }
30
- ]
31
- },
32
- client: {
33
- providers: []
34
- }
35
- },
36
- metadata: {
37
- jskit: {
38
- tableOwnership: {
39
- tables: [
40
- {
41
- tableName: "workspaces",
42
- provenance: "runtime-package",
43
- ownerKind: "package-runtime"
44
- },
45
- {
46
- tableName: "workspace_memberships",
47
- provenance: "runtime-package",
48
- ownerKind: "package-runtime"
49
- },
50
- {
51
- tableName: "workspace_settings",
52
- provenance: "runtime-package",
53
- ownerKind: "package-runtime"
54
- },
55
- {
56
- tableName: "workspace_invites",
57
- provenance: "runtime-package",
58
- ownerKind: "package-runtime"
59
- }
60
- ]
61
- }
62
- },
63
- apiSummary: {
64
- surfaces: [
65
- {
66
- subpath: "./server",
67
- summary: "Exports the workspace runtime provider and workspace route registration surface."
68
- }
69
- ],
70
- containerTokens: {
71
- server: [
72
- "workspaces.server.scope-support"
73
- ],
74
- client: []
75
- }
76
- },
77
- server: {
78
- routes: [
79
- {
80
- method: "POST",
81
- path: "/api/workspaces",
82
- summary: "Create a workspace for the authenticated user."
83
- },
84
- {
85
- method: "GET",
86
- path: "/api/workspaces",
87
- summary: "List workspaces visible to authenticated user."
88
- },
89
- {
90
- method: "GET",
91
- path: "/api/workspace/invitations/resolve",
92
- summary: "Resolve safe public workspace invitation metadata."
93
- },
94
- {
95
- method: "GET",
96
- path: "/api/workspace/invitations/pending",
97
- summary: "List pending workspace invitations for authenticated user."
98
- },
99
- {
100
- method: "POST",
101
- path: "/api/workspace/invitations/redeem",
102
- summary: "Accept or refuse a workspace invitation using an invite token."
103
- },
104
- {
105
- method: "GET",
106
- path: "/api/w/:workspaceSlug/settings",
107
- summary: "Get workspace settings and role catalog by workspace slug."
108
- },
109
- {
110
- method: "PATCH",
111
- path: "/api/w/:workspaceSlug/settings",
112
- summary: "Update workspace settings by workspace slug."
113
- },
114
- {
115
- method: "GET",
116
- path: "/api/w/:workspaceSlug/roles",
117
- summary: "Get role catalog by workspace slug."
118
- },
119
- {
120
- method: "GET",
121
- path: "/api/w/:workspaceSlug/members",
122
- summary: "List members by workspace slug."
123
- },
124
- {
125
- method: "PATCH",
126
- path: "/api/w/:workspaceSlug/members/:memberUserId/role",
127
- summary: "Update workspace member role by workspace slug."
128
- },
129
- {
130
- method: "GET",
131
- path: "/api/w/:workspaceSlug/invites",
132
- summary: "List workspace invites by workspace slug."
133
- },
134
- {
135
- method: "POST",
136
- path: "/api/w/:workspaceSlug/invites",
137
- summary: "Create workspace invite by workspace slug."
138
- },
139
- {
140
- method: "DELETE",
141
- path: "/api/w/:workspaceSlug/invites/:inviteId",
142
- summary: "Revoke workspace invite by workspace slug."
143
- }
144
- ]
145
- }
146
- },
147
- mutations: {
148
- dependencies: {
149
- runtime: {
150
- "@jskit-ai/json-rest-api-core": "0.1.92",
151
- "@jskit-ai/resource-core": "0.1.90",
152
- "@jskit-ai/resource-crud-core": "0.1.90",
153
- "@jskit-ai/users-core": "0.1.161"
154
- },
155
- dev: {}
156
- },
157
- packageJson: {
158
- scripts: {
159
- "server:app": "SERVER_SURFACE=app node ./bin/server.js",
160
- "server:admin": "SERVER_SURFACE=admin node ./bin/server.js"
161
- }
162
- },
163
- procfile: {},
164
- files: [
165
- {
166
- op: "install-migration",
167
- from: "templates/migrations/workspaces_core_initial.cjs",
168
- toDir: "migrations",
169
- extension: ".cjs",
170
- reason: "Install workspace tenancy schema migration.",
171
- category: "migration",
172
- id: "workspaces-core-initial-schema"
173
- },
174
- {
175
- op: "install-migration",
176
- from: "templates/migrations/workspaces_core_workspace_settings_single_name_source.cjs",
177
- toDir: "migrations",
178
- extension: ".cjs",
179
- reason: "Remove workspace_settings name/avatar fields so workspace identity data comes from workspaces only.",
180
- category: "migration",
181
- id: "users-core-workspace-settings-single-name-source"
182
- },
183
- {
184
- op: "install-migration",
185
- from: "templates/migrations/workspaces_core_workspaces_drop_color.cjs",
186
- toDir: "migrations",
187
- extension: ".cjs",
188
- reason: "Drop workspaces.color now that workspace theme colors live in workspace_settings.",
189
- category: "migration",
190
- id: "users-core-workspaces-drop-color"
191
- },
192
- {
193
- from: "templates/config/roles.js",
194
- to: "config/roles.js",
195
- ownership: "app",
196
- preserveOnRemove: true,
197
- reason: "Install app-owned role catalog in a dedicated config file.",
198
- category: "workspaces-core",
199
- id: "users-core-app-owned-role-catalog-config"
200
- },
201
- {
202
- from: "templates/packages/main/src/server/email/workspaceInviteEmail.js",
203
- to: "packages/main/src/server/email/workspaceInviteEmail.js",
204
- ownership: "app",
205
- preserveOnRemove: true,
206
- reason: "Install app-owned editable workspace invite email template.",
207
- category: "workspaces-core",
208
- id: "workspaces-core-main-workspace-invite-email-template"
209
- }
210
- ],
211
- source: [
212
- {
213
- op: "ensure-import",
214
- file: "config/server.js",
215
- namedImports: ["renderWorkspaceInviteEmail"],
216
- from: "../packages/main/src/server/email/workspaceInviteEmail.js",
217
- reason: "Load app-owned workspace invite email renderer from packages/main.",
218
- category: "workspaces-core",
219
- id: "workspaces-core-server-config-workspace-invite-email-import"
220
- },
221
- {
222
- op: "ensure-assignment",
223
- file: "config/server.js",
224
- target: "config.workspaceInviteEmailTemplate",
225
- value: "renderWorkspaceInviteEmail",
226
- reason: "Bind app-owned workspace invite email renderer into server config.",
227
- category: "workspaces-core",
228
- id: "workspaces-core-server-config-workspace-invite-email-template"
229
- }
230
- ],
231
- text: [
232
- {
233
- op: "append-text",
234
- file: "config/public.js",
235
- position: "top",
236
- skipIfContains: "import { roleCatalog } from \"./roles.js\";",
237
- value: "import { roleCatalog } from \"./roles.js\";\n",
238
- reason: "Load app-owned role catalog from dedicated config file.",
239
- category: "workspaces-core",
240
- id: "users-core-role-catalog-public-import"
241
- },
242
- {
243
- op: "append-text",
244
- file: "config/public.js",
245
- position: "top",
246
- skipIfContains: "import { surfaceAccessPolicies } from \"./surfaceAccessPolicies.js\";",
247
- value: "import { surfaceAccessPolicies } from \"./surfaceAccessPolicies.js\";\n",
248
- reason: "Load app-owned surface access policy catalog from dedicated config file.",
249
- category: "workspaces-core",
250
- id: "users-core-surface-access-policies-public-import"
251
- },
252
- {
253
- op: "append-text",
254
- file: "config/surfaceAccessPolicies.js",
255
- position: "top",
256
- skipIfContains: "export const surfaceAccessPolicies = {};",
257
- value: "export const surfaceAccessPolicies = {};\n\n",
258
- reason: "Initialize app-owned surface access policy config if missing.",
259
- category: "workspaces-core",
260
- id: "users-core-surface-access-policies-config-init"
261
- },
262
- {
263
- op: "append-text",
264
- file: "config/surfaceAccessPolicies.js",
265
- position: "bottom",
266
- skipIfContains: "surfaceAccessPolicies.workspace_member = {",
267
- value: "\nsurfaceAccessPolicies.workspace_member = {\n requireAuth: true,\n requireWorkspaceMembership: true\n};\n",
268
- reason: "Register workspace-member surface access policy for workspace surfaces.",
269
- category: "workspaces-core",
270
- id: "users-core-surface-access-policies-workspace-member"
271
- },
272
- {
273
- op: "append-text",
274
- file: "config/public.js",
275
- position: "bottom",
276
- skipIfContains: "config.surfaceDefinitions.app = {",
277
- value:
278
- "\nconfig.surfaceDefinitions.app = {\n id: \"app\",\n label: \"App\",\n pagesRoot: \"w/[workspaceSlug]\",\n enabled: true,\n requiresAuth: true,\n requiresWorkspace: true,\n accessPolicyId: \"workspace_member\",\n origin: \"\"\n};\n\nconfig.surfaceDefinitions.admin = {\n id: \"admin\",\n label: \"Admin\",\n pagesRoot: \"w/[workspaceSlug]/admin\",\n enabled: true,\n requiresAuth: true,\n requiresWorkspace: true,\n accessPolicyId: \"workspace_member\",\n origin: \"\"\n};\n",
279
- reason: "Append workspace surface topology when tenancy enables workspace routing.",
280
- category: "workspaces-core",
281
- id: "users-core-surface-config-workspace",
282
- when: {
283
- config: "tenancyMode",
284
- in: ["personal", "workspaces"]
285
- }
286
- },
287
- {
288
- op: "append-text",
289
- file: "config/public.js",
290
- position: "bottom",
291
- skipIfContains: "config.workspaceSwitching =",
292
- value:
293
- "\nconfig.workspaceSwitching = true;\nconfig.workspaceInvitations = {\n enabled: true,\n allowInPersonalMode: true\n};\n",
294
- reason: "Append default workspace feature toggles into app-owned config.",
295
- category: "workspaces-core",
296
- id: "users-core-public-config"
297
- },
298
- {
299
- op: "append-text",
300
- file: "config/public.js",
301
- position: "bottom",
302
- skipIfContains: "config.roleCatalog = roleCatalog;",
303
- value: "\nconfig.roleCatalog = roleCatalog;\n",
304
- reason: "Bind app-owned role catalog onto public config.",
305
- category: "workspaces-core",
306
- id: "users-core-role-catalog-public-config"
307
- },
308
- {
309
- op: "append-text",
310
- file: "config/public.js",
311
- position: "bottom",
312
- skipIfContains: "config.surfaceAccessPolicies = surfaceAccessPolicies;",
313
- value: "\nconfig.surfaceAccessPolicies = surfaceAccessPolicies;\n",
314
- reason: "Bind app-owned surface access policies onto public config.",
315
- category: "workspaces-core",
316
- id: "users-core-surface-access-policies-public-config"
317
- },
318
- {
319
- op: "append-text",
320
- file: "config/server.js",
321
- position: "bottom",
322
- skipIfContains: "config.workspaceColor =",
323
- value: "\nconfig.workspaceColor = \"#1867C0\";\n",
324
- reason: "Append default workspace server settings into app-owned config.",
325
- category: "workspaces-core",
326
- id: "users-core-server-config"
327
- },
328
- {
329
- op: "append-text",
330
- file: "config/server.js",
331
- position: "bottom",
332
- skipIfContains: "config.workspaceSettings =",
333
- value:
334
- "\nconfig.workspaceSettings = {\n defaults: {\n invitesEnabled: true\n }\n};\n",
335
- reason: "Append app-owned workspace settings defaults into the server config.",
336
- category: "workspaces-core",
337
- id: "users-core-workspace-settings-server-config"
338
- },
339
- {
340
- op: "append-text",
341
- file: "config/server.js",
342
- position: "bottom",
343
- skipIfContains: "config.workspaceMembers =",
344
- value:
345
- "\nconfig.workspaceMembers = {\n defaults: {\n inviteExpiresInMs: 604800000\n }\n};\n",
346
- reason: "Append app-owned workspace member invite policy defaults into the server config.",
347
- category: "workspaces-core",
348
- id: "users-core-workspace-members-server-config"
349
- }
350
- ]
351
- }
352
- });