@jskit-ai/workspaces-core 0.1.125 → 0.1.128

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/workspaces-core",
3
- "version": "0.1.125",
3
+ "version": "0.1.128",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -17,14 +17,358 @@
17
17
  "./shared/resources/workspaceSettingsResource": "./src/shared/resources/workspaceSettingsResource.js"
18
18
  },
19
19
  "dependencies": {
20
- "@jskit-ai/auth-core": "0.1.144",
21
- "@jskit-ai/database-runtime": "0.1.145",
22
- "@jskit-ai/http-runtime": "0.1.144",
23
- "@jskit-ai/json-rest-api-core": "0.1.90",
24
- "@jskit-ai/kernel": "0.1.146",
25
- "@jskit-ai/resource-crud-core": "0.1.89",
26
- "@jskit-ai/resource-core": "0.1.89",
27
- "@jskit-ai/users-core": "0.1.159",
28
- "json-rest-schema": "1.x.x"
20
+ "@jskit-ai/auth-core": "0.1.148",
21
+ "@jskit-ai/database-runtime": "0.1.150",
22
+ "@jskit-ai/http-runtime": "0.1.148",
23
+ "@jskit-ai/json-rest-api-core": "0.1.94",
24
+ "@jskit-ai/kernel": "0.1.150",
25
+ "@jskit-ai/resource-core": "0.1.92",
26
+ "@jskit-ai/resource-crud-core": "0.1.92",
27
+ "@jskit-ai/users-core": "0.1.163",
28
+ "json-rest-schema": "^1.0.17"
29
+ },
30
+ "description": "Workspace tenancy runtime plus HTTP routes, role catalog, and workspace config scaffolding.",
31
+ "jskit": {
32
+ "kind": "runtime",
33
+ "capabilities": {
34
+ "provides": [
35
+ "workspaces.core",
36
+ "workspaces.server-routes"
37
+ ],
38
+ "requires": [
39
+ "users.core",
40
+ "json-rest-api.core"
41
+ ]
42
+ },
43
+ "runtime": {
44
+ "server": {
45
+ "providers": [
46
+ {
47
+ "entrypoint": "src/server/WorkspacesCoreServiceProvider.js",
48
+ "export": "WorkspacesCoreServiceProvider"
49
+ }
50
+ ]
51
+ },
52
+ "client": {
53
+ "providers": []
54
+ }
55
+ },
56
+ "metadata": {
57
+ "jskit": {
58
+ "tableOwnership": {
59
+ "tables": [
60
+ {
61
+ "tableName": "workspaces",
62
+ "provenance": "runtime-package",
63
+ "ownerKind": "package-runtime"
64
+ },
65
+ {
66
+ "tableName": "workspace_memberships",
67
+ "provenance": "runtime-package",
68
+ "ownerKind": "package-runtime"
69
+ },
70
+ {
71
+ "tableName": "workspace_settings",
72
+ "provenance": "runtime-package",
73
+ "ownerKind": "package-runtime"
74
+ },
75
+ {
76
+ "tableName": "workspace_invites",
77
+ "provenance": "runtime-package",
78
+ "ownerKind": "package-runtime"
79
+ }
80
+ ]
81
+ }
82
+ },
83
+ "apiSummary": {
84
+ "surfaces": [
85
+ {
86
+ "subpath": "./server",
87
+ "summary": "Exports the workspace runtime provider and workspace route registration surface."
88
+ }
89
+ ],
90
+ "containerTokens": {
91
+ "server": [
92
+ "workspaces.server.scope-support"
93
+ ],
94
+ "client": []
95
+ }
96
+ },
97
+ "server": {
98
+ "routes": [
99
+ {
100
+ "method": "POST",
101
+ "path": "/api/workspaces",
102
+ "summary": "Create a workspace for the authenticated user."
103
+ },
104
+ {
105
+ "method": "GET",
106
+ "path": "/api/workspaces",
107
+ "summary": "List workspaces visible to authenticated user."
108
+ },
109
+ {
110
+ "method": "GET",
111
+ "path": "/api/workspace/invitations/resolve",
112
+ "summary": "Resolve safe public workspace invitation metadata."
113
+ },
114
+ {
115
+ "method": "GET",
116
+ "path": "/api/workspace/invitations/pending",
117
+ "summary": "List pending workspace invitations for authenticated user."
118
+ },
119
+ {
120
+ "method": "POST",
121
+ "path": "/api/workspace/invitations/redeem",
122
+ "summary": "Accept or refuse a workspace invitation using an invite token."
123
+ },
124
+ {
125
+ "method": "GET",
126
+ "path": "/api/w/:workspaceSlug/settings",
127
+ "summary": "Get workspace settings and role catalog by workspace slug."
128
+ },
129
+ {
130
+ "method": "PATCH",
131
+ "path": "/api/w/:workspaceSlug/settings",
132
+ "summary": "Update workspace settings by workspace slug."
133
+ },
134
+ {
135
+ "method": "GET",
136
+ "path": "/api/w/:workspaceSlug/roles",
137
+ "summary": "Get role catalog by workspace slug."
138
+ },
139
+ {
140
+ "method": "GET",
141
+ "path": "/api/w/:workspaceSlug/members",
142
+ "summary": "List members by workspace slug."
143
+ },
144
+ {
145
+ "method": "PATCH",
146
+ "path": "/api/w/:workspaceSlug/members/:memberUserId/role",
147
+ "summary": "Update workspace member role by workspace slug."
148
+ },
149
+ {
150
+ "method": "GET",
151
+ "path": "/api/w/:workspaceSlug/invites",
152
+ "summary": "List workspace invites by workspace slug."
153
+ },
154
+ {
155
+ "method": "POST",
156
+ "path": "/api/w/:workspaceSlug/invites",
157
+ "summary": "Create workspace invite by workspace slug."
158
+ },
159
+ {
160
+ "method": "DELETE",
161
+ "path": "/api/w/:workspaceSlug/invites/:inviteId",
162
+ "summary": "Revoke workspace invite by workspace slug."
163
+ }
164
+ ]
165
+ }
166
+ },
167
+ "mutations": {
168
+ "dependencies": {
169
+ "runtime": {
170
+ "@jskit-ai/json-rest-api-core": "0.1.94",
171
+ "@jskit-ai/resource-core": "0.1.92",
172
+ "@jskit-ai/resource-crud-core": "0.1.92",
173
+ "@jskit-ai/users-core": "0.1.163"
174
+ },
175
+ "dev": {}
176
+ },
177
+ "packageJson": {
178
+ "scripts": {
179
+ "server:app": "SERVER_SURFACE=app node ./bin/server.js",
180
+ "server:admin": "SERVER_SURFACE=admin node ./bin/server.js"
181
+ }
182
+ },
183
+ "procfile": {},
184
+ "files": [
185
+ {
186
+ "op": "install-migration",
187
+ "from": "templates/migrations/workspaces_core_initial.cjs",
188
+ "toDir": "migrations",
189
+ "extension": ".cjs",
190
+ "reason": "Install workspace tenancy schema migration.",
191
+ "category": "migration",
192
+ "id": "workspaces-core-initial-schema"
193
+ },
194
+ {
195
+ "op": "install-migration",
196
+ "from": "templates/migrations/workspaces_core_workspace_settings_single_name_source.cjs",
197
+ "toDir": "migrations",
198
+ "extension": ".cjs",
199
+ "reason": "Remove workspace_settings name/avatar fields so workspace identity data comes from workspaces only.",
200
+ "category": "migration",
201
+ "id": "users-core-workspace-settings-single-name-source"
202
+ },
203
+ {
204
+ "op": "install-migration",
205
+ "from": "templates/migrations/workspaces_core_workspaces_drop_color.cjs",
206
+ "toDir": "migrations",
207
+ "extension": ".cjs",
208
+ "reason": "Drop workspaces.color now that workspace theme colors live in workspace_settings.",
209
+ "category": "migration",
210
+ "id": "users-core-workspaces-drop-color"
211
+ },
212
+ {
213
+ "from": "templates/config/roles.js",
214
+ "to": "config/roles.js",
215
+ "ownership": "app",
216
+ "preserveOnRemove": true,
217
+ "reason": "Install app-owned role catalog in a dedicated config file.",
218
+ "category": "workspaces-core",
219
+ "id": "users-core-app-owned-role-catalog-config"
220
+ },
221
+ {
222
+ "from": "templates/packages/main/src/server/email/workspaceInviteEmail.js",
223
+ "to": "packages/main/src/server/email/workspaceInviteEmail.js",
224
+ "ownership": "app",
225
+ "preserveOnRemove": true,
226
+ "reason": "Install app-owned editable workspace invite email template.",
227
+ "category": "workspaces-core",
228
+ "id": "workspaces-core-main-workspace-invite-email-template"
229
+ }
230
+ ],
231
+ "source": [
232
+ {
233
+ "op": "ensure-import",
234
+ "file": "config/server.js",
235
+ "namedImports": [
236
+ "renderWorkspaceInviteEmail"
237
+ ],
238
+ "from": "../packages/main/src/server/email/workspaceInviteEmail.js",
239
+ "reason": "Load app-owned workspace invite email renderer from packages/main.",
240
+ "category": "workspaces-core",
241
+ "id": "workspaces-core-server-config-workspace-invite-email-import"
242
+ },
243
+ {
244
+ "op": "ensure-assignment",
245
+ "file": "config/server.js",
246
+ "target": "config.workspaceInviteEmailTemplate",
247
+ "value": "renderWorkspaceInviteEmail",
248
+ "reason": "Bind app-owned workspace invite email renderer into server config.",
249
+ "category": "workspaces-core",
250
+ "id": "workspaces-core-server-config-workspace-invite-email-template"
251
+ }
252
+ ],
253
+ "text": [
254
+ {
255
+ "op": "append-text",
256
+ "file": "config/public.js",
257
+ "position": "top",
258
+ "skipIfContains": "import { roleCatalog } from \"./roles.js\";",
259
+ "value": "import { roleCatalog } from \"./roles.js\";\n",
260
+ "reason": "Load app-owned role catalog from dedicated config file.",
261
+ "category": "workspaces-core",
262
+ "id": "users-core-role-catalog-public-import"
263
+ },
264
+ {
265
+ "op": "append-text",
266
+ "file": "config/public.js",
267
+ "position": "top",
268
+ "skipIfContains": "import { surfaceAccessPolicies } from \"./surfaceAccessPolicies.js\";",
269
+ "value": "import { surfaceAccessPolicies } from \"./surfaceAccessPolicies.js\";\n",
270
+ "reason": "Load app-owned surface access policy catalog from dedicated config file.",
271
+ "category": "workspaces-core",
272
+ "id": "users-core-surface-access-policies-public-import"
273
+ },
274
+ {
275
+ "op": "append-text",
276
+ "file": "config/surfaceAccessPolicies.js",
277
+ "position": "top",
278
+ "skipIfContains": "export const surfaceAccessPolicies = {};",
279
+ "value": "export const surfaceAccessPolicies = {};\n\n",
280
+ "reason": "Initialize app-owned surface access policy config if missing.",
281
+ "category": "workspaces-core",
282
+ "id": "users-core-surface-access-policies-config-init"
283
+ },
284
+ {
285
+ "op": "append-text",
286
+ "file": "config/surfaceAccessPolicies.js",
287
+ "position": "bottom",
288
+ "skipIfContains": "surfaceAccessPolicies.workspace_member = {",
289
+ "value": "\nsurfaceAccessPolicies.workspace_member = {\n requireAuth: true,\n requireWorkspaceMembership: true\n};\n",
290
+ "reason": "Register workspace-member surface access policy for workspace surfaces.",
291
+ "category": "workspaces-core",
292
+ "id": "users-core-surface-access-policies-workspace-member"
293
+ },
294
+ {
295
+ "op": "append-text",
296
+ "file": "config/public.js",
297
+ "position": "bottom",
298
+ "skipIfContains": "config.surfaceDefinitions.app = {",
299
+ "value": "\nconfig.surfaceDefinitions.app = {\n id: \"app\",\n label: \"App\",\n pagesRoot: \"w/[workspaceSlug]\",\n enabled: true,\n requiresAuth: true,\n requiresWorkspace: true,\n accessPolicyId: \"workspace_member\",\n origin: \"\"\n};\n\nconfig.surfaceDefinitions.admin = {\n id: \"admin\",\n label: \"Admin\",\n pagesRoot: \"w/[workspaceSlug]/admin\",\n enabled: true,\n requiresAuth: true,\n requiresWorkspace: true,\n accessPolicyId: \"workspace_member\",\n origin: \"\"\n};\n",
300
+ "reason": "Append workspace surface topology when tenancy enables workspace routing.",
301
+ "category": "workspaces-core",
302
+ "id": "users-core-surface-config-workspace",
303
+ "when": {
304
+ "config": "tenancyMode",
305
+ "in": [
306
+ "personal",
307
+ "workspaces"
308
+ ]
309
+ }
310
+ },
311
+ {
312
+ "op": "append-text",
313
+ "file": "config/public.js",
314
+ "position": "bottom",
315
+ "skipIfContains": "config.workspaceSwitching =",
316
+ "value": "\nconfig.workspaceSwitching = true;\nconfig.workspaceInvitations = {\n enabled: true,\n allowInPersonalMode: true\n};\n",
317
+ "reason": "Append default workspace feature toggles into app-owned config.",
318
+ "category": "workspaces-core",
319
+ "id": "users-core-public-config"
320
+ },
321
+ {
322
+ "op": "append-text",
323
+ "file": "config/public.js",
324
+ "position": "bottom",
325
+ "skipIfContains": "config.roleCatalog = roleCatalog;",
326
+ "value": "\nconfig.roleCatalog = roleCatalog;\n",
327
+ "reason": "Bind app-owned role catalog onto public config.",
328
+ "category": "workspaces-core",
329
+ "id": "users-core-role-catalog-public-config"
330
+ },
331
+ {
332
+ "op": "append-text",
333
+ "file": "config/public.js",
334
+ "position": "bottom",
335
+ "skipIfContains": "config.surfaceAccessPolicies = surfaceAccessPolicies;",
336
+ "value": "\nconfig.surfaceAccessPolicies = surfaceAccessPolicies;\n",
337
+ "reason": "Bind app-owned surface access policies onto public config.",
338
+ "category": "workspaces-core",
339
+ "id": "users-core-surface-access-policies-public-config"
340
+ },
341
+ {
342
+ "op": "append-text",
343
+ "file": "config/server.js",
344
+ "position": "bottom",
345
+ "skipIfContains": "config.workspaceColor =",
346
+ "value": "\nconfig.workspaceColor = \"#1867C0\";\n",
347
+ "reason": "Append default workspace server settings into app-owned config.",
348
+ "category": "workspaces-core",
349
+ "id": "users-core-server-config"
350
+ },
351
+ {
352
+ "op": "append-text",
353
+ "file": "config/server.js",
354
+ "position": "bottom",
355
+ "skipIfContains": "config.workspaceSettings =",
356
+ "value": "\nconfig.workspaceSettings = {\n defaults: {\n invitesEnabled: true\n }\n};\n",
357
+ "reason": "Append app-owned workspace settings defaults into the server config.",
358
+ "category": "workspaces-core",
359
+ "id": "users-core-workspace-settings-server-config"
360
+ },
361
+ {
362
+ "op": "append-text",
363
+ "file": "config/server.js",
364
+ "position": "bottom",
365
+ "skipIfContains": "config.workspaceMembers =",
366
+ "value": "\nconfig.workspaceMembers = {\n defaults: {\n inviteExpiresInMs: 604800000\n }\n};\n",
367
+ "reason": "Append app-owned workspace member invite policy defaults into the server config.",
368
+ "category": "workspaces-core",
369
+ "id": "users-core-workspace-members-server-config"
370
+ }
371
+ ]
372
+ }
29
373
  }
