@mycreat-labs/integrations-sdk 0.0.0-stage → 0.1.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 (54) hide show
  1. package/README.md +54 -2
  2. package/dist/adapters/bullmq.cjs +110 -0
  3. package/dist/adapters/bullmq.cjs.map +1 -0
  4. package/dist/adapters/bullmq.d.cts +11 -0
  5. package/dist/adapters/bullmq.d.ts +11 -0
  6. package/dist/adapters/bullmq.js +85 -0
  7. package/dist/adapters/bullmq.js.map +1 -0
  8. package/dist/adapters/postgres.cjs +396 -0
  9. package/dist/adapters/postgres.cjs.map +1 -0
  10. package/dist/adapters/postgres.d.cts +51 -0
  11. package/dist/adapters/postgres.d.ts +51 -0
  12. package/dist/adapters/postgres.js +368 -0
  13. package/dist/adapters/postgres.js.map +1 -0
  14. package/dist/adapters/prisma.cjs +414 -0
  15. package/dist/adapters/prisma.cjs.map +1 -0
  16. package/dist/adapters/prisma.d.cts +15 -0
  17. package/dist/adapters/prisma.d.ts +15 -0
  18. package/dist/adapters/prisma.js +386 -0
  19. package/dist/adapters/prisma.js.map +1 -0
  20. package/dist/adapters/redis.cjs +125 -0
  21. package/dist/adapters/redis.cjs.map +1 -0
  22. package/dist/adapters/redis.d.cts +20 -0
  23. package/dist/adapters/redis.d.ts +20 -0
  24. package/dist/adapters/redis.js +98 -0
  25. package/dist/adapters/redis.js.map +1 -0
  26. package/dist/errors-3hDEtzuZ.d.cts +11 -0
  27. package/dist/errors-3hDEtzuZ.d.ts +11 -0
  28. package/dist/index.cjs +1328 -0
  29. package/dist/index.cjs.map +1 -0
  30. package/dist/index.d.cts +128 -0
  31. package/dist/index.d.ts +128 -0
  32. package/dist/index.js +1297 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/providers/index.cjs +1287 -0
  35. package/dist/providers/index.cjs.map +1 -0
  36. package/dist/providers/index.d.cts +144 -0
  37. package/dist/providers/index.d.ts +144 -0
  38. package/dist/providers/index.js +1252 -0
  39. package/dist/providers/index.js.map +1 -0
  40. package/dist/testing.cjs +275 -0
  41. package/dist/testing.cjs.map +1 -0
  42. package/dist/testing.d.cts +20 -0
  43. package/dist/testing.d.ts +20 -0
  44. package/dist/testing.js +248 -0
  45. package/dist/testing.js.map +1 -0
  46. package/dist/types-Y4-DoJ1A.d.cts +260 -0
  47. package/dist/types-Y4-DoJ1A.d.ts +260 -0
  48. package/docs/migration.md +23 -0
  49. package/docs/security.md +26 -0
  50. package/docs/verification.md +22 -0
  51. package/examples/embedded-host.ts +62 -0
  52. package/migrations/0001_initial.sql +40 -0
  53. package/package.json +90 -4
  54. package/schema.prisma +80 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,1328 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ IntegrationError: () => IntegrationError,
