@sixb/core 0.1.13 → 0.1.15

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 (236) hide show
  1. package/dist/actions/index.js +22 -22
  2. package/dist/agents/index.js +34 -34
  3. package/dist/agents/index.js.map +1 -1
  4. package/dist/agents/streams/index.js +2 -2
  5. package/dist/agents/streams/internal.js +2 -2
  6. package/dist/auth/errors.d.ts +12 -0
  7. package/dist/auth/errors.d.ts.map +1 -1
  8. package/dist/auth/index.d.ts +1 -1
  9. package/dist/auth/index.d.ts.map +1 -1
  10. package/dist/auth/index.js +7 -5
  11. package/dist/auth/index.js.map +1 -1
  12. package/dist/auth/runtime.d.ts.map +1 -1
  13. package/dist/auth/strategy.d.ts +2 -0
  14. package/dist/auth/strategy.d.ts.map +1 -1
  15. package/dist/auth/strategy.js +4 -2
  16. package/dist/auth/strategy.js.map +1 -1
  17. package/dist/auth/types.d.ts +8 -0
  18. package/dist/auth/types.d.ts.map +1 -1
  19. package/dist/authorization/index.js +2 -2
  20. package/dist/bootstrap/index.js +37 -37
  21. package/dist/{chunk-693f3zn8.js → chunk-1jvq82rp.js} +3 -3
  22. package/dist/{chunk-0321g649.js → chunk-2jntasmh.js} +13 -2
  23. package/dist/{chunk-0321g649.js.map → chunk-2jntasmh.js.map} +3 -3
  24. package/dist/{chunk-cvrbhvtr.js → chunk-2tz5vnvz.js} +28 -28
  25. package/dist/{chunk-cvrbhvtr.js.map → chunk-2tz5vnvz.js.map} +1 -1
  26. package/dist/{chunk-898y24dd.js → chunk-3c90jvfj.js} +2 -2
  27. package/dist/{chunk-vn3yh259.js → chunk-4gmng3vf.js} +1 -1
  28. package/dist/{chunk-91wthfwe.js → chunk-4jph1t3v.js} +7 -7
  29. package/dist/{chunk-znw0zvy4.js → chunk-4wn7bxxa.js} +25 -7
  30. package/dist/chunk-4wn7bxxa.js.map +12 -0
  31. package/dist/{chunk-3qpcrcj8.js → chunk-6aawea28.js} +2 -4
  32. package/dist/chunk-6aawea28.js.map +10 -0
  33. package/dist/{chunk-rzkr70gr.js → chunk-6fsr7y93.js} +5 -1
  34. package/dist/{chunk-rzkr70gr.js.map → chunk-6fsr7y93.js.map} +3 -3
  35. package/dist/{chunk-y487ywrv.js → chunk-6q7c68yx.js} +4 -4
  36. package/dist/{chunk-ypj2wqtj.js → chunk-70jmg2zx.js} +2 -2
  37. package/dist/{chunk-s24qbp32.js → chunk-7jc21943.js} +3 -3
  38. package/dist/{chunk-rzcjp5m2.js → chunk-8fjmqjyx.js} +1 -1
  39. package/dist/{chunk-m8zkvep1.js → chunk-8w27xf9e.js} +1 -1
  40. package/dist/{chunk-z9h698s8.js → chunk-a4g5yjm0.js} +1 -1
  41. package/dist/{chunk-gmt22nxx.js → chunk-adbve9zt.js} +1 -1
  42. package/dist/{chunk-mf6hwpzh.js → chunk-dqkd3kdj.js} +41 -23
  43. package/dist/{chunk-mf6hwpzh.js.map → chunk-dqkd3kdj.js.map} +3 -3
  44. package/dist/{chunk-gbcrz0gr.js → chunk-ex8s8wsk.js} +6 -6
  45. package/dist/{chunk-4tnrzqqh.js → chunk-fj1dvp37.js} +1 -1
  46. package/dist/{chunk-gc226ten.js → chunk-fjw417jf.js} +127 -69
  47. package/dist/{chunk-gc226ten.js.map → chunk-fjw417jf.js.map} +4 -4
  48. package/dist/{chunk-ht4a7mjg.js → chunk-g4rbm4a6.js} +42 -8
  49. package/dist/{chunk-ht4a7mjg.js.map → chunk-g4rbm4a6.js.map} +3 -3
  50. package/dist/{chunk-qma80ych.js → chunk-hn2r7z41.js} +3 -2
  51. package/dist/chunk-hn2r7z41.js.map +11 -0
  52. package/dist/{chunk-bdhfn0ww.js → chunk-kkqwzy6h.js} +1 -1
  53. package/dist/{chunk-bep5rrdq.js → chunk-mmac1mp6.js} +2 -2
  54. package/dist/{chunk-w9bp61e7.js → chunk-n5t4mq6z.js} +5 -5
  55. package/dist/{chunk-n33e8dz8.js → chunk-p0nbkys4.js} +1 -1
  56. package/dist/{chunk-04h4t2b5.js → chunk-py2dw8cw.js} +3 -3
  57. package/dist/{chunk-jhyc777j.js → chunk-q3grgzp1.js} +2 -2
  58. package/dist/{chunk-9pfw47qx.js → chunk-qne45xqq.js} +53 -48
  59. package/dist/{chunk-9pfw47qx.js.map → chunk-qne45xqq.js.map} +7 -7
  60. package/dist/{chunk-cbn96bve.js → chunk-r5sc1d2b.js} +2 -2
  61. package/dist/{chunk-y6vnvayj.js → chunk-vjcd8d27.js} +1 -1
  62. package/dist/{chunk-jmwy25mf.js → chunk-wd34wnsy.js} +6 -6
  63. package/dist/{chunk-63fp9x3q.js → chunk-wem5vqjw.js} +7 -7
  64. package/dist/{chunk-darm7eeg.js → chunk-xr7y35yj.js} +3 -3
  65. package/dist/{chunk-a69fkrtr.js → chunk-y0zmae99.js} +5 -5
  66. package/dist/{chunk-xgptywp0.js → chunk-y2aq6ag9.js} +1 -1
  67. package/dist/{chunk-2hbhn1ff.js → chunk-yfppyaty.js} +1 -1
  68. package/dist/{chunk-rqtdtm43.js → chunk-z708gemf.js} +129 -70
  69. package/dist/{chunk-rqtdtm43.js.map → chunk-z708gemf.js.map} +5 -5
  70. package/dist/deploy/define.d.ts +4 -0
  71. package/dist/deploy/define.d.ts.map +1 -0
  72. package/dist/deploy/index.d.ts +3 -0
  73. package/dist/deploy/index.d.ts.map +1 -0
  74. package/dist/deploy/index.js +10 -0
  75. package/dist/deploy/index.js.map +10 -0
  76. package/dist/deploy/internal.d.ts +3 -0
  77. package/dist/deploy/internal.d.ts.map +1 -0
  78. package/dist/deploy/internal.js +244 -0
  79. package/dist/deploy/internal.js.map +11 -0
  80. package/dist/deploy/services.d.ts +8 -0
  81. package/dist/deploy/services.d.ts.map +1 -0
  82. package/dist/deploy/types.d.ts +309 -0
  83. package/dist/deploy/types.d.ts.map +1 -0
  84. package/dist/deploy/validation.d.ts +7 -0
  85. package/dist/deploy/validation.d.ts.map +1 -0
  86. package/dist/error-reporting/internal.js +3 -3
  87. package/dist/errors/catalog.d.ts +4 -0
  88. package/dist/errors/catalog.d.ts.map +1 -1
  89. package/dist/errors/internal.js +1 -1
  90. package/dist/events/index.js +3 -3
  91. package/dist/execution/agent.js +33 -33
  92. package/dist/execution/primitive.js +29 -29
  93. package/dist/execution/request.js +31 -31
  94. package/dist/execution/request.js.map +1 -1
  95. package/dist/index.js +49 -49
  96. package/dist/index.js.map +1 -1
  97. package/dist/maintenance/ontology-maintenance.d.ts +2 -0
  98. package/dist/maintenance/ontology-maintenance.d.ts.map +1 -1
  99. package/dist/maintenance/types.d.ts +8 -2
  100. package/dist/maintenance/types.d.ts.map +1 -1
  101. package/dist/materialization/dataset-watermark.d.ts +6 -1
  102. package/dist/materialization/dataset-watermark.d.ts.map +1 -1
  103. package/dist/materialization/index.js +1 -1
  104. package/dist/materialization/model.d.ts +6 -0
  105. package/dist/materialization/model.d.ts.map +1 -1
  106. package/dist/materializer/index.js +18 -18
  107. package/dist/materializer/index.js.map +1 -1
  108. package/dist/materializer/projections/finish-run.d.ts.map +1 -1
  109. package/dist/materializer/projections/replace.d.ts.map +1 -1
  110. package/dist/materializer/projections/source-materialization.d.ts +6 -2
  111. package/dist/materializer/projections/source-materialization.d.ts.map +1 -1
  112. package/dist/models/execution/index.js +14 -14
  113. package/dist/objects/index.js +21 -21
  114. package/dist/objects/query/index.js +15 -15
  115. package/dist/pipelines/index.js +19 -19
  116. package/dist/projections/internal.js +16 -16
  117. package/dist/projections/types.d.ts +1 -1
  118. package/dist/projections/types.d.ts.map +1 -1
  119. package/dist/queues/in-memory.d.ts +1 -1
  120. package/dist/queues/in-memory.d.ts.map +1 -1
  121. package/dist/runtime/internal.js +19 -19
  122. package/dist/shares/internal.js +14 -14
  123. package/dist/shares/internal.js.map +1 -1
  124. package/dist/storage/action-runs/failure.js +2 -2
  125. package/dist/storage/ai-limits/enforcement.js +2 -2
  126. package/dist/storage/auth/errors.d.ts +1 -1
  127. package/dist/storage/auth/errors.d.ts.map +1 -1
  128. package/dist/storage/auth/in-memory/auth-storage.d.ts +2 -1
  129. package/dist/storage/auth/in-memory/auth-storage.d.ts.map +1 -1
  130. package/dist/storage/auth/types.d.ts +15 -7
  131. package/dist/storage/auth/types.d.ts.map +1 -1
  132. package/dist/storage/connector-connections/provider.js +1 -1
  133. package/dist/storage/file-upload-sessions/provider.js +5 -5
  134. package/dist/storage/index.d.ts +2 -2
  135. package/dist/storage/index.d.ts.map +1 -1
  136. package/dist/storage/index.js +29 -29
  137. package/dist/storage/index.js.map +1 -1
  138. package/dist/storage/internal.js +1 -1
  139. package/dist/storage/ontology/in-memory/sources.d.ts +6 -3
  140. package/dist/storage/ontology/in-memory/sources.d.ts.map +1 -1
  141. package/dist/storage/ontology/index.d.ts +1 -1
  142. package/dist/storage/ontology/index.d.ts.map +1 -1
  143. package/dist/storage/ontology/provider.d.ts +16 -3
  144. package/dist/storage/ontology/provider.d.ts.map +1 -1
  145. package/dist/storage/ontology/provider.js +8 -4
  146. package/dist/storage/ontology/provider.js.map +1 -1
  147. package/dist/storage/ontology/sources.d.ts +36 -8
  148. package/dist/storage/ontology/sources.d.ts.map +1 -1
  149. package/dist/storage/projection-runs/in-memory.d.ts +3 -1
  150. package/dist/storage/projection-runs/in-memory.d.ts.map +1 -1
  151. package/dist/storage/projection-runs/index.d.ts +1 -1
  152. package/dist/storage/projection-runs/index.d.ts.map +1 -1
  153. package/dist/storage/projection-runs/lifecycle.d.ts +12 -0
  154. package/dist/storage/projection-runs/lifecycle.d.ts.map +1 -1
  155. package/dist/storage/projection-runs/provider.js +10 -4
  156. package/dist/storage/projection-runs/provider.js.map +1 -1
  157. package/dist/storage/projection-runs/types.d.ts +32 -1
  158. package/dist/storage/projection-runs/types.d.ts.map +1 -1
  159. package/dist/storage/workflow-runs/provider.js +5 -5
  160. package/dist/syncs/index.js +20 -20
  161. package/dist/testing/auth-storage-contract.d.ts.map +1 -1
  162. package/dist/testing/index.js +381 -171
  163. package/dist/testing/index.js.map +5 -5
  164. package/dist/testing/ontology-storage-contract.d.ts.map +1 -1
  165. package/dist/testing/projection-run-storage-contract.d.ts.map +1 -1
  166. package/dist/webhooks/internal.js +15 -15
  167. package/dist/webhooks/internal.js.map +1 -1
  168. package/dist/workers/index.js +1 -1
  169. package/dist/workflows/index.js +34 -34
  170. package/package.json +13 -1
  171. package/src/auth/errors.ts +23 -0
  172. package/src/auth/index.ts +1 -1
  173. package/src/auth/runtime.ts +56 -22
  174. package/src/auth/strategy.ts +2 -0
  175. package/src/auth/types.ts +8 -0
  176. package/src/deploy/define.ts +6 -0
  177. package/src/deploy/index.ts +41 -0
  178. package/src/deploy/internal.ts +7 -0
  179. package/src/deploy/services.ts +51 -0
  180. package/src/deploy/types.ts +338 -0
  181. package/src/deploy/validation.ts +261 -0
  182. package/src/errors/catalog.ts +4 -0
  183. package/src/maintenance/ontology-maintenance.ts +32 -2
  184. package/src/maintenance/types.ts +8 -2
  185. package/src/materialization/dataset-watermark.ts +6 -4
  186. package/src/materialization/model.ts +7 -0
  187. package/src/materializer/README.md +26 -6
  188. package/src/materializer/projections/finish-run.ts +129 -26
  189. package/src/materializer/projections/replace.ts +67 -49
  190. package/src/materializer/projections/source-materialization.ts +11 -6
  191. package/src/projections/types.ts +1 -0
  192. package/src/storage/auth/errors.ts +0 -1
  193. package/src/storage/auth/in-memory/auth-storage.ts +44 -65
  194. package/src/storage/auth/types.ts +15 -7
  195. package/src/storage/index.ts +6 -1
  196. package/src/storage/ontology/in-memory/sources.ts +119 -55
  197. package/src/storage/ontology/index.ts +4 -1
  198. package/src/storage/ontology/provider.ts +64 -7
  199. package/src/storage/ontology/sources.ts +42 -8
  200. package/src/storage/projection-runs/in-memory.ts +41 -2
  201. package/src/storage/projection-runs/index.ts +2 -0
  202. package/src/storage/projection-runs/lifecycle.ts +61 -4
  203. package/src/storage/projection-runs/types.ts +41 -1
  204. package/src/testing/auth-storage-contract.ts +104 -130
  205. package/src/testing/ontology-storage-contract.ts +141 -15
  206. package/src/testing/projection-run-storage-contract.ts +174 -1
  207. package/dist/chunk-3qpcrcj8.js.map +0 -10
  208. package/dist/chunk-qma80ych.js.map +0 -11
  209. package/dist/chunk-znw0zvy4.js.map +0 -12
  210. /package/dist/{chunk-693f3zn8.js.map → chunk-1jvq82rp.js.map} +0 -0
  211. /package/dist/{chunk-898y24dd.js.map → chunk-3c90jvfj.js.map} +0 -0
  212. /package/dist/{chunk-vn3yh259.js.map → chunk-4gmng3vf.js.map} +0 -0
  213. /package/dist/{chunk-91wthfwe.js.map → chunk-4jph1t3v.js.map} +0 -0
  214. /package/dist/{chunk-y487ywrv.js.map → chunk-6q7c68yx.js.map} +0 -0
  215. /package/dist/{chunk-ypj2wqtj.js.map → chunk-70jmg2zx.js.map} +0 -0
  216. /package/dist/{chunk-s24qbp32.js.map → chunk-7jc21943.js.map} +0 -0
  217. /package/dist/{chunk-rzcjp5m2.js.map → chunk-8fjmqjyx.js.map} +0 -0
  218. /package/dist/{chunk-m8zkvep1.js.map → chunk-8w27xf9e.js.map} +0 -0
  219. /package/dist/{chunk-z9h698s8.js.map → chunk-a4g5yjm0.js.map} +0 -0
  220. /package/dist/{chunk-gmt22nxx.js.map → chunk-adbve9zt.js.map} +0 -0
  221. /package/dist/{chunk-gbcrz0gr.js.map → chunk-ex8s8wsk.js.map} +0 -0
  222. /package/dist/{chunk-4tnrzqqh.js.map → chunk-fj1dvp37.js.map} +0 -0
  223. /package/dist/{chunk-bdhfn0ww.js.map → chunk-kkqwzy6h.js.map} +0 -0
  224. /package/dist/{chunk-bep5rrdq.js.map → chunk-mmac1mp6.js.map} +0 -0
  225. /package/dist/{chunk-w9bp61e7.js.map → chunk-n5t4mq6z.js.map} +0 -0
  226. /package/dist/{chunk-n33e8dz8.js.map → chunk-p0nbkys4.js.map} +0 -0
  227. /package/dist/{chunk-04h4t2b5.js.map → chunk-py2dw8cw.js.map} +0 -0
  228. /package/dist/{chunk-jhyc777j.js.map → chunk-q3grgzp1.js.map} +0 -0
  229. /package/dist/{chunk-cbn96bve.js.map → chunk-r5sc1d2b.js.map} +0 -0
  230. /package/dist/{chunk-y6vnvayj.js.map → chunk-vjcd8d27.js.map} +0 -0
  231. /package/dist/{chunk-jmwy25mf.js.map → chunk-wd34wnsy.js.map} +0 -0
  232. /package/dist/{chunk-63fp9x3q.js.map → chunk-wem5vqjw.js.map} +0 -0
  233. /package/dist/{chunk-darm7eeg.js.map → chunk-xr7y35yj.js.map} +0 -0
  234. /package/dist/{chunk-a69fkrtr.js.map → chunk-y0zmae99.js.map} +0 -0
  235. /package/dist/{chunk-xgptywp0.js.map → chunk-y2aq6ag9.js.map} +0 -0
  236. /package/dist/{chunk-2hbhn1ff.js.map → chunk-yfppyaty.js.map} +0 -0
