@adcp/sdk 15.0.0 → 15.2.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 (142) hide show
  1. package/bin/adcp-grade.js +46 -2
  2. package/bin/adcp.js +7 -2
  3. package/dist/lib/canonical-references/index.d.mts +19 -1
  4. package/dist/lib/canonical-references/index.d.ts +19 -1
  5. package/dist/lib/canonical-references/index.js +81 -9
  6. package/dist/lib/canonical-references/index.mjs +80 -9
  7. package/dist/lib/client/core.d.mts +1 -0
  8. package/dist/lib/client/core.d.ts +1 -0
  9. package/dist/lib/core/AgentClient.d.mts +9 -1
  10. package/dist/lib/core/AgentClient.d.ts +9 -1
  11. package/dist/lib/core/AgentClient.js +10 -0
  12. package/dist/lib/core/AgentClient.mjs +10 -0
  13. package/dist/lib/core/SingleAgentClient.d.mts +13 -2
  14. package/dist/lib/core/SingleAgentClient.d.ts +13 -2
  15. package/dist/lib/core/SingleAgentClient.js +23 -5
  16. package/dist/lib/core/SingleAgentClient.mjs +23 -5
  17. package/dist/lib/index.d.mts +1 -1
  18. package/dist/lib/index.d.ts +1 -1
  19. package/dist/lib/reporting/gcs/adapter.d.mts +31 -0
  20. package/dist/lib/reporting/gcs/adapter.d.ts +31 -0
  21. package/dist/lib/reporting/gcs/adapter.js +342 -0
  22. package/dist/lib/reporting/gcs/adapter.mjs +325 -0
  23. package/dist/lib/reporting/gcs/bucket-policy.d.mts +3 -0
  24. package/dist/lib/reporting/gcs/bucket-policy.d.ts +3 -0
  25. package/dist/lib/reporting/gcs/bucket-policy.js +31 -0
  26. package/dist/lib/reporting/gcs/bucket-policy.mjs +7 -0
  27. package/dist/lib/reporting/gcs/errors.d.mts +5 -0
  28. package/dist/lib/reporting/gcs/errors.d.ts +5 -0
  29. package/dist/lib/reporting/gcs/errors.js +41 -0
  30. package/dist/lib/reporting/gcs/errors.mjs +16 -0
  31. package/dist/lib/reporting/gcs/fence.d.mts +36 -0
  32. package/dist/lib/reporting/gcs/fence.d.ts +36 -0
  33. package/dist/lib/reporting/gcs/fence.js +250 -0
  34. package/dist/lib/reporting/gcs/fence.mjs +230 -0
  35. package/dist/lib/reporting/gcs/index.d.mts +7 -0
  36. package/dist/lib/reporting/gcs/index.d.ts +7 -0
  37. package/dist/lib/reporting/gcs/index.js +41 -0
  38. package/dist/lib/reporting/gcs/index.mjs +12 -0
  39. package/dist/lib/reporting/gcs/io.d.mts +7 -0
  40. package/dist/lib/reporting/gcs/io.d.ts +7 -0
  41. package/dist/lib/reporting/gcs/io.js +95 -0
  42. package/dist/lib/reporting/gcs/io.mjs +69 -0
  43. package/dist/lib/reporting/gcs/reader.d.mts +43 -0
  44. package/dist/lib/reporting/gcs/reader.d.ts +43 -0
  45. package/dist/lib/reporting/gcs/reader.js +193 -0
  46. package/dist/lib/reporting/gcs/reader.mjs +169 -0
  47. package/dist/lib/reporting/gcs/validation.d.mts +5 -0
  48. package/dist/lib/reporting/gcs/validation.d.ts +5 -0
  49. package/dist/lib/reporting/gcs/validation.js +49 -0
  50. package/dist/lib/reporting/gcs/validation.mjs +25 -0
  51. package/dist/lib/reporting/ledger/handler.d.mts +1 -0
  52. package/dist/lib/reporting/ledger/handler.d.ts +1 -0
  53. package/dist/lib/reporting/ledger/index.d.mts +1 -0
  54. package/dist/lib/reporting/ledger/index.d.ts +1 -0
  55. package/dist/lib/reporting/ledger/index.js +2 -0
  56. package/dist/lib/reporting/ledger/index.mjs +1 -0
  57. package/dist/lib/reporting/ledger/managed-postgres.d.mts +16 -1
  58. package/dist/lib/reporting/ledger/managed-postgres.d.ts +16 -1
  59. package/dist/lib/reporting/ledger/managed-postgres.js +243 -2
  60. package/dist/lib/reporting/ledger/managed-postgres.mjs +248 -2
  61. package/dist/lib/reporting/ledger/object-writes.d.mts +69 -0
  62. package/dist/lib/reporting/ledger/object-writes.d.ts +69 -0
  63. package/dist/lib/reporting/ledger/object-writes.js +147 -0
  64. package/dist/lib/reporting/ledger/object-writes.mjs +114 -0
  65. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  66. package/dist/lib/server/a2a-adapter.d.mts +50 -1
  67. package/dist/lib/server/a2a-adapter.d.ts +50 -1
  68. package/dist/lib/server/a2a-adapter.js +230 -7
  69. package/dist/lib/server/a2a-adapter.mjs +237 -7
  70. package/dist/lib/server/auth-signature.d.mts +84 -46
  71. package/dist/lib/server/auth-signature.d.ts +84 -46
  72. package/dist/lib/server/auth-signature.js +48 -14
  73. package/dist/lib/server/auth-signature.mjs +58 -15
  74. package/dist/lib/server/create-adcp-server.js +5 -16
  75. package/dist/lib/server/create-adcp-server.mjs +5 -16
  76. package/dist/lib/server/index.d.mts +1 -1
  77. package/dist/lib/server/index.d.ts +1 -1
  78. package/dist/lib/server/index.js +2 -0
  79. package/dist/lib/server/index.mjs +3 -1
  80. package/dist/lib/signing/agent-fetch.js +3 -33
  81. package/dist/lib/signing/agent-fetch.mjs +3 -33
  82. package/dist/lib/signing/middleware.d.mts +9 -1
  83. package/dist/lib/signing/middleware.d.ts +9 -1
  84. package/dist/lib/signing/operation-resolution.d.mts +37 -0
  85. package/dist/lib/signing/operation-resolution.d.ts +37 -0
  86. package/dist/lib/signing/operation-resolution.js +154 -0
  87. package/dist/lib/signing/operation-resolution.mjs +125 -0
  88. package/dist/lib/signing/postgres-replay-store.d.mts +1 -1
  89. package/dist/lib/signing/postgres-replay-store.d.ts +1 -1
  90. package/dist/lib/signing/redis-replay-store.d.mts +1 -1
  91. package/dist/lib/signing/redis-replay-store.d.ts +1 -1
  92. package/dist/lib/signing/server.d.mts +1 -0
  93. package/dist/lib/signing/server.d.ts +1 -0
  94. package/dist/lib/signing/server.js +7 -0
  95. package/dist/lib/signing/server.mjs +8 -0
  96. package/dist/lib/signing/verifier.d.mts +36 -1
  97. package/dist/lib/signing/verifier.d.ts +36 -1
  98. package/dist/lib/signing/verifier.js +66 -34
  99. package/dist/lib/signing/verifier.mjs +61 -34
  100. package/dist/lib/testing/storyboard/request-signing/a2a-dispatch.d.mts +12 -0
  101. package/dist/lib/testing/storyboard/request-signing/a2a-dispatch.d.ts +12 -0
  102. package/dist/lib/testing/storyboard/request-signing/a2a-dispatch.js +29 -2
  103. package/dist/lib/testing/storyboard/request-signing/a2a-dispatch.mjs +27 -1
  104. package/dist/lib/testing/storyboard/request-signing/a2a-operation-resolution.d.mts +61 -0
  105. package/dist/lib/testing/storyboard/request-signing/a2a-operation-resolution.d.ts +61 -0
  106. package/dist/lib/testing/storyboard/request-signing/a2a-operation-resolution.js +298 -0
  107. package/dist/lib/testing/storyboard/request-signing/a2a-operation-resolution.mjs +276 -0
  108. package/dist/lib/testing/storyboard/request-signing/grader.d.mts +31 -1
  109. package/dist/lib/testing/storyboard/request-signing/grader.d.ts +31 -1
  110. package/dist/lib/testing/storyboard/request-signing/grader.js +11 -0
  111. package/dist/lib/testing/storyboard/request-signing/grader.mjs +11 -0
  112. package/dist/lib/testing/storyboard/request-signing/index.d.mts +5 -4
  113. package/dist/lib/testing/storyboard/request-signing/index.d.ts +5 -4
  114. package/dist/lib/testing/storyboard/request-signing/index.js +13 -0
  115. package/dist/lib/testing/storyboard/request-signing/index.mjs +18 -2
  116. package/dist/lib/testing/storyboard/request-signing/types.d.mts +43 -0
  117. package/dist/lib/testing/storyboard/request-signing/types.d.ts +43 -0
  118. package/dist/lib/testing/storyboard/request-signing/types.js +3 -0
  119. package/dist/lib/testing/storyboard/request-signing/types.mjs +2 -0
  120. package/dist/lib/testing/storyboard/request-signing/vector-loader.d.mts +42 -1
  121. package/dist/lib/testing/storyboard/request-signing/vector-loader.d.ts +42 -1
  122. package/dist/lib/testing/storyboard/request-signing/vector-loader.js +100 -1
  123. package/dist/lib/testing/storyboard/request-signing/vector-loader.mjs +104 -3
  124. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  125. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  126. package/dist/lib/version.d.mts +3 -3
  127. package/dist/lib/version.d.ts +3 -3
  128. package/dist/lib/version.js +3 -3
  129. package/dist/lib/version.mjs +3 -3
  130. package/docs/TYPE-SUMMARY.md +2 -2
  131. package/docs/guides/BUILD-AN-AGENT.md +9 -4
  132. package/docs/guides/CREATIVE-DELIVERY.md +34 -0
  133. package/docs/guides/REPORTING-GCS-FENCE.md +52 -0
  134. package/docs/guides/REPORTING-GCS-MANAGED.md +56 -0
  135. package/docs/guides/REPORTING-OPERATIONS.md +4 -0
  136. package/docs/guides/SIGNING-GUIDE.md +20 -6
  137. package/docs/guides/VALIDATE-YOUR-AGENT.md +4 -0
  138. package/docs/llms.txt +2 -2
  139. package/docs/migration-14.x-rc-worksheet.md +5 -4
  140. package/docs/migration-buyer-agent-registry.md +1 -1
  141. package/package.json +19 -1
  142. package/skills/build-seller-agent/specialisms/signed-requests.md +2 -0
