@intx/db 0.3.0 → 0.4.0

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 (71) hide show
  1. package/README.md +11 -0
  2. package/dist/approval-store.d.ts +7 -0
  3. package/dist/approval-store.js +13 -0
  4. package/dist/backfill-principal-keys.d.ts +9 -0
  5. package/dist/backfill-principal-keys.js +51 -0
  6. package/dist/client.d.ts +27 -2
  7. package/dist/client.js +6 -0
  8. package/dist/config.d.ts +1 -0
  9. package/dist/config.js +2 -0
  10. package/dist/connection.js +1 -0
  11. package/dist/credential-resolution.d.ts +54 -5
  12. package/dist/credential-resolution.js +109 -0
  13. package/dist/index.d.ts +11 -5
  14. package/dist/index.js +10 -4
  15. package/dist/model-source-resolution.d.ts +6 -5
  16. package/dist/model-source-resolution.js +42 -13
  17. package/dist/parse-row.d.ts +39 -8
  18. package/dist/parse-row.js +8 -0
  19. package/dist/principal-key-store.d.ts +51 -0
  20. package/dist/principal-key-store.js +82 -0
  21. package/dist/principal-store.d.ts +44 -0
  22. package/dist/principal-store.js +67 -0
  23. package/dist/schema/approvals.js +5 -0
  24. package/dist/schema/git-tokens.js +1 -1
  25. package/dist/schema/index.d.ts +2 -0
  26. package/dist/schema/index.js +2 -0
  27. package/dist/schema/principal-keys.d.ts +126 -0
  28. package/dist/schema/principal-keys.js +28 -0
  29. package/dist/schema/sidecar-allocation.d.ts +19 -40
  30. package/dist/schema/sidecar-allocation.js +4 -8
  31. package/dist/schema/sidecar.d.ts +0 -17
  32. package/dist/schema/sidecar.js +4 -12
  33. package/dist/schema/tenants.js +13 -3
  34. package/dist/schema/workflow-probe.d.ts +358 -0
  35. package/dist/schema/workflow-probe.js +51 -0
  36. package/dist/schema/workflow-run-dispatch.d.ts +17 -0
  37. package/dist/schema/workflow-run-dispatch.js +7 -0
  38. package/dist/schema/workflow-run.d.ts +27 -0
  39. package/dist/schema/workflow-run.js +8 -0
  40. package/dist/sender-key-resolver.d.ts +91 -0
  41. package/dist/sender-key-resolver.js +184 -0
  42. package/dist/sidecar-allocation-store.d.ts +38 -3
  43. package/dist/sidecar-allocation-store.js +224 -22
  44. package/dist/signer-identity.d.ts +12 -0
  45. package/dist/signer-identity.js +15 -0
  46. package/dist/tenant-hierarchy.d.ts +2 -0
  47. package/dist/tenant-hierarchy.js +27 -0
  48. package/dist/workflow-probe-store.d.ts +47 -0
  49. package/dist/workflow-probe-store.js +124 -0
  50. package/dist/workflow-run-dispatch-store.d.ts +3 -0
  51. package/dist/workflow-run-dispatch-store.js +12 -2
  52. package/migrations/0085_add_approval_run_idx.sql +1 -0
  53. package/migrations/0086_cool_human_cannonball.sql +1 -0
  54. package/migrations/0087_drop_sidecar_placement.sql +3 -0
  55. package/migrations/0088_thick_sprite.sql +32 -0
  56. package/migrations/0089_tense_selene.sql +13 -0
  57. package/migrations/0090_tenant_domain_lower_unique.sql +2 -0
  58. package/migrations/0091_workflow_run_dispatch_sender_address.sql +21 -0
  59. package/migrations/0092_sidecar_destroy_failed.sql +4 -0
  60. package/migrations/0093_sidecar_initialization.sql +2 -0
  61. package/migrations/meta/0085_snapshot.json +4111 -0
  62. package/migrations/meta/0086_snapshot.json +4117 -0
  63. package/migrations/meta/0087_snapshot.json +4100 -0
  64. package/migrations/meta/0088_snapshot.json +4286 -0
  65. package/migrations/meta/0089_snapshot.json +4376 -0
  66. package/migrations/meta/0090_snapshot.json +4387 -0
  67. package/migrations/meta/0091_snapshot.json +4397 -0
  68. package/migrations/meta/0092_snapshot.json +4397 -0
  69. package/migrations/meta/0093_snapshot.json +4407 -0
  70. package/migrations/meta/_journal.json +63 -0
  71. package/package.json +6 -5
@@ -1,6 +1,6 @@
1
1
  import { type } from "arktype";
2
- import { and, asc, eq, inArray, isNull, lte, or, sql } from "drizzle-orm";
3
- import { SidecarPlacementRequirement, sidecarAllocationStatuses, } from "@intx/types";
2
+ import { and, asc, eq, gt, inArray, isNull, lte, notInArray, or, sql, } from "drizzle-orm";
3
+ import { sidecarAllocationStatuses, } from "@intx/types";
4
4
  import { createWorkflowRunDispatchStore } from "./workflow-run-dispatch-store.js";
