@adcp/sdk 14.0.0-beta.12 → 14.0.0-beta.13

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 (175) hide show
  1. package/dist/lib/index.d.mts +3 -3
  2. package/dist/lib/index.d.ts +3 -3
  3. package/dist/lib/index.d.ts.map +1 -1
  4. package/dist/lib/index.js +2 -0
  5. package/dist/lib/index.js.map +1 -1
  6. package/dist/lib/index.mjs +3 -1
  7. package/dist/lib/index.mjs.map +1 -1
  8. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  9. package/dist/lib/server/create-adcp-server.d.mts +2 -2
  10. package/dist/lib/server/create-adcp-server.d.ts +2 -2
  11. package/dist/lib/server/create-adcp-server.d.ts.map +1 -1
  12. package/dist/lib/server/create-adcp-server.js +6 -1
  13. package/dist/lib/server/create-adcp-server.js.map +1 -1
  14. package/dist/lib/server/create-adcp-server.mjs +6 -1
  15. package/dist/lib/server/create-adcp-server.mjs.map +1 -1
  16. package/dist/lib/server/decisioning/index.d.mts +3 -3
  17. package/dist/lib/server/decisioning/index.d.ts +3 -3
  18. package/dist/lib/server/decisioning/index.d.ts.map +1 -1
  19. package/dist/lib/server/decisioning/index.js.map +1 -1
  20. package/dist/lib/server/decisioning/index.mjs.map +1 -1
  21. package/dist/lib/server/decisioning/runtime/from-platform.d.mts +24 -17
  22. package/dist/lib/server/decisioning/runtime/from-platform.d.ts +24 -17
  23. package/dist/lib/server/decisioning/runtime/from-platform.d.ts.map +1 -1
  24. package/dist/lib/server/decisioning/runtime/from-platform.js +71 -42
  25. package/dist/lib/server/decisioning/runtime/from-platform.js.map +1 -1
  26. package/dist/lib/server/decisioning/runtime/from-platform.mjs +75 -43
  27. package/dist/lib/server/decisioning/runtime/from-platform.mjs.map +1 -1
  28. package/dist/lib/server/decisioning/runtime/postgres-task-registry.d.mts +15 -21
  29. package/dist/lib/server/decisioning/runtime/postgres-task-registry.d.ts +15 -21
  30. package/dist/lib/server/decisioning/runtime/postgres-task-registry.d.ts.map +1 -1
  31. package/dist/lib/server/decisioning/runtime/postgres-task-registry.js +100 -30
  32. package/dist/lib/server/decisioning/runtime/postgres-task-registry.js.map +1 -1
  33. package/dist/lib/server/decisioning/runtime/postgres-task-registry.mjs +100 -30
  34. package/dist/lib/server/decisioning/runtime/postgres-task-registry.mjs.map +1 -1
  35. package/dist/lib/server/decisioning/runtime/task-registry.d.mts +30 -16
  36. package/dist/lib/server/decisioning/runtime/task-registry.d.ts +30 -16
  37. package/dist/lib/server/decisioning/runtime/task-registry.d.ts.map +1 -1
  38. package/dist/lib/server/decisioning/runtime/task-registry.js +40 -20
  39. package/dist/lib/server/decisioning/runtime/task-registry.js.map +1 -1
  40. package/dist/lib/server/decisioning/runtime/task-registry.mjs +38 -19
  41. package/dist/lib/server/decisioning/runtime/task-registry.mjs.map +1 -1
  42. package/dist/lib/server/decisioning/runtime/to-context.d.mts +2 -2
  43. package/dist/lib/server/decisioning/runtime/to-context.d.ts +2 -2
  44. package/dist/lib/server/decisioning/runtime/to-context.d.ts.map +1 -1
  45. package/dist/lib/server/decisioning/runtime/to-context.js +2 -2
  46. package/dist/lib/server/decisioning/runtime/to-context.js.map +1 -1
  47. package/dist/lib/server/decisioning/runtime/to-context.mjs +2 -2
  48. package/dist/lib/server/decisioning/runtime/to-context.mjs.map +1 -1
  49. package/dist/lib/server/decisioning/specialisms/creative-ad-server.d.mts +3 -2
  50. package/dist/lib/server/decisioning/specialisms/creative-ad-server.d.ts +3 -2
  51. package/dist/lib/server/decisioning/specialisms/creative-ad-server.d.ts.map +1 -1
  52. package/dist/lib/server/decisioning/specialisms/creative-ad-server.js.map +1 -1
  53. package/dist/lib/server/decisioning/specialisms/creative.d.mts +10 -10
  54. package/dist/lib/server/decisioning/specialisms/creative.d.ts +10 -10
  55. package/dist/lib/server/decisioning/specialisms/creative.d.ts.map +1 -1
  56. package/dist/lib/server/decisioning/specialisms/creative.js.map +1 -1
  57. package/dist/lib/server/index.d.mts +1 -1
  58. package/dist/lib/server/index.d.ts +1 -1
  59. package/dist/lib/server/index.d.ts.map +1 -1
  60. package/dist/lib/server/index.js.map +1 -1
  61. package/dist/lib/server/index.mjs.map +1 -1
  62. package/dist/lib/testing/storyboard/default-invariants.d.ts.map +1 -1
  63. package/dist/lib/testing/storyboard/default-invariants.js +11 -2
  64. package/dist/lib/testing/storyboard/default-invariants.js.map +1 -1
  65. package/dist/lib/testing/storyboard/default-invariants.mjs +11 -2
  66. package/dist/lib/testing/storyboard/default-invariants.mjs.map +1 -1
  67. package/dist/lib/testing/storyboard/index.d.mts +1 -1
  68. package/dist/lib/testing/storyboard/index.d.ts +1 -1
  69. package/dist/lib/testing/storyboard/index.d.ts.map +1 -1
  70. package/dist/lib/testing/storyboard/index.js.map +1 -1
  71. package/dist/lib/testing/storyboard/index.mjs.map +1 -1
  72. package/dist/lib/testing/storyboard/loader.d.ts.map +1 -1
  73. package/dist/lib/testing/storyboard/loader.js +14 -0
  74. package/dist/lib/testing/storyboard/loader.js.map +1 -1
  75. package/dist/lib/testing/storyboard/loader.mjs +14 -0
  76. package/dist/lib/testing/storyboard/loader.mjs.map +1 -1
  77. package/dist/lib/testing/storyboard/phase-condition.d.mts +41 -0
  78. package/dist/lib/testing/storyboard/phase-condition.d.ts +42 -0
  79. package/dist/lib/testing/storyboard/phase-condition.d.ts.map +1 -0
  80. package/dist/lib/testing/storyboard/phase-condition.js +215 -0
  81. package/dist/lib/testing/storyboard/phase-condition.js.map +1 -0
  82. package/dist/lib/testing/storyboard/phase-condition.mjs +189 -0
  83. package/dist/lib/testing/storyboard/phase-condition.mjs.map +1 -0
  84. package/dist/lib/testing/storyboard/request-builder.d.ts.map +1 -1
  85. package/dist/lib/testing/storyboard/request-builder.js +15 -2
  86. package/dist/lib/testing/storyboard/request-builder.js.map +1 -1
  87. package/dist/lib/testing/storyboard/request-builder.mjs +15 -2
  88. package/dist/lib/testing/storyboard/request-builder.mjs.map +1 -1
  89. package/dist/lib/testing/storyboard/runner.d.ts.map +1 -1
  90. package/dist/lib/testing/storyboard/runner.js +57 -23
  91. package/dist/lib/testing/storyboard/runner.js.map +1 -1
  92. package/dist/lib/testing/storyboard/runner.mjs +57 -23
  93. package/dist/lib/testing/storyboard/runner.mjs.map +1 -1
  94. package/dist/lib/testing/storyboard/task-map.d.ts.map +1 -1
  95. package/dist/lib/testing/storyboard/task-map.js +6 -2
  96. package/dist/lib/testing/storyboard/task-map.js.map +1 -1
  97. package/dist/lib/testing/storyboard/task-map.mjs +6 -2
  98. package/dist/lib/testing/storyboard/task-map.mjs.map +1 -1
  99. package/dist/lib/testing/storyboard/types.d.mts +5 -4
  100. package/dist/lib/testing/storyboard/types.d.ts +5 -4
  101. package/dist/lib/testing/storyboard/types.d.ts.map +1 -1
  102. package/dist/lib/testing/storyboard/types.js.map +1 -1
  103. package/dist/lib/testing/storyboard/types.mjs.map +1 -1
  104. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  105. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  106. package/dist/lib/testing/storyboard/webhook-assertions.d.ts.map +1 -1
  107. package/dist/lib/testing/storyboard/webhook-assertions.js +60 -4
  108. package/dist/lib/testing/storyboard/webhook-assertions.js.map +1 -1
  109. package/dist/lib/testing/storyboard/webhook-assertions.mjs +60 -4
  110. package/dist/lib/testing/storyboard/webhook-assertions.mjs.map +1 -1
  111. package/dist/lib/testing/storyboard/webhook-receiver.d.mts +15 -0
  112. package/dist/lib/testing/storyboard/webhook-receiver.d.ts +15 -0
  113. package/dist/lib/testing/storyboard/webhook-receiver.d.ts.map +1 -1
  114. package/dist/lib/testing/storyboard/webhook-receiver.js +57 -6
  115. package/dist/lib/testing/storyboard/webhook-receiver.js.map +1 -1
  116. package/dist/lib/testing/storyboard/webhook-receiver.mjs +57 -6
  117. package/dist/lib/testing/storyboard/webhook-receiver.mjs.map +1 -1
  118. package/dist/lib/testing/types.d.mts +2 -0
  119. package/dist/lib/testing/types.d.ts +2 -0
  120. package/dist/lib/testing/types.d.ts.map +1 -1
  121. package/dist/lib/testing/types.js.map +1 -1
  122. package/dist/lib/types/core.generated.d.mts +5 -2
  123. package/dist/lib/types/core.generated.d.ts +5 -2
  124. package/dist/lib/types/core.generated.d.ts.map +1 -1
  125. package/dist/lib/types/core.generated.js.map +1 -1
  126. package/dist/lib/types/index.d.mts +1 -1
  127. package/dist/lib/types/index.d.ts +1 -1
  128. package/dist/lib/types/index.d.ts.map +1 -1
  129. package/dist/lib/types/index.js.map +1 -1
  130. package/dist/lib/types/index.mjs.map +1 -1
  131. package/dist/lib/types/list-transformers.d.ts +9 -5
  132. package/dist/lib/types/schemas.generated.d.ts +21 -298
  133. package/dist/lib/types/schemas.generated.d.ts.map +1 -1
  134. package/dist/lib/types/schemas.generated.js +5 -2
  135. package/dist/lib/types/schemas.generated.js.map +1 -1
  136. package/dist/lib/types/schemas.generated.mjs +4 -2
  137. package/dist/lib/types/schemas.generated.mjs.map +1 -1
  138. package/dist/lib/types/server-payload-aliases.d.mts +2 -1
  139. package/dist/lib/types/server-payload-aliases.d.ts +2 -1
  140. package/dist/lib/types/server-payload-aliases.d.ts.map +1 -1
  141. package/dist/lib/types/server-payload-aliases.js.map +1 -1
  142. package/dist/lib/types/tools.generated.d.mts +5 -2
  143. package/dist/lib/types/tools.generated.d.ts +5 -2
  144. package/dist/lib/types/tools.generated.d.ts.map +1 -1
  145. package/dist/lib/types/tools.generated.js.map +1 -1
  146. package/dist/lib/utils/build-creative-return-builders.d.mts +5 -1
  147. package/dist/lib/utils/build-creative-return-builders.d.ts +5 -1
  148. package/dist/lib/utils/build-creative-return-builders.d.ts.map +1 -1
  149. package/dist/lib/utils/build-creative-return-builders.js +9 -3
  150. package/dist/lib/utils/build-creative-return-builders.js.map +1 -1
  151. package/dist/lib/utils/build-creative-return-builders.mjs +7 -2
  152. package/dist/lib/utils/build-creative-return-builders.mjs.map +1 -1
  153. package/dist/lib/utils/response-unwrapper.js +1 -1
  154. package/dist/lib/utils/response-unwrapper.js.map +1 -1
  155. package/dist/lib/utils/response-unwrapper.mjs +1 -1
  156. package/dist/lib/utils/response-unwrapper.mjs.map +1 -1
  157. package/dist/lib/validation/schema-loader.d.ts.map +1 -1
  158. package/dist/lib/validation/schema-loader.js +16 -7
  159. package/dist/lib/validation/schema-loader.js.map +1 -1
  160. package/dist/lib/validation/schema-loader.mjs +16 -7
  161. package/dist/lib/validation/schema-loader.mjs.map +1 -1
  162. package/dist/lib/version.d.mts +3 -3
  163. package/dist/lib/version.d.ts +3 -3
  164. package/dist/lib/version.js +3 -3
  165. package/dist/lib/version.js.map +1 -1
  166. package/dist/lib/version.mjs +3 -3
  167. package/dist/lib/version.mjs.map +1 -1
  168. package/docs/llms.txt +2 -2
  169. package/docs/migration-5.x-to-6.x.md +3 -2
  170. package/docs/migration-task-registry-scoping.md +61 -0
  171. package/examples/hello_seller_adapter_guaranteed.ts +1 -1
  172. package/package.json +4 -4
  173. package/skills/build-decisioning-creative-template/SKILL.md +2 -2
  174. package/skills/build-decisioning-platform/SKILL.md +4 -2
  175. package/skills/build-decisioning-platform/advanced/REFERENCE.md +7 -6