@@ -26,6 +26,7 @@ __reExport(ledger_exports, require('./status-ingest.js'), module.exports);
26
26
  __reExport(ledger_exports, require('./consumer-status-identity.js'), module.exports);
27
27
  __reExport(ledger_exports, require('./managed.js'), module.exports);
28
28
  __reExport(ledger_exports, require('./managed-postgres.js'), module.exports);
29
+ __reExport(ledger_exports, require('./object-writes.js'), module.exports);
29
30
  __reExport(ledger_exports, require('./notification-activity.js'), module.exports);
30
31
  // Annotate the CommonJS export names for ESM import in node:
31
32
  0 && (module.exports = {
@@ -40,5 +41,6 @@ __reExport(ledger_exports, require('./notification-activity.js'), module.exports
40
41
  ...require('./consumer-status-identity.js'),
41
42
  ...require('./managed.js'),
42
43
  ...require('./managed-postgres.js'),
44
+ ...require('./object-writes.js'),
43
45
  ...require('./notification-activity.js')
44
46
  });
@@ -9,4 +9,5 @@ export * from "./status-ingest.mjs";
9
9
  export * from "./consumer-status-identity.mjs";
10
10
  export * from "./managed.mjs";
11
11
  export * from "./managed-postgres.mjs";
12
+ export * from "./object-writes.mjs";
12
13
  export * from "./notification-activity.mjs";
@@ -1,6 +1,7 @@
1
1
  import type { ReportingMaterialization, SyncReportingReceiptsResponse } from '../../types/index.mjs';
2
2
  import type { ReportingPgPool } from './postgres.mjs';
3
3
  import type { ReportingLedgerNotificationActivityPortV1, ReportingLedgerTransactionV1, ReportingManagedDeliveryBindingV1, ReportingLedgerStore } from './types.mjs';
4
+ import { type ReportingObjectWritePlanV1, type ReportingObjectWriteScopeV1, type ReportingObjectWriteCursorV1, type ReportingObjectWriteV1, type ReportingObjectWriteBindingV1, type ReportingObjectWriteContextV1, type ReportingObjectWriteStoreV1, type ReportingObjectWriteAuthorityStoreV1 } from './object-writes.mjs';
4
5
  import { type ReportingDestinationAuthorizationV1, type ReportingDestinationRevocationLeaseV1, type ReportingManagedDeliveryLeaseV1, type ReportingManagedDeliveryAdvertisedPoliciesV1, type ReportingManagedDeliveryStore, type ReportingReceiptBatchInputV1 } from './managed.mjs';
5
6
  /**
6
7
  * Additive migration owned by #2944. It intentionally does not alter the Core
@@ -52,7 +53,7 @@ export interface PostgresReportingManagedDeliveryStoreOptions {
52
53
  */
53
54
  evidenceRetentionDays?: number;
54
55
  }