5
5
  import { isLiveWorkflowRunStatus, liveWorkflowRunStatuses, principal, sidecar, sidecarAllocation, workflowRun, workflowRunLaunchSpec, } from "./schema/index.js";
6
6
  const SidecarAllocationStatusValidator = type.enumerated(...sidecarAllocationStatuses);
@@ -13,10 +13,6 @@ const activeStatuses = [
13
13
  "releasing",
14
14
  ];
15
15
  function parseSidecarAllocationRow(row) {
16
- const placement = SidecarPlacementRequirement.assert({
17
- sharing: row.placementSharing,
18
- reuse: row.sidecarReuse,
19
- });
20
16
  return {
21
17
  id: row.id,
22
18
  anchorRunId: row.anchorRunId,
@@ -25,7 +21,6 @@ function parseSidecarAllocationRow(row) {
25
21
  provisionerApiVersion: SidecarProvisionerApiVersion.assert(row.provisionerApiVersion),
26
22
  provisionerBindingFingerprint: row.provisionerBindingFingerprint,
27
23
  ...(row.sidecarId !== null ? { sidecarId: row.sidecarId } : {}),
28
- placement,
29
24
  status: SidecarAllocationStatusValidator.assert(row.status),
30
25
  generation: row.generation,
31
26
  ...(row.ensureAcceptedGeneration !== null
@@ -39,6 +34,9 @@ function parseSidecarAllocationRow(row) {
39
34
  ...(row.reconciliationLeaseExpiresAt !== null
40
35
  ? { reconciliationLeaseExpiresAt: row.reconciliationLeaseExpiresAt }
41
36
  : {}),
37
+ ...(row.initializationLeaseId !== null
38
+ ? { initializationLeaseId: row.initializationLeaseId }
39
+ : {}),
42
40
  ensureAttempts: row.ensureAttempts,
43
41
  destroyAttempts: row.destroyAttempts,
44
42
  ...(row.connectDeadline !== null
@@ -58,16 +56,92 @@ function databaseTimestamp(override) {
58
56
  function leaseCondition(expectedLeaseId) {
59
57
  return expectedLeaseId === undefined
60
58
  ? []
61
- : [eq(sidecarAllocation.reconciliationLeaseId, expectedLeaseId)];
59
+ : [
60
+ eq(sidecarAllocation.reconciliationLeaseId, expectedLeaseId),
61
+ gt(sidecarAllocation.reconciliationLeaseExpiresAt, sql `clock_timestamp()`),
62
+ ];
62
63
  }
63
64
  export function createSidecarAllocationStore(db) {
64
65
  const workflowRunDispatchStore = createWorkflowRunDispatchStore(db);
66
+ function initializationConditions(args, expectedMarker, { requireCurrentLease = true } = {}) {
67
+ return and(eq(sidecarAllocation.id, args.allocationId), eq(sidecarAllocation.anchorRunId, args.anchorRunId), eq(sidecarAllocation.tenantId, args.tenantId), eq(sidecarAllocation.status, "allocated"), eq(sidecarAllocation.generation, args.generation), eq(sidecarAllocation.ensureAcceptedGeneration, args.generation), ...(requireCurrentLease ? leaseCondition(args.leaseId) : []), expectedMarker === null
68
+ ? isNull(sidecarAllocation.initializationLeaseId)
69
+ : eq(sidecarAllocation.initializationLeaseId, expectedMarker));
70
+ }
71
+ async function writeInitialization(args, completion) {
72
+ args.signal.throwIfAborted();
73
+ return db.transaction(async (tx) => {
74
+ const condition = initializationConditions(args, completion === undefined ? null : args.leaseId);
75
+ const [allocation] = await tx
76
+ .select({ id: sidecarAllocation.id })
77
+ .from(sidecarAllocation)
78
+ .where(condition)
79
+ .for("update");
80
+ args.signal.throwIfAborted();
81
+ if (allocation === undefined)
82
+ return null;
83
+ let previousPublicKey = null;
84
+ if (completion === undefined) {
85
+ const [previous] = await tx
86
+ .select({ publicKey: workflowRun.publicKey })
87
+ .from(workflowRun)
88
+ .where(and(eq(workflowRun.id, args.anchorRunId), eq(workflowRun.anchorRunId, args.anchorRunId), eq(workflowRun.tenantId, args.tenantId)))
89
+ .for("update");
90
+ if (previous === undefined)
91
+ throw new Error("Initialization anchor is missing");
92
+ previousPublicKey = previous.publicKey;
93
+ }
94
+ const [anchor] = await tx
95
+ .update(workflowRun)
96
+ .set({
97
+ publicKey: completion?.publicKey ?? null,
98
+ ...(completion?.credentialRefs !== undefined
99
+ ? { credentialRefs: completion.credentialRefs }
100
+ : {}),
101
+ })
102
+ .where(and(eq(workflowRun.id, args.anchorRunId), eq(workflowRun.anchorRunId, args.anchorRunId), eq(workflowRun.tenantId, args.tenantId)))
103
+ .returning({ id: workflowRun.id });
104
+ if (anchor === undefined)
105
+ throw new Error("Initialization anchor is missing");
106
+ // Check the lease again after waiting on the anchor row. Both writes roll
107
+ // back if ownership expired while the transaction held the allocation lock.
108
+ const [updated] = await tx
109
+ .update(sidecarAllocation)
110
+ .set({
111
+ initializationLeaseId: completion === undefined ? args.leaseId : null,
112
+ })
113
+ .where(condition)
114
+ .returning({ id: sidecarAllocation.id });
115
+ args.signal.throwIfAborted();
116
+ if (updated === undefined)
117
+ throw new Error("Initialization lease expired");
118
+ return { previousPublicKey };
119
+ });
120
+ }
121
+ async function initializationCompleted(tx, args) {
122
+ const [allocation] = await tx
123
+ .select({
124
+ anchorRunId: sidecarAllocation.anchorRunId,
125
+ initializationLeaseId: sidecarAllocation.initializationLeaseId,
126
+ })
127
+ .from(sidecarAllocation)
128
+ .where(and(eq(sidecarAllocation.id, args.allocationId), eq(sidecarAllocation.generation, args.expectedGeneration), ...leaseCondition(args.expectedLeaseId)))
129
+ .for("update");
130
+ if (allocation === undefined || allocation.initializationLeaseId !== null)
131
+ return false;
132
+ // Read the key after acquiring the allocation lock, so a completion that
133
+ // committed while we waited is visible even if its response was lost.
134
+ const [anchor] = await tx
135
+ .select({ publicKey: workflowRun.publicKey })
136
+ .from(workflowRun)
137
+ .where(eq(workflowRun.id, allocation.anchorRunId));
138
+ return anchor !== undefined && anchor.publicKey !== null;
139
+ }
65
140
  async function insertSidecarIdentity(tx, args) {
66
141
  await tx.insert(sidecar).values({
67
142
  id: args.sidecarId,
68
143
  url: null,
69
144
  tokenHashSha256: args.tokenHashSha256,
70
- credentialScope: "allocated",
71
145
  status: "offline",
72
146
  createdAt: args.now,
73
147
  updatedAt: args.now,
@@ -96,6 +170,7 @@ export function createSidecarAllocationStore(db) {
96
170
  .set({
97
171
  status: "releasing",
98
172
  generation: args.expectedGeneration + 1,
173
+ initializationLeaseId: null,
99
174
  nextAttemptAt: now,
100
175
  reconciliationLeaseId: null,
101
176
  reconciliationLeaseExpiresAt: null,
@@ -108,11 +183,59 @@ export function createSidecarAllocationStore(db) {
108
183
  : {}),
109
184
  updatedAt: now,
110
185
  })
111
- .where(and(eq(sidecarAllocation.id, args.allocationId), eq(sidecarAllocation.status, args.expectedStatus), eq(sidecarAllocation.generation, args.expectedGeneration), ...leaseCondition(args.expectedLeaseId)))
186
+ .where(and(eq(sidecarAllocation.id, args.allocationId), eq(sidecarAllocation.status, args.expectedStatus), eq(sidecarAllocation.generation, args.expectedGeneration), ...leaseCondition(args.expectedLeaseId), ...(args.expectedInitializationLeaseId !== undefined
187
+ ? [
188
+ eq(sidecarAllocation.initializationLeaseId, args.expectedInitializationLeaseId),
189
+ ]
190
+ : [])))
112
191
  .returning();
113
192
  return updated === undefined ? null : parseSidecarAllocationRow(updated);
114
193
  }
115
194
  return {
195
+ beginInitialization(args) {
196
+ return writeInitialization(args);
197
+ },
198
+ async completeInitialization(args) {
199
+ return (await writeInitialization(args, args)) !== null;
200
+ },
201
+ async clearUnsentInitialization(args) {
202
+ // No signal gate: the sole caller invokes this only for a proven-unsent
203
+ // frame, which is reachable precisely when the attempt was cancelled.
204
+ // The WHERE clause is the guard. Marker equality proves no newer
205
+ // attempt began, and generation/status equality proves the allocation
206
+ // did not move on. The reconciliation lease is deliberately not
207
+ // required: a disconnect nulls the lease while leaving this attempt's
208
+ // marker behind, and only this attempt's own clear may remove it. Restore
209
+ // its previous key in the same transaction so absence of the marker cannot
210
+ // expose a live workflow as keyless or certify a different attempt.
211
+ return db.transaction(async (tx) => {
212
+ const condition = initializationConditions(args, args.leaseId, {
213
+ requireCurrentLease: false,
214
+ });
215
+ const [allocation] = await tx
216
+ .select({ id: sidecarAllocation.id })
217
+ .from(sidecarAllocation)
218
+ .where(condition)
219
+ .for("update");
220
+ if (allocation === undefined)
221
+ return false;
222
+ const [anchor] = await tx
223
+ .update(workflowRun)
224
+ .set({ publicKey: args.previousPublicKey })
225
+ .where(and(eq(workflowRun.id, args.anchorRunId), eq(workflowRun.anchorRunId, args.anchorRunId), eq(workflowRun.tenantId, args.tenantId)))
226
+ .returning({ id: workflowRun.id });
227
+ if (anchor === undefined)
228
+ throw new Error("Initialization anchor is missing");
229
+ const [updated] = await tx
230
+ .update(sidecarAllocation)
231
+ .set({ initializationLeaseId: null })
232
+ .where(condition)
233
+ .returning({ id: sidecarAllocation.id });
234
+ if (updated === undefined)
235
+ throw new Error("Initialization attempt changed before rollback");
236
+ return true;
237
+ });
238
+ },
116
239
  async createPending(args, tx) {
117
240
  const executor = tx ?? db;
118
241
  const [anchor] = await executor
@@ -144,7 +267,6 @@ export function createSidecarAllocationStore(db) {
144
267
  if (args.provisionerApiVersion !== 1) {
145
268
  throw new Error("sidecarAllocationStore.createPending: unsupported API version");
146
269
  }
147
- const placement = SidecarPlacementRequirement.assert(args.placement ?? { sharing: "exclusive", reuse: "never" });
148
270
  const now = databaseTimestamp(args.now);
149
271
  const [inserted] = await executor
150
272
  .insert(sidecarAllocation)
@@ -155,8 +277,6 @@ export function createSidecarAllocationStore(db) {
155
277
  provisionerId: args.provisionerId,
156
278
  provisionerApiVersion: args.provisionerApiVersion,
157
279
  provisionerBindingFingerprint: args.provisionerBindingFingerprint,
158
- placementSharing: placement.sharing,
159
- sidecarReuse: placement.reuse ?? "never",
160
280
  status: "pending",
161
281
  generation: 0,
162
282
  nextAttemptAt: now,
@@ -169,12 +289,41 @@ export function createSidecarAllocationStore(db) {
169
289
  }
170
290
  return parseSidecarAllocationRow(inserted);
171
291
  },
292
+ async createAdopted(args, tx) {
293
+ const createdAt = databaseTimestamp(args.now);
294
+ const [inserted] = await (tx ?? db)
295
+ .insert(sidecarAllocation)
296
+ .values({
297
+ id: args.id,
298
+ anchorRunId: args.anchorRunId,
299
+ tenantId: args.tenantId,
300
+ provisionerId: args.provisionerId,
301
+ provisionerApiVersion: args.provisionerApiVersion,
302
+ provisionerBindingFingerprint: args.provisionerBindingFingerprint,
303
+ sidecarId: args.sidecarId,
304
+ status: "allocated",
305
+ generation: args.generation,
306
+ ensureAcceptedGeneration: args.generation,
307
+ ...(args.externalRef !== undefined
308
+ ? { externalRef: args.externalRef }
309
+ : {}),
310
+ connectDeadline: args.connectDeadline,
311
+ nextAttemptAt: createdAt,
312
+ createdAt,
313
+ updatedAt: createdAt,
314
+ })
315
+ .returning();
316
+ if (inserted === undefined) {
317
+ throw new Error(`sidecarAllocationStore.createAdopted: insert returned no row for ${args.id}`);
318
+ }
319
+ return parseSidecarAllocationRow(inserted);
320
+ },
172
321
  async bindInitialSidecar(args) {
173
322
  return db.transaction(async (tx) => {
174
323
  const [allocation] = await tx
175
324
  .select()
176
325
  .from(sidecarAllocation)
177
- .where(eq(sidecarAllocation.id, args.allocationId))
326
+ .where(and(eq(sidecarAllocation.id, args.allocationId), ...leaseCondition(args.expectedLeaseId)))
178
327
  .limit(1)
179
328
  .for("update");
180
329
  if (allocation === undefined ||
@@ -217,7 +366,7 @@ export function createSidecarAllocationStore(db) {
217
366
  const [allocation] = await tx
218
367
  .select()
219
368
  .from(sidecarAllocation)
220
- .where(eq(sidecarAllocation.id, args.allocationId))
369
+ .where(and(eq(sidecarAllocation.id, args.allocationId), ...leaseCondition(args.expectedLeaseId)))
221
370
  .limit(1)
222
371
  .for("update");
223
372
  if (allocation === undefined ||
@@ -300,11 +449,15 @@ export function createSidecarAllocationStore(db) {
300
449
  async beginReplacement(args) {
301
450
  const now = databaseTimestamp(args.now);
302
451
  return db.transaction(async (tx) => {
452
+ if (args.onlyIfInitializationIncomplete &&
453
+ (await initializationCompleted(tx, args)))
454
+ return null;
303
455
  const [updated] = await tx
304
456
  .update(sidecarAllocation)
305
457
  .set({
306
458
  status: "replacing",
307
459
  generation: args.expectedGeneration + 1,
460
+ initializationLeaseId: null,
308
461
  ensureAcceptedGeneration: null,
309
462
  nextAttemptAt: args.nextAttemptAt,
310
463
  reconciliationLeaseId: null,
@@ -314,7 +467,11 @@ export function createSidecarAllocationStore(db) {
314
467
  failureMessage: args.failureMessage,
315
468
  updatedAt: now,
316
469
  })
317
- .where(and(eq(sidecarAllocation.id, args.allocationId), eq(sidecarAllocation.status, args.expectedStatus), eq(sidecarAllocation.generation, args.expectedGeneration), ...leaseCondition(args.expectedLeaseId)))
470
+ .where(and(eq(sidecarAllocation.id, args.allocationId), eq(sidecarAllocation.status, args.expectedStatus), eq(sidecarAllocation.generation, args.expectedGeneration), ...leaseCondition(args.expectedLeaseId), ...(args.expectedInitializationLeaseId !== undefined
471
+ ? [
472
+ eq(sidecarAllocation.initializationLeaseId, args.expectedInitializationLeaseId),
473
+ ]
474
+ : [])))
318
475
  .returning();
319
476
  if (updated === undefined)
320
477
  return null;
@@ -333,6 +490,9 @@ export function createSidecarAllocationStore(db) {
333
490
  async beginUnrecoverableRelease(args) {
334
491
  const now = databaseTimestamp(args.now);
335
492
  return db.transaction(async (tx) => {
493
+ if (args.onlyIfInitializationIncomplete &&
494
+ (await initializationCompleted(tx, args)))
495
+ return null;
336
496
  const releasing = await beginRelease({
337
497
  ...args,
338
498
  expectedStatus: "allocated",
@@ -360,6 +520,31 @@ export function createSidecarAllocationStore(db) {
360
520
  .returning();
361
521
  return updated === undefined ? null : parseSidecarAllocationRow(updated);
362
522
  },
523
+ async markDestroyFailed(args) {
524
+ const now = databaseTimestamp(args.now);
525
+ return db.transaction(async (tx) => {
526
+ const [updated] = await tx
527
+ .update(sidecarAllocation)
528
+ .set({
529
+ status: "destroy_failed",
530
+ failureCode: args.code,
531
+ failureMessage: args.message,
532
+ nextAttemptAt: null,
533
+ reconciliationLeaseId: null,
534
+ reconciliationLeaseExpiresAt: null,
535
+ connectDeadline: null,
536
+ destroyAttempts: sql `${sidecarAllocation.destroyAttempts} + 1`,
537
+ updatedAt: now,
538
+ })
539
+ .where(and(eq(sidecarAllocation.id, args.allocationId), inArray(sidecarAllocation.status, ["replacing", "releasing"]), eq(sidecarAllocation.generation, args.expectedGeneration), ...leaseCondition(args.expectedLeaseId)))
540
+ .returning();
541
+ if (updated === undefined)
542
+ return null;
543
+ await failRunningRuns(tx, updated.anchorRunId, now);
544
+ await workflowRunDispatchStore.failUnsettled(updated.anchorRunId, args.code, args.message, now, tx);
545
+ return parseSidecarAllocationRow(updated);
546
+ });
547
+ },
363
548
  async failWithoutInfrastructure(args, tx) {
364
549
  const now = databaseTimestamp(args.now);
365
550
  const fail = async (executor) => {
@@ -402,7 +587,14 @@ export function createSidecarAllocationStore(db) {
402
587
  const [candidate] = await tx
403
588
  .select({ id: sidecarAllocation.id })
404
589
  .from(sidecarAllocation)
405
- .where(and(inArray(sidecarAllocation.status, activeStatuses), lte(sidecarAllocation.nextAttemptAt, sql `now()`), or(isNull(sidecarAllocation.reconciliationLeaseExpiresAt), lte(sidecarAllocation.reconciliationLeaseExpiresAt, sql `now()`))))
590
+ .where(and(inArray(sidecarAllocation.status, activeStatuses), lte(sidecarAllocation.nextAttemptAt, sql `now()`), ...(args.excludedAllocationIds !== undefined &&
591
+ args.excludedAllocationIds.length > 0
592
+ ? [
593
+ notInArray(sidecarAllocation.id, [
594
+ ...args.excludedAllocationIds,
595
+ ]),
596
+ ]
597
+ : []), or(isNull(sidecarAllocation.reconciliationLeaseExpiresAt), lte(sidecarAllocation.reconciliationLeaseExpiresAt, sql `now()`))))
406
598
  .orderBy(asc(sidecarAllocation.nextAttemptAt), asc(sidecarAllocation.createdAt))
407
599
  .limit(1)
408
600
  .for("update", { skipLocked: true });
@@ -425,12 +617,20 @@ export function createSidecarAllocationStore(db) {
425
617
  const [updated] = await db
426
618
  .update(sidecarAllocation)
427
619
  .set({
428
- reconciliationLeaseExpiresAt: sql `now() + (${leaseDurationMs} * interval '1 millisecond')`,
620
+ reconciliationLeaseExpiresAt: sql `clock_timestamp() + (${leaseDurationMs} * interval '1 millisecond')`,
429
621
  })
430
- .where(and(eq(sidecarAllocation.id, allocationId), eq(sidecarAllocation.reconciliationLeaseId, leaseId)))
622
+ .where(and(eq(sidecarAllocation.id, allocationId), ...leaseCondition(leaseId)))
431
623
  .returning({ id: sidecarAllocation.id });
432
624
  return updated !== undefined;
433
625
  },
626
+ async isReconciliationLeaseCurrent(allocationId, generation, leaseId) {
627
+ const [allocation] = await db
628
+ .select({ id: sidecarAllocation.id })
629
+ .from(sidecarAllocation)
630
+ .where(and(eq(sidecarAllocation.id, allocationId), eq(sidecarAllocation.generation, generation), ...leaseCondition(leaseId)))
631
+ .limit(1);
632
+ return allocation !== undefined;
633
+ },
434
634
  async markConnectionReady(args) {
435
635
  const [updated] = await db
436
636
  .update(sidecarAllocation)
@@ -443,7 +643,7 @@ export function createSidecarAllocationStore(db) {
443
643
  failureMessage: null,
444
644
  updatedAt: databaseTimestamp(args.now),
445
645
  })
446
- .where(and(eq(sidecarAllocation.id, args.allocationId), eq(sidecarAllocation.status, "allocated"), eq(sidecarAllocation.generation, args.generation), eq(sidecarAllocation.ensureAcceptedGeneration, args.generation), ...leaseCondition(args.expectedLeaseId)))
646
+ .where(and(eq(sidecarAllocation.id, args.allocationId), eq(sidecarAllocation.status, "allocated"), eq(sidecarAllocation.generation, args.generation), eq(sidecarAllocation.ensureAcceptedGeneration, args.generation), isNull(sidecarAllocation.initializationLeaseId), ...leaseCondition(args.expectedLeaseId)))
447
647
  .returning();
448
648
  return updated === undefined ? null : parseSidecarAllocationRow(updated);
449
649
  },
@@ -489,14 +689,16 @@ export function createSidecarAllocationStore(db) {
489
689
  reconciliationLeaseId: null,
490
690
  reconciliationLeaseExpiresAt: null,
491
691
  })
492
- .where(and(eq(sidecarAllocation.id, allocationId), eq(sidecarAllocation.reconciliationLeaseId, leaseId)))
692
+ .where(and(eq(sidecarAllocation.id, allocationId), ...leaseCondition(leaseId)))
493
693
  .returning({ id: sidecarAllocation.id });
494
694
  return updated !== undefined;
495
695
  },
496
696
  async wakeReconciliation(allocationId, generation) {
497
697
  const [updated] = await db
498
698
  .update(sidecarAllocation)
499
- .set({ nextAttemptAt: sql `now()` })
699
+ .set({
700
+ nextAttemptAt: sql `now()`,
701
+ })
500
702
  .where(and(eq(sidecarAllocation.id, allocationId), eq(sidecarAllocation.generation, generation), inArray(sidecarAllocation.status, activeStatuses)))
501
703
  .returning({ id: sidecarAllocation.id });
502
704
  return updated !== undefined;
@@ -0,0 +1,12 @@
1
+ import type { LocalPrincipalSigner } from "@intx/types";
2
+ import type { DBExecutor } from "./client.js";
3
+ import type { PrincipalKeyStore } from "./principal-key-store.js";
4
+ /**
5
+ * Resolve a principal's trusted signer identity from the hub's own key store.
6
+ *
7
+ * The returned `publicKey` is the principal's active key as the hub holds it, so
8
+ * a verifier can check a signature against it without trusting any key on the
9
+ * wire. Throws when the principal has no active key -- an unresolvable signer
10
+ * must fail loudly rather than default.
11
+ */
12
+ export declare function lookupLocalPrincipalSigner(principalKeyStore: PrincipalKeyStore, principalId: string, tx?: DBExecutor): Promise<LocalPrincipalSigner>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Resolve a principal's trusted signer identity from the hub's own key store.
3
+ *
4
+ * The returned `publicKey` is the principal's active key as the hub holds it, so
5
+ * a verifier can check a signature against it without trusting any key on the
6
+ * wire. Throws when the principal has no active key -- an unresolvable signer
7
+ * must fail loudly rather than default.
8
+ */
9
+ export async function lookupLocalPrincipalSigner(principalKeyStore, principalId, tx) {
10
+ return {
11
+ kind: "local-principal",
12
+ principalId,
13
+ publicKey: await principalKeyStore.getPublicKey(principalId, tx),
14
+ };
15
+ }
@@ -1,3 +1,4 @@
1
+ import { type TenantSidecarCapabilityPolicy } from "@intx/types";
1
2
  import type { DB } from "./client.js";
2
3
  /**
3
4
  * Walks the tenant parentId chain from the given tenant up to the root.
@@ -7,6 +8,7 @@ import type { DB } from "./client.js";
7
8
  * chain that could omit inherited policy or configuration.
8
9
  */
9
10
  export declare function getAncestorChain(db: DB["db"], tenantId: string): Promise<string[]>;
11
+ export declare function resolveTenantSidecarCapabilityPolicies(db: DB["db"], tenantId: string): Promise<readonly TenantSidecarCapabilityPolicy[]>;
10
12
  /**
11
13
  * Returns the tenant and every distinct tenant in its subtree (children,
12
14
  * their children, and so on), discovered breadth-first. Used to find every
@@ -1,4 +1,6 @@
1
+ import { type } from "arktype";
1
2
  import { eq, inArray } from "drizzle-orm";
3
+ import { TenantConfig } from "@intx/types";
2
4
  import { tenant } from "./schema/tenants.js";
3
5
  /**
4
6
  * Walks the tenant parentId chain from the given tenant up to the root.
@@ -27,6 +29,31 @@ export async function getAncestorChain(db, tenantId) {
27
29
  }
28
30
  return chain;
29
31
  }
32
+ export async function resolveTenantSidecarCapabilityPolicies(db, tenantId) {
33
+ const chain = await getAncestorChain(db, tenantId);
34
+ const rows = await db.query.tenant.findMany({
35
+ where: inArray(tenant.id, chain),
36
+ columns: { id: true, config: true },
37
+ });
38
+ const rowsById = new Map(rows.map((row) => [row.id, row]));
39
+ const policies = [];
40
+ for (const ancestorId of chain) {
41
+ const row = rowsById.get(ancestorId);
42
+ if (row === undefined)
43
+ continue;
44
+ if (row.config === null)
45
+ continue;
46
+ const config = TenantConfig(row.config);
47
+ if (config instanceof type.errors) {
48
+ throw new Error(`Tenant ${row.id} has invalid configuration: ${config.summary}`);
49
+ }
50
+ const rules = config.sidecarPlacement?.capabilities;
51
+ if (rules !== undefined && rules.length > 0) {
52
+ policies.push({ tenantId: row.id, rules });
53
+ }
54
+ }
55
+ return policies;
56
+ }
30
57
  /**
31
58
  * Returns the tenant and every distinct tenant in its subtree (children,
32
59
  * their children, and so on), discovered breadth-first. Used to find every
@@ -0,0 +1,47 @@
1
+ import type { WorkflowProbeResultFrame } from "@intx/types/sidecar";
2
+ import type { WorkflowDefinitionSource } from "@intx/types/workflow-sources";
3
+ import type { DB, DBExecutor } from "./client.js";
4
+ import { workflowProbe, type WorkflowProbeStatus } from "./schema/index.js";
5
+ type DBHandle = DB["db"];
6
+ type WorkflowProbeResult = Omit<WorkflowProbeResultFrame, "type" | "requestId">;
7
+ type WorkflowProbeRow = typeof workflowProbe.$inferSelect;
8
+ export type WorkflowProbe = WorkflowProbeRow;
9
+ export type CreateWorkflowProbeArgs = {
10
+ readonly id: string;
11
+ readonly tenantId: string;
12
+ readonly definitionAssetId: string;
13
+ readonly source: WorkflowDefinitionSource;
14
+ readonly entry: string;
15
+ readonly pin?: string;
16
+ readonly provisionerId: string;
17
+ readonly provisionerApiVersion: 1;
18
+ readonly provisionerBindingFingerprint: string;
19
+ readonly now?: Date;
20
+ };
21
+ export type BindWorkflowProbeSidecarArgs = {
22
+ readonly probeId: string;
23
+ readonly sidecarId: string;
24
+ readonly tokenHashSha256: Uint8Array;
25
+ readonly now?: Date;
26
+ };
27
+ export declare function createWorkflowProbeStore(db: DBHandle): {
28
+ create(args: CreateWorkflowProbeArgs, tx?: DBExecutor): Promise<WorkflowProbe>;
29
+ bindSidecar(args: BindWorkflowProbeSidecarArgs, tx?: DBExecutor): Promise<WorkflowProbe | null>;
30
+ markProbing(args: {
31
+ probeId: string;
32
+ externalRef?: string;
33
+ now?: Date;
34
+ }): Promise<WorkflowProbe | null>;
35
+ recordResult(probeId: string, result: WorkflowProbeResult, now?: Date): Promise<WorkflowProbe | null>;
36
+ transition(probeId: string, from: readonly WorkflowProbeStatus[], to: WorkflowProbeStatus, opts?: {
37
+ failureCode?: string;
38
+ failureMessage?: string;
39
+ now?: Date;
40
+ tx?: DBExecutor;
41
+ }): Promise<WorkflowProbe | null>;
42
+ get(probeId: string): Promise<WorkflowProbe | null>;
43
+ listActive(): Promise<WorkflowProbe[]>;
44
+ listReleasing(): Promise<WorkflowProbe[]>;
45
+ };
46
+ export type WorkflowProbeStore = ReturnType<typeof createWorkflowProbeStore>;
47
+ export {};
@@ -0,0 +1,124 @@
1
+ import { and, eq, inArray, sql } from "drizzle-orm";
2
+ import { sidecar, workflowProbe } from "./schema/index.js";
3
+ const activeStatuses = [
4
+ "pending",
5
+ "provisioning",
6
+ "probing",
7
+ "releasing",
8
+ ];
9
+ function timestamp(now) {
10
+ return now ?? sql `now()`;
11
+ }
12
+ export function createWorkflowProbeStore(db) {
13
+ return {
14
+ async create(args, tx) {
15
+ const createdAt = timestamp(args.now);
16
+ const [created] = await (tx ?? db)
17
+ .insert(workflowProbe)
18
+ .values({
19
+ id: args.id,
20
+ tenantId: args.tenantId,
21
+ definitionAssetId: args.definitionAssetId,
22
+ source: args.source,
23
+ entry: args.entry,
24
+ ...(args.pin !== undefined ? { pin: args.pin } : {}),
25
+ provisionerId: args.provisionerId,
26
+ provisionerApiVersion: args.provisionerApiVersion,
27
+ provisionerBindingFingerprint: args.provisionerBindingFingerprint,
28
+ createdAt,
29
+ updatedAt: createdAt,
30
+ })
31
+ .returning();
32
+ if (created === undefined) {
33
+ throw new Error(`Failed to create workflow probe ${args.id}`);
34
+ }
35
+ return created;
36
+ },
37
+ async bindSidecar(args, tx) {
38
+ const bind = async (executor) => {
39
+ const updatedAt = timestamp(args.now);
40
+ const [probe] = await executor
41
+ .select({ status: workflowProbe.status })
42
+ .from(workflowProbe)
43
+ .where(eq(workflowProbe.id, args.probeId))
44
+ .limit(1)
45
+ .for("update");
46
+ if (probe?.status !== "pending")
47
+ return null;
48
+ await executor.insert(sidecar).values({
49
+ id: args.sidecarId,
50
+ url: null,
51
+ tokenHashSha256: args.tokenHashSha256,
52
+ status: "offline",
53
+ createdAt: updatedAt,
54
+ updatedAt,
55
+ });
56
+ const [updated] = await executor
57
+ .update(workflowProbe)
58
+ .set({
59
+ sidecarId: args.sidecarId,
60
+ status: "provisioning",
61
+ updatedAt,
62
+ })
63
+ .where(eq(workflowProbe.id, args.probeId))
64
+ .returning();
65
+ return updated ?? null;
66
+ };
67
+ return tx === undefined ? db.transaction(bind) : bind(tx);
68
+ },
69
+ async markProbing(args) {
70
+ const [updated] = await db
71
+ .update(workflowProbe)
72
+ .set({
73
+ status: "probing",
74
+ ...(args.externalRef !== undefined
75
+ ? { externalRef: args.externalRef }
76
+ : {}),
77
+ updatedAt: timestamp(args.now),
78
+ })
79
+ .where(and(eq(workflowProbe.id, args.probeId), eq(workflowProbe.status, "provisioning")))
80
+ .returning();
81
+ return updated ?? null;
82
+ },
83
+ async recordResult(probeId, result, now) {
84
+ const [updated] = await db
85
+ .update(workflowProbe)
86
+ .set({ result, updatedAt: timestamp(now) })
87
+ .where(and(eq(workflowProbe.id, probeId), eq(workflowProbe.status, "probing")))
88
+ .returning();
89
+ return updated ?? null;
90
+ },
91
+ async transition(probeId, from, to, opts) {
92
+ const [updated] = await (opts?.tx ?? db)
93
+ .update(workflowProbe)
94
+ .set({
95
+ status: to,
96
+ ...(opts?.failureCode !== undefined
97
+ ? { failureCode: opts.failureCode }
98
+ : {}),
99
+ ...(opts?.failureMessage !== undefined
100
+ ? { failureMessage: opts.failureMessage }
101
+ : {}),
102
+ updatedAt: timestamp(opts?.now),
103
+ })
104
+ .where(and(eq(workflowProbe.id, probeId), inArray(workflowProbe.status, [...from])))
105
+ .returning();
106
+ return updated ?? null;
107
+ },
108
+ async get(probeId) {
109
+ return ((await db.query.workflowProbe.findFirst({
110
+ where: eq(workflowProbe.id, probeId),
111
+ })) ?? null);
112
+ },
113
+ async listActive() {
114
+ return db.query.workflowProbe.findMany({
115
+ where: inArray(workflowProbe.status, [...activeStatuses]),
116
+ });
117
+ },
118
+ async listReleasing() {
119
+ return db.query.workflowProbe.findMany({
120
+ where: eq(workflowProbe.status, "releasing"),
121
+ });
122
+ },
123
+ };
124
+ }