@@ -5,7 +5,7 @@
5
5
  * restart and doesn't share state across instances behind a load balancer —
6
6
  * fine for tests and local dev, broken for production HITL paths
7
7
  * (`createMediaBuyTask`, `syncCreativesTask`, etc.). Wire this in via
8
- * `createAdcpServerFromPlatform({ taskRegistry: createPostgresTaskRegistry({ pool }) })`
8
+ * `createAdcpServerFromPlatform({ taskRegistry: createPostgresTaskRegistry({ pool, namespace }) })`
9
9
  * to persist task lifecycle across requests, processes, and crashes.
10
10
  *
11
11
  * @example
@@ -19,13 +19,14 @@
19
19
  *
20
20
  * const pool = new Pool({ connectionString: process.env.DATABASE_URL });
21
21
  *
22
- * // Run once at boot — idempotent CREATE TABLE IF NOT EXISTS.
23
- * await pool.query(getDecisioningTaskRegistryMigration());
22
+ * const taskRegistryNamespace = 'tenant:my-agent';
23
+ * // Run once at boot, using the same trusted namespace as the registry.
24
+ * await pool.query(getDecisioningTaskRegistryMigration({ namespace: taskRegistryNamespace }));
24
25
  *
25
26
  * const server = createAdcpServerFromPlatform(platform, {
26
27
  * name: 'My Ad Network',
27
28
  * version: '1.0.0',
28
- * taskRegistry: createPostgresTaskRegistry({ pool }),
29
+ * taskRegistry: createPostgresTaskRegistry({ pool, namespace: taskRegistryNamespace }),
29
30
  * });
30
31
  * ```
31
32
  *
@@ -59,6 +60,8 @@ export interface PgQueryable {
59
60
  export interface CreatePostgresTaskRegistryOptions {
60
61
  /** A `pg.Pool` instance (or any `PgQueryable`). */
61
62
  pool: PgQueryable;
63
+ /** Trusted deployment/tenant namespace. Use a distinct value per hosted tenant. */
64
+ namespace: string;
62
65
  /**
63
66
  * Table name. Defaults to `'adcp_decisioning_tasks'` (vendor-prefixed to
64
67
  * avoid collisions with the MCP-level `adcp_mcp_tasks` table from
@@ -80,12 +83,13 @@ export interface CreatePostgresTaskRegistryOptions {
80
83
  * @example
81
84
  * ```typescript
82
85
  * import { getDecisioningTaskRegistryMigration } from '@adcp/sdk/server/decisioning';
83
- * await pool.query(getDecisioningTaskRegistryMigration());
84
- * await pool.query(getDecisioningTaskRegistryMigration({ tableName: 'my_tasks' }));
86
+ * await pool.query(getDecisioningTaskRegistryMigration({ namespace: tenantId }));
87
+ * await pool.query(getDecisioningTaskRegistryMigration({ tableName: 'my_tasks', namespace: tenantId }));
85
88
  * ```
86
89
  */
87
- export declare function getDecisioningTaskRegistryMigration(options?: {
90
+ export declare function getDecisioningTaskRegistryMigration(options: {
88
91
  tableName?: string;
92
+ namespace: string;
89
93
  }): string;
90
94
  /**
91
95
  * Build a Postgres-backed `TaskRegistry`.
@@ -95,19 +99,9 @@ export declare function getDecisioningTaskRegistryMigration(options?: {
95
99
  * is enforced via SQL `WHERE status = 'submitted'` predicates so concurrent
96
100
  * webhook deliveries can't race to overwrite each other.
97
101
  *
98
- * **Multi-tenant deployments accountId namespacing.** When sharing a
99
- * single Postgres registry across tenants in a `TenantRegistry`
100
- * deployment (one table for all tenants), prefix `account_id` per-tenant
101
- * to prevent cross-tenant collisions. If tenant A and tenant B both
102
- * resolve a buyer to literal `account_id: 'acme'`, their tasks land in
103
- * the same row keyspace and `tasks_get` can't distinguish ownership
104
- * by `account_id` alone (the tenant boundary upstream IS protected by
105
- * `accounts.resolve(ref, ctx)` returning each tenant's own Account, but
106
- * the registry sees the same `acme` string in both rows). Recommended
107
- * pattern: adopters' `accounts.resolve` returns
108
- * `id: \`tenant_${tenantId}_${accountId}\`` so the table-level keys
109
- * stay distinct. Alternative: separate `tableName` per tenant, one
110
- * `createPostgresTaskRegistry({ pool, tableName })` per `TenantRegistry`
111
- * tenant — heavier ops, stronger isolation.
102
+ * **Multi-tenant deployments.** `namespace` is a trusted deployment/tenant
103
+ * partition and participates in every query plus the primary key. Construct
104
+ * one registry per tenant with a stable unique namespace; never derive it
105
+ * from buyer request parameters. Tenants may safely share the same table.
112
106
  */
113
107
  export declare function createPostgresTaskRegistry(opts: CreatePostgresTaskRegistryOptions): TaskRegistry;
@@ -5,7 +5,7 @@
5
5
  * restart and doesn't share state across instances behind a load balancer —
6
6
  * fine for tests and local dev, broken for production HITL paths
7
7
  * (`createMediaBuyTask`, `syncCreativesTask`, etc.). Wire this in via
8
- * `createAdcpServerFromPlatform({ taskRegistry: createPostgresTaskRegistry({ pool }) })`
8
+ * `createAdcpServerFromPlatform({ taskRegistry: createPostgresTaskRegistry({ pool, namespace }) })`
9
9
  * to persist task lifecycle across requests, processes, and crashes.
10
10
  *
11
11
  * @example
@@ -19,13 +19,14 @@
19
19
  *
20
20
  * const pool = new Pool({ connectionString: process.env.DATABASE_URL });
21
21
  *
22
- * // Run once at boot — idempotent CREATE TABLE IF NOT EXISTS.
23
- * await pool.query(getDecisioningTaskRegistryMigration());
22
+ * const taskRegistryNamespace = 'tenant:my-agent';
23
+ * // Run once at boot, using the same trusted namespace as the registry.
24
+ * await pool.query(getDecisioningTaskRegistryMigration({ namespace: taskRegistryNamespace }));
24
25
  *
