@jskit-ai/workspaces-core 0.1.14 → 0.1.16

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 (83) hide show
  1. package/package.descriptor.mjs +2 -2
  2. package/package.json +18 -3
  3. package/src/server/WorkspacesCoreServiceProvider.js +41 -2
  4. package/src/server/common/contributors/workspaceActionContextContributor.js +88 -0
  5. package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +34 -0
  6. package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +78 -0
  7. package/src/server/common/formatters/workspaceFormatter.js +53 -0
  8. package/src/server/common/repositories/repositoryUtils.js +59 -0
  9. package/src/server/common/repositories/workspaceInvitesRepository.js +208 -0
  10. package/src/server/common/repositories/workspaceMembershipsRepository.js +190 -0
  11. package/src/server/common/repositories/workspacesRepository.js +202 -0
  12. package/src/server/common/services/workspaceContextService.js +281 -0
  13. package/src/server/common/support/deepFreeze.js +1 -0
  14. package/src/server/common/support/realtimeServiceEvents.js +91 -0
  15. package/src/server/common/support/resolveActionUser.js +9 -0
  16. package/src/server/common/support/workspaceRoutePaths.js +18 -0
  17. package/src/server/common/validators/authenticatedUserValidator.js +43 -0
  18. package/src/server/common/validators/routeParamsValidator.js +62 -0
  19. package/src/server/registerWorkspaceBootstrap.js +27 -0
  20. package/src/server/registerWorkspaceCore.js +100 -0
  21. package/src/server/registerWorkspaceRepositories.js +26 -0
  22. package/src/server/support/resolveWorkspace.js +16 -0
  23. package/src/server/support/workspaceActionSurfaces.js +118 -0
  24. package/src/server/support/workspaceInvitationsPolicy.js +45 -0
  25. package/src/server/support/workspaceRouteInput.js +22 -0
  26. package/src/server/workspaceBootstrapContributor.js +233 -0
  27. package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +133 -0
  28. package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +19 -0
  29. package/src/server/workspaceDirectory/workspaceDirectoryActions.js +133 -0
  30. package/src/server/workspaceMembers/bootWorkspaceMembers.js +236 -0
  31. package/src/server/workspaceMembers/registerWorkspaceMembers.js +108 -0
  32. package/src/server/workspaceMembers/workspaceMembersActions.js +186 -0
  33. package/src/server/workspaceMembers/workspaceMembersService.js +222 -0
  34. package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +62 -0
  35. package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +119 -0
  36. package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +74 -0
  37. package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +138 -0
  38. package/src/server/workspaceSettings/bootWorkspaceSettings.js +76 -0
  39. package/src/server/workspaceSettings/registerWorkspaceSettings.js +62 -0
  40. package/src/server/workspaceSettings/workspaceSettingsActions.js +72 -0
  41. package/src/server/workspaceSettings/workspaceSettingsRepository.js +154 -0
  42. package/src/server/workspaceSettings/workspaceSettingsService.js +66 -0
  43. package/src/shared/operationMessages.js +16 -0
  44. package/src/shared/resources/resolveGlobalArrayRegistry.js +6 -0
  45. package/src/shared/resources/workspaceMembersResource.js +354 -0
  46. package/src/shared/resources/workspacePendingInvitationsResource.js +82 -0
  47. package/src/shared/resources/workspaceResource.js +176 -0
  48. package/src/shared/resources/workspaceSettingsFields.js +59 -0
  49. package/src/shared/resources/workspaceSettingsResource.js +169 -0
  50. package/src/shared/roles.js +161 -0
  51. package/src/shared/settings.js +119 -0
  52. package/src/shared/support/workspacePathModel.js +145 -0
  53. package/src/shared/tenancyMode.js +35 -0
  54. package/src/shared/tenancyProfile.js +73 -0
  55. package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +2 -2
  56. package/test/registerServiceRealtimeEvents.test.js +116 -0
  57. package/test/registerWorkspaceDirectory.test.js +31 -0
  58. package/test/registerWorkspaceSettings.test.js +40 -0
  59. package/test/repositoryContracts.test.js +34 -0
  60. package/test/resourcesCanonical.test.js +74 -0
  61. package/test/roles.test.js +159 -0
  62. package/test/routeParamsValidator.test.js +49 -0
  63. package/test/settingsFieldRegistriesSingleton.test.js +14 -0
  64. package/test/tenancyProfile.test.js +67 -0
  65. package/test/usersRouteResources.test.js +97 -0
  66. package/test/workspaceActionContextContributor.test.js +344 -0
  67. package/test/workspaceActionSurfaces.test.js +85 -0
  68. package/test/workspaceAuthPolicyContextResolver.test.js +119 -0
  69. package/test/workspaceBootstrapContributor.test.js +169 -0
  70. package/test/workspaceInvitationsPolicy.test.js +71 -0
  71. package/test/workspaceInvitesRepository.test.js +111 -0
  72. package/test/workspaceMembersService.test.js +398 -0
  73. package/test/workspacePathModel.test.js +93 -0
  74. package/test/workspacePendingInvitationsResource.test.js +38 -0
  75. package/test/workspacePendingInvitationsService.test.js +151 -0
  76. package/test/workspaceRouteVisibilityResolver.test.js +83 -0
  77. package/test/workspaceService.test.js +546 -0
  78. package/test/workspaceSettingsActions.test.js +52 -0
  79. package/test/workspaceSettingsRepository.test.js +202 -0
  80. package/test/workspaceSettingsResource.test.js +169 -0
  81. package/test/workspaceSettingsService.test.js +140 -0
  82. package/test/workspacesRouteRequestInputValidator.test.js +5 -5
  83. package/test-support/registerDefaultSettingsFields.js +1 -0
