@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,329 +0,0 @@
1
- const Boom = require('@hapi/boom');
2
- const { FetchError } = require('../../errors/fetch-error');
3
- const { getLogger } = require('../../logs');
4
- const { AuthorizationSession } = require('../domain/authorization-session');
5
- const { findDefinition, getAuthStepCount } = require('./entity-types');
6
- const { describeRecord } = require('../scope-visibility');
7
- const { DEFAULT_SCOPE_POLICY } = require('../scope-policy');
8
-
9
- const log = getLogger('frigg.api.authorize');
10
-
11
- const defaultCreateModule = (params) => {
12
- const { Module } = require('../module');
13
- return Module.create(params);
14
- };
15
-
16
- const isMultiStep = (definition) =>
17
- getAuthStepCount(definition) > 1 ||
18
- typeof definition.processAuthorizationStep === 'function';
19
-
20
- const sessionNotFound = () =>
21
- Boom.notFound('Authorization session not found or expired', {
22
- code: 'AUTHORIZATION_SESSION_NOT_FOUND',
23
- });
24
-
25
- /**
26
- * Maps a failure from the module or the provider to a client-facing error.
27
- * Boom errors pass through. A provider HTTP failure is a 502; a failed auth
28
- * test or a ClientSafeError is a 400. Anything else stays a 500.
29
- */
30
- function toAuthorizationError(error, { passMessage = false } = {}) {
31
- if (error?.isBoom) return error;
32
- if (error instanceof FetchError) {
33
- return Boom.badGateway('The provider rejected the authorization request', {
34
- code: 'UPSTREAM_ERROR',
35
- expose: true,
36
- details: { upstreamStatus: error.statusCode ?? null },
37
- });
38
- }
39
- if (error?.isClientSafe || passMessage) {
40
- return Boom.badRequest(error?.message || 'Authorization failed', {
41
- code: 'AUTHORIZATION_FAILED',
42
- });
43
- }
44
- if (error?.message === 'Authorization failed') {
45
- return Boom.badRequest(
46
- 'Authorization failed: the credentials did not pass the module auth test',
47
- { code: 'AUTHORIZATION_FAILED' }
48
- );
49
- }
50
- return error;
51
- }
52
-
53
- /**
54
- * Loads the session a caller continues, checking it belongs to them, to the
55
- * entity type and (for re-authorization) to the credential.
56
- */
57
- async function loadSession(repository, sessionId, binding) {
58
- if (typeof sessionId !== 'string' || !sessionId) throw sessionNotFound();
59
- const session = await repository.findBySessionId(sessionId);
60
- if (!session || session.isExpired() || !session.belongsTo(binding)) {
61
- throw sessionNotFound();
62
- }
63
- if (session.completed) {
64
- throw Boom.conflict('Authorization session is already complete', {
65
- code: 'AUTHORIZATION_SESSION_COMPLETED',
66
- });
67
- }
68
- return session;
69
- }
70
-
71
- async function startSession(repository, binding, maxSteps) {
72
- // Expired sessions are removed opportunistically; PostgreSQL has no TTL.
73
- Promise.resolve()
74
- .then(() => repository.deleteExpired())
75
- .catch((error) =>
76
- log.warn('Expired authorization sessions were not removed', {
77
- eventName: `${log.name}.session_cleanup_failed`,
78
- error,
79
- })
80
- );
81
- return repository.create(AuthorizationSession.start({ ...binding, maxSteps }));
82
- }
83
-
84
- /**
85
- * GET /api/v2/authorize (and GET /api/v2/credentials/:id/reauthorize): what
86
- * the caller must submit next. A multi-step flow gets a session on step 1;
87
- * later steps need that sessionId and must be the session's current step.
88
- */
89
- class GetAuthorizationStep {
90
- /**
91
- * @param {Object} params
92
- * @param {Array<Object>} params.moduleDefinitions
93
- * @param {import('./get-authorization-requirements').GetAuthorizationRequirements} params.getAuthorizationRequirements
94
- * @param {import('../repositories/authorization-session-repository-interface').AuthorizationSessionRepositoryInterface} params.authorizationSessionRepository
95
- */
96
- constructor({ moduleDefinitions, getAuthorizationRequirements, authorizationSessionRepository }) {
97
- this.moduleDefinitions = moduleDefinitions || [];
98
- this.getAuthorizationRequirements = getAuthorizationRequirements;
99
- this.sessions = authorizationSessionRepository;
100
- }
101
-
102
- async execute({ user, entityType, step = 1, sessionId, credentialId = null, state }) {
103
- const definition = findDefinition(this.moduleDefinitions, entityType);
104
- const userId = user.getId();
105
- const requirementsFor = (n) =>
106
- this.getAuthorizationRequirements.execute({ entityType, step: n, userId, state });
107
-
108
- if (!isMultiStep(definition)) return requirementsFor(step);
109
-
110
- const binding = { userId, entityType, credentialId };
111
- let session;
112
- if (sessionId) {
113
- session = await loadSession(this.sessions, sessionId, binding);
114
- if (step !== session.currentStep) {
115
- throw Boom.conflict(
116
- `This session expects step ${session.currentStep}`,
117
- { code: 'STEP_OUT_OF_ORDER', details: { expectedStep: session.currentStep } }
118
- );
119
- }
120
- } else {
121
- if (step !== 1) {
122
- throw Boom.badRequest('sessionId is required after step 1', {
123
- code: 'SESSION_REQUIRED',
124
- });
125
- }
126
- session = await startSession(this.sessions, binding, getAuthStepCount(definition));
127
- }
128
- return { ...(await requirementsFor(step)), sessionId: session.sessionId };
129
- }
130
- }
131
-
132
- /**
133
- * POST /api/v2/authorize (and POST /api/v2/credentials/:id/reauthorize).
134
- *
135
- * Single-step modules complete at once through ProcessAuthorizationCallback
136
- * (the v1 authorize logic). Multi-step modules run
137
- * definition.processAuthorizationStep(api, step, data, stepData) per step;
138
- * when it reports `completed`, its `authData` goes through the same
139
- * callback. Returns either
140
- * `{ status: 'complete', entity, credential }` (domain records; the handler
141
- * masks them) or
142
- * `{ status: 'pending', step, totalSteps, sessionId, requirements, message? }`.
143
- */
144
- class SubmitAuthorizationStep {
145
- constructor({
146
- moduleDefinitions,
147
- getAuthorizationRequirements,
148
- authorizationSessionRepository,
149
- authorizeEntityForUser,
150
- moduleRepository,
151
- credentialRepository,
152
- createModule,
153
- scopePolicy,
154
- }) {
155
- this.scopePolicy = scopePolicy || DEFAULT_SCOPE_POLICY;
156
- this.moduleDefinitions = moduleDefinitions || [];
157
- this.getAuthorizationRequirements = getAuthorizationRequirements;
158
- this.sessions = authorizationSessionRepository;
159
- this.authorizeEntityForUser = authorizeEntityForUser;
160
- this.moduleRepository = moduleRepository;
161
- this.credentialRepository = credentialRepository;
162
- this.createModule = createModule || defaultCreateModule;
163
- }
164
-
165
- /**
166
- * @param {Object} params
167
- * @param {string} [params.scope] - the scope the caller asks for, from the
168
- * deciding level's allow list (ADR-056 §3.1). Never `global`.
169
- * @param {string} [params.fixedScope] - re-authorization keeps the scope the
170
- * credential already has, whatever the config says now.
171
- */
172
- async execute({ user, entityType, data, step = 1, sessionId, credentialId = null, scope, fixedScope }) {
173
- if (!data || typeof data !== 'object' || Array.isArray(data)) {
174
- throw Boom.badRequest('data must be an object', {
175
- code: 'VALIDATION_ERROR',
176
- details: { field: 'data' },
177
- });
178
- }
179
- const definition = findDefinition(this.moduleDefinitions, entityType);
180
- // `userId` binds a session to the caller and is what GET /authorize
181
- // computes too. The owner of the entity this creates is decided by
182
- // scope (§3.2), not by user.primary, and is `ownership.ownerId`.
183
- const userId = user.getId();
184
- const ownership = this.authorizeEntityForUser.resolveOwnership({
185
- user,
186
- entityType,
187
- scope,
188
- fixedScope,
189
- });
190
-
191
- if (!isMultiStep(definition)) {
192
- if (step !== 1) {
193
- throw Boom.badRequest(`${entityType} has a single authorization step`, {
194
- code: 'INVALID_STEP',
195
- });
196
- }
197
- return this.complete({ ownership, user, entityType, params: data, credentialId });
198
- }
199
-
200
- const totalSteps = getAuthStepCount(definition);
201
- const binding = { userId, entityType, credentialId };
202
- let session;
203
- if (sessionId) {
204
- session = await loadSession(this.sessions, sessionId, binding);
205
- if (step === 1 && session.currentStep !== 1) {
206
- session.restart();
207
- } else if (step !== session.currentStep) {
208
- throw Boom.conflict(`This session expects step ${session.currentStep}`, {
209
- code: 'STEP_OUT_OF_ORDER',
210
- details: { expectedStep: session.currentStep },
211
- });
212
- }
213
- } else {
214
- if (step !== 1) {
215
- throw Boom.badRequest('sessionId is required after step 1', {
216
- code: 'SESSION_REQUIRED',
217
- });
218
- }
219
- session = await startSession(this.sessions, binding, totalSteps);
220
- }
221
-
222
- if (typeof definition.processAuthorizationStep !== 'function') {
223
- throw Boom.badImplementation(
224
- `Module ${entityType} declares ${totalSteps} steps but no processAuthorizationStep`
225
- );
226
- }
227
-
228
- const module = await this.createModule({ userId: ownership.ownerId, definition });
229
- let result;
230
- try {
231
- result = await definition.processAuthorizationStep(
232
- module.api,
233
- step,
234
- data,
235
- session.stepData
236
- );
237
- } catch (error) {
238
- // processAuthorizationStep errors are written for the end user
239
- // (docs/MULTI_STEP_AUTH_MIGRATION_GUIDE.md), e.g. "Invalid code".
240
- throw toAuthorizationError(error, { passMessage: true });
241
- }
242
-
243
- if (result?.completed) {
244
- session.markComplete();
245
- await this.sessions.update(session);
246
- const outcome = await this.complete({
247
- ownership,
248
- user,
249
- entityType,
250
- params: result.authData || {},
251
- credentialId,
252
- });
253
- // The collected data is no longer needed once the credential exists.
254
- await Promise.resolve()
255
- .then(() => this.sessions.deleteBySessionId(session.sessionId))
256
- .catch(() => {});
257
- return outcome;
258
- }
259
-
260
- const nextStep = result?.nextStep ?? step + 1;
261
- try {
262
- session.advanceTo(nextStep, result?.stepData);
263
- } catch (error) {
264
- throw Boom.badImplementation(
265
- `Module ${entityType} returned an invalid next step: ${error.message}`
266
- );
267
- }
268
- await this.sessions.update(session);
269
-
270
- const requirements = await this.getAuthorizationRequirements.execute({
271
- entityType,
272
- step: nextStep,
273
- userId,
274
- });
275
- return {
276
- status: 'pending',
277
- step: nextStep,
278
- totalSteps,
279
- sessionId: session.sessionId,
280
- requirements,
281
- ...(result?.message && { message: String(result.message) }),
282
- };
283
- }
284
-
285
- async complete({ ownership, user, entityType, params, credentialId }) {
286
- let callback;
287
- try {
288
- ({ result: callback } = await this.authorizeEntityForUser.execute({
289
- user,
290
- entityType,
291
- data: params,
292
- ownership,
293
- }));
294
- } catch (error) {
295
- throw toAuthorizationError(error);
296
- }
297
- const [entity, credential] = await Promise.all([
298
- this.moduleRepository.findEntityById(callback.entity_id),
299
- this.credentialRepository.findCredentialById(callback.credential_id),
300
- ]);
301
- // What the new entity is to this caller (§3.4), so a client need not list again.
302
- const tenancy = entity
303
- ? describeRecord(entity, this.scopePolicy.accessContext(user), 'entity')
304
- : null;
305
- const outcome = {
306
- status: 'complete',
307
- entity: entity && tenancy ? { ...entity, tenancy } : entity,
308
- credential: credential && {
309
- ...credential,
310
- type: entityType,
311
- entityIds: entity ? [String(entity.id)] : [],
312
- ...(tenancy && { tenancy }),
313
- },
314
- };
315
- if (credentialId && String(callback.credential_id) !== String(credentialId)) {
316
- // The provider account differs from the one being re-authorized,
317
- // so a separate credential was stored.
318
- outcome.previousCredentialId = String(credentialId);
319
- }
320
- return outcome;
321
- }
322
- }
323
-
324
- module.exports = {
325
- GetAuthorizationStep,
326
- SubmitAuthorizationStep,
327
- toAuthorizationError,
328
- isMultiStep,
329
- };
@@ -1,28 +0,0 @@
1
- -- CreateTable
2
- -- Multi-step authorization state for Management API v2 (/api/v2/authorize).
3
- -- Short-lived rows; "stepData" is encrypted at rest by the application.
4
- CREATE TABLE "AuthorizationSession" (
5
- "id" SERIAL NOT NULL,
6
- "sessionId" TEXT NOT NULL,
7
- "userId" TEXT NOT NULL,
8
- "entityType" TEXT NOT NULL,
9
- "credentialId" TEXT,
10
- "currentStep" INTEGER NOT NULL DEFAULT 1,
11
- "maxSteps" INTEGER NOT NULL,
12
- "stepData" JSONB NOT NULL DEFAULT '{}',
13
- "expiresAt" TIMESTAMP(3) NOT NULL,
14
- "completed" BOOLEAN NOT NULL DEFAULT false,
15
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
16
- "updatedAt" TIMESTAMP(3) NOT NULL,
17
-
18
- CONSTRAINT "AuthorizationSession_pkey" PRIMARY KEY ("id")
19
- );
20
-
21
- -- CreateIndex
22
- CREATE UNIQUE INDEX "AuthorizationSession_sessionId_key" ON "AuthorizationSession"("sessionId");
23
-
24
- -- CreateIndex
25
- CREATE INDEX "AuthorizationSession_userId_entityType_idx" ON "AuthorizationSession"("userId", "entityType");
26
-
27
- -- CreateIndex
28
- CREATE INDEX "AuthorizationSession_expiresAt_idx" ON "AuthorizationSession"("expiresAt");
@@ -1,26 +0,0 @@
1
- -- CreateTable
2
- -- Organization memberships (ADR-056 §3.6). One row per person per organization.
3
- -- "User"."organizationId" stays as the legacy single-org pointer. It is NOT
4
- -- backfilled into this table: the domain (user/membership.js) reads it as an
5
- -- implicit membership when no row exists (see docs/guides/organization-memberships.md).
6
- CREATE TABLE "OrganizationMembership" (
7
- "id" SERIAL NOT NULL,
8
- "individualUserId" INTEGER NOT NULL,
9
- "organizationUserId" INTEGER NOT NULL,
10
- "orgRole" TEXT NOT NULL DEFAULT 'MEMBER',
11
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
12
-
13
- CONSTRAINT "OrganizationMembership_pkey" PRIMARY KEY ("id")
14
- );
15
-
16
- -- CreateIndex
17
- CREATE INDEX "OrganizationMembership_organizationUserId_idx" ON "OrganizationMembership"("organizationUserId");
18
-
19
- -- CreateIndex
20
- CREATE UNIQUE INDEX "OrganizationMembership_individualUserId_organizationUserId_key" ON "OrganizationMembership"("individualUserId", "organizationUserId");
21
-
22
- -- AddForeignKey
23
- ALTER TABLE "OrganizationMembership" ADD CONSTRAINT "OrganizationMembership_individualUserId_fkey" FOREIGN KEY ("individualUserId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
24
-
25
- -- AddForeignKey
26
- ALTER TABLE "OrganizationMembership" ADD CONSTRAINT "OrganizationMembership_organizationUserId_fkey" FOREIGN KEY ("organizationUserId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
@@ -1,25 +0,0 @@
1
- -- Entity and Integration carry the organization they were created in (ADR-056 §3.6).
2
- -- Nullable, no default and no backfill: a row written before this migration keeps
3
- -- NULL, and the domain (modules/entity-scope.js) reads NULL as "predates scoping"
4
- -- (docs/guides/entity-scopes.md). No scope column: the owner's user type says it.
5
- -- Deliberately no foreign key: a deleted organization must leave its rows
6
- -- invisible, and ON DELETE SET NULL would turn them into unscoped ones.
7
- --
8
- -- Release note: the two CREATE INDEX statements below are plain (not
9
- -- CONCURRENTLY, which Prisma migrations cannot run inside a transaction). Each
10
- -- takes a SHARE lock on its table while it builds, so writes to "Entity" and
11
- -- "Integration" wait for it. Both tables are small in a typical Frigg
12
- -- deployment; on a very large one, create the indexes by hand first with
13
- -- CREATE INDEX CONCURRENTLY IF NOT EXISTS under the same names, then deploy.
14
-
15
- -- AlterTable
16
- ALTER TABLE "Entity" ADD COLUMN "organizationId" INTEGER;
17
-
18
- -- AlterTable
19
- ALTER TABLE "Integration" ADD COLUMN "organizationId" INTEGER;
20
-
21
- -- CreateIndex
22
- CREATE INDEX "Entity_userId_organizationId_idx" ON "Entity"("userId", "organizationId");
23
-
24
- -- CreateIndex
25
- CREATE INDEX "Integration_userId_organizationId_idx" ON "Integration"("userId", "organizationId");
@@ -1,28 +0,0 @@
1
- -- One-time scheduled jobs for the poll-and-dispatch scheduler
2
- -- (NetlifySchedulerAdapter, friggframework/frigg#545): a cron function reads
3
- -- PENDING rows that are due and dispatches them to a queue.
4
- --
5
- -- IF NOT EXISTS on purpose: #545 shipped this model in the Prisma schema with
6
- -- no migration, so an app that ran its canary may already have the table from
7
- -- `prisma db push`. On such a database this migration is a no-op; on a fresh
8
- -- one it is the SQL `prisma migrate diff` generates.
9
-
10
- -- CreateTable
11
- CREATE TABLE IF NOT EXISTS "ScheduledJob" (
12
- "id" SERIAL NOT NULL,
13
- "scheduleName" TEXT NOT NULL,
14
- "scheduledAt" TIMESTAMP(3) NOT NULL,
15
- "queueResourceId" TEXT NOT NULL,
16
- "payload" JSONB,
17
- "state" TEXT NOT NULL DEFAULT 'PENDING',
18
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
19
- "updatedAt" TIMESTAMP(3) NOT NULL,
20
-
21
- CONSTRAINT "ScheduledJob_pkey" PRIMARY KEY ("id")
22
- );
23
-
24
- -- CreateIndex
25
- CREATE UNIQUE INDEX IF NOT EXISTS "ScheduledJob_scheduleName_key" ON "ScheduledJob"("scheduleName");
26
-
27
- -- CreateIndex
28
- CREATE INDEX IF NOT EXISTS "ScheduledJob_state_scheduledAt_idx" ON "ScheduledJob"("state", "scheduledAt");
@@ -1,23 +0,0 @@
1
- const {
2
- resolveProvider,
3
- determineProviderName,
4
- providerPackageName,
5
- KNOWN_PROVIDERS,
6
- } = require('./resolve-provider');
7
- const {
8
- isDeployedRuntime,
9
- isAwsLambdaRuntime,
10
- isLocalRun,
11
- resolveActiveProvider,
12
- } = require('./runtime-detection');
13
-
14
- module.exports = {
15
- resolveProvider,
16
- determineProviderName,
17
- providerPackageName,
18
- KNOWN_PROVIDERS,
19
- isDeployedRuntime,
20
- isAwsLambdaRuntime,
21
- isLocalRun,
22
- resolveActiveProvider,
23
- };
@@ -1,93 +0,0 @@
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
- };
@@ -1,124 +0,0 @@
1
- /**
2
- * Deployed-runtime detection, provider-aware.
3
- *
4
- * "Deployed" decides whether a missing secret may be tolerated: a deployed
5
- * function must fail closed whatever STAGE says, while a laptop, a test run or
6
- * a local emulator may run in development mode. Reading only AWS Lambda's
7
- * variables (AWS_LAMBDA_FUNCTION_NAME, LAMBDA_TASK_ROOT, AWS_EXECUTION_ENV)
8
- * makes every other host look local: a Netlify Function with STAGE=dev would
9
- * be treated like `frigg start`.
10
- *
11
- * So the active provider is asked as well. The provider is the app
12
- * definition's `provider` or FRIGG_PROVIDER (which provider-netlify's
13
- * generated function entry points set to 'netlify'), resolved to its
14
- * @friggframework/provider-<name> package, and its `detect(env)` says whether
15
- * the process runs on that platform.
16
- *
17
- * A local run is never deployed: serverless-offline / `serverless invoke
18
- * local` (IS_OFFLINE, IS_LOCAL), `netlify dev` (NETLIFY_DEV=true) and Jest
19
- * (JEST_WORKER_ID).
20
- */
21
- const { resolveProvider } = require('./resolve-provider');
22
-
23
- function isSet(value) {
24
- return typeof value === 'string' && value.trim() !== '';
25
- }
26
-
27
- function isTrue(value) {
28
- if (typeof value !== 'string') return false;
29
- const normalized = value.trim().toLowerCase();
30
- return normalized === 'true' || normalized === '1';
31
- }
32
-
33
- /**
34
- * @param {Object} [env=process.env]
35
- * @returns {boolean} true under a local emulator or test runner
36
- */
37
- function isLocalRun(env = process.env) {
38
- return (
39
- isTrue(env.IS_OFFLINE) ||
40
- isTrue(env.IS_LOCAL) ||
41
- isTrue(env.NETLIFY_DEV) ||
42
- isSet(env.JEST_WORKER_ID)
43
- );
44
- }
45
-
46
- /**
47
- * The AWS Lambda runtime sets AWS_LAMBDA_FUNCTION_NAME, LAMBDA_TASK_ROOT and
48
- * AWS_EXECUTION_ENV (AWS_Lambda_<runtime>).
49
- *
50
- * @param {Object} [env=process.env]
51
- * @returns {boolean}
52
- */
53
- function isAwsLambdaRuntime(env = process.env) {
54
- return (
55
- isSet(env.AWS_LAMBDA_FUNCTION_NAME) ||
56
- isSet(env.LAMBDA_TASK_ROOT) ||
57
- String(env.AWS_EXECUTION_ENV ?? '').startsWith('AWS_Lambda_')
58
- );
59
- }
60
-
61
- /**
62
- * The provider plugin of the running app, or null for AWS (covered by
63
- * isAwsLambdaRuntime) or when no provider is named or its package is absent.
64
- *
65
- * @param {Object} [env=process.env]
66
- * @param {Object} [options]
67
- * @param {Object} [options.appDefinition]
68
- * @param {Function} [options.resolve=resolveProvider]
69
- * @returns {Object|null}
70
- */
71
- function resolveActiveProvider(env = process.env, { appDefinition, resolve = resolveProvider } = {}) {
72
- const name = appDefinition?.provider || env.FRIGG_PROVIDER;
73
- if (!isSet(name) || name === 'aws') return null;
74
- try {
75
- return resolve(null, { provider: name });
76
- } catch (_) {
77
- return null;
78
- }
79
- }
80
-
81
- /**
82
- * True when the process runs as a deployed function on AWS Lambda or on the
83
- * active provider's platform, and not under a local emulator or test runner.
84
- *
85
- * @param {Object} [env=process.env]
86
- * @param {Object} [options]
87
- * @param {Object} [options.provider] - provider plugin to ask (default: the active one)
88
- * @param {Object} [options.appDefinition] - reads `provider`
89
- * @returns {boolean}
90
- */
91
- function isDeployedRuntime(env = process.env, { provider, appDefinition } = {}) {
92
- if (isLocalRun(env)) return false;
93
- if (isAwsLambdaRuntime(env)) return true;
94
- if (provider === undefined) {
95
- // A named non-AWS provider whose package cannot be loaded (or has no
96
- // detect()) cannot say where we run. Fail closed: provider-netlify's
97
- // generated entry points set FRIGG_PROVIDER, so treating that as local
98
- // would let a deployed function accept unsigned queue messages.
99
- const name = appDefinition?.provider || env.FRIGG_PROVIDER;
100
- if (isSet(name) && name !== 'aws') {
101
- const active = resolveActiveProvider(env, { appDefinition });
102
- if (!active || typeof active.detect !== 'function') return true;
103
- return askProvider(active, env, true);
104
- }
105
- return false;
106
- }
107
- if (!provider || typeof provider.detect !== 'function') return false;
108
- return askProvider(provider, env, false);
109
- }
110
-
111
- function askProvider(active, env, onError) {
112
- try {
113
- return active.detect(env) === true;
114
- } catch (_) {
115
- return onError;
116
- }
117
- }
118
-
119
- module.exports = {
120
- isDeployedRuntime,
121
- isAwsLambdaRuntime,
122
- isLocalRun,
123
- resolveActiveProvider,
124
- };