@hotmeshio/hotmesh 0.13.0 → 0.14.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 (194) hide show
  1. package/README.md +18 -22
  2. package/build/modules/enums.d.ts +60 -5
  3. package/build/modules/enums.js +62 -7
  4. package/build/modules/errors.d.ts +15 -3
  5. package/build/modules/errors.js +17 -2
  6. package/build/package.json +6 -1
  7. package/build/services/activities/activity/context.d.ts +22 -0
  8. package/build/services/activities/activity/context.js +76 -0
  9. package/build/services/activities/activity/index.d.ts +116 -0
  10. package/build/services/activities/activity/index.js +299 -0
  11. package/build/services/activities/activity/mapping.d.ts +12 -0
  12. package/build/services/activities/activity/mapping.js +63 -0
  13. package/build/services/activities/activity/process.d.ts +28 -0
  14. package/build/services/activities/activity/process.js +100 -0
  15. package/build/services/activities/activity/protocol.d.ts +39 -0
  16. package/build/services/activities/activity/protocol.js +151 -0
  17. package/build/services/activities/activity/state.d.ts +40 -0
  18. package/build/services/activities/activity/state.js +143 -0
  19. package/build/services/activities/activity/transition.d.ts +23 -0
  20. package/build/services/activities/activity/transition.js +71 -0
  21. package/build/services/activities/activity/verify.d.ts +22 -0
  22. package/build/services/activities/activity/verify.js +85 -0
  23. package/build/services/activities/await.d.ts +1 -4
  24. package/build/services/activities/await.js +2 -36
  25. package/build/services/activities/cycle.d.ts +1 -11
  26. package/build/services/activities/cycle.js +3 -46
  27. package/build/services/activities/hook.d.ts +2 -11
  28. package/build/services/activities/hook.js +30 -50
  29. package/build/services/activities/interrupt.d.ts +2 -4
  30. package/build/services/activities/interrupt.js +4 -38
  31. package/build/services/activities/signal.d.ts +1 -11
  32. package/build/services/activities/signal.js +3 -48
  33. package/build/services/activities/trigger.d.ts +1 -3
  34. package/build/services/activities/trigger.js +0 -3
  35. package/build/services/activities/worker.d.ts +3 -6
  36. package/build/services/activities/worker.js +4 -40
  37. package/build/services/connector/factory.d.ts +6 -0
  38. package/build/services/connector/factory.js +24 -0
  39. package/build/services/durable/activity.d.ts +1 -1
  40. package/build/services/durable/activity.js +2 -2
  41. package/build/services/durable/client.d.ts +24 -29
  42. package/build/services/durable/client.js +24 -29
  43. package/build/services/durable/connection.d.ts +13 -7
  44. package/build/services/durable/connection.js +13 -7
  45. package/build/services/durable/handle.d.ts +58 -40
  46. package/build/services/durable/handle.js +60 -40
  47. package/build/services/durable/index.d.ts +148 -286
  48. package/build/services/durable/index.js +157 -292
  49. package/build/services/durable/interceptor.d.ts +43 -33
  50. package/build/services/durable/interceptor.js +59 -39
  51. package/build/services/durable/schemas/factory.d.ts +1 -1
  52. package/build/services/durable/schemas/factory.js +168 -38
  53. package/build/services/durable/telemetry.d.ts +80 -0
  54. package/build/services/durable/telemetry.js +137 -0
  55. package/build/services/durable/worker.d.ts +100 -21
  56. package/build/services/durable/worker.js +304 -63
  57. package/build/services/durable/workflow/all.d.ts +1 -1
  58. package/build/services/durable/workflow/all.js +1 -1
  59. package/build/services/durable/workflow/cancellationScope.d.ts +104 -0
  60. package/build/services/durable/workflow/cancellationScope.js +139 -0
  61. package/build/services/durable/workflow/common.d.ts +5 -4
  62. package/build/services/durable/workflow/common.js +6 -1
  63. package/build/services/durable/workflow/{waitFor.d.ts → condition.d.ts} +9 -8
  64. package/build/services/durable/workflow/{waitFor.js → condition.js} +44 -11
  65. package/build/services/durable/workflow/continueAsNew.d.ts +65 -0
  66. package/build/services/durable/workflow/continueAsNew.js +92 -0
  67. package/build/services/durable/workflow/didRun.d.ts +1 -1
  68. package/build/services/durable/workflow/didRun.js +3 -3
  69. package/build/services/durable/workflow/enrich.d.ts +5 -0
  70. package/build/services/durable/workflow/enrich.js +5 -0
  71. package/build/services/durable/workflow/entityMethods.d.ts +7 -0
  72. package/build/services/durable/workflow/entityMethods.js +7 -0
  73. package/build/services/durable/workflow/execHook.js +3 -3
  74. package/build/services/durable/workflow/execHookBatch.js +2 -2
  75. package/build/services/durable/workflow/{execChild.d.ts → executeChild.d.ts} +4 -40
  76. package/build/services/durable/workflow/{execChild.js → executeChild.js} +36 -45
  77. package/build/services/durable/workflow/hook.d.ts +1 -1
  78. package/build/services/durable/workflow/hook.js +4 -3
  79. package/build/services/durable/workflow/index.d.ts +45 -50
  80. package/build/services/durable/workflow/index.js +46 -51
  81. package/build/services/durable/workflow/interruption.d.ts +7 -6
  82. package/build/services/durable/workflow/interruption.js +11 -7
  83. package/build/services/durable/workflow/patched.d.ts +72 -0
  84. package/build/services/durable/workflow/patched.js +110 -0
  85. package/build/services/durable/workflow/proxyActivities.d.ts +7 -7
  86. package/build/services/durable/workflow/proxyActivities.js +50 -15
  87. package/build/services/durable/workflow/searchMethods.d.ts +7 -0
  88. package/build/services/durable/workflow/searchMethods.js +7 -0
  89. package/build/services/durable/workflow/signal.d.ts +4 -4
  90. package/build/services/durable/workflow/signal.js +4 -4
  91. package/build/services/durable/workflow/{sleepFor.d.ts → sleep.d.ts} +7 -7
  92. package/build/services/durable/workflow/{sleepFor.js → sleep.js} +39 -10
  93. package/build/services/durable/workflow/terminate.d.ts +55 -0
  94. package/build/services/durable/workflow/{interrupt.js → terminate.js} +21 -21
  95. package/build/services/durable/workflow/trace.js +2 -2
  96. package/build/services/durable/workflow/uuid4.d.ts +14 -0
  97. package/build/services/durable/workflow/uuid4.js +39 -0
  98. package/build/services/durable/workflow/{context.d.ts → workflowInfo.d.ts} +5 -5
  99. package/build/services/durable/workflow/{context.js → workflowInfo.js} +7 -7
  100. package/build/services/engine/compiler.d.ts +19 -0
  101. package/build/services/engine/compiler.js +20 -0
  102. package/build/services/engine/completion.d.ts +46 -0
  103. package/build/services/engine/completion.js +145 -0
  104. package/build/services/engine/dispatch.d.ts +24 -0
  105. package/build/services/engine/dispatch.js +98 -0
  106. package/build/services/engine/index.d.ts +49 -81
  107. package/build/services/engine/index.js +175 -573
  108. package/build/services/engine/init.d.ts +42 -0
  109. package/build/services/engine/init.js +74 -0
  110. package/build/services/engine/pubsub.d.ts +50 -0
  111. package/build/services/engine/pubsub.js +118 -0
  112. package/build/services/engine/reporting.d.ts +20 -0
  113. package/build/services/engine/reporting.js +38 -0
  114. package/build/services/engine/schema.d.ts +23 -0
  115. package/build/services/engine/schema.js +62 -0
  116. package/build/services/engine/signal.d.ts +57 -0
  117. package/build/services/engine/signal.js +117 -0
  118. package/build/services/engine/state.d.ts +35 -0
  119. package/build/services/engine/state.js +61 -0
  120. package/build/services/engine/version.d.ts +31 -0
  121. package/build/services/engine/version.js +73 -0
  122. package/build/services/hotmesh/deployment.d.ts +21 -0
  123. package/build/services/hotmesh/deployment.js +25 -0
  124. package/build/services/hotmesh/index.d.ts +141 -532
  125. package/build/services/hotmesh/index.js +222 -673
  126. package/build/services/hotmesh/init.d.ts +42 -0
  127. package/build/services/hotmesh/init.js +93 -0
  128. package/build/services/hotmesh/jobs.d.ts +67 -0
  129. package/build/services/hotmesh/jobs.js +99 -0
  130. package/build/services/hotmesh/pubsub.d.ts +38 -0
  131. package/build/services/hotmesh/pubsub.js +54 -0
  132. package/build/services/hotmesh/quorum.d.ts +30 -0
  133. package/build/services/hotmesh/quorum.js +62 -0
  134. package/build/services/hotmesh/validation.d.ts +6 -0
  135. package/build/services/hotmesh/validation.js +28 -0
  136. package/build/services/quorum/index.js +1 -0
  137. package/build/services/router/consumption/index.d.ts +11 -5
  138. package/build/services/router/consumption/index.js +24 -17
  139. package/build/services/router/error-handling/index.d.ts +2 -2
  140. package/build/services/router/error-handling/index.js +14 -14
  141. package/build/services/router/index.d.ts +1 -1
  142. package/build/services/router/index.js +2 -2
  143. package/build/services/serializer/index.d.ts +22 -0
  144. package/build/services/serializer/index.js +39 -1
  145. package/build/services/store/index.d.ts +1 -0
  146. package/build/services/store/providers/postgres/exporter-sql.d.ts +2 -2
  147. package/build/services/store/providers/postgres/exporter-sql.js +4 -4
  148. package/build/services/store/providers/postgres/kvtables.js +7 -6
  149. package/build/services/store/providers/postgres/kvtypes/hash/basic.js +67 -52
  150. package/build/services/store/providers/postgres/kvtypes/hash/jsonb.js +87 -72
  151. package/build/services/store/providers/postgres/kvtypes/hash/udata.js +106 -79
  152. package/build/services/store/providers/postgres/kvtypes/hash/utils.d.ts +16 -0
  153. package/build/services/store/providers/postgres/kvtypes/hash/utils.js +29 -16
  154. package/build/services/store/providers/postgres/postgres.d.ts +1 -0
  155. package/build/services/store/providers/postgres/postgres.js +14 -4
  156. package/build/services/stream/factory.d.ts +3 -1
  157. package/build/services/stream/factory.js +2 -2
  158. package/build/services/stream/index.d.ts +1 -0
  159. package/build/services/stream/providers/nats/nats.d.ts +1 -0
  160. package/build/services/stream/providers/nats/nats.js +1 -0
  161. package/build/services/stream/providers/postgres/credentials.d.ts +56 -0
  162. package/build/services/stream/providers/postgres/credentials.js +129 -0
  163. package/build/services/stream/providers/postgres/kvtables.js +18 -0
  164. package/build/services/stream/providers/postgres/messages.js +7 -7
  165. package/build/services/stream/providers/postgres/notifications.js +16 -2
  166. package/build/services/stream/providers/postgres/postgres.d.ts +7 -0
  167. package/build/services/stream/providers/postgres/postgres.js +35 -4
  168. package/build/services/stream/providers/postgres/procedures.d.ts +21 -0
  169. package/build/services/stream/providers/postgres/procedures.js +213 -0
  170. package/build/services/stream/providers/postgres/secured.d.ts +34 -0
  171. package/build/services/stream/providers/postgres/secured.js +146 -0
  172. package/build/services/stream/providers/postgres/stats.d.ts +1 -0
  173. package/build/services/stream/providers/postgres/stats.js +1 -0
  174. package/build/services/stream/registry.d.ts +1 -1
  175. package/build/services/stream/registry.js +5 -2
  176. package/build/services/telemetry/index.d.ts +10 -1
  177. package/build/services/telemetry/index.js +40 -7
  178. package/build/services/worker/credentials.d.ts +51 -0
  179. package/build/services/worker/credentials.js +87 -0
  180. package/build/services/worker/index.d.ts +2 -2
  181. package/build/services/worker/index.js +7 -6
  182. package/build/types/codec.d.ts +84 -0
  183. package/build/types/codec.js +2 -0
  184. package/build/types/durable.d.ts +104 -28
  185. package/build/types/error.d.ts +10 -1
  186. package/build/types/hotmesh.d.ts +67 -4
  187. package/build/types/index.d.ts +2 -1
  188. package/build/types/provider.d.ts +2 -2
  189. package/build/types/quorum.d.ts +35 -1
  190. package/build/types/stream.d.ts +12 -6
  191. package/package.json +6 -1
  192. package/build/services/activities/activity.d.ts +0 -192
  193. package/build/services/activities/activity.js +0 -786
  194. package/build/services/durable/workflow/interrupt.d.ts +0 -55
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Worker role credential lifecycle management.
3
+ *
4
+ * These are the low-level functions that create, rotate, and revoke
5
+ * Postgres roles for worker routers. Each role is scoped to specific
6
+ * stream_name(s) via the `app.allowed_streams` session variable.
7
+ *
8
+ * **Prefer the high-level API**: Call `HotMesh.provisionWorkerRole()`,
9
+ * `HotMesh.rotateWorkerPassword()`, etc. — those methods handle
10
+ * connection creation and cleanup automatically.
11
+ */
12
+ import { PostgresClientType } from '../../../../types/postgres';
13
+ export interface WorkerCredential {
14
+ roleName: string;
15
+ password: string;
16
+ }
17
+ export interface WorkerCredentialInfo {
18
+ id: number;
19
+ roleName: string;
20
+ streamNames: string[];
21
+ createdAt: Date;
22
+ revokedAt: Date | null;
23
+ lastRotatedAt: Date;
24
+ }
25
+ /**
26
+ * Provision a new Postgres role scoped to specific stream names.
27
+ *
28
+ * The role:
29
+ * - Can LOGIN with the returned password
30
+ * - Has USAGE on the schema
31
+ * - Has EXECUTE on the 5 worker stored procedures
32
+ * - Has `app.allowed_streams` set to the comma-separated stream names
33
+ * - Has NO direct table access
34
+ *
35
+ * @param adminClient - A Postgres client connected as the admin/owner
36
+ * @param schema - The appId schema name (e.g., 'durable')
37
+ * @param streamNames - Stream names this role can access (e.g., ['payment-activity'])
38
+ * @param password - Optional password (generated if not provided)
39
+ * @returns The created role name and password
40
+ */
41
+ export declare function provisionWorkerRole(adminClient: PostgresClientType, schema: string, streamNames: string[], password?: string): Promise<WorkerCredential>;
42
+ /**
43
+ * Rotate the password for an existing worker role.
44
+ */
45
+ export declare function rotateWorkerPassword(adminClient: PostgresClientType, schema: string, roleName: string, newPassword?: string): Promise<{
46
+ password: string;
47
+ }>;
48
+ /**
49
+ * Revoke a worker role by disabling login.
50
+ * The role is not dropped — this preserves the audit trail.
51
+ */
52
+ export declare function revokeWorkerRole(adminClient: PostgresClientType, schema: string, roleName: string): Promise<void>;
53
+ /**
54
+ * List all worker roles for a schema.
55
+ */
56
+ export declare function listWorkerRoles(adminClient: PostgresClientType, schema: string): Promise<WorkerCredentialInfo[]>;
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ /**
3
+ * Worker role credential lifecycle management.
4
+ *
5
+ * These are the low-level functions that create, rotate, and revoke
6
+ * Postgres roles for worker routers. Each role is scoped to specific
7
+ * stream_name(s) via the `app.allowed_streams` session variable.
8
+ *
9
+ * **Prefer the high-level API**: Call `HotMesh.provisionWorkerRole()`,
10
+ * `HotMesh.rotateWorkerPassword()`, etc. — those methods handle
11
+ * connection creation and cleanup automatically.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.listWorkerRoles = exports.revokeWorkerRole = exports.rotateWorkerPassword = exports.provisionWorkerRole = void 0;
15
+ const crypto_1 = require("crypto");
16
+ /**
17
+ * Sanitize a stream name for use in a Postgres role name.
18
+ * Replaces non-alphanumeric characters with underscores.
19
+ */
20
+ function sanitizeForRoleName(name) {
21
+ return name.replace(/[^a-zA-Z0-9]/g, '_');
22
+ }
23
+ /**
24
+ * Provision a new Postgres role scoped to specific stream names.
25
+ *
26
+ * The role:
27
+ * - Can LOGIN with the returned password
28
+ * - Has USAGE on the schema
29
+ * - Has EXECUTE on the 5 worker stored procedures
30
+ * - Has `app.allowed_streams` set to the comma-separated stream names
31
+ * - Has NO direct table access
32
+ *
33
+ * @param adminClient - A Postgres client connected as the admin/owner
34
+ * @param schema - The appId schema name (e.g., 'durable')
35
+ * @param streamNames - Stream names this role can access (e.g., ['payment-activity'])
36
+ * @param password - Optional password (generated if not provided)
37
+ * @returns The created role name and password
38
+ */
39
+ async function provisionWorkerRole(adminClient, schema, streamNames, password) {
40
+ const safeSchema = schema.replace(/[^a-zA-Z0-9_]/g, '_');
41
+ const streamSuffix = streamNames.map(sanitizeForRoleName).join('__');
42
+ const roleName = `hmsh_wrk_${safeSchema}_${streamSuffix}`.substring(0, 63);
43
+ const rolePassword = password || (0, crypto_1.randomUUID)();
44
+ const allowedStreams = streamNames.join(',');
45
+ // Create the role (idempotent — drop if exists first for credential reset)
46
+ await adminClient.query(`
47
+ DO $$
48
+ BEGIN
49
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${roleName}') THEN
50
+ EXECUTE format('ALTER ROLE %I LOGIN PASSWORD %L', '${roleName}', '${rolePassword}');
51
+ ELSE
52
+ EXECUTE format('CREATE ROLE %I LOGIN PASSWORD %L', '${roleName}', '${rolePassword}');
53
+ END IF;
54
+ END
55
+ $$;
56
+ `);
57
+ // Grant schema usage (required to call functions in the schema)
58
+ await adminClient.query(`GRANT USAGE ON SCHEMA ${safeSchema} TO ${roleName};`);
59
+ // Grant EXECUTE on the 5 worker procedures
60
+ const procedures = [
61
+ `${safeSchema}.worker_dequeue(TEXT, INT, TEXT, INT)`,
62
+ `${safeSchema}.worker_ack(TEXT, BIGINT[])`,
63
+ `${safeSchema}.worker_dead_letter(TEXT, BIGINT[])`,
64
+ `${safeSchema}.worker_respond(TEXT, TEXT, INT, NUMERIC, INT, TIMESTAMPTZ, INT)`,
65
+ `${safeSchema}.worker_listen(TEXT)`,
66
+ `${safeSchema}.worker_unlisten(TEXT)`,
67
+ ];
68
+ for (const proc of procedures) {
69
+ await adminClient.query(`GRANT EXECUTE ON FUNCTION ${proc} TO ${roleName};`);
70
+ }
71
+ // Set the allowed streams as a session default for this role
72
+ await adminClient.query(`ALTER ROLE ${roleName} SET app.allowed_streams = '${allowedStreams}';`);
73
+ // Explicitly revoke direct table access (defense in depth)
74
+ await adminClient.query(`
75
+ REVOKE ALL ON ALL TABLES IN SCHEMA ${safeSchema} FROM ${roleName};
76
+ `);
77
+ // Record in worker_credentials table
78
+ await adminClient.query(`INSERT INTO ${safeSchema}.worker_credentials (role_name, stream_names)
79
+ VALUES ($1, $2)
80
+ ON CONFLICT (role_name) DO UPDATE
81
+ SET stream_names = $2,
82
+ revoked_at = NULL,
83
+ last_rotated_at = NOW()`, [roleName, streamNames]);
84
+ return { roleName, password: rolePassword };
85
+ }
86
+ exports.provisionWorkerRole = provisionWorkerRole;
87
+ /**
88
+ * Rotate the password for an existing worker role.
89
+ */
90
+ async function rotateWorkerPassword(adminClient, schema, roleName, newPassword) {
91
+ const safeSchema = schema.replace(/[^a-zA-Z0-9_]/g, '_');
92
+ const password = newPassword || (0, crypto_1.randomUUID)();
93
+ await adminClient.query(`ALTER ROLE ${roleName} PASSWORD '${password}';`);
94
+ await adminClient.query(`UPDATE ${safeSchema}.worker_credentials
95
+ SET last_rotated_at = NOW()
96
+ WHERE role_name = $1`, [roleName]);
97
+ return { password };
98
+ }
99
+ exports.rotateWorkerPassword = rotateWorkerPassword;
100
+ /**
101
+ * Revoke a worker role by disabling login.
102
+ * The role is not dropped — this preserves the audit trail.
103
+ */
104
+ async function revokeWorkerRole(adminClient, schema, roleName) {
105
+ const safeSchema = schema.replace(/[^a-zA-Z0-9_]/g, '_');
106
+ await adminClient.query(`ALTER ROLE ${roleName} NOLOGIN;`);
107
+ await adminClient.query(`UPDATE ${safeSchema}.worker_credentials
108
+ SET revoked_at = NOW()
109
+ WHERE role_name = $1`, [roleName]);
110
+ }
111
+ exports.revokeWorkerRole = revokeWorkerRole;
112
+ /**
113
+ * List all worker roles for a schema.
114
+ */
115
+ async function listWorkerRoles(adminClient, schema) {
116
+ const safeSchema = schema.replace(/[^a-zA-Z0-9_]/g, '_');
117
+ const result = await adminClient.query(`SELECT id, role_name, stream_names, created_at, revoked_at, last_rotated_at
118
+ FROM ${safeSchema}.worker_credentials
119
+ ORDER BY created_at`);
120
+ return result.rows.map((row) => ({
121
+ id: row.id,
122
+ roleName: row.role_name,
123
+ streamNames: row.stream_names,
124
+ createdAt: row.created_at,
125
+ revokedAt: row.revoked_at,
126
+ lastRotatedAt: row.last_rotated_at,
127
+ }));
128
+ }
129
+ exports.listWorkerRoles = listWorkerRoles;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getNotificationChannelName = exports.deploySchema = void 0;
4
4
  const enums_1 = require("../../../../modules/enums");
