@jskit-ai/workspaces-core 0.1.137 → 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 (49) hide show
  1. package/package.json +38 -238
  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 +18 -6
  6. package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +25 -4
  7. package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +22 -4
  8. package/src/server/common/services/workspaceContextService.js +7 -2
  9. package/src/server/common/support/realtimeServiceEvents.js +102 -61
  10. package/src/server/support/workspaceActionSurfaces.js +18 -13
  11. package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +5 -11
  12. package/src/server/workspaceDirectory/workspaceDirectoryActions.js +24 -14
  13. package/src/server/workspaceMembers/bootWorkspaceMembers.js +5 -12
  14. package/src/server/workspaceMembers/workspaceMembersActions.js +63 -23
  15. package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +4 -6
  16. package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +31 -12
  17. package/src/server/workspaceSettings/bootWorkspaceSettings.js +5 -8
  18. package/src/server/workspaceSettings/workspaceSettingsActions.js +26 -8
  19. package/src/server/workspaceSettings/workspaceSettingsRepository.js +4 -4
  20. package/test/exportsContract.test.js +2 -1
  21. package/test/featureRuntime.test.js +116 -0
  22. package/test/packageMetadata.test.js +20 -54
  23. package/test/workspaceActionContextContributor.test.js +49 -8
  24. package/test/workspaceActionSurfaces.test.js +36 -1
  25. package/test/workspaceAuthPolicyContextResolver.test.js +61 -0
  26. package/test/workspaceRouteVisibilityResolver.test.js +31 -0
  27. package/test/workspaceService.test.js +62 -0
  28. package/test/workspaceSettingsActions.test.js +102 -16
  29. package/test/workspacesRouteRequestInputValidator.test.js +14 -65
  30. package/src/server/WorkspacesCoreServiceProvider.js +0 -64
  31. package/src/server/registerWorkspaceBootstrap.js +0 -27
  32. package/src/server/registerWorkspaceCore.js +0 -101
  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/registerServiceRealtimeEvents.test.js +0 -116
  40. package/test/registerWorkspaceBootstrap.test.js +0 -86
  41. package/test/registerWorkspaceDirectory.test.js +0 -31
  42. package/test/registerWorkspaceMembers.test.js +0 -140
  43. package/test/registerWorkspaceSettings.test.js +0 -40
  44. package/test/workspaceServerScopeSupport.test.js +0 -131
  45. /package/{templates/migrations → migrations}/workspaces_core_initial.cjs +0 -0
  46. /package/{templates/migrations → migrations}/workspaces_core_workspace_settings_single_name_source.cjs +0 -0
  47. /package/{templates/migrations → migrations}/workspaces_core_workspaces_drop_color.cjs +0 -0
  48. /package/{templates → patterns/workspace-server/example}/config/roles.js +0 -0
  49. /package/{templates → patterns/workspace-server/example}/packages/main/src/server/email/workspaceInviteEmail.js +0 -0