@@ -7,6 +7,7 @@ import type {
7
7
  CompleteOidcSignInInput,
8
8
  CompleteSignInResult,
9
9
  GroupMembershipRecord,
10
+ GroupMembershipSource,
10
11
  InvitationRecord,
11
12
  SessionRecord,
12
13
  SuspendUserAndRevokeSessionsInput,
@@ -42,6 +43,7 @@ import {
42
43
  normalizeEmail,
43
44
  normalizeGroupIds,
44
45
  oidcAttemptKey,
46
+ removeGroupMembershipRecord,
45
47
  revokeActiveSessionsForUser,
46
48
  upsertGroupMembershipRecord,
47
49
  userKey,
@@ -219,25 +221,6 @@ export class InMemoryAuthStorage implements AuthStorage {
219
221
  )
220
222
  }
221
223
 
222
- if (
223
- shouldCreateUser &&
224
- !activeInvitation &&
225
- input.allowUserCreationWithoutInvitation &&
226
- input.requireNoActiveUsersForUserCreation &&
227
- hasActiveUsers(this.state, projectId)
228
- ) {
229
- consumeMagicLinkRecord(this.state, {
230
- projectId,
231
- id: input.magicLinkId,
232
- tokenHash: input.tokenHash,
233
- consumedAt: completedAt,
234
- })
235
- throw new AuthStorageError(
236
- "user_creation_not_allowed",
237
- `[Sixb] Magic link '${input.magicLinkId}' cannot create a user for project '${projectId}'.`
238
- )
239
- }
240
-
241
224
  validateCompleteSessionInput(this.state, projectId, input.session)
242
225
  assertSignInSessionAudience(projectId, input.session.audience, magicLink.audience)
243
226
 
@@ -280,8 +263,9 @@ export class InMemoryAuthStorage implements AuthStorage {
280
263
  projectId,
281
264
  user,
282
265
  })
