@friggframework/core 2.0.0--canary.690.d043f23.0 → 2.0.0--canary.691.2fd013b.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 (193) 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 +4 -43
  20. package/generated/prisma-mongodb/index-browser.js +1 -40
  21. package/generated/prisma-mongodb/index.d.ts +111 -4739
  22. package/generated/prisma-mongodb/index.js +4 -43
  23. package/generated/prisma-mongodb/package.json +1 -1
  24. package/generated/prisma-mongodb/schema.prisma +0 -92
  25. package/generated/prisma-mongodb/wasm.js +4 -43
  26. package/generated/prisma-postgresql/edge.js +4 -43
  27. package/generated/prisma-postgresql/index-browser.js +1 -40
  28. package/generated/prisma-postgresql/index.d.ts +33 -4966
  29. package/generated/prisma-postgresql/index.js +4 -43
  30. package/generated/prisma-postgresql/package.json +1 -1
  31. package/generated/prisma-postgresql/schema.prisma +0 -87
  32. package/generated/prisma-postgresql/wasm.js +4 -43
  33. package/handlers/WEBHOOKS.md +29 -0
  34. package/handlers/app-definition-loader.js +19 -118
  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 -11
  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 -63
  44. package/infrastructure/scheduler/index.js +6 -19
  45. package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
  46. package/integrations/index.js +0 -1
  47. package/integrations/integration-base.js +29 -0
  48. package/integrations/integration-router.js +96 -131
  49. package/integrations/repositories/integration-mapping-repository-documentdb.js +50 -0
  50. package/integrations/repositories/integration-mapping-repository-interface.js +15 -0
  51. package/integrations/repositories/integration-mapping-repository-mongo.js +29 -0
  52. package/integrations/repositories/integration-mapping-repository-postgres.js +30 -0
  53. package/integrations/repositories/integration-mapping-repository.js +29 -0
  54. package/integrations/repositories/integration-repository-documentdb.js +3 -35
  55. package/integrations/repositories/integration-repository-interface.js +1 -17
  56. package/integrations/repositories/integration-repository-mongo.js +77 -60
  57. package/integrations/repositories/integration-repository-postgres.js +76 -62
  58. package/integrations/repositories/process-repository-mongo.js +1 -1
  59. package/integrations/tests/doubles/test-integration-repository.js +1 -12
  60. package/integrations/use-cases/create-integration.js +20 -55
  61. package/integrations/use-cases/get-integrations-for-user.js +40 -80
  62. package/integrations/use-cases/index.js +1 -5
  63. package/integrations/utils/map-integration-dto.js +14 -43
  64. package/modules/index.js +0 -14
  65. package/modules/module-factory.js +0 -12
  66. package/modules/module.js +2 -53
  67. package/modules/repositories/module-repository-documentdb.js +6 -51
  68. package/modules/repositories/module-repository-interface.js +2 -19
  69. package/modules/repositories/module-repository-mongo.js +74 -67
  70. package/modules/repositories/module-repository-postgres.js +74 -66
  71. package/modules/repositories/module-repository.js +1 -2
  72. package/modules/requester/requester.js +2 -27
  73. package/modules/use-cases/get-entities-for-user.js +13 -26
  74. package/modules/use-cases/get-entity-options-by-id.js +4 -10
  75. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  76. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  77. package/modules/use-cases/get-module.js +4 -10
  78. package/modules/use-cases/process-authorization-callback.js +19 -53
  79. package/modules/use-cases/refresh-entity-options.js +4 -10
  80. package/modules/use-cases/test-module-auth.js +4 -10
  81. package/package.json +5 -6
  82. package/prisma-mongodb/schema.prisma +0 -92
  83. package/prisma-postgresql/schema.prisma +0 -87
  84. package/queues/index.js +0 -25
  85. package/types/index.d.ts +0 -2
  86. package/user/repositories/user-repository-documentdb.js +2 -27
  87. package/user/repositories/user-repository-interface.js +0 -18
  88. package/user/repositories/user-repository-mongo.js +0 -24
  89. package/user/repositories/user-repository-postgres.js +0 -18
  90. package/user/tests/doubles/test-user-repository.js +1 -1
  91. package/user/use-cases/authenticate-user.js +1 -7
  92. package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
  93. package/user/use-cases/get-user-from-bearer-token.js +7 -2
  94. package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
  95. package/user/user.js +1 -64
  96. package/credential/use-cases/credential-operations-for-user.js +0 -202
  97. package/credential/use-cases/reauthorize-credential-for-user.js +0 -73
  98. package/database/organization-where.js +0 -18
  99. package/database/unique-violation.js +0 -25
  100. package/extensions/bootstrap-runner.js +0 -43
  101. package/extensions/extension-loader.js +0 -167
  102. package/extensions/index.js +0 -41
  103. package/extensions/initialize-app.js +0 -123
  104. package/extensions/route-mounter.js +0 -103
  105. package/extensions/schema-composer.js +0 -188
  106. package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -216
  107. package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
  108. package/integrations/repositories/integration-not-found.js +0 -14
  109. package/integrations/use-cases/integration-operations-for-user.js +0 -256
  110. package/integrations/use-cases/resolve-integration-ownership.js +0 -177
  111. package/management-api/create-management-api-router.js +0 -49
  112. package/management-api/error-response.js +0 -116
  113. package/management-api/management-api-config.js +0 -151
  114. package/management-api/meta-router.js +0 -57
  115. package/management-api/openapi/build-openapi.js +0 -144
  116. package/management-api/openapi/components.js +0 -414
  117. package/management-api/openapi/v1-operations.js +0 -83
  118. package/management-api/openapi/v2-operations.js +0 -264
  119. package/management-api/route-registry.js +0 -294
  120. package/management-api/test-support/http-client.js +0 -39
  121. package/management-api/use-cases/get-management-api-meta.js +0 -57
  122. package/management-api/v1-deprecation.js +0 -120
  123. package/management-api/v2/create-v2-router.js +0 -85
  124. package/management-api/v2/dependencies.js +0 -281
  125. package/management-api/v2/dto.js +0 -143
  126. package/management-api/v2/handlers/authorization.js +0 -94
  127. package/management-api/v2/handlers/credentials.js +0 -33
  128. package/management-api/v2/handlers/entities.js +0 -77
  129. package/management-api/v2/handlers/integrations.js +0 -128
  130. package/management-api/v2/handlers/proxy.js +0 -34
  131. package/management-api/v2/request.js +0 -85
  132. package/management-api/v2/test-support/fake-dependencies.js +0 -103
  133. package/modules/domain/authorization-session.js +0 -144
  134. package/modules/entity-scope.js +0 -15
  135. package/modules/entity-type-registry.js +0 -311
  136. package/modules/proxy/proxy-bases.js +0 -169
  137. package/modules/proxy/proxy-errors.js +0 -31
  138. package/modules/proxy/proxy-path.js +0 -183
  139. package/modules/proxy/proxy-policy.js +0 -265
  140. package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
  141. package/modules/repositories/authorization-session-repository-factory.js +0 -31
  142. package/modules/repositories/authorization-session-repository-interface.js +0 -47
  143. package/modules/repositories/authorization-session-repository-prisma.js +0 -87
  144. package/modules/repositories/entity-not-found.js +0 -18
  145. package/modules/requester/proxy-request.js +0 -245
  146. package/modules/scope-config.js +0 -192
  147. package/modules/scope-policy.js +0 -112
  148. package/modules/scope-resolution.js +0 -288
  149. package/modules/scope-visibility.js +0 -247
  150. package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
  151. package/modules/tests/doubles/ownership-matching.js +0 -18
  152. package/modules/use-cases/authorize-entity-for-user.js +0 -61
  153. package/modules/use-cases/create-entity-from-credential.js +0 -113
  154. package/modules/use-cases/entity-operations-for-user.js +0 -162
  155. package/modules/use-cases/entity-types.js +0 -139
  156. package/modules/use-cases/execute-entity-proxy-request.js +0 -331
  157. package/modules/use-cases/get-authorization-requirements.js +0 -100
  158. package/modules/use-cases/get-entity-module-for-user.js +0 -113
  159. package/modules/use-cases/manage-access.js +0 -21
  160. package/modules/use-cases/multi-step-authorization.js +0 -329
  161. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
  162. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
  163. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
  164. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
  165. package/providers/index.js +0 -13
  166. package/providers/resolve-provider.js +0 -93
  167. package/queues/providers/index.js +0 -11
  168. package/queues/providers/netlify-background-provider.js +0 -150
  169. package/queues/providers/qstash-queue-provider.js +0 -199
  170. package/queues/providers/sqs-queue-provider.js +0 -70
  171. package/queues/queue-client-interface.js +0 -55
  172. package/queues/queue-message-auth.js +0 -187
  173. package/queues/queue-provider-factory.js +0 -101
  174. package/queues/queue-provider.js +0 -65
  175. package/queues/sqs-queue-client.js +0 -81
  176. package/types/modules/index.d.ts +0 -214
  177. package/types/user/index.d.ts +0 -134
  178. package/user/adopter-jwt-config.js +0 -177
  179. package/user/adopter-jwt-errors.js +0 -59
  180. package/user/adopter-token-verifier.js +0 -122
  181. package/user/app-user-helpers.js +0 -97
  182. package/user/create-user-authentication.js +0 -64
  183. package/user/membership.js +0 -51
  184. package/user/repositories/organization-membership-record.js +0 -19
  185. package/user/repositories/organization-membership-repository-documentdb.js +0 -106
  186. package/user/repositories/organization-membership-repository-factory.js +0 -42
  187. package/user/repositories/organization-membership-repository-interface.js +0 -57
  188. package/user/repositories/organization-membership-repository-mongo.js +0 -13
  189. package/user/repositories/organization-membership-repository-postgres.js +0 -29
  190. package/user/repositories/organization-membership-repository-prisma.js +0 -81
  191. package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
  192. package/user/use-cases/ensure-organization-membership.js +0 -92
  193. package/user/use-cases/find-individual-users-by-organization.js +0 -54