25
26
  * const server = createAdcpServerFromPlatform(platform, {
26
27
  * name: 'My Ad Network',
27
28
  * version: '1.0.0',
28
- * taskRegistry: createPostgresTaskRegistry({ pool }),
29
+ * taskRegistry: createPostgresTaskRegistry({ pool, namespace: taskRegistryNamespace }),
29
30
  * });
30
31
  * ```
31
32
  *
@@ -59,6 +60,8 @@ export interface PgQueryable {
59
60
  export interface CreatePostgresTaskRegistryOptions {
60
61
  /** A `pg.Pool` instance (or any `PgQueryable`). */
61
62
  pool: PgQueryable;
63
+ /** Trusted deployment/tenant namespace. Use a distinct value per hosted tenant. */
64
+ namespace: string;
62
65
  /**
63
66
  * Table name. Defaults to `'adcp_decisioning_tasks'` (vendor-prefixed to
64
67
  * avoid collisions with the MCP-level `adcp_mcp_tasks` table from
@@ -80,12 +83,13 @@ export interface CreatePostgresTaskRegistryOptions {
80
83
  * @example
81
84
  * ```typescript
82
85
  * import { getDecisioningTaskRegistryMigration } from '@adcp/sdk/server/decisioning';
83
- * await pool.query(getDecisioningTaskRegistryMigration());
84
- * await pool.query(getDecisioningTaskRegistryMigration({ tableName: 'my_tasks' }));
86
+ * await pool.query(getDecisioningTaskRegistryMigration({ namespace: tenantId }));
87
+ * await pool.query(getDecisioningTaskRegistryMigration({ tableName: 'my_tasks', namespace: tenantId }));
85
88
  * ```
86
89
  */
87
- export declare function getDecisioningTaskRegistryMigration(options?: {
90
+ export declare function getDecisioningTaskRegistryMigration(options: {
88
91
  tableName?: string;
92
+ namespace: string;
89
93
  }): string;
90
94
  /**
91
95
  * Build a Postgres-backed `TaskRegistry`.
@@ -95,20 +99,10 @@ export declare function getDecisioningTaskRegistryMigration(options?: {
95
99
  * is enforced via SQL `WHERE status = 'submitted'` predicates so concurrent
96
100
  * webhook deliveries can't race to overwrite each other.
97
101
  *
98
- * **Multi-tenant deployments accountId namespacing.** When sharing a
99
- * single Postgres registry across tenants in a `TenantRegistry`
100
- * deployment (one table for all tenants), prefix `account_id` per-tenant
101
- * to prevent cross-tenant collisions. If tenant A and tenant B both
102
- * resolve a buyer to literal `account_id: 'acme'`, their tasks land in
103
- * the same row keyspace and `tasks_get` can't distinguish ownership
104
- * by `account_id` alone (the tenant boundary upstream IS protected by
105
- * `accounts.resolve(ref, ctx)` returning each tenant's own Account, but
106
- * the registry sees the same `acme` string in both rows). Recommended
107
- * pattern: adopters' `accounts.resolve` returns
108
- * `id: \`tenant_${tenantId}_${accountId}\`` so the table-level keys
109
- * stay distinct. Alternative: separate `tableName` per tenant, one
110
- * `createPostgresTaskRegistry({ pool, tableName })` per `TenantRegistry`
111
- * tenant — heavier ops, stronger isolation.
102
+ * **Multi-tenant deployments.** `namespace` is a trusted deployment/tenant
103
+ * partition and participates in every query plus the primary key. Construct
104
+ * one registry per tenant with a stable unique namespace; never derive it
105
+ * from buyer request parameters. Tenants may safely share the same table.
112
106
  */
113
107
  export declare function createPostgresTaskRegistry(opts: CreatePostgresTaskRegistryOptions): TaskRegistry;
114
108
  //# sourceMappingURL=postgres-task-registry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"postgres-task-registry.d.ts","sourceRoot":"","sources":["../../../../../src/lib/server/decisioning/runtime/postgres-task-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAIH,OAAO,KAAK,EAAc,YAAY,EAAc,MAAM,iBAAiB,CAAC;AAE5E;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;CAChH;AAED,MAAM,WAAW,iCAAiC;IAChD,mDAAmD;IACnD,IAAI,EAAE,WAAW,CAAC;IAClB;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA4DD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mCAAmC,CAAC,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CA0C5F;AAkCD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,iCAAiC,GAAG,YAAY,CA8GhG"}