283
- const memberships = this.applyManualGroups({
266
+ const memberships = this.applyGroups({
284
267
  completedAt,
268
+ source: "manual",
285
269
  groupIds: manualGroupIds,
286
270
  projectId,
287
271
  userId: user.id,
@@ -352,14 +336,6 @@ export class InMemoryAuthStorage implements AuthStorage {
352
336
  )
353
337
  }
354
338
 
355
- if (!identity && user && (!input.autoLinkByVerifiedEmail || !input.emailVerified)) {
356
- this.consumeOidcAttempt(input, completedAt, projectId)
357
- throw new AuthStorageError(
358
- "email_link_not_allowed",
359
- `[Sixb] OIDC identity cannot auto-link to user '${user.id}' for project '${projectId}'.`
360
- )
361
- }
362
-
363
339
  if (user?.status === "suspended") {
364
340
  this.consumeOidcAttempt(input, completedAt, projectId)
365
341
  throw new AuthStorageError(
@@ -368,14 +344,6 @@ export class InMemoryAuthStorage implements AuthStorage {
368
344
  )
369
345
  }
370
346
 
371
- if (shouldCreateUser && !input.emailVerified) {
372
- this.consumeOidcAttempt(input, completedAt, projectId)
373
- throw new AuthStorageError(
374
- "user_creation_not_allowed",
375
- `[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`
376
- )
377
- }
378
-
379
347
  if (shouldCreateUser && !activeInvitation && !input.allowUserCreationWithoutInvitation) {
380
348
  this.consumeOidcAttempt(input, completedAt, projectId)
381
349
  throw new AuthStorageError(
@@ -384,20 +352,6 @@ export class InMemoryAuthStorage implements AuthStorage {
384
352
  )
385
353
  }
386
354
 
387
- if (
388
- shouldCreateUser &&
389
- !activeInvitation &&
390
- input.allowUserCreationWithoutInvitation &&
391
- input.requireNoActiveUsersForUserCreation &&
392
- hasActiveUsers(this.state, projectId)
393
- ) {
394
- this.consumeOidcAttempt(input, completedAt, projectId)
395
- throw new AuthStorageError(
396
- "user_creation_not_allowed",
397
- `[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`
398
- )
399
- }
400
-
401
355
  validateCompleteSessionInput(this.state, projectId, input.session)
402
356
  assertSignInSessionAudience(projectId, input.session.audience, attempt.audience)
403
357
 
@@ -434,12 +388,19 @@ export class InMemoryAuthStorage implements AuthStorage {
434
388
  projectId,
435
389
  user,
436
390
  })
437
- const memberships = this.applyManualGroups({
391
+ const memberships = this.syncDirectoryGroups({
438
392
  completedAt,
439
- groupIds: manualGroupIds,
393
+ groupIds: input.directoryGroupIds,
440
394
  projectId,
441
395
  userId: user.id,
442
- existing: invitation.memberships,
396
+ existing: this.applyGroups({
397
+ completedAt,
398
+ source: "manual",
399
+ groupIds: manualGroupIds,
400
+ projectId,
401
+ userId: user.id,
402
+ existing: invitation.memberships,
403
+ }),
443
404
  })
444
405
 
445
406
  const nextIdentity: UserIdentityRecord = {
@@ -533,10 +494,11 @@ export class InMemoryAuthStorage implements AuthStorage {
533
494
  return { invitation, memberships }
534
495
  }
535
496
 
536
- private applyManualGroups(input: {
497
+ private applyGroups(input: {
537
498
  readonly completedAt: Date
538
499
  readonly groupIds: readonly string[]
539
500
  readonly projectId: string
501
+ readonly source: GroupMembershipSource
540
502
  readonly userId: string
541
503
  readonly existing: readonly GroupMembershipRecord[]
542
504
  }): readonly GroupMembershipRecord[] {
@@ -547,13 +509,40 @@ export class InMemoryAuthStorage implements AuthStorage {
547
509
  projectId: input.projectId,
548
510
  userId: input.userId,
549
511
  groupId,
550
- source: "manual",
512
+ source: input.source,
551
513
  createdAt: input.completedAt,
552
514
  })
553
515
  )
554
516
  }
555
517
  return memberships
556
518
  }
519
+ // Directory memberships become exactly `groupIds`. A group the user already holds from another
520
+ // source keeps that source, and memberships from other sources are never removed.
521
+ private syncDirectoryGroups(input: {
522
+ readonly completedAt: Date
523
+ readonly groupIds?: readonly string[]
524
+ readonly projectId: string
525
+ readonly userId: string
526
+ readonly existing: readonly GroupMembershipRecord[]
527
+ }): readonly GroupMembershipRecord[] {
528
+ if (!input.groupIds) {
529
+ return input.existing
530
+ }
531
+
532
+ const groupIds = normalizeGroupIds(input.groupIds)
533
+ const stale = [...this.state.groupMemberships.values()].filter(
534
+ (membership) =>
535
+ membership.projectId === input.projectId &&
536
+ membership.userId === input.userId &&
537
+ membership.source === "directory" &&
538
+ !groupIds.includes(membership.groupId)
539
+ )
540
+ for (const membership of stale) {
541
+ removeGroupMembershipRecord(this.state, membership)
542
+ }
543
+
544
+ return this.applyGroups({ ...input, groupIds, source: "directory" })
545
+ }
557
546
 
558
547
  private createSignInSession(input: {
559
548
  readonly projectId: string
@@ -599,16 +588,6 @@ function restoreMap<TKey, TValue>(target: Map<TKey, TValue>, snapshot: Map<TKey,
599
588
  }
600
589
  }
601
590
 
602
- function hasActiveUsers(state: AuthStorageState, projectId: string): boolean {
603
- for (const user of state.users.values()) {
604
- if (user.projectId === projectId && user.status === "active") {
605
- return true
606
- }
607
- }
608
-
609
- return false
610
- }
611
-
612
591
  function assertSignInSessionAudience(
613
592
  projectId: string,
614
593
  sessionAudience: string,
@@ -4,7 +4,8 @@ import type { AuthorizablePrincipal } from "../../execution/types"
4
4
  export type UserStatus = "active" | "suspended"
5
5
  export type ServiceAccountStatus = "active" | "suspended"
6
6
  export type InvitationStatus = "pending" | "accepted" | "revoked"
7
- export type GroupMembershipSource = "invitation" | "manual" | "agent"
7
+ /** `directory`: owned by the identity provider, and synced to its groups on every sign-in. */
8
+ export type GroupMembershipSource = "invitation" | "manual" | "agent" | "directory"
8
9
  export type DeviceAuthorizationStatus = "pending" | "approved" | "denied" | "consumed"
9
10
  export const MAX_PENDING_DEVICE_AUTHORIZATIONS = 100
10
11
 
@@ -45,7 +46,8 @@ export interface ServiceAccountGroupMembershipRecord {
45
46
  readonly projectId: string
46
47
  readonly serviceAccountId: string
47
48
  readonly groupId: string
48
- readonly source: GroupMembershipSource
49
+ /** Service accounts never sign in, so no identity provider owns their memberships. */
50
+ readonly source: Exclude<GroupMembershipSource, "directory">
49
51
  readonly createdAt: Date
50
52
  }
51
53
 
@@ -269,7 +271,7 @@ export interface UpsertAuthServiceAccountGroupMembershipInput {
269
271
  readonly projectId: string
270
272
  readonly serviceAccountId: string
271
273
  readonly groupId: string
272
- readonly source: GroupMembershipSource
274
+ readonly source: ServiceAccountGroupMembershipRecord["source"]
273
275
  readonly createdAt?: Date
274
276
  }
275
277
 
@@ -431,7 +433,6 @@ export interface CompleteMagicLinkSignInInput {
431
433
  readonly newUserDisplayName?: string
432
434
  readonly newUserAvatarUrl?: string
433
435
  readonly allowUserCreationWithoutInvitation?: boolean
434
- readonly requireNoActiveUsersForUserCreation?: boolean
435
436
  readonly manualGroupIds?: readonly string[]
436
437
  readonly session: CompleteAuthSessionInput
437
438
  }
@@ -442,15 +443,22 @@ export interface CompleteOidcSignInInput {
442
443
  readonly stateHash: string
443
444
  readonly completedAt: Date
444
445
  readonly subject: string
446
+ /**
447
+ * An address the strategy has already decided to trust. Without an identity for `subject`, it
448
+ * links the user who has this email, or claims this email's invitation for a new user.
449
+ */
445
450
  readonly email: string
446
- readonly emailVerified?: boolean
447
451
  readonly displayName?: string
448
452
  readonly avatarUrl?: string
449
453
  readonly claims?: Readonly<Record<string, unknown>>
450
- readonly autoLinkByVerifiedEmail?: boolean
451
454
  readonly allowUserCreationWithoutInvitation?: boolean
452
- readonly requireNoActiveUsersForUserCreation?: boolean
453
455
  readonly manualGroupIds?: readonly string[]
456
+ /**
457
+ * The groups the identity provider grants this user. When present, the user's `directory`
458
+ * memberships become exactly these; memberships from any other source are never touched, and a
459
+ * group the user already has from another source keeps it. Omitted, nothing is synced.
460
+ */
461
+ readonly directoryGroupIds?: readonly string[]
454
462
  readonly newUserId: string
455
463
  readonly session: CompleteAuthSessionInput
456
464
  }
@@ -449,8 +449,11 @@ export {
449
449
  snapshotObjectReadExecutionLimits,
450
450
  } from "./objects"
451
451
  export type {
452
+ AbandonRunSourceMaterializationInput,
452
453
  AbandonSourceMaterializationCandidateInput,
453
454
  AbandonSourceMaterializationInput,
455
+ AdoptedSourceMaterialization,
456
+ AdoptSourceMaterializationInput,
454
457
  ApplyMaterializationChunkInput,
455
458
  ApplyMaterializationResult,
456
459
  AssertSourceMaterializationExecution,
@@ -530,9 +533,9 @@ export type {
530
533
  OntologyStorage,
531
534
  OntologyVectorStorage,
532
535
  ProjectionOntologyCommitIntent,
536
+ PurgeAbandonedSourceMaterializationsInput,
533
537
  PurgePublishedOntologyOutboxInput,
534
538
  ReadMaterializationObjectExistenceInput,
535
- ReclaimSourceMaterializationInput,
536
539
  RescheduleOntologyOutboxLeaseInput,
537
540
  SourceActivationWrite,
538
541
  SourceReplacementLinkState,
@@ -595,6 +598,7 @@ export {
595
598
  export type {
596
599
  AdvanceProjectionTelemetryCheckpointInput,
597
600
  FailProjectionRunEnqueueInput,
601
+ FindSupersedingProjectionRunInput,
598
602
  FinishProjectionRunInput,
599
603
  LinkProjectionRunRecord,
600
604
  LinkProjectionTarget,
@@ -616,6 +620,7 @@ export type {
616
620
  ProjectionTarget,
617
621
  ProjectionTelemetryCheckpoint,
618
622
  QueueProjectionRunInput,
623
+ RecordProjectionAttemptFailureInput,
619
624
  RecordProjectionMissingTargetInput,
620
625
  StartOrReclaimProjectionRunInput,
621
626
  TelemetryProjectionRunRecord,
@@ -16,8 +16,11 @@ import {
16
16
  sourceStageRoots,
17
17
  } from "../provider"
18
18
  import type {
19
+ AbandonRunSourceMaterializationInput,
19
20
  AbandonSourceMaterializationCandidateInput,
20
21
  AbandonSourceMaterializationInput,
22
+ AdoptedSourceMaterialization,
23
+ AdoptSourceMaterializationInput,
21
24
  AssertSourceMaterializationExecution,
22
25
  BeginSourceMaterializationInput,
23
26
  CleanupTerminalSourceMaterializationsInput,
@@ -26,7 +29,7 @@ import type {
26
29
  MarkSourceMaterializationReadyInput,
27
30
  OntologySourceRecord,
28
31
  OntologySourceStorage,
29
- ReclaimSourceMaterializationInput,
32
+ PurgeAbandonedSourceMaterializationsInput,
30
33
  StageSourceAssertion,
31
34
  StageSourceRowsInput,
32
35
  StageSourceRowsResult,
@@ -38,6 +41,7 @@ import {
38
41
  assertNonblank,
39
42
  type InMemoryOntologyState,
40
43
  type InMemorySourceMaterialization,
44
+ type InMemorySourceRoot,
41
45
  insertBounded,
42
46
  sourceMaterializationKey,
43
47
  sourceMaterializationRecord,
@@ -78,7 +82,7 @@ export class InMemoryOntologySourceStorage implements OntologySourceStorage {
78
82
  )
79
83
  if (nonterminal.length > 0) {
80
84
  throw sourceConflict(
81
- `Projection run '${input.execution.projectionRunId}' already has a nonterminal source materialization; reclaim it before beginning another.`
85
+ `Projection run '${input.execution.projectionRunId}' already has a nonterminal source materialization; adopt it before beginning another.`
82
86
  )
83
87
  }
84
88
 
@@ -191,8 +195,42 @@ export class InMemoryOntologySourceStorage implements OntologySourceStorage {
191
195
  })
192
196
  }
193
197
 
198
+ async adopt(
199
+ input: AdoptSourceMaterializationInput
200
+ ): Promise<AdoptedSourceMaterialization | null> {
201
+ return this.runRootOperation(async () => {
202
+ assertProjectAndSource(input)
203
+ assertSourceExecutionIdentity(input.execution.projectionRunId, input.execution.executionToken)
204
+ assertCanonicalTimestamp(input.adoptedAt, "Source adoptedAt")
205
+ await this.assertCurrentExecution(input)
206
+ const [candidate, ...others] = this.findRunCandidates(
207
+ input.projectId,
208
+ input.source.projectionId,
209
+ input.execution.projectionRunId
210
+ )
211
+ if (!candidate) return null
212
+ if (others.length > 0) throw multipleRunCandidates(input.execution.projectionRunId)
213
+ const [key, materialization] = candidate
214
+ const adopted: InMemorySourceMaterialization = {
215
+ ...materialization,
216
+ executionToken: input.execution.executionToken,
217
+ // Workers' clocks differ; adoption never moves the record back in time.
218
+ updatedAt:
219
+ input.adoptedAt > materialization.updatedAt ? input.adoptedAt : materialization.updatedAt,
220
+ }
221
+ this.state.sourceMaterializations.set(key, adopted)
222
+ return {
223
+ record: sourceMaterializationRecord(adopted),
224
+ resumeStagingOrdinal:
225
+ adopted.status === "ready"
226
+ ? (adopted.rootCount ?? 0)
227
+ : Math.max(0, ...adopted.ordinalRoots.keys()),
228
+ }
229
+ })
230
+ }
231
+
194
232
  async abandon(input: AbandonSourceMaterializationCandidateInput): Promise<OntologySourceRecord>
195
- async abandon(input: ReclaimSourceMaterializationInput): Promise<OntologySourceRecord | null>
233
+ async abandon(input: AbandonRunSourceMaterializationInput): Promise<OntologySourceRecord | null>
196
234
  async abandon(input: AbandonSourceMaterializationInput): Promise<OntologySourceRecord | null> {
197
235
  return this.runRootOperation(async () => {
198
236
  assertProjectAndSource(input)
@@ -201,7 +239,7 @@ export class InMemoryOntologySourceStorage implements OntologySourceStorage {
201
239
  await this.assertCurrentExecution(input)
202
240
  return input.kind === "candidate"
203
241
  ? this.abandonCandidate(input)
204
- : this.abandonForReclaim(input)
242
+ : this.abandonRunCandidate(input)
205
243
  })
206
244
  }
207
245
 
@@ -211,18 +249,14 @@ export class InMemoryOntologySourceStorage implements OntologySourceStorage {
211
249
  return this.runRootOperation(() => {
212
250
  assertNonblank(input.projectId, "Terminal source cleanup project id")
213
251
  assertCanonicalTimestamp(input.terminalBefore, "Terminal source cleanup cutoff")
214
- if (!Number.isSafeInteger(input.limit) || input.limit <= 0) {
215
- throw new MaterializationValidationError(
216
- "Terminal source cleanup limit must be a positive safe integer."
217
- )
218
- }
252
+ assertCleanupLimit(input.limit)
219
253
 
220
254
  const candidates: [string, InMemorySourceMaterialization][] = []
221
255
  for (const entry of this.state.sourceMaterializations.entries()) {
222
256
  const materialization = entry[1]
223
257
  if (
224
258
  materialization.projectId !== input.projectId ||
225
- (materialization.status !== "superseded" && materialization.status !== "abandoned") ||
259
+ materialization.status !== "superseded" ||
226
260
  materialization.terminalAt === null ||
227
261
  materialization.terminalAt >= input.terminalBefore
228
262
  ) {
@@ -239,42 +273,69 @@ export class InMemoryOntologySourceStorage implements OntologySourceStorage {
239
273
  insertBounded(candidates, entry, input.limit, compareTerminalMaterializations)
240
274
  }
241
275
 
242
- let remaining = input.limit
243
- let rowsDeleted = 0
244
- let materializationsDeleted = 0
245
- for (const [key, candidate] of candidates) {
276
+ return this.deleteRetiredRoots(
277
+ candidates,
278
+ input.limit,
279
+ (root) => !root.active && root.retiredAt !== null && root.retiredAt < input.terminalBefore
280
+ )
281
+ })
282
+ }
283
+
284
+ async purgeAbandoned(
285
+ input: PurgeAbandonedSourceMaterializationsInput
286
+ ): Promise<CleanupTerminalSourceMaterializationsResult> {
287
+ return this.runRootOperation(() => {
288
+ assertNonblank(input.projectId, "Abandoned source purge project id")
289
+ assertCleanupLimit(input.limit)
290
+ const abandoned = [...this.state.sourceMaterializations.entries()]
291
+ .filter(
292
+ ([, materialization]) =>
293
+ materialization.projectId === input.projectId && materialization.status === "abandoned"
294
+ )
295
+ .sort(compareTerminalMaterializations)
296
+ return this.deleteRetiredRoots(abandoned, input.limit, () => true)
297
+ })
298
+ }
299
+
300
+ private deleteRetiredRoots(
301
+ candidates: readonly [string, InMemorySourceMaterialization][],
302
+ limit: number,
303
+ retired: (root: InMemorySourceRoot) => boolean
304
+ ): CleanupTerminalSourceMaterializationsResult {
305
+ let remaining = limit
306
+ let rowsDeleted = 0
307
+ let materializationsDeleted = 0
308
+ for (const [key, candidate] of candidates) {
309
+ if (remaining === 0) break
310
+ const current = this.state.sourceMaterializations.get(key)
311
+ if (current !== candidate) continue
312
+ current.rootOrdinals.clear()
313
+ current.ordinalRoots.clear()
314
+ for (const [rootKey, root] of current.roots) {
246
315
  if (remaining === 0) break
247
- const current = this.state.sourceMaterializations.get(key)
248
- if (current !== candidate) continue
249
- current.rootOrdinals.clear()
250
- current.ordinalRoots.clear()
251
- for (const [rootKey, root] of current.roots) {
316
+ if (!retired(root)) continue
317
+ for (const entityKey of root.entityKeys) {
252
318
  if (remaining === 0) break
253
- if (root.active || root.retiredAt === null || root.retiredAt >= input.terminalBefore)
254
- continue
255
- for (const entityKey of root.entityKeys) {
256
- if (remaining === 0) break
257
- current.rowsByEntity.delete(entityKey)
258
- root.entityKeys.delete(entityKey)
259
- rowsDeleted += 1
260
- remaining -= 1
261
- }
262
- if (root.entityKeys.size === 0 && remaining > 0) {
263
- current.roots.delete(rootKey)
264
- current.rootOrdinals.delete(rootKey)
265
- current.ordinalRoots.delete(root.stagingOrdinal)
266
- rowsDeleted += 1
267
- remaining -= 1
268
- }
319
+ current.rowsByEntity.delete(entityKey)
320
+ root.entityKeys.delete(entityKey)
321
+ rowsDeleted += 1
322
+ remaining -= 1
269
323
  }
270
- if (current.roots.size === 0 && current.rowsByEntity.size === 0 && remaining > 0) {
271
- this.state.sourceMaterializations.delete(key)
272
- materializationsDeleted += 1
324
+ if (root.entityKeys.size === 0 && remaining > 0) {
325
+ current.roots.delete(rootKey)
326
+ current.rootOrdinals.delete(rootKey)
327
+ current.ordinalRoots.delete(root.stagingOrdinal)
328
+ rowsDeleted += 1
273
329
  remaining -= 1
274
330
  }
275
331
  }
276
- return { rowsDeleted, materializationsDeleted }
277
- })
332
+ if (current.roots.size === 0 && current.rowsByEntity.size === 0 && remaining > 0) {
333
+ this.state.sourceMaterializations.delete(key)
334
+ materializationsDeleted += 1
335
+ remaining -= 1
336
+ }
337
+ }
338
+ return { rowsDeleted, materializationsDeleted }
278
339
  }
279
340
 
280
341
  async summarizeTerminal(
@@ -356,26 +417,15 @@ export class InMemoryOntologySourceStorage implements OntologySourceStorage {
356
417
  return this.transitionToAbandoned(materialization, input.abandonedAt)
357
418
  }
358
419
 
359
- private abandonForReclaim(input: ReclaimSourceMaterializationInput): OntologySourceRecord | null {
420
+ private abandonRunCandidate(
421
+ input: AbandonRunSourceMaterializationInput
422
+ ): OntologySourceRecord | null {
360
423
  const candidates = this.findRunCandidates(
361
424
  input.projectId,
362
425
  input.source.projectionId,
363
426
  input.execution.projectionRunId
364
427
  )
365
- if (
366
- candidates.some(
367
- ([, materialization]) => materialization.executionToken === input.execution.executionToken
368
- )
369
- ) {
370
- throw sourceConflict(
371
- `Projection run '${input.execution.projectionRunId}' already has a source materialization owned by the current execution.`
372
- )
373
- }
374
- if (candidates.length > 1) {
375
- throw sourceConflict(
376
- `Projection run '${input.execution.projectionRunId}' has multiple nonterminal source materializations.`
377
- )
378
- }
428
+ if (candidates.length > 1) throw multipleRunCandidates(input.execution.projectionRunId)
379
429
  const candidate = candidates[0]?.[1]
380
430
  return candidate ? this.transitionToAbandoned(candidate, input.abandonedAt) : null
381
431
  }
@@ -609,6 +659,20 @@ function assertNotBefore(
609
659
  }
610
660
  }
611
661
 
662
+ function assertCleanupLimit(limit: number): void {
663
+ if (!Number.isSafeInteger(limit) || limit <= 0) {
664
+ throw new MaterializationValidationError(
665
+ "Terminal source cleanup limit must be a positive safe integer."
666
+ )
667
+ }
668
+ }
669
+
670
+ function multipleRunCandidates(projectionRunId: string) {
671
+ return sourceConflict(
672
+ `Projection run '${projectionRunId}' has multiple nonterminal source materializations.`
673
+ )
674
+ }
675
+
612
676
  function compareTerminalMaterializations(
613
677
  [, left]: readonly [string, InMemorySourceMaterialization],
614
678
  [, right]: readonly [string, InMemorySourceMaterialization]
@@ -97,8 +97,11 @@ export type {
97
97
  } from "./outbox"
98
98
  export { ONTOLOGY_OUTBOX_FAILURE_CODES } from "./outbox"
99
99
  export type {
100
+ AbandonRunSourceMaterializationInput,
100
101
  AbandonSourceMaterializationCandidateInput,
101
102
  AbandonSourceMaterializationInput,
103
+ AdoptedSourceMaterialization,
104
+ AdoptSourceMaterializationInput,
102
105
  AssertSourceMaterializationExecution,
103
106
  AssertSourceMaterializationExecutionInput,
104
107
  BeginSourceMaterializationInput,
@@ -109,7 +112,7 @@ export type {
109
112
  OntologySourceMaterializationStatus,
110
113
  OntologySourceRecord,
111
114
  OntologySourceStorage,
112
- ReclaimSourceMaterializationInput,
115
+ PurgeAbandonedSourceMaterializationsInput,
113
116
  StageSourceAssertion,
114
117
  StageSourceRoot,
115
118
  StageSourceRowsInput,
@@ -11,6 +11,8 @@ import type {
11
11
  ExpectedObjectRevision,
12
12
  OntologyLinkRef,
13
13
  OntologyObjectRef,
14
+ ProjectionEntityRef,
15
+ ProjectionSourceAssertion,
14
16
  } from "../../materialization/model"
15
17
  import { linkRefKey, objectRefKey, projectionEntityKey } from "../../materialization/refs"
16
18
  import type { OntologyCommitOriginSelector, OntologyCommitWrite } from "./commits"
@@ -716,7 +718,6 @@ export function sourceStageRow(row: StageSourceAssertion): SourceStageRow {
716
718
  export interface SourceStageRoot {
717
719
  readonly root: StageSourceAssertion["root"]
718
720
  readonly rootKey: string
719
- readonly sortKey: string
720
721
  readonly stagingOrdinal: number
721
722
  readonly deleted: boolean
722
723
  }
@@ -763,7 +764,6 @@ export function sourceStageRoots(
763
764
  roots.set(key, {
764
765
  root: row.root,
765
766
  rootKey: key,
766
- sortKey: utf8SortKey(key),
767
767
  stagingOrdinal: row.stagingOrdinal,
768
768
  deleted,
769
769
  })
@@ -965,12 +965,69 @@ export function sourceEntityColumns(entity: StageSourceAssertion["root"]): {
965
965
  }
966
966
  }
967
967
 
968
- export function utf8SortKey(canonicalKey: string): string {
969
- let key = ""
970
- for (const byte of new TextEncoder().encode(canonicalKey)) {
971
- key += byte.toString(16).padStart(2, "0")
968
+ /** The inverse of `projectionEntityKey`, for providers that store only the canonical key. */
969
+ export function sourceEntityFromKey(key: string): ProjectionEntityRef {
970
+ const parts: unknown = JSON.parse(key)
971
+ if (Array.isArray(parts) && parts.every((part) => typeof part === "string")) {
972
+ if (parts[0] === "object" && parts.length === 3) {
973
+ return { kind: "object", ref: { objectTypeId: parts[1]!, primaryId: parts[2]! } }
974
+ }
975
+ if (parts[0] === "link" && parts.length === 6) {
976
+ return {
977
+ kind: "link",
978
+ ref: {
979
+ source: { objectTypeId: parts[1]!, primaryId: parts[2]! },
980
+ linkId: parts[3]!,
981
+ target: { objectTypeId: parts[4]!, primaryId: parts[5]! },
982
+ },
983
+ }
984
+ }
972
985
  }
973
- return key
986
+ throw new MaterializationConflictError(
987
+ "source-materialization",
988
+ `Stored source key ${key} is not a canonical entity key.`
989
+ )
990
+ }
991
+
992
+ /** What an assertion carries beyond its identity, or null when that is nothing. */
993
+ export function sourceAssertionPayload(
994
+ assertion: ProjectionSourceAssertion
995
+ ): Readonly<Record<string, unknown>> | null {
996
+ const { kind: _kind, ref: _ref, ...payload } = assertion
997
+ return Object.keys(payload).length === 0 ? null : payload
998
+ }
999
+
1000
+ /** Rebuilds an assertion from its typed identity columns and its payload. */
1001
+ export function sourceAssertionFromColumns(
1002
+ columns: {
1003
+ readonly entity_kind: "object" | "link"
1004
+ readonly object_type_id: string | null
1005
+ readonly primary_id: string | null
1006
+ readonly source_type_id: string | null
1007
+ readonly source_primary_id: string | null
1008
+ readonly link_id: string | null
1009
+ readonly target_type_id: string | null
1010
+ readonly target_primary_id: string | null
1011
+ },
1012
+ payload: unknown
1013
+ ): ProjectionSourceAssertion {
1014
+ const rest = (payload ?? {}) as Record<string, unknown>
1015
+ if (columns.entity_kind === "object") {
1016
+ return {
1017
+ kind: "object",
1018
+ ref: { objectTypeId: columns.object_type_id!, primaryId: columns.primary_id! },
1019
+ ...rest,
1020
+ } as ProjectionSourceAssertion
1021
+ }
1022
+ return {
1023
+ kind: "link",
1024
+ ref: {
1025
+ source: { objectTypeId: columns.source_type_id!, primaryId: columns.source_primary_id! },
1026
+ linkId: columns.link_id!,
1027
+ target: { objectTypeId: columns.target_type_id!, primaryId: columns.target_primary_id! },
1028
+ },
1029
+ ...rest,
1030
+ } as ProjectionSourceAssertion
974
1031
  }
975
1032
 
976
1033
  export function isExactStagingManifest(