@@ -1,144 +0,0 @@
1
- const crypto = require('node:crypto');
2
-
3
- const DEFAULT_TTL_MINUTES = 15;
4
-
5
- function sessionTtlMs() {
6
- const minutes = parseInt(process.env.AUTH_SESSION_EXPIRY_MINUTES || '', 10);
7
- return (Number.isInteger(minutes) && minutes > 0 ? minutes : DEFAULT_TTL_MINUTES) * 60 * 1000;
8
- }
9
-
10
- /**
11
- * State of one multi-step authorization (Management API v2
12
- * `/api/v2/authorize` and `/api/v2/credentials/:id/reauthorize`).
13
- *
14
- * `stepData` holds what earlier steps collected (for example the email an OTP
15
- * was sent to) and is encrypted at rest (encryption-schema-registry). A
16
- * session is bound to the user who started it, the entity type, and, for a
17
- * re-authorization, the credential being renewed.
18
- */
19
- class AuthorizationSession {
20
- /**
21
- * @param {Object} params
22
- * @param {string} params.sessionId - unguessable id (UUID v4)
23
- * @param {string} params.userId
24
- * @param {string} params.entityType - module name
25
- * @param {string|null} [params.credentialId] - set for re-authorization
26
- * @param {number} [params.currentStep=1] - the step the caller submits next
27
- * @param {number} params.maxSteps
28
- * @param {Object} [params.stepData={}]
29
- * @param {Date} params.expiresAt
30
- * @param {boolean} [params.completed=false]
31
- * @param {Date} [params.createdAt]
32
- * @param {Date} [params.updatedAt]
33
- */
34
- constructor({
35
- sessionId,
36
- userId,
37
- entityType,
38
- credentialId = null,
39
- currentStep = 1,
40
- maxSteps,
41
- stepData = {},
42
- expiresAt,
43
- completed = false,
44
- createdAt = new Date(),
45
- updatedAt = new Date(),
46
- }) {
47
- this.sessionId = sessionId;
48
- this.userId = userId === undefined || userId === null ? userId : String(userId);
49
- this.entityType = entityType;
50
- this.credentialId =
51
- credentialId === undefined || credentialId === null ? null : String(credentialId);
52
- this.currentStep = currentStep;
53
- this.maxSteps = maxSteps;
54
- this.stepData = stepData && typeof stepData === 'object' ? stepData : {};
55
- this.expiresAt = expiresAt instanceof Date ? expiresAt : new Date(expiresAt);
56
- this.completed = completed;
57
- this.createdAt = createdAt;
58
- this.updatedAt = updatedAt;
59
- this.validate();
60
- }
61
-
62
- /**
63
- * A new session for step 1, expiring after AUTH_SESSION_EXPIRY_MINUTES
64
- * (default 15).
65
- */
66
- static start({ userId, entityType, maxSteps, credentialId = null, now = new Date() }) {
67
- return new AuthorizationSession({
68
- sessionId: crypto.randomUUID(),
69
- userId,
70
- entityType,
71
- credentialId,
72
- currentStep: 1,
73
- maxSteps,
74
- stepData: {},
75
- expiresAt: new Date(now.getTime() + sessionTtlMs()),
76
- completed: false,
77
- createdAt: now,
78
- updatedAt: now,
79
- });
80
- }
81
-
82
- validate() {
83
- if (!this.sessionId) throw new Error('sessionId is required');
84
- if (!this.userId) throw new Error('userId is required');
85
- if (!this.entityType) throw new Error('entityType is required');
86
- if (!Number.isInteger(this.maxSteps) || this.maxSteps < 1) {
87
- throw new Error('maxSteps must be a positive integer');
88
- }
89
- if (
90
- !Number.isInteger(this.currentStep) ||
91
- this.currentStep < 1 ||
92
- this.currentStep > this.maxSteps
93
- ) {
94
- throw new Error(`currentStep must be between 1 and ${this.maxSteps}`);
95
- }
96
- if (Number.isNaN(this.expiresAt.getTime())) {
97
- throw new Error('expiresAt must be a valid date');
98
- }
99
- }
100
-
101
- isExpired(now = new Date()) {
102
- return this.expiresAt.getTime() <= now.getTime();
103
- }
104
-
105
- /** True when this session may be used by `userId` for `entityType`/`credentialId`. */
106
- belongsTo({ userId, entityType, credentialId = null }) {
107
- return (
108
- this.userId === String(userId) &&
109
- this.entityType === entityType &&
110
- this.credentialId === (credentialId === null || credentialId === undefined ? null : String(credentialId))
111
- );
112
- }
113
-
114
- /**
115
- * Records a finished step and moves to `nextStep`, merging what the step
116
- * collected into stepData.
117
- */
118
- advanceTo(nextStep, collected = {}) {
119
- if (this.completed) throw new Error('Cannot advance a completed session');
120
- if (nextStep !== this.currentStep + 1 || nextStep > this.maxSteps) {
121
- throw new Error(
122
- `Cannot move from step ${this.currentStep} to step ${nextStep} of ${this.maxSteps}`
123
- );
124
- }
125
- this.currentStep = nextStep;
126
- this.stepData = { ...this.stepData, ...(collected || {}) };
127
- this.updatedAt = new Date();
128
- }
129
-
130
- /** Starts the flow over at step 1, forgetting collected data. */
131
- restart() {
132
- this.currentStep = 1;
133
- this.stepData = {};
134
- this.completed = false;
135
- this.updatedAt = new Date();
136
- }
137
-
138
- markComplete() {
139
- this.completed = true;
140
- this.updatedAt = new Date();
141
- }
142
- }
143
-
144
- module.exports = { AuthorizationSession, DEFAULT_TTL_MINUTES };
@@ -1,15 +0,0 @@
1
- /**
2
- * Entity scopes and ownership (ADR-056 §3), re-exported from the four files it
3
- * is split into:
4
- *
5
- * scope-resolution constants, scope resolution, ownership on create
6
- * scope-config reading and validating app-definition settings
7
- * scope-visibility who may see which stored row
8
- * scope-policy the object a use case asks
9
- */
10
- module.exports = {
11
- ...require('./scope-resolution'),
12
- ...require('./scope-config'),
13
- ...require('./scope-visibility'),
14
- ...require('./scope-policy'),
15
- };
@@ -1,311 +0,0 @@
1
- /**
2
- * App-level entity type registry (ADR-059).
3
- *
4
- * Pure: reads an app definition, does no I/O. It answers "which entity types
5
- * (API modules) can this app connect?" from two sources:
6
- *
7
- * 1. `appDefinition.entities`, an object keyed by entity type name whose
8
- * values are `{ definition, scope?, proxy?, display? }`.
9
- * 2. Inference: every module bound by `appDefinition.integrations[*]
10
- * .Definition.modules` (skipped when `entities.$infer === false`).
11
- *
12
- * `loadAppDefinition()` builds the registry once, at load, and returns it as
13
- * `entityTypeRegistry` / `moduleDefinitions`; every consumer that holds a
14
- * loaded app definition reads those instead of rebuilding.
15
- */
16
-
17
- const { BaseError } = require('../errors/base-error');
18
- const { getLogger } = require('../logs');
19
-
20
- const log = getLogger('frigg.modules.entity_registry');
21
-
22
- const INFER_KEY = '$infer';
23
- const UNNAMED = '(unnamed definition)';
24
-
25
- /**
26
- * Raised for an invalid `entities` registry. `code` is always
27
- * `ENTITY_REGISTRY_INVALID`, so callers can tell it from other load errors.
28
- */
29
- class EntityRegistryError extends BaseError {
30
- constructor(message, options) {
31
- super(message, options);
32
- this.code = 'ENTITY_REGISTRY_INVALID';
33
- }
34
- }
35
-
36
- const isPlainObject = (value) =>
37
- value !== null && typeof value === 'object' && !Array.isArray(value);
38
-
39
- const appSource = () => ({ kind: 'app' });
40
- const integrationSource = (integrationClass) => ({
41
- kind: 'integration',
42
- name: integrationClass?.Definition?.name || 'unnamed',
43
- });
44
- const describeSource = (source) =>
45
- source.kind === 'app' ? 'app' : `integration:${source.name}`;
46
-
47
- const safeGetName = (definition) => {
48
- if (typeof definition.getName !== 'function') return undefined;
49
- try {
50
- const name = definition.getName();
51
- return typeof name === 'string' && name ? name : undefined;
52
- } catch {
53
- return undefined;
54
- }
55
- };
56
-
57
- const moduleNameOf = (definition) =>
58
- typeof definition?.moduleName === 'string' && definition.moduleName
59
- ? definition.moduleName
60
- : undefined;
61
-
62
- /** The name an entity type is known by: moduleName, else getName(). */
63
- function entityTypeNameOf(definition) {
64
- if (!definition) return undefined;
65
- return moduleNameOf(definition) ?? safeGetName(definition);
66
- }
67
-
68
- /** Same object, or the same package name and version when both can be read. */
69
- function isSameDefinition(a, b) {
70
- if (a === b) return true;
71
- const packageId = (definition) => {
72
- const { packageName, version } = definition || {};
73
- return typeof packageName === 'string' &&
74
- packageName &&
75
- typeof version === 'string' &&
76
- version
77
- ? `${packageName}@${version}`
78
- : null;
79
- };
80
- const left = packageId(a);
81
- return left !== null && left === packageId(b);
82
- }
83
-
84
- function conflictError(name, existing, source) {
85
- return new EntityRegistryError(
86
- `Entity type '${name}' is supplied by two different definitions: ` +
87
- `${existing.sources
88
- .map(describeSource)
89
- .join(', ')} and ${describeSource(source)}. ` +
90
- 'They must be the same definition object (or the same package name and version).'
91
- );
92
- }
93
-
94
- /**
95
- * A registry key must equal the definition's moduleName and, when it has one,
96
- * its getName(). Case matters. This leaves one name per entity type.
97
- */
98
- function assertKeyMatchesDefinition(key, definition) {
99
- const moduleName = moduleNameOf(definition);
100
- const getName = safeGetName(definition);
101
- const matches =
102
- (moduleName !== undefined || getName !== undefined) &&
103
- (moduleName === undefined || key === moduleName) &&
104
- (getName === undefined || key === getName);
105
- if (!matches) {
106
- throw new EntityRegistryError(
107
- `appDefinition.entities key '${key}' does not match its definition's name ` +
108
- `(moduleName '${moduleName ?? '(none)'}', getName() '${
109
- getName ?? '(none)'
110
- }'). ` +
111
- 'The key must equal both, so each entity type has one name.'
112
- );
113
- }
114
- }
115
-
116
- const settingsOf = (entry) => {
117
- const settings = {};
118
- for (const field of ['scope', 'proxy', 'display']) {
119
- if (entry[field] !== undefined) settings[field] = entry[field];
120
- }
121
- return settings;
122
- };
123
-
124
- /**
125
- * `entities` absent keeps today's behaviour: inference on, and a name clash
126
- * between integrations is not fatal. `entities` present (even `{}`) opts in to
127
- * the strict rules.
128
- */
129
- function readEntities(appDefinition) {
130
- const entities = appDefinition?.entities;
131
- if (entities === undefined || entities === null) {
132
- return { strict: false, infer: true, entries: [] };
133
- }
134
- if (!isPlainObject(entities)) {
135
- throw new EntityRegistryError(
136
- 'appDefinition.entities must be an object keyed by entity type name'
137
- );
138
- }
139
- const { [INFER_KEY]: inferFlag, ...rest } = entities;
140
- if (inferFlag !== undefined && typeof inferFlag !== 'boolean') {
141
- throw new EntityRegistryError(
142
- `appDefinition.entities.${INFER_KEY} must be a boolean`
143
- );
144
- }
145
- return {
146
- strict: true,
147
- infer: inferFlag !== false,
148
- entries: Object.entries(rest),
149
- };
150
- }
151
-
152
- function registerAppEntries(registry, entries) {
153
- for (const [key, entry] of entries) {
154
- if (
155
- !isPlainObject(entry) ||
156
- !entry.definition ||
157
- typeof entry.definition !== 'object'
158
- ) {
159
- throw new EntityRegistryError(
160
- `appDefinition.entities.${key} must be an object with a 'definition'`
161
- );
162
- }
163
- assertKeyMatchesDefinition(key, entry.definition);
164
- registry.set(key, {
165
- definition: entry.definition,
166
- sources: [appSource()],
167
- settings: settingsOf(entry),
168
- });
169
- }
170
- }
171
-
172
- function* boundDefinitions(integrationClass) {
173
- const modules = integrationClass?.Definition?.modules;
174
- if (!isPlainObject(modules)) return;
175
- for (const bound of Object.values(modules)) {
176
- if (bound?.definition) yield bound.definition;
177
- }
178
- }
179
-
180
- const addSource = (entry, source) => {
181
- const known = entry.sources.some(
182
- (s) => s.kind === source.kind && s.name === source.name
183
- );
184
- if (!known) entry.sources.push(source);
185
- };
186
-
187
- /**
188
- * Folds one integration-bound definition into the registry. Returns the name
189
- * it could not register (only possible when inference is off), else undefined.
190
- */
191
- function mergeBoundDefinition(registry, definition, source, { strict, infer }) {
192
- const name = entityTypeNameOf(definition);
193
-
194
- if (!name) {
195
- // No moduleName / getName(): it cannot be keyed by name. Offering it
196
- // as before, de-duplicated by identity, keeps apps that never named
197
- // one working. It cannot be registered, so `$infer: false` rejects it.
198
- if (!infer) return UNNAMED;
199
- const known = [...registry.values()].find(
200
- (entry) => entry.definition === definition
201
- );
202
- if (known) addSource(known, source);
203
- else {
204
- registry.set(`${UNNAMED} #${registry.size}`, {
205
- definition,
206
- sources: [source],
207
- settings: {},
208
- });
209
- }
210
- return undefined;
211
- }
212
-
213
- const existing = registry.get(name);
214
- if (existing) {
215
- if (isSameDefinition(existing.definition, definition)) {
216
- addSource(existing, source);
217
- } else if (strict) {
218
- throw conflictError(name, existing, source);
219
- } else {
220
- // Pre-ADR-059 apps keep working: the first definition wins, as
221
- // ModuleFactory would have picked it.
222
- log.warn('Two different definitions share one entity type name', {
223
- eventName: `${log.name}.duplicate_definition`,
224
- entityType: name,
225
- kept: existing.sources.map(describeSource).join(', '),
226
- ignored: describeSource(source),
227
- });
228
- }
229
- return undefined;
230
- }
231
-
232
- if (!infer) return name;
233
- registry.set(name, { definition, sources: [source], settings: {} });
234
- return undefined;
235
- }
236
-
237
- function assertAllRegistered(unregistered) {
238
- if (unregistered.size === 0) return;
239
- const detail = [...unregistered]
240
- .sort(([a], [b]) => a.localeCompare(b))
241
- .map(
242
- ([name, sources]) => `${name} (bound by ${[...sources].join(', ')})`
243
- );
244
- throw new EntityRegistryError(
245
- `appDefinition.entities.${INFER_KEY} is false, so every module an integration binds must be registered. Missing: ${detail.join(
246
- '; '
247
- )}`
248
- );
249
- }
250
-
251
- const freezeEntries = (registry) => {
252
- for (const entry of registry.values()) {
253
- entry.sources.forEach(Object.freeze);
254
- Object.freeze(entry.sources);
255
- Object.freeze(entry.settings);
256
- Object.freeze(entry);
257
- }
258
- };
259
-
260
- /**
261
- * Builds the registry for an app definition.
262
- *
263
- * @param {object} [appDefinition] - `{ entities?, integrations? }`
264
- * @returns {Map<string, {definition: object, sources: Array<{kind: 'app'} | {kind: 'integration', name: string}>, settings: {scope?: string|object, proxy?: object, display?: object}}>}
265
- * @throws {EntityRegistryError} on a key/name mismatch, two different
266
- * definitions under one name (when `entities` is present), or, with
267
- * `$infer: false`, a bound module that is not registered.
268
- */
269
- function getEntityTypeRegistry(appDefinition = {}) {
270
- const mode = readEntities(appDefinition);
271
- const registry = new Map();
272
- registerAppEntries(registry, mode.entries);
273
-
274
- const unregistered = new Map();
275
- for (const integrationClass of appDefinition?.integrations || []) {
276
- const source = integrationSource(integrationClass);
277
- for (const definition of boundDefinitions(integrationClass)) {
278
- const missing = mergeBoundDefinition(
279
- registry,
280
- definition,
281
- source,
282
- mode
283
- );
284
- if (missing !== undefined) {
285
- if (!unregistered.has(missing))
286
- unregistered.set(missing, new Set());
287
- unregistered.get(missing).add(describeSource(source));
288
- }
289
- }
290
- }
291
- assertAllRegistered(unregistered);
292
-
293
- freezeEntries(registry);
294
- return registry;
295
- }
296
-
297
- /** The definitions of a registry, in registry order. */
298
- const getEntityTypeDefinitions = (registry) =>
299
- [...registry.values()].map((entry) => entry.definition);
300
-
301
- /** Names of the integrations that bind an entity type; empty for registry-only. */
302
- const usedByOf = (entry) =>
303
- entry.sources.filter((s) => s.kind === 'integration').map((s) => s.name);
304
-
305
- module.exports = {
306
- getEntityTypeRegistry,
307
- getEntityTypeDefinitions,
308
- usedByOf,
309
- entityTypeNameOf,
310
- EntityRegistryError,
311
- };
@@ -1,169 +0,0 @@
1
- const { ProxyConfigError } = require('./proxy-errors');
2
-
3
- /**
4
- * Named base URLs for the entity proxy (ADR-052, amendment 2026-10-18).
5
- *
6
- * A module declares where its API lives. One host is `proxy.baseUrl`, which
7
- * means `{ default: baseUrl }`. A module whose API spans several hosts (Google:
8
- * Drive, Docs, Sheets, Analytics Data, Ads) declares `proxy.baseUrls`, a map of
9
- * name to URL. A caller picks a base by NAME in the proxy request's `base`
10
- * field; it never supplies a URL or a host, so the host still comes only from
11
- * the module.
12
- */
13
-
14
- const DEFAULT_BASE = 'default';
15
- const BASE_NAME = /^[a-z][a-z0-9-]*$/;
16
-
17
- const isPlainObject = (value) =>
18
- value !== null && typeof value === 'object' && !Array.isArray(value);
19
-
20
- const describeRule = (rule) =>
21
- `${rule && typeof rule.method === 'string' ? rule.method : '?'} ${
22
- rule && typeof rule.path === 'string' ? rule.path : '?'
23
- }`;
24
-
25
- function isValidBaseName(name) {
26
- return typeof name === 'string' && name.length <= 64 && BASE_NAME.test(name);
27
- }
28
-
29
- function assertBaseUrl(moduleName, name, value) {
30
- const where = `Module ${moduleName} proxy.baseUrls.${name}`;
31
- if (typeof value !== 'string') {
32
- throw new ProxyConfigError(`${where} must be an absolute https URL string (functions are not accepted)`);
33
- }
34
- let url;
35
- try {
36
- url = new URL(value);
37
- } catch {
38
- throw new ProxyConfigError(`${where} is not a valid absolute URL`);
39
- }
40
- if (url.protocol !== 'https:') {
41
- throw new ProxyConfigError(`${where} must use https`);
42
- }
43
- if (url.username || url.password || url.search || url.hash) {
44
- throw new ProxyConfigError(
45
- `${where} must not carry credentials, a query or a fragment`
46
- );
47
- }
48
- }
49
-
50
- /**
51
- * The bases a module declares, validated.
52
- *
53
- * @param {object} definition - a module definition
54
- * @returns {{ names: string[], values: Map<string, string|Function|undefined>, declared: boolean }}
55
- * `declared` is false when the module names no base of its own; it then has
56
- * the single base `default`, resolved from the requester at call time.
57
- * @throws {ProxyConfigError}
58
- */
59
- function resolveModuleBases(definition) {
60
- const moduleName = definition?.moduleName ?? '(unnamed)';
61
- const proxy = definition?.proxy;
62
- const hasBaseUrl = proxy?.baseUrl !== undefined;
63
- const hasBaseUrls = proxy?.baseUrls !== undefined;
64
-
65
- if (hasBaseUrl && hasBaseUrls) {
66
- throw new ProxyConfigError(
67
- `Module ${moduleName} declares both proxy.baseUrl and proxy.baseUrls; use one ` +
68
- '(baseUrl means { default: baseUrl })'
69
- );
70
- }
71
- if (hasBaseUrls) {
72
- if (!isPlainObject(proxy.baseUrls) || Object.keys(proxy.baseUrls).length === 0) {
73
- throw new ProxyConfigError(
74
- `Module ${moduleName} proxy.baseUrls must be a non-empty object of name to URL`
75
- );
76
- }
77
- const values = new Map();
78
- for (const [name, value] of Object.entries(proxy.baseUrls)) {
79
- if (!isValidBaseName(name)) {
80
- throw new ProxyConfigError(
81
- `Module ${moduleName} proxy.baseUrls has the invalid name ${JSON.stringify(
82
- name
83
- )}; names match ${BASE_NAME}`
84
- );
85
- }
86
- assertBaseUrl(moduleName, name, value);
87
- values.set(name, value);
88
- }
89
- return { names: [...values.keys()], values, declared: true };
90
- }
91
- return {
92
- names: [DEFAULT_BASE],
93
- values: new Map([[DEFAULT_BASE, proxy?.baseUrl]]),
94
- declared: hasBaseUrl,
95
- };
96
- }
97
-
98
- /** The base a rule applies to; a rule without `base` applies to `default`. */
99
- const ruleBase = (rule) => (rule && rule.base !== undefined ? rule.base : DEFAULT_BASE);
100
-
101
- /**
102
- * Load-time check of every allow rule that names a base: the app's
103
- * `managementApi.proxy.modules.<module>.allow` and each module's own
104
- * `proxy.allow`. A rule for a base the module does not declare could never
105
- * match, which reads as "allowed" and is not, so it stops the app.
106
- *
107
- * @param {Array<object>} moduleDefinitions
108
- * @param {{ modules?: Object }} [proxyConfig] - resolved managementApi.proxy
109
- * @throws {ProxyConfigError}
110
- */
111
- function assertProxyConfig(moduleDefinitions, proxyConfig = {}) {
112
- const namesByModule = new Map();
113
- for (const definition of moduleDefinitions || []) {
114
- if (!definition?.moduleName) continue;
115
- const { names } = resolveModuleBases(definition);
116
- namesByModule.set(definition.moduleName, names);
117
- const own = definition.proxy?.allow;
118
- if (Array.isArray(own)) {
119
- assertRulesUseDeclaredBases(
120
- definition.moduleName,
121
- names,
122
- own,
123
- `Module ${definition.moduleName} proxy.allow`
124
- );
125
- }
126
- }
127
- for (const [moduleName, override] of Object.entries(proxyConfig.modules || {})) {
128
- const names = namesByModule.get(moduleName);
129
- // An override for a module that is not registered is not new: it never applied.
130
- if (!names || !Array.isArray(override?.allow)) continue;
131
- assertRulesUseDeclaredBases(
132
- moduleName,
133
- names,
134
- override.allow,
135
- `managementApi.proxy.modules.${moduleName}.allow`
136
- );
137
- }
138
- }
139
-
140
- function assertRulesUseDeclaredBases(moduleName, names, rules, where) {
141
- for (const rule of rules) {
142
- if (!isPlainObject(rule) || rule.base === undefined) continue;
143
- if (!isValidBaseName(rule.base) || !names.includes(rule.base)) {
144
- throw new ProxyConfigError(
145
- `${where}: the rule ${describeRule(rule)} names the base ${JSON.stringify(
146
- rule.base
147
- )}, which module ${moduleName} does not declare (declared: ${names.join(', ')})`
148
- );
149
- }
150
- }
151
- // A rule with no base means `default`; the module must have one.
152
- if (!names.includes(DEFAULT_BASE)) {
153
- const bare = rules.find((rule) => isPlainObject(rule) && rule.base === undefined);
154
- if (bare) {
155
- throw new ProxyConfigError(
156
- `${where}: the rule ${describeRule(bare)} names no base, so it applies to ` +
157
- `"default", which module ${moduleName} does not declare (declared: ${names.join(', ')})`
158
- );
159
- }
160
- }
161
- }
162
-
163
- module.exports = {
164
- DEFAULT_BASE,
165
- isValidBaseName,
166
- resolveModuleBases,
167
- ruleBase,
168
- assertProxyConfig,
169
- };
@@ -1,31 +0,0 @@
1
- /**
2
- * A proxied call was refused or failed in a way the Management API maps to a
3
- * documented status (ADR-052 §10). `code` is the public error code; nothing
4
- * here carries the upstream URL, headers or credential material.
5
- */
6
- class ProxyRequestError extends Error {
7
- /**
8
- * @param {string} code - e.g. INVALID_PROXY_REQUEST, PROXY_NOT_ALLOWED, TIMEOUT
9
- * @param {string} message - safe to show the caller
10
- * @param {Object} [details] - safe structured details
11
- */
12
- constructor(code, message, details) {
13
- super(message);
14
- this.name = 'ProxyRequestError';
15
- this.code = code;
16
- this.details = details;
17
- }
18
- }
19
-
20
- /**
21
- * A module definition or the app's proxy settings are wrong. Thrown at load,
22
- * so the app stops starting instead of failing on the first proxied call.
23
- */
24
- class ProxyConfigError extends Error {
25
- constructor(message) {
26
- super(message);
27
- this.name = 'ProxyConfigError';
28
- }
29
- }
30
-
31
- module.exports = { ProxyRequestError, ProxyConfigError };