@friggframework/core 2.0.0--canary.686.3e27a92.0 → 2.0.0--canary.663.8de669c.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 (184) hide show
  1. package/CLAUDE.md +0 -28
  2. package/README.md +0 -105
  3. package/application/commands/integration-commands.js +10 -20
  4. package/application/commands/scheduler-commands.js +7 -11
  5. package/application/commands/user-commands.js +5 -17
  6. package/application/index.js +2 -6
  7. package/core/Worker.js +7 -31
  8. package/core/create-handler.js +0 -9
  9. package/credential/repositories/credential-repository-documentdb.js +0 -34
  10. package/credential/repositories/credential-repository-interface.js +0 -15
  11. package/credential/repositories/credential-repository-mongo.js +0 -23
  12. package/credential/repositories/credential-repository-postgres.js +0 -28
  13. package/database/config.js +24 -48
  14. package/database/documentdb-utils.js +1 -6
  15. package/database/encryption/encryption-config.js +31 -10
  16. package/database/encryption/encryption-schema-registry.js +12 -93
  17. package/database/prisma.js +2 -4
  18. package/generated/prisma-mongodb/edge.js +4 -43
  19. package/generated/prisma-mongodb/index-browser.js +1 -40
  20. package/generated/prisma-mongodb/index.d.ts +111 -4739
  21. package/generated/prisma-mongodb/index.js +4 -43
  22. package/generated/prisma-mongodb/package.json +1 -1
  23. package/generated/prisma-mongodb/schema.prisma +0 -92
  24. package/generated/prisma-mongodb/wasm.js +4 -43
  25. package/generated/prisma-postgresql/edge.js +4 -43
  26. package/generated/prisma-postgresql/index-browser.js +1 -40
  27. package/generated/prisma-postgresql/index.d.ts +33 -4966
  28. package/generated/prisma-postgresql/index.js +4 -43
  29. package/generated/prisma-postgresql/package.json +1 -1
  30. package/generated/prisma-postgresql/schema.prisma +0 -87
  31. package/generated/prisma-postgresql/wasm.js +4 -43
  32. package/handlers/app-definition-loader.js +19 -121
  33. package/handlers/app-handler-helpers.js +3 -25
  34. package/handlers/backend-utils.js +25 -31
  35. package/handlers/middleware/admin-auth.js +1 -14
  36. package/handlers/routers/auth.js +10 -46
  37. package/handlers/routers/health.js +4 -13
  38. package/handlers/routers/integration-defined-routers.js +92 -144
  39. package/handlers/routers/integration-webhook-routers.js +64 -102
  40. package/handlers/workers/integration-defined-workers.js +2 -5
  41. package/index.js +1 -65
  42. package/infrastructure/scheduler/index.js +6 -19
  43. package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
  44. package/integrations/index.js +0 -1
  45. package/integrations/integration-router.js +96 -131
  46. package/integrations/repositories/integration-repository-documentdb.js +3 -35
  47. package/integrations/repositories/integration-repository-interface.js +1 -17
  48. package/integrations/repositories/integration-repository-mongo.js +76 -59
  49. package/integrations/repositories/integration-repository-postgres.js +76 -62
  50. package/integrations/tests/doubles/test-integration-repository.js +1 -12
  51. package/integrations/use-cases/create-integration.js +20 -55
  52. package/integrations/use-cases/get-integrations-for-user.js +40 -80
  53. package/integrations/use-cases/index.js +1 -7
  54. package/integrations/utils/map-integration-dto.js +14 -43
  55. package/modules/index.js +0 -14
  56. package/modules/module-factory.js +0 -12
  57. package/modules/module.js +2 -53
  58. package/modules/repositories/module-repository-documentdb.js +6 -51
  59. package/modules/repositories/module-repository-interface.js +2 -19
  60. package/modules/repositories/module-repository-mongo.js +74 -67
  61. package/modules/repositories/module-repository-postgres.js +74 -66
  62. package/modules/repositories/module-repository.js +1 -2
  63. package/modules/requester/requester.js +2 -27
  64. package/modules/use-cases/get-entities-for-user.js +13 -26
  65. package/modules/use-cases/get-entity-options-by-id.js +4 -10
  66. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  67. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  68. package/modules/use-cases/get-module.js +4 -10
  69. package/modules/use-cases/process-authorization-callback.js +19 -53
  70. package/modules/use-cases/refresh-entity-options.js +4 -10
  71. package/modules/use-cases/test-module-auth.js +4 -10
  72. package/package.json +5 -6
  73. package/prisma-mongodb/schema.prisma +0 -92
  74. package/prisma-postgresql/schema.prisma +0 -87
  75. package/queues/index.js +0 -25
  76. package/types/index.d.ts +0 -2
  77. package/user/repositories/user-repository-documentdb.js +2 -27
  78. package/user/repositories/user-repository-interface.js +0 -18
  79. package/user/repositories/user-repository-mongo.js +0 -24
  80. package/user/repositories/user-repository-postgres.js +0 -18
  81. package/user/use-cases/authenticate-user.js +1 -7
  82. package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
  83. package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
  84. package/user/user.js +1 -64
  85. package/credential/use-cases/credential-operations-for-user.js +0 -202
  86. package/credential/use-cases/reauthorize-credential-for-user.js +0 -73
  87. package/database/organization-where.js +0 -18
  88. package/database/unique-violation.js +0 -25
  89. package/extensions/bootstrap-runner.js +0 -43
  90. package/extensions/extension-loader.js +0 -187
  91. package/extensions/index.js +0 -41
  92. package/extensions/initialize-app.js +0 -123
  93. package/extensions/route-mounter.js +0 -103
  94. package/extensions/schema-composer.js +0 -188
  95. package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -216
  96. package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
  97. package/integrations/repositories/integration-not-found.js +0 -14
  98. package/integrations/schedule-config.js +0 -80
  99. package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
  100. package/integrations/use-cases/integration-operations-for-user.js +0 -256
  101. package/integrations/use-cases/resolve-integration-ownership.js +0 -177
  102. package/management-api/create-management-api-router.js +0 -49
  103. package/management-api/error-response.js +0 -116
  104. package/management-api/management-api-config.js +0 -151
  105. package/management-api/meta-router.js +0 -76
  106. package/management-api/openapi/build-openapi.js +0 -144
  107. package/management-api/openapi/components.js +0 -408
  108. package/management-api/openapi/v1-operations.js +0 -83
  109. package/management-api/openapi/v2-operations.js +0 -264
  110. package/management-api/route-registry.js +0 -411
  111. package/management-api/test-support/http-client.js +0 -39
  112. package/management-api/use-cases/get-management-api-meta.js +0 -57
  113. package/management-api/v1-deprecation.js +0 -120
  114. package/management-api/v2/create-v2-router.js +0 -85
  115. package/management-api/v2/dependencies.js +0 -281
  116. package/management-api/v2/dto.js +0 -143
  117. package/management-api/v2/handlers/authorization.js +0 -94
  118. package/management-api/v2/handlers/credentials.js +0 -33
  119. package/management-api/v2/handlers/entities.js +0 -77
  120. package/management-api/v2/handlers/integrations.js +0 -128
  121. package/management-api/v2/handlers/proxy.js +0 -34
  122. package/management-api/v2/request.js +0 -85
  123. package/management-api/v2/test-support/fake-dependencies.js +0 -103
  124. package/modules/domain/authorization-session.js +0 -144
  125. package/modules/entity-scope.js +0 -15
  126. package/modules/entity-type-registry.js +0 -311
  127. package/modules/proxy/proxy-errors.js +0 -20
  128. package/modules/proxy/proxy-path.js +0 -179
  129. package/modules/proxy/proxy-policy.js +0 -248
  130. package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
  131. package/modules/repositories/authorization-session-repository-factory.js +0 -31
  132. package/modules/repositories/authorization-session-repository-interface.js +0 -47
  133. package/modules/repositories/authorization-session-repository-prisma.js +0 -87
  134. package/modules/repositories/entity-not-found.js +0 -18
  135. package/modules/requester/proxy-request.js +0 -244
  136. package/modules/scope-config.js +0 -192
  137. package/modules/scope-policy.js +0 -112
  138. package/modules/scope-resolution.js +0 -288
  139. package/modules/scope-visibility.js +0 -247
  140. package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
  141. package/modules/tests/doubles/ownership-matching.js +0 -18
  142. package/modules/use-cases/authorize-entity-for-user.js +0 -61
  143. package/modules/use-cases/create-entity-from-credential.js +0 -113
  144. package/modules/use-cases/entity-operations-for-user.js +0 -162
  145. package/modules/use-cases/entity-types.js +0 -139
  146. package/modules/use-cases/execute-entity-proxy-request.js +0 -286
  147. package/modules/use-cases/get-authorization-requirements.js +0 -100
  148. package/modules/use-cases/get-entity-module-for-user.js +0 -113
  149. package/modules/use-cases/manage-access.js +0 -21
  150. package/modules/use-cases/multi-step-authorization.js +0 -329
  151. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
  152. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
  153. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
  154. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
  155. package/providers/index.js +0 -23
  156. package/providers/resolve-provider.js +0 -93
  157. package/providers/runtime-detection.js +0 -124
  158. package/queues/providers/index.js +0 -11
  159. package/queues/providers/netlify-background-provider.js +0 -150
  160. package/queues/providers/qstash-queue-provider.js +0 -219
  161. package/queues/providers/sqs-queue-provider.js +0 -70
  162. package/queues/queue-client-interface.js +0 -55
  163. package/queues/queue-message-auth.js +0 -193
  164. package/queues/queue-provider-factory.js +0 -101
  165. package/queues/queue-provider.js +0 -65
  166. package/queues/sqs-queue-client.js +0 -81
  167. package/types/modules/index.d.ts +0 -197
  168. package/types/user/index.d.ts +0 -134
  169. package/user/adopter-jwt-config.js +0 -177
  170. package/user/adopter-jwt-errors.js +0 -59
  171. package/user/adopter-token-verifier.js +0 -122
  172. package/user/app-user-helpers.js +0 -97
  173. package/user/create-user-authentication.js +0 -64
  174. package/user/membership.js +0 -51
  175. package/user/repositories/organization-membership-record.js +0 -19
  176. package/user/repositories/organization-membership-repository-documentdb.js +0 -106
  177. package/user/repositories/organization-membership-repository-factory.js +0 -42
  178. package/user/repositories/organization-membership-repository-interface.js +0 -57
  179. package/user/repositories/organization-membership-repository-mongo.js +0 -13
  180. package/user/repositories/organization-membership-repository-postgres.js +0 -29
  181. package/user/repositories/organization-membership-repository-prisma.js +0 -81
  182. package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
  183. package/user/use-cases/ensure-organization-membership.js +0 -92
  184. package/user/use-cases/find-individual-users-by-organization.js +0 -54