@@ -0,0 +1,169 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createWorkspaceBootstrapContributor } from "../src/server/workspaceBootstrapContributor.js";
4
+
5
+ function createAuthenticatedProfile(overrides = {}) {
6
+ return {
7
+ id: "7",
8
+ authProvider: "local",
9
+ authProviderUserSid: "user-7",
10
+ username: "tester",
11
+ displayName: "Test User",
12
+ email: "test@example.com",
13
+ ...overrides
14
+ };
15
+ }
16
+
17
+ test("workspace bootstrap contributor passes actor context to pending invites service", async () => {
18
+ const profile = createAuthenticatedProfile();
19
+ const pendingServiceCalls = [];
20
+ const contributor = createWorkspaceBootstrapContributor({
21
+ workspaceService: {
22
+ async listWorkspacesForUser() {
23
+ return [];
24
+ },
25
+ async resolveWorkspaceContextForUserBySlug() {
26
+ return null;
27
+ }
28
+ },
29
+ workspacePendingInvitationsService: {
30
+ async listPendingInvitesForUser(user, options = {}) {
31
+ pendingServiceCalls.push({
32
+ user,
33
+ options
34
+ });
35
+ return [];
36
+ }
37
+ },
38
+ usersRepository: {
39
+ async findById() {
40
+ return profile;
41
+ }
42
+ },
43
+ workspaceInvitationsEnabled: true,
44
+ appConfig: {
45
+ tenancyMode: "workspaces"
46
+ }
47
+ });
48
+
49
+ await contributor.contribute({
50
+ payload: {
51
+ session: {
52
+ authenticated: true,
53
+ userId: profile.id
54
+ }
55
+ }
56
+ });
57
+
58
+ assert.equal(pendingServiceCalls.length, 1);
59
+ assert.equal(pendingServiceCalls[0].user.id, profile.id);
60
+ assert.equal(pendingServiceCalls[0].options?.context?.actor?.id, profile.id);
61
+ });
62
+
63
+ test("workspace bootstrap contributor resolves workspace slug from bootstrap query", async () => {
64
+ const profile = createAuthenticatedProfile();
65
+ const calls = [];
66
+ const contributor = createWorkspaceBootstrapContributor({
67
+ workspaceService: {
68
+ async listWorkspacesForUser() {
69
+ return [];
70
+ },
71
+ async resolveWorkspaceContextForUserBySlug(_user, workspaceSlug) {
72
+ calls.push(workspaceSlug);
73
+ return null;
74
+ }
75
+ },
76
+ workspacePendingInvitationsService: {
77
+ async listPendingInvitesForUser() {
78
+ return [];
79
+ }
80
+ },
81
+ usersRepository: {
82
+ async findById() {
83
+ return profile;
84
+ }
85
+ },
86
+ workspaceInvitationsEnabled: true,
87
+ appConfig: {
88
+ tenancyMode: "workspaces"
89
+ }
90
+ });
91
+
92
+ const payload = await contributor.contribute({
93
+ query: {
94
+ workspaceSlug: " AcMe "
95
+ },
96
+ payload: {
97
+ session: {
98
+ authenticated: true,
99
+ userId: profile.id
100
+ }
101
+ }
102
+ });
103
+
104
+ assert.deepEqual(calls, ["acme"]);
105
+ assert.deepEqual(payload.requestedWorkspace, {
106
+ slug: "acme",
107
+ status: "resolved"
108
+ });
109
+ });
110
+
111
+ test("workspace bootstrap contributor reports unauthenticated requested workspace without generic bootstrap work", async () => {
112
+ const contributor = createWorkspaceBootstrapContributor({
113
+ workspaceService: {
114
+ async listWorkspacesForUser() {
115
+ assert.fail("listWorkspacesForUser should not run for unauthenticated payloads");
116
+ },
117
+ async resolveWorkspaceContextForUserBySlug() {
118
+ assert.fail("resolveWorkspaceContextForUserBySlug should not run for unauthenticated payloads");
119
+ }
120
+ },
121
+ workspacePendingInvitationsService: {
122
+ async listPendingInvitesForUser() {
123
+ assert.fail("listPendingInvitesForUser should not run for unauthenticated payloads");
124
+ }
125
+ },
126
+ usersRepository: {
127
+ async findById() {
128
+ assert.fail("findById should not run for unauthenticated payloads");
129
+ }
130
+ },
131
+ workspaceInvitationsEnabled: true,
132
+ appConfig: {
133
+ tenancyMode: "workspaces"
134
+ }
135
+ });
136
+
137
+ const payload = await contributor.contribute({
138
+ query: {
139
+ workspaceSlug: "AcMe"
140
+ },
141
+ payload: {
142
+ session: {
143
+ authenticated: false
144
+ }
145
+ }
146
+ });
147
+
148
+ assert.deepEqual(payload, {
149
+ tenancy: {
150
+ mode: "workspaces",
151
+ workspace: {
152
+ enabled: true,
153
+ autoProvision: false,
154
+ allowSelfCreate: false,
155
+ slugPolicy: "user_selected"
156
+ }
157
+ },
158
+ app: {
159
+ features: {
160
+ workspaceSwitching: true,
161
+ workspaceInvites: true
162
+ }
163
+ },
164
+ requestedWorkspace: {
165
+ slug: "acme",
166
+ status: "unauthenticated"
167
+ }
168
+ });
169
+ });
@@ -0,0 +1,71 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { resolveWorkspaceInvitationsPolicy } from "../src/server/support/workspaceInvitationsPolicy.js";
4
+
5
+ test("workspace invitations policy enables invitations by default in personal mode", () => {
6
+ const policy = resolveWorkspaceInvitationsPolicy({
7
+ appConfig: {},
8
+ tenancyProfile: {
9
+ mode: "personal",
10
+ workspace: {
11
+ enabled: true
12
+ }
13
+ }
14
+ });
15
+
16
+ assert.equal(policy.enabled, true);
17
+ assert.equal(policy.allowInPersonalMode, true);
18
+ });
19
+
20
+ test("workspace invitations policy disables invitations in personal mode when explicitly configured", () => {
21
+ const policy = resolveWorkspaceInvitationsPolicy({
22
+ appConfig: {
23
+ workspaceInvitations: {
24
+ allowInPersonalMode: false
25
+ }
26
+ },
27
+ tenancyProfile: {
28
+ mode: "personal",
29
+ workspace: {
30
+ enabled: true
31
+ }
32
+ }
33
+ });
34
+
35
+ assert.equal(policy.enabled, false);
36
+ assert.equal(policy.allowInPersonalMode, false);
37
+ });
38
+
39
+ test("workspace invitations policy disables invitations when workspace mode is disabled", () => {
40
+ const policy = resolveWorkspaceInvitationsPolicy({
41
+ appConfig: {},
42
+ tenancyProfile: {
43
+ mode: "none",
44
+ workspace: {
45
+ enabled: false
46
+ }
47
+ }
48
+ });
49
+
50
+ assert.equal(policy.enabled, false);
51
+ assert.equal(policy.workspaceEnabled, false);
52
+ });
53
+
54
+ test("workspace invitations policy disables invitations when app config disables feature", () => {
55
+ const policy = resolveWorkspaceInvitationsPolicy({
56
+ appConfig: {
57
+ workspaceInvitations: {
58
+ enabled: false,
59
+ allowInPersonalMode: true
60
+ }
61
+ },
62
+ tenancyProfile: {
63
+ mode: "workspace",
64
+ workspace: {
65
+ enabled: true
66
+ }
67
+ }
68
+ });
69
+
70
+ assert.equal(policy.enabled, false);
71
+ });
@@ -0,0 +1,111 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createRepository } from "../src/server/common/repositories/workspaceInvitesRepository.js";
4
+
5
+ function createKnexStub() {
6
+ const state = {
7
+ insertPayload: null
8
+ };
9
+
10
+ const row = {
11
+ id: 1,
12
+ workspace_id: 1,
13
+ email: "invitee@example.com",
14
+ role_sid: "member",
15
+ status: "pending",
16
+ token_hash: "hash",
17
+ invited_by_user_id: 1,
18
+ expires_at: "2026-03-16 00:26:35.709",
19
+ accepted_at: null,
20
+ revoked_at: null,
21
+ created_at: "2026-03-09 00:26:35.710",
22
+ updated_at: "2026-03-09 00:26:35.710"
23
+ };
24
+
25
+ function tableBuilder(tableName) {
26
+ assert.equal(tableName, "workspace_invites");
27
+ return {
28
+ insert(payload) {
29
+ state.insertPayload = payload;
30
+ return Promise.resolve([1]);
31
+ },
32
+ where(criteria) {
33
+ assert.equal(typeof criteria, "object");
34
+ return {
35
+ first() {
36
+ return Promise.resolve({ ...row });
37
+ },
38
+ orderBy() {
39
+ return {
40
+ first() {
41
+ return Promise.resolve({ ...row });
42
+ }
43
+ };
44
+ }
45
+ };
46
+ }
47
+ };
48
+ }
49
+
50
+ return { knexStub: tableBuilder, state };
51
+ }
52
+
53
+ test("workspaceInvitesRepository.insert normalizes expiresAt ISO input to database datetime", async () => {
54
+ const { knexStub, state } = createKnexStub();
55
+ const repository = createRepository(knexStub);
56
+
57
+ await repository.insert({
58
+ workspaceId: "1",
59
+ email: "invitee@example.com",
60
+ roleSid: "member",
61
+ status: "pending",
62
+ tokenHash: "hash",
63
+ invitedByUserId: "1",
64
+ expiresAt: "2026-03-16T00:26:35.709Z"
65
+ });
66
+
67
+ assert.equal(state.insertPayload.expires_at, "2026-03-16 00:26:35.709");
68
+ });
69
+
70
+ test("workspaceInvitesRepository.findPendingByTokenHash reads from invites table without workspace join", async () => {
71
+ const calls = {
72
+ tableName: "",
73
+ whereCriteria: null
74
+ };
75
+ const row = {
76
+ id: 44,
77
+ workspace_id: 9,
78
+ email: "invitee@example.com",
79
+ role_sid: "member",
80
+ status: "pending",
81
+ token_hash: "hash-token",
82
+ invited_by_user_id: 1,
83
+ expires_at: "2030-01-01 00:00:00.000",
84
+ accepted_at: null,
85
+ revoked_at: null,
86
+ created_at: "2026-03-09 00:26:35.710",
87
+ updated_at: "2026-03-09 00:26:35.710"
88
+ };
89
+
90
+ const repository = createRepository((tableName) => {
91
+ calls.tableName = String(tableName || "");
92
+ return {
93
+ where(criteria) {
94
+ calls.whereCriteria = criteria;
95
+ return this;
96
+ },
97
+ first() {
98
+ return Promise.resolve({ ...row });
99
+ }
100
+ };
101
+ });
102
+
103
+ const invite = await repository.findPendingByTokenHash("hash-token");
104
+ assert.equal(calls.tableName, "workspace_invites");
105
+ assert.deepEqual(calls.whereCriteria, {
106
+ token_hash: "hash-token",
107
+ status: "pending"
108
+ });
109
+ assert.equal(invite?.workspaceId, "9");
110
+ assert.equal(invite?.workspaceSlug, undefined);
111
+ });