@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.
- package/CLAUDE.md +0 -28
- package/README.md +0 -105
- package/application/commands/integration-commands.js +10 -20
- package/application/commands/scheduler-commands.js +7 -11
- package/application/commands/user-commands.js +5 -17
- package/application/index.js +2 -6
- package/core/Worker.js +7 -31
- package/core/create-handler.js +0 -9
- package/credential/repositories/credential-repository-documentdb.js +0 -34
- package/credential/repositories/credential-repository-interface.js +0 -15
- package/credential/repositories/credential-repository-mongo.js +0 -23
- package/credential/repositories/credential-repository-postgres.js +0 -28
- package/database/config.js +24 -48
- package/database/documentdb-utils.js +0 -2
- package/database/encryption/encryption-schema-registry.js +12 -93
- package/database/prisma.js +2 -4
- package/errors/index.js +4 -0
- package/errors/mapping-already-exists-error.js +14 -0
- package/generated/prisma-mongodb/edge.js +4 -43
- package/generated/prisma-mongodb/index-browser.js +1 -40
- package/generated/prisma-mongodb/index.d.ts +111 -4739
- package/generated/prisma-mongodb/index.js +4 -43
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +0 -92
- package/generated/prisma-mongodb/wasm.js +4 -43
- package/generated/prisma-postgresql/edge.js +4 -43
- package/generated/prisma-postgresql/index-browser.js +1 -40
- package/generated/prisma-postgresql/index.d.ts +33 -4966
- package/generated/prisma-postgresql/index.js +4 -43
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +0 -87
- package/generated/prisma-postgresql/wasm.js +4 -43
- package/handlers/WEBHOOKS.md +29 -0
- package/handlers/app-definition-loader.js +19 -118
- package/handlers/app-handler-helpers.js +3 -25
- package/handlers/backend-utils.js +25 -31
- package/handlers/middleware/admin-auth.js +1 -14
- package/handlers/routers/auth.js +10 -46
- package/handlers/routers/health.js +4 -11
- package/handlers/routers/integration-defined-routers.js +92 -144
- package/handlers/routers/integration-webhook-routers.js +64 -102
- package/handlers/workers/integration-defined-workers.js +2 -5
- package/index.js +3 -63
- package/infrastructure/scheduler/index.js +6 -19
- package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
- package/integrations/index.js +0 -1
- package/integrations/integration-base.js +29 -0
- package/integrations/integration-router.js +96 -131
- package/integrations/repositories/integration-mapping-repository-documentdb.js +50 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +15 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +29 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +30 -0
- package/integrations/repositories/integration-mapping-repository.js +29 -0
- package/integrations/repositories/integration-repository-documentdb.js +3 -35
- package/integrations/repositories/integration-repository-interface.js +1 -17
- package/integrations/repositories/integration-repository-mongo.js +77 -60
- package/integrations/repositories/integration-repository-postgres.js +76 -62
- package/integrations/repositories/process-repository-mongo.js +1 -1
- package/integrations/tests/doubles/test-integration-repository.js +1 -12
- package/integrations/use-cases/create-integration.js +20 -55
- package/integrations/use-cases/get-integrations-for-user.js +40 -80
- package/integrations/use-cases/index.js +1 -5
- package/integrations/utils/map-integration-dto.js +14 -43
- package/modules/index.js +0 -14
- package/modules/module-factory.js +0 -12
- package/modules/module.js +2 -53
- package/modules/repositories/module-repository-documentdb.js +6 -51
- package/modules/repositories/module-repository-interface.js +2 -19
- package/modules/repositories/module-repository-mongo.js +74 -67
- package/modules/repositories/module-repository-postgres.js +74 -66
- package/modules/repositories/module-repository.js +1 -2
- package/modules/requester/requester.js +2 -27
- package/modules/use-cases/get-entities-for-user.js +13 -26
- package/modules/use-cases/get-entity-options-by-id.js +4 -10
- package/modules/use-cases/get-entity-options-by-type.js +1 -1
- package/modules/use-cases/get-module-instance-from-type.js +1 -1
- package/modules/use-cases/get-module.js +4 -10
- package/modules/use-cases/process-authorization-callback.js +19 -53
- package/modules/use-cases/refresh-entity-options.js +4 -10
- package/modules/use-cases/test-module-auth.js +4 -10
- package/package.json +5 -6
- package/prisma-mongodb/schema.prisma +0 -92
- package/prisma-postgresql/schema.prisma +0 -87
- package/queues/index.js +0 -25
- package/types/index.d.ts +0 -2
- package/user/repositories/user-repository-documentdb.js +2 -27
- package/user/repositories/user-repository-interface.js +0 -18
- package/user/repositories/user-repository-mongo.js +0 -24
- package/user/repositories/user-repository-postgres.js +0 -18
- package/user/tests/doubles/test-user-repository.js +1 -1
- package/user/use-cases/authenticate-user.js +1 -7
- package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
- package/user/use-cases/get-user-from-bearer-token.js +7 -2
- package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
- package/user/user.js +1 -64
- package/credential/use-cases/credential-operations-for-user.js +0 -202
- package/credential/use-cases/reauthorize-credential-for-user.js +0 -73
- package/database/organization-where.js +0 -18
- package/database/unique-violation.js +0 -25
- package/extensions/bootstrap-runner.js +0 -43
- package/extensions/extension-loader.js +0 -167
- package/extensions/index.js +0 -41
- package/extensions/initialize-app.js +0 -123
- package/extensions/route-mounter.js +0 -103
- package/extensions/schema-composer.js +0 -188
- package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -216
- package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
- package/integrations/repositories/integration-not-found.js +0 -14
- package/integrations/use-cases/integration-operations-for-user.js +0 -256
- package/integrations/use-cases/resolve-integration-ownership.js +0 -177
- package/management-api/create-management-api-router.js +0 -49
- package/management-api/error-response.js +0 -116
- package/management-api/management-api-config.js +0 -151
- package/management-api/meta-router.js +0 -57
- package/management-api/openapi/build-openapi.js +0 -144
- package/management-api/openapi/components.js +0 -414
- package/management-api/openapi/v1-operations.js +0 -83
- package/management-api/openapi/v2-operations.js +0 -264
- package/management-api/route-registry.js +0 -294
- package/management-api/test-support/http-client.js +0 -39
- package/management-api/use-cases/get-management-api-meta.js +0 -57
- package/management-api/v1-deprecation.js +0 -120
- package/management-api/v2/create-v2-router.js +0 -85
- package/management-api/v2/dependencies.js +0 -281
- package/management-api/v2/dto.js +0 -143
- package/management-api/v2/handlers/authorization.js +0 -94
- package/management-api/v2/handlers/credentials.js +0 -33
- package/management-api/v2/handlers/entities.js +0 -77
- package/management-api/v2/handlers/integrations.js +0 -128
- package/management-api/v2/handlers/proxy.js +0 -34
- package/management-api/v2/request.js +0 -85
- package/management-api/v2/test-support/fake-dependencies.js +0 -103
- package/modules/domain/authorization-session.js +0 -144
- package/modules/entity-scope.js +0 -15
- package/modules/entity-type-registry.js +0 -311
- package/modules/proxy/proxy-bases.js +0 -169
- package/modules/proxy/proxy-errors.js +0 -31
- package/modules/proxy/proxy-path.js +0 -183
- package/modules/proxy/proxy-policy.js +0 -265
- package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
- package/modules/repositories/authorization-session-repository-factory.js +0 -31
- package/modules/repositories/authorization-session-repository-interface.js +0 -47
- package/modules/repositories/authorization-session-repository-prisma.js +0 -87
- package/modules/repositories/entity-not-found.js +0 -18
- package/modules/requester/proxy-request.js +0 -245
- package/modules/scope-config.js +0 -192
- package/modules/scope-policy.js +0 -112
- package/modules/scope-resolution.js +0 -288
- package/modules/scope-visibility.js +0 -247
- package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
- package/modules/tests/doubles/ownership-matching.js +0 -18
- package/modules/use-cases/authorize-entity-for-user.js +0 -61
- package/modules/use-cases/create-entity-from-credential.js +0 -113
- package/modules/use-cases/entity-operations-for-user.js +0 -162
- package/modules/use-cases/entity-types.js +0 -139
- package/modules/use-cases/execute-entity-proxy-request.js +0 -331
- package/modules/use-cases/get-authorization-requirements.js +0 -100
- package/modules/use-cases/get-entity-module-for-user.js +0 -113
- package/modules/use-cases/manage-access.js +0 -21
- package/modules/use-cases/multi-step-authorization.js +0 -329
- package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
- package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
- package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
- package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
- package/providers/index.js +0 -13
- package/providers/resolve-provider.js +0 -93
- package/queues/providers/index.js +0 -11
- package/queues/providers/netlify-background-provider.js +0 -150
- package/queues/providers/qstash-queue-provider.js +0 -199
- package/queues/providers/sqs-queue-provider.js +0 -70
- package/queues/queue-client-interface.js +0 -55
- package/queues/queue-message-auth.js +0 -187
- package/queues/queue-provider-factory.js +0 -101
- package/queues/queue-provider.js +0 -65
- package/queues/sqs-queue-client.js +0 -81
- package/types/modules/index.d.ts +0 -214
- package/types/user/index.d.ts +0 -134
- package/user/adopter-jwt-config.js +0 -177
- package/user/adopter-jwt-errors.js +0 -59
- package/user/adopter-token-verifier.js +0 -122
- package/user/app-user-helpers.js +0 -97
- package/user/create-user-authentication.js +0 -64
- package/user/membership.js +0 -51
- package/user/repositories/organization-membership-record.js +0 -19
- package/user/repositories/organization-membership-repository-documentdb.js +0 -106
- package/user/repositories/organization-membership-repository-factory.js +0 -42
- package/user/repositories/organization-membership-repository-interface.js +0 -57
- package/user/repositories/organization-membership-repository-mongo.js +0 -13
- package/user/repositories/organization-membership-repository-postgres.js +0 -29
- package/user/repositories/organization-membership-repository-prisma.js +0 -81
- package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
- package/user/use-cases/ensure-organization-membership.js +0 -92
- 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 };
|
package/modules/entity-scope.js
DELETED
|
@@ -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 };
|