@friggframework/core 2.0.0--canary.679.a2bcc2f.0 → 2.0.0--canary.686.87e9875.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 (136) hide show
  1. package/CLAUDE.md +25 -0
  2. package/README.md +105 -0
  3. package/application/commands/integration-commands.js +2 -2
  4. package/application/commands/user-commands.js +17 -5
  5. package/core/Worker.js +31 -7
  6. package/credential/use-cases/credential-operations-for-user.js +110 -25
  7. package/credential/use-cases/reauthorize-credential-for-user.js +15 -8
  8. package/database/config.js +48 -24
  9. package/database/documentdb-utils.js +6 -1
  10. package/database/encryption/encryption-schema-registry.js +80 -12
  11. package/database/organization-where.js +18 -0
  12. package/database/prisma.js +4 -2
  13. package/database/unique-violation.js +25 -0
  14. package/extensions/bootstrap-runner.js +43 -0
  15. package/extensions/extension-loader.js +167 -0
  16. package/extensions/index.js +41 -0
  17. package/extensions/initialize-app.js +123 -0
  18. package/extensions/route-mounter.js +103 -0
  19. package/extensions/schema-composer.js +188 -0
  20. package/generated/prisma-mongodb/edge.js +27 -4
  21. package/generated/prisma-mongodb/index-browser.js +24 -1
  22. package/generated/prisma-mongodb/index.d.ts +3227 -47
  23. package/generated/prisma-mongodb/index.js +27 -4
  24. package/generated/prisma-mongodb/package.json +1 -1
  25. package/generated/prisma-mongodb/schema.prisma +65 -0
  26. package/generated/prisma-mongodb/wasm.js +27 -4
  27. package/generated/prisma-postgresql/edge.js +27 -4
  28. package/generated/prisma-postgresql/index-browser.js +24 -1
  29. package/generated/prisma-postgresql/index.d.ts +3519 -67
  30. package/generated/prisma-postgresql/index.js +27 -4
  31. package/generated/prisma-postgresql/package.json +1 -1
  32. package/generated/prisma-postgresql/schema.prisma +61 -0
  33. package/generated/prisma-postgresql/wasm.js +27 -4
  34. package/handlers/app-definition-loader.js +93 -12
  35. package/handlers/routers/auth.js +40 -12
  36. package/handlers/routers/integration-defined-routers.js +144 -92
  37. package/handlers/routers/integration-webhook-routers.js +102 -64
  38. package/index.js +58 -1
  39. package/infrastructure/scheduler/index.js +19 -6
  40. package/infrastructure/scheduler/netlify-scheduler-adapter.js +185 -0
  41. package/infrastructure/scheduler/scheduler-service-factory.js +36 -7
  42. package/integrations/integration-router.js +115 -73
  43. package/integrations/repositories/integration-not-found.js +14 -0
  44. package/integrations/repositories/integration-repository-documentdb.js +35 -3
  45. package/integrations/repositories/integration-repository-interface.js +17 -1
  46. package/integrations/repositories/integration-repository-mongo.js +59 -76
  47. package/integrations/repositories/integration-repository-postgres.js +62 -76
  48. package/integrations/tests/doubles/test-integration-repository.js +12 -1
  49. package/integrations/use-cases/create-integration.js +55 -20
  50. package/integrations/use-cases/get-integrations-for-user.js +80 -40
  51. package/integrations/use-cases/index.js +5 -1
  52. package/integrations/use-cases/integration-operations-for-user.js +55 -24
  53. package/integrations/use-cases/resolve-integration-ownership.js +177 -0
  54. package/management-api/management-api-config.js +71 -2
  55. package/management-api/openapi/components.js +40 -0
  56. package/management-api/openapi/v2-operations.js +16 -4
  57. package/management-api/v2/dependencies.js +40 -15
  58. package/management-api/v2/dto.js +20 -0
  59. package/management-api/v2/handlers/authorization.js +1 -0
  60. package/management-api/v2/handlers/credentials.js +6 -2
  61. package/management-api/v2/handlers/entities.js +10 -7
  62. package/management-api/v2/handlers/integrations.js +6 -3
  63. package/management-api/v2/request.js +32 -1
  64. package/modules/entity-scope.js +15 -0
  65. package/modules/index.js +8 -0
  66. package/modules/module-factory.js +12 -0
  67. package/modules/module.js +35 -2
  68. package/modules/repositories/entity-not-found.js +18 -0
  69. package/modules/repositories/module-repository-documentdb.js +51 -6
  70. package/modules/repositories/module-repository-interface.js +19 -2
  71. package/modules/repositories/module-repository-mongo.js +67 -74
  72. package/modules/repositories/module-repository-postgres.js +66 -74
  73. package/modules/repositories/module-repository.js +2 -1
  74. package/modules/scope-config.js +192 -0
  75. package/modules/scope-policy.js +112 -0
  76. package/modules/scope-resolution.js +288 -0
  77. package/modules/scope-visibility.js +247 -0
  78. package/modules/tests/doubles/ownership-matching.js +18 -0
  79. package/modules/use-cases/authorize-entity-for-user.js +61 -0
  80. package/modules/use-cases/create-entity-from-credential.js +113 -0
  81. package/modules/use-cases/entity-operations-for-user.js +50 -13
  82. package/modules/use-cases/entity-types.js +31 -7
  83. package/modules/use-cases/execute-entity-proxy-request.js +50 -3
  84. package/modules/use-cases/get-entities-for-user.js +16 -5
  85. package/modules/use-cases/get-entity-module-for-user.js +32 -5
  86. package/modules/use-cases/get-entity-options-by-id.js +9 -3
  87. package/modules/use-cases/get-module.js +9 -3
  88. package/modules/use-cases/manage-access.js +21 -0
  89. package/modules/use-cases/multi-step-authorization.js +39 -12
  90. package/modules/use-cases/process-authorization-callback.js +52 -18
  91. package/modules/use-cases/refresh-entity-options.js +9 -3
  92. package/modules/use-cases/test-module-auth.js +9 -3
  93. package/package.json +6 -5
  94. package/prisma-mongodb/schema.prisma +65 -0
  95. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
  96. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
  97. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +28 -0
  98. package/prisma-postgresql/schema.prisma +61 -0
  99. package/providers/index.js +13 -0
  100. package/providers/resolve-provider.js +93 -0
  101. package/queues/index.js +25 -0
  102. package/queues/providers/index.js +11 -0
  103. package/queues/providers/netlify-background-provider.js +76 -0
  104. package/queues/providers/qstash-queue-provider.js +122 -0
  105. package/queues/providers/sqs-queue-provider.js +61 -0
  106. package/queues/queue-client-interface.js +55 -0
  107. package/queues/queue-provider-factory.js +101 -0
  108. package/queues/queue-provider.js +46 -0
  109. package/queues/sqs-queue-client.js +81 -0
  110. package/types/index.d.ts +1 -0
  111. package/types/modules/index.d.ts +129 -0
  112. package/types/user/index.d.ts +134 -0
  113. package/user/adopter-jwt-config.js +177 -0
  114. package/user/adopter-jwt-errors.js +59 -0
  115. package/user/adopter-token-verifier.js +122 -0
  116. package/user/app-user-helpers.js +97 -0
  117. package/user/create-user-authentication.js +64 -0
  118. package/user/membership.js +51 -0
  119. package/user/repositories/organization-membership-record.js +19 -0
  120. package/user/repositories/organization-membership-repository-documentdb.js +106 -0
  121. package/user/repositories/organization-membership-repository-factory.js +42 -0
  122. package/user/repositories/organization-membership-repository-interface.js +57 -0
  123. package/user/repositories/organization-membership-repository-mongo.js +13 -0
  124. package/user/repositories/organization-membership-repository-postgres.js +29 -0
  125. package/user/repositories/organization-membership-repository-prisma.js +81 -0
  126. package/user/repositories/user-repository-documentdb.js +27 -2
  127. package/user/repositories/user-repository-interface.js +18 -0
  128. package/user/repositories/user-repository-mongo.js +24 -0
  129. package/user/repositories/user-repository-postgres.js +18 -0
  130. package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
  131. package/user/use-cases/authenticate-user.js +7 -1
  132. package/user/use-cases/ensure-organization-membership.js +92 -0
  133. package/user/use-cases/find-individual-users-by-organization.js +54 -0
  134. package/user/use-cases/get-user-from-adopter-jwt.js +95 -124
  135. package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
  136. package/user/user.js +64 -1
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Provider Resolver
3
+ *
4
+ * Resolves a provider name from the appDefinition into the corresponding
5
+ * @friggframework/provider-{name} package. This is the bridge between
6
+ * `appDefinition.provider` and the provider plugin interface.
7
+ *
8
+ * Resolution order:
9
+ * 1. Explicit `providerName` argument
10
+ * 2. `appDefinition.provider`
11
+ * 3. `FRIGG_PROVIDER` environment variable
12
+ * 4. Default: 'aws'
13
+ *
14
+ * Package naming convention:
15
+ * provider: 'netlify' → require('@friggframework/provider-netlify')
16
+ * provider: 'aws' → require('@friggframework/provider-aws')
17
+ *
18
+ * @example
19
+ * const provider = resolveProvider({ provider: 'netlify' });
20
+ * // provider.name === 'netlify'
21
+ * // provider.deploy(appDefinition, options)
22
+ * // provider.createHandler(options)
23
+ * // provider.QueueProvider (class)
24
+ * // etc.
25
+ */
26
+
27
+ const KNOWN_PROVIDERS = ['aws', 'netlify'];
28
+
29
+ /**
30
+ * Determine the provider name from available sources.
31
+ *
32
+ * @param {Object} [appDefinition] - Frigg app definition
33
+ * @param {string} [providerName] - Explicit override
34
+ * @returns {string} Provider name (e.g. 'netlify', 'aws')
35
+ */
36
+ function determineProviderName(appDefinition, providerName) {
37
+ if (providerName) {
38
+ return providerName;
39
+ }
40
+ if (appDefinition?.provider) {
41
+ return appDefinition.provider;
42
+ }
43
+ if (process.env.FRIGG_PROVIDER) {
44
+ return process.env.FRIGG_PROVIDER;
45
+ }
46
+ return 'aws';
47
+ }
48
+
49
+ /**
50
+ * Resolve a provider name to a package name.
51
+ *
52
+ * @param {string} name - Provider name (e.g. 'netlify')
53
+ * @returns {string} npm package name (e.g. '@friggframework/provider-netlify')
54
+ */
55
+ function providerPackageName(name) {
56
+ return `@friggframework/provider-${name}`;
57
+ }
58
+
59
+ /**
60
+ * Resolve and load the provider plugin for the given app definition.
61
+ *
62
+ * @param {Object} [appDefinition] - Frigg app definition (reads .provider)
63
+ * @param {Object} [options]
64
+ * @param {string} [options.provider] - Explicit provider name override
65
+ * @returns {Object} Provider plugin (conforms to the provider plugin interface)
66
+ * @throws {Error} If the provider package is not installed
67
+ */
68
+ function resolveProvider(appDefinition, options = {}) {
69
+ const name = determineProviderName(appDefinition, options.provider);
70
+ const packageName = providerPackageName(name);
71
+
72
+ try {
73
+ return require(packageName);
74
+ } catch (error) {
75
+ if (error.code === 'MODULE_NOT_FOUND') {
76
+ const hint = KNOWN_PROVIDERS.includes(name)
77
+ ? `Install it with: npm install ${packageName}`
78
+ : `Is '${name}' a valid Frigg provider? Known providers: ${KNOWN_PROVIDERS.join(', ')}`;
79
+
80
+ throw new Error(
81
+ `Provider '${name}' specified in appDefinition but package '${packageName}' is not installed.\n ${hint}`
82
+ );
83
+ }
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ module.exports = {
89
+ resolveProvider,
90
+ determineProviderName,
91
+ providerPackageName,
92
+ KNOWN_PROVIDERS,
93
+ };
package/queues/index.js CHANGED
@@ -1,4 +1,29 @@
1
1
  const { QueuerUtil } = require('./queuer-util');
2
+ const { QueueProvider } = require('./queue-provider');
3
+ const { QueueClientInterface } = require('./queue-client-interface');
4
+ const {
5
+ createQueueProvider,
6
+ determineProvider,
7
+ QUEUE_PROVIDERS,
8
+ } = require('./queue-provider-factory');
9
+ const {
10
+ NetlifyBackgroundProvider,
11
+ QStashQueueProvider,
12
+ } = require('./providers');
13
+
2
14
  module.exports = {
3
15
  QueuerUtil,
16
+ QueueProvider,
17
+ QueueClientInterface,
18
+ createQueueProvider,
19
+ determineProvider,
20
+ QUEUE_PROVIDERS,
21
+ NetlifyBackgroundProvider,
22
+ QStashQueueProvider,
23
+ get SqsQueueProvider() {
24
+ return require('./providers/sqs-queue-provider').SqsQueueProvider;
25
+ },
26
+ get SqsQueueClient() {
27
+ return require('./sqs-queue-client').SqsQueueClient;
28
+ },
4
29
  };
@@ -0,0 +1,11 @@
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
+ };
@@ -0,0 +1,76 @@
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
+ * Limitations:
12
+ * - No built-in retry (must implement manually)
13
+ * - No dead-letter queue
14
+ * - No FIFO ordering guarantees
15
+ * - 15-minute max execution time
16
+ */
17
+ const { QueueProvider } = require('../queue-provider');
18
+
19
+ class NetlifyBackgroundProvider extends QueueProvider {
20
+ constructor(options = {}) {
21
+ super();
22
+ // Base URL for triggering background functions (e.g., https://mysite.netlify.app)
23
+ this.baseUrl = options.baseUrl || process.env.URL || '';
24
+ }
25
+
26
+ /**
27
+ * Send a message by invoking a Netlify Background Function via HTTP POST.
28
+ *
29
+ * @param {Object} message - Message payload
30
+ * @param {string} queueId - Background function path (e.g., '/.netlify/functions/worker-background')
31
+ */
32
+ async send(message, queueId) {
33
+ const url = `${this.baseUrl}${queueId}`;
34
+ const response = await fetch(url, {
35
+ method: 'POST',
36
+ headers: { 'Content-Type': 'application/json' },
37
+ body: JSON.stringify(message),
38
+ });
39
+
40
+ if (!response.ok && response.status !== 202) {
41
+ throw new Error(
42
+ `Failed to invoke background function at ${queueId}: ${response.status} ${response.statusText}`
43
+ );
44
+ }
45
+
46
+ return { status: response.status, queueId };
47
+ }
48
+
49
+ /**
50
+ * Send batch of messages sequentially (background functions are HTTP-triggered).
51
+ */
52
+ async batchSend(entries = [], queueId) {
53
+ const results = [];
54
+ for (const entry of entries) {
55
+ const result = await this.send(entry, queueId);
56
+ results.push(result);
57
+ }
58
+ return { results };
59
+ }
60
+
61
+ /**
62
+ * Parse Netlify Background Function event.
63
+ * Background functions receive the raw HTTP event body as the event.
64
+ */
65
+ parseEvent(event) {
66
+ const body =
67
+ typeof event.body === 'string'
68
+ ? JSON.parse(event.body)
69
+ : event.body;
70
+
71
+ // Background functions receive a single message per invocation
72
+ return [body];
73
+ }
74
+ }
75
+
76
+ module.exports = { NetlifyBackgroundProvider };
@@ -0,0 +1,122 @@
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
+ const { QueueProvider } = require('../queue-provider');
21
+
22
+ const QSTASH_API_BASE = 'https://qstash.upstash.io/v2';
23
+
24
+ class QStashQueueProvider extends QueueProvider {
25
+ constructor(options = {}) {
26
+ super();
27
+ this.token =
28
+ options.token || process.env.QSTASH_TOKEN;
29
+ this.retries =
30
+ options.retries !== undefined ? options.retries : 3;
31
+ }
32
+
33
+ /**
34
+ * Send a message via QStash.
35
+ *
36
+ * @param {Object} message - Message payload
37
+ * @param {string} queueId - Destination URL that QStash will POST to
38
+ */
39
+ async send(message, queueId) {
40
+ if (!this.token) {
41
+ throw new Error(
42
+ 'QSTASH_TOKEN is required. Get one at https://console.upstash.com'
43
+ );
44
+ }
45
+
46
+ const headers = {
47
+ Authorization: `Bearer ${this.token}`,
48
+ 'Content-Type': 'application/json',
49
+ 'Upstash-Retries': String(this.retries),
50
+ };
51
+
52
+ const response = await fetch(
53
+ `${QSTASH_API_BASE}/publish/${queueId}`,
54
+ {
55
+ method: 'POST',
56
+ headers,
57
+ body: JSON.stringify(message),
58
+ }
59
+ );
60
+
61
+ if (!response.ok) {
62
+ const errorBody = await response.text().catch(() => 'unknown');
63
+ throw new Error(
64
+ `QStash publish failed: ${response.status} ${response.statusText} - ${errorBody}`
65
+ );
66
+ }
67
+
68
+ return response.json();
69
+ }
70
+
71
+ /**
72
+ * Send batch of messages via QStash batch API.
73
+ */
74
+ async batchSend(entries = [], queueId) {
75
+ if (!this.token) {
76
+ throw new Error(
77
+ 'QSTASH_TOKEN is required. Get one at https://console.upstash.com'
78
+ );
79
+ }
80
+
81
+ const messages = entries.map((entry) => ({
82
+ destination: queueId,
83
+ body: JSON.stringify(entry),
84
+ headers: { 'Content-Type': 'application/json' },
85
+ retries: this.retries,
86
+ }));
87
+
88
+ const response = await fetch(`${QSTASH_API_BASE}/batch`, {
89
+ method: 'POST',
90
+ headers: {
91
+ Authorization: `Bearer ${this.token}`,
92
+ 'Content-Type': 'application/json',
93
+ },
94
+ body: JSON.stringify(messages),
95
+ });
96
+
97
+ if (!response.ok) {
98
+ const errorBody = await response.text().catch(() => 'unknown');
99
+ throw new Error(
100
+ `QStash batch publish failed: ${response.status} - ${errorBody}`
101
+ );
102
+ }
103
+
104
+ return response.json();
105
+ }
106
+
107
+ /**
108
+ * Parse QStash webhook delivery event.
109
+ * QStash POSTs the message body directly to the destination URL.
110
+ */
111
+ parseEvent(event) {
112
+ const body =
113
+ typeof event.body === 'string'
114
+ ? JSON.parse(event.body)
115
+ : event.body;
116
+
117
+ // QStash delivers a single message per invocation
118
+ return [body];
119
+ }
120
+ }
121
+
122
+ module.exports = { QStashQueueProvider };
@@ -0,0 +1,61 @@
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(entries = [], queueUrl) {
38
+ let last = {};
39
+ for (let i = 0; i < entries.length; i += SQS_BATCH_SIZE) {
40
+ const Entries = entries.slice(i, i + SQS_BATCH_SIZE).map((entry) => ({
41
+ Id: uuid(),
42
+ MessageBody: JSON.stringify(entry),
43
+ }));
44
+ last = await this.queueClient.sendMessageBatch({
45
+ Entries,
46
+ QueueUrl: queueUrl,
47
+ });
48
+ }
49
+ return last;
50
+ }
51
+
52
+ /**
53
+ * Parse an SQS event: event.Records[].body (JSON string).
54
+ */
55
+ parseEvent(event) {
56
+ const records = event?.Records || [];
57
+ return records.map((record) => JSON.parse(record.body));
58
+ }
59
+ }
60
+
61
+ module.exports = { SqsQueueProvider };
@@ -0,0 +1,55 @@
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 };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Queue Provider Factory
3
+ *
4
+ * Creates a QueueProvider (send / batchSend / parseEvent) for the active
5
+ * platform. From friggframework/frigg#545, adapted: #545 resolved every
6
+ * provider through resolveProvider(), so an AWS app had to install
7
+ * @friggframework/provider-aws. In this stack the AWS SDK is a core
8
+ * dependency, so the built-in queues are created here and only an unknown
9
+ * name goes through the provider plugin system.
10
+ *
11
+ * Priority order for determining the provider:
12
+ * 1. Explicit `provider` option
13
+ * 2. `appDefinition.queue.provider`
14
+ * 3. `QUEUE_PROVIDER` environment variable
15
+ * 4. Default: SQS
16
+ *
17
+ * Built-in names:
18
+ * - 'sqs' | 'aws' -> SqsQueueProvider
19
+ * - 'netlify-background' | 'netlify' -> NetlifyBackgroundProvider
20
+ * - 'qstash' -> QStashQueueProvider
21
+ * Any other name -> require('@friggframework/provider-<name>').QueueProvider
22
+ */
23
+
24
+ const QUEUE_PROVIDERS = {
25
+ SQS: 'sqs',
26
+ NETLIFY_BACKGROUND: 'netlify-background',
27
+ QSTASH: 'qstash',
28
+ };
29
+
30
+ // Platform provider names (appDefinition.provider) that map to a built-in queue.
31
+ const PLATFORM_ALIASES = {
32
+ aws: QUEUE_PROVIDERS.SQS,
33
+ netlify: QUEUE_PROVIDERS.NETLIFY_BACKGROUND,
34
+ };
35
+
36
+ /**
37
+ * Determine the queue provider from the config hierarchy.
38
+ *
39
+ * @param {Object} [options]
40
+ * @param {string} [options.provider]
41
+ * @param {Object} [options.appDefinition]
42
+ * @returns {string|undefined}
43
+ */
44
+ function determineProvider(options = {}) {
45
+ if (options.provider) {
46
+ return options.provider;
47
+ }
48
+ if (options.appDefinition?.queue?.provider) {
49
+ return options.appDefinition.queue.provider;
50
+ }
51
+ if (process.env.QUEUE_PROVIDER) {
52
+ return process.env.QUEUE_PROVIDER;
53
+ }
54
+ return undefined;
55
+ }
56
+
57
+ /**
58
+ * Create a queue provider instance.
59
+ *
60
+ * @param {Object} [options]
61
+ * @param {string} [options.provider] - Queue provider name
62
+ * @param {Object} [options.appDefinition] - reads `queue.provider`
63
+ * @param {Object} [options.providerOptions] - passed to the provider constructor
64
+ * @returns {import('./queue-provider').QueueProvider}
65
+ */
66
+ function createQueueProvider(options = {}) {
67
+ const explicit = determineProvider(options);
68
+ const name = PLATFORM_ALIASES[explicit] || explicit || QUEUE_PROVIDERS.SQS;
69
+ const providerOptions = options.providerOptions || {};
70
+
71
+ switch (name) {
72
+ case QUEUE_PROVIDERS.SQS: {
73
+ const { SqsQueueProvider } = require('./providers/sqs-queue-provider');
74
+ return new SqsQueueProvider(providerOptions);
75
+ }
76
+ case QUEUE_PROVIDERS.NETLIFY_BACKGROUND: {
77
+ const {
78
+ NetlifyBackgroundProvider,
79
+ } = require('./providers/netlify-background-provider');
80
+ return new NetlifyBackgroundProvider(providerOptions);
81
+ }
82
+ case QUEUE_PROVIDERS.QSTASH: {
83
+ const { QStashQueueProvider } = require('./providers/qstash-queue-provider');
84
+ return new QStashQueueProvider(providerOptions);
85
+ }
86
+ default: {
87
+ const { resolveProvider } = require('../providers/resolve-provider');
88
+ const provider = resolveProvider(null, { provider: name });
89
+ if (!provider.QueueProvider) {
90
+ throw new Error(`Provider '${provider.name || name}' does not export a QueueProvider`);
91
+ }
92
+ return new provider.QueueProvider(providerOptions);
93
+ }
94
+ }
95
+ }
96
+
97
+ module.exports = {
98
+ createQueueProvider,
99
+ determineProvider,
100
+ QUEUE_PROVIDERS,
101
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Queue Provider Interface (Port)
3
+ *
4
+ * Defines the contract for queue message sending.
5
+ * All queue adapters must extend this interface.
6
+ *
7
+ * Following Frigg's hexagonal architecture pattern:
8
+ * - Port defines WHAT the service does (contract)
9
+ * - Adapters implement HOW (AWS SQS, Netlify Background Functions, QStash, etc.)
10
+ */
11
+ class QueueProvider {
12
+ /**
13
+ * Send a single message to a queue
14
+ *
15
+ * @param {Object} message - Message payload (will be JSON-serialized)
16
+ * @param {string} queueId - Queue identifier (URL, name, or endpoint depending on provider)
17
+ * @returns {Promise<Object>} Provider-specific response
18
+ */
19
+ async send(message, queueId) {
20
+ throw new Error('Method send must be implemented by subclass');
21
+ }
22
+
23
+ /**
24
+ * Send a batch of messages to a queue
25
+ *
26
+ * @param {Object[]} entries - Array of message payloads
27
+ * @param {string} queueId - Queue identifier
28
+ * @returns {Promise<Object>} Provider-specific response
29
+ */
30
+ async batchSend(entries, queueId) {
31
+ throw new Error('Method batchSend must be implemented by subclass');
32
+ }
33
+
34
+ /**
35
+ * Parse an incoming event into an array of message payloads.
36
+ * Each provider has its own event shape (SQS Records, HTTP body, etc.)
37
+ *
38
+ * @param {Object} event - Raw event from the runtime (Lambda event, HTTP request, etc.)
39
+ * @returns {Object[]} Array of parsed message payloads
40
+ */
41
+ parseEvent(event) {
42
+ throw new Error('Method parseEvent must be implemented by subclass');
43
+ }
44
+ }
45
+
46
+ module.exports = { QueueProvider };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * SQS Queue Client (Adapter)
3
+ *
4
+ * AWS SQS implementation of QueueClientInterface, used by Worker.
5
+ *
6
+ * In friggframework/frigg#545 this adapter moved to
7
+ * @friggframework/provider-aws and Worker required it to be injected. In
8
+ * this stack the AWS SDK stays a core dependency, so the adapter lives here,
9
+ * Worker defaults to it, and provider-aws re-exports it. The SDK is
10
+ * required on first use, not at module load.
11
+ */
12
+ const { QueueClientInterface } = require('./queue-client-interface');
13
+
14
+ let sqsModule = null;
15
+ function getSqsModule() {
16
+ if (!sqsModule) {
17
+ sqsModule = require('@aws-sdk/client-sqs');
18
+ }
19
+ return sqsModule;
20
+ }
21
+
22
+ /**
23
+ * Client config #545's adapter used: IS_OFFLINE credentials, AWS_REGION and
24
+ * AWS_ENDPOINT.
25
+ */
26
+ function defaultSqsClientConfig() {
27
+ const config = {};
28
+ if (process.env.IS_OFFLINE) {
29
+ config.credentials = {
30
+ accessKeyId: 'test-aws-key',
31
+ secretAccessKey: 'test-aws-secret',
32
+ };
33
+ config.region = 'us-east-1';
34
+ }
35
+ if (!config.region && process.env.AWS_REGION) {
36
+ config.region = process.env.AWS_REGION;
37
+ }
38
+ if (process.env.AWS_ENDPOINT) {
39
+ config.endpoint = process.env.AWS_ENDPOINT;
40
+ }
41
+ return config;
42
+ }
43
+
44
+ class SqsQueueClient extends QueueClientInterface {
45
+ /**
46
+ * @param {Object} [options]
47
+ * @param {Object} [options.clientConfig] - SQSClient config; defaults to
48
+ * defaultSqsClientConfig().
49
+ */
50
+ constructor({ clientConfig } = {}) {
51
+ super();
52
+ this._clientConfig = clientConfig;
53
+ this._sqs = null;
54
+ }
55
+
56
+ _client() {
57
+ if (!this._sqs) {
58
+ const { SQSClient } = getSqsModule();
59
+ this._sqs = new SQSClient(this._clientConfig || defaultSqsClientConfig());
60
+ }
61
+ return this._sqs;
62
+ }
63
+
64
+ async sendMessage(params) {
65
+ const { SendMessageCommand } = getSqsModule();
66
+ return this._client().send(new SendMessageCommand(params));
67
+ }
68
+
69
+ async sendMessageBatch(params) {
70
+ const { SendMessageBatchCommand } = getSqsModule();
71
+ return this._client().send(new SendMessageBatchCommand(params));
72
+ }
73
+
74
+ async getQueueUrl(params) {
75
+ const { GetQueueUrlCommand } = getSqsModule();
76
+ const data = await this._client().send(new GetQueueUrlCommand(params));
77
+ return data.QueueUrl;
78
+ }
79
+ }
80
+
81
+ module.exports = { SqsQueueClient, defaultSqsClientConfig };
package/types/index.d.ts CHANGED
@@ -12,4 +12,5 @@
12
12
  /// <reference path="./eslint-config/index.d.ts" />
13
13
  /// <reference path="./associations/index.d.ts" />
14
14
  /// <reference path="./syncs/index.d.ts" />
15
+ /// <reference path="./user/index.d.ts" />
15
16
  /// <reference path="./modules/index.d.ts" />