5
5
  const utils_1 = require("../../../../modules/utils");
6
+ const procedures_1 = require("./procedures");
6
7
  async function deploySchema(streamClient, appId, logger) {
7
8
  const isPool = streamClient?.totalCount !== undefined &&
8
9
  streamClient?.idleCount !== undefined;
@@ -279,6 +280,23 @@ async function createTables(client, schemaName) {
279
280
  ON ${workerTable} (jid, msg_type, created_at)
280
281
  WHERE jid != '';
281
282
  `);
283
+ // ---- ROLES table (used by scout for distributed leader election) ----
284
+ const rolesTable = `${schemaName}.roles`;
285
+ await client.query(`
286
+ CREATE TABLE IF NOT EXISTS ${rolesTable} (
287
+ key TEXT PRIMARY KEY,
288
+ value TEXT,
289
+ expiry TIMESTAMP WITH TIME ZONE
290
+ );
291
+ `);
292
+ // ---- WORKER ACCESS CONTROL ----
293
+ // SECURITY DEFINER stored procedures for scoped worker access
294
+ const procedureStatements = (0, procedures_1.getCreateProceduresSQL)(schemaName);
295
+ for (const sql of procedureStatements) {
296
+ await client.query(sql);
297
+ }
298
+ // Worker credentials registry
299
+ await client.query((0, procedures_1.getCreateWorkerCredentialsTableSQL)(schemaName));
282
300
  }
283
301
  async function createNotificationTriggers(client, schemaName) {
284
302
  const engineTable = `${schemaName}.engine_streams`;
@@ -57,13 +57,13 @@ function buildPublishSQL(tableName, streamName, isEngine, messages, options) {
57
57
  const msgType = data.type || '';
58
58
  const topic = data.metadata?.topic || '';
59
59
  // Determine if this message has explicit retry config
60
- const hasExplicitConfig = (retryConfig && 'max_retry_attempts' in retryConfig) || options?.retryPolicy;
60
+ const hasExplicitConfig = (retryConfig && 'max_retry_attempts' in retryConfig) || options?.retry;
61
61
  let normalizedPolicy = null;
62
62
  if (retryConfig && 'max_retry_attempts' in retryConfig) {
63
63
  normalizedPolicy = retryConfig;
64
64
  }
65
- else if (options?.retryPolicy) {
66
- normalizedPolicy = (0, utils_1.normalizeRetryPolicy)(options.retryPolicy, {
65
+ else if (options?.retry) {
66
+ normalizedPolicy = (0, utils_1.normalizeRetryPolicy)(options.retry, {
67
67
  maximumAttempts: 3,
68
68
  backoffCoefficient: 10,
69
69
  maximumInterval: 120,
@@ -72,7 +72,7 @@ function buildPublishSQL(tableName, streamName, isEngine, messages, options) {
72
72
  return {
73
73
  message: JSON.stringify(data),
74
74
  hasExplicitConfig,
75
- retryPolicy: normalizedPolicy,
75
+ retry: normalizedPolicy,
76
76
  visibilityDelayMs: visibilityDelayMs || 0,
77
77
  retryAttempt: retryAttempt || 0,
78
78
  workflowName,
@@ -123,7 +123,7 @@ function buildPublishSQL(tableName, streamName, isEngine, messages, options) {
123
123
  if (pm.hasExplicitConfig) {
124
124
  const paramOffset = params.length + 1;
125
125
  valuesClauses.push(`($1, $${paramOffset}, $${paramOffset + 1}, $${paramOffset + 2}, $${paramOffset + 3}, ${visibleAtClause}, $${paramOffset + 4})`);
126
- params.push(pm.message, pm.retryPolicy.max_retry_attempts, pm.retryPolicy.backoff_coefficient, pm.retryPolicy.maximum_interval_seconds, pm.retryAttempt);
126
+ params.push(pm.message, pm.retry.max_retry_attempts, pm.retry.backoff_coefficient, pm.retry.maximum_interval_seconds, pm.retryAttempt);
127
127
  }
128
128
  else {
129
129
  const paramOffset = params.length + 1;
@@ -167,7 +167,7 @@ function buildPublishSQL(tableName, streamName, isEngine, messages, options) {
167
167
  if (pm.hasExplicitConfig) {
168
168
  const paramOffset = params.length + 1;
169
169
  valuesClauses.push(`($1, $${paramOffset}, $${paramOffset + 1}, $${paramOffset + 2}, $${paramOffset + 3}, $${paramOffset + 4}, $${paramOffset + 5}, $${paramOffset + 6}, $${paramOffset + 7}, $${paramOffset + 8}, $${paramOffset + 9}, ${visibleAtClause}, $${paramOffset + 10})`);
170
- params.push(pm.workflowName, pm.jid, pm.aid, pm.dad, pm.msgType, pm.topic, pm.message, pm.retryPolicy.max_retry_attempts, pm.retryPolicy.backoff_coefficient, pm.retryPolicy.maximum_interval_seconds, pm.retryAttempt);
170
+ params.push(pm.workflowName, pm.jid, pm.aid, pm.dad, pm.msgType, pm.topic, pm.message, pm.retry.max_retry_attempts, pm.retry.backoff_coefficient, pm.retry.maximum_interval_seconds, pm.retryAttempt);
171
171
  }
172
172
  else {
173
173
  const paramOffset = params.length + 1;
@@ -243,7 +243,7 @@ async function fetchMessages(client, tableName, streamName, isEngine, consumerNa
243
243
  return {
244
244
  id: row.id.toString(),
245
245
  data,
246
- retryPolicy: (row.max_retry_attempts !== null && !hasDefaultRetryPolicy) ? {
246
+ retry: (row.max_retry_attempts !== null && !hasDefaultRetryPolicy) ? {
247
247
  maximumAttempts: row.max_retry_attempts,
248
248
  backoffCoefficient: parseFloat(row.backoff_coefficient),
249
249
  maximumInterval: row.maximum_interval_seconds,
@@ -192,7 +192,14 @@ class NotificationManager {
192
192
  // Set up LISTEN for this channel (only once per channel)
193
193
  try {
194
194
  const listenStart = Date.now();
195
- await this.client.query(`LISTEN "${channelName}"`);
195
+ // In secured mode, use stored procedure for validated LISTEN
196
+ if (serviceAny.securedMode && serviceAny.safeName) {
197
+ const schema = serviceAny.safeName(serviceAny.appId);
198
+ await this.client.query(`SELECT ${schema}.worker_listen($1)`, [resolvedStreamName]);
199
+ }
200
+ else {
201
+ await this.client.query(`LISTEN "${channelName}"`);
202
+ }
196
203
  this.logger.debug('postgres-stream-listen-start', {
197
204
  streamName,
198
205
  groupName,
@@ -257,7 +264,14 @@ class NotificationManager {
257
264
  clientNotificationConsumers.delete(consumerKey);
258
265
  const channelName = (0, kvtables_1.getNotificationChannelName)(resolvedStreamName, isEngine);
259
266
  try {
260
- await this.client.query(`UNLISTEN "${channelName}"`);
267
+ // In secured mode, use stored procedure for validated UNLISTEN
268
+ if (serviceAny.securedMode && serviceAny.safeName) {
269
+ const schema = serviceAny.safeName(serviceAny.appId);
270
+ await this.client.query(`SELECT ${schema}.worker_unlisten($1)`, [resolvedStreamName]);
271
+ }
272
+ else {
273
+ await this.client.query(`UNLISTEN "${channelName}"`);
274
+ }
261
275
  this.logger.debug('postgres-stream-unlisten', {
262
276
  streamName,
263
277
  groupName,
@@ -24,6 +24,12 @@ declare class PostgresStreamService extends StreamService<PostgresClientType & P
24
24
  namespace: string;
25
25
  appId: string;
26
26
  logger: ILogger;
27
+ /**
28
+ * When true, all worker stream operations use SECURITY DEFINER
29
+ * stored procedures instead of raw SQL. Enabled when the worker
30
+ * connects with scoped `workerCredentials`.
31
+ */
32
+ securedMode: boolean;
27
33
  private scoutManager;
28
34
  private notificationManager;
29
35
  constructor(streamClient: PostgresClientType & ProviderClient, storeClient: ProviderClient, config?: StreamConfig);
@@ -104,6 +110,7 @@ declare class PostgresStreamService extends StreamService<PostgresClientType & P
104
110
  supportsTrimming: boolean;
105
111
  supportsRetry: boolean;
106
112
  supportsNotifications: boolean;
113
+ supportsParallelProcessing: boolean;
107
114
  maxMessageSize: number;
108
115
  maxBatchSize: number;
109
116
  };
@@ -29,6 +29,7 @@ const index_1 = require("../../index");
29
29
  const kvtables_1 = require("./kvtables");
30
30
  const Stats = __importStar(require("./stats"));
31
31
  const Messages = __importStar(require("./messages"));
32
+ const Secured = __importStar(require("./secured"));
32
33
  const scout_1 = require("./scout");
33
34
  const notifications_1 = require("./notifications");
34
35
  const Lifecycle = __importStar(require("./lifecycle"));
@@ -42,21 +43,35 @@ const Lifecycle = __importStar(require("./lifecycle"));
42
43
  class PostgresStreamService extends index_1.StreamService {
43
44
  constructor(streamClient, storeClient, config = {}) {
44
45
  super(streamClient, storeClient, config);
46
+ /**
47
+ * When true, all worker stream operations use SECURITY DEFINER
48
+ * stored procedures instead of raw SQL. Enabled when the worker
49
+ * connects with scoped `workerCredentials`.
50
+ */
51
+ this.securedMode = false;
52
+ if (config?.securedWorker) {
53
+ this.securedMode = true;
54
+ }
45
55
  }
46
56
  async init(namespace, appId, logger) {
47
57
  this.namespace = namespace;
48
58
  this.appId = appId;
49
59
  this.logger = logger;
50
- await (0, kvtables_1.deploySchema)(this.streamClient, this.appId, this.logger);
51
- // Initialize scout manager
52
- this.scoutManager = new scout_1.ScoutManager(this.streamClient, this.appId, this.getEngineTableName.bind(this), this.mintKey.bind(this), this.logger);
60
+ // Secured workers skip schema deployment and scout initialization —
61
+ // they use SECURITY DEFINER stored procedures for all stream ops
62
+ // and never need direct table access.
63
+ if (!this.securedMode) {
64
+ await (0, kvtables_1.deploySchema)(this.streamClient, this.appId, this.logger);
65
+ }
66
+ // Initialize scout manager (skipped in secured mode — roles table inaccessible)
67
+ this.scoutManager = this.securedMode ? null : new scout_1.ScoutManager(this.streamClient, this.appId, this.getEngineTableName.bind(this), this.mintKey.bind(this), this.logger);
53
68
  // Initialize notification manager
54
69
  this.notificationManager = new notifications_1.NotificationManager(this.streamClient, this.getEngineTableName.bind(this), () => (0, notifications_1.getFallbackInterval)(this.config), this.logger);
55
70
  // Set up notification handler if supported
56
71
  if (this.streamClient.on && this.isNotificationsEnabled()) {
57
72
  this.notificationManager.setupClientNotificationHandler(this);
58
73
  this.notificationManager.startClientFallbackPoller(this.checkForMissedMessages.bind(this));
59
- this.scoutManager.startRouterScoutPoller();
74
+ this.scoutManager?.startRouterScoutPoller();
60
75
  }
61
76
  }
62
77
  isNotificationsEnabled() {
@@ -152,6 +167,10 @@ class PostgresStreamService extends index_1.StreamService {
152
167
  * added to the transaction for atomic execution.
153
168
  */
154
169
  async publishMessages(streamName, messages, options) {
170
+ if (this.securedMode) {
171
+ const target = this.resolveStreamTarget(streamName);
172
+ return Secured.publishMessagesSecured(this.streamClient, this.safeName(this.appId), target.streamName, messages, this.logger);
173
+ }
155
174
  const target = this.resolveStreamTarget(streamName);
156
175
  return Messages.publishMessages(this.streamClient, target.tableName, target.streamName, target.isEngine, messages, options, this.logger);
157
176
  }
@@ -213,14 +232,26 @@ class PostgresStreamService extends index_1.StreamService {
213
232
  await this.notificationManager.stopNotificationConsumer(this, streamName, groupName);
214
233
  }
215
234
  async fetchMessages(streamName, groupName, consumerName, options) {
235
+ if (this.securedMode) {
236
+ const target = this.resolveStreamTarget(streamName);
237
+ return Secured.fetchMessagesSecured(this.streamClient, this.safeName(this.appId), target.streamName, consumerName, options || {}, this.logger);
238
+ }
216
239
  const target = this.resolveStreamTarget(streamName);
217
240
  return Messages.fetchMessages(this.streamClient, target.tableName, target.streamName, target.isEngine, consumerName, options || {}, this.logger);
218
241
  }
219
242
  async ackAndDelete(streamName, groupName, messageIds) {
243
+ if (this.securedMode) {
244
+ const target = this.resolveStreamTarget(streamName);
245
+ return Secured.ackAndDeleteSecured(this.streamClient, this.safeName(this.appId), target.streamName, messageIds, this.logger);
246
+ }
220
247
  const target = this.resolveStreamTarget(streamName);
221
248
  return Messages.ackAndDelete(this.streamClient, target.tableName, target.streamName, messageIds, this.logger);
222
249
  }
223
250
  async deadLetterMessages(streamName, groupName, messageIds) {
251
+ if (this.securedMode) {
252
+ const target = this.resolveStreamTarget(streamName);
253
+ return Secured.deadLetterMessagesSecured(this.streamClient, this.safeName(this.appId), target.streamName, messageIds, this.logger);
254
+ }
224
255
  const target = this.resolveStreamTarget(streamName);
225
256
  return Messages.deadLetterMessages(this.streamClient, target.tableName, target.streamName, messageIds, this.logger);
226
257
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * SECURITY DEFINER stored procedures for worker router access control.
3
+ *
4
+ * Each procedure runs as the schema owner (admin) and validates the
5
+ * caller's `app.allowed_streams` session variable before executing.
6
+ * Worker roles are granted EXECUTE on these procedures only — they
7
+ * have zero direct table access.
8
+ *
9
+ * ## Procedures
10
+ *
11
+ * | Procedure | Purpose |
12
+ * |-----------|---------|
13
+ * | `worker_dequeue` | Fetch and reserve messages from `worker_streams` |
14
+ * | `worker_ack` | Soft-delete (ack) messages in `worker_streams` |
15
+ * | `worker_dead_letter` | Dead-letter messages in `worker_streams` |
16
+ * | `worker_respond` | Publish a response into `engine_streams` |
17
+ * | `worker_listen` | Subscribe to NOTIFY channel for a stream |
18
+ * | `worker_unlisten` | Unsubscribe from NOTIFY channel |
19
+ */
20
+ export declare function getCreateProceduresSQL(schemaName: string): string[];
21
+ export declare function getCreateWorkerCredentialsTableSQL(schemaName: string): string;
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ /**
3
+ * SECURITY DEFINER stored procedures for worker router access control.
4
+ *
5
+ * Each procedure runs as the schema owner (admin) and validates the
6
+ * caller's `app.allowed_streams` session variable before executing.
7
+ * Worker roles are granted EXECUTE on these procedures only — they
8
+ * have zero direct table access.
9
+ *
10
+ * ## Procedures
11
+ *
12
+ * | Procedure | Purpose |
13
+ * |-----------|---------|
14
+ * | `worker_dequeue` | Fetch and reserve messages from `worker_streams` |
15
+ * | `worker_ack` | Soft-delete (ack) messages in `worker_streams` |
16
+ * | `worker_dead_letter` | Dead-letter messages in `worker_streams` |
17
+ * | `worker_respond` | Publish a response into `engine_streams` |
18
+ * | `worker_listen` | Subscribe to NOTIFY channel for a stream |
19
+ * | `worker_unlisten` | Unsubscribe from NOTIFY channel |
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.getCreateWorkerCredentialsTableSQL = exports.getCreateProceduresSQL = void 0;
23
+ const STREAM_ACCESS_CHECK = `
24
+ DECLARE
25
+ allowed_streams TEXT[];
26
+ BEGIN
27
+ allowed_streams := string_to_array(current_setting('app.allowed_streams', true), ',');
28
+ IF allowed_streams IS NULL OR NOT (p_stream_name = ANY(allowed_streams)) THEN
29
+ RAISE EXCEPTION 'access denied: stream_name "%" not in allowed streams', p_stream_name
30
+ USING ERRCODE = '42501';
31
+ END IF;
32
+ `;
33
+ function getCreateProceduresSQL(schemaName) {
34
+ const engineTable = `${schemaName}.engine_streams`;
35
+ const workerTable = `${schemaName}.worker_streams`;
36
+ return [
37
+ // -- worker_dequeue --
38
+ `CREATE OR REPLACE FUNCTION ${schemaName}.worker_dequeue(
39
+ p_stream_name TEXT,
40
+ p_batch_size INT,
41
+ p_consumer_id TEXT,
42
+ p_reservation_timeout_sec INT DEFAULT 30
43
+ )
44
+ RETURNS TABLE (
45
+ id BIGINT,
46
+ message TEXT,
47
+ workflow_name TEXT,
48
+ max_retry_attempts INT,
49
+ backoff_coefficient NUMERIC,
50
+ maximum_interval_seconds INT,
51
+ retry_attempt INT
52
+ )
53
+ LANGUAGE plpgsql SECURITY DEFINER
54
+ SET search_path = ${schemaName}, pg_temp
55
+ AS $$
56
+ ${STREAM_ACCESS_CHECK}
57
+ RETURN QUERY
58
+ UPDATE ${workerTable} ws
59
+ SET reserved_at = NOW(), reserved_by = p_consumer_id
60
+ WHERE ws.id IN (
61
+ SELECT ws2.id FROM ${workerTable} ws2
62
+ WHERE ws2.stream_name = p_stream_name
63
+ AND (ws2.reserved_at IS NULL OR ws2.reserved_at < NOW() - (p_reservation_timeout_sec || ' seconds')::INTERVAL)
64
+ AND ws2.expired_at IS NULL
65
+ AND ws2.visible_at <= NOW()
66
+ ORDER BY ws2.id
67
+ LIMIT p_batch_size
68
+ FOR UPDATE SKIP LOCKED
69
+ )
70
+ RETURNING ws.id, ws.message, ws.workflow_name, ws.max_retry_attempts,
71
+ ws.backoff_coefficient, ws.maximum_interval_seconds, ws.retry_attempt;
72
+ END;
73
+ $$;`,
74
+ // -- worker_ack --
75
+ `CREATE OR REPLACE FUNCTION ${schemaName}.worker_ack(
76
+ p_stream_name TEXT,
77
+ p_message_ids BIGINT[]
78
+ )
79
+ RETURNS INT
80
+ LANGUAGE plpgsql SECURITY DEFINER
81
+ SET search_path = ${schemaName}, pg_temp
82
+ AS $$
83
+ DECLARE
84
+ affected INT;
85
+ ${STREAM_ACCESS_CHECK}
86
+ UPDATE ${workerTable}
87
+ SET expired_at = NOW()
88
+ WHERE stream_name = p_stream_name AND id = ANY(p_message_ids);
89
+ GET DIAGNOSTICS affected = ROW_COUNT;
90
+ RETURN affected;
91
+ END;
92
+ $$;`,
93
+ // -- worker_dead_letter --
94
+ `CREATE OR REPLACE FUNCTION ${schemaName}.worker_dead_letter(
95
+ p_stream_name TEXT,
96
+ p_message_ids BIGINT[]
97
+ )
98
+ RETURNS INT
99
+ LANGUAGE plpgsql SECURITY DEFINER
100
+ SET search_path = ${schemaName}, pg_temp
101
+ AS $$
102
+ DECLARE
103
+ affected INT;
104
+ ${STREAM_ACCESS_CHECK}
105
+ UPDATE ${workerTable}
106
+ SET dead_lettered_at = NOW(), expired_at = NOW()
107
+ WHERE stream_name = p_stream_name AND id = ANY(p_message_ids);
108
+ GET DIAGNOSTICS affected = ROW_COUNT;
109
+ RETURN affected;
110
+ END;
111
+ $$;`,
112
+ // -- worker_respond --
113
+ // Inserts into engine_streams. The engine stream_name is the appId
114
+ // (hardcoded from the schema name, not caller-controlled).
115
+ // NOTE: Does NOT use STREAM_ACCESS_CHECK because p_stream_name is
116
+ // not the target — the engine stream is always hardcoded. Instead,
117
+ // we verify the caller has at least one allowed stream (i.e., is a
118
+ // legitimate provisioned worker).
119
+ `CREATE OR REPLACE FUNCTION ${schemaName}.worker_respond(
120
+ p_stream_name TEXT,
121
+ p_message TEXT,
122
+ p_max_retry_attempts INT DEFAULT NULL,
123
+ p_backoff_coefficient NUMERIC DEFAULT NULL,
124
+ p_maximum_interval_seconds INT DEFAULT NULL,
125
+ p_visible_at TIMESTAMPTZ DEFAULT NOW(),
126
+ p_retry_attempt INT DEFAULT 0
127
+ )
128
+ RETURNS BIGINT
129
+ LANGUAGE plpgsql SECURITY DEFINER
130
+ SET search_path = ${schemaName}, pg_temp
131
+ AS $$
132
+ DECLARE
133
+ new_id BIGINT;
134
+ engine_stream_name TEXT;
135
+ allowed_streams TEXT[];
136
+ BEGIN
137
+ allowed_streams := string_to_array(current_setting('app.allowed_streams', true), ',');
138
+ IF allowed_streams IS NULL OR array_length(allowed_streams, 1) IS NULL THEN
139
+ RAISE EXCEPTION 'access denied: caller has no allowed streams'
140
+ USING ERRCODE = '42501';
141
+ END IF;
142
+ -- Engine stream_name is the schema/appId name
143
+ engine_stream_name := '${schemaName}';
144
+
145
+ IF p_max_retry_attempts IS NOT NULL THEN
146
+ INSERT INTO ${engineTable}
147
+ (stream_name, message, max_retry_attempts, backoff_coefficient, maximum_interval_seconds, visible_at, retry_attempt)
148
+ VALUES
149
+ (engine_stream_name, p_message, p_max_retry_attempts, p_backoff_coefficient, p_maximum_interval_seconds, p_visible_at, p_retry_attempt)
150
+ RETURNING id INTO new_id;
151
+ ELSE
152
+ INSERT INTO ${engineTable}
153
+ (stream_name, message, visible_at, retry_attempt)
154
+ VALUES
155
+ (engine_stream_name, p_message, p_visible_at, p_retry_attempt)
156
+ RETURNING id INTO new_id;
157
+ END IF;
158
+
159
+ RETURN new_id;
160
+ END;
161
+ $$;`,
162
+ // -- worker_listen --
163
+ `CREATE OR REPLACE FUNCTION ${schemaName}.worker_listen(
164
+ p_stream_name TEXT
165
+ )
166
+ RETURNS VOID
167
+ LANGUAGE plpgsql SECURITY DEFINER
168
+ SET search_path = ${schemaName}, pg_temp
169
+ AS $$
170
+ DECLARE
171
+ channel_name TEXT;
172
+ ${STREAM_ACCESS_CHECK}
173
+ channel_name := 'wrk_' || p_stream_name;
174
+ IF length(channel_name) > 63 THEN
175
+ channel_name := left(channel_name, 63);
176
+ END IF;
177
+ EXECUTE format('LISTEN %I', channel_name);
178
+ END;
179
+ $$;`,
180
+ // -- worker_unlisten --
181
+ `CREATE OR REPLACE FUNCTION ${schemaName}.worker_unlisten(
182
+ p_stream_name TEXT
183
+ )
184
+ RETURNS VOID
185
+ LANGUAGE plpgsql SECURITY DEFINER
186
+ SET search_path = ${schemaName}, pg_temp
187
+ AS $$
188
+ DECLARE
189
+ channel_name TEXT;
190
+ ${STREAM_ACCESS_CHECK}
191
+ channel_name := 'wrk_' || p_stream_name;
192
+ IF length(channel_name) > 63 THEN
193
+ channel_name := left(channel_name, 63);
194
+ END IF;
195
+ EXECUTE format('UNLISTEN %I', channel_name);
196
+ END;
197
+ $$;`,
198
+ ];
199
+ }
200
+ exports.getCreateProceduresSQL = getCreateProceduresSQL;
201
+ function getCreateWorkerCredentialsTableSQL(schemaName) {
202
+ return `
203
+ CREATE TABLE IF NOT EXISTS ${schemaName}.worker_credentials (
204
+ id SERIAL PRIMARY KEY,
205
+ role_name TEXT NOT NULL UNIQUE,
206
+ stream_names TEXT[] NOT NULL,
207
+ created_at TIMESTAMPTZ DEFAULT NOW(),
208
+ revoked_at TIMESTAMPTZ,
209
+ last_rotated_at TIMESTAMPTZ DEFAULT NOW()
210
+ );
211
+ `;
212
+ }
213
+ exports.getCreateWorkerCredentialsTableSQL = getCreateWorkerCredentialsTableSQL;