@@ -1,116 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { registerWorkspaceMembers } from "../src/server/workspaceMembers/registerWorkspaceMembers.js";
4
- import { registerWorkspacePendingInvitations } from "../src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js";
5
-
6
- function createAppDouble() {
7
- const serviceCalls = [];
8
-
9
- return {
10
- serviceCalls,
11
- app: {
12
- singleton() {
13
- return this;
14
- },
15
- service(token, factory, metadata) {
16
- serviceCalls.push({
17
- token,
18
- factory,
19
- metadata
20
- });
21
- return this;
22
- },
23
- actions() {
24
- return this;
25
- }
26
- }
27
- };
28
- }
29
-
30
- function findServiceCall(serviceCalls, token) {
31
- return serviceCalls.find((entry) => entry.token === token) || null;
32
- }
33
-
34
- test("workspace register functions publish members/invites/workspace-list realtime events", async () => {
35
- const membersApp = createAppDouble();
36
- registerWorkspaceMembers(membersApp.app);
37
- const members = findServiceCall(membersApp.serviceCalls, "workspaces.members.service");
38
- assert.equal(members?.metadata?.events?.updateMemberRole?.[0]?.realtime?.event, "workspace.members.changed");
39
- assert.equal(members?.metadata?.events?.updateMemberRole?.[1]?.realtime?.event, "users.bootstrap.changed");
40
- assert.equal(members?.metadata?.events?.removeMember?.[0]?.realtime?.event, "workspace.members.changed");
41
- assert.equal(members?.metadata?.events?.removeMember?.[1]?.realtime?.event, "users.bootstrap.changed");
42
- assert.equal(members?.metadata?.events?.createInvite?.[0]?.realtime?.event, "workspace.invites.changed");
43
- assert.equal(members?.metadata?.events?.createInvite?.[1]?.realtime?.event, "users.bootstrap.changed");
44
- assert.equal(members?.metadata?.events?.createInvite?.[1]?.entityId?.({ result: { createdInviteId: "91" } }), "91");
45
- assert.equal(members?.metadata?.events?.createInvite?.[1]?.realtime?.audience?.preset, "event_scope");
46
- assert.equal(typeof members?.metadata?.events?.createInvite?.[1]?.realtime?.audience?.userQuery, "function");
47
- const createInviteAudienceQueryResult = await members?.metadata?.events?.createInvite?.[1]?.realtime?.audience?.userQuery({
48
- knex() {
49
- return {
50
- join() {
51
- return this;
52
- },
53
- where(field, value) {
54
- assert.equal(field, "wi.id");
55
- assert.equal(value, "91");
56
- return this;
57
- },
58
- async first() {
59
- return {
60
- user_id: 55
61
- };
62
- }
63
- };
64
- },
65
- event: {
66
- entityId: "91"
67
- }
68
- });
69
- assert.deepEqual(createInviteAudienceQueryResult, [{ userId: "55" }]);
70
- assert.equal(members?.metadata?.events?.revokeInvite?.[0]?.realtime?.event, "workspace.invites.changed");
71
- assert.equal(members?.metadata?.events?.revokeInvite?.[1]?.realtime?.event, "users.bootstrap.changed");
72
- assert.equal(members?.metadata?.events?.revokeInvite?.[1]?.entityId?.({ result: { revokedInviteId: "19" } }), "19");
73
- assert.equal(members?.metadata?.events?.revokeInvite?.[1]?.realtime?.audience?.preset, "event_scope");
74
- assert.equal(typeof members?.metadata?.events?.revokeInvite?.[1]?.realtime?.audience?.userQuery, "function");
75
-
76
- const pendingApp = createAppDouble();
77
- registerWorkspacePendingInvitations(pendingApp.app);
78
- const pending = findServiceCall(pendingApp.serviceCalls, "workspaces.pending-invitations.service");
79
- const acceptInviteEvents = Array.isArray(pending?.metadata?.events?.acceptInviteByToken)
80
- ? pending.metadata.events.acceptInviteByToken
81
- : [];
82
- const acceptInviteRealtimeEvents = acceptInviteEvents.map((entry) => entry?.realtime?.event).filter(Boolean);
83
- assert.ok(acceptInviteRealtimeEvents.includes("workspace.invitations.pending.changed"));
84
- assert.ok(acceptInviteRealtimeEvents.includes("users.bootstrap.changed"));
85
- assert.ok(acceptInviteRealtimeEvents.includes("workspaces.changed"));
86
- assert.ok(acceptInviteRealtimeEvents.includes("workspace.members.changed"));
87
- assert.ok(acceptInviteRealtimeEvents.includes("workspace.invites.changed"));
88
-
89
- const acceptedMembersChange = acceptInviteEvents.find(
90
- (entry) => entry?.realtime?.event === "workspace.members.changed"
91
- );
92
- assert.equal(acceptedMembersChange?.entityId?.({ result: { workspaceId: "9" } }), "9");
93
- assert.deepEqual(
94
- acceptedMembersChange?.realtime?.audience?.({
95
- event: {
96
- entityId: "9"
97
- }
98
- }),
99
- {
100
- workspaceId: "9"
101
- }
102
- );
103
-
104
- const acceptedInvitesChange = acceptInviteEvents.find(
105
- (entry) => entry?.realtime?.event === "workspace.invites.changed"
106
- );
107
- assert.equal(acceptedInvitesChange?.entityId?.({ result: { workspaceId: "9" } }), "9");
108
-
109
- const refuseInviteEvents = Array.isArray(pending?.metadata?.events?.refuseInviteByToken)
110
- ? pending.metadata.events.refuseInviteByToken
111
- : [];
112
- const refuseInviteRealtimeEvents = refuseInviteEvents.map((entry) => entry?.realtime?.event).filter(Boolean);
113
- assert.ok(refuseInviteRealtimeEvents.includes("workspace.invitations.pending.changed"));
114
- assert.ok(refuseInviteRealtimeEvents.includes("users.bootstrap.changed"));
115
- assert.ok(refuseInviteRealtimeEvents.includes("workspace.invites.changed"));
116
- });
@@ -1,86 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { registerWorkspaceBootstrap } from "../src/server/registerWorkspaceBootstrap.js";
4
-
5
- function createAppDouble() {
6
- const singletons = new Map();
7
- const tags = [];
8
-
9
- return {
10
- singletons,
11
- tags,
12
- app: {
13
- singleton(token, factory) {
14
- singletons.set(token, factory);
15
- return this;
16
- },
17
- tag(token, name) {
18
- tags.push({ token, name });
19
- return this;
20
- }
21
- }
22
- };
23
- }
24
-
25
- test("registerWorkspaceBootstrap resolves the canonical pending invitations service token when enabled", () => {
26
- const { app, singletons, tags } = createAppDouble();
27
- registerWorkspaceBootstrap(app);
28
-
29
- assert.deepEqual(tags, [
30
- {
31
- token: "workspaces.core.bootstrap.payloadContributor",
32
- name: "jskit.runtime.bootstrap.payloadContributors"
33
- }
34
- ]);
35
-
36
- const factory = singletons.get("workspaces.core.bootstrap.payloadContributor");
37
- assert.equal(typeof factory, "function");
38
-
39
- const resolvedTokens = [];
40
- factory({
41
- make(token) {
42
- resolvedTokens.push(token);
43
-
44
- if (token === "workspaces.invitations.enabled") {
45
- return true;
46
- }
47
- if (token === "workspaces.service") {
48
- return {
49
- listWorkspacesForUser() {},
50
- resolveWorkspaceContextForUserBySlug() {}
51
- };
52
- }
53
- if (token === "workspaces.pending-invitations.service") {
54
- return {
55
- listPendingInvitesForUser() {}
56
- };
57
- }
58
- if (token === "internal.repository.user-profiles") {
59
- return {
60
- findById() {}
61
- };
62
- }
63
- if (token === "config") {
64
- return {
65
- tenancyMode: "workspaces"
66
- };
67
- }
68
- if (token === "workspaces.tenancy.profile") {
69
- return {
70
- mode: "workspaces",
71
- workspace: {
72
- enabled: true,
73
- autoProvision: false,
74
- allowSelfCreate: false,
75
- slugPolicy: "user_selected"
76
- }
77
- };
78
- }
79
-
80
- throw new Error(`Unexpected token: ${token}`);
81
- }
82
- });
83
-
84
- assert.ok(resolvedTokens.includes("workspaces.pending-invitations.service"));
85
- assert.ok(!resolvedTokens.includes("users.workspace.pending-invitations.service"));
86
- });
@@ -1,31 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { registerWorkspaceDirectory } from "../src/server/workspaceDirectory/registerWorkspaceDirectory.js";
4
-
5
- function createAppDouble() {
6
- const actionBatches = [];
7
-
8
- return {
9
- actionBatches,
10
- singleton() {},
11
- actions(entries) {
12
- actionBatches.push(Array.isArray(entries) ? entries : [entries]);
13
- }
14
- };
15
- }
16
-
17
- function listActionIds(app) {
18
- return app.actionBatches.flat().map((entry) => String(entry?.id || ""));
19
- }
20
-
21
- test("registerWorkspaceDirectory registers workspace directory actions without resolving runtime tenancy tokens", () => {
22
- const app = createAppDouble();
23
-
24
- registerWorkspaceDirectory(app);
25
- assert.deepEqual(listActionIds(app), [
26
- "workspace.workspaces.create",
27
- "workspace.workspaces.list",
28
- "workspace.workspaces.read",
29
- "workspace.workspaces.update"
30
- ]);
31
- });
@@ -1,140 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { registerWorkspaceMembers } from "../src/server/workspaceMembers/registerWorkspaceMembers.js";
4
-
5
- function createAppDouble() {
6
- const serviceCalls = [];
7
-
8
- return {
9
- serviceCalls,
10
- app: {
11
- singleton() {
12
- return this;
13
- },
14
- service(token, factory, metadata) {
15
- serviceCalls.push({
16
- token,
17
- factory,
18
- metadata
19
- });
20
- return this;
21
- },
22
- actions() {
23
- return this;
24
- }
25
- }
26
- };
27
- }
28
-
29
- function createScopeDouble(bindings = {}) {
30
- return {
31
- has(token) {
32
- return Object.hasOwn(bindings, token);
33
- },
34
- make(token) {
35
- if (!Object.hasOwn(bindings, token)) {
36
- throw new Error(`Missing test binding: ${token}`);
37
- }
38
- const binding = bindings[token];
39
- return typeof binding === "function" && token !== "appConfig" ? binding(this) : binding;
40
- }
41
- };
42
- }
43
-
44
- test("registerWorkspaceMembers uses app-owned workspace invite email template from appConfig", async () => {
45
- const templateCalls = [];
46
- const mailerCalls = [];
47
- const fixture = createAppDouble();
48
- registerWorkspaceMembers(fixture.app);
49
- const serviceCall = fixture.serviceCalls.find((entry) => entry.token === "workspaces.members.service");
50
-
51
- const service = serviceCall.factory(createScopeDouble({
52
- appConfig: {
53
- workspaceMembers: {
54
- defaults: {
55
- inviteExpiresInMs: 60_000
56
- }
57
- },
58
- roleCatalog: {
59
- workspace: {
60
- defaultInviteRole: "member"
61
- },
62
- roles: {
63
- owner: {
64
- assignable: false,
65
- permissions: ["*"]
66
- },
67
- member: {
68
- assignable: true,
69
- permissions: ["workspace.members.view"]
70
- }
71
- }
72
- },
73
- workspaceInviteEmailTemplate(payload) {
74
- templateCalls.push(payload);
75
- return {
76
- subject: "Custom invite",
77
- text: payload.inviteUrl,
78
- html: null
79
- };
80
- }
81
- },
82
- "jskit.env": {
83
- APP_PUBLIC_URL: "https://app.example.test"
84
- },
85
- "workspaces.invitations.enabled": true,
86
- "workspaces.invite.mailer": {
87
- async sendWorkspaceInvite(payload) {
88
- mailerCalls.push(payload);
89
- return {
90
- status: "sent",
91
- message: "sent",
92
- providerMessageId: "msg-custom"
93
- };
94
- }
95
- },
96
- "internal.repository.workspace-memberships": {
97
- async listActiveByWorkspaceId() {
98
- return [];
99
- }
100
- },
101
- "internal.repository.workspace-invites": {
102
- async listPendingByWorkspaceIdWithWorkspace() {
103
- return [];
104
- },
105
- async expirePendingByWorkspaceIdAndEmail() {},
106
- async insert(payload) {
107
- return {
108
- id: "91",
109
- expiresAt: payload.expiresAt
110
- };
111
- },
112
- async findPendingByIdForWorkspace() {
113
- return null;
114
- },
115
- async revokeById() {}
116
- }
117
- }));
118
-
119
- const result = await service.createInvite(
120
- {
121
- id: "7",
122
- slug: "acme",
123
- name: "Acme",
124
- ownerUserId: "9",
125
- avatarUrl: ""
126
- },
127
- { id: "9", email: "owner@example.com" },
128
- {
129
- email: "invitee@example.com",
130
- roleSid: "member"
131
- }
132
- );
133
-
134
- assert.equal(result.inviteDelivery.providerMessageId, "msg-custom");
135
- assert.equal(templateCalls.length, 1);
136
- assert.equal(templateCalls[0].workspace.name, "Acme");
137
- assert.equal(mailerCalls.length, 1);
138
- assert.equal(mailerCalls[0].message.subject, "Custom invite");
139
- assert.match(mailerCalls[0].inviteUrl, /^https:\/\/app\.example\.test\/invite\//);
140
- });
@@ -1,40 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { registerWorkspaceSettings } from "../src/server/workspaceSettings/registerWorkspaceSettings.js";
4
-
5
- test("registerWorkspaceSettings registers workspace settings service realtime event metadata", () => {
6
- const singletonBindings = new Map();
7
- const actionCalls = [];
8
- const serviceCalls = [];
9
-
10
- const app = {
11
- singleton(token, factory) {
12
- singletonBindings.set(token, factory);
13
- return this;
14
- },
15
- service(token, factory, metadata) {
16
- serviceCalls.push({
17
- token,
18
- factory,
19
- metadata
20
- });
21
- return this;
22
- },
23
- actions(definitions) {
24
- actionCalls.push(definitions);
25
- return this;
26
- }
27
- };
28
-
29
- registerWorkspaceSettings(app);
30
-
31
- assert.equal(singletonBindings.has("internal.repository.workspace-settings"), true);
32
- assert.equal(serviceCalls.length, 1);
33
- assert.equal(serviceCalls[0].token, "workspaces.settings.service");
34
- assert.equal(typeof serviceCalls[0].factory, "function");
35
- assert.equal(serviceCalls[0].metadata?.events?.updateWorkspaceSettings?.[0]?.realtime?.event, "workspace.settings.changed");
36
- assert.equal(serviceCalls[0].metadata?.events?.updateWorkspaceSettings?.[0]?.realtime?.audience, "event_scope");
37
- assert.equal(serviceCalls[0].metadata?.events?.updateWorkspaceSettings?.[1]?.realtime?.event, "users.bootstrap.changed");
38
- assert.equal(serviceCalls[0].metadata?.events?.updateWorkspaceSettings?.[1]?.realtime?.audience, "event_scope");
39
- assert.equal(actionCalls.length, 1);
40
- });
@@ -1,131 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { AUTH_POLICY_CONTEXT_RESOLVER_TAG } from "@jskit-ai/auth-core/server/authPolicyContextResolverRegistry";
4
- import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
5
- import { PROFILE_SYNC_LIFECYCLE_CONTRIBUTOR_TAG } from "@jskit-ai/users-core/server/profileSyncLifecycleContributorRegistry";
6
- import { createWorkspaceServerScopeSupport } from "../src/server/support/workspaceServerScopeSupport.js";
7
- import { registerWorkspaceCore } from "../src/server/registerWorkspaceCore.js";
8
-
9
- test("workspace server scope support exposes the canonical workspace helper surface", async () => {
10
- const support = createWorkspaceServerScopeSupport();
11
-
12
- assert.equal(support.available, true);
13
- assert.equal(typeof support.params?.schema, "object");
14
- assert.equal(support.params?.mode, "patch");
15
- assert.deepEqual(
16
- await validateSchemaPayload(support.params, { workspaceSlug: " ACME " }, {
17
- phase: "input",
18
- context: "workspaceServerScopeSupport.params"
19
- }),
20
- {
21
- workspaceSlug: "acme"
22
- }
23
- );
24
- assert.deepEqual(support.buildInputFromRouteParams({ workspaceSlug: " ACME " }), {
25
- workspaceSlug: "acme"
26
- });
27
- assert.deepEqual(
28
- support.resolveWorkspace(
29
- {
30
- requestMeta: {
31
- resolvedWorkspaceContext: {
32
- workspace: {
33
- id: 7,
34
- slug: "acme"
35
- }
36
- }
37
- }
38
- },
39
- {}
40
- ),
41
- {
42
- id: 7,
43
- slug: "acme"
44
- }
45
- );
46
- });
47
-
48
- test("registerWorkspaceCore registers the workspace server scope support token", () => {
49
- const singletons = new Map();
50
- const tags = new Map();
51
- const app = {
52
- singleton(token, factory) {
53
- singletons.set(token, factory);
54
- return this;
55
- },
56
- tag(token, tagName) {
57
- const key = String(tagName || "");
58
- const list = tags.get(key) || [];
59
- list.push(String(token || ""));
60
- tags.set(key, list);
61
- return this;
62
- },
63
- has() {
64
- return false;
65
- }
66
- };
67
-
68
- registerWorkspaceCore(app);
69
-
70
- assert.equal(singletons.has("workspaces.server.scope-support"), true);
71
- assert.deepEqual(tags.get(AUTH_POLICY_CONTEXT_RESOLVER_TAG), ["workspaces.core.authPolicyContextResolver"]);
72
- const support = singletons.get("workspaces.server.scope-support")();
73
- assert.equal(support.available, true);
74
- assert.equal(typeof support.buildInputFromRouteParams, "function");
75
- assert.equal(typeof support.resolveWorkspace, "function");
76
- });
77
-
78
- test("registerWorkspaceCore ensures personal workspace provisioning on every authenticated profile sync", async () => {
79
- const singletons = new Map();
80
- const tags = new Map();
81
- const app = {
82
- singleton(token, factory) {
83
- singletons.set(token, factory);
84
- return this;
85
- },
86
- tag(token, tagName) {
87
- const key = String(tagName || "");
88
- const list = tags.get(key) || [];
89
- list.push(String(token || ""));
90
- tags.set(key, list);
91
- return this;
92
- },
93
- has() {
94
- return false;
95
- }
96
- };
97
-
98
- registerWorkspaceCore(app);
99
-
100
- assert.deepEqual(tags.get(PROFILE_SYNC_LIFECYCLE_CONTRIBUTOR_TAG), ["workspaces.core.profileSyncLifecycleContributor"]);
101
-
102
- const lifecycleFactory = singletons.get("workspaces.core.profileSyncLifecycleContributor");
103
- assert.equal(typeof lifecycleFactory, "function");
104
-
105
- const calls = [];
106
- const contributor = lifecycleFactory({
107
- make(token) {
108
- if (token === "workspaces.service") {
109
- return {
110
- async ensureProvisionedWorkspaceForAuthenticatedUser(profile, options = {}) {
111
- calls.push({ profile, options });
112
- }
113
- };
114
- }
115
- throw new Error(`Unexpected token: ${token}`);
116
- }
117
- });
118
-
119
- await contributor.afterIdentityProfileSynced({
120
- profile: { id: "7", username: "tonymobily" },
121
- created: false,
122
- options: { trx: { id: "tx-1" } }
123
- });
124
-
125
- assert.deepEqual(calls, [
126
- {
127
- profile: { id: "7", username: "tonymobily" },
128
- options: { trx: { id: "tx-1" } }
129
- }
130
- ]);
131
- });