@@ -1,11 +0,0 @@
1
- const { NetlifyBackgroundProvider } = require('./netlify-background-provider');
2
- const { QStashQueueProvider } = require('./qstash-queue-provider');
3
-
4
- module.exports = {
5
- NetlifyBackgroundProvider,
6
- QStashQueueProvider,
7
- // Lazy: requiring the barrel must not construct anything AWS-specific.
8
- get SqsQueueProvider() {
9
- return require('./sqs-queue-provider').SqsQueueProvider;
10
- },
11
- };
@@ -1,150 +0,0 @@
1
- /**
2
- * Netlify Background Functions Queue Provider (Adapter)
3
- *
4
- * Uses Netlify Background Functions for async job processing.
5
- * Background functions have a 15-minute execution limit and are triggered via HTTP POST.
6
- * They return 202 immediately while processing continues.
7
- *
8
- * Queue ID format: The function name or URL path for the background function.
9
- * Convention: function files named with '-background' suffix (e.g., 'worker-background.js')
10
- *
11
- * Authentication: a background function is a public URL and Netlify does
12
- * not authenticate its caller, so every message is signed with
13
- * FRIGG_QUEUE_SIGNING_SECRET (HMAC-SHA256 over timestamp + raw body, see
14
- * ../queue-message-auth.js) and verifyEvent() checks that signature before
15
- * the worker parses or dispatches anything. Without the secret, send() and
16
- * verifyEvent() refuse, except outside production with
17
- * FRIGG_QUEUE_ALLOW_UNSIGNED=true.
18
- *
19
- * Limitations:
20
- * - No built-in retry (must implement manually)
21
- * - No dead-letter queue
22
- * - No FIFO ordering guarantees
23
- * - 15-minute max execution time
24
- */
25
- const { QueueProvider } = require('../queue-provider');
26
- const {
27
- SIGNING_SECRET_ENV,
28
- ALLOW_UNSIGNED_ENV,
29
- SIGNATURE_HEADER,
30
- TIMESTAMP_HEADER,
31
- allowsUnsignedMessages,
32
- readSigningSecret,
33
- signQueueMessage,
34
- verifyQueueMessage,
35
- readHeader,
36
- readRawBody,
37
- } = require('../queue-message-auth');
38
-
39
- class NetlifyBackgroundProvider extends QueueProvider {
40
- /**
41
- * @param {Object} [options]
42
- * @param {string} [options.baseUrl] - site URL (default process.env.URL)
43
- * @param {string} [options.signingSecret] - default FRIGG_QUEUE_SIGNING_SECRET, read per call
44
- * @param {Object} [options.env] - environment to read (default process.env)
45
- * @param {() => number} [options.now] - clock in milliseconds (tests)
46
- */
47
- constructor(options = {}) {
48
- super();
49
- // Base URL for triggering background functions (e.g., https://mysite.netlify.app)
50
- this.baseUrl = options.baseUrl || process.env.URL || '';
51
- this.signingSecret = options.signingSecret;
52
- this.env = options.env || process.env;
53
- this.now = options.now || Date.now;
54
- // verifyEvent() proves an HTTP delivery came from Frigg.
55
- this.verifiesHttpDelivery = true;
56
- }
57
-
58
- // Read on every call, not in the constructor: secrets may be loaded into
59
- // the environment after the provider is built (loadSecrets).
60
- _secret() {
61
- return this.signingSecret || readSigningSecret(this.env);
62
- }
63
-
64
- /**
65
- * Send a message by invoking a Netlify Background Function via HTTP POST.
66
- *
67
- * @param {Object} message - Message payload
68
- * @param {string} queueId - Background function path (e.g., '/.netlify/functions/worker-background')
69
- */
70
- async send(message, queueId) {
71
- const url = `${this.baseUrl}${queueId}`;
72
- const body = JSON.stringify(message);
73
- const headers = { 'Content-Type': 'application/json' };
74
-
75
- const secret = this._secret();
76
- if (secret) {
77
- Object.assign(headers, signQueueMessage({ body, secret, now: this.now() }));
78
- } else if (!allowsUnsignedMessages(this.env)) {
79
- // The worker would refuse the message, and a background function
80
- // answers 202 either way, so the loss would be silent. Fail here.
81
- throw new Error(
82
- `${SIGNING_SECRET_ENV} is required to send to a Netlify background function ` +
83
- `(${ALLOW_UNSIGNED_ENV}=true allows unsigned messages outside production only)`
84
- );
85
- }
86
-
87
- const response = await fetch(url, {
88
- method: 'POST',
89
- headers,
90
- body,
91
- });
92
-
93
- if (!response.ok && response.status !== 202) {
94
- throw new Error(
95
- `Failed to invoke background function at ${queueId}: ${response.status} ${response.statusText}`
96
- );
97
- }
98
-
99
- return { status: response.status, queueId };
100
- }
101
-
102
- /**
103
- * Send batch of messages sequentially (background functions are HTTP-triggered).
104
- */
105
- async batchSend(batch, queueId) {
106
- const entries = batch ?? [];
107
- const results = [];
108
- for (const entry of entries) {
109
- const result = await this.send(entry, queueId);
110
- results.push(result);
111
- }
112
- return { results };
113
- }
114
-
115
- /**
116
- * Check the Frigg signature on an incoming background-function event.
117
- *
118
- * @param {Object} event - Netlify v1 (Lambda-shaped) event
119
- * @returns {Promise<{ ok: boolean, reason?: string, unsigned?: boolean }>}
120
- */
121
- async verifyEvent(event) {
122
- const secret = this._secret();
123
- if (!secret) {
124
- return allowsUnsignedMessages(this.env)
125
- ? { ok: true, unsigned: true }
126
- : { ok: false, reason: 'signing_secret_not_configured' };
127
- }
128
- return verifyQueueMessage({
129
- body: readRawBody(event),
130
- timestamp: readHeader(event, TIMESTAMP_HEADER),
131
- signature: readHeader(event, SIGNATURE_HEADER),
132
- secret,
133
- now: this.now(),
134
- });
135
- }
136
-
137
- /**
138
- * Parse Netlify Background Function event.
139
- * Background functions receive the raw HTTP event body as the event.
140
- */
141
- parseEvent(event) {
142
- const raw = readRawBody(event);
143
- const body = raw !== null ? JSON.parse(raw) : event.body;
144
-
145
- // Background functions receive a single message per invocation
146
- return [body];
147
- }
148
- }
149
-
150
- module.exports = { NetlifyBackgroundProvider };
@@ -1,219 +0,0 @@
1
- /**
2
- * QStash Queue Provider (Adapter)
3
- *
4
- * Upstash QStash implementation - platform-agnostic HTTP-based message queue.
5
- * Works on any platform (Netlify, Vercel, AWS, bare metal, etc.)
6
- *
7
- * Features:
8
- * - Configurable retry with exponential backoff
9
- * - Dead-letter queue via callbacks
10
- * - At-least-once delivery
11
- * - Scheduling support (delay, cron)
12
- * - Message deduplication
13
- *
14
- * Requires:
15
- * - QSTASH_TOKEN env var (from Upstash console)
16
- * - QSTASH_CURRENT_SIGNING_KEY and QSTASH_NEXT_SIGNING_KEY for verification
17
- *
18
- * Queue ID format: The destination URL that QStash will POST to when delivering the message.
19
- *
20
- * Verification (verifyEvent), per Upstash's documented scheme and its
21
- * @upstash/qstash Receiver: every delivery carries an `Upstash-Signature`
22
- * header, a JWT signed HS256 with the current signing key (the next key
23
- * during rotation). Its claims are iss "Upstash", sub the destination URL,
24
- * exp / nbf / iat, and body = base64url(SHA-256(raw body)). A delivery is
25
- * accepted when the JWT verifies with either key, carries exp, nbf, sub and
26
- * body, is inside its exp / nbf window, names this request's URL as its sub
27
- * (the configured destinationUrl, else the event's rawUrl: a delivery
28
- * published to another endpoint cannot be replayed here), and its body claim
29
- * matches the body received (trailing '=' padding ignored, as the Receiver
30
- * does). A request whose URL cannot be determined is refused. With neither
31
- * key configured it is refused, except outside production with
32
- * FRIGG_QUEUE_ALLOW_UNSIGNED=true.
33
- */
34
- const crypto = require('crypto');
35
- const { QueueProvider } = require('../queue-provider');
36
- const {
37
- allowsUnsignedMessages,
38
- readHeader,
39
- readRawBody,
40
- } = require('../queue-message-auth');
41
-
42
- const QSTASH_API_BASE = 'https://qstash.upstash.io/v2';
43
-
44
- /**
45
- * @param {*} value
46
- * @returns {string|null} the URL in canonical form, or null when it is not one
47
- */
48
- function normalizeUrl(value) {
49
- if (typeof value !== 'string' || value.length === 0) return null;
50
- try {
51
- return new URL(value).href;
52
- } catch (_) {
53
- return null;
54
- }
55
- }
56
-
57
- class QStashQueueProvider extends QueueProvider {
58
- constructor(options = {}) {
59
- super();
60
- this.token =
61
- options.token || process.env.QSTASH_TOKEN;
62
- this.retries =
63
- options.retries !== undefined ? options.retries : 3;
64
- this.currentSigningKey = options.currentSigningKey;
65
- this.nextSigningKey = options.nextSigningKey;
66
- // The URL QStash delivers to. When unset, the JWT's sub is compared
67
- // with the request's own URL (event.rawUrl, set by Netlify).
68
- this.destinationUrl = options.destinationUrl;
69
- this.env = options.env || process.env;
70
- this.clockToleranceSeconds = options.clockToleranceSeconds || 0;
71
- // verifyEvent() proves an HTTP delivery came from QStash.
72
- this.verifiesHttpDelivery = true;
73
- }
74
-
75
- // Read per call: secrets may reach the environment after construction.
76
- _signingKeys() {
77
- return [
78
- this.currentSigningKey || this.env.QSTASH_CURRENT_SIGNING_KEY,
79
- this.nextSigningKey || this.env.QSTASH_NEXT_SIGNING_KEY,
80
- ].filter((key) => typeof key === 'string' && key.length > 0);
81
- }
82
-
83
- /**
84
- * Verify the Upstash-Signature JWT of a QStash delivery.
85
- *
86
- * @param {Object} event - Lambda-shaped HTTP event
87
- * @returns {Promise<{ ok: boolean, reason?: string, unsigned?: boolean }>}
88
- */
89
- async verifyEvent(event) {
90
- const keys = this._signingKeys();
91
- if (keys.length === 0) {
92
- return allowsUnsignedMessages(this.env)
93
- ? { ok: true, unsigned: true }
94
- : { ok: false, reason: 'signing_keys_not_configured' };
95
- }
96
- const token = readHeader(event, 'upstash-signature');
97
- if (!token) return { ok: false, reason: 'missing_signature' };
98
- const body = readRawBody(event);
99
- if (body === null) return { ok: false, reason: 'missing_body' };
100
-
101
- const jose = require('jose');
102
- let payload;
103
- for (const key of keys) {
104
- try {
105
- ({ payload } = await jose.jwtVerify(token, new TextEncoder().encode(key), {
106
- issuer: 'Upstash',
107
- algorithms: ['HS256'],
108
- requiredClaims: ['exp', 'nbf', 'sub', 'body'],
109
- clockTolerance: this.clockToleranceSeconds,
110
- }));
111
- break;
112
- } catch (_) {
113
- // try the next key
114
- }
115
- }
116
- if (!payload) return { ok: false, reason: 'invalid_signature' };
117
-
118
- const requestUrl = normalizeUrl(this.destinationUrl || event?.rawUrl);
119
- if (!requestUrl) return { ok: false, reason: 'missing_request_url' };
120
- if (normalizeUrl(payload.sub) !== requestUrl) {
121
- return { ok: false, reason: 'invalid_subject' };
122
- }
123
- const bodyHash = crypto.createHash('sha256').update(body, 'utf8').digest('base64url');
124
- const claimed = typeof payload.body === 'string' ? payload.body.replace(/=+$/, '') : '';
125
- if (claimed !== bodyHash.replace(/=+$/, '')) {
126
- return { ok: false, reason: 'body_mismatch' };
127
- }
128
- return { ok: true };
129
- }
130
-
131
- /**
132
- * Send a message via QStash.
133
- *
134
- * @param {Object} message - Message payload
135
- * @param {string} queueId - Destination URL that QStash will POST to
136
- */
137
- async send(message, queueId) {
138
- if (!this.token) {
139
- throw new Error(
140
- 'QSTASH_TOKEN is required. Get one at https://console.upstash.com'
141
- );
142
- }
143
-
144
- const headers = {
145
- Authorization: `Bearer ${this.token}`,
146
- 'Content-Type': 'application/json',
147
- 'Upstash-Retries': String(this.retries),
148
- };
149
-
150
- const response = await fetch(
151
- `${QSTASH_API_BASE}/publish/${queueId}`,
152
- {
153
- method: 'POST',
154
- headers,
155
- body: JSON.stringify(message),
156
- }
157
- );
158
-
159
- if (!response.ok) {
160
- const errorBody = await response.text().catch(() => 'unknown');
161
- throw new Error(
162
- `QStash publish failed: ${response.status} ${response.statusText} - ${errorBody}`
163
- );
164
- }
165
-
166
- return response.json();
167
- }
168
-
169
- /**
170
- * Send batch of messages via QStash batch API.
171
- */
172
- async batchSend(batch, queueId) {
173
- const entries = batch ?? [];
174
- if (!this.token) {
175
- throw new Error(
176
- 'QSTASH_TOKEN is required. Get one at https://console.upstash.com'
177
- );
178
- }
179
-
180
- const messages = entries.map((entry) => ({
181
- destination: queueId,
182
- body: JSON.stringify(entry),
183
- headers: { 'Content-Type': 'application/json' },
184
- retries: this.retries,
185
- }));
186
-
187
- const response = await fetch(`${QSTASH_API_BASE}/batch`, {
188
- method: 'POST',
189
- headers: {
190
- Authorization: `Bearer ${this.token}`,
191
- 'Content-Type': 'application/json',
192
- },
193
- body: JSON.stringify(messages),
194
- });
195
-
196
- if (!response.ok) {
197
- const errorBody = await response.text().catch(() => 'unknown');
198
- throw new Error(
199
- `QStash batch publish failed: ${response.status} - ${errorBody}`
200
- );
201
- }
202
-
203
- return response.json();
204
- }
205
-
206
- /**
207
- * Parse QStash webhook delivery event.
208
- * QStash POSTs the message body directly to the destination URL.
209
- */
210
- parseEvent(event) {
211
- const raw = readRawBody(event);
212
- const body = raw !== null ? JSON.parse(raw) : event.body;
213
-
214
- // QStash delivers a single message per invocation
215
- return [body];
216
- }
217
- }
218
-
219
- module.exports = { QStashQueueProvider };
@@ -1,70 +0,0 @@
1
- /**
2
- * SQS Queue Provider (Adapter)
3
- *
4
- * AWS SQS implementation of the QueueProvider port, from
5
- * friggframework/frigg#545 (where it lives in @friggframework/provider-aws).
6
- * Kept in core here because the AWS SDK is still a core dependency; the SDK
7
- * is required on first send, not at module load.
8
- */
9
- const { v4: uuid } = require('uuid');
10
- const { QueueProvider } = require('../queue-provider');
11
- const {
12
- SqsQueueClient,
13
- defaultSqsClientConfig,
14
- } = require('../sqs-queue-client');
15
-
16
- const SQS_BATCH_SIZE = 10;
17
-
18
- class SqsQueueProvider extends QueueProvider {
19
- /**
20
- * @param {Object} [options]
21
- * @param {QueueClientInterface} [options.queueClient] - defaults to an SqsQueueClient.
22
- */
23
- constructor({ queueClient } = {}) {
24
- super();
25
- this.queueClient =
26
- queueClient ||
27
- new SqsQueueClient({ clientConfig: defaultSqsClientConfig() });
28
- }
29
-
30
- async send(message, queueUrl) {
31
- return this.queueClient.sendMessage({
32
- MessageBody: JSON.stringify(message),
33
- QueueUrl: queueUrl,
34
- });
35
- }
36
-
37
- async batchSend(batch, queueUrl) {
38
- const entries = batch ?? [];
39
- let last = {};
40
- for (let i = 0; i < entries.length; i += SQS_BATCH_SIZE) {
41
- const Entries = entries.slice(i, i + SQS_BATCH_SIZE).map((entry) => ({
42
- Id: uuid(),
43
- MessageBody: JSON.stringify(entry),
44
- }));
45
- last = await this.queueClient.sendMessageBatch({
46
- Entries,
47
- QueueUrl: queueUrl,
48
- });
49
- }
50
- return last;
51
- }
52
-
53
- /**
54
- * Parse an SQS event: event.Records[].body (JSON string).
55
- */
56
- parseEvent(event) {
57
- const records = event?.Records || [];
58
- return records.map((record) => JSON.parse(record.body));
59
- }
60
-
61
- /**
62
- * SQS events reach a Lambda only through its event source mapping, which
63
- * AWS authenticates; there is no public URL to post to.
64
- */
65
- async verifyEvent() {
66
- return { ok: true };
67
- }
68
- }
69
-
70
- module.exports = { SqsQueueProvider };
@@ -1,55 +0,0 @@
1
- /**
2
- * Queue Client Interface (Port)
3
- *
4
- * Defines the contract for low-level queue message operations.
5
- * Used by Worker class and QueuerUtil for sending/receiving queue messages.
6
- *
7
- * Following Frigg's hexagonal architecture pattern:
8
- * - Port defines WHAT the service does (contract)
9
- * - Adapters implement HOW (AWS SQS, Netlify, QStash, etc.)
10
- *
11
- * Distinct from QueueProvider which is the higher-level provider for
12
- * integration-level queue operations (send/batchSend/parseEvent).
13
- * QueueClientInterface is the lower-level "how do I talk to the queue service" port.
14
- */
15
- class QueueClientInterface {
16
- /**
17
- * Send a single message to a queue
18
- *
19
- * @param {Object} params
20
- * @param {string} params.QueueUrl - Queue endpoint/identifier
21
- * @param {string} params.MessageBody - Serialized message body
22
- * @param {number} [params.DelaySeconds] - Delay before message becomes visible
23
- * @returns {Promise<{MessageId: string}>} Result with message identifier
24
- */
25
- async sendMessage(params) {
26
- throw new Error('Method sendMessage must be implemented by subclass');
27
- }
28
-
29
- /**
30
- * Send a batch of messages to a queue
31
- *
32
- * @param {Object} params
33
- * @param {string} params.QueueUrl - Queue endpoint/identifier
34
- * @param {Array<{Id: string, MessageBody: string}>} params.Entries - Messages to send
35
- * @returns {Promise<Object>} Batch send result
36
- */
37
- async sendMessageBatch(params) {
38
- throw new Error(
39
- 'Method sendMessageBatch must be implemented by subclass'
40
- );
41
- }
42
-
43
- /**
44
- * Resolve a queue name to a URL/endpoint
45
- *
46
- * @param {Object} params
47
- * @param {string} params.QueueName - Queue name to resolve
48
- * @returns {Promise<string>} Resolved queue URL/endpoint
49
- */
50
- async getQueueUrl(params) {
51
- throw new Error('Method getQueueUrl must be implemented by subclass');
52
- }
53
- }
54
-
55
- module.exports = { QueueClientInterface };
@@ -1,193 +0,0 @@
1
- /**
2
- * Queue message authentication for HTTP-delivered queues.
3
- *
4
- * An SQS-triggered Lambda is invoked by AWS itself, so its events are trusted
5
- * by the transport. An HTTP-delivered queue (a Netlify Background Function,
6
- * a QStash destination) is a public URL: anyone who can reach it can post a
7
- * message. These helpers let the sending adapter prove a message came from
8
- * Frigg and let the receiving adapter check that proof before anything runs.
9
- *
10
- * Scheme (Frigg-signed messages):
11
- * timestamp = whole seconds since the epoch, as a decimal string
12
- * signature = 'v1=' + hex(HMAC-SHA256(secret, `${timestamp}.${rawBody}`))
13
- * sent as the `x-frigg-queue-timestamp` and `x-frigg-queue-signature`
14
- * headers. The receiver recomputes the HMAC over the raw body it was given,
15
- * compares in constant time, and refuses a timestamp more than
16
- * MAX_MESSAGE_AGE_SECONDS away from its own clock (replay window).
17
- *
18
- * Fail closed: with no secret configured, a message is accepted unsigned only
19
- * when FRIGG_QUEUE_ALLOW_UNSIGNED=true AND the environment is not production
20
- * (isProductionEnvironment; a deployed function always is). Anything else is
21
- * refused.
22
- */
23
- const crypto = require('crypto');
24
- const { isDeployedRuntime } = require('../providers/runtime-detection');
25
-
26
- const SIGNING_SECRET_ENV = 'FRIGG_QUEUE_SIGNING_SECRET';
27
- const ALLOW_UNSIGNED_ENV = 'FRIGG_QUEUE_ALLOW_UNSIGNED';
28
- const SIGNATURE_HEADER = 'x-frigg-queue-signature';
29
- const TIMESTAMP_HEADER = 'x-frigg-queue-timestamp';
30
- const SIGNATURE_VERSION = 'v1';
31
- const MAX_MESSAGE_AGE_SECONDS = 5 * 60;
32
-
33
- // Stages core already treats as non-production (database/prisma.js bypasses
34
- // encryption for the same list).
35
- const NON_PRODUCTION_STAGES = ['dev', 'test', 'local'];
36
- const NON_PRODUCTION_NODE_ENVS = ['development', 'test'];
37
-
38
- /**
39
- * Production unless the environment says otherwise: an unset STAGE and
40
- * NODE_ENV count as production, so a missing setting never opens the door.
41
- * A deployed function (AWS Lambda, or the active provider's platform such as
42
- * a Netlify Function: providers/runtime-detection) is production whatever
43
- * STAGE says, so a deployed `dev` stage cannot opt out of signatures.
44
- *
45
- * @param {Object} [env=process.env]
46
- * @returns {boolean}
47
- */
48
- function isProductionEnvironment(env = process.env) {
49
- if (isDeployedRuntime(env)) return true;
50
- if (NON_PRODUCTION_STAGES.includes(env.STAGE)) return false;
51
- if (NON_PRODUCTION_NODE_ENVS.includes(env.NODE_ENV)) return false;
52
- if (env.NETLIFY_DEV === 'true') return false;
53
- return true;
54
- }
55
-
56
- /**
57
- * Unsigned messages are allowed only by an explicit opt-in outside production.
58
- *
59
- * @param {Object} [env=process.env]
60
- * @returns {boolean}
61
- */
62
- function allowsUnsignedMessages(env = process.env) {
63
- return env[ALLOW_UNSIGNED_ENV] === 'true' && !isProductionEnvironment(env);
64
- }
65
-
66
- /**
67
- * @param {Object} [env=process.env]
68
- * @returns {string|undefined} the configured signing secret, if non-empty
69
- */
70
- function readSigningSecret(env = process.env) {
71
- const secret = env[SIGNING_SECRET_ENV];
72
- return typeof secret === 'string' && secret.length > 0 ? secret : undefined;
73
- }
74
-
75
- function computeSignature(secret, timestamp, rawBody) {
76
- const mac = crypto
77
- .createHmac('sha256', secret)
78
- .update(`${timestamp}.${rawBody}`)
79
- .digest('hex');
80
- return `${SIGNATURE_VERSION}=${mac}`;
81
- }
82
-
83
- /**
84
- * Sign a raw message body.
85
- *
86
- * @param {Object} params
87
- * @param {string} params.body - the exact string that will be sent
88
- * @param {string} params.secret
89
- * @param {number} [params.now] - milliseconds since the epoch (default Date.now())
90
- * @returns {{ [header: string]: string }} headers to add to the request
91
- */
92
- function signQueueMessage({ body, secret, now = Date.now() }) {
93
- if (!secret) {
94
- throw new Error('A signing secret is required to sign a queue message');
95
- }
96
- const timestamp = String(Math.floor(now / 1000));
97
- return {
98
- [TIMESTAMP_HEADER]: timestamp,
99
- [SIGNATURE_HEADER]: computeSignature(secret, timestamp, body),
100
- };
101
- }
102
-
103
- function constantTimeEqual(a, b) {
104
- const left = Buffer.from(a, 'utf8');
105
- const right = Buffer.from(b, 'utf8');
106
- // timingSafeEqual throws on unequal lengths; the length of a v1 signature
107
- // is fixed, so comparing lengths first leaks nothing secret.
108
- if (left.length !== right.length) return false;
109
- return crypto.timingSafeEqual(left, right);
110
- }
111
-
112
- /**
113
- * Verify a Frigg-signed message.
114
- *
115
- * @param {Object} params
116
- * @param {string|null|undefined} params.body - raw body as received
117
- * @param {string|undefined} params.timestamp - x-frigg-queue-timestamp
118
- * @param {string|undefined} params.signature - x-frigg-queue-signature
119
- * @param {string} params.secret
120
- * @param {number} [params.now] - milliseconds since the epoch
121
- * @param {number} [params.maxAgeSeconds]
122
- * @returns {{ ok: true } | { ok: false, reason: string }}
123
- */
124
- function verifyQueueMessage({
125
- body,
126
- timestamp,
127
- signature,
128
- secret,
129
- now = Date.now(),
130
- maxAgeSeconds = MAX_MESSAGE_AGE_SECONDS,
131
- }) {
132
- if (!secret) return { ok: false, reason: 'signing_secret_not_configured' };
133
- if (!signature || !timestamp) return { ok: false, reason: 'missing_signature' };
134
- if (typeof body !== 'string') return { ok: false, reason: 'missing_body' };
135
- if (!/^\d{1,15}$/.test(timestamp)) return { ok: false, reason: 'invalid_timestamp' };
136
-
137
- const ageSeconds = Math.abs(Math.floor(now / 1000) - Number(timestamp));
138
- if (ageSeconds > maxAgeSeconds) return { ok: false, reason: 'stale_timestamp' };
139
-
140
- const expected = computeSignature(secret, timestamp, body);
141
- if (!constantTimeEqual(expected, signature)) {
142
- return { ok: false, reason: 'invalid_signature' };
143
- }
144
- return { ok: true };
145
- }
146
-
147
- /**
148
- * Case-insensitive header lookup over a Lambda/Netlify v1 event's headers.
149
- *
150
- * @param {Object} event
151
- * @param {string} name - lower-case header name
152
- * @returns {string|undefined}
153
- */
154
- function readHeader(event, name) {
155
- const headers = event?.headers || {};
156
- for (const [key, value] of Object.entries(headers)) {
157
- if (key.toLowerCase() === name) {
158
- return Array.isArray(value) ? value[0] : value;
159
- }
160
- }
161
- return undefined;
162
- }
163
-
164
- /**
165
- * The raw request body exactly as signed: a string body as is, a base64
166
- * body decoded to its UTF-8 text. An already-parsed object body cannot be
167
- * verified (its bytes are gone), so it reads as absent.
168
- *
169
- * @param {Object} event
170
- * @returns {string|null}
171
- */
172
- function readRawBody(event) {
173
- if (typeof event?.body !== 'string') return null;
174
- if (event.isBase64Encoded) {
175
- return Buffer.from(event.body, 'base64').toString('utf8');
176
- }
177
- return event.body;
178
- }
179
-
180
- module.exports = {
181
- SIGNING_SECRET_ENV,
182
- ALLOW_UNSIGNED_ENV,
183
- SIGNATURE_HEADER,
184
- TIMESTAMP_HEADER,
185
- MAX_MESSAGE_AGE_SECONDS,
186
- isProductionEnvironment,
187
- allowsUnsignedMessages,
188
- readSigningSecret,
189
- signQueueMessage,
190
- verifyQueueMessage,
191
- readHeader,
192
- readRawBody,
193
- };