@friggframework/core 2.0.0--canary.690.08cd07e.0 → 2.0.0--canary.693.d8859c2.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 (261) 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 +0 -2
  15. package/database/encryption/encryption-schema-registry.js +12 -93
  16. package/database/prisma.js +2 -4
  17. package/errors/index.js +4 -0
  18. package/errors/mapping-already-exists-error.js +14 -0
  19. package/generated/prisma-mongodb/edge.js +38 -33
  20. package/generated/prisma-mongodb/index-browser.js +35 -30
  21. package/generated/prisma-mongodb/index.d.ts +1880 -3191
  22. package/generated/prisma-mongodb/index.js +38 -33
  23. package/generated/prisma-mongodb/package.json +1 -1
  24. package/generated/prisma-mongodb/schema.prisma +50 -86
  25. package/generated/prisma-mongodb/wasm.js +38 -33
  26. package/generated/prisma-postgresql/edge.js +38 -33
  27. package/generated/prisma-postgresql/index-browser.js +35 -30
  28. package/generated/prisma-postgresql/index.d.ts +2050 -3390
  29. package/generated/prisma-postgresql/index.js +38 -33
  30. package/generated/prisma-postgresql/package.json +1 -1
  31. package/generated/prisma-postgresql/schema.prisma +48 -81
  32. package/generated/prisma-postgresql/wasm.js +38 -33
  33. package/handlers/WEBHOOKS.md +29 -0
  34. package/handlers/app-definition-loader.js +19 -128
  35. package/handlers/app-handler-helpers.js +3 -25
  36. package/handlers/backend-utils.js +25 -31
  37. package/handlers/middleware/admin-auth.js +1 -14
  38. package/handlers/routers/auth.js +10 -46
  39. package/handlers/routers/health.js +4 -13
  40. package/handlers/routers/integration-defined-routers.js +92 -144
  41. package/handlers/routers/integration-webhook-routers.js +64 -102
  42. package/handlers/workers/integration-defined-workers.js +2 -5
  43. package/index.js +3 -65
  44. package/infrastructure/scheduler/index.js +6 -19
  45. package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
  46. package/integrations/data-defined/README.md +169 -0
  47. package/integrations/data-defined/canonical.js +11 -0
  48. package/integrations/data-defined/capabilities/acmeerp.capabilities.json +608 -0
  49. package/integrations/data-defined/capabilities/asana.capabilities.json +265 -0
  50. package/integrations/data-defined/capabilities/calendly.capabilities.json +385 -0
  51. package/integrations/data-defined/capabilities/docusign.capabilities.json +314 -0
  52. package/integrations/data-defined/capabilities/fathom.capabilities.json +228 -0
  53. package/integrations/data-defined/capabilities/github.capabilities.json +563 -0
  54. package/integrations/data-defined/capabilities/hubspot.capabilities.json +715 -0
  55. package/integrations/data-defined/capabilities/jira.capabilities.json +376 -0
  56. package/integrations/data-defined/capabilities/klaviyo.capabilities.json +475 -0
  57. package/integrations/data-defined/capabilities/linear.capabilities.json +324 -0
  58. package/integrations/data-defined/capabilities/netsuite.capabilities.json +491 -0
  59. package/integrations/data-defined/capabilities/notion.capabilities.json +208 -0
  60. package/integrations/data-defined/capabilities/phone.capabilities.json +470 -0
  61. package/integrations/data-defined/capabilities/pipedrive.capabilities.json +388 -0
  62. package/integrations/data-defined/capabilities/quickbooks.capabilities.json +615 -0
  63. package/integrations/data-defined/capabilities/salesforce.capabilities.json +507 -0
  64. package/integrations/data-defined/capabilities/shopify.capabilities.json +1082 -0
  65. package/integrations/data-defined/capabilities/slack.capabilities.json +363 -0
  66. package/integrations/data-defined/capabilities/stripe.capabilities.json +580 -0
  67. package/integrations/data-defined/capabilities/webflow.capabilities.json +196 -0
  68. package/integrations/data-defined/capabilities/zendesk.capabilities.json +409 -0
  69. package/integrations/data-defined/compile.js +77 -0
  70. package/integrations/data-defined/config-form.js +110 -0
  71. package/integrations/data-defined/data-defined-integration.js +366 -0
  72. package/integrations/data-defined/examples/acme-erp-quickbooks.definition.json +467 -0
  73. package/integrations/data-defined/examples/asana-slack.definition.json +220 -0
  74. package/integrations/data-defined/examples/calendly-salesforce.definition.json +459 -0
  75. package/integrations/data-defined/examples/docusign-salesforce.definition.json +305 -0
  76. package/integrations/data-defined/examples/fathom-hubspot-notion.definition.json +575 -0
  77. package/integrations/data-defined/examples/github-linear.definition.json +316 -0
  78. package/integrations/data-defined/examples/hubspot-slack.definition.json +409 -0
  79. package/integrations/data-defined/examples/klaviyo-hubspot.definition.json +339 -0
  80. package/integrations/data-defined/examples/phone-hubspot.definition.json +637 -0
  81. package/integrations/data-defined/examples/pipedrive-hubspot.definition.json +136 -0
  82. package/integrations/data-defined/examples/pipedrive-phone.definition.json +340 -0
  83. package/integrations/data-defined/examples/records/phone-hubspot.definition-version.json +652 -0
  84. package/integrations/data-defined/examples/records/phone-hubspot.integration.json +86 -0
  85. package/integrations/data-defined/examples/records/phone-hubspot.mappings.json +39 -0
  86. package/integrations/data-defined/examples/records/phone-hubspot.workflow-run.json +62 -0
  87. package/integrations/data-defined/examples/shopify-klaviyo.definition.json +488 -0
  88. package/integrations/data-defined/examples/shopify-netsuite.definition.json +622 -0
  89. package/integrations/data-defined/examples/stripe-quickbooks.definition.json +476 -0
  90. package/integrations/data-defined/examples/webflow-hubspot-slack.definition.json +262 -0
  91. package/integrations/data-defined/examples/zendesk-jira.definition.json +604 -0
  92. package/integrations/data-defined/expressions.js +225 -0
  93. package/integrations/data-defined/index.js +39 -0
  94. package/integrations/data-defined/interpreter.js +89 -0
  95. package/integrations/data-defined/manifests.js +125 -0
  96. package/integrations/data-defined/repositories/definition-version-repository.js +67 -0
  97. package/integrations/data-defined/repositories/workflow-run-repository.js +59 -0
  98. package/integrations/data-defined/router.js +97 -0
  99. package/integrations/data-defined/runtime.js +208 -0
  100. package/integrations/data-defined/schema/capability-manifest.v0.1.schema.json +377 -0
  101. package/integrations/data-defined/schema/installation.v0.1.schema.json +83 -0
  102. package/integrations/data-defined/schema/integration-definition.v0.1.schema.json +848 -0
  103. package/integrations/data-defined/schema/records.md +98 -0
  104. package/integrations/data-defined/schema-check.js +58 -0
  105. package/integrations/data-defined/scripts/validate.js +66 -0
  106. package/integrations/data-defined/steps.js +89 -0
  107. package/integrations/data-defined/transforms.js +91 -0
  108. package/integrations/data-defined/use-cases/definition-use-cases.js +187 -0
  109. package/integrations/data-defined/validate.js +370 -0
  110. package/integrations/data-defined/versioning.js +161 -0
  111. package/integrations/data-defined/webhooks.js +119 -0
  112. package/integrations/index.js +0 -1
  113. package/integrations/integration-base.js +29 -0
  114. package/integrations/integration-router.js +101 -131
  115. package/integrations/repositories/integration-mapping-repository-documentdb.js +50 -0
  116. package/integrations/repositories/integration-mapping-repository-interface.js +15 -0
  117. package/integrations/repositories/integration-mapping-repository-mongo.js +29 -0
  118. package/integrations/repositories/integration-mapping-repository-postgres.js +30 -0
  119. package/integrations/repositories/integration-mapping-repository.js +29 -0
  120. package/integrations/repositories/integration-repository-documentdb.js +3 -35
  121. package/integrations/repositories/integration-repository-interface.js +1 -17
  122. package/integrations/repositories/integration-repository-mongo.js +77 -60
  123. package/integrations/repositories/integration-repository-postgres.js +76 -62
  124. package/integrations/repositories/process-repository-mongo.js +1 -1
  125. package/integrations/tests/doubles/test-integration-repository.js +1 -12
  126. package/integrations/use-cases/create-integration.js +20 -55
  127. package/integrations/use-cases/get-integrations-for-user.js +40 -80
  128. package/integrations/use-cases/index.js +1 -7
  129. package/integrations/utils/map-integration-dto.js +14 -43
  130. package/modules/index.js +0 -14
  131. package/modules/module-factory.js +0 -12
  132. package/modules/module.js +2 -53
  133. package/modules/repositories/module-repository-documentdb.js +6 -51
  134. package/modules/repositories/module-repository-interface.js +2 -19
  135. package/modules/repositories/module-repository-mongo.js +74 -67
  136. package/modules/repositories/module-repository-postgres.js +74 -66
  137. package/modules/repositories/module-repository.js +1 -2
  138. package/modules/requester/requester.js +5 -35
  139. package/modules/use-cases/get-entities-for-user.js +13 -26
  140. package/modules/use-cases/get-entity-options-by-id.js +4 -10
  141. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  142. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  143. package/modules/use-cases/get-module.js +4 -10
  144. package/modules/use-cases/process-authorization-callback.js +19 -53
  145. package/modules/use-cases/refresh-entity-options.js +4 -10
  146. package/modules/use-cases/test-module-auth.js +4 -10
  147. package/package.json +6 -6
  148. package/prisma-mongodb/schema.prisma +50 -86
  149. package/prisma-postgresql/migrations/20261018000000_add_data_defined_integrations/migration.sql +66 -0
  150. package/prisma-postgresql/schema.prisma +48 -81
  151. package/queues/index.js +0 -25
  152. package/types/index.d.ts +0 -2
  153. package/user/repositories/user-repository-documentdb.js +2 -27
  154. package/user/repositories/user-repository-interface.js +0 -18
  155. package/user/repositories/user-repository-mongo.js +0 -24
  156. package/user/repositories/user-repository-postgres.js +0 -18
  157. package/user/use-cases/authenticate-user.js +1 -7
  158. package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
  159. package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
  160. package/user/user.js +1 -64
  161. package/credential/use-cases/credential-operations-for-user.js +0 -202
  162. package/credential/use-cases/reauthorize-credential-for-user.js +0 -73
  163. package/database/organization-where.js +0 -18
  164. package/database/unique-violation.js +0 -25
  165. package/extensions/bootstrap-runner.js +0 -43
  166. package/extensions/extension-loader.js +0 -187
  167. package/extensions/index.js +0 -41
  168. package/extensions/initialize-app.js +0 -123
  169. package/extensions/route-mounter.js +0 -103
  170. package/extensions/schema-composer.js +0 -188
  171. package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -216
  172. package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
  173. package/integrations/repositories/integration-not-found.js +0 -14
  174. package/integrations/schedule-config.js +0 -80
  175. package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
  176. package/integrations/use-cases/integration-operations-for-user.js +0 -256
  177. package/integrations/use-cases/resolve-integration-ownership.js +0 -177
  178. package/management-api/create-management-api-router.js +0 -49
  179. package/management-api/error-response.js +0 -116
  180. package/management-api/management-api-config.js +0 -151
  181. package/management-api/meta-router.js +0 -76
  182. package/management-api/openapi/build-openapi.js +0 -144
  183. package/management-api/openapi/components.js +0 -415
  184. package/management-api/openapi/v1-operations.js +0 -83
  185. package/management-api/openapi/v2-operations.js +0 -264
  186. package/management-api/route-registry.js +0 -411
  187. package/management-api/test-support/http-client.js +0 -39
  188. package/management-api/use-cases/get-management-api-meta.js +0 -57
  189. package/management-api/v1-deprecation.js +0 -120
  190. package/management-api/v2/create-v2-router.js +0 -85
  191. package/management-api/v2/dependencies.js +0 -281
  192. package/management-api/v2/dto.js +0 -143
  193. package/management-api/v2/handlers/authorization.js +0 -94
  194. package/management-api/v2/handlers/credentials.js +0 -33
  195. package/management-api/v2/handlers/entities.js +0 -77
  196. package/management-api/v2/handlers/integrations.js +0 -128
  197. package/management-api/v2/handlers/proxy.js +0 -34
  198. package/management-api/v2/request.js +0 -85
  199. package/management-api/v2/test-support/fake-dependencies.js +0 -103
  200. package/modules/domain/authorization-session.js +0 -144
  201. package/modules/entity-scope.js +0 -15
  202. package/modules/entity-type-registry.js +0 -311
  203. package/modules/proxy/proxy-bases.js +0 -169
  204. package/modules/proxy/proxy-errors.js +0 -31
  205. package/modules/proxy/proxy-path.js +0 -183
  206. package/modules/proxy/proxy-policy.js +0 -277
  207. package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
  208. package/modules/repositories/authorization-session-repository-factory.js +0 -31
  209. package/modules/repositories/authorization-session-repository-interface.js +0 -47
  210. package/modules/repositories/authorization-session-repository-prisma.js +0 -87
  211. package/modules/repositories/entity-not-found.js +0 -18
  212. package/modules/requester/proxy-request.js +0 -245
  213. package/modules/scope-config.js +0 -192
  214. package/modules/scope-policy.js +0 -112
  215. package/modules/scope-resolution.js +0 -288
  216. package/modules/scope-visibility.js +0 -247
  217. package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
  218. package/modules/tests/doubles/ownership-matching.js +0 -18
  219. package/modules/use-cases/authorize-entity-for-user.js +0 -61
  220. package/modules/use-cases/create-entity-from-credential.js +0 -113
  221. package/modules/use-cases/entity-operations-for-user.js +0 -162
  222. package/modules/use-cases/entity-types.js +0 -139
  223. package/modules/use-cases/execute-entity-proxy-request.js +0 -331
  224. package/modules/use-cases/get-authorization-requirements.js +0 -100
  225. package/modules/use-cases/get-entity-module-for-user.js +0 -113
  226. package/modules/use-cases/manage-access.js +0 -21
  227. package/modules/use-cases/multi-step-authorization.js +0 -329
  228. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
  229. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
  230. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
  231. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
  232. package/providers/index.js +0 -23
  233. package/providers/resolve-provider.js +0 -93
  234. package/providers/runtime-detection.js +0 -124
  235. package/queues/providers/index.js +0 -11
  236. package/queues/providers/netlify-background-provider.js +0 -150
  237. package/queues/providers/qstash-queue-provider.js +0 -219
  238. package/queues/providers/sqs-queue-provider.js +0 -70
  239. package/queues/queue-client-interface.js +0 -55
  240. package/queues/queue-message-auth.js +0 -193
  241. package/queues/queue-provider-factory.js +0 -101
  242. package/queues/queue-provider.js +0 -65
  243. package/queues/sqs-queue-client.js +0 -81
  244. package/types/modules/index.d.ts +0 -214
  245. package/types/user/index.d.ts +0 -134
  246. package/user/adopter-jwt-config.js +0 -177
  247. package/user/adopter-jwt-errors.js +0 -59
  248. package/user/adopter-token-verifier.js +0 -122
  249. package/user/app-user-helpers.js +0 -97
  250. package/user/create-user-authentication.js +0 -64
  251. package/user/membership.js +0 -51
  252. package/user/repositories/organization-membership-record.js +0 -19
  253. package/user/repositories/organization-membership-repository-documentdb.js +0 -106
  254. package/user/repositories/organization-membership-repository-factory.js +0 -42
  255. package/user/repositories/organization-membership-repository-interface.js +0 -57
  256. package/user/repositories/organization-membership-repository-mongo.js +0 -13
  257. package/user/repositories/organization-membership-repository-postgres.js +0 -29
  258. package/user/repositories/organization-membership-repository-prisma.js +0 -81
  259. package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
  260. package/user/use-cases/ensure-organization-membership.js +0 -92
  261. package/user/use-cases/find-individual-users-by-organization.js +0 -54
@@ -1,21 +0,0 @@
1
- const Boom = require('@hapi/boom');
2
- const { ACCESS } = require('../scope-resolution');
3
-
4
- /**
5
- * The one check behind every "may this caller change or delete it" gate
6
- * (ADR-056 §3 permissions): the caller may use it but not manage it, so 403
7
- * `ORG_ADMIN_REQUIRED`. `access` comes from describeRecord (entities,
8
- * integrations) or scopePolicy.accessToOwnedBy (credentials).
9
- *
10
- * @param {string|null} access - ACCESS value
11
- * @param {string} subject - e.g. `entity 5`, for the message
12
- */
13
- function assertCanManage(access, subject) {
14
- if (access !== ACCESS.MANAGE) {
15
- throw Boom.forbidden(`You may use ${subject} but not change or delete it`, {
16
- code: 'ORG_ADMIN_REQUIRED',
17
- });
18
- }
19
- }
20
-
21
- module.exports = { assertCanManage };
@@ -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
- };