@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
@@ -4,15 +4,21 @@ import {
4
4
  import { returnJsonApiData } from "@jskit-ai/http-runtime/shared";
5
5
  import { workspaceMembersResource } from "../../shared/resources/workspaceMembersResource.js";
6
6
  import { workspacePendingInvitationsResource } from "../../shared/resources/workspacePendingInvitationsResource.js";
7
+ import { createInviteDecisionEvents } from "../common/support/realtimeServiceEvents.js";
7
8
  import { resolveActionUser } from "../common/support/resolveActionUser.js";
8
9
 
9
- const workspacePendingInvitationsActions = Object.freeze([
10
+ const WORKSPACE_INVITE_DECISION_EVENTS = createInviteDecisionEvents();
11
+
12
+ const workspacePendingInvitationsActionSpecifications = Object.freeze([
10
13
  {
11
14
  id: "workspace.invitation.resolve",
12
15
  version: 1,
13
16
  kind: "query",
14
17
  channels: ["api", "automation", "internal"],
15
- surfacesFrom: "enabled",
18
+ surfaces: ["*"],
19
+ permission: {
20
+ require: "none"
21
+ },
16
22
  input: workspacePendingInvitationsResource.operations.resolve.query,
17
23
  output: null,
18
24
  idempotency: "none",
@@ -20,9 +26,9 @@ const workspacePendingInvitationsActions = Object.freeze([
20
26
  actionName: "workspace.invitation.resolve"
21
27
  },
22
28
  observability: {},
23
- async execute(input, context, deps) {
29
+ async run(workspacePendingInvitationsService, input, context) {
24
30
  return returnJsonApiData(
25
- await deps.workspacePendingInvitationsService.resolveInviteByToken(input?.token, {
31
+ await workspacePendingInvitationsService.resolveInviteByToken(input?.token, {
26
32
  context
27
33
  })
28
34
  );
@@ -33,7 +39,7 @@ const workspacePendingInvitationsActions = Object.freeze([
33
39
  version: 1,
34
40
  kind: "query",
35
41
  channels: ["api", "automation", "internal"],
36
- surfacesFrom: "enabled",
42
+ surfaces: ["*"],
37
43
  permission: {
38
44
  require: "authenticated"
39
45
  },
@@ -44,9 +50,9 @@ const workspacePendingInvitationsActions = Object.freeze([
44
50
  actionName: "workspace.invitations.pending.list"
45
51
  },
46
52
  observability: {},
47
- async execute(input, context, deps) {
53
+ async run(workspacePendingInvitationsService, input, context) {
48
54
  return returnJsonApiData({
49
- pendingInvites: await deps.workspacePendingInvitationsService.listPendingInvitesForUser(resolveActionUser(context, input), {
55
+ pendingInvites: await workspacePendingInvitationsService.listPendingInvitesForUser(resolveActionUser(context, input), {
50
56
  context
51
57
  })
52
58
  });
@@ -57,7 +63,7 @@ const workspacePendingInvitationsActions = Object.freeze([
57
63
  version: 1,
58
64
  kind: "command",
59
65
  channels: ["api", "automation", "internal"],
60
- surfacesFrom: "enabled",
66
+ surfaces: ["*"],
61
67
  permission: {
62
68
  require: "authenticated"
63
69
  },
@@ -68,12 +74,13 @@ const workspacePendingInvitationsActions = Object.freeze([
68
74
  actionName: "workspace.invite.redeem"
69
75
  },
70
76
  observability: {},
71
- async execute(input, context, deps) {
77
+ events: WORKSPACE_INVITE_DECISION_EVENTS,
78
+ async run(workspacePendingInvitationsService, input, context) {
72
79
  const payload = input || {};
73
80
  const user = resolveActionUser(context, input);
74
81
 
75
82
  if (payload.decision === "accept") {
76
- return returnJsonApiData(await deps.workspacePendingInvitationsService.acceptInviteByToken({
83
+ return returnJsonApiData(await workspacePendingInvitationsService.acceptInviteByToken({
77
84
  user,
78
85
  token: payload.token
79
86
  }, {
@@ -81,7 +88,7 @@ const workspacePendingInvitationsActions = Object.freeze([
81
88
  }));
82
89
  }
83
90
 
84
- return returnJsonApiData(await deps.workspacePendingInvitationsService.refuseInviteByToken({
91
+ return returnJsonApiData(await workspacePendingInvitationsService.refuseInviteByToken({
85
92
  user,
86
93
  token: payload.token
87
94
  }, {
@@ -91,4 +98,16 @@ const workspacePendingInvitationsActions = Object.freeze([
91
98
  }
92
99
  ]);
93
100
 
94
- export { workspacePendingInvitationsActions };
101
+ function buildWorkspacePendingInvitationsActions({ workspacePendingInvitationsService } = {}) {
102
+ if (!workspacePendingInvitationsService) {
103
+ throw new TypeError("buildWorkspacePendingInvitationsActions requires workspacePendingInvitationsService.");
104
+ }
105
+ return workspacePendingInvitationsActionSpecifications.map(({ run, ...definition }) => Object.freeze({
106
+ ...definition,
107
+ execute(input, context) {
108
+ return run(workspacePendingInvitationsService, input, context);
109
+ }
110
+ }));
111
+ }
112
+
113
+ export { workspacePendingInvitationsActionSpecifications, buildWorkspacePendingInvitationsActions };
@@ -19,14 +19,11 @@ function resolveWorkspaceSettingsRecordId(record = {}, context = {}) {
19
19
  throw new Error("Workspace settings JSON:API response requires workspace id.");
20
20
  }
21
21
 
22
- function bootWorkspaceSettings(app) {
23
- if (!app || typeof app.make !== "function") {
24
- throw new Error("bootWorkspaceSettings requires application make().");
22
+ function registerWorkspaceSettingsRoutes(router, { config = {} } = {}) {
23
+ if (!router || typeof router.register !== "function") {
24
+ throw new TypeError("registerWorkspaceSettingsRoutes requires router.register().");
25
25
  }
26
-
27
- const router = app.make("jskit.http.router");
28
- const appConfig = typeof app.has === "function" && app.has("appConfig") ? app.make("appConfig") : {};
29
- const workspaceRouteSurfaceId = resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig(appConfig);
26
+ const workspaceRouteSurfaceId = resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig(config);
30
27
 
31
28
  router.register(
32
29
  "GET",
@@ -92,4 +89,4 @@ function bootWorkspaceSettings(app) {
92
89
  );
93
90
  }
94
91
 
95
- export { bootWorkspaceSettings };
92
+ export { registerWorkspaceSettingsRoutes };
@@ -2,8 +2,15 @@ import { composeSchemaDefinitions } from "@jskit-ai/kernel/shared/validators";
2
2
  import { returnJsonApiData } from "@jskit-ai/http-runtime/shared";
3
3
  import { workspaceSettingsResource } from "../../shared/resources/workspaceSettingsResource.js";
4
4
  import { workspaceSlugParamsValidator } from "../common/validators/routeParamsValidator.js";
5
+ import { createWorkspaceEntityAndBootstrapEvents } from "../common/support/realtimeServiceEvents.js";
5
6
  import { resolveWorkspace } from "../support/resolveWorkspace.js";
6
7
 
8
+ const WORKSPACE_SETTINGS_CHANGED_EVENTS = createWorkspaceEntityAndBootstrapEvents({
9
+ workspaceEntity: "settings",
10
+ workspaceOperation: "updated",
11
+ workspaceRealtimeEvent: "workspace.settings.changed"
12
+ });
13
+
7
14
  const workspaceSettingsUpdateInputValidator = composeSchemaDefinitions([
8
15
  workspaceSlugParamsValidator,
9
16
  workspaceSettingsResource.operations.patch.body
@@ -12,13 +19,13 @@ const workspaceSettingsUpdateInputValidator = composeSchemaDefinitions([
12
19
  context: "workspaceSettingsActions.workspaceSettingsUpdateInputValidator"
13
20
  });
14
21
 
15
- const workspaceSettingsActions = Object.freeze([
22
+ const workspaceSettingsActionSpecifications = Object.freeze([
16
23
  {
17
24
  id: "workspace.settings.read",
18
25
  version: 1,
19
26
  kind: "query",
20
27
  channels: ["api", "automation", "internal"],
21
- surfacesFrom: "workspace",
28
+ surfaces: ["*"],
22
29
  permission: {
23
30
  require: "any",
24
31
  permissions: ["workspace.settings.view", "workspace.settings.update"]
@@ -30,8 +37,8 @@ const workspaceSettingsActions = Object.freeze([
30
37
  actionName: "workspace.settings.read"
31
38
  },
32
39
  observability: {},
33
- async execute(input, context, deps) {
34
- const response = await deps.workspaceSettingsService.getWorkspaceSettings(resolveWorkspace(context, input), {
40
+ async run(workspaceSettingsService, input, context) {
41
+ const response = await workspaceSettingsService.getWorkspaceSettings(resolveWorkspace(context, input), {
35
42
  context
36
43
  });
37
44
 
@@ -43,7 +50,7 @@ const workspaceSettingsActions = Object.freeze([
43
50
  version: 1,
44
51
  kind: "command",
45
52
  channels: ["api", "assistant_tool", "automation", "internal"],
46
- surfacesFrom: "workspace",
53
+ surfaces: ["*"],
47
54
  permission: {
48
55
  require: "all",
49
56
  permissions: ["workspace.settings.update"]
@@ -55,14 +62,15 @@ const workspaceSettingsActions = Object.freeze([
55
62
  actionName: "workspace.settings.update"
56
63
  },
57
64
  observability: {},
65
+ events: WORKSPACE_SETTINGS_CHANGED_EVENTS,
58
66
  extensions: {
59
67
  assistant: {
60
68
  description: "Update workspace settings."
61
69
  }
62
70
  },
63
- async execute(input, context, deps) {
71
+ async run(workspaceSettingsService, input, context) {
64
72
  const { workspaceSlug, ...patch } = input;
65
- const response = await deps.workspaceSettingsService.updateWorkspaceSettings(
73
+ const response = await workspaceSettingsService.updateWorkspaceSettings(
66
74
  resolveWorkspace(context, input),
67
75
  patch,
68
76
  {
@@ -75,4 +83,14 @@ const workspaceSettingsActions = Object.freeze([
75
83
  }
76
84
  ]);
77
85
 
78
- export { workspaceSettingsActions };
86
+ function buildWorkspaceSettingsActions({ workspaceSettingsService } = {}) {
87
+ if (!workspaceSettingsService) throw new TypeError("buildWorkspaceSettingsActions requires workspaceSettingsService.");
88
+ return workspaceSettingsActionSpecifications.map(({ run, ...definition }) => Object.freeze({
89
+ ...definition,
90
+ execute(input, context) {
91
+ return run(workspaceSettingsService, input, context);
92
+ }
93
+ }));
94
+ }
95
+
96
+ export { workspaceSettingsActionSpecifications, buildWorkspaceSettingsActions };
@@ -35,7 +35,7 @@ function pickPatchFields(source = {}) {
35
35
  return patch;
36
36
  }
37
37
 
38
- function createDefaultWorkspaceSettingsCreatePayload(workspaceId) {
38
+ function createDefaultWorkspaceSettingsCreatePayload(workspaceId, defaultInvitesEnabled) {
39
39
  const palettes = resolveWorkspaceThemePalettes({});
40
40
 
41
41
  return {
@@ -48,11 +48,11 @@ function createDefaultWorkspaceSettingsCreatePayload(workspaceId) {
48
48
  darkSecondaryColor: palettes.dark.secondaryColor,
49
49
  darkSurfaceColor: palettes.dark.surfaceColor,
50
50
  darkSurfaceVariantColor: palettes.dark.surfaceVariantColor,
51
- invitesEnabled: true
51
+ invitesEnabled: defaultInvitesEnabled
52
52
  };
53
53
  }
54
54
 
55
- function createRepository({ api, knex } = {}) {
55
+ function createRepository({ api, knex, defaultInvitesEnabled = true } = {}) {
56
56
  if (!api?.resources?.workspaceSettings) {
57
57
  throw new TypeError("workspaceSettingsRepository requires json-rest-api workspaceSettings resource.");
58
58
  }
@@ -103,7 +103,7 @@ function createRepository({ api, knex } = {}) {
103
103
  {
104
104
  inputRecord: createJsonApiInputRecord(
105
105
  RESOURCE_TYPE,
106
- createDefaultWorkspaceSettingsCreatePayload(normalizedWorkspaceId),
106
+ createDefaultWorkspaceSettingsCreatePayload(normalizedWorkspaceId, defaultInvitesEnabled === true),
107
107
  {
108
108
  id: normalizedWorkspaceId
109
109
  }
@@ -14,7 +14,8 @@ test("workspaces-core exports are explicit and aligned with production usage", (
14
14
  packageDir: PACKAGE_DIR,
15
15
  packageId: "@jskit-ai/workspaces-core",
16
16
  requiredExports: [
17
- "./server/WorkspacesCoreServiceProvider",
17
+ "./server/WorkspacesFeature",
18
+ "./server/WorkspacesIntegrationsProvider",
18
19
  "./server/validators/routeParamsValidator",
19
20
  "./server/support/workspaceRouteInput",
20
21
  "./shared/resources/workspaceResource",
@@ -0,0 +1,116 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createAuthExtensions } from "@jskit-ai/auth-core/server/authExtensions";
4
+ import { createActionProvider } from "@jskit-ai/kernel/server/actions";
5
+ import { createCapabilityRuntime, defineProvider } from "@jskit-ai/kernel/shared/capabilities";
6
+ import { createBootstrapRuntime } from "@jskit-ai/kernel/server/runtime";
7
+ import { createUsersExtensions } from "@jskit-ai/users-core/server/usersExtensions";
8
+ import { WorkspacesFeature } from "../src/server/WorkspacesFeature.js";
9
+ import { WorkspacesIntegrationsProvider } from "../src/server/WorkspacesIntegrationsProvider.js";
10
+
11
+ function createJsonRestApiFixture() {
12
+ const resources = {};
13
+ return {
14
+ resources,
15
+ async addResource(scopeName) {
16
+ resources[scopeName] = Object.freeze({
17
+ query: async () => ({ data: [] }),
18
+ post: async () => null,
19
+ patch: async () => null
20
+ });
21
+ }
22
+ };
23
+ }
24
+
25
+ function runtimeConfig() {
26
+ return {
27
+ appPublicUrl: "http://localhost:5173",
28
+ tenancyMode: "workspaces",
29
+ tenancyPolicy: { workspace: { allowSelfCreate: true } },
30
+ workspaceInvitations: { enabled: true },
31
+ workspaceMembers: { defaults: { inviteExpiresInMs: 86400000 } },
32
+ workspaceSettings: { defaults: { invitesEnabled: true } },
33
+ surfaceDefaultId: "customer",
34
+ surfaceDefinitions: {
35
+ customer: { id: "customer", requiresWorkspace: true, accessPolicyId: "workspace_authenticated" }
36
+ },
37
+ surfaceAccessPolicies: {
38
+ workspace_authenticated: { requireWorkspaceMembership: false }
39
+ },
40
+ roleCatalog: {
41
+ workspace: { defaultInviteRole: "member" },
42
+ roles: {
43
+ owner: { permissions: ["workspace.*"] },
44
+ member: { assignable: true, permissions: ["workspace.settings.view"] }
45
+ }
46
+ }
47
+ };
48
+ }
49
+
50
+ test("WorkspacesFeature assembles workspace behavior and explicit integrations", async () => {
51
+ const routes = [];
52
+ const visibilityResolvers = [];
53
+ const bootstrap = createBootstrapRuntime();
54
+ const authExtensions = createAuthExtensions();
55
+ const usersExtensions = createUsersExtensions();
56
+ const jsonRestApi = createJsonRestApiFixture();
57
+ let workspaces = null;
58
+ let actionCatalogue = null;
59
+ const probe = defineProvider({
60
+ id: "test.workspaces.probe",
61
+ requires: {
62
+ actions: "runtime.actions",
63
+ workspaceCapability: "workspaces.core"
64
+ },
65
+ setup({ actions, workspaceCapability }) {
66
+ actionCatalogue = actions;
67
+ workspaces = workspaceCapability;
68
+ return {};
69
+ }
70
+ });
71
+ const runtime = createCapabilityRuntime({
72
+ inputs: {
73
+ "auth.extensions": authExtensions,
74
+ "runtime.bootstrap": bootstrap,
75
+ "runtime.config": runtimeConfig(),
76
+ "runtime.database": { knex() {} },
77
+ "runtime.env": {},
78
+ "runtime.http": {
79
+ router: {
80
+ register(method, path) {
81
+ routes.push(`${method} ${path}`);
82
+ }
83
+ },
84
+ registerVisibilityResolver(resolver) {
85
+ visibilityResolvers.push(resolver);
86
+ }
87
+ },
88
+ "runtime.json-rest-api": jsonRestApi,
89
+ "users.core": {
90
+ repositories: {
91
+ userProfiles: { async findById() { return null; } }
92
+ }
93
+ },
94
+ "users.extensions": usersExtensions
95
+ },
96
+ providers: [createActionProvider(), WorkspacesFeature, WorkspacesIntegrationsProvider, probe]
97
+ });
98
+
99
+ await runtime.start();
100
+
101
+ assert.deepEqual(Object.keys(jsonRestApi.resources).sort(), [
102
+ "workspaceInvites",
103
+ "workspaceMemberships",
104
+ "workspaceSettings",
105
+ "workspaces"
106
+ ]);
107
+ assert.equal(typeof workspaces.services.directory.resolveWorkspaceContextForUserBySlug, "function");
108
+ assert.equal(routes.includes("POST /api/workspaces"), true);
109
+ assert.equal(routes.includes("GET /api/workspace/invitations/resolve"), true);
110
+ assert.equal(actionCatalogue.listDefinitions().length, 16);
111
+ assert.deepEqual(bootstrap.diagnostics().contributorIds, ["workspaces.bootstrap"]);
112
+ assert.deepEqual(visibilityResolvers.map((entry) => entry.id), ["workspaces.visibility"]);
113
+ assert.deepEqual(usersExtensions.diagnostics().profileSyncLifecycleContributorIds, ["workspaces.provisioning"]);
114
+ assert.deepEqual(authExtensions.diagnostics().invitationContextResolverIds, ["workspaces.invitation"]);
115
+ assert.deepEqual(authExtensions.diagnostics().policyContextResolverIds, ["workspaces.policy"]);
116
+ });
@@ -0,0 +1,59 @@
1
+ import assert from "node:assert/strict";
2
+ import path from "node:path";
3
+ import test from "node:test";
4
+ import { readFile } from "node:fs/promises";
5
+ import { fileURLToPath } from "node:url";
6
+ import packageJson from "../package.json" with { type: "json" };
7
+
8
+ const packageMetadata = packageJson.jskit;
9
+
10
+ const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
11
+ const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
12
+
13
+ function findRoute(method, routePath) {
14
+ const routes = packageMetadata?.metadata?.server?.routes;
15
+ return Array.isArray(routes)
16
+ ? routes.find((entry) => entry?.method === method && entry?.path === routePath) || null
17
+ : null;
18
+ }
19
+
20
+ test("workspaces-core packageMetadata advertises public invite resolution route metadata", () => {
21
+ assert.deepEqual(findRoute("GET", "/api/workspace/invitations/resolve"), {
22
+ method: "GET",
23
+ path: "/api/workspace/invitations/resolve",
24
+ summary: "Resolve safe public workspace invitation metadata."
25
+ });
26
+ });
27
+
28
+ test("workspaces-core publishes an app-owned editable role catalog pattern", async () => {
29
+ const source = await readFile(
30
+ path.join(PACKAGE_DIR, "patterns", "workspace-server", "example", "config", "roles.js"),
31
+ "utf8"
32
+ );
33
+
34
+ assert.match(source, /export const roleCatalog/);
35
+ assert.equal(Object.hasOwn(packageMetadata, "mutations"), false);
36
+ });
37
+
38
+ test("workspaces-core publishes an app-owned editable workspace invite email pattern", async () => {
39
+ const source = await readFile(
40
+ path.join(
41
+ PACKAGE_DIR,
42
+ "patterns",
43
+ "workspace-server",
44
+ "example",
45
+ "packages",
46
+ "main",
47
+ "src",
48
+ "server",
49
+ "email",
50
+ "workspaceInviteEmail.js"
51
+ ),
52
+ "utf8"
53
+ );
54
+
55
+ assert.match(source, /function renderWorkspaceInviteEmail/);
56
+ assert.match(source, /export \{ renderWorkspaceInviteEmail \}/);
57
+ assert.match(source, /inviteUrl/);
58
+ assert.deepEqual(packageMetadata.migrations, { directories: ["migrations"] });
59
+ });
@@ -95,7 +95,7 @@ test("workspace settings and invite operations expose canonical validators", ()
95
95
  }
96
96
  });
97
97
 
98
- test("workspaces-core no longer uses a workspace schema helper that exposes raw schema leaves", () => {
98
+ test("workspaces-core keeps raw workspace schema leaves private", () => {
99
99
  const testFilePath = fileURLToPath(import.meta.url);
100
100
  const packageRoot = path.resolve(path.dirname(testFilePath), "..");
101
101
  const workspaceRoutesFilePath = path.join(packageRoot, "src", "server", "common", "routes", "workspaceRoutes.js");
@@ -47,8 +47,7 @@ test("workspace action context contributor resolves workspace context for worksp
47
47
  requestMeta: {
48
48
  request
49
49
  }
50
- },
51
- request
50
+ }
52
51
  });
53
52
 
54
53
  assert.deepEqual(calls, [
@@ -147,8 +146,7 @@ test("workspace action context contributor always resolves and stores resolved c
147
146
  requestMeta: {
148
147
  request
149
148
  }
150
- },
151
- request
149
+ }
152
150
  });
153
151
 
154
152
  assert.deepEqual(calls, [
@@ -224,8 +222,7 @@ test("workspace action context contributor resolves context for workspace-visibl
224
222
  requestMeta: {
225
223
  request
226
224
  }
227
- },
228
- request
225
+ }
229
226
  });
230
227
 
231
228
  assert.deepEqual(calls, [
@@ -306,8 +303,7 @@ test("workspace action context contributor resolves context for workspace surfac
306
303
  requestMeta: {
307
304
  request
308
305
  }
309
- },
310
- request
306
+ }
311
307
  });
312
308
 
313
309
  assert.deepEqual(calls, [
@@ -376,10 +372,9 @@ test("workspace action context contributor permits missing membership only for a
376
372
  },
377
373
  input: { workspaceSlug: "acme" },
378
374
  context: {
379
- requestMeta: { request }
380
- },
381
- request,
382
- surface: "app"
375
+ requestMeta: { request },
376
+ surface: "app"
377
+ }
383
378
  });
384
379
 
385
380
  assert.equal(calls.length, 1);
@@ -148,3 +148,33 @@ test("workspace auth policy context resolver treats optional context as membersh
148
148
  assert.equal(resolved.membership, null);
149
149
  assert.deepEqual(resolved.permissions, []);
150
150
  });
151
+
152
+ test("workspace auth policy honors membership-optional workspace surfaces", async () => {
153
+ const calls = [];
154
+ const request = {
155
+ params: { workspaceSlug: "dog-and-groom" },
156
+ routeOptions: { config: { surface: "customer" } }
157
+ };
158
+ const resolver = createWorkspaceAuthPolicyContextResolver({
159
+ workspaceMembershipOptionalSurfaceIds: ["customer"],
160
+ workspaceService: {
161
+ async resolveWorkspaceContextForUserBySlug(actor, workspaceSlug, options) {
162
+ calls.push({ actor, workspaceSlug, options });
163
+ return {
164
+ workspace: { id: 11, slug: workspaceSlug },
165
+ membership: null,
166
+ permissions: []
167
+ };
168
+ }
169
+ }
170
+ });
171
+
172
+ const resolved = await resolver({
173
+ request,
174
+ actor: { id: 7 },
175
+ meta: { contextPolicy: "required" }
176
+ });
177
+
178
+ assert.equal(calls[0].options.requireMembership, false);
179
+ assert.equal(resolved.membership, null);
180
+ });
@@ -81,3 +81,34 @@ test("workspace route visibility resolver still marks workspace_user as actor-sc
81
81
  userId: "user_99"
82
82
  });
83
83
  });
84
+
85
+ test("workspace route visibility honors membership-optional workspace surfaces", async () => {
86
+ const calls = [];
87
+ const request = {
88
+ routeOptions: { config: { surface: "customer" } }
89
+ };
90
+ const resolver = createWorkspaceRouteVisibilityResolver({
91
+ workspaceMembershipOptionalSurfaceIds: ["customer"],
92
+ workspaceService: {
93
+ async resolveWorkspaceContextForUserBySlug(actor, workspaceSlug, options) {
94
+ calls.push({ actor, workspaceSlug, options });
95
+ return { workspace: { id: 11 } };
96
+ }
97
+ }
98
+ });
99
+
100
+ const contribution = await resolver.resolve({
101
+ visibility: "workspace_user",
102
+ context: { actor: { id: "user_99" } },
103
+ input: { workspaceSlug: "dog-and-groom" },
104
+ request
105
+ });
106
+
107
+ assert.equal(calls[0].options.requireMembership, false);
108
+ assert.deepEqual(contribution, {
109
+ scopeKind: "workspace_user",
110
+ requiresActorScope: true,
111
+ scopeOwnerId: "11",
112
+ userId: "user_99"
113
+ });
114
+ });
@@ -179,7 +179,7 @@ function createWorkspaceServiceFixture({
179
179
  return { service, calls, insertedPayloads };
180
180
  }
181
181
 
182
- test("workspaceService no longer exposes bootstrap payload assembly", () => {
182
+ test("workspaceService keeps bootstrap payload assembly outside its API", () => {
183
183
  const { service } = createWorkspaceServiceFixture();
184
184
  assert.equal(service.buildBootstrapPayload, undefined);
185
185
  });
@@ -199,7 +199,7 @@ test("workspaceService.listWorkspacesForUser returns only accessible workspaces"
199
199
  assert.equal(calls.insert, 0);
200
200
  });
201
201
 
202
- test("workspaceService.listWorkspacesForUser no longer provisions personal workspace in workspace mode", async () => {
202
+ test("workspaceService.listWorkspacesForUser does not provision a personal workspace in workspace mode", async () => {
203
203
  const { service, calls } = createWorkspaceServiceFixture({
204
204
  tenancyMode: "workspaces",
205
205
  personalWorkspace: null