@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
@@ -81,6 +81,34 @@ async function createUser(
81
81
  })
82
82
  }
83
83
 
84
+ async function completeOidcSignInForAva(
85
+ storage: AuthStorage,
86
+ directoryGroupIds: readonly string[] | undefined
87
+ ): Promise<void> {
88
+ await storage.oidcAuthorizationAttempts.create({
89
+ id: "oidc_directory",
90
+ projectId,
91
+ strategyId: "entra",
92
+ audience: "atlas",
93
+ stateHash: "directory-state",
94
+ nonceHash: "nonce-hash",
95
+ codeVerifier: "verifier",
96
+ createdAt: at("2026-05-14T10:00:00.000Z"),
97
+ expiresAt: at("2026-05-14T10:10:00.000Z"),
98
+ })
99
+ await storage.completeOidcSignIn({
100
+ projectId,
101
+ oidcAuthorizationAttemptId: "oidc_directory",
102
+ stateHash: "directory-state",
103
+ completedAt: at("2026-05-14T10:01:00.000Z"),
104
+ subject: "entra-ava",
105
+ email: "ava@acme.com",
106
+ directoryGroupIds,
107
+ newUserId: "usr_unused",
108
+ session: sessionInput("ses_directory"),
109
+ })
110
+ }
111
+
84
112
  /**
85
113
  * Runs the shared `AuthStorage` contract against any storage implementation.
86
114
  *
@@ -1721,91 +1749,6 @@ export function runAuthStorageContractSuite<TStorage extends AuthStorage>(
1721
1749
  })
1722
1750
  })
1723
1751
 
1724
- test("closes bootstrap user creation when an active user already exists", async () => {
1725
- await withStorage(async (storage) => {
1726
- await createUser(storage, {
1727
- id: "usr_existing",
1728
- email: "existing@acme.com",
1729
- })
1730
- await storage.magicLinks.create({
1731
- id: "ml_1",
1732
- projectId,
1733
- strategyId: "magic-link",
1734
- audience: "atlas",
1735
- email: "founder@acme.com",
1736
- tokenHash: "link-hash",
1737
- createdAt: at("2026-05-14T10:01:00.000Z"),
1738
- expiresAt: at("2026-05-14T10:16:00.000Z"),
1739
- })
1740
-
1741
- await expectAuthError(
1742
- storage.completeMagicLinkSignIn({
1743
- projectId,
1744
- magicLinkId: "ml_1",
1745
- tokenHash: "link-hash",
1746
- completedAt: at("2026-05-14T10:03:00.000Z"),
1747
- newUserId: "usr_founder",
1748
- allowUserCreationWithoutInvitation: true,
1749
- requireNoActiveUsersForUserCreation: true,
1750
- session: sessionInput("ses_founder"),
1751
- }),
1752
- "user_creation_not_allowed"
1753
- )
1754
- await expect(
1755
- storage.users.getByEmail({ projectId, email: "founder@acme.com" })
1756
- ).resolves.toBeNull()
1757
- await expect(storage.magicLinks.getById({ projectId, id: "ml_1" })).resolves.toMatchObject({
1758
- consumedAt: at("2026-05-14T10:03:00.000Z"),
1759
- })
1760
- })
1761
- })
1762
-
1763
- test("does not apply the bootstrap closure guard to invited user creation", async () => {
1764
- await withStorage(async (storage) => {
1765
- await createUser(storage, {
1766
- id: "usr_existing",
1767
- email: "existing@acme.com",
1768
- })
1769
- await storage.invitations.createOrUpdateActive({
1770
- id: "inv_1",
1771
- projectId,
1772
- email: "invited@acme.com",
1773
- groupIds: ["commercial"],
1774
- createdAt: at("2026-05-14T10:00:00.000Z"),
1775
- expiresAt: at("2026-05-21T10:00:00.000Z"),
1776
- })
1777
- await storage.magicLinks.create({
1778
- id: "ml_1",
1779
- projectId,
1780
- strategyId: "magic-link",
1781
- audience: "atlas",
1782
- email: "invited@acme.com",
1783
- tokenHash: "link-hash",
1784
- createdAt: at("2026-05-14T10:01:00.000Z"),
1785
- expiresAt: at("2026-05-14T10:16:00.000Z"),
1786
- })
1787
-
1788
- const result = await storage.completeMagicLinkSignIn({
1789
- projectId,
1790
- magicLinkId: "ml_1",
1791
- tokenHash: "link-hash",
1792
- completedAt: at("2026-05-14T10:03:00.000Z"),
1793
- newUserId: "usr_invited",
1794
- requireNoActiveUsersForUserCreation: true,
1795
- session: sessionInput("ses_invited"),
1796
- })
1797
-
1798
- expect(result.user).toMatchObject({
1799
- id: "usr_invited",
1800
- email: "invited@acme.com",
1801
- })
1802
- expect(result.invitation).toMatchObject({
1803
- id: "inv_1",
1804
- status: "accepted",
1805
- })
1806
- })
1807
- })
1808
-
1809
1752
  test("consumes valid magic links when an existing user is suspended", async () => {
1810
1753
  await withStorage(async (storage) => {
1811
1754
  await createUser(storage, { status: "suspended" })
@@ -1867,7 +1810,6 @@ export function runAuthStorageContractSuite<TStorage extends AuthStorage>(
1867
1810
  completedAt: at("2026-05-14T10:02:00.000Z"),
1868
1811
  subject: "00u1",
1869
1812
  email: "renamed@acme.com",
1870
- emailVerified: true,
1871
1813
  claims: { email: "renamed@acme.com" },
1872
1814
  newUserId: "usr_unused",
1873
1815
  session: sessionInput("ses_oidc"),
@@ -1885,65 +1827,35 @@ export function runAuthStorageContractSuite<TStorage extends AuthStorage>(
1885
1827
  })
1886
1828
  })
1887
1829
 
1888
- test("auto-links OIDC by verified email only when explicitly allowed", async () => {
1830
+ test("links a new OIDC identity to the existing user with its email", async () => {
1889
1831
  await withStorage(async (storage) => {
1890
1832
  await createUser(storage, { id: "usr_existing", email: "ava@acme.com" })
1891
1833
  await storage.oidcAuthorizationAttempts.create({
1892
- id: "oidc_blocked",
1834
+ id: "oidc_1",
1893
1835
  projectId,
1894
1836
  strategyId: "okta",
1895
1837
  audience: "atlas",
1896
- stateHash: "blocked-state",
1838
+ stateHash: "state-hash",
1897
1839
  nonceHash: "nonce-hash",
1898
1840
  codeVerifier: "verifier",
1899
1841
  createdAt: at("2026-05-14T10:00:00.000Z"),
1900
1842
  expiresAt: at("2026-05-14T10:10:00.000Z"),
1901
1843
  })
1902
1844
 
1903
- await expectAuthError(
1904
- storage.completeOidcSignIn({
1905
- projectId,
1906
- oidcAuthorizationAttemptId: "oidc_blocked",
1907
- stateHash: "blocked-state",
1908
- completedAt: at("2026-05-14T10:01:00.000Z"),
1909
- subject: "00u1",
1910
- email: "ava@acme.com",
1911
- emailVerified: false,
1912
- autoLinkByVerifiedEmail: true,
1913
- newUserId: "usr_unused",
1914
- session: sessionInput("ses_blocked"),
1915
- }),
1916
- "email_link_not_allowed"
1917
- )
1918
-
1919
- await storage.oidcAuthorizationAttempts.create({
1920
- id: "oidc_allowed",
1921
- projectId,
1922
- strategyId: "okta",
1923
- audience: "atlas",
1924
- stateHash: "allowed-state",
1925
- nonceHash: "nonce-hash",
1926
- codeVerifier: "verifier",
1927
- createdAt: at("2026-05-14T10:02:00.000Z"),
1928
- expiresAt: at("2026-05-14T10:12:00.000Z"),
1929
- })
1930
1845
  const result = await storage.completeOidcSignIn({
1931
1846
  projectId,
1932
- oidcAuthorizationAttemptId: "oidc_allowed",
1933
- stateHash: "allowed-state",
1934
- completedAt: at("2026-05-14T10:03:00.000Z"),
1847
+ oidcAuthorizationAttemptId: "oidc_1",
1848
+ stateHash: "state-hash",
1849
+ completedAt: at("2026-05-14T10:01:00.000Z"),
1935
1850
  subject: "00u1",
1936
1851
  email: "ava@acme.com",
1937
- emailVerified: true,
1938
- autoLinkByVerifiedEmail: true,
1939
1852
  newUserId: "usr_unused",
1940
- session: sessionInput("ses_allowed"),
1853
+ session: sessionInput("ses_oidc"),
1941
1854
  })
1942
1855
 
1943
1856
  expect(result.user.id).toBe("usr_existing")
1944
- expect(result.identity).toMatchObject({
1945
- userId: "usr_existing",
1946
- })
1857
+ expect(result.identity).toMatchObject({ subject: "00u1", userId: "usr_existing" })
1858
+ await expect(storage.users.getById({ projectId, id: "usr_unused" })).resolves.toBeNull()
1947
1859
  })
1948
1860
  })
1949
1861
 
@@ -1977,7 +1889,6 @@ export function runAuthStorageContractSuite<TStorage extends AuthStorage>(
1977
1889
  subject: "00u1",
1978
1890
  email: "ava@acme.com",
1979
1891
  displayName: "Ava Chen",
1980
- emailVerified: true,
1981
1892
  newUserId: "usr_oidc",
1982
1893
  session: sessionInput("ses_oidc"),
1983
1894
  })
@@ -2024,7 +1935,6 @@ export function runAuthStorageContractSuite<TStorage extends AuthStorage>(
2024
1935
  completedAt: at("2026-05-14T10:01:00.000Z"),
2025
1936
  subject: "00u1",
2026
1937
  email: "ava@acme.com",
2027
- emailVerified: true,
2028
1938
  newUserId: "usr_oidc",
2029
1939
  session: sessionInput("ses_oidc"),
2030
1940
  }),
@@ -2036,7 +1946,7 @@ export function runAuthStorageContractSuite<TStorage extends AuthStorage>(
2036
1946
  })
2037
1947
  })
2038
1948
 
2039
- test("allows first OIDC bootstrap user creation with manual groups", async () => {
1949
+ test("allows OIDC bootstrap user creation with manual groups", async () => {
2040
1950
  await withStorage(async (storage) => {
2041
1951
  await storage.oidcAuthorizationAttempts.create({
2042
1952
  id: "oidc_bootstrap",
@@ -2057,9 +1967,7 @@ export function runAuthStorageContractSuite<TStorage extends AuthStorage>(
2057
1967
  completedAt: at("2026-05-14T10:01:00.000Z"),
2058
1968
  subject: "00u1",
2059
1969
  email: "founder@acme.com",
2060
- emailVerified: true,
2061
1970
  allowUserCreationWithoutInvitation: true,
2062
- requireNoActiveUsersForUserCreation: true,
2063
1971
  manualGroupIds: ["security-admins"],
2064
1972
  newUserId: "usr_founder",
2065
1973
  session: sessionInput("ses_oidc_bootstrap"),
@@ -2075,6 +1983,72 @@ export function runAuthStorageContractSuite<TStorage extends AuthStorage>(
2075
1983
  })
2076
1984
  })
2077
1985
 
1986
+ test("syncs directory memberships to exactly the groups the provider grants", async () => {
1987
+ await withStorage(async (storage) => {
1988
+ await createUser(storage)
1989
+ for (const [groupId, source] of [
1990
+ ["security-admins", "manual"],
1991
+ ["commercial", "invitation"],
1992
+ ["legacy-team", "directory"],
1993
+ ] as const) {
1994
+ await storage.groupMemberships.upsert({ projectId, userId: "usr_1", groupId, source })
1995
+ }
1996
+
1997
+ await completeOidcSignInForAva(storage, ["security-admins", "field-ops"])
1998
+
1999
+ // legacy-team is no longer granted; security-admins keeps its manual source.
2000
+ await expect(
2001
+ storage.groupMemberships.listForUser({ projectId, userId: "usr_1" })
2002
+ ).resolves.toMatchObject([
2003
+ { groupId: "commercial", source: "invitation" },
2004
+ { groupId: "field-ops", source: "directory" },
2005
+ { groupId: "security-admins", source: "manual" },
2006
+ ])
2007
+ })
2008
+ })
2009
+
2010
+ test("removes every directory membership when the provider grants no groups", async () => {
2011
+ await withStorage(async (storage) => {
2012
+ await createUser(storage)
2013
+ await storage.groupMemberships.upsert({
2014
+ projectId,
2015
+ userId: "usr_1",
2016
+ groupId: "security-admins",
2017
+ source: "manual",
2018
+ })
2019
+ await storage.groupMemberships.upsert({
2020
+ projectId,
2021
+ userId: "usr_1",
2022
+ groupId: "field-ops",
2023
+ source: "directory",
2024
+ })
2025
+
2026
+ await completeOidcSignInForAva(storage, [])
2027
+
2028
+ await expect(
2029
+ storage.groupMemberships.listForUser({ projectId, userId: "usr_1" })
2030
+ ).resolves.toMatchObject([{ groupId: "security-admins", source: "manual" }])
2031
+ })
2032
+ })
2033
+
2034
+ test("leaves directory memberships alone when sign-in syncs no groups", async () => {
2035
+ await withStorage(async (storage) => {
2036
+ await createUser(storage)
2037
+ await storage.groupMemberships.upsert({
2038
+ projectId,
2039
+ userId: "usr_1",
2040
+ groupId: "field-ops",
2041
+ source: "directory",
2042
+ })
2043
+
2044
+ await completeOidcSignInForAva(storage, undefined)
2045
+
2046
+ await expect(
2047
+ storage.groupMemberships.listForUser({ projectId, userId: "usr_1" })
2048
+ ).resolves.toMatchObject([{ groupId: "field-ops", source: "directory" }])
2049
+ })
2050
+ })
2051
+
2078
2052
  test("suspends users and revokes active sessions immediately", async () => {
2079
2053
  await withStorage(async (storage) => {
2080
2054
  await createUser(storage)
@@ -410,38 +410,76 @@ export function runOntologyStorageContractSuite<TStorage extends OntologyStorage
410
410
  abandonedAt: "2026-01-01T00:03:00.000Z",
411
411
  })
412
412
  ).rejects.toMatchObject({ kind: "execution-lost" })
413
+ const adopted = await storage.ontology.sources.adopt({
414
+ projectId: "contract-project",
415
+ source,
416
+ execution: reclaimed.execution,
417
+ adoptedAt: "2026-01-01T00:01:30.000Z",
418
+ })
419
+ expect(adopted).toMatchObject({
420
+ record: {
421
+ materializationId: "staging-candidate",
422
+ status: "ready",
423
+ executionToken: reclaimed.execution.executionToken,
424
+ updatedAt: "2026-01-01T00:01:30.000Z",
425
+ },
426
+ resumeStagingOrdinal: 1,
427
+ })
428
+ // Adopting twice from the same execution is a no-op, not a second owner change.
429
+ expect(
430
+ await storage.ontology.sources.adopt({
431
+ projectId: "contract-project",
432
+ source,
433
+ execution: reclaimed.execution,
434
+ adoptedAt: "2026-01-01T00:01:40.000Z",
435
+ })
436
+ ).toMatchObject({ record: { executionToken: reclaimed.execution.executionToken } })
437
+
413
438
  const abandoned = await storage.ontology.sources.abandon({
414
- kind: "reclaim",
439
+ kind: "run",
415
440
  projectId: "contract-project",
416
441
  source,
417
442
  execution: reclaimed.execution,
418
443
  abandonedAt: "2026-01-01T00:02:00.000Z",
419
444
  })
420
445
  expect(abandoned).toMatchObject({ status: "abandoned", executionToken: null })
446
+ expect(
447
+ await storage.ontology.sources.abandon({
448
+ kind: "run",
449
+ projectId: "contract-project",
450
+ source,
451
+ execution: reclaimed.execution,
452
+ abandonedAt: "2026-01-01T00:02:00.000Z",
453
+ })
454
+ ).toBeNull()
455
+ expect(
456
+ await storage.ontology.sources.adopt({
457
+ projectId: "contract-project",
458
+ source,
459
+ execution: reclaimed.execution,
460
+ adoptedAt: "2026-01-01T00:02:10.000Z",
461
+ })
462
+ ).toBeNull()
421
463
  expect(
422
464
  await storage.ontology.sources.summarizeTerminal({ projectId: "contract-project" })
423
465
  ).toEqual({ count: 1, oldestTerminalAt: "2026-01-01T00:02:00.000Z" })
424
466
 
467
+ // Superseded retention does not hold an abandoned candidate back.
425
468
  expect(
426
469
  await storage.ontology.sources.cleanupTerminal({
427
470
  projectId: "contract-project",
428
471
  terminalBefore: "2026-02-01T00:00:00.000Z",
429
- limit: 1,
472
+ limit: 10,
430
473
  })
474
+ ).toEqual({ rowsDeleted: 0, materializationsDeleted: 0 })
475
+ expect(
476
+ await storage.ontology.sources.purgeAbandoned({ projectId: "contract-project", limit: 1 })
431
477
  ).toEqual({ rowsDeleted: 1, materializationsDeleted: 0 })
432
478
  expect(
433
- await storage.ontology.sources.cleanupTerminal({
434
- projectId: "contract-project",
435
- terminalBefore: "2026-02-01T00:00:00.000Z",
436
- limit: 1,
437
- })
479
+ await storage.ontology.sources.purgeAbandoned({ projectId: "contract-project", limit: 1 })
438
480
  ).toEqual({ rowsDeleted: 1, materializationsDeleted: 0 })
439
481
  expect(
440
- await storage.ontology.sources.cleanupTerminal({
441
- projectId: "contract-project",
442
- terminalBefore: "2026-02-01T00:00:00.000Z",
443
- limit: 1,
444
- })
482
+ await storage.ontology.sources.purgeAbandoned({ projectId: "contract-project", limit: 1 })
445
483
  ).toEqual({ rowsDeleted: 0, materializationsDeleted: 1 })
446
484
  expect(
447
485
  await storage.ontology.sources.summarizeTerminal({ projectId: "contract-project" })
@@ -449,6 +487,95 @@ export function runOntologyStorageContractSuite<TStorage extends OntologyStorage
449
487
  })
450
488
  })
451
489
 
490
+ test("adopts a staging candidate at its last staged root", async () => {
491
+ await withStorage(async (storage) => {
492
+ const identity = replacementIdentity("01")
493
+ const claimed = await startTestProjectionRun(storage, {
494
+ id: "resumed-run",
495
+ projectId: "contract-project",
496
+ identity,
497
+ target: { objectTypeId: "ContractDevice" },
498
+ })
499
+ const source = { projectionId: identity.projectionId }
500
+ await storage.ontology.sources.beginMaterialization({
501
+ projectId: "contract-project",
502
+ source,
503
+ materializationId: "resumed-candidate",
504
+ execution: claimed.execution,
505
+ projectionKind: "object",
506
+ protocol: "replacement",
507
+ datasetVersion: identity.datasetVersion,
508
+ projectionRevision: identity.projectionRevision,
509
+ ownershipHash: identity.ownershipHash,
510
+ ontologyRevision: identity.ontologyRevision,
511
+ createdAt: "2026-01-01T00:00:00.000Z",
512
+ })
513
+ const row = (primaryId: string, stagingOrdinal: number) => ({
514
+ root: { kind: "object" as const, ref: { objectTypeId: "ContractDevice", primaryId } },
515
+ assertion: {
516
+ kind: "object" as const,
517
+ ref: { objectTypeId: "ContractDevice", primaryId },
518
+ properties: { name: primaryId },
519
+ },
520
+ stagingOrdinal,
521
+ })
522
+ await storage.ontology.sources.stageRows({
523
+ projectId: "contract-project",
524
+ source,
525
+ materializationId: "resumed-candidate",
526
+ execution: claimed.execution,
527
+ rows: [row("zero", 0), row("one", 1)],
528
+ })
529
+
530
+ const reclaimed = await storage.projectionRuns.startOrReclaim({
531
+ id: claimed.run.id,
532
+ projectId: claimed.run.projectId,
533
+ identity,
534
+ target: { objectTypeId: "ContractDevice" },
535
+ })
536
+ const adopted = await storage.ontology.sources.adopt({
537
+ projectId: "contract-project",
538
+ source,
539
+ execution: reclaimed.execution,
540
+ adoptedAt: "2026-01-01T00:01:00.000Z",
541
+ })
542
+ expect(adopted).toMatchObject({
543
+ record: { status: "staging", executionToken: reclaimed.execution.executionToken },
544
+ resumeStagingOrdinal: 1,
545
+ })
546
+ // The previous execution lost the candidate with the run.
547
+ await expect(
548
+ storage.ontology.sources.stageRows({
549
+ projectId: "contract-project",
550
+ source,
551
+ materializationId: "resumed-candidate",
552
+ execution: claimed.execution,
553
+ rows: [row("two", 2)],
554
+ })
555
+ ).rejects.toMatchObject({ kind: "execution-lost" })
556
+ expect(
557
+ await storage.ontology.sources.stageRows({
558
+ projectId: "contract-project",
559
+ source,
560
+ materializationId: "resumed-candidate",
561
+ execution: reclaimed.execution,
562
+ rows: [row("one", 1), row("two", 2)],
563
+ })
564
+ ).toEqual({ inserted: 1, unchanged: 1 })
565
+ expect(
566
+ await storage.ontology.sources.markReady({
567
+ projectId: "contract-project",
568
+ source,
569
+ materializationId: "resumed-candidate",
570
+ execution: reclaimed.execution,
571
+ rootCount: 3,
572
+ assertionCount: 3,
573
+ readyAt: "2026-01-01T00:02:00.000Z",
574
+ })
575
+ ).toMatchObject({ status: "ready", rootCount: 3 })
576
+ })
577
+ })
578
+
452
579
  test("atomically activates explicit empty candidates and rejects competing stale heads", async () => {
453
580
  await withStorage(async (storage) => {
454
581
  const first = await createReadyEmptyCandidate(storage, "run-one", "candidate-one", "01")
@@ -532,16 +659,15 @@ export function runOntologyStorageContractSuite<TStorage extends OntologyStorage
532
659
  )
533
660
  ).rejects.toThrow("execution token is stale")
534
661
  await storage.ontology.sources.abandon({
535
- kind: "reclaim",
662
+ kind: "run",
536
663
  projectId: "contract-project",
537
664
  source: stale.source,
538
665
  execution: reclaimed.execution,
539
666
  abandonedAt: "2026-01-06T00:01:00.000Z",
540
667
  })
541
668
 
542
- const cleanup = await storage.ontology.sources.cleanupTerminal({
669
+ const cleanup = await storage.ontology.sources.purgeAbandoned({
543
670
  projectId: "contract-project",
544
- terminalBefore: "2026-02-01T00:00:00.000Z",
545
671
  limit: 10,
546
672
  })
547
673
  expect(cleanup.materializationsDeleted).toBeGreaterThanOrEqual(1)