1
+ {"version":3,"file":"postgres-task-registry.d.ts","sourceRoot":"","sources":["../../../../../src/lib/server/decisioning/runtime/postgres-task-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAIH,OAAO,KAAK,EAAc,YAAY,EAAiC,MAAM,iBAAiB,CAAC;AAE/F;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;CAChH;AAED,MAAM,WAAW,iCAAiC;IAChD,mDAAmD;IACnD,IAAI,EAAE,WAAW,CAAC;IAClB,mFAAmF;IACnF,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAqED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mCAAmC,CAAC,OAAO,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAoF9G;AAkCD;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,iCAAiC,GAAG,YAAY,CAsIhG"}
@@ -25,10 +25,18 @@ module.exports = __toCommonJS(postgres_task_registry_exports);
25
25
  var import_node_crypto = require("node:crypto");
26
26
  const DEFAULT_TABLE = "adcp_decisioning_tasks";
27
27
  const VALID_IDENTIFIER = /^[a-z_][a-z0-9_]*$/;
28
+ const VALID_NAMESPACE = /^[A-Za-z0-9_.:-]{1,255}$/;
28
29
  function assertValidIdentifier(name) {
29
- if (!VALID_IDENTIFIER.test(name)) {
30
+ if (!VALID_IDENTIFIER.test(name) || Buffer.byteLength(name, "utf8") > 40) {
30
31
  throw new Error(
31
- `Invalid table name "${name}": must be lowercase letters, digits, or underscores, starting with a letter or underscore.`
32
+ `Invalid table name "${name}": must be lowercase letters, digits, or underscores, starting with a letter or underscore, and at most 40 bytes.`
33
+ );
34
+ }
35
+ }
36
+ function assertValidNamespace(namespace) {
37
+ if (typeof namespace !== "string" || !VALID_NAMESPACE.test(namespace)) {
38
+ throw new Error(
39
+ "Invalid registry namespace: namespace is required and must be 1-255 ASCII letters, digits, dots, underscores, colons, or hyphens."
32
40
  );
33
41
  }
34
42
  }
@@ -51,14 +59,17 @@ function safeStringify(value, taskId) {
51
59
  }
52
60
  }
53
61
  function getDecisioningTaskRegistryMigration(options) {
62
+ assertValidNamespace(options?.namespace);
54
63
  const table = options?.tableName ?? DEFAULT_TABLE;
55
64
  assertValidIdentifier(table);
65
+ const namespace = options.namespace;
56
66
  return `
57
67
  CREATE TABLE IF NOT EXISTS ${table} (
58
- task_id TEXT PRIMARY KEY,
68
+ registry_namespace TEXT NOT NULL DEFAULT '${namespace}',
69
+ task_id TEXT NOT NULL,
59
70
  tool TEXT NOT NULL,
60
71
  account_id TEXT NOT NULL,
61
- owner_scope TEXT,
72
+ owner_scope TEXT NOT NULL,
62
73
  status TEXT NOT NULL DEFAULT 'submitted',
63
74
  status_message TEXT,
64
75
  result JSONB,
@@ -89,6 +100,45 @@ CREATE INDEX IF NOT EXISTS idx_${table}_status_created
89
100
  ALTER TABLE ${table}
90
101
  ADD COLUMN IF NOT EXISTS owner_scope TEXT;
91
102
 
103
+ ALTER TABLE ${table}
104
+ ADD COLUMN IF NOT EXISTS registry_namespace TEXT NOT NULL DEFAULT '__adcp_legacy_unscoped__';
105
+
106
+ UPDATE ${table}
107
+ SET registry_namespace = '${namespace}'
108
+ WHERE registry_namespace = '__adcp_legacy_unscoped__';
109
+
110
+ ALTER TABLE ${table}
111
+ ALTER COLUMN registry_namespace SET DEFAULT '${namespace}';
112
+
113
+ UPDATE ${table}
114
+ SET owner_scope = 'account:' || account_id
115
+ WHERE owner_scope IS NULL;
116
+
117
+ ALTER TABLE ${table}
118
+ ALTER COLUMN owner_scope SET NOT NULL;
119
+
120
+ DO $$
121
+ DECLARE
122
+ current_primary_key TEXT;
123
+ BEGIN
124
+ PERFORM pg_advisory_xact_lock(hashtext('adcp-task-registry:${table}'));
125
+ SELECT conname INTO current_primary_key
126
+ FROM pg_constraint
127
+ WHERE conrelid = '${table}'::regclass AND contype = 'p';
128
+ IF current_primary_key IS NOT NULL AND current_primary_key <> '${table}_scope_pkey' THEN
129
+ EXECUTE format('ALTER TABLE %I DROP CONSTRAINT %I', '${table}', current_primary_key);
130
+ END IF;
131
+ IF NOT EXISTS (
132
+ SELECT 1 FROM pg_constraint
133
+ WHERE conname = '${table}_scope_pkey'
134
+ AND conrelid = '${table}'::regclass
135
+ ) THEN
136
+ ALTER TABLE ${table}
137
+ ADD CONSTRAINT ${table}_scope_pkey
138
+ PRIMARY KEY (registry_namespace, account_id, owner_scope, task_id);
139
+ END IF;
140
+ END $$;
141
+
92
142
  CREATE INDEX IF NOT EXISTS idx_${table}_owner_account
93
143
  ON ${table}(owner_scope, account_id);
94
144
  `.trim();
@@ -113,18 +163,22 @@ function createPostgresTaskRegistry(opts) {
113
163
  const table = opts.tableName ?? DEFAULT_TABLE;
114
164
  assertValidIdentifier(table);
115
165
  const pool = opts.pool;
166
+ const namespace = opts.namespace;
167
+ assertValidNamespace(namespace);
116
168
  const backgrounds = /* @__PURE__ */ new Map();
117
169
  return {
170
+ scopeVersion: 1,
118
171
  async create(createOpts) {
119
172
  const taskId = createOpts.overrideTaskId ?? `task_${(0, import_node_crypto.randomUUID)()}`;
120
173
  const result = await pool.query(
121
- `INSERT INTO ${table} (task_id, tool, account_id, owner_scope, status, has_webhook) VALUES ($1, $2, $3, $4, 'submitted', $5) ON CONFLICT (task_id) DO NOTHING`,
174
+ `INSERT INTO ${table} (task_id, tool, account_id, owner_scope, status, has_webhook, registry_namespace) VALUES ($1, $2, $3, $4, 'submitted', $5, $6) ON CONFLICT (registry_namespace, account_id, owner_scope, task_id) DO NOTHING`,
122
175
  [
123
176
  taskId,
124
177
  createOpts.tool,
125
178
  createOpts.accountId,
126
179
  createOpts.ownerScope ?? `account:${createOpts.accountId}`,
127
- createOpts.hasWebhook === true
180
+ createOpts.hasWebhook === true,
181
+ namespace
128
182
  ]
129
183
  );
130
184
  if ((result.rowCount ?? 0) === 0) {
@@ -132,73 +186,89 @@ function createPostgresTaskRegistry(opts) {
132
186
  }
133
187
  return { taskId };
134
188
  },
135
- async getTask(taskId) {
189
+ async getTask(taskId, scope) {
136
190
  const { rows } = await pool.query(
137
191
  `SELECT task_id, tool, account_id, owner_scope, status, status_message, result, error, progress, has_webhook, created_at, updated_at
138
- FROM ${table} WHERE task_id = $1`,
139
- [taskId]
192
+ FROM ${table} WHERE task_id = $1 AND registry_namespace = $2 AND account_id = $3 AND owner_scope = $4`,
193
+ [taskId, namespace, scope.accountId, scope.ownerScope]
140
194
  );
141
195
  if (rows.length === 0) return null;
142
196
  return rowToRecord(rows[0]);
143
197
  },
198
+ async _getTaskUnsafe(taskId) {
199
+ const { rows } = await pool.query(
200
+ `SELECT task_id, tool, account_id, owner_scope, status, status_message, result, error, progress, has_webhook, created_at, updated_at
201
+ FROM ${table} WHERE task_id = $1 AND registry_namespace = $2 LIMIT 2`,
202
+ [taskId, namespace]
203
+ );
204
+ if (rows.length !== 1) return null;
205
+ return rowToRecord(rows[0]);
206
+ },
144
207
  async list(listOpts) {
145
- if (listOpts.ownerScope === void 0) return { tasks: [] };
146
208
  const { rows } = await pool.query(
147
209
  `SELECT task_id, tool, account_id, owner_scope, status, status_message, result, error, progress, has_webhook, created_at, updated_at
148
210
  FROM ${table}
149
- WHERE account_id = $1 AND (owner_scope = $2 OR (owner_scope IS NULL AND $2 = $3))
211
+ WHERE registry_namespace = $1 AND account_id = $2 AND owner_scope = $3
150
212
  ORDER BY created_at DESC, task_id DESC`,
151
- [listOpts.accountId, listOpts.ownerScope, `account:${listOpts.accountId}`]
213
+ [namespace, listOpts.accountId, listOpts.ownerScope]
152
214
  );
153
215
  return { tasks: rows.map((row) => rowToRecord(row)) };
154
216
  },
155
- async complete(taskId, result) {
217
+ async complete(taskId, scope, result) {
156
218
  const json = safeStringify(result, taskId);
157
219
  assertResultSize(json, taskId);
158
220
  await pool.query(
159
221
  `UPDATE ${table}
160
- SET status = 'completed', result = $2::jsonb, updated_at = NOW()
161
- WHERE task_id = $1 AND status NOT IN ('completed', 'failed', 'rejected', 'canceled')`,
162
- [taskId, json]
222
+ SET status = 'completed', result = $5::jsonb, updated_at = NOW()
223
+ WHERE task_id = $1 AND registry_namespace = $2 AND account_id = $3 AND owner_scope = $4
224
+ AND status NOT IN ('completed', 'failed', 'rejected', 'canceled')`,
225
+ [taskId, namespace, scope.accountId, scope.ownerScope, json]
163
226
  );
164
227
  },
165
- async fail(taskId, error, result) {
228
+ async fail(taskId, scope, error, result) {
166
229
  const errorJson = safeStringify(error, taskId);
167
230
  const resultJson = result === void 0 ? void 0 : safeStringify(result, taskId);
168
231
  assertResultSize(errorJson, taskId);
169
232
  if (resultJson !== void 0) assertResultSize(resultJson, taskId);
170
233
  await pool.query(
171
234
  `UPDATE ${table}
172
- SET status = 'failed', error = $2::jsonb, result = $3::jsonb, status_message = $4, updated_at = NOW()
173
- WHERE task_id = $1 AND status NOT IN ('completed', 'failed', 'rejected', 'canceled')`,
174
- [taskId, errorJson, resultJson ?? null, error.message]
235
+ SET status = 'failed', error = $5::jsonb, result = $6::jsonb, status_message = $7, updated_at = NOW()
236
+ WHERE task_id = $1 AND registry_namespace = $2 AND account_id = $3 AND owner_scope = $4
237
+ AND status NOT IN ('completed', 'failed', 'rejected', 'canceled')`,
238
+ [taskId, namespace, scope.accountId, scope.ownerScope, errorJson, resultJson ?? null, error.message]
175
239
  );
176
240
  },
177
- async updateProgress(taskId, progress) {
241
+ async updateProgress(taskId, scope, progress) {
178
242
  const json = safeStringify(progress, taskId);
179
243
  await pool.query(
180
244
  `UPDATE ${table}
181
- SET progress = $2::jsonb,
245
+ SET progress = $5::jsonb,
182
246
  status = CASE WHEN status = 'submitted' THEN 'working' ELSE status END,
183
247
  updated_at = NOW()
184
- WHERE task_id = $1 AND status NOT IN ('completed', 'failed', 'rejected', 'canceled')`,
185
- [taskId, json]
248
+ WHERE task_id = $1 AND registry_namespace = $2 AND account_id = $3 AND owner_scope = $4
249
+ AND status NOT IN ('completed', 'failed', 'rejected', 'canceled')`,
250
+ [taskId, namespace, scope.accountId, scope.ownerScope, json]
186
251
  );
187
252
  },
188
- _registerBackground(taskId, completion) {
253
+ _registerBackground(taskId, scope, completion) {
254
+ const backgroundKey = JSON.stringify([scope.accountId, scope.ownerScope, taskId]);
189
255
  const composed = completion.then(
190
256
  () => {
191
- if (backgrounds.get(taskId) === composed) backgrounds.delete(taskId);
257
+ if (backgrounds.get(backgroundKey) === composed) backgrounds.delete(backgroundKey);
192
258
  },
193
259
  () => {
194
- if (backgrounds.get(taskId) === composed) backgrounds.delete(taskId);
260
+ if (backgrounds.get(backgroundKey) === composed) backgrounds.delete(backgroundKey);
195
261
  }
196
262
  );
197
- backgrounds.set(taskId, composed);
263
+ backgrounds.set(backgroundKey, composed);
198
264
  },
199
- async awaitTask(taskId) {
200
- const pending = backgrounds.get(taskId);
265
+ async awaitTask(taskId, scope) {
266
+ const pending = backgrounds.get(JSON.stringify([scope.accountId, scope.ownerScope, taskId]));
201
267
  if (pending) await pending;
268
+ },
269
+ async _awaitTaskUnsafe(taskId) {
270
+ const pending = Array.from(backgrounds.entries()).filter(([key]) => JSON.parse(key)[2] === taskId).map(([, completion]) => completion);
271
+ await Promise.all(pending);
202
272
  }
203
273
  };
204
274
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../src/lib/server/decisioning/runtime/postgres-task-registry.ts"],"sourcesContent":["/**\n * PostgreSQL-backed `TaskRegistry` for the v6.0 decisioning runtime.\n *\n * The default `createInMemoryTaskRegistry()` loses task state on process\n * restart and doesn't share state across instances behind a load balancer —\n * fine for tests and local dev, broken for production HITL paths\n * (`createMediaBuyTask`, `syncCreativesTask`, etc.). Wire this in via\n * `createAdcpServerFromPlatform({ taskRegistry: createPostgresTaskRegistry({ pool }) })`\n * to persist task lifecycle across requests, processes, and crashes.\n *\n * @example\n * ```typescript\n * import { Pool } from 'pg';\n * import {\n * createAdcpServerFromPlatform,\n * createPostgresTaskRegistry,\n * getDecisioningTaskRegistryMigration,\n * } from '@adcp/sdk/server/decisioning';\n *\n * const pool = new Pool({ connectionString: process.env.DATABASE_URL });\n *\n * // Run once at boot — idempotent CREATE TABLE IF NOT EXISTS.\n * await pool.query(getDecisioningTaskRegistryMigration());\n *\n * const server = createAdcpServerFromPlatform(platform, {\n * name: 'My Ad Network',\n * version: '1.0.0',\n * taskRegistry: createPostgresTaskRegistry({ pool }),\n * });\n * ```\n *\n * **Background-completion lifecycle.** `_registerBackground` / `awaitTask`\n * are PROCESS-LOCAL — promises don't serialize. When a HITL `*Task` method\n * is invoked, the returned promise lives on the originating process; if\n * that process restarts before the method completes, the task record stays\n * in `submitted` state in Postgres but no `awaitTask` resolution is\n * possible from a different instance. Production HITL flows that span\n * process boundaries should drive completion via webhook → an explicit\n * `complete()` / `fail()` call from the webhook handler, not via\n * `awaitTask`. The MCP `tasks/get` wire path reads via `getTask` and is\n * already cross-instance.\n *\n * Status: Preview / 6.0.\n *\n * @public\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type { AdcpStructuredError, TaskHandoffProgress } from '../async-outcome';\nimport type { TaskRecord, TaskRegistry, TaskStatus } from './task-registry';\n\n/**\n * Minimal subset of the `pg.Pool` interface used by the registry.\n * Mirrors `PgQueryable` in `postgres-task-store.ts` so callers can pass\n * pools, transaction wrappers, or any pg-compatible query executor.\n */\nexport interface PgQueryable {\n query(text: string, values?: unknown[]): Promise<{ rows: Record<string, unknown>[]; rowCount: number | null }>;\n}\n\nexport interface CreatePostgresTaskRegistryOptions {\n /** A `pg.Pool` instance (or any `PgQueryable`). */\n pool: PgQueryable;\n /**\n * Table name. Defaults to `'adcp_decisioning_tasks'` (vendor-prefixed to\n * avoid collisions with the MCP-level `adcp_mcp_tasks` table from\n * `PostgresTaskStore` and any consumer tables).\n *\n * Must match `^[a-z_][a-z0-9_]*$` — schema-qualified names are not\n * supported by this validator. Set search_path on the pool if you need\n * a non-default schema.\n */\n tableName?: string;\n}\n\n/** Default table name — vendor-prefixed to avoid collisions. */\nconst DEFAULT_TABLE = 'adcp_decisioning_tasks';\n\n/** Validates a SQL identifier to prevent injection via table names. */\nconst VALID_IDENTIFIER = /^[a-z_][a-z0-9_]*$/;\n\nfunction assertValidIdentifier(name: string): void {\n if (!VALID_IDENTIFIER.test(name)) {\n throw new Error(\n `Invalid table name \"${name}\": must be lowercase letters, digits, ` +\n `or underscores, starting with a letter or underscore.`\n );\n }\n}\n\n/**\n * Cap on `result` / `error` JSONB column size. Adopter `*Task` returns are\n * written verbatim — a buggy or malicious adopter handing back a 1GB\n * result would OOM the Node process before pg complains. 4MB matches the\n * default Postgres `toast` row threshold and gives plenty of headroom for\n * legitimate task payloads.\n *\n * **This cap protects the DB write path only.** Adopter code that\n * serializes `result` / `error` for logs, metrics, or downstream services\n * MUST impose its own size cap — `JSON.stringify(result)` in a logger\n * call is unbounded.\n */\nconst MAX_RESULT_BYTES = 4 * 1024 * 1024;\n\nfunction assertResultSize(json: string, taskId: string): void {\n // `Buffer.byteLength` is utf-8 byte length, which is what Postgres stores.\n if (Buffer.byteLength(json, 'utf8') > MAX_RESULT_BYTES) {\n throw new Error(\n `Task ${taskId}: result/error JSON exceeds ${MAX_RESULT_BYTES} bytes ` +\n `(adopter *Task method returned an oversized payload — investigate ` +\n `whether the body should be persisted via blob storage and referenced).`\n );\n }\n}\n\n/**\n * Wrap `JSON.stringify` with a clearer error when the adopter `*Task`\n * return contains circular references. Default `TypeError: Converting\n * circular structure to JSON` doesn't surface the task id; this version\n * bubbles a registry-write error pointing at the adopter return shape.\n */\nfunction safeStringify(value: unknown, taskId: string): string {\n try {\n return JSON.stringify(value);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(\n `Task ${taskId}: adopter *Task return is not JSON-serializable: ${msg}. ` +\n `Strip circular refs / non-plain-data before returning from your *Task method.`\n );\n }\n}\n\n/**\n * Generate the SQL DDL for the decisioning task registry table.\n *\n * Run once at boot (idempotent — `CREATE TABLE IF NOT EXISTS`). Constraint\n * and index names are derived from the table name to avoid collisions when\n * multiple registries share the same database.\n *\n * @example\n * ```typescript\n * import { getDecisioningTaskRegistryMigration } from '@adcp/sdk/server/decisioning';\n * await pool.query(getDecisioningTaskRegistryMigration());\n * await pool.query(getDecisioningTaskRegistryMigration({ tableName: 'my_tasks' }));\n * ```\n */\nexport function getDecisioningTaskRegistryMigration(options?: { tableName?: string }): string {\n const table = options?.tableName ?? DEFAULT_TABLE;\n assertValidIdentifier(table);\n return `\nCREATE TABLE IF NOT EXISTS ${table} (\n task_id TEXT PRIMARY KEY,\n tool TEXT NOT NULL,\n account_id TEXT NOT NULL,\n owner_scope TEXT,\n status TEXT NOT NULL DEFAULT 'submitted',\n status_message TEXT,\n result JSONB,\n error JSONB,\n progress JSONB,\n has_webhook BOOLEAN NOT NULL DEFAULT FALSE,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n\n CONSTRAINT ${table}_valid_status CHECK (\n -- Framework-written values: 'submitted' (initial), 'working'\n -- (after first updateProgress() call), 'completed' / 'failed'\n -- (terminal). The other 5 spec-defined states ('input-required',\n -- 'canceled', 'rejected', 'auth-required', 'unknown') are reserved\n -- for adopter-emitted transitions via the v6.1\n -- \\`taskRegistry.transition()\\` API; the v6.1 migration will widen\n -- this CHECK.\n status IN ('submitted', 'working', 'completed', 'failed')\n )\n);\n\nCREATE INDEX IF NOT EXISTS idx_${table}_account_id\n ON ${table}(account_id);\n\nCREATE INDEX IF NOT EXISTS idx_${table}_status_created\n ON ${table}(status, created_at);\n\nALTER TABLE ${table}\n ADD COLUMN IF NOT EXISTS owner_scope TEXT;\n\nCREATE INDEX IF NOT EXISTS idx_${table}_owner_account\n ON ${table}(owner_scope, account_id);\n`.trim();\n}\n\ninterface DbTaskRow {\n task_id: string;\n tool: string;\n account_id: string;\n owner_scope: string | null;\n status: TaskStatus;\n status_message: string | null;\n result: unknown;\n error: AdcpStructuredError | null;\n progress: TaskHandoffProgress | null;\n has_webhook: boolean;\n created_at: Date;\n updated_at: Date;\n}\n\nfunction rowToRecord<TResult>(row: DbTaskRow): TaskRecord<TResult> {\n return {\n taskId: row.task_id,\n tool: row.tool,\n accountId: row.account_id,\n ...(row.owner_scope ? { ownerScope: row.owner_scope } : {}),\n status: row.status,\n ...(row.status_message !== null && { statusMessage: row.status_message }),\n ...(row.result !== null && row.result !== undefined && { result: row.result as TResult }),\n ...(row.error !== null && row.error !== undefined && { error: row.error }),\n ...(row.progress !== null && row.progress !== undefined && { progress: row.progress }),\n ...(row.has_webhook && { hasWebhook: true }),\n createdAt: row.created_at.toISOString(),\n updatedAt: row.updated_at.toISOString(),\n };\n}\n\n/**\n * Build a Postgres-backed `TaskRegistry`.\n *\n * Idempotency: `complete()` and `fail()` are no-ops on already-terminal\n * tasks, matching `createInMemoryTaskRegistry()`. The terminal-state guard\n * is enforced via SQL `WHERE status = 'submitted'` predicates so concurrent\n * webhook deliveries can't race to overwrite each other.\n *\n * **Multi-tenant deployments — accountId namespacing.** When sharing a\n * single Postgres registry across tenants in a `TenantRegistry`\n * deployment (one table for all tenants), prefix `account_id` per-tenant\n * to prevent cross-tenant collisions. If tenant A and tenant B both\n * resolve a buyer to literal `account_id: 'acme'`, their tasks land in\n * the same row keyspace and `tasks_get` can't distinguish ownership\n * by `account_id` alone (the tenant boundary upstream IS protected by\n * `accounts.resolve(ref, ctx)` returning each tenant's own Account, but\n * the registry sees the same `acme` string in both rows). Recommended\n * pattern: adopters' `accounts.resolve` returns\n * `id: \\`tenant_${tenantId}_${accountId}\\`` so the table-level keys\n * stay distinct. Alternative: separate `tableName` per tenant, one\n * `createPostgresTaskRegistry({ pool, tableName })` per `TenantRegistry`\n * tenant — heavier ops, stronger isolation.\n */\nexport function createPostgresTaskRegistry(opts: CreatePostgresTaskRegistryOptions): TaskRegistry {\n const table = opts.tableName ?? DEFAULT_TABLE;\n assertValidIdentifier(table);\n const pool = opts.pool;\n\n // Process-local background tracking — see file header note. Promises\n // can't be persisted; cross-instance HITL completion happens via\n // webhook → explicit complete()/fail(), not via awaitTask.\n const backgrounds = new Map<string, Promise<void>>();\n\n return {\n async create(createOpts: {\n tool: string;\n accountId: string;\n ownerScope?: string;\n hasWebhook?: boolean;\n overrideTaskId?: string;\n }): Promise<{ taskId: string }> {\n const taskId = createOpts.overrideTaskId ?? `task_${randomUUID()}`;\n const result = await pool.query(\n `INSERT INTO ${table} (task_id, tool, account_id, owner_scope, status, has_webhook) VALUES ($1, $2, $3, $4, 'submitted', $5) ON CONFLICT (task_id) DO NOTHING`,\n [\n taskId,\n createOpts.tool,\n createOpts.accountId,\n createOpts.ownerScope ?? `account:${createOpts.accountId}`,\n createOpts.hasWebhook === true,\n ]\n );\n if ((result.rowCount ?? 0) === 0) {\n throw new Error(`task_id already registered: ${taskId}`);\n }\n return { taskId };\n },\n\n async getTask<TResult = unknown>(taskId: string): Promise<TaskRecord<TResult> | null> {\n const { rows } = await pool.query(\n `SELECT task_id, tool, account_id, owner_scope, status, status_message, result, error, progress, has_webhook, created_at, updated_at\n FROM ${table} WHERE task_id = $1`,\n [taskId]\n );\n if (rows.length === 0) return null;\n return rowToRecord<TResult>(rows[0] as unknown as DbTaskRow);\n },\n\n async list(listOpts: { accountId: string; ownerScope?: string }): Promise<{ tasks: TaskRecord[] }> {\n if (listOpts.ownerScope === undefined) return { tasks: [] };\n const { rows } = await pool.query(\n `SELECT task_id, tool, account_id, owner_scope, status, status_message, result, error, progress, has_webhook, created_at, updated_at\n FROM ${table}\n WHERE account_id = $1 AND (owner_scope = $2 OR (owner_scope IS NULL AND $2 = $3))\n ORDER BY created_at DESC, task_id DESC`,\n [listOpts.accountId, listOpts.ownerScope, `account:${listOpts.accountId}`]\n );\n return { tasks: rows.map(row => rowToRecord(row as unknown as DbTaskRow)) };\n },\n\n async complete<TResult>(taskId: string, result: TResult): Promise<void> {\n const json = safeStringify(result, taskId);\n assertResultSize(json, taskId);\n await pool.query(\n `UPDATE ${table}\n SET status = 'completed', result = $2::jsonb, updated_at = NOW()\n WHERE task_id = $1 AND status NOT IN ('completed', 'failed', 'rejected', 'canceled')`,\n [taskId, json]\n );\n },\n\n async fail(taskId: string, error: AdcpStructuredError, result?: unknown): Promise<void> {\n const errorJson = safeStringify(error, taskId);\n const resultJson = result === undefined ? undefined : safeStringify(result, taskId);\n assertResultSize(errorJson, taskId);\n if (resultJson !== undefined) assertResultSize(resultJson, taskId);\n await pool.query(\n `UPDATE ${table}\n SET status = 'failed', error = $2::jsonb, result = $3::jsonb, status_message = $4, updated_at = NOW()\n WHERE task_id = $1 AND status NOT IN ('completed', 'failed', 'rejected', 'canceled')`,\n [taskId, errorJson, resultJson ?? null, error.message]\n );\n },\n\n async updateProgress(taskId: string, progress: TaskHandoffProgress): Promise<void> {\n const json = safeStringify(progress, taskId);\n await pool.query(\n `UPDATE ${table}\n SET progress = $2::jsonb,\n status = CASE WHEN status = 'submitted' THEN 'working' ELSE status END,\n updated_at = NOW()\n WHERE task_id = $1 AND status NOT IN ('completed', 'failed', 'rejected', 'canceled')`,\n [taskId, json]\n );\n },\n\n _registerBackground(taskId: string, completion: Promise<void>): void {\n const composed: Promise<void> = completion.then(\n () => {\n if (backgrounds.get(taskId) === composed) backgrounds.delete(taskId);\n },\n () => {\n if (backgrounds.get(taskId) === composed) backgrounds.delete(taskId);\n }\n );\n backgrounds.set(taskId, composed);\n },\n\n async awaitTask(taskId: string): Promise<void> {\n const pending = backgrounds.get(taskId);\n if (pending) await pending;\n },\n } satisfies TaskRegistry;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA+CA,yBAA2B;AA6B3B,MAAM,gBAAgB;AAGtB,MAAM,mBAAmB;AAEzB,SAAS,sBAAsB,MAAoB;AACjD,MAAI,CAAC,iBAAiB,KAAK,IAAI,GAAG;AAChC,UAAM,IAAI;AAAA,MACR,uBAAuB,IAAI;AAAA,IAE7B;AAAA,EACF;AACF;AAcA,MAAM,mBAAmB,IAAI,OAAO;AAEpC,SAAS,iBAAiB,MAAc,QAAsB;AAE5D,MAAI,OAAO,WAAW,MAAM,MAAM,IAAI,kBAAkB;AACtD,UAAM,IAAI;AAAA,MACR,QAAQ,MAAM,+BAA+B,gBAAgB;AAAA,IAG/D;AAAA,EACF;AACF;AAQA,SAAS,cAAc,OAAgB,QAAwB;AAC7D,MAAI;AACF,WAAO,KAAK,UAAU,KAAK;AAAA,EAC7B,SAAS,KAAK;AACZ,UAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,UAAM,IAAI;AAAA,MACR,QAAQ,MAAM,oDAAoD,GAAG;AAAA,IAEvE;AAAA,EACF;AACF;AAgBO,SAAS,oCAAoC,SAA0C;AAC5F,QAAM,QAAQ,SAAS,aAAa;AACpC,wBAAsB,KAAK;AAC3B,SAAO;AAAA,6BACoB,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,eAcnB,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iCAYa,KAAK;AAAA,OAC/B,KAAK;AAAA;AAAA,iCAEqB,KAAK;AAAA,OAC/B,KAAK;AAAA;AAAA,cAEE,KAAK;AAAA;AAAA;AAAA,iCAGc,KAAK;AAAA,OAC/B,KAAK;AAAA,EACV,KAAK;AACP;AAiBA,SAAS,YAAqB,KAAqC;AACjE,SAAO;AAAA,IACL,QAAQ,IAAI;AAAA,IACZ,MAAM,IAAI;AAAA,IACV,WAAW,IAAI;AAAA,IACf,GAAI,IAAI,cAAc,EAAE,YAAY,IAAI,YAAY,IAAI,CAAC;AAAA,IACzD,QAAQ,IAAI;AAAA,IACZ,GAAI,IAAI,mBAAmB,QAAQ,EAAE,eAAe,IAAI,eAAe;AAAA,IACvE,GAAI,IAAI,WAAW,QAAQ,IAAI,WAAW,UAAa,EAAE,QAAQ,IAAI,OAAkB;AAAA,IACvF,GAAI,IAAI,UAAU,QAAQ,IAAI,UAAU,UAAa,EAAE,OAAO,IAAI,MAAM;AAAA,IACxE,GAAI,IAAI,aAAa,QAAQ,IAAI,aAAa,UAAa,EAAE,UAAU,IAAI,SAAS;AAAA,IACpF,GAAI,IAAI,eAAe,EAAE,YAAY,KAAK;AAAA,IAC1C,WAAW,IAAI,WAAW,YAAY;AAAA,IACtC,WAAW,IAAI,WAAW,YAAY;AAAA,EACxC;AACF;AAyBO,SAAS,2BAA2B,MAAuD;AAChG,QAAM,QAAQ,KAAK,aAAa;AAChC,wBAAsB,KAAK;AAC3B,QAAM,OAAO,KAAK;AAKlB,QAAM,cAAc,oBAAI,IAA2B;AAEnD,SAAO;AAAA,IACL,MAAM,OAAO,YAMmB;AAC9B,YAAM,SAAS,WAAW,kBAAkB,YAAQ,+BAAW,CAAC;AAChE,YAAM,SAAS,MAAM,KAAK;AAAA,QACxB,eAAe,KAAK;AAAA,QACpB;AAAA,UACE;AAAA,UACA,WAAW;AAAA,UACX,WAAW;AAAA,UACX,WAAW,cAAc,WAAW,WAAW,SAAS;AAAA,UACxD,WAAW,eAAe;AAAA,QAC5B;AAAA,MACF;AACA,WAAK,OAAO,YAAY,OAAO,GAAG;AAChC,cAAM,IAAI,MAAM,+BAA+B,MAAM,EAAE;AAAA,MACzD;AACA,aAAO,EAAE,OAAO;AAAA,IAClB;AAAA,IAEA,MAAM,QAA2B,QAAqD;AACpF,YAAM,EAAE,KAAK,IAAI,MAAM,KAAK;AAAA,QAC1B;AAAA,gBACQ,KAAK;AAAA,QACb,CAAC,MAAM;AAAA,MACT;AACA,UAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,aAAO,YAAqB,KAAK,CAAC,CAAyB;AAAA,IAC7D;AAAA,IAEA,MAAM,KAAK,UAAwF;AACjG,UAAI,SAAS,eAAe,OAAW,QAAO,EAAE,OAAO,CAAC,EAAE;AAC1D,YAAM,EAAE,KAAK,IAAI,MAAM,KAAK;AAAA,QAC1B;AAAA,gBACQ,KAAK;AAAA;AAAA;AAAA,QAGb,CAAC,SAAS,WAAW,SAAS,YAAY,WAAW,SAAS,SAAS,EAAE;AAAA,MAC3E;AACA,aAAO,EAAE,OAAO,KAAK,IAAI,SAAO,YAAY,GAA2B,CAAC,EAAE;AAAA,IAC5E;AAAA,IAEA,MAAM,SAAkB,QAAgB,QAAgC;AACtE,YAAM,OAAO,cAAc,QAAQ,MAAM;AACzC,uBAAiB,MAAM,MAAM;AAC7B,YAAM,KAAK;AAAA,QACT,UAAU,KAAK;AAAA;AAAA;AAAA,QAGf,CAAC,QAAQ,IAAI;AAAA,MACf;AAAA,IACF;AAAA,IAEA,MAAM,KAAK,QAAgB,OAA4B,QAAiC;AACtF,YAAM,YAAY,cAAc,OAAO,MAAM;AAC7C,YAAM,aAAa,WAAW,SAAY,SAAY,cAAc,QAAQ,MAAM;AAClF,uBAAiB,WAAW,MAAM;AAClC,UAAI,eAAe,OAAW,kBAAiB,YAAY,MAAM;AACjE,YAAM,KAAK;AAAA,QACT,UAAU,KAAK;AAAA;AAAA;AAAA,QAGf,CAAC,QAAQ,WAAW,cAAc,MAAM,MAAM,OAAO;AAAA,MACvD;AAAA,IACF;AAAA,IAEA,MAAM,eAAe,QAAgB,UAA8C;AACjF,YAAM,OAAO,cAAc,UAAU,MAAM;AAC3C,YAAM,KAAK;AAAA,QACT,UAAU,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA,QAKf,CAAC,QAAQ,IAAI;AAAA,MACf;AAAA,IACF;AAAA,IAEA,oBAAoB,QAAgB,YAAiC;AACnE,YAAM,WAA0B,WAAW;AAAA,QACzC,MAAM;AACJ,cAAI,YAAY,IAAI,MAAM,MAAM,SAAU,aAAY,OAAO,MAAM;AAAA,QACrE;AAAA,QACA,MAAM;AACJ,cAAI,YAAY,IAAI,MAAM,MAAM,SAAU,aAAY,OAAO,MAAM;AAAA,QACrE;AAAA,MACF;AACA,kBAAY,IAAI,QAAQ,QAAQ;AAAA,IAClC;AAAA,IAEA,MAAM,UAAU,QAA+B;AAC7C,YAAM,UAAU,YAAY,IAAI,MAAM;AACtC,UAAI,QAAS,OAAM;AAAA,IACrB;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../../../src/lib/server/decisioning/runtime/postgres-task-registry.ts"],"sourcesContent":["/**\n * PostgreSQL-backed `TaskRegistry` for the v6.0 decisioning runtime.\n *\n * The default `createInMemoryTaskRegistry()` loses task state on process\n * restart and doesn't share state across instances behind a load balancer —\n * fine for tests and local dev, broken for production HITL paths\n * (`createMediaBuyTask`, `syncCreativesTask`, etc.). Wire this in via\n * `createAdcpServerFromPlatform({ taskRegistry: createPostgresTaskRegistry({ pool, namespace }) })`\n * to persist task lifecycle across requests, processes, and crashes.\n *\n * @example\n * ```typescript\n * import { Pool } from 'pg';\n * import {\n * createAdcpServerFromPlatform,\n * createPostgresTaskRegistry,\n * getDecisioningTaskRegistryMigration,\n * } from '@adcp/sdk/server/decisioning';\n *\n * const pool = new Pool({ connectionString: process.env.DATABASE_URL });\n *\n * const taskRegistryNamespace = 'tenant:my-agent';\n * // Run once at boot, using the same trusted namespace as the registry.\n * await pool.query(getDecisioningTaskRegistryMigration({ namespace: taskRegistryNamespace }));\n *\n * const server = createAdcpServerFromPlatform(platform, {\n * name: 'My Ad Network',\n * version: '1.0.0',\n * taskRegistry: createPostgresTaskRegistry({ pool, namespace: taskRegistryNamespace }),\n * });\n * ```\n *\n * **Background-completion lifecycle.** `_registerBackground` / `awaitTask`\n * are PROCESS-LOCAL — promises don't serialize. When a HITL `*Task` method\n * is invoked, the returned promise lives on the originating process; if\n * that process restarts before the method completes, the task record stays\n * in `submitted` state in Postgres but no `awaitTask` resolution is\n * possible from a different instance. Production HITL flows that span\n * process boundaries should drive completion via webhook → an explicit\n * `complete()` / `fail()` call from the webhook handler, not via\n * `awaitTask`. The MCP `tasks/get` wire path reads via `getTask` and is\n * already cross-instance.\n *\n * Status: Preview / 6.0.\n *\n * @public\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type { AdcpStructuredError, TaskHandoffProgress } from '../async-outcome';\nimport type { TaskRecord, TaskRegistry, TaskRegistryScope, TaskStatus } from './task-registry';\n\n/**\n * Minimal subset of the `pg.Pool` interface used by the registry.\n * Mirrors `PgQueryable` in `postgres-task-store.ts` so callers can pass\n * pools, transaction wrappers, or any pg-compatible query executor.\n */\nexport interface PgQueryable {\n query(text: string, values?: unknown[]): Promise<{ rows: Record<string, unknown>[]; rowCount: number | null }>;\n}\n\nexport interface CreatePostgresTaskRegistryOptions {\n /** A `pg.Pool` instance (or any `PgQueryable`). */\n pool: PgQueryable;\n /** Trusted deployment/tenant namespace. Use a distinct value per hosted tenant. */\n namespace: string;\n /**\n * Table name. Defaults to `'adcp_decisioning_tasks'` (vendor-prefixed to\n * avoid collisions with the MCP-level `adcp_mcp_tasks` table from\n * `PostgresTaskStore` and any consumer tables).\n *\n * Must match `^[a-z_][a-z0-9_]*$` — schema-qualified names are not\n * supported by this validator. Set search_path on the pool if you need\n * a non-default schema.\n */\n tableName?: string;\n}\n\n/** Default table name — vendor-prefixed to avoid collisions. */\nconst DEFAULT_TABLE = 'adcp_decisioning_tasks';\n\n/** Validates a SQL identifier to prevent injection via table names. */\nconst VALID_IDENTIFIER = /^[a-z_][a-z0-9_]*$/;\nconst VALID_NAMESPACE = /^[A-Za-z0-9_.:-]{1,255}$/;\n\nfunction assertValidIdentifier(name: string): void {\n if (!VALID_IDENTIFIER.test(name) || Buffer.byteLength(name, 'utf8') > 40) {\n throw new Error(\n `Invalid table name \"${name}\": must be lowercase letters, digits, ` +\n `or underscores, starting with a letter or underscore, and at most 40 bytes.`\n );\n }\n}\n\nfunction assertValidNamespace(namespace: unknown): asserts namespace is string {\n if (typeof namespace !== 'string' || !VALID_NAMESPACE.test(namespace)) {\n throw new Error(\n 'Invalid registry namespace: namespace is required and must be 1-255 ASCII letters, digits, dots, underscores, colons, or hyphens.'\n );\n }\n}\n\n/**\n * Cap on `result` / `error` JSONB column size. Adopter `*Task` returns are\n * written verbatim — a buggy or malicious adopter handing back a 1GB\n * result would OOM the Node process before pg complains. 4MB matches the\n * default Postgres `toast` row threshold and gives plenty of headroom for\n * legitimate task payloads.\n *\n * **This cap protects the DB write path only.** Adopter code that\n * serializes `result` / `error` for logs, metrics, or downstream services\n * MUST impose its own size cap — `JSON.stringify(result)` in a logger\n * call is unbounded.\n */\nconst MAX_RESULT_BYTES = 4 * 1024 * 1024;\n\nfunction assertResultSize(json: string, taskId: string): void {\n // `Buffer.byteLength` is utf-8 byte length, which is what Postgres stores.\n if (Buffer.byteLength(json, 'utf8') > MAX_RESULT_BYTES) {\n throw new Error(\n `Task ${taskId}: result/error JSON exceeds ${MAX_RESULT_BYTES} bytes ` +\n `(adopter *Task method returned an oversized payload — investigate ` +\n `whether the body should be persisted via blob storage and referenced).`\n );\n }\n}\n\n/**\n * Wrap `JSON.stringify` with a clearer error when the adopter `*Task`\n * return contains circular references. Default `TypeError: Converting\n * circular structure to JSON` doesn't surface the task id; this version\n * bubbles a registry-write error pointing at the adopter return shape.\n */\nfunction safeStringify(value: unknown, taskId: string): string {\n try {\n return JSON.stringify(value);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(\n `Task ${taskId}: adopter *Task return is not JSON-serializable: ${msg}. ` +\n `Strip circular refs / non-plain-data before returning from your *Task method.`\n );\n }\n}\n\n/**\n * Generate the SQL DDL for the decisioning task registry table.\n *\n * Run once at boot (idempotent — `CREATE TABLE IF NOT EXISTS`). Constraint\n * and index names are derived from the table name to avoid collisions when\n * multiple registries share the same database.\n *\n * @example\n * ```typescript\n * import { getDecisioningTaskRegistryMigration } from '@adcp/sdk/server/decisioning';\n * await pool.query(getDecisioningTaskRegistryMigration({ namespace: tenantId }));\n * await pool.query(getDecisioningTaskRegistryMigration({ tableName: 'my_tasks', namespace: tenantId }));\n * ```\n */\nexport function getDecisioningTaskRegistryMigration(options: { tableName?: string; namespace: string }): string {\n assertValidNamespace(options?.namespace);\n const table = options?.tableName ?? DEFAULT_TABLE;\n assertValidIdentifier(table);\n const namespace = options.namespace;\n return `\nCREATE TABLE IF NOT EXISTS ${table} (\n registry_namespace TEXT NOT NULL DEFAULT '${namespace}',\n task_id TEXT NOT NULL,\n tool TEXT NOT NULL,\n account_id TEXT NOT NULL,\n owner_scope TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'submitted',\n status_message TEXT,\n result JSONB,\n error JSONB,\n progress JSONB,\n has_webhook BOOLEAN NOT NULL DEFAULT FALSE,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n\n CONSTRAINT ${table}_valid_status CHECK (\n -- Framework-written values: 'submitted' (initial), 'working'\n -- (after first updateProgress() call), 'completed' / 'failed'\n -- (terminal). The other 5 spec-defined states ('input-required',\n -- 'canceled', 'rejected', 'auth-required', 'unknown') are reserved\n -- for adopter-emitted transitions via the v6.1\n -- \\`taskRegistry.transition()\\` API; the v6.1 migration will widen\n -- this CHECK.\n status IN ('submitted', 'working', 'completed', 'failed')\n )\n);\n\nCREATE INDEX IF NOT EXISTS idx_${table}_account_id\n ON ${table}(account_id);\n\nCREATE INDEX IF NOT EXISTS idx_${table}_status_created\n ON ${table}(status, created_at);\n\nALTER TABLE ${table}\n ADD COLUMN IF NOT EXISTS owner_scope TEXT;\n\nALTER TABLE ${table}\n ADD COLUMN IF NOT EXISTS registry_namespace TEXT NOT NULL DEFAULT '__adcp_legacy_unscoped__';\n\nUPDATE ${table}\n SET registry_namespace = '${namespace}'\n WHERE registry_namespace = '__adcp_legacy_unscoped__';\n\nALTER TABLE ${table}\n ALTER COLUMN registry_namespace SET DEFAULT '${namespace}';\n\nUPDATE ${table}\n SET owner_scope = 'account:' || account_id\n WHERE owner_scope IS NULL;\n\nALTER TABLE ${table}\n ALTER COLUMN owner_scope SET NOT NULL;\n\nDO $$\nDECLARE\n current_primary_key TEXT;\nBEGIN\n PERFORM pg_advisory_xact_lock(hashtext('adcp-task-registry:${table}'));\n SELECT conname INTO current_primary_key\n FROM pg_constraint\n WHERE conrelid = '${table}'::regclass AND contype = 'p';\n IF current_primary_key IS NOT NULL AND current_primary_key <> '${table}_scope_pkey' THEN\n EXECUTE format('ALTER TABLE %I DROP CONSTRAINT %I', '${table}', current_primary_key);\n END IF;\n IF NOT EXISTS (\n SELECT 1 FROM pg_constraint\n WHERE conname = '${table}_scope_pkey'\n AND conrelid = '${table}'::regclass\n ) THEN\n ALTER TABLE ${table}\n ADD CONSTRAINT ${table}_scope_pkey\n PRIMARY KEY (registry_namespace, account_id, owner_scope, task_id);\n END IF;\nEND $$;\n\nCREATE INDEX IF NOT EXISTS idx_${table}_owner_account\n ON ${table}(owner_scope, account_id);\n`.trim();\n}\n\ninterface DbTaskRow {\n task_id: string;\n tool: string;\n account_id: string;\n owner_scope: string | null;\n status: TaskStatus;\n status_message: string | null;\n result: unknown;\n error: AdcpStructuredError | null;\n progress: TaskHandoffProgress | null;\n has_webhook: boolean;\n created_at: Date;\n updated_at: Date;\n}\n\nfunction rowToRecord<TResult>(row: DbTaskRow): TaskRecord<TResult> {\n return {\n taskId: row.task_id,\n tool: row.tool,\n accountId: row.account_id,\n ...(row.owner_scope ? { ownerScope: row.owner_scope } : {}),\n status: row.status,\n ...(row.status_message !== null && { statusMessage: row.status_message }),\n ...(row.result !== null && row.result !== undefined && { result: row.result as TResult }),\n ...(row.error !== null && row.error !== undefined && { error: row.error }),\n ...(row.progress !== null && row.progress !== undefined && { progress: row.progress }),\n ...(row.has_webhook && { hasWebhook: true }),\n createdAt: row.created_at.toISOString(),\n updatedAt: row.updated_at.toISOString(),\n };\n}\n\n/**\n * Build a Postgres-backed `TaskRegistry`.\n *\n * Idempotency: `complete()` and `fail()` are no-ops on already-terminal\n * tasks, matching `createInMemoryTaskRegistry()`. The terminal-state guard\n * is enforced via SQL `WHERE status = 'submitted'` predicates so concurrent\n * webhook deliveries can't race to overwrite each other.\n *\n * **Multi-tenant deployments.** `namespace` is a trusted deployment/tenant\n * partition and participates in every query plus the primary key. Construct\n * one registry per tenant with a stable unique namespace; never derive it\n * from buyer request parameters. Tenants may safely share the same table.\n */\nexport function createPostgresTaskRegistry(opts: CreatePostgresTaskRegistryOptions): TaskRegistry {\n const table = opts.tableName ?? DEFAULT_TABLE;\n assertValidIdentifier(table);\n const pool = opts.pool;\n const namespace = opts.namespace;\n assertValidNamespace(namespace);\n\n // Process-local background tracking — see file header note. Promises\n // can't be persisted; cross-instance HITL completion happens via\n // webhook → explicit complete()/fail(), not via awaitTask.\n const backgrounds = new Map<string, Promise<void>>();\n\n return {\n scopeVersion: 1,\n async create(createOpts: {\n tool: string;\n accountId: string;\n ownerScope?: string;\n hasWebhook?: boolean;\n overrideTaskId?: string;\n }): Promise<{ taskId: string }> {\n const taskId = createOpts.overrideTaskId ?? `task_${randomUUID()}`;\n const result = await pool.query(\n `INSERT INTO ${table} (task_id, tool, account_id, owner_scope, status, has_webhook, registry_namespace) VALUES ($1, $2, $3, $4, 'submitted', $5, $6) ON CONFLICT (registry_namespace, account_id, owner_scope, task_id) DO NOTHING`,\n [\n taskId,\n createOpts.tool,\n createOpts.accountId,\n createOpts.ownerScope ?? `account:${createOpts.accountId}`,\n createOpts.hasWebhook === true,\n namespace,\n ]\n );\n if ((result.rowCount ?? 0) === 0) {\n throw new Error(`task_id already registered: ${taskId}`);\n }\n return { taskId };\n },\n\n async getTask<TResult = unknown>(taskId: string, scope: TaskRegistryScope): Promise<TaskRecord<TResult> | null> {\n const { rows } = await pool.query(\n `SELECT task_id, tool, account_id, owner_scope, status, status_message, result, error, progress, has_webhook, created_at, updated_at\n FROM ${table} WHERE task_id = $1 AND registry_namespace = $2 AND account_id = $3 AND owner_scope = $4`,\n [taskId, namespace, scope.accountId, scope.ownerScope]\n );\n if (rows.length === 0) return null;\n return rowToRecord<TResult>(rows[0] as unknown as DbTaskRow);\n },\n\n async _getTaskUnsafe<TResult = unknown>(taskId: string): Promise<TaskRecord<TResult> | null> {\n const { rows } = await pool.query(\n `SELECT task_id, tool, account_id, owner_scope, status, status_message, result, error, progress, has_webhook, created_at, updated_at\n FROM ${table} WHERE task_id = $1 AND registry_namespace = $2 LIMIT 2`,\n [taskId, namespace]\n );\n if (rows.length !== 1) return null;\n return rowToRecord<TResult>(rows[0] as unknown as DbTaskRow);\n },\n\n async list(listOpts: { accountId: string; ownerScope: string }): Promise<{ tasks: TaskRecord[] }> {\n const { rows } = await pool.query(\n `SELECT task_id, tool, account_id, owner_scope, status, status_message, result, error, progress, has_webhook, created_at, updated_at\n FROM ${table}\n WHERE registry_namespace = $1 AND account_id = $2 AND owner_scope = $3\n ORDER BY created_at DESC, task_id DESC`,\n [namespace, listOpts.accountId, listOpts.ownerScope]\n );\n return { tasks: rows.map(row => rowToRecord(row as unknown as DbTaskRow)) };\n },\n\n async complete<TResult>(taskId: string, scope: TaskRegistryScope, result: TResult): Promise<void> {\n const json = safeStringify(result, taskId);\n assertResultSize(json, taskId);\n await pool.query(\n `UPDATE ${table}\n SET status = 'completed', result = $5::jsonb, updated_at = NOW()\n WHERE task_id = $1 AND registry_namespace = $2 AND account_id = $3 AND owner_scope = $4\n AND status NOT IN ('completed', 'failed', 'rejected', 'canceled')`,\n [taskId, namespace, scope.accountId, scope.ownerScope, json]\n );\n },\n\n async fail(taskId: string, scope: TaskRegistryScope, error: AdcpStructuredError, result?: unknown): Promise<void> {\n const errorJson = safeStringify(error, taskId);\n const resultJson = result === undefined ? undefined : safeStringify(result, taskId);\n assertResultSize(errorJson, taskId);\n if (resultJson !== undefined) assertResultSize(resultJson, taskId);\n await pool.query(\n `UPDATE ${table}\n SET status = 'failed', error = $5::jsonb, result = $6::jsonb, status_message = $7, updated_at = NOW()\n WHERE task_id = $1 AND registry_namespace = $2 AND account_id = $3 AND owner_scope = $4\n AND status NOT IN ('completed', 'failed', 'rejected', 'canceled')`,\n [taskId, namespace, scope.accountId, scope.ownerScope, errorJson, resultJson ?? null, error.message]\n );\n },\n\n async updateProgress(taskId: string, scope: TaskRegistryScope, progress: TaskHandoffProgress): Promise<void> {\n const json = safeStringify(progress, taskId);\n await pool.query(\n `UPDATE ${table}\n SET progress = $5::jsonb,\n status = CASE WHEN status = 'submitted' THEN 'working' ELSE status END,\n updated_at = NOW()\n WHERE task_id = $1 AND registry_namespace = $2 AND account_id = $3 AND owner_scope = $4\n AND status NOT IN ('completed', 'failed', 'rejected', 'canceled')`,\n [taskId, namespace, scope.accountId, scope.ownerScope, json]\n );\n },\n\n _registerBackground(taskId: string, scope: TaskRegistryScope, completion: Promise<void>): void {\n const backgroundKey = JSON.stringify([scope.accountId, scope.ownerScope, taskId]);\n const composed: Promise<void> = completion.then(\n () => {\n if (backgrounds.get(backgroundKey) === composed) backgrounds.delete(backgroundKey);\n },\n () => {\n if (backgrounds.get(backgroundKey) === composed) backgrounds.delete(backgroundKey);\n }\n );\n backgrounds.set(backgroundKey, composed);\n },\n\n async awaitTask(taskId: string, scope: TaskRegistryScope): Promise<void> {\n const pending = backgrounds.get(JSON.stringify([scope.accountId, scope.ownerScope, taskId]));\n if (pending) await pending;\n },\n\n async _awaitTaskUnsafe(taskId: string): Promise<void> {\n const pending = Array.from(backgrounds.entries())\n .filter(([key]) => (JSON.parse(key) as [string, string, string])[2] === taskId)\n .map(([, completion]) => completion);\n await Promise.all(pending);\n },\n } satisfies TaskRegistry;\n}\n"],"mappings":"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","names":[]}