30
374
  }
@@ -1,24 +1,11 @@
1
1
  import {
2
2
  normalizeDbRecordId,
3
- toInsertDateTime,
4
- toNullableDateTime,
5
3
  toIsoString,
6
4
  createWithTransaction
7
5
  } from "@jskit-ai/database-runtime/shared";
8
6
  import { isDuplicateEntryError } from "@jskit-ai/database-runtime/shared/duplicateEntry";
9
7
  import { normalizeLowerText, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
10
8
 
11
- function nowDb() {
12
- return toInsertDateTime();
13
- }
14
-
15
- function toNullableIso(value) {
16
- if (!value) {
17
- return null;
18
- }
19
- return toIsoString(value);
20
- }
21
-
22
9
  function uniqueSorted(values) {
23
10
  return [...new Set(values)].sort((left, right) => String(left).localeCompare(String(right)));
24
11
  }
@@ -43,15 +30,12 @@ function toDbJson(value, fallback = {}) {
43
30
  }
44
31
 
45
32
  export {
46
- toNullableDateTime,
47
33
  toIsoString,
48
34
  isDuplicateEntryError,
49
35
  normalizeText,
50
36
  normalizeLowerText,
51
37
  normalizeRecordId,
52
38
  normalizeDbRecordId,
53
- nowDb,
54
- toNullableIso,
55
39
  uniqueSorted,
56
40
  parseJson,
57
41
  toDbJson,
@@ -4,7 +4,6 @@ import {
4
4
  normalizeRecordId,
5
5
  normalizeDbRecordId,
6
6
  normalizeText,
7
- nowDb,
8
7
  isDuplicateEntryError,
9
8
  toIsoString
10
9
  } from "./repositoryUtils.js";
@@ -55,13 +54,13 @@ function normalizeInvitePatchPayload(payload = {}) {
55
54
  : normalizeRecordId(source.invitedByUserId, { fallback: null });
56
55
  }
57
56
  if (Object.hasOwn(source, "expiresAt")) {
58
- normalized.expiresAt = source.expiresAt == null ? null : new Date(source.expiresAt);
57
+ normalized.expiresAt = source.expiresAt == null ? null : toIsoString(source.expiresAt);
59
58
  }
60
59
  if (Object.hasOwn(source, "acceptedAt")) {
61
- normalized.acceptedAt = source.acceptedAt == null ? null : new Date(source.acceptedAt);
60
+ normalized.acceptedAt = source.acceptedAt == null ? null : toIsoString(source.acceptedAt);
62
61
  }
63
62
  if (Object.hasOwn(source, "revokedAt")) {
64
- normalized.revokedAt = source.revokedAt == null ? null : new Date(source.revokedAt);
63
+ normalized.revokedAt = source.revokedAt == null ? null : toIsoString(source.revokedAt);
65
64
  }
66
65
 
67
66
  return normalized;
@@ -208,6 +207,7 @@ function createRepository({ api, knex } = {}) {
208
207
  };
209
208
 
210
209
  try {
210
+ const createdAt = new Date().toISOString();
211
211
  const created = await api.resources.workspaceInvites.post(
212
212
  {
213
213
  inputRecord: createJsonApiInputRecord(
@@ -217,11 +217,11 @@ function createRepository({ api, knex } = {}) {
217
217
  roleSid: createPayload.roleSid,
218
218
  status: createPayload.status,
219
219
  tokenHash: createPayload.tokenHash,
220
- expiresAt: createPayload.expiresAt ?? null,
220
+ expiresAt: createPayload.expiresAt == null ? null : toIsoString(createPayload.expiresAt),
221
221
  acceptedAt: null,
222
222
  revokedAt: null,
223
- createdAt: new Date(),
224
- updatedAt: new Date()
223
+ createdAt,
224
+ updatedAt: createdAt
225
225
  },
226
226
  {
227
227
  relationships: createInviteRelationships({
@@ -281,7 +281,7 @@ function createRepository({ api, knex } = {}) {
281
281
  RESOURCE_TYPE,
282
282
  {
283
283
  status: patch.status,
284
- updatedAt: nowDb()
284
+ updatedAt: new Date().toISOString()
285
285
  },
286
286
  {
287
287
  id: row.id
@@ -300,14 +300,15 @@ function createRepository({ api, knex } = {}) {
300
300
  return;
301
301
  }
302
302
 
303
+ const acceptedAt = new Date().toISOString();
303
304
  await api.resources.workspaceInvites.patch(
304
305
  {
305
306
  inputRecord: createJsonApiInputRecord(
306
307
  RESOURCE_TYPE,
307
308
  {
308
309
  status: "accepted",
309
- acceptedAt: new Date(),
310
- updatedAt: new Date()
310
+ acceptedAt,
311
+ updatedAt: acceptedAt
311
312
  },
312
313
  {
313
314
  id: normalizedInviteId
@@ -325,14 +326,15 @@ function createRepository({ api, knex } = {}) {
325
326
  return;
326
327
  }
327
328
 
329
+ const revokedAt = new Date().toISOString();
328
330
  await api.resources.workspaceInvites.patch(
329
331
  {
330
332
  inputRecord: createJsonApiInputRecord(
331
333
  RESOURCE_TYPE,
332
334
  {
333
335
  status: "revoked",
334
- revokedAt: new Date(),
335
- updatedAt: new Date()
336
+ revokedAt,
337
+ updatedAt: revokedAt
336
338
  },
337
339
  {
338
340
  id: normalizedInviteId
@@ -136,6 +136,7 @@ function createRepository({ api, knex } = {}) {
136
136
  const existing = await findByWorkspaceIdAndUserId(normalizedWorkspaceId, normalizedUserId, options);
137
137
  if (existing) {
138
138
  if (existing.roleSid !== OWNER_ROLE_ID || existing.status !== "active") {
139
+ const updatedAt = new Date().toISOString();
139
140
  await api.resources.workspaceMemberships.patch(
140
141
  {
141
142
  inputRecord: createJsonApiInputRecord(
@@ -143,7 +144,7 @@ function createRepository({ api, knex } = {}) {
143
144
  {
144
145
  roleSid: OWNER_ROLE_ID,
145
146
  status: "active",
146
- updatedAt: new Date()
147
+ updatedAt
147
148
  },
148
149
  {
149
150
  id: existing.id
@@ -158,6 +159,7 @@ function createRepository({ api, knex } = {}) {
158
159
  }
159
160
 
160
161
  try {
162
+ const createdAt = new Date().toISOString();
161
163
  await api.resources.workspaceMemberships.post(
162
164
  {
163
165
  inputRecord: createJsonApiInputRecord(
@@ -165,8 +167,8 @@ function createRepository({ api, knex } = {}) {
165
167
  {
166
168
  roleSid: OWNER_ROLE_ID,
167
169
  status: "active",
168
- createdAt: new Date(),
169
- updatedAt: new Date()
170
+ createdAt,
171
+ updatedAt: createdAt
170
172
  },
171
173
  {
172
174
  relationships: createMembershipRelationships({
@@ -205,6 +207,7 @@ function createRepository({ api, knex } = {}) {
205
207
 
206
208
  if (!existing) {
207
209
  try {
210
+ const createdAt = new Date().toISOString();
208
211
  await api.resources.workspaceMemberships.post(
209
212
  {
210
213
  inputRecord: createJsonApiInputRecord(
@@ -212,8 +215,8 @@ function createRepository({ api, knex } = {}) {
212
215
  {
213
216
  roleSid,
214
217
  status,
215
- createdAt: new Date(),
216
- updatedAt: new Date()
218
+ createdAt,
219
+ updatedAt: createdAt
217
220
  },
218
221
  {
219
222
  relationships: createMembershipRelationships({
@@ -234,6 +237,7 @@ function createRepository({ api, knex } = {}) {
234
237
  return findByWorkspaceIdAndUserId(normalizedWorkspaceId, normalizedUserId, options);
235
238
  }
236
239
 
240
+ const updatedAt = new Date().toISOString();
237
241
  await api.resources.workspaceMemberships.patch(
238
242
  {
239
243
  inputRecord: createJsonApiInputRecord(
@@ -241,7 +245,7 @@ function createRepository({ api, knex } = {}) {
241
245
  {
242
246
  roleSid,
243
247
  status,
244
- updatedAt: new Date()
248
+ updatedAt
245
249
  },
246
250
  {
247
251
  id: existing.id
@@ -137,14 +137,15 @@ function createRepository({ api, knex } = {}) {
137
137
  };
138
138
 
139
139
  try {
140
+ const createdAt = new Date().toISOString();
140
141
  const created = await api.resources.workspaces.post(
141
142
  {
142
143
  inputRecord: createJsonApiInputRecord(
143
144
  RESOURCE_TYPE,
144
145
  {
145
146
  ...createPayload,
146
- createdAt: new Date(),
147
- updatedAt: new Date()
147
+ createdAt,
148
+ updatedAt: createdAt
148
149
  },
149
150
  {
150
151
  relationships: createWorkspaceRelationships({ ownerUserId })
@@ -181,7 +182,7 @@ function createRepository({ api, knex } = {}) {
181
182
  const sourcePatch = patch && typeof patch === "object" && !Array.isArray(patch) ? patch : {};
182
183
  const workspacePatch = {
183
184
  ...sourcePatch,
184
- updatedAt: new Date()
185
+ updatedAt: new Date().toISOString()
185
186
  };
186
187
  const relationships = createWorkspaceRelationships(sourcePatch);
187
188
 
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  normalizeRecordId,
3
- nowDb,
4
3
  isDuplicateEntryError,
5
4
  createWithTransaction
6
5
  } from "../common/repositories/repositoryUtils.js";
@@ -142,7 +141,7 @@ function createRepository({ api, knex } = {}) {
142
141
  RESOURCE_TYPE,
143
142
  {
144
143
  ...updatePayload,
145
- updatedAt: nowDb()
144
+ updatedAt: new Date().toISOString()
146
145
  },
147
146
  {
148
147
  id: normalizedWorkspaceId
@@ -3,33 +3,35 @@ import path from "node:path";
3
3
  import test from "node:test";
4
4
  import { readFile } from "node:fs/promises";
5
5
  import { fileURLToPath } from "node:url";
6
- import descriptor from "../package.descriptor.mjs";
6
+ import packageJson from "../package.json" with { type: "json" };
7
+
8
+ const packageMetadata = packageJson.jskit;
7
9
 
8
10
  const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
9
11
  const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
10
12
 
11
13
  function findRoute(method, routePath) {
12
- const routes = descriptor?.metadata?.server?.routes;
14
+ const routes = packageMetadata?.metadata?.server?.routes;
13
15
  return Array.isArray(routes)
14
16
  ? routes.find((entry) => entry?.method === method && entry?.path === routePath) || null
15
17
  : null;
16
18
  }
17
19
 
18
20
  function findFileMutation(id) {
19
- const fileMutations = descriptor?.mutations?.files;
21
+ const fileMutations = packageMetadata?.mutations?.files;
20
22
  return Array.isArray(fileMutations)
21
23
  ? fileMutations.find((entry) => String(entry?.id || "").trim() === id) || null
22
24
  : null;
23
25
  }
24
26
 
25
27
  function findSourceMutation(id) {
26
- const sourceMutations = descriptor?.mutations?.source;
28
+ const sourceMutations = packageMetadata?.mutations?.source;
27
29
  return Array.isArray(sourceMutations)
28
30
  ? sourceMutations.find((entry) => String(entry?.id || "").trim() === id) || null
29
31
  : null;
30
32
  }
31
33
 
32
- test("workspaces-core descriptor advertises public invite resolution route metadata", () => {
34
+ test("workspaces-core packageMetadata advertises public invite resolution route metadata", () => {
33
35
  assert.deepEqual(findRoute("GET", "/api/workspace/invitations/resolve"), {
34
36
  method: "GET",
35
37
  path: "/api/workspace/invitations/resolve",
@@ -48,8 +48,8 @@ test("workspace memberships derived bodies accept normalized internal writes", a
48
48
  assert.equal(String(create.value.userId), "9");
49
49
  assert.equal(create.value.roleSid, "OWNER");
50
50
  assert.equal(create.value.status, "ACTIVE");
51
- assert.equal(typeof create.value.createdAt, "object");
52
- assert.equal(typeof create.value.updatedAt, "object");
51
+ assert.equal(create.value.createdAt, "2026-05-02T10:11:12.000Z");
52
+ assert.equal(create.value.updatedAt, "2026-05-02T10:11:12.000Z");
53
53
 
54
54
  const patch = await parseBody(workspaceMembershipsResource.operations.patch, {
55
55
  roleSid: "ADMIN",
@@ -59,7 +59,7 @@ test("workspace memberships derived bodies accept normalized internal writes", a
59
59
  assert.equal(patch.ok, true);
60
60
  assert.equal(patch.value.roleSid, "ADMIN");
61
61
  assert.equal(patch.value.status, "ACTIVE");
62
- assert.equal(typeof patch.value.updatedAt, "object");
62
+ assert.equal(patch.value.updatedAt, "2026-05-02T10:11:12.000Z");
63
63
  });
64
64
 
65
65
  test("workspace invites derived bodies keep lifecycle fields available for internal writes", async () => {
@@ -83,8 +83,8 @@ test("workspace invites derived bodies keep lifecycle fields available for inter
83
83
  assert.equal(create.value.status, "PENDING");
84
84
  assert.equal(create.value.tokenHash, "invite-token-hash");
85
85
  assert.equal(String(create.value.invitedByUserId), "9");
86
- assert.equal(typeof create.value.createdAt, "object");
87
- assert.equal(typeof create.value.updatedAt, "object");
86
+ assert.equal(create.value.createdAt, "2026-05-02T10:11:12.000Z");
87
+ assert.equal(create.value.updatedAt, "2026-05-02T10:11:12.000Z");
88
88
 
89
89
  const patch = await parseBody(workspaceInvitesResource.operations.patch, {
90
90
  status: "ACCEPTED",
@@ -93,6 +93,6 @@ test("workspace invites derived bodies keep lifecycle fields available for inter
93
93
  });
94
94
  assert.equal(patch.ok, true);
95
95
  assert.equal(patch.value.status, "ACCEPTED");
96
- assert.equal(typeof patch.value.acceptedAt, "object");
97
- assert.equal(typeof patch.value.updatedAt, "object");
96
+ assert.equal(patch.value.acceptedAt, "2026-05-11T00:00:00.000Z");
97
+ assert.equal(patch.value.updatedAt, "2026-05-11T00:00:00.000Z");
98
98
  });
@@ -233,8 +233,8 @@ test("workspaceInvitesRepository.markAcceptedById uses the internal invite resou
233
233
 
234
234
  const payload = state.patchPayloads[0];
235
235
  assert.equal(payload.attributes?.status, "accepted");
236
- assert.equal(typeof payload.attributes?.acceptedAt, "object");
237
- assert.equal(typeof payload.attributes?.updatedAt, "object");
236
+ assert.equal(typeof payload.attributes?.acceptedAt, "string");
237
+ assert.equal(payload.attributes?.updatedAt, payload.attributes?.acceptedAt);
238
238
  });
239
239
 
240
240
  test("workspaceInvitesRepository.listPendingByWorkspaceIdWithWorkspace keeps workspace join fields outside the base resource contract", async () => {
@@ -191,7 +191,7 @@ test("workspaceMembershipsRepository.ensureOwnerMembership upgrades an existing
191
191
 
192
192
  assert.equal(state.patchPayload.attributes?.roleSid, "owner");
193
193
  assert.equal(state.patchPayload.attributes?.status, "active");
194
- assert.equal(typeof state.patchPayload.attributes?.updatedAt, "object");
194
+ assert.equal(typeof state.patchPayload.attributes?.updatedAt, "string");
195
195
  assert.deepEqual(membership, {
196
196
  id: "11",
197
197
  workspaceId: "7",
@@ -232,8 +232,8 @@ test("workspacesRepository.insert writes canonical fields through json-rest-api"
232
232
  assert.equal(state.postPayload.attributes?.name, "TonyMobily3");
233
233
  assert.equal(state.postPayload.attributes?.isPersonal, false);
234
234
  assert.equal(state.postPayload.attributes?.avatarUrl, "");
235
- assert.equal(typeof state.postPayload.attributes?.createdAt, "object");
236
- assert.equal(typeof state.postPayload.attributes?.updatedAt, "object");
235
+ assert.equal(typeof state.postPayload.attributes?.createdAt, "string");
236
+ assert.equal(state.postPayload.attributes?.updatedAt, state.postPayload.attributes?.createdAt);
237
237
  assert.equal(inserted.id, "1");
238
238
  assert.equal(inserted.ownerUserId, "9");
239
239
  });
@@ -290,7 +290,7 @@ test("workspacesRepository.updateById patches canonical fields and updatedAt", a
290
290
 
291
291
  assert.equal(state.patchPayload.id, "7");
292
292
  assert.equal(state.patchPayload.attributes?.name, "TonyMobily 4");
293
- assert.equal(typeof state.patchPayload.attributes?.updatedAt, "object");
293
+ assert.equal(typeof state.patchPayload.attributes?.updatedAt, "string");
294
294
  });
295
295
 
296
296
  test("workspacesRepository.listForUserId keeps membership fields outside the canonical workspace row", async () => {
@@ -1,352 +0,0 @@
1
- export default Object.freeze({
2
- packageVersion: 1,
3
- packageId: "@jskit-ai/workspaces-core",
4
- version: "0.1.125",
5
- kind: "runtime",
6
- description: "Workspace tenancy runtime plus HTTP routes, role catalog, and workspace config scaffolding.",
7
- dependsOn: [
8
- "@jskit-ai/json-rest-api-core",
9
- "@jskit-ai/resource-core",
10
- "@jskit-ai/resource-crud-core",
11
- "@jskit-ai/users-core"
12
- ],
13
- capabilities: {
14
- provides: [
15
- "workspaces.core",
16
- "workspaces.server-routes"
17
- ],
18
- requires: [
19
- "users.core",
20
- "json-rest-api.core"
21
- ]
22
- },
23
- runtime: {
24
- server: {
25
- providers: [
26
- {
27
- entrypoint: "src/server/WorkspacesCoreServiceProvider.js",
28
- export: "WorkspacesCoreServiceProvider"
29
- }
30
- ]
31
- },
32
- client: {
33
- providers: []
34
- }
35
- },
36
- metadata: {
37
- jskit: {
38
- tableOwnership: {
39
- tables: [
40
- {
41
- tableName: "workspaces",
42
- provenance: "runtime-package",
43
- ownerKind: "package-runtime"
44
- },
45
- {
46
- tableName: "workspace_memberships",
47
- provenance: "runtime-package",
48
- ownerKind: "package-runtime"
49
- },
50
- {
51
- tableName: "workspace_settings",
52
- provenance: "runtime-package",
53
- ownerKind: "package-runtime"
54
- },
55
- {
56
- tableName: "workspace_invites",
57
- provenance: "runtime-package",
58
- ownerKind: "package-runtime"
59
- }
60
- ]
61
- }
62
- },
63
- apiSummary: {
64
- surfaces: [
65
- {
66
- subpath: "./server",
67
- summary: "Exports the workspace runtime provider and workspace route registration surface."
68
- }
69
- ],
70
- containerTokens: {
71
- server: [
72
- "workspaces.server.scope-support"
73
- ],
74
- client: []
75
- }
76
- },
77
- server: {
78
- routes: [
79
- {
80
- method: "POST",
81
- path: "/api/workspaces",
82
- summary: "Create a workspace for the authenticated user."
83
- },
84
- {
85
- method: "GET",
86
- path: "/api/workspaces",
87
- summary: "List workspaces visible to authenticated user."
88
- },
89
- {
90
- method: "GET",
91
- path: "/api/workspace/invitations/resolve",
92
- summary: "Resolve safe public workspace invitation metadata."
93
- },
94
- {
95
- method: "GET",
96
- path: "/api/workspace/invitations/pending",
97
- summary: "List pending workspace invitations for authenticated user."
98
- },
99
- {
100
- method: "POST",
101
- path: "/api/workspace/invitations/redeem",
102
- summary: "Accept or refuse a workspace invitation using an invite token."
103
- },
104
- {
105
- method: "GET",
106
- path: "/api/w/:workspaceSlug/settings",
107
- summary: "Get workspace settings and role catalog by workspace slug."
108
- },
109
- {
110
- method: "PATCH",
111
- path: "/api/w/:workspaceSlug/settings",
112
- summary: "Update workspace settings by workspace slug."
113
- },
114
- {
115
- method: "GET",
116
- path: "/api/w/:workspaceSlug/roles",
117
- summary: "Get role catalog by workspace slug."
118
- },
119
- {
120
- method: "GET",
121
- path: "/api/w/:workspaceSlug/members",
122
- summary: "List members by workspace slug."
123
- },
124
- {
125
- method: "PATCH",
126
- path: "/api/w/:workspaceSlug/members/:memberUserId/role",
127
- summary: "Update workspace member role by workspace slug."
128
- },
129
- {
130
- method: "GET",
131
- path: "/api/w/:workspaceSlug/invites",
132
- summary: "List workspace invites by workspace slug."
133
- },
134
- {
135
- method: "POST",
136
- path: "/api/w/:workspaceSlug/invites",
137
- summary: "Create workspace invite by workspace slug."
138
- },
139
- {
140
- method: "DELETE",
141
- path: "/api/w/:workspaceSlug/invites/:inviteId",
142
- summary: "Revoke workspace invite by workspace slug."
143
- }
144
- ]
145
- }
146
- },
147
- mutations: {
148
- dependencies: {
149
- runtime: {
150
- "@jskit-ai/json-rest-api-core": "0.1.90",
151
- "@jskit-ai/resource-core": "0.1.89",
152
- "@jskit-ai/resource-crud-core": "0.1.89",
153
- "@jskit-ai/users-core": "0.1.159"
154
- },
155
- dev: {}
156
- },
157
- packageJson: {
158
- scripts: {
159
- "server:app": "SERVER_SURFACE=app node ./bin/server.js",
160
- "server:admin": "SERVER_SURFACE=admin node ./bin/server.js"
161
- }
162
- },
163
- procfile: {},
164
- files: [
165
- {
166
- op: "install-migration",
167
- from: "templates/migrations/workspaces_core_initial.cjs",
168
- toDir: "migrations",
169
- extension: ".cjs",
170
- reason: "Install workspace tenancy schema migration.",
171
- category: "migration",
172
- id: "workspaces-core-initial-schema"
173
- },
174
- {
175
- op: "install-migration",
176
- from: "templates/migrations/workspaces_core_workspace_settings_single_name_source.cjs",
177
- toDir: "migrations",
178
- extension: ".cjs",
179
- reason: "Remove workspace_settings name/avatar fields so workspace identity data comes from workspaces only.",
180
- category: "migration",
181
- id: "users-core-workspace-settings-single-name-source"
182
- },
183
- {
184
- op: "install-migration",
185
- from: "templates/migrations/workspaces_core_workspaces_drop_color.cjs",
186
- toDir: "migrations",
187
- extension: ".cjs",
188
- reason: "Drop workspaces.color now that workspace theme colors live in workspace_settings.",
189
- category: "migration",
190
- id: "users-core-workspaces-drop-color"
191
- },
192
- {
193
- from: "templates/config/roles.js",
194
- to: "config/roles.js",
195
- ownership: "app",
196
- preserveOnRemove: true,
197
- reason: "Install app-owned role catalog in a dedicated config file.",
198
- category: "workspaces-core",
199
- id: "users-core-app-owned-role-catalog-config"
200
- },
201
- {
202
- from: "templates/packages/main/src/server/email/workspaceInviteEmail.js",
203
- to: "packages/main/src/server/email/workspaceInviteEmail.js",
204
- ownership: "app",
205
- preserveOnRemove: true,
206
- reason: "Install app-owned editable workspace invite email template.",
207
- category: "workspaces-core",
208
- id: "workspaces-core-main-workspace-invite-email-template"
209
- }
210
- ],
211
- source: [
212
- {
213
- op: "ensure-import",
214
- file: "config/server.js",
215
- namedImports: ["renderWorkspaceInviteEmail"],
216
- from: "../packages/main/src/server/email/workspaceInviteEmail.js",
217
- reason: "Load app-owned workspace invite email renderer from packages/main.",
218
- category: "workspaces-core",
219
- id: "workspaces-core-server-config-workspace-invite-email-import"
220
- },
221
- {
222
- op: "ensure-assignment",
223
- file: "config/server.js",
224
- target: "config.workspaceInviteEmailTemplate",
225
- value: "renderWorkspaceInviteEmail",
226
- reason: "Bind app-owned workspace invite email renderer into server config.",
227
- category: "workspaces-core",
228
- id: "workspaces-core-server-config-workspace-invite-email-template"
229
- }
230
- ],
231
- text: [
232
- {
233
- op: "append-text",
234
- file: "config/public.js",
235
- position: "top",
236
- skipIfContains: "import { roleCatalog } from \"./roles.js\";",
237
- value: "import { roleCatalog } from \"./roles.js\";\n",
238
- reason: "Load app-owned role catalog from dedicated config file.",
239
- category: "workspaces-core",
240
- id: "users-core-role-catalog-public-import"
241
- },
242
- {
243
- op: "append-text",
244
- file: "config/public.js",
245
- position: "top",
246
- skipIfContains: "import { surfaceAccessPolicies } from \"./surfaceAccessPolicies.js\";",
247
- value: "import { surfaceAccessPolicies } from \"./surfaceAccessPolicies.js\";\n",
248
- reason: "Load app-owned surface access policy catalog from dedicated config file.",
249
- category: "workspaces-core",
250
- id: "users-core-surface-access-policies-public-import"
251
- },
252
- {
253
- op: "append-text",
254
- file: "config/surfaceAccessPolicies.js",
255
- position: "top",
256
- skipIfContains: "export const surfaceAccessPolicies = {};",
257
- value: "export const surfaceAccessPolicies = {};\n\n",
258
- reason: "Initialize app-owned surface access policy config if missing.",
259
- category: "workspaces-core",
260
- id: "users-core-surface-access-policies-config-init"
261
- },
262
- {
263
- op: "append-text",
264
- file: "config/surfaceAccessPolicies.js",
265
- position: "bottom",
266
- skipIfContains: "surfaceAccessPolicies.workspace_member = {",
267
- value: "\nsurfaceAccessPolicies.workspace_member = {\n requireAuth: true,\n requireWorkspaceMembership: true\n};\n",
268
- reason: "Register workspace-member surface access policy for workspace surfaces.",
269
- category: "workspaces-core",
270
- id: "users-core-surface-access-policies-workspace-member"
271
- },
272
- {
273
- op: "append-text",
274
- file: "config/public.js",
275
- position: "bottom",
276
- skipIfContains: "config.surfaceDefinitions.app = {",
277
- value:
278
- "\nconfig.surfaceDefinitions.app = {\n id: \"app\",\n label: \"App\",\n pagesRoot: \"w/[workspaceSlug]\",\n enabled: true,\n requiresAuth: true,\n requiresWorkspace: true,\n accessPolicyId: \"workspace_member\",\n origin: \"\"\n};\n\nconfig.surfaceDefinitions.admin = {\n id: \"admin\",\n label: \"Admin\",\n pagesRoot: \"w/[workspaceSlug]/admin\",\n enabled: true,\n requiresAuth: true,\n requiresWorkspace: true,\n accessPolicyId: \"workspace_member\",\n origin: \"\"\n};\n",
279
- reason: "Append workspace surface topology when tenancy enables workspace routing.",
280
- category: "workspaces-core",
281
- id: "users-core-surface-config-workspace",
282
- when: {
283
- config: "tenancyMode",
284
- in: ["personal", "workspaces"]
285
- }
286
- },
287
- {
288
- op: "append-text",
289
- file: "config/public.js",
290
- position: "bottom",
291
- skipIfContains: "config.workspaceSwitching =",
292
- value:
293
- "\nconfig.workspaceSwitching = true;\nconfig.workspaceInvitations = {\n enabled: true,\n allowInPersonalMode: true\n};\n",
294
- reason: "Append default workspace feature toggles into app-owned config.",
295
- category: "workspaces-core",
296
- id: "users-core-public-config"
297
- },
298
- {
299
- op: "append-text",
300
- file: "config/public.js",
301
- position: "bottom",
302
- skipIfContains: "config.roleCatalog = roleCatalog;",
303
- value: "\nconfig.roleCatalog = roleCatalog;\n",
304
- reason: "Bind app-owned role catalog onto public config.",
305
- category: "workspaces-core",
306
- id: "users-core-role-catalog-public-config"
307
- },
308
- {
309
- op: "append-text",
310
- file: "config/public.js",
311
- position: "bottom",
312
- skipIfContains: "config.surfaceAccessPolicies = surfaceAccessPolicies;",
313
- value: "\nconfig.surfaceAccessPolicies = surfaceAccessPolicies;\n",
314
- reason: "Bind app-owned surface access policies onto public config.",
315
- category: "workspaces-core",
316
- id: "users-core-surface-access-policies-public-config"
317
- },
318
- {
319
- op: "append-text",
320
- file: "config/server.js",
321
- position: "bottom",
322
- skipIfContains: "config.workspaceColor =",
323
- value: "\nconfig.workspaceColor = \"#1867C0\";\n",
324
- reason: "Append default workspace server settings into app-owned config.",
325
- category: "workspaces-core",
326
- id: "users-core-server-config"
327
- },
328
- {
329
- op: "append-text",
330
- file: "config/server.js",
331
- position: "bottom",
332
- skipIfContains: "config.workspaceSettings =",
333
- value:
334
- "\nconfig.workspaceSettings = {\n defaults: {\n invitesEnabled: true\n }\n};\n",
335
- reason: "Append app-owned workspace settings defaults into the server config.",
336
- category: "workspaces-core",
337
- id: "users-core-workspace-settings-server-config"
338
- },
339
- {
340
- op: "append-text",
341
- file: "config/server.js",
342
- position: "bottom",
343
- skipIfContains: "config.workspaceMembers =",
344
- value:
345
- "\nconfig.workspaceMembers = {\n defaults: {\n inviteExpiresInMs: 604800000\n }\n};\n",
346
- reason: "Append app-owned workspace member invite policy defaults into the server config.",
347
- category: "workspaces-core",
348
- id: "users-core-workspace-members-server-config"
349
- }
350
- ]
351
- }
352
- });