55
- export declare class PostgresReportingManagedDeliveryStore implements ReportingManagedDeliveryStore {
56
+ export declare class PostgresReportingManagedDeliveryStore implements ReportingManagedDeliveryStore, ReportingObjectWriteStoreV1, ReportingObjectWriteAuthorityStoreV1 {
56
57
  private readonly pool;
57
58
  private readonly evidenceRetentionDays;
58
59
  private readonly statusRetentionDays;
@@ -213,6 +214,20 @@ export declare class PostgresReportingManagedDeliveryStore implements ReportingM
213
214
  };
214
215
  binding: ReportingManagedDeliveryBindingV1;
215
216
  } | null>;
217
+ /** Freeze all writable names under the same locks as revokeDestination. */
218
+ registerObjectWritePlan(input: ReportingObjectWritePlanV1, context?: ReportingObjectWriteContextV1): Promise<'registered' | 'unchanged' | 'revoked'>;
219
+ listRevokedObjectWrites(inputScope: ReportingObjectWriteScopeV1, inputOptions?: {
220
+ after?: ReportingObjectWriteCursorV1;
221
+ limit?: number;
222
+ }, context?: ReportingObjectWriteContextV1): Promise<Array<ReportingObjectWriteV1 & ReportingObjectWriteCursorV1>>;
223
+ markObjectWriteFenced(input: ReportingObjectWriteScopeV1 & ReportingObjectWriteCursorV1 & {
224
+ tombstone_generation: string;
225
+ }, context?: ReportingObjectWriteContextV1): Promise<void>;
226
+ getObjectWriteBinding(input: ReportingObjectWriteScopeV1, context?: ReportingObjectWriteContextV1): Promise<ReportingObjectWriteBindingV1 | null>;
227
+ probeObjectWriteAuthority(context?: ReportingObjectWriteContextV1): Promise<boolean>;
228
+ getObjectWriteAuthorityId(context?: ReportingObjectWriteContextV1): Promise<string>;
229
+ getAuthorizedObjectWriteBinding(input: ReportingObjectWriteScopeV1, context?: ReportingObjectWriteContextV1): Promise<ReportingObjectWriteBindingV1 | null>;
230
+ getObjectWritePlan(input: ReportingObjectWriteScopeV1, planId: string, context?: ReportingObjectWriteContextV1): Promise<ReportingObjectWritePlanV1 | null>;
216
231
  isAuthorizationCurrent(input: {
217
232
  account_id: string;
218
233
  destination_ref: string;
@@ -1,6 +1,7 @@
1
1
  import type { ReportingMaterialization, SyncReportingReceiptsResponse } from '../../types';
2
2
  import type { ReportingPgPool } from './postgres';
3
3
  import type { ReportingLedgerNotificationActivityPortV1, ReportingLedgerTransactionV1, ReportingManagedDeliveryBindingV1, ReportingLedgerStore } from './types';
4
+ import { type ReportingObjectWritePlanV1, type ReportingObjectWriteScopeV1, type ReportingObjectWriteCursorV1, type ReportingObjectWriteV1, type ReportingObjectWriteBindingV1, type ReportingObjectWriteContextV1, type ReportingObjectWriteStoreV1, type ReportingObjectWriteAuthorityStoreV1 } from './object-writes';
4
5
  import { type ReportingDestinationAuthorizationV1, type ReportingDestinationRevocationLeaseV1, type ReportingManagedDeliveryLeaseV1, type ReportingManagedDeliveryAdvertisedPoliciesV1, type ReportingManagedDeliveryStore, type ReportingReceiptBatchInputV1 } from './managed';
5
6
  /**
6
7
  * Additive migration owned by #2944. It intentionally does not alter the Core
@@ -52,7 +53,7 @@ export interface PostgresReportingManagedDeliveryStoreOptions {
52
53
  */
53
54
  evidenceRetentionDays?: number;
54
55
  }
55
- export declare class PostgresReportingManagedDeliveryStore implements ReportingManagedDeliveryStore {
56
+ export declare class PostgresReportingManagedDeliveryStore implements ReportingManagedDeliveryStore, ReportingObjectWriteStoreV1, ReportingObjectWriteAuthorityStoreV1 {
56
57
  private readonly pool;
57
58
  private readonly evidenceRetentionDays;
58
59
  private readonly statusRetentionDays;
@@ -213,6 +214,20 @@ export declare class PostgresReportingManagedDeliveryStore implements ReportingM
213
214
  };
214
215
  binding: ReportingManagedDeliveryBindingV1;
215
216
  } | null>;
217
+ /** Freeze all writable names under the same locks as revokeDestination. */
218
+ registerObjectWritePlan(input: ReportingObjectWritePlanV1, context?: ReportingObjectWriteContextV1): Promise<'registered' | 'unchanged' | 'revoked'>;
219
+ listRevokedObjectWrites(inputScope: ReportingObjectWriteScopeV1, inputOptions?: {
220
+ after?: ReportingObjectWriteCursorV1;
221
+ limit?: number;
222
+ }, context?: ReportingObjectWriteContextV1): Promise<Array<ReportingObjectWriteV1 & ReportingObjectWriteCursorV1>>;
223
+ markObjectWriteFenced(input: ReportingObjectWriteScopeV1 & ReportingObjectWriteCursorV1 & {
224
+ tombstone_generation: string;
225
+ }, context?: ReportingObjectWriteContextV1): Promise<void>;
226
+ getObjectWriteBinding(input: ReportingObjectWriteScopeV1, context?: ReportingObjectWriteContextV1): Promise<ReportingObjectWriteBindingV1 | null>;
227
+ probeObjectWriteAuthority(context?: ReportingObjectWriteContextV1): Promise<boolean>;
228
+ getObjectWriteAuthorityId(context?: ReportingObjectWriteContextV1): Promise<string>;
229
+ getAuthorizedObjectWriteBinding(input: ReportingObjectWriteScopeV1, context?: ReportingObjectWriteContextV1): Promise<ReportingObjectWriteBindingV1 | null>;
230
+ getObjectWritePlan(input: ReportingObjectWriteScopeV1, planId: string, context?: ReportingObjectWriteContextV1): Promise<ReportingObjectWritePlanV1 | null>;
216
231
  isAuthorizationCurrent(input: {
217
232
  account_id: string;
218
233
  destination_ref: string;
@@ -26,6 +26,7 @@ var import_node_crypto = require("node:crypto");
26
26
  var import_jcs = require('../../utils/jcs.js');
27
27
  var import_evidence = require('../evidence.js');
28
28
  var import_types = require('./types.js');
29
+ var import_object_writes = require('./object-writes.js');
29
30
  var import_managed = require('./managed.js');
30
31
  const REPORTING_MANAGED_DELIVERY_MIGRATION = `
31
32
  CREATE TABLE IF NOT EXISTS adcp_reporting_destination_authorizations (
@@ -1522,6 +1523,231 @@ class PostgresReportingManagedDeliveryStore {
1522
1523
  const row = result.rows[0];
1523
1524
  return row ? { materialization: structuredClone(row.materialization), binding: structuredClone(row.binding) } : null;
1524
1525
  }
1526
+ /** Freeze all writable names under the same locks as revokeDestination. */
1527
+ async registerObjectWritePlan(input, context = {}) {
1528
+ const plan = structuredClone(input);
1529
+ const fingerprint = (0, import_object_writes.reportingObjectWritePlanFingerprint)(plan);
1530
+ return this.transaction(async (client) => {
1531
+ await advisoryLock(client, accountLock(plan.account_id));
1532
+ await advisoryLock(client, authLock(plan.account_id, plan.destination_ref));
1533
+ const authorized = await client.query(
1534
+ `SELECT 1 FROM adcp_reporting_destination_authorizations
1535
+ WHERE account_id = $1 AND destination_ref = $2 AND generation = $3 AND revoked_at IS NULL`,
1536
+ [plan.account_id, plan.destination_ref, plan.generation]
1537
+ );
1538
+ if (!authorized.rowCount) return "revoked";
1539
+ await client.query(
1540
+ `INSERT INTO adcp_reporting_object_write_bindings (account_id,destination_ref,generation,bucket,namespace_key)
1541
+ VALUES ($1,$2,$3,$4,$5) ON CONFLICT DO NOTHING`,
1542
+ [plan.account_id, plan.destination_ref, plan.generation, plan.provider.bucket, plan.provider.namespace_key]
1543
+ );
1544
+ const binding = await client.query(
1545
+ `SELECT bucket,namespace_key FROM adcp_reporting_object_write_bindings
1546
+ WHERE account_id=$1 AND destination_ref=$2 AND generation=$3`,
1547
+ [plan.account_id, plan.destination_ref, plan.generation]
1548
+ );
1549
+ if (binding.rows[0]?.bucket !== plan.provider.bucket || binding.rows[0]?.namespace_key !== plan.provider.namespace_key)
1550
+ throw new import_object_writes.ReportingObjectWriteConflictError();
1551
+ const inserted = await client.query(
1552
+ `INSERT INTO adcp_reporting_object_write_plans (account_id, destination_ref, generation, plan_id, fingerprint)
1553
+ VALUES ($1,$2,$3,$4,$5) ON CONFLICT DO NOTHING`,
1554
+ [plan.account_id, plan.destination_ref, plan.generation, plan.plan_id, fingerprint]
1555
+ );
1556
+ if (!inserted.rowCount) {
1557
+ const existing = await client.query(
1558
+ `SELECT fingerprint FROM adcp_reporting_object_write_plans
1559
+ WHERE account_id=$1 AND destination_ref=$2 AND generation=$3 AND plan_id=$4`,
1560
+ [plan.account_id, plan.destination_ref, plan.generation, plan.plan_id]
1561
+ );
1562
+ if (existing.rows[0]?.fingerprint !== fingerprint) throw new import_object_writes.ReportingObjectWriteConflictError();
1563
+ return "unchanged";
1564
+ }
1565
+ await client.query(
1566
+ `INSERT INTO adcp_reporting_object_writes
1567
+ (account_id,destination_ref,generation,plan_id,object_index,bucket,object_name,sha256,size_bytes)
1568
+ SELECT $1,$2,$3,$4,(ordinality-1)::integer,value->>'bucket',value->>'object_name',value->>'sha256',
1569
+ (value->>'size_bytes')::bigint FROM jsonb_array_elements($5::jsonb) WITH ORDINALITY`,
1570
+ [
1571
+ plan.account_id,
1572
+ plan.destination_ref,
1573
+ plan.generation,
1574
+ plan.plan_id,
1575
+ JSON.stringify(
1576
+ plan.objects.map(({ bucket, object_name, sha256, size_bytes }) => ({
1577
+ bucket,
1578
+ object_name,
1579
+ sha256,
1580
+ size_bytes
1581
+ }))
1582
+ )
1583
+ ]
1584
+ );
1585
+ return "registered";
1586
+ }, context.signal);
1587
+ }
1588
+ async listRevokedObjectWrites(inputScope, inputOptions = {}, context = {}) {
1589
+ const scope = structuredClone(inputScope);
1590
+ const options = structuredClone(inputOptions);
1591
+ (0, import_object_writes.assertReportingObjectWriteScope)(scope);
1592
+ const limit = options.limit ?? 100;
1593
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e3 || options.after && (!/^[a-f0-9]{64}$/.test(options.after.plan_id) || !Number.isSafeInteger(options.after.object_index) || options.after.object_index < 0 || options.after.object_index >= 128)) {
1594
+ throw new Error("Invalid reporting object-write page");
1595
+ }
1596
+ return this.transaction(async (client) => {
1597
+ await advisoryLock(client, accountLock(scope.account_id));
1598
+ await advisoryLock(client, authLock(scope.account_id, scope.destination_ref));
1599
+ const revoked = await client.query(
1600
+ `SELECT 1 FROM adcp_reporting_destination_authorizations
1601
+ WHERE account_id=$1 AND destination_ref=$2 AND generation=$3 AND revoked_at IS NOT NULL`,
1602
+ [scope.account_id, scope.destination_ref, scope.generation]
1603
+ );
1604
+ if (!revoked.rowCount) throw new import_object_writes.ReportingObjectWriteNotRevokedError();
1605
+ const rows = await client.query(
1606
+ `SELECT plan_id,object_index,bucket,object_name,sha256,size_bytes::float8 AS size_bytes
1607
+ FROM adcp_reporting_object_writes WHERE account_id=$1 AND destination_ref=$2 AND generation=$3 AND fenced_at IS NULL
1608
+ AND ($4::text IS NULL OR (plan_id,object_index) > ($4,$5::integer))
1609
+ ORDER BY plan_id,object_index LIMIT $6`,
1610
+ [
1611
+ scope.account_id,
1612
+ scope.destination_ref,
1613
+ scope.generation,
1614
+ options.after?.plan_id ?? null,
1615
+ options.after?.object_index ?? null,
1616
+ limit
1617
+ ]
1618
+ );
1619
+ return rows.rows;
1620
+ }, context.signal);
1621
+ }
1622
+ async markObjectWriteFenced(input, context = {}) {
1623
+ const frozen = structuredClone(input);
1624
+ (0, import_object_writes.assertReportingObjectWriteScope)(frozen);
1625
+ if (!/^[a-f0-9]{64}$/.test(frozen.plan_id) || !Number.isSafeInteger(frozen.object_index) || frozen.object_index < 0 || frozen.object_index >= 128 || !/^[1-9][0-9]{0,39}$/.test(frozen.tombstone_generation)) {
1626
+ throw new Error("Invalid reporting object-write fence");
1627
+ }
1628
+ await this.transaction(async (client) => {
1629
+ await advisoryLock(client, accountLock(frozen.account_id));
1630
+ await advisoryLock(client, authLock(frozen.account_id, frozen.destination_ref));
1631
+ const updated = await client.query(
1632
+ `UPDATE adcp_reporting_object_writes object SET fenced_at=COALESCE(fenced_at,clock_timestamp()), tombstone_generation=$6
1633
+ WHERE account_id=$1 AND destination_ref=$2 AND generation=$3 AND plan_id=$4 AND object_index=$5
1634
+ AND (tombstone_generation IS NULL OR tombstone_generation=$6)
1635
+ AND EXISTS (SELECT 1 FROM adcp_reporting_destination_authorizations authz
1636
+ WHERE authz.account_id=object.account_id AND authz.destination_ref=object.destination_ref
1637
+ AND authz.generation=object.generation AND authz.revoked_at IS NOT NULL)`,
1638
+ [
1639
+ frozen.account_id,
1640
+ frozen.destination_ref,
1641
+ frozen.generation,
1642
+ frozen.plan_id,
1643
+ frozen.object_index,
1644
+ frozen.tombstone_generation
1645
+ ]
1646
+ );
1647
+ if (updated.rowCount !== 1) throw new Error("Reporting object-write fence is not eligible");
1648
+ }, context.signal);
1649
+ }
1650
+ async getObjectWriteBinding(input, context = {}) {
1651
+ const scope = structuredClone(input);
1652
+ (0, import_object_writes.assertReportingObjectWriteScope)(scope);
1653
+ return this.transaction(async (client) => {
1654
+ await advisoryLock(client, accountLock(scope.account_id));
1655
+ await advisoryLock(client, authLock(scope.account_id, scope.destination_ref));
1656
+ const revoked = await client.query(
1657
+ `SELECT 1 FROM adcp_reporting_destination_authorizations
1658
+ WHERE account_id=$1 AND destination_ref=$2 AND generation=$3 AND revoked_at IS NOT NULL`,
1659
+ [scope.account_id, scope.destination_ref, scope.generation]
1660
+ );
1661
+ if (!revoked.rowCount) throw new import_object_writes.ReportingObjectWriteNotRevokedError();
1662
+ const binding = await client.query(
1663
+ `SELECT bucket,namespace_key FROM adcp_reporting_object_write_bindings
1664
+ WHERE account_id=$1 AND destination_ref=$2 AND generation=$3`,
1665
+ [scope.account_id, scope.destination_ref, scope.generation]
1666
+ );
1667
+ return binding.rows[0] ?? null;
1668
+ }, context.signal);
1669
+ }
1670
+ async probeObjectWriteAuthority(context = {}) {
1671
+ await this.transaction(async (client) => {
1672
+ await client.query(
1673
+ `SELECT installation_id,schema_version FROM adcp_reporting_object_write_authority WHERE singleton AND schema_version=1`
1674
+ );
1675
+ await client.query(
1676
+ `SELECT account_id,destination_ref,generation,bucket,namespace_key FROM adcp_reporting_object_write_bindings LIMIT 0`
1677
+ );
1678
+ await client.query(
1679
+ `SELECT account_id,destination_ref,generation,plan_id,fingerprint FROM adcp_reporting_object_write_plans LIMIT 0`
1680
+ );
1681
+ await client.query(
1682
+ `SELECT plan_id,object_index,bucket,object_name,sha256,size_bytes,fenced_at,tombstone_generation FROM adcp_reporting_object_writes LIMIT 0`
1683
+ );
1684
+ }, context.signal);
1685
+ await this.getObjectWriteAuthorityId(context);
1686
+ return true;
1687
+ }
1688
+ async getObjectWriteAuthorityId(context = {}) {
1689
+ return this.transaction(async (client) => {
1690
+ const result = await client.query(
1691
+ `SELECT installation_id::text FROM adcp_reporting_object_write_authority WHERE singleton AND schema_version=1`
1692
+ );
1693
+ const id = result.rows[0]?.installation_id;
1694
+ if (result.rowCount !== 1 || !id || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(id))
1695
+ throw new Error("Reporting object-write authority migration is unavailable");
1696
+ return id;
1697
+ }, context.signal);
1698
+ }
1699
+ async getAuthorizedObjectWriteBinding(input, context = {}) {
1700
+ const scope = {
1701
+ account_id: input.account_id,
1702
+ destination_ref: input.destination_ref,
1703
+ generation: input.generation
1704
+ };
1705
+ (0, import_object_writes.assertReportingObjectWriteScope)(scope);
1706
+ return this.transaction(async (client) => {
1707
+ const result = await client.query(
1708
+ `SELECT binding.bucket,binding.namespace_key FROM adcp_reporting_object_write_bindings binding
1709
+ JOIN adcp_reporting_destination_authorizations authz USING (account_id,destination_ref,generation)
1710
+ WHERE binding.account_id=$1 AND binding.destination_ref=$2 AND binding.generation=$3 AND authz.revoked_at IS NULL`,
1711
+ [scope.account_id, scope.destination_ref, scope.generation]
1712
+ );
1713
+ return result.rows[0] ?? null;
1714
+ }, context.signal);
1715
+ }
1716
+ async getObjectWritePlan(input, planId, context = {}) {
1717
+ const scope = {
1718
+ account_id: input.account_id,
1719
+ destination_ref: input.destination_ref,
1720
+ generation: input.generation
1721
+ };
1722
+ (0, import_object_writes.assertReportingObjectWriteScope)(scope);
1723
+ if (typeof planId !== "string" || !/^[a-f0-9]{64}$/.test(planId))
1724
+ throw new Error("Invalid reporting object-write plan identity");
1725
+ return this.transaction(async (client) => {
1726
+ const selected = await client.query(
1727
+ `SELECT binding.bucket,binding.namespace_key,plan.fingerprint FROM adcp_reporting_object_write_plans plan
1728
+ JOIN adcp_reporting_object_write_bindings binding USING (account_id,destination_ref,generation)
1729
+ JOIN adcp_reporting_destination_authorizations authz USING (account_id,destination_ref,generation)
1730
+ WHERE plan.account_id=$1 AND plan.destination_ref=$2 AND plan.generation=$3 AND plan.plan_id=$4 AND authz.revoked_at IS NULL`,
1731
+ [scope.account_id, scope.destination_ref, scope.generation, planId]
1732
+ );
1733
+ if (!selected.rows[0]) return null;
1734
+ const selectedBinding = selected.rows[0];
1735
+ const objects = await client.query(
1736
+ `SELECT bucket,object_name,sha256,size_bytes::float8 AS size_bytes FROM adcp_reporting_object_writes
1737
+ WHERE account_id=$1 AND destination_ref=$2 AND generation=$3 AND plan_id=$4 ORDER BY object_index`,
1738
+ [scope.account_id, scope.destination_ref, scope.generation, planId]
1739
+ );
1740
+ const plan = {
1741
+ ...scope,
1742
+ plan_id: planId,
1743
+ provider: { bucket: selectedBinding.bucket, namespace_key: selectedBinding.namespace_key },
1744
+ objects: objects.rows
1745
+ };
1746
+ if ((0, import_object_writes.reportingObjectWritePlanFingerprint)(plan) !== selectedBinding.fingerprint)
1747
+ throw new import_object_writes.ReportingObjectWriteConflictError();
1748
+ return plan;
1749
+ }, context.signal);
1750
+ }
1525
1751
  async isAuthorizationCurrent(input) {
1526
1752
  const result = await this.query(
1527
1753
  `SELECT 1 FROM adcp_reporting_destination_authorizations
@@ -1822,7 +2048,8 @@ class PostgresReportingManagedDeliveryStore {
1822
2048
  );
1823
2049
  return result.rows[0] ? { subjectId: receipt.reporting_adjustment_id, adjustment: result.rows[0].adjustment } : null;
1824
2050
  }
1825
- async transaction(body) {
2051
+ async transaction(body, signal) {
2052
+ signal?.throwIfAborted();
1826
2053
  let client;
1827
2054
  try {
1828
2055
  client = await this.pool.connect();
@@ -1832,9 +2059,23 @@ class PostgresReportingManagedDeliveryStore {
1832
2059
  let releaseError;
1833
2060
  let transactionStarted = false;
1834
2061
  try {
2062
+ signal?.throwIfAborted();
1835
2063
  await client.query("BEGIN");
1836
2064
  transactionStarted = true;
1837
- const value = await body(client);
2065
+ if (signal) await client.query("SET LOCAL lock_timeout = '5s'; SET LOCAL statement_timeout = '10s'");
2066
+ const guarded = signal ? {
2067
+ query: async (sql, values) => {
2068
+ signal.throwIfAborted();
2069
+ const result = await client.query(sql, values);
2070
+ signal.throwIfAborted();
2071
+ return result;
2072
+ },
2073
+ release: () => {
2074
+ throw new Error("Transaction body cannot release connection");
2075
+ }
2076
+ } : client;
2077
+ const value = await body(guarded);
2078
+ signal?.throwIfAborted();
1838
2079
  await client.query("COMMIT");
1839
2080
  return value;
1840
2081
  } catch (cause) {
@@ -2,6 +2,12 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { canonicalize } from "../../utils/jcs.mjs";
3
3
  import { isReportingAdjustmentReceiptEvidence, isReportingReceiptEvidence } from "../evidence.mjs";
4
4
  import { REPORTING_LEDGER_AUTHORITY } from "./types.mjs";
5
+ import {
6
+ assertReportingObjectWriteScope,
7
+ reportingObjectWritePlanFingerprint,
8
+ ReportingObjectWriteConflictError,
9
+ ReportingObjectWriteNotRevokedError
10
+ } from "./object-writes.mjs";
5
11
  import {
6
12
  adjustmentReceiptEvidenceMatches,
7
13
  assertMaterializationOutcome,
@@ -1502,6 +1508,231 @@ class PostgresReportingManagedDeliveryStore {
1502
1508
  const row = result.rows[0];
1503
1509
  return row ? { materialization: structuredClone(row.materialization), binding: structuredClone(row.binding) } : null;
1504
1510
  }
1511
+ /** Freeze all writable names under the same locks as revokeDestination. */
1512
+ async registerObjectWritePlan(input, context = {}) {
1513
+ const plan = structuredClone(input);
1514
+ const fingerprint = reportingObjectWritePlanFingerprint(plan);
1515
+ return this.transaction(async (client) => {
1516
+ await advisoryLock(client, accountLock(plan.account_id));
1517
+ await advisoryLock(client, authLock(plan.account_id, plan.destination_ref));
1518
+ const authorized = await client.query(
1519
+ `SELECT 1 FROM adcp_reporting_destination_authorizations
1520
+ WHERE account_id = $1 AND destination_ref = $2 AND generation = $3 AND revoked_at IS NULL`,
1521
+ [plan.account_id, plan.destination_ref, plan.generation]
1522
+ );
1523
+ if (!authorized.rowCount) return "revoked";
1524
+ await client.query(
1525
+ `INSERT INTO adcp_reporting_object_write_bindings (account_id,destination_ref,generation,bucket,namespace_key)
1526
+ VALUES ($1,$2,$3,$4,$5) ON CONFLICT DO NOTHING`,
1527
+ [plan.account_id, plan.destination_ref, plan.generation, plan.provider.bucket, plan.provider.namespace_key]
1528
+ );
1529
+ const binding = await client.query(
1530
+ `SELECT bucket,namespace_key FROM adcp_reporting_object_write_bindings
1531
+ WHERE account_id=$1 AND destination_ref=$2 AND generation=$3`,
1532
+ [plan.account_id, plan.destination_ref, plan.generation]
1533
+ );
1534
+ if (binding.rows[0]?.bucket !== plan.provider.bucket || binding.rows[0]?.namespace_key !== plan.provider.namespace_key)
1535
+ throw new ReportingObjectWriteConflictError();
1536
+ const inserted = await client.query(
1537
+ `INSERT INTO adcp_reporting_object_write_plans (account_id, destination_ref, generation, plan_id, fingerprint)
1538
+ VALUES ($1,$2,$3,$4,$5) ON CONFLICT DO NOTHING`,
1539
+ [plan.account_id, plan.destination_ref, plan.generation, plan.plan_id, fingerprint]
1540
+ );
1541
+ if (!inserted.rowCount) {
1542
+ const existing = await client.query(
1543
+ `SELECT fingerprint FROM adcp_reporting_object_write_plans
1544
+ WHERE account_id=$1 AND destination_ref=$2 AND generation=$3 AND plan_id=$4`,
1545
+ [plan.account_id, plan.destination_ref, plan.generation, plan.plan_id]
1546
+ );
1547
+ if (existing.rows[0]?.fingerprint !== fingerprint) throw new ReportingObjectWriteConflictError();
1548
+ return "unchanged";
1549
+ }
1550
+ await client.query(
1551
+ `INSERT INTO adcp_reporting_object_writes
1552
+ (account_id,destination_ref,generation,plan_id,object_index,bucket,object_name,sha256,size_bytes)
1553
+ SELECT $1,$2,$3,$4,(ordinality-1)::integer,value->>'bucket',value->>'object_name',value->>'sha256',
1554
+ (value->>'size_bytes')::bigint FROM jsonb_array_elements($5::jsonb) WITH ORDINALITY`,
1555
+ [
1556
+ plan.account_id,
1557
+ plan.destination_ref,
1558
+ plan.generation,
1559
+ plan.plan_id,
1560
+ JSON.stringify(
1561
+ plan.objects.map(({ bucket, object_name, sha256, size_bytes }) => ({
1562
+ bucket,
1563
+ object_name,
1564
+ sha256,
1565
+ size_bytes
1566
+ }))
1567
+ )
1568
+ ]
1569
+ );
1570
+ return "registered";
1571
+ }, context.signal);
1572
+ }
1573
+ async listRevokedObjectWrites(inputScope, inputOptions = {}, context = {}) {
1574
+ const scope = structuredClone(inputScope);
1575
+ const options = structuredClone(inputOptions);
1576
+ assertReportingObjectWriteScope(scope);
1577
+ const limit = options.limit ?? 100;
1578
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e3 || options.after && (!/^[a-f0-9]{64}$/.test(options.after.plan_id) || !Number.isSafeInteger(options.after.object_index) || options.after.object_index < 0 || options.after.object_index >= 128)) {
1579
+ throw new Error("Invalid reporting object-write page");
1580
+ }
1581
+ return this.transaction(async (client) => {
1582
+ await advisoryLock(client, accountLock(scope.account_id));
1583
+ await advisoryLock(client, authLock(scope.account_id, scope.destination_ref));
1584
+ const revoked = await client.query(
1585
+ `SELECT 1 FROM adcp_reporting_destination_authorizations
1586
+ WHERE account_id=$1 AND destination_ref=$2 AND generation=$3 AND revoked_at IS NOT NULL`,
1587
+ [scope.account_id, scope.destination_ref, scope.generation]
1588
+ );
1589
+ if (!revoked.rowCount) throw new ReportingObjectWriteNotRevokedError();
1590
+ const rows = await client.query(
1591
+ `SELECT plan_id,object_index,bucket,object_name,sha256,size_bytes::float8 AS size_bytes
1592
+ FROM adcp_reporting_object_writes WHERE account_id=$1 AND destination_ref=$2 AND generation=$3 AND fenced_at IS NULL
1593
+ AND ($4::text IS NULL OR (plan_id,object_index) > ($4,$5::integer))
1594
+ ORDER BY plan_id,object_index LIMIT $6`,
1595
+ [
1596
+ scope.account_id,
1597
+ scope.destination_ref,
1598
+ scope.generation,
1599
+ options.after?.plan_id ?? null,
1600
+ options.after?.object_index ?? null,
1601
+ limit
1602
+ ]
1603
+ );
1604
+ return rows.rows;
1605
+ }, context.signal);
1606
+ }
1607
+ async markObjectWriteFenced(input, context = {}) {
1608
+ const frozen = structuredClone(input);
1609
+ assertReportingObjectWriteScope(frozen);
1610
+ if (!/^[a-f0-9]{64}$/.test(frozen.plan_id) || !Number.isSafeInteger(frozen.object_index) || frozen.object_index < 0 || frozen.object_index >= 128 || !/^[1-9][0-9]{0,39}$/.test(frozen.tombstone_generation)) {
1611
+ throw new Error("Invalid reporting object-write fence");
1612
+ }
1613
+ await this.transaction(async (client) => {
1614
+ await advisoryLock(client, accountLock(frozen.account_id));
1615
+ await advisoryLock(client, authLock(frozen.account_id, frozen.destination_ref));
1616
+ const updated = await client.query(
1617
+ `UPDATE adcp_reporting_object_writes object SET fenced_at=COALESCE(fenced_at,clock_timestamp()), tombstone_generation=$6
1618
+ WHERE account_id=$1 AND destination_ref=$2 AND generation=$3 AND plan_id=$4 AND object_index=$5
1619
+ AND (tombstone_generation IS NULL OR tombstone_generation=$6)
1620
+ AND EXISTS (SELECT 1 FROM adcp_reporting_destination_authorizations authz
1621
+ WHERE authz.account_id=object.account_id AND authz.destination_ref=object.destination_ref
1622
+ AND authz.generation=object.generation AND authz.revoked_at IS NOT NULL)`,
1623
+ [
1624
+ frozen.account_id,
1625
+ frozen.destination_ref,
1626
+ frozen.generation,
1627
+ frozen.plan_id,
1628
+ frozen.object_index,
1629
+ frozen.tombstone_generation
1630
+ ]
1631
+ );
1632
+ if (updated.rowCount !== 1) throw new Error("Reporting object-write fence is not eligible");
1633
+ }, context.signal);
1634
+ }
1635
+ async getObjectWriteBinding(input, context = {}) {
1636
+ const scope = structuredClone(input);
1637
+ assertReportingObjectWriteScope(scope);
1638
+ return this.transaction(async (client) => {
1639
+ await advisoryLock(client, accountLock(scope.account_id));
1640
+ await advisoryLock(client, authLock(scope.account_id, scope.destination_ref));
1641
+ const revoked = await client.query(
1642
+ `SELECT 1 FROM adcp_reporting_destination_authorizations
1643
+ WHERE account_id=$1 AND destination_ref=$2 AND generation=$3 AND revoked_at IS NOT NULL`,
1644
+ [scope.account_id, scope.destination_ref, scope.generation]
1645
+ );
1646
+ if (!revoked.rowCount) throw new ReportingObjectWriteNotRevokedError();
1647
+ const binding = await client.query(
1648
+ `SELECT bucket,namespace_key FROM adcp_reporting_object_write_bindings
1649
+ WHERE account_id=$1 AND destination_ref=$2 AND generation=$3`,
1650
+ [scope.account_id, scope.destination_ref, scope.generation]
1651
+ );
1652
+ return binding.rows[0] ?? null;
1653
+ }, context.signal);
1654
+ }
1655
+ async probeObjectWriteAuthority(context = {}) {
1656
+ await this.transaction(async (client) => {
1657
+ await client.query(
1658
+ `SELECT installation_id,schema_version FROM adcp_reporting_object_write_authority WHERE singleton AND schema_version=1`
1659
+ );
1660
+ await client.query(
1661
+ `SELECT account_id,destination_ref,generation,bucket,namespace_key FROM adcp_reporting_object_write_bindings LIMIT 0`
1662
+ );
1663
+ await client.query(
1664
+ `SELECT account_id,destination_ref,generation,plan_id,fingerprint FROM adcp_reporting_object_write_plans LIMIT 0`
1665
+ );
1666
+ await client.query(
1667
+ `SELECT plan_id,object_index,bucket,object_name,sha256,size_bytes,fenced_at,tombstone_generation FROM adcp_reporting_object_writes LIMIT 0`
1668
+ );
1669
+ }, context.signal);
1670
+ await this.getObjectWriteAuthorityId(context);
1671
+ return true;
1672
+ }
1673
+ async getObjectWriteAuthorityId(context = {}) {
1674
+ return this.transaction(async (client) => {
1675
+ const result = await client.query(
1676
+ `SELECT installation_id::text FROM adcp_reporting_object_write_authority WHERE singleton AND schema_version=1`
1677
+ );
1678
+ const id = result.rows[0]?.installation_id;
1679
+ if (result.rowCount !== 1 || !id || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(id))
1680
+ throw new Error("Reporting object-write authority migration is unavailable");
1681
+ return id;
1682
+ }, context.signal);
1683
+ }
1684
+ async getAuthorizedObjectWriteBinding(input, context = {}) {
1685
+ const scope = {
1686
+ account_id: input.account_id,
1687
+ destination_ref: input.destination_ref,
1688
+ generation: input.generation
1689
+ };
1690
+ assertReportingObjectWriteScope(scope);
1691
+ return this.transaction(async (client) => {
1692
+ const result = await client.query(
1693
+ `SELECT binding.bucket,binding.namespace_key FROM adcp_reporting_object_write_bindings binding
1694
+ JOIN adcp_reporting_destination_authorizations authz USING (account_id,destination_ref,generation)
1695
+ WHERE binding.account_id=$1 AND binding.destination_ref=$2 AND binding.generation=$3 AND authz.revoked_at IS NULL`,
1696
+ [scope.account_id, scope.destination_ref, scope.generation]
1697
+ );
1698
+ return result.rows[0] ?? null;
1699
+ }, context.signal);
1700
+ }
1701
+ async getObjectWritePlan(input, planId, context = {}) {
1702
+ const scope = {
1703
+ account_id: input.account_id,
1704
+ destination_ref: input.destination_ref,
1705
+ generation: input.generation
1706
+ };
1707
+ assertReportingObjectWriteScope(scope);
1708
+ if (typeof planId !== "string" || !/^[a-f0-9]{64}$/.test(planId))
1709
+ throw new Error("Invalid reporting object-write plan identity");
1710
+ return this.transaction(async (client) => {
1711
+ const selected = await client.query(
1712
+ `SELECT binding.bucket,binding.namespace_key,plan.fingerprint FROM adcp_reporting_object_write_plans plan
1713
+ JOIN adcp_reporting_object_write_bindings binding USING (account_id,destination_ref,generation)
1714
+ JOIN adcp_reporting_destination_authorizations authz USING (account_id,destination_ref,generation)
1715
+ WHERE plan.account_id=$1 AND plan.destination_ref=$2 AND plan.generation=$3 AND plan.plan_id=$4 AND authz.revoked_at IS NULL`,
1716
+ [scope.account_id, scope.destination_ref, scope.generation, planId]
1717
+ );
1718
+ if (!selected.rows[0]) return null;
1719
+ const selectedBinding = selected.rows[0];
1720
+ const objects = await client.query(
1721
+ `SELECT bucket,object_name,sha256,size_bytes::float8 AS size_bytes FROM adcp_reporting_object_writes
1722
+ WHERE account_id=$1 AND destination_ref=$2 AND generation=$3 AND plan_id=$4 ORDER BY object_index`,
1723
+ [scope.account_id, scope.destination_ref, scope.generation, planId]
1724
+ );
1725
+ const plan = {
1726
+ ...scope,
1727
+ plan_id: planId,
1728
+ provider: { bucket: selectedBinding.bucket, namespace_key: selectedBinding.namespace_key },
1729
+ objects: objects.rows
1730
+ };
1731
+ if (reportingObjectWritePlanFingerprint(plan) !== selectedBinding.fingerprint)
1732
+ throw new ReportingObjectWriteConflictError();
1733
+ return plan;
1734
+ }, context.signal);
1735
+ }
1505
1736
  async isAuthorizationCurrent(input) {
1506
1737
  const result = await this.query(
1507
1738
  `SELECT 1 FROM adcp_reporting_destination_authorizations
@@ -1802,7 +2033,8 @@ class PostgresReportingManagedDeliveryStore {
1802
2033
  );
1803
2034
  return result.rows[0] ? { subjectId: receipt.reporting_adjustment_id, adjustment: result.rows[0].adjustment } : null;
1804
2035
  }
1805
- async transaction(body) {
2036
+ async transaction(body, signal) {
2037
+ signal?.throwIfAborted();
1806
2038
  let client;
1807
2039
  try {
1808
2040
  client = await this.pool.connect();
@@ -1812,9 +2044,23 @@ class PostgresReportingManagedDeliveryStore {
1812
2044
  let releaseError;
1813
2045
  let transactionStarted = false;
1814
2046
  try {
2047
+ signal?.throwIfAborted();
1815
2048
  await client.query("BEGIN");
1816
2049
  transactionStarted = true;
1817
- const value = await body(client);
2050
+ if (signal) await client.query("SET LOCAL lock_timeout = '5s'; SET LOCAL statement_timeout = '10s'");
2051
+ const guarded = signal ? {
2052
+ query: async (sql, values) => {
2053
+ signal.throwIfAborted();
2054
+ const result = await client.query(sql, values);
2055
+ signal.throwIfAborted();
2056
+ return result;
2057
+ },
2058
+ release: () => {
2059
+ throw new Error("Transaction body cannot release connection");
2060
+ }
2061
+ } : client;
2062
+ const value = await body(guarded);
2063
+ signal?.throwIfAborted();
1818
2064
  await client.query("COMMIT");
1819
2065
  return value;
1820
2066
  } catch (cause) {