24
+ createIntegrationSdk: () => createIntegrationSdk,
25
+ createRuntime: () => createRuntime,
26
+ createTokenVault: () => createTokenVault,
27
+ integrationError: () => integrationError
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/errors.ts
32
+ var IntegrationError = class extends Error {
33
+ constructor(code, disposition = "permanent_failure", retryAfterMs) {
34
+ super(code);
35
+ this.code = code;
36
+ this.disposition = disposition;
37
+ this.retryAfterMs = retryAfterMs;
38
+ this.name = "IntegrationError";
39
+ }
40
+ code;
41
+ disposition;
42
+ retryAfterMs;
43
+ sdkError = "@mycreat-labs/integrations-sdk/v1";
44
+ };
45
+ function integrationError(error) {
46
+ if (error instanceof IntegrationError) return error;
47
+ if (error instanceof Error && "sdkError" in error && error.sdkError === "@mycreat-labs/integrations-sdk/v1" && "code" in error && typeof error.code === "string" && /^[a-zA-Z0-9_]{1,100}$/.test(error.code) && "disposition" in error && [
48
+ "retryable",
49
+ "requires_reauth",
50
+ "outcome_unknown",
51
+ "permanent_failure"
52
+ ].includes(String(error.disposition))) {
53
+ const delay = "retryAfterMs" in error && typeof error.retryAfterMs === "number" && Number.isFinite(error.retryAfterMs) && error.retryAfterMs >= 0 ? error.retryAfterMs : void 0;
54
+ return new IntegrationError(
55
+ error.code,
56
+ error.disposition,
57
+ delay
58
+ );
59
+ }
60
+ return new IntegrationError("provider_unavailable", "outcome_unknown");
61
+ }
62
+
63
+ // src/crypto.ts
64
+ var import_node_crypto = require("crypto");
65
+ function createTokenVault(options) {
66
+ const keys = new Map(
67
+ Object.entries(options.keys).map(([id, value]) => [
68
+ id,
69
+ Buffer.from(value, "base64")
70
+ ])
71
+ );
72
+ if (!keys.has(options.activeKeyId) || [...keys.values()].some((value) => value.length !== 32))
73
+ throw new IntegrationError("invalid_vault_configuration");
74
+ return {
75
+ async seal(value, context) {
76
+ const iv = (0, import_node_crypto.randomBytes)(12);
77
+ const cipher = (0, import_node_crypto.createCipheriv)(
78
+ "aes-256-gcm",
79
+ keys.get(options.activeKeyId),
80
+ iv
81
+ );
82
+ cipher.setAAD(
83
+ Buffer.from(`integration-sdk:v1:${options.activeKeyId}:${context}`)
84
+ );
85
+ const data = Buffer.concat([
86
+ cipher.update(JSON.stringify(value), "utf8"),
87
+ cipher.final()
88
+ ]);
89
+ return {
90
+ keyId: options.activeKeyId,
91
+ value: Buffer.concat([iv, cipher.getAuthTag(), data]).toString(
92
+ "base64url"
93
+ )
94
+ };
95
+ },
96
+ async open(sealed, context) {
97
+ try {
98
+ const key = keys.get(sealed.keyId);
99
+ if (!key) throw new Error("unknown key");
100
+ const buffer = Buffer.from(sealed.value, "base64url");
101
+ if (buffer.length < 28) throw new Error("invalid ciphertext");
102
+ const decipher = (0, import_node_crypto.createDecipheriv)(
103
+ "aes-256-gcm",
104
+ key,
105
+ buffer.subarray(0, 12)
106
+ );
107
+ decipher.setAAD(
108
+ Buffer.from(`integration-sdk:v1:${sealed.keyId}:${context}`)
109
+ );
110
+ decipher.setAuthTag(buffer.subarray(12, 28));
111
+ return JSON.parse(
112
+ Buffer.concat([
113
+ decipher.update(buffer.subarray(28)),
114
+ decipher.final()
115
+ ]).toString("utf8")
116
+ );
117
+ } catch {
118
+ throw new IntegrationError("credential_decryption_failed");
119
+ }
120
+ }
121
+ };
122
+ }
123
+
124
+ // src/sdk.ts
125
+ var import_node_crypto2 = require("crypto");
126
+ var hash = (value) => (0, import_node_crypto2.createHash)("sha256").update(value).digest("hex");
127
+ var pkce = (value) => (0, import_node_crypto2.createHash)("sha256").update(value).digest("base64url");
128
+ var random = () => (0, import_node_crypto2.randomBytes)(32).toString("base64url");
129
+ function canonical(value) {
130
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
131
+ if (value !== null && typeof value === "object")
132
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
133
+ return JSON.stringify(value);
134
+ }
135
+ var required = (value, name, max = 512) => {
136
+ if (typeof value !== "string" || !value.trim() || value.length > max)
137
+ throw new IntegrationError(`invalid_${name}`);
138
+ return value;
139
+ };
140
+ function createIntegrationSdk(options) {
141
+ required(options.namespace, "namespace", 128);
142
+ const providers = /* @__PURE__ */ new Map();
143
+ for (const provider of options.providers) {
144
+ if (providers.has(provider.key))
145
+ throw new IntegrationError("duplicate_provider");
146
+ required(provider.registrationId, "registration");
147
+ providers.set(provider.key, provider);
148
+ }
149
+ const now = options.clock ?? Date.now;
150
+ const request = options.fetch ?? globalThis.fetch;
151
+ const apiFetch = (provider, budget = "api") => async (input, init) => {
152
+ if (options.rateLimiter) {
153
+ const signal = init?.signal;
154
+ const deadline = Date.now() + 1e4;
155
+ for (; ; ) {
156
+ let admission;
157
+ let timeout;
158
+ try {
159
+ admission = await Promise.race([
160
+ options.rateLimiter.take(
161
+ hash(
162
+ `${options.namespace}\0${provider.key}\0${provider.registrationId}\0${budget}`
163
+ )
164
+ ),
165
+ new Promise((_, reject) => {
166
+ timeout = setTimeout(
167
+ () => reject(new Error("admission timeout")),
168
+ 1e3
169
+ );
170
+ })
171
+ ]);
172
+ if (typeof admission.allowed !== "boolean" || !Number.isFinite(admission.retryAfterMs) || admission.retryAfterMs < 0)
173
+ throw new Error("invalid admission");
174
+ } catch {
175
+ throw Object.assign(
176
+ new IntegrationError("rate_limited", "retryable", 1e3),
177
+ { definite: true }
178
+ );
179
+ } finally {
180
+ if (timeout) clearTimeout(timeout);
181
+ }
182
+ if (signal?.aborted)
183
+ throw Object.assign(
184
+ new IntegrationError("rate_limited", "retryable", 1e3),
185
+ { definite: true }
186
+ );
187
+ if (admission.allowed) break;
188
+ const delay = Math.max(1, admission.retryAfterMs);
189
+ if (Date.now() + delay > deadline)
190
+ throw Object.assign(
191
+ new IntegrationError(
192
+ "rate_limited",
193
+ "retryable",
194
+ Math.min(delay, 864e5)
195
+ ),
196
+ { definite: true }
197
+ );
198
+ await new Promise((resolve, reject) => {
199
+ const onAbort = () => {
200
+ clearTimeout(wait);
201
+ signal?.removeEventListener("abort", onAbort);
202
+ reject(
203
+ Object.assign(
204
+ new IntegrationError("rate_limited", "retryable", 1e3),
205
+ { definite: true }
206
+ )
207
+ );
208
+ };
209
+ const wait = setTimeout(() => {
210
+ signal?.removeEventListener("abort", onAbort);
211
+ resolve();
212
+ }, delay);
213
+ signal?.addEventListener("abort", onAbort, { once: true });
214
+ if (signal?.aborted) onAbort();
215
+ });
216
+ }
217
+ }
218
+ return request(input, init);
219
+ };
220
+ const leaseMs = options.leaseMs ?? 12e4;
221
+ const maxAttempts = options.maximumAttempts ?? 12;
222
+ const scope = (ownerId) => ({
223
+ namespace: options.namespace,
224
+ ownerId: required(ownerId, "owner")
225
+ });
226
+ const providerFor = (key, registrationId) => {
227
+ const provider = providers.get(key);
228
+ if (!provider || registrationId && provider.registrationId !== registrationId)
229
+ throw new IntegrationError("provider_unavailable");
230
+ return provider;
231
+ };
232
+ const authorizationKey = (provider) => hash(`${provider.key}\0${provider.registrationId}`);
233
+ const grantContext = (grant) => `${grant.namespace}:grant:${grant.ownerId}:${grant.registrationId}:${grant.id}`;
234
+ const flowContext = (flow) => `${flow.namespace}:flow:${flow.purpose}:${flow.id}`;
235
+ const operationContext = (op) => `${op.namespace}:operation:${op.ownerId}:${op.id}`;
236
+ async function connection(ownerId, id) {
237
+ const value = await options.store.transaction(async (tx) => {
238
+ await tx.lockScope(scope(ownerId));
239
+ return tx.getConnection(scope(ownerId), id);
240
+ });
241
+ if (!value) throw new IntegrationError("connection_not_found");
242
+ providerFor(value.provider, value.registrationId);
243
+ if (value.status !== "active")
244
+ throw new IntegrationError("reauth_required", "requires_reauth");
245
+ return value;
246
+ }
247
+ async function start(input, purpose, native = false) {
248
+ const owner = scope(input.ownerId);
249
+ required(input.binding, "binding");
250
+ if (input.binding.length < 32)
251
+ throw new IntegrationError("invalid_binding");
252
+ const provider = providerFor(input.provider);
253
+ if (!provider.purposes.includes(purpose) || !native && !provider.authorization || native && !provider.verifyNative)
254
+ throw new IntegrationError("unsupported_capability");
255
+ let current;
256
+ if (input.connectionId) {
257
+ const existing = await options.store.transaction(
258
+ (tx) => tx.getConnection(owner, input.connectionId)
259
+ );
260
+ if (!existing || existing.provider !== provider.key || existing.registrationId !== provider.registrationId || existing.status === "revoked")
261
+ throw new IntegrationError("connection_not_found");
262
+ current = existing;
263
+ }
264
+ const state = random();
265
+ const verifier = random();
266
+ const nonce = random();
267
+ const flow = {
268
+ id: hash(state),
269
+ ...owner,
270
+ provider: provider.key,
271
+ registrationId: provider.registrationId,
272
+ purpose,
273
+ bindingHash: hash(input.binding),
274
+ expiresAt: now() + 6e5,
275
+ secret: { keyId: "", value: "" },
276
+ ...current ? { connectionId: current.id, generation: current.generation } : {}
277
+ };
278
+ flow.secret = await options.vault.seal(
279
+ { verifier, nonce, native },
280
+ flowContext(flow)
281
+ );
282
+ await options.store.transaction(async (tx) => {
283
+ if (purpose === "connect") {
284
+ await tx.lockScope(owner);
285
+ const connections = await tx.listConnections(owner);
286
+ for (const connection2 of connections.filter(
287
+ (c) => c.provider === provider.key && c.registrationId === provider.registrationId && c.status === "revoked"
288
+ )) {
289
+ if (await tx.hasPendingRevocation(owner, connection2.id))
290
+ throw new IntegrationError("revocation_pending", "retryable", 3e3);
291
+ }
292
+ flow.authorizationEpoch = await tx.authorizationEpoch(
293
+ owner,
294
+ authorizationKey(provider),
295
+ true
296
+ );
297
+ }
298
+ await tx.insertFlow(flow);
299
+ });
300
+ return {
301
+ state,
302
+ nonce,
303
+ authorizationUrl: native ? "" : provider.authorization.url({
304
+ state,
305
+ verifier,
306
+ challenge: pkce(verifier),
307
+ nonce,
308
+ purpose
309
+ })
310
+ };
311
+ }
312
+ async function consume(input, purpose, native = false) {
313
+ required(input.state, "state");
314
+ required(input.binding, "binding");
315
+ const flow = await options.store.transaction(
316
+ (tx) => tx.consumeFlow(
317
+ options.namespace,
318
+ hash(input.state),
319
+ hash(input.binding),
320
+ purpose,
321
+ now()
322
+ )
323
+ );
324
+ if (!flow || flow.ownerId !== input.ownerId)
325
+ throw new IntegrationError("authorization_expired");
326
+ const secret = await options.vault.open(
327
+ flow.secret,
328
+ flowContext(flow)
329
+ );
330
+ if (secret.native !== native)
331
+ throw new IntegrationError("authorization_purpose_mismatch");
332
+ return {
333
+ flow,
334
+ secret,
335
+ provider: providerFor(flow.provider, flow.registrationId)
336
+ };
337
+ }
338
+ async function credentials(current, signal, force = false, rejectedAccessToken) {
339
+ const provider = providerFor(current.provider, current.registrationId);
340
+ const owner = scope(current.ownerId);
341
+ const lease = (0, import_node_crypto2.randomUUID)();
342
+ const result = await options.store.transaction(async (tx) => {
343
+ await tx.lockScope(owner);
344
+ const live = await tx.getConnection(owner, current.id, true);
345
+ if (!live || live.status !== "active" || live.generation !== current.generation)
346
+ throw new IntegrationError("connection_revoked", "requires_reauth");
347
+ const grant = await tx.getGrant(owner, live.grantId, true);
348
+ if (!grant || grant.status !== "active")
349
+ throw new IntegrationError("reauth_required", "requires_reauth");
350
+ if (grant.authorizationLease) {
351
+ if ((grant.authorizationUntil ?? 0) > now())
352
+ throw new IntegrationError(
353
+ "authorization_in_progress",
354
+ "retryable",
355
+ 2e3
356
+ );
357
+ const invalidated = {
358
+ ...grant,
359
+ status: "reauth_required",
360
+ version: grant.version + 1,
361
+ authorizationLease: void 0,
362
+ authorizationUntil: void 0
363
+ };
364
+ await tx.saveGrant(invalidated, grant.version);
365
+ return { grant: invalidated, refresh: false };
366
+ }
367
+ if (!force && (!grant.expiresAt || grant.expiresAt > now() + 3e4))
368
+ return { grant, refresh: false };
369
+ if (force && rejectedAccessToken !== void 0) {
370
+ const latest = await options.vault.open(
371
+ grant.credentials,
372
+ grantContext(grant)
373
+ );
374
+ if (latest.accessToken !== rejectedAccessToken)
375
+ return { grant, refresh: false };
376
+ }
377
+ if (!provider.refresh)
378
+ throw new IntegrationError("reauth_required", "requires_reauth");
379
+ if (grant.refreshLease && (grant.refreshUntil ?? 0) > now())
380
+ throw new IntegrationError("refresh_in_progress", "retryable", 2e3);
381
+ if (grant.refreshLease) {
382
+ const invalidated = {
383
+ ...grant,
384
+ status: "reauth_required",
385
+ version: grant.version + 1,
386
+ refreshLease: void 0,
387
+ refreshUntil: void 0
388
+ };
389
+ await tx.saveGrant(invalidated, grant.version);
390
+ return { grant: invalidated, refresh: false };
391
+ }
392
+ if (!await tx.saveGrant(
393
+ { ...grant, refreshLease: lease, refreshUntil: now() + leaseMs },
394
+ grant.version
395
+ ))
396
+ throw new IntegrationError("credential_conflict", "retryable", 2e3);
397
+ return { grant, refresh: true };
398
+ });
399
+ if (result.grant.status !== "active")
400
+ throw new IntegrationError("reauth_required", "requires_reauth");
401
+ const material = await options.vault.open(
402
+ result.grant.credentials,
403
+ grantContext(result.grant)
404
+ );
405
+ if (!result.refresh) return material;
406
+ let refreshReturned = false;
407
+ try {
408
+ const refreshed = await provider.refresh(material, {
409
+ fetch: apiFetch(provider, "credentials"),
410
+ signal
411
+ });
412
+ refreshReturned = true;
413
+ const sealed = await options.vault.seal(
414
+ refreshed,
415
+ grantContext(result.grant)
416
+ );
417
+ await options.store.transaction(async (tx) => {
418
+ const live = await tx.getConnection(owner, current.id, true);
419
+ const grant = await tx.getGrant(owner, current.grantId, true);
420
+ if (!live || live.status !== "active" || live.generation !== current.generation || !grant || grant.status !== "active" || grant.version !== result.grant.version || grant.refreshLease !== lease || (grant.refreshUntil ?? 0) <= now())
421
+ throw new IntegrationError("credential_conflict", "requires_reauth");
422
+ if (!await tx.saveGrant(
423
+ {
424
+ ...grant,
425
+ credentials: sealed,
426
+ expiresAt: refreshed.expiresAt,
427
+ version: grant.version + 1,
428
+ refreshLease: void 0,
429
+ refreshUntil: void 0
430
+ },
431
+ grant.version
432
+ ))
433
+ throw new IntegrationError("credential_conflict", "requires_reauth");
434
+ });
435
+ return refreshed;
436
+ } catch (error) {
437
+ const failure = integrationError(error);
438
+ const definiteRetry = !refreshReturned && failure.disposition === "retryable" && error instanceof Error && "definite" in error && error.definite === true;
439
+ await options.store.transaction(async (tx) => {
440
+ const grant = await tx.getGrant(owner, current.grantId, true);
441
+ if (grant?.version === result.grant.version && grant.refreshLease === lease)
442
+ await tx.saveGrant(
443
+ {
444
+ ...grant,
445
+ status: definiteRetry ? "active" : "reauth_required",
446
+ version: grant.version + 1,
447
+ refreshLease: void 0,
448
+ refreshUntil: void 0
449
+ },
450
+ grant.version
451
+ );
452
+ });
453
+ if (definiteRetry) throw failure;
454
+ options.logger?.warn?.(
455
+ { provider: provider.key, code: integrationError(error).code },
456
+ "Credential refresh requires authorization"
457
+ );
458
+ throw new IntegrationError("reauth_required", "requires_reauth");
459
+ }
460
+ }
461
+ async function enqueue(input) {
462
+ required(input.idempotencyKey, "idempotency_key");
463
+ const current = await connection(input.ownerId, input.connectionId);
464
+ const action = providerFor(current.provider, current.registrationId).actions?.[input.action];
465
+ if (!action) throw new IntegrationError("unsupported_action");
466
+ const validated = action.validate(input.input);
467
+ const payload = canonical(validated);
468
+ if (Buffer.byteLength(payload) > 2e6)
469
+ throw new IntegrationError("input_too_large");
470
+ const operation = {
471
+ id: (0, import_node_crypto2.randomUUID)(),
472
+ ...scope(input.ownerId),
473
+ connectionId: current.id,
474
+ generation: current.generation,
475
+ action: input.action,
476
+ idempotencyKey: input.idempotencyKey,
477
+ fingerprint: hash(
478
+ canonical({
479
+ connectionId: current.id,
480
+ generation: current.generation,
481
+ input: validated,
482
+ hostContext: input.hostContext ?? null
483
+ })
484
+ ),
485
+ input: { keyId: "", value: "" },
486
+ ...input.hostContext !== void 0 ? { hostContext: input.hostContext } : {},
487
+ status: "pending",
488
+ version: 1,
489
+ attempts: 0,
490
+ availableAt: now(),
491
+ checkpoint: null,
492
+ dispatching: false,
493
+ createdAt: now(),
494
+ updatedAt: now()
495
+ };
496
+ operation.input = await options.vault.seal(
497
+ validated,
498
+ operationContext(operation)
499
+ );
500
+ return options.store.transaction(async (tx) => {
501
+ await tx.lockScope(scope(input.ownerId));
502
+ const live = await tx.getConnection(
503
+ scope(input.ownerId),
504
+ current.id,
505
+ true
506
+ );
507
+ if (!live || live.status !== "active" || live.generation !== current.generation)
508
+ throw new IntegrationError("connection_revoked");
509
+ const persisted = await tx.insertOperation(operation);
510
+ if (persisted.fingerprint !== operation.fingerprint)
511
+ throw new IntegrationError("idempotency_conflict");
512
+ return persisted;
513
+ });
514
+ }
515
+ async function getOperation(ownerId, id) {
516
+ const op = await options.store.transaction(
517
+ (tx) => tx.getOperation(scope(ownerId), id)
518
+ );
519
+ if (!op) throw new IntegrationError("operation_not_found");
520
+ return op;
521
+ }
522
+ async function process(message) {
523
+ const lease = (0, import_node_crypto2.randomUUID)();
524
+ let operation = await options.store.transaction(
525
+ (tx) => tx.claimOperation(
526
+ options.namespace,
527
+ message.taskId,
528
+ message.version,
529
+ now(),
530
+ lease,
531
+ now() + leaseMs
532
+ )
533
+ );
534
+ if (!operation) return;
535
+ const owner = scope(operation.ownerId);
536
+ let lost = false;
537
+ let renewing;
538
+ const abort = new AbortController();
539
+ const renew = async () => {
540
+ await options.store.transaction(async (tx) => {
541
+ const live = await tx.getOperation(owner, operation.id, true);
542
+ if (!live || live.status !== "running" || live.leaseToken !== lease || (live.leaseUntil ?? 0) <= now())
543
+ throw new IntegrationError("lease_lost");
544
+ const next = { ...live, leaseUntil: now() + leaseMs, updatedAt: now() };
545
+ if (!await tx.saveOperation(next, live.version, lease))
546
+ throw new IntegrationError("lease_lost");
547
+ operation = next;
548
+ });
549
+ };
550
+ const timer = setInterval(
551
+ () => {
552
+ if (renewing) return;
553
+ renewing = renew().catch(() => {
554
+ lost = true;
555
+ abort.abort();
556
+ }).finally(() => {
557
+ renewing = void 0;
558
+ });
559
+ },
560
+ Math.max(100, Math.floor(leaseMs / 3))
561
+ );
562
+ timer.unref?.();
563
+ let input = null;
564
+ let attemptedWrite = false;
565
+ let remoteSucceeded = false;
566
+ let dispatchedConnection;
567
+ let dispatchedAccessToken;
568
+ let authenticationRecovered = false;
569
+ try {
570
+ input = await options.vault.open(
571
+ operation.input,
572
+ operationContext(operation)
573
+ );
574
+ const current = await options.store.transaction(
575
+ (tx) => tx.getConnection(owner, operation.connectionId)
576
+ );
577
+ if (!current || current.generation !== operation.generation || current.status !== "active" && operation.action !== "__sdk.revoke")
578
+ throw new IntegrationError("connection_revoked");
579
+ if (operation.action === "__sdk.revoke" && !providers.has(current.provider))
580
+ throw new IntegrationError(
581
+ "provider_configuration_missing",
582
+ "retryable",
583
+ 6e4
584
+ );
585
+ const provider = providerFor(current.provider, current.registrationId);
586
+ let material;
587
+ if (operation.action === "__sdk.revoke") {
588
+ const grant = await options.store.transaction(
589
+ (tx) => tx.getGrant(owner, current.grantId)
590
+ );
591
+ if (!grant || grant.status !== "revoked")
592
+ throw new IntegrationError("invalid_revocation");
593
+ material = await options.vault.open(
594
+ grant.credentials,
595
+ grantContext(grant)
596
+ );
597
+ } else material = await credentials(current, abort.signal);
598
+ const context = {
599
+ credentials: material,
600
+ fetch: apiFetch(provider),
601
+ signal: abort.signal,
602
+ operationId: operation.id,
603
+ checkpoint: operation.checkpoint,
604
+ saveCheckpoint: async (checkpoint) => {
605
+ await options.store.transaction(async (tx) => {
606
+ const live = await tx.getOperation(owner, operation.id, true);
607
+ if (!live || live.leaseToken !== lease || live.status !== "running")
608
+ throw new IntegrationError("lease_lost");
609
+ const next = { ...live, checkpoint, updatedAt: now() };
610
+ if (!await tx.saveOperation(next, live.version, lease))
611
+ throw new IntegrationError("lease_lost");
612
+ operation = next;
613
+ });
614
+ }
615
+ };
616
+ let result;
617
+ const action = provider.actions?.[operation.action];
618
+ if (operation.action === "__sdk.revoke") {
619
+ await provider.revoke?.(material, {
620
+ fetch: request,
621
+ signal: abort.signal
622
+ });
623
+ result = { revoked: true };
624
+ remoteSucceeded = true;
625
+ } else if (!action) throw new IntegrationError("unsupported_action");
626
+ else if (operation.dispatching) {
627
+ await options.store.transaction(async (tx) => {
628
+ await tx.lockScope(owner);
629
+ await options.hooks?.beforeExecute?.(
630
+ tx.nativeContext,
631
+ operation,
632
+ input
633
+ );
634
+ });
635
+ if (!action.reconcile)
636
+ throw new IntegrationError("outcome_unknown", "outcome_unknown");
637
+ const reconciled = await action.reconcile(input, context);
638
+ if (!reconciled.resolved)
639
+ throw new IntegrationError("outcome_unknown", "outcome_unknown");
640
+ result = reconciled.result;
641
+ remoteSucceeded = true;
642
+ } else {
643
+ await options.store.transaction(async (tx) => {
644
+ await tx.lockScope(owner);
645
+ const live = await tx.getOperation(owner, operation.id, true);
646
+ const active = await tx.getConnection(owner, current.id, true);
647
+ if (!active || active.generation !== current.generation || active.status !== "active")
648
+ throw new IntegrationError("connection_revoked");
649
+ if (!live || live.leaseToken !== lease)
650
+ throw new IntegrationError("lease_lost");
651
+ await options.hooks?.beforeExecute?.(tx.nativeContext, live, input);
652
+ const next = { ...live, dispatching: true, updatedAt: now() };
653
+ if (!await tx.saveOperation(next, live.version, lease))
654
+ throw new IntegrationError("lease_lost");
655
+ operation = next;
656
+ });
657
+ attemptedWrite = true;
658
+ dispatchedConnection = current;
659
+ dispatchedAccessToken = material.accessToken;
660
+ result = await action.execute(input, context);
661
+ remoteSucceeded = true;
662
+ }
663
+ if (renewing) await renewing;
664
+ if (lost || abort.signal.aborted) return;
665
+ await options.store.transaction(async (tx) => {
666
+ const erased = await tx.lockScope(
667
+ owner,
668
+ operation.action === "__sdk.revoke"
669
+ );
670
+ const live = await tx.getOperation(owner, operation.id, true);
671
+ const active = await tx.getConnection(owner, current.id, true);
672
+ if (!live || live.leaseToken !== lease || live.status !== "running")
673
+ throw new IntegrationError("lease_lost");
674
+ if (!active || active.generation !== current.generation || active.status !== "active" && operation.action !== "__sdk.revoke")
675
+ throw new IntegrationError("connection_revoked");
676
+ const next = {
677
+ ...live,
678
+ status: "succeeded",
679
+ result,
680
+ input: await options.vault.seal(null, operationContext(live)),
681
+ checkpoint: null,
682
+ leaseToken: void 0,
683
+ leaseUntil: void 0,
684
+ errorCode: void 0,
685
+ version: live.version + 1,
686
+ updatedAt: now()
687
+ };
688
+ const event = {
689
+ id: (0, import_node_crypto2.randomUUID)(),
690
+ ...owner,
691
+ operationId: next.id,
692
+ type: "operation.succeeded",
693
+ createdAt: now(),
694
+ data: result
695
+ };
696
+ if (!await tx.saveOperation(next, live.version, lease))
697
+ throw new IntegrationError("lease_lost");
698
+ await tx.insertEvent(event);
699
+ if (operation.action !== "__sdk.revoke")
700
+ await options.hooks?.onResult?.(tx.nativeContext, next, event, input);
701
+ if (operation.action === "__sdk.revoke" && erased)
702
+ await tx.eraseOwner(owner, true);
703
+ });
704
+ } catch (error) {
705
+ if (renewing) await renewing;
706
+ if (lost) return;
707
+ let failure = integrationError(error);
708
+ const definiteUnauthorized = failure.disposition === "requires_reauth" && error instanceof Error && "status" in error && error.status === 401 && "definite" in error && error.definite === true;
709
+ if (definiteUnauthorized && attemptedWrite && !remoteSucceeded && dispatchedConnection && providerFor(
710
+ dispatchedConnection.provider,
711
+ dispatchedConnection.registrationId
712
+ ).refresh) {
713
+ if ((operation.authRecoveries ?? 0) > 0) {
714
+ failure = new IntegrationError(
715
+ "credential_refresh_exhausted",
716
+ "requires_reauth"
717
+ );
718
+ } else {
719
+ try {
720
+ await credentials(
721
+ dispatchedConnection,
722
+ abort.signal,
723
+ true,
724
+ dispatchedAccessToken
725
+ );
726
+ authenticationRecovered = true;
727
+ failure = new IntegrationError(
728
+ "credential_refreshed",
729
+ "retryable",
730
+ 0
731
+ );
732
+ } catch (refreshError) {
733
+ failure = integrationError(refreshError);
734
+ }
735
+ }
736
+ }
737
+ await options.store.transaction(async (tx) => {
738
+ const live = await tx.getOperation(owner, operation.id, true);
739
+ if (!live || live.leaseToken !== lease || live.status !== "running" || (live.leaseUntil ?? 0) <= now())
740
+ return;
741
+ const retry = failure.disposition === "retryable" && live.attempts < maxAttempts;
742
+ const unknown = failure.disposition === "outcome_unknown" || live.dispatching && (!attemptedWrite || remoteSucceeded);
743
+ const next = {
744
+ ...live,
745
+ authRecoveries: Math.min(
746
+ 1,
747
+ (live.authRecoveries ?? 0) + (authenticationRecovered ? 1 : 0)
748
+ ),
749
+ status: ["connection_revoked", "owner_erased"].includes(failure.code) ? "cancelled" : unknown ? "outcome_unknown" : retry ? "pending" : failure.disposition === "requires_reauth" ? "blocked" : "failed",
750
+ dispatching: unknown ? live.dispatching : false,
751
+ availableAt: now() + Math.max(
752
+ failure.retryAfterMs ?? 0,
753
+ Math.min(36e5, 15e3 * 2 ** Math.min(live.attempts, 8))
754
+ ),
755
+ leaseToken: void 0,
756
+ leaseUntil: void 0,
757
+ errorCode: failure.code,
758
+ version: live.version + 1,
759
+ updatedAt: now()
760
+ };
761
+ if (unknown && live.attempts >= maxAttempts)
762
+ next.availableAt = Number.MAX_SAFE_INTEGER;
763
+ if (!await tx.saveOperation(next, live.version, lease)) return;
764
+ await tx.insertEvent({
765
+ id: (0, import_node_crypto2.randomUUID)(),
766
+ ...owner,
767
+ operationId: next.id,
768
+ type: `operation.${next.status}`,
769
+ createdAt: now(),
770
+ data: { code: failure.code }
771
+ });
772
+ });
773
+ options.logger?.warn?.(
774
+ { code: failure.code },
775
+ "Integration operation interrupted"
776
+ );
777
+ } finally {
778
+ clearInterval(timer);
779
+ abort.abort();
780
+ }
781
+ }
782
+ const sdk = {
783
+ namespace: options.namespace,
784
+ withStore: (store) => createIntegrationSdk({ ...options, store }),
785
+ assertCompatible: () => options.store.assertCompatible(),
786
+ catalog: () => [...providers.values()].map((p) => ({
787
+ key: p.key,
788
+ registrationId: p.registrationId,
789
+ purposes: [...p.purposes],
790
+ resources: Boolean(p.resources),
791
+ actions: Object.keys(p.actions ?? {}),
792
+ remoteRevocationSupported: Boolean(p.revoke)
793
+ })),
794
+ identity: {
795
+ start: (input) => start(input, "identity"),
796
+ startNative: (input) => start(input, "identity", true),
797
+ async complete(input) {
798
+ const { provider, secret } = await consume(input, "identity");
799
+ const result = await provider.authorization.exchange(
800
+ {
801
+ code: required(input.code, "code", 4096),
802
+ verifier: secret.verifier,
803
+ nonce: secret.nonce,
804
+ purpose: "identity"
805
+ },
806
+ request
807
+ );
808
+ if (!result.identity) throw new IntegrationError("invalid_identity");
809
+ return result.identity;
810
+ },
811
+ async verifyNative(input) {
812
+ const { provider, secret } = await consume(input, "identity", true);
813
+ return provider.verifyNative(
814
+ {
815
+ idToken: input.idToken,
816
+ nonce: secret.nonce,
817
+ authorizationCode: input.authorizationCode
818
+ },
819
+ request
820
+ );
821
+ }
822
+ },
823
+ connections: {
824
+ start: (input) => start(input, "connect"),
825
+ async complete(input) {
826
+ const { flow, secret, provider } = await consume(input, "connect");
827
+ const reservation = (0, import_node_crypto2.randomUUID)();
828
+ const reserved = [];
829
+ await options.store.transaction(async (tx) => {
830
+ await tx.lockScope(scope(input.ownerId));
831
+ if (flow.authorizationEpoch !== await tx.authorizationEpoch(
832
+ scope(input.ownerId),
833
+ authorizationKey(provider)
834
+ ))
835
+ throw new IntegrationError("authorization_conflict");
836
+ for (const current of (await tx.listConnections(scope(input.ownerId))).filter(
837
+ (c) => c.provider === provider.key && c.registrationId === provider.registrationId && c.status !== "revoked"
838
+ )) {
839
+ const previous = await tx.getGrant(
840
+ scope(input.ownerId),
841
+ current.grantId,
842
+ true
843
+ );
844
+ if (!previous || previous.status === "revoked") continue;
845
+ if (previous.refreshLease || previous.authorizationLease && (previous.authorizationUntil ?? 0) > now())
846
+ throw new IntegrationError("credential_busy", "retryable", 2e3);
847
+ if (!await tx.saveGrant(
848
+ {
849
+ ...previous,
850
+ version: previous.version + 1,
851
+ authorizationLease: reservation,
852
+ authorizationUntil: now() + 6e4
853
+ },
854
+ previous.version
855
+ ))
856
+ throw new IntegrationError("authorization_conflict");
857
+ reserved.push(previous.id);
858
+ }
859
+ });
860
+ let completed = false;
861
+ try {
862
+ const result = await provider.authorization.exchange(
863
+ {
864
+ code: required(input.code, "code", 4096),
865
+ verifier: secret.verifier,
866
+ nonce: secret.nonce,
867
+ purpose: "connect"
868
+ },
869
+ request
870
+ );
871
+ if (!result.credentials || !result.account)
872
+ throw new IntegrationError("invalid_connection");
873
+ const grant = {
874
+ id: (0, import_node_crypto2.randomUUID)(),
875
+ ...scope(input.ownerId),
876
+ provider: provider.key,
877
+ registrationId: provider.registrationId,
878
+ account: result.account,
879
+ version: 1,
880
+ status: "active",
881
+ credentials: { keyId: "", value: "" },
882
+ expiresAt: result.credentials.expiresAt
883
+ };
884
+ grant.credentials = await options.vault.seal(
885
+ result.credentials,
886
+ grantContext(grant)
887
+ );
888
+ const connected = await options.store.transaction(async (tx) => {
889
+ await tx.lockScope(scope(input.ownerId));
890
+ if (flow.authorizationEpoch !== await tx.authorizationEpoch(
891
+ scope(input.ownerId),
892
+ authorizationKey(provider)
893
+ ))
894
+ throw new IntegrationError("authorization_conflict");
895
+ await tx.assertAccountOwner(
896
+ scope(input.ownerId),
897
+ provider.key,
898
+ provider.registrationId,
899
+ result.account.id
900
+ );
901
+ const existing = flow.connectionId ? await tx.getConnection(
902
+ scope(input.ownerId),
903
+ flow.connectionId,
904
+ true
905
+ ) : (await tx.listConnections(scope(input.ownerId))).find(
906
+ (c) => c.provider === provider.key && c.registrationId === provider.registrationId && c.account.id === result.account.id
907
+ );
908
+ if (flow.connectionId && (!existing || existing.generation !== flow.generation || existing.status === "revoked" || existing.account.id !== result.account.id))
909
+ throw new IntegrationError("authorization_conflict");
910
+ if (existing) {
911
+ const previous = await tx.getGrant(
912
+ scope(input.ownerId),
913
+ existing.grantId,
914
+ true
915
+ );
916
+ if (previous && previous.status !== "revoked" && (previous.authorizationLease !== reservation || (previous.authorizationUntil ?? 0) <= now()))
917
+ throw new IntegrationError("authorization_conflict");
918
+ if (previous)
919
+ await tx.saveGrant(
920
+ {
921
+ ...previous,
922
+ status: "revoked",
923
+ version: previous.version + 1,
924
+ authorizationLease: void 0,
925
+ authorizationUntil: void 0
926
+ },
927
+ previous.version
928
+ );
929
+ await tx.insertGrant(grant);
930
+ const updated = {
931
+ ...existing,
932
+ grantId: grant.id,
933
+ generation: existing.generation + 1,
934
+ status: "active",
935
+ account: result.account,
936
+ updatedAt: now()
937
+ };
938
+ if (!await tx.saveConnection(updated, existing.generation))
939
+ throw new IntegrationError("authorization_conflict");
940
+ return updated;
941
+ }
942
+ await tx.insertGrant(grant);
943
+ const created = {
944
+ id: (0, import_node_crypto2.randomUUID)(),
945
+ ...scope(input.ownerId),
946
+ provider: provider.key,
947
+ registrationId: provider.registrationId,
948
+ grantId: grant.id,
949
+ account: result.account,
950
+ generation: 1,
951
+ status: "active",
952
+ createdAt: now(),
953
+ updatedAt: now()
954
+ };
955
+ await tx.insertConnection(created);
956
+ return created;
957
+ });
958
+ completed = true;
959
+ return connected;
960
+ } finally {
961
+ await options.store.transaction(async (tx) => {
962
+ await tx.lockScope(scope(input.ownerId), true);
963
+ for (const id of reserved) {
964
+ const grant = await tx.getGrant(scope(input.ownerId), id, true);
965
+ if (grant?.authorizationLease !== reservation) continue;
966
+ await tx.saveGrant(
967
+ {
968
+ ...grant,
969
+ status: !completed && grant.status === "active" ? "reauth_required" : grant.status,
970
+ version: grant.version + 1,
971
+ authorizationLease: void 0,
972
+ authorizationUntil: void 0
973
+ },
974
+ grant.version
975
+ );
976
+ }
977
+ }).catch(() => {
978
+ options.logger?.warn?.(
979
+ { code: "authorization_cleanup_pending" },
980
+ "Authorization lease awaits recovery"
981
+ );
982
+ });
983
+ }
984
+ },
985
+ async list(ownerId) {
986
+ return options.store.transaction(async (tx) => {
987
+ const values = await tx.listConnections(scope(ownerId));
988
+ return Promise.all(
989
+ values.map(async (value) => {
990
+ const grant = await tx.getGrant(scope(ownerId), value.grantId);
991
+ return value.status === "active" && grant?.status === "reauth_required" ? { ...value, status: "reauth_required" } : value;
992
+ })
993
+ );
994
+ });
995
+ },
996
+ async disconnect(ownerId, id) {
997
+ return options.store.transaction(async (tx) => {
998
+ await tx.lockScope(scope(ownerId), true);
999
+ const current = await tx.getConnection(scope(ownerId), id, true);
1000
+ if (!current) throw new IntegrationError("connection_not_found");
1001
+ const provider = providers.get(current.provider);
1002
+ if (current.status === "revoked")
1003
+ return {
1004
+ disconnected: true,
1005
+ remoteRevocationSupported: Boolean(provider?.revoke)
1006
+ };
1007
+ await tx.authorizationEpoch(
1008
+ scope(ownerId),
1009
+ hash(`${current.provider}\0${current.registrationId}`),
1010
+ true
1011
+ );
1012
+ const grant = await tx.getGrant(
1013
+ scope(ownerId),
1014
+ current.grantId,
1015
+ true
1016
+ );
1017
+ const next = {
1018
+ ...current,
1019
+ status: "revoked",
1020
+ generation: current.generation + 1,
1021
+ updatedAt: now()
1022
+ };
1023
+ if (!await tx.saveConnection(next, current.generation))
1024
+ throw new IntegrationError("connection_conflict");
1025
+ if (grant)
1026
+ await tx.saveGrant(
1027
+ {
1028
+ ...grant,
1029
+ status: "revoked",
1030
+ version: grant.version + 1,
1031
+ refreshLease: void 0,
1032
+ refreshUntil: void 0,
1033
+ authorizationLease: void 0,
1034
+ authorizationUntil: void 0
1035
+ },
1036
+ grant.version
1037
+ );
1038
+ if (grant && (!provider || provider.revoke)) {
1039
+ const op = {
1040
+ id: (0, import_node_crypto2.randomUUID)(),
1041
+ ...scope(ownerId),
1042
+ connectionId: id,
1043
+ generation: next.generation,
1044
+ action: "__sdk.revoke",
1045
+ idempotencyKey: `disconnect:${id}:${next.generation}`,
1046
+ fingerprint: hash(id),
1047
+ input: { keyId: "", value: "" },
1048
+ status: "pending",
1049
+ version: 1,
1050
+ attempts: 0,
1051
+ availableAt: now(),
1052
+ checkpoint: null,
1053
+ dispatching: false,
1054
+ createdAt: now(),
1055
+ updatedAt: now()
1056
+ };
1057
+ op.input = await options.vault.seal(null, operationContext(op));
1058
+ await tx.insertOperation(op);
1059
+ }
1060
+ return {
1061
+ disconnected: true,
1062
+ remoteRevocationSupported: Boolean(provider?.revoke)
1063
+ };
1064
+ });
1065
+ }
1066
+ },
1067
+ async eraseOwner(ownerId) {
1068
+ await options.store.transaction(async (tx) => {
1069
+ await tx.lockScope(scope(ownerId), true);
1070
+ await tx.markOwnerErased(scope(ownerId));
1071
+ });
1072
+ const existing = await options.store.transaction(
1073
+ (tx) => tx.listConnections(scope(ownerId))
1074
+ );
1075
+ for (const current of existing)
1076
+ await sdk.connections.disconnect(ownerId, current.id);
1077
+ await options.store.transaction(async (tx) => {
1078
+ await tx.eraseOwner(scope(ownerId), true);
1079
+ });
1080
+ },
1081
+ resources: {
1082
+ async list(input) {
1083
+ const current = await connection(input.ownerId, input.connectionId);
1084
+ const provider = providerFor(current.provider, current.registrationId);
1085
+ if (!provider.resources)
1086
+ throw new IntegrationError("unsupported_capability");
1087
+ let material = await credentials(current);
1088
+ let result;
1089
+ try {
1090
+ result = await provider.resources(input, {
1091
+ credentials: material,
1092
+ fetch: apiFetch(provider)
1093
+ });
1094
+ } catch (error) {
1095
+ const failure = integrationError(error);
1096
+ const definiteUnauthorized = failure.disposition === "requires_reauth" && error instanceof Error && "status" in error && error.status === 401 && "definite" in error && error.definite === true;
1097
+ if (!definiteUnauthorized || !provider.refresh) throw error;
1098
+ material = await credentials(
1099
+ current,
1100
+ void 0,
1101
+ true,
1102
+ material.accessToken
1103
+ );
1104
+ result = await provider.resources(input, {
1105
+ credentials: material,
1106
+ fetch: apiFetch(provider)
1107
+ });
1108
+ }
1109
+ const live = await connection(input.ownerId, input.connectionId);
1110
+ if (live.generation !== current.generation)
1111
+ throw new IntegrationError("connection_changed");
1112
+ return result;
1113
+ }
1114
+ },
1115
+ operations: {
1116
+ enqueue,
1117
+ get: getOperation,
1118
+ list: (ownerId, connectionId) => options.store.transaction(
1119
+ (tx) => tx.listOperations(scope(ownerId), connectionId)
1120
+ ),
1121
+ async recover(ownerId, id) {
1122
+ return options.store.transaction(async (tx) => {
1123
+ const current = await tx.getOperation(scope(ownerId), id, true);
1124
+ if (!current) throw new IntegrationError("operation_not_found");
1125
+ if (current.errorCode === "payload_expired")
1126
+ throw new IntegrationError("payload_expired");
1127
+ if (!["outcome_unknown", "blocked", "failed"].includes(current.status))
1128
+ return current;
1129
+ const connection2 = await tx.getConnection(
1130
+ scope(ownerId),
1131
+ current.connectionId,
1132
+ true
1133
+ );
1134
+ const revoke = current.action === "__sdk.revoke";
1135
+ if (!connection2 || connection2.generation !== current.generation || (revoke ? connection2.status !== "revoked" : connection2.status !== "active"))
1136
+ throw new IntegrationError("connection_revoked");
1137
+ const next = {
1138
+ ...current,
1139
+ status: current.dispatching ? "outcome_unknown" : "pending",
1140
+ attempts: 0,
1141
+ availableAt: now(),
1142
+ version: current.version + 1,
1143
+ updatedAt: now()
1144
+ };
1145
+ if (!await tx.saveOperation(next, current.version))
1146
+ throw new IntegrationError("operation_conflict");
1147
+ return next;
1148
+ });
1149
+ }
1150
+ },
1151
+ events: {
1152
+ list: (ownerId, after) => options.store.transaction((tx) => tx.listEvents(scope(ownerId), after))
1153
+ },
1154
+ runtime: {
1155
+ process,
1156
+ async due(limit = 100) {
1157
+ await options.store.transaction(async (tx) => {
1158
+ await tx.recoverOperations(options.namespace, now());
1159
+ await tx.collectGarbage(
1160
+ options.namespace,
1161
+ now(),
1162
+ options.failedPayloadRetentionMs ?? 7 * 864e5
1163
+ );
1164
+ });
1165
+ return options.store.transaction(
1166
+ (tx) => tx.dueOperations(options.namespace, now(), limit)
1167
+ );
1168
+ }
1169
+ }
1170
+ };
1171
+ return sdk;
1172
+ }
1173
+
1174
+ // src/runtime.ts
1175
+ function createRuntime(options) {
1176
+ let timer;
1177
+ let ticking;
1178
+ let lifecycle = Promise.resolve();
1179
+ let starting;
1180
+ let stopping;
1181
+ let desiredRunning = false;
1182
+ let running = false;
1183
+ let generation = 0;
1184
+ let queueStarted = false;
1185
+ let lastSuccess;
1186
+ let lastError;
1187
+ const current = (epoch) => desiredRunning && running && generation === epoch;
1188
+ const clearTimer = () => {
1189
+ if (timer) clearInterval(timer);
1190
+ timer = void 0;
1191
+ };
1192
+ const serialize = (work) => {
1193
+ const pending = lifecycle.then(work);
1194
+ lifecycle = pending.catch(() => void 0);
1195
+ return pending;
1196
+ };
1197
+ const tick = (epoch) => {
1198
+ if (!current(epoch)) return Promise.resolve();
1199
+ if (ticking) return ticking;
1200
+ ticking = (async () => {
1201
+ try {
1202
+ const messages = await options.sdk.runtime.due(
1203
+ options.batchSize ?? 100
1204
+ );
1205
+ if (!current(epoch)) return;
1206
+ await options.queue.publish(
1207
+ messages.map((message) => ({
1208
+ taskId: message.id,
1209
+ version: message.version
1210
+ }))
1211
+ );
1212
+ if (!current(epoch)) return;
1213
+ lastSuccess = Date.now();
1214
+ lastError = void 0;
1215
+ } catch {
1216
+ if (current(epoch)) lastError = "dispatch_unavailable";
1217
+ }
1218
+ })().finally(() => {
1219
+ ticking = void 0;
1220
+ });
1221
+ return ticking;
1222
+ };
1223
+ return {
1224
+ start() {
1225
+ if (desiredRunning && (starting || running))
1226
+ return starting ?? Promise.resolve();
1227
+ desiredRunning = true;
1228
+ const epoch = ++generation;
1229
+ const pending = serialize(async () => {
1230
+ if (!desiredRunning || generation !== epoch) return;
1231
+ let queueAttempted = false;
1232
+ try {
1233
+ if (queueStarted) {
1234
+ await options.queue.stop();
1235
+ queueStarted = false;
1236
+ }
1237
+ await options.sdk.assertCompatible();
1238
+ if (!desiredRunning || generation !== epoch) return;
1239
+ queueAttempted = true;
1240
+ queueStarted = true;
1241
+ await options.queue.start((message) => {
1242
+ if (!desiredRunning || generation !== epoch)
1243
+ return Promise.resolve();
1244
+ return options.sdk.runtime.process(message);
1245
+ });
1246
+ if (!desiredRunning || generation !== epoch) return;
1247
+ lastSuccess = void 0;
1248
+ lastError = void 0;
1249
+ running = true;
1250
+ await tick(epoch);
1251
+ if (!current(epoch)) return;
1252
+ timer = setInterval(() => {
1253
+ void tick(epoch);
1254
+ }, options.intervalMs ?? 3e3);
1255
+ timer.unref?.();
1256
+ } catch (error) {
1257
+ running = false;
1258
+ clearTimer();
1259
+ if (generation === epoch) desiredRunning = false;
1260
+ if (queueAttempted) {
1261
+ try {
1262
+ await options.queue.stop();
1263
+ queueStarted = false;
1264
+ } catch {
1265
+ }
1266
+ }
1267
+ throw error;
1268
+ }
1269
+ });
1270
+ starting = pending;
1271
+ void pending.then(
1272
+ () => {
1273
+ if (starting === pending) starting = void 0;
1274
+ },
1275
+ () => {
1276
+ if (starting === pending) starting = void 0;
1277
+ }
1278
+ );
1279
+ return pending;
1280
+ },
1281
+ stop() {
1282
+ if (!desiredRunning && stopping) return stopping;
1283
+ desiredRunning = false;
1284
+ running = false;
1285
+ generation++;
1286
+ clearTimer();
1287
+ const pending = serialize(async () => {
1288
+ if (ticking) await ticking;
1289
+ if (queueStarted) {
1290
+ await options.queue.stop();
1291
+ queueStarted = false;
1292
+ }
1293
+ });
1294
+ stopping = pending;
1295
+ void pending.then(
1296
+ () => {
1297
+ if (stopping === pending) stopping = void 0;
1298
+ },
1299
+ () => {
1300
+ if (stopping === pending) stopping = void 0;
1301
+ }
1302
+ );
1303
+ return pending;
1304
+ },
1305
+ async dispatch() {
1306
+ if (!running || !desiredRunning)
1307
+ throw new IntegrationError("runtime_not_started");
1308
+ await tick(generation);
1309
+ },
1310
+ health() {
1311
+ return {
1312
+ running,
1313
+ ready: running && !lastError && lastSuccess !== void 0 && Date.now() - lastSuccess < 9e4,
1314
+ lastSuccess,
1315
+ lastError
1316
+ };
1317
+ }
1318
+ };
1319
+ }
1320
+ // Annotate the CommonJS export names for ESM import in node:
1321
+ 0 && (module.exports = {
1322
+ IntegrationError,
1323
+ createIntegrationSdk,
1324
+ createRuntime,
1325
+ createTokenVault,
1326
+ integrationError
1327
+ });
1328
+ //# sourceMappingURL=index.cjs.map