@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.
- package/package.json +154 -10
- package/patterns/workspace-server/PATTERN.md +60 -0
- package/src/server/WorkspacesFeature.js +226 -0
- package/src/server/WorkspacesIntegrationsProvider.js +74 -0
- package/src/server/common/contributors/workspaceActionContextContributor.js +5 -9
- package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +16 -2
- package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +22 -4
- package/src/server/common/support/realtimeServiceEvents.js +102 -61
- package/src/server/support/workspaceActionSurfaces.js +1 -13
- package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +5 -11
- package/src/server/workspaceDirectory/workspaceDirectoryActions.js +24 -14
- package/src/server/workspaceMembers/bootWorkspaceMembers.js +5 -12
- package/src/server/workspaceMembers/workspaceMembersActions.js +63 -23
- package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +4 -6
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +31 -12
- package/src/server/workspaceSettings/bootWorkspaceSettings.js +5 -8
- package/src/server/workspaceSettings/workspaceSettingsActions.js +26 -8
- package/src/server/workspaceSettings/workspaceSettingsRepository.js +4 -4
- package/test/exportsContract.test.js +2 -1
- package/test/featureRuntime.test.js +116 -0
- package/test/packageMetadata.test.js +59 -0
- package/test/usersRouteResources.test.js +1 -1
- package/test/workspaceActionContextContributor.test.js +7 -12
- package/test/workspaceAuthPolicyContextResolver.test.js +30 -0
- package/test/workspaceRouteVisibilityResolver.test.js +31 -0
- package/test/workspaceService.test.js +2 -2
- package/test/workspaceSettingsActions.test.js +103 -17
- package/test/workspacesRouteRequestInputValidator.test.js +14 -65
- package/package.descriptor.mjs +0 -352
- package/src/server/WorkspacesCoreServiceProvider.js +0 -64
- package/src/server/registerWorkspaceBootstrap.js +0 -27
- package/src/server/registerWorkspaceCore.js +0 -103
- package/src/server/registerWorkspaceRepositories.js +0 -30
- package/src/server/support/workspaceServerScopeSupport.js +0 -18
- package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +0 -19
- package/src/server/workspaceMembers/registerWorkspaceMembers.js +0 -137
- package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +0 -130
- package/src/server/workspaceSettings/registerWorkspaceSettings.js +0 -66
- package/test/packageDescriptor.test.js +0 -91
- package/test/registerServiceRealtimeEvents.test.js +0 -116
- package/test/registerWorkspaceBootstrap.test.js +0 -86
- package/test/registerWorkspaceDirectory.test.js +0 -31
- package/test/registerWorkspaceMembers.test.js +0 -140
- package/test/registerWorkspaceSettings.test.js +0 -40
- package/test/workspaceServerScopeSupport.test.js +0 -131
- /package/{templates/migrations → migrations}/workspaces_core_initial.cjs +0 -0
- /package/{templates/migrations → migrations}/workspaces_core_workspace_settings_single_name_source.cjs +0 -0
- /package/{templates/migrations → migrations}/workspaces_core_workspaces_drop_color.cjs +0 -0
- /package/{templates → patterns/workspace-server/example}/config/roles.js +0 -0
- /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
|
|
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
|
-
|
|
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
|
|
29
|
+
async run(workspacePendingInvitationsService, input, context) {
|
|
24
30
|
return returnJsonApiData(
|
|
25
|
-
await
|
|
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
|
-
|
|
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
|
|
53
|
+
async run(workspacePendingInvitationsService, input, context) {
|
|
48
54
|
return returnJsonApiData({
|
|
49
|
-
pendingInvites: await
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
23
|
-
if (!
|
|
24
|
-
throw new
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
34
|
-
const response = await
|
|
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
|
-
|
|
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
|
|
71
|
+
async run(workspaceSettingsService, input, context) {
|
|
64
72
|
const { workspaceSlug, ...patch } = input;
|
|
65
|
-
const response = await
|
|
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
|
-
|
|
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:
|
|
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/
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|