@friggframework/core 2.0.0--canary.707.df80bfd.0 → 2.0.0--canary.680.d670696.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/CHANGELOG.md +3 -0
- package/CLAUDE.md +0 -28
- package/README.md +0 -105
- package/application/commands/integration-commands.js +2 -2
- package/application/commands/scheduler-commands.js +7 -11
- package/application/commands/user-commands.js +5 -17
- package/core/Worker.js +7 -31
- package/core/create-handler.js +0 -9
- package/credential/use-cases/credential-operations-for-user.js +25 -110
- package/credential/use-cases/reauthorize-credential-for-user.js +8 -15
- package/database/config.js +24 -48
- package/database/documentdb-encryption-service.js +0 -1
- package/database/documentdb-utils.js +1 -6
- package/database/encryption/encryption-config.js +44 -13
- package/database/encryption/encryption-schema-registry.js +31 -88
- package/database/prisma.js +2 -4
- package/generated/prisma-mongodb/edge.js +4 -27
- package/generated/prisma-mongodb/index-browser.js +1 -24
- package/generated/prisma-mongodb/index.d.ts +48 -3228
- package/generated/prisma-mongodb/index.js +4 -27
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +0 -65
- package/generated/prisma-mongodb/wasm.js +4 -27
- package/generated/prisma-postgresql/edge.js +4 -27
- package/generated/prisma-postgresql/index-browser.js +1 -24
- package/generated/prisma-postgresql/index.d.ts +51 -3503
- package/generated/prisma-postgresql/index.js +4 -27
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +0 -61
- package/generated/prisma-postgresql/wasm.js +4 -27
- package/handlers/app-definition-loader.js +12 -110
- package/handlers/routers/auth.js +12 -40
- package/handlers/routers/health.js +2 -4
- package/handlers/routers/integration-defined-routers.js +92 -144
- package/handlers/routers/integration-webhook-routers.js +64 -102
- package/index.js +1 -64
- package/infrastructure/scheduler/index.js +6 -19
- package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
- package/integrations/index.js +0 -5
- package/integrations/integration-router.js +73 -115
- 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 +76 -59
- package/integrations/repositories/integration-repository-postgres.js +76 -62
- 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 -7
- package/integrations/use-cases/integration-operations-for-user.js +24 -55
- package/integrations/utils/map-integration-dto.js +1 -44
- package/management-api/management-api-config.js +2 -71
- package/management-api/meta-router.js +2 -21
- package/management-api/openapi/components.js +3 -50
- package/management-api/openapi/v2-operations.js +5 -17
- package/management-api/route-registry.js +6 -123
- package/management-api/v2/dependencies.js +15 -40
- package/management-api/v2/dto.js +0 -20
- package/management-api/v2/handlers/authorization.js +0 -1
- package/management-api/v2/handlers/credentials.js +2 -6
- package/management-api/v2/handlers/entities.js +7 -10
- package/management-api/v2/handlers/integrations.js +3 -6
- package/management-api/v2/request.js +1 -32
- package/modules/index.js +0 -8
- package/modules/module-factory.js +0 -12
- package/modules/module.js +2 -53
- package/modules/proxy/proxy-errors.js +1 -12
- package/modules/proxy/proxy-path.js +3 -7
- package/modules/proxy/proxy-policy.js +5 -34
- 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/proxy-request.js +1 -2
- package/modules/requester/requester.js +3 -8
- package/modules/use-cases/entity-operations-for-user.js +13 -50
- package/modules/use-cases/entity-types.js +7 -31
- package/modules/use-cases/execute-entity-proxy-request.js +15 -107
- package/modules/use-cases/get-authorization-requirements.js +2 -2
- package/modules/use-cases/get-entities-for-user.js +13 -26
- package/modules/use-cases/get-entity-module-for-user.js +7 -34
- 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/multi-step-authorization.js +13 -40
- 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 -65
- package/prisma-postgresql/schema.prisma +0 -61
- package/queues/index.js +0 -25
- package/types/index.d.ts +0 -1
- package/types/modules/index.d.ts +1 -157
- 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/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-x-frigg-headers.js +80 -83
- package/user/user.js +1 -64
- 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 -230
- package/extensions/index.js +0 -44
- package/extensions/initialize-app.js +0 -123
- package/extensions/prisma-composer.js +0 -270
- 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/schedule-config.js +0 -80
- package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
- package/integrations/use-cases/resolve-integration-ownership.js +0 -177
- package/modules/entity-scope.js +0 -15
- package/modules/proxy/proxy-bases.js +0 -169
- package/modules/repositories/entity-not-found.js +0 -18
- 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/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/manage-access.js +0 -21
- 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 -23
- package/providers/resolve-provider.js +0 -93
- package/providers/runtime-detection.js +0 -124
- package/queues/providers/index.js +0 -11
- package/queues/providers/netlify-background-provider.js +0 -150
- package/queues/providers/qstash-queue-provider.js +0 -219
- package/queues/providers/sqs-queue-provider.js +0 -70
- package/queues/queue-client-interface.js +0 -55
- package/queues/queue-message-auth.js +0 -193
- 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/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,80 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* An integration's schedule configuration (`static Definition.schedule`),
|
|
3
|
-
* read by hosts that tick ONGOING_SYNC from a cron (provider-netlify's
|
|
4
|
-
* scheduled-sync function).
|
|
5
|
-
*
|
|
6
|
-
* schedule: {
|
|
7
|
-
* // 'class' (default): one ONGOING_SYNC per integration class per tick,
|
|
8
|
-
* // with no integration id: the worker runs it on an unhydrated
|
|
9
|
-
* // instance, so the handler finds its own installations.
|
|
10
|
-
* // 'installation': one ONGOING_SYNC per ENABLED integration record of
|
|
11
|
-
* // the class, carrying `data.integrationId`, so the worker hydrates
|
|
12
|
-
* // that installation before the handler runs.
|
|
13
|
-
* scope: 'installation',
|
|
14
|
-
* // Optional, 'installation' only: (integration, { now }) => boolean |
|
|
15
|
-
* // Promise<boolean>. Return false to skip that installation this tick.
|
|
16
|
-
* // Without it every installation is dispatched on every tick.
|
|
17
|
-
* isDue: (integration, { now }) => true,
|
|
18
|
-
* }
|
|
19
|
-
*
|
|
20
|
-
* Validated at load (loadAppDefinition) so a mistyped scope stops the app
|
|
21
|
-
* rather than silently falling back to 'class'.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
const SCHEDULE_SCOPES = Object.freeze({
|
|
25
|
-
CLASS: 'class',
|
|
26
|
-
INSTALLATION: 'installation',
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
const DEFAULT_SCHEDULE_SCOPE = SCHEDULE_SCOPES.CLASS;
|
|
30
|
-
|
|
31
|
-
class ScheduleConfigError extends Error {
|
|
32
|
-
constructor(message) {
|
|
33
|
-
super(message);
|
|
34
|
-
this.name = 'ScheduleConfigError';
|
|
35
|
-
this.code = 'SCHEDULE_CONFIG_INVALID';
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* @param {Object} [definition] - an integration's static Definition
|
|
41
|
-
* @returns {{ scope: 'class'|'installation', isDue: Function|null }}
|
|
42
|
-
* @throws {ScheduleConfigError} when `schedule` is malformed
|
|
43
|
-
*/
|
|
44
|
-
function resolveScheduleConfig(definition) {
|
|
45
|
-
const name = definition?.name || 'integration';
|
|
46
|
-
const schedule = definition?.schedule;
|
|
47
|
-
if (schedule === undefined || schedule === null) {
|
|
48
|
-
return { scope: DEFAULT_SCHEDULE_SCOPE, isDue: null };
|
|
49
|
-
}
|
|
50
|
-
if (typeof schedule !== 'object' || Array.isArray(schedule)) {
|
|
51
|
-
throw new ScheduleConfigError(`${name}: Definition.schedule must be an object`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const scopes = Object.values(SCHEDULE_SCOPES);
|
|
55
|
-
const scope = schedule.scope === undefined ? DEFAULT_SCHEDULE_SCOPE : schedule.scope;
|
|
56
|
-
if (!scopes.includes(scope)) {
|
|
57
|
-
throw new ScheduleConfigError(
|
|
58
|
-
`${name}: Definition.schedule.scope must be one of ${scopes.map((s) => `'${s}'`).join(', ')} (got ${JSON.stringify(schedule.scope)})`
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const isDue = schedule.isDue === undefined ? null : schedule.isDue;
|
|
63
|
-
if (isDue !== null && typeof isDue !== 'function') {
|
|
64
|
-
throw new ScheduleConfigError(`${name}: Definition.schedule.isDue must be a function`);
|
|
65
|
-
}
|
|
66
|
-
if (isDue && scope !== SCHEDULE_SCOPES.INSTALLATION) {
|
|
67
|
-
throw new ScheduleConfigError(
|
|
68
|
-
`${name}: Definition.schedule.isDue applies to scope 'installation' only`
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return { scope, isDue };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
module.exports = {
|
|
76
|
-
SCHEDULE_SCOPES,
|
|
77
|
-
DEFAULT_SCHEDULE_SCOPE,
|
|
78
|
-
ScheduleConfigError,
|
|
79
|
-
resolveScheduleConfig,
|
|
80
|
-
};
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
const {
|
|
2
|
-
SCHEDULE_SCOPES,
|
|
3
|
-
resolveScheduleConfig,
|
|
4
|
-
} = require('../schedule-config');
|
|
5
|
-
|
|
6
|
-
const ONGOING_SYNC_EVENT = 'ONGOING_SYNC';
|
|
7
|
-
const ACTIVE_STATUS = 'ENABLED';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Dispatches one cron tick of ONGOING_SYNC to the queue worker, per each
|
|
11
|
-
* integration class's `Definition.schedule.scope` (schedule-config.js):
|
|
12
|
-
*
|
|
13
|
-
* - 'class' (default): one message per class, `data: {}`.
|
|
14
|
-
* - 'installation': one message per ENABLED integration record of the class,
|
|
15
|
-
* `data: { integrationId }`, so the worker hydrates that installation;
|
|
16
|
-
* skipped when the definition's `isDue(integration, { now })` says no.
|
|
17
|
-
*
|
|
18
|
-
* Only classes that implement `startOngoingSync` take part. A failure for one
|
|
19
|
-
* class or installation is counted and logged; the rest still go out.
|
|
20
|
-
*/
|
|
21
|
-
class DispatchOngoingSyncs {
|
|
22
|
-
/**
|
|
23
|
-
* @param {Object} params
|
|
24
|
-
* @param {Object} params.integrationRepository - findIntegrations({ type, status })
|
|
25
|
-
* @param {Object} params.queueProvider - send(message, queueId)
|
|
26
|
-
* @param {string} params.queueId - the worker the messages go to
|
|
27
|
-
* @param {() => Date} [params.now]
|
|
28
|
-
* @param {Object} [params.logger=console]
|
|
29
|
-
*/
|
|
30
|
-
constructor({ integrationRepository, queueProvider, queueId, now = () => new Date(), logger = console }) {
|
|
31
|
-
this.integrationRepository = integrationRepository;
|
|
32
|
-
this.queueProvider = queueProvider;
|
|
33
|
-
this.queueId = queueId;
|
|
34
|
-
this.now = now;
|
|
35
|
-
this.logger = logger;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* @param {Array<Function>} integrationClasses
|
|
40
|
-
* @returns {Promise<{ dispatched: number, notDue: number, errors: number }>}
|
|
41
|
-
*/
|
|
42
|
-
async execute(integrationClasses = []) {
|
|
43
|
-
const result = { dispatched: 0, notDue: 0, errors: 0 };
|
|
44
|
-
const now = this.now();
|
|
45
|
-
|
|
46
|
-
for (const IntegrationClass of integrationClasses) {
|
|
47
|
-
const name = IntegrationClass?.Definition?.name;
|
|
48
|
-
if (!name) continue;
|
|
49
|
-
if (typeof IntegrationClass.prototype?.startOngoingSync !== 'function') continue;
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
const { scope, isDue } = resolveScheduleConfig(IntegrationClass.Definition);
|
|
53
|
-
if (scope === SCHEDULE_SCOPES.INSTALLATION) {
|
|
54
|
-
await this._dispatchPerInstallation(name, isDue, now, result);
|
|
55
|
-
} else {
|
|
56
|
-
await this._send(name, {});
|
|
57
|
-
result.dispatched++;
|
|
58
|
-
}
|
|
59
|
-
} catch (error) {
|
|
60
|
-
result.errors++;
|
|
61
|
-
this.logger.error(`[ScheduledSync] Failed to dispatch sync for ${name}:`, error.message);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return result;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async _dispatchPerInstallation(name, isDue, now, result) {
|
|
69
|
-
const integrations = await this.integrationRepository.findIntegrations({
|
|
70
|
-
type: name,
|
|
71
|
-
status: ACTIVE_STATUS,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
for (const integration of integrations || []) {
|
|
75
|
-
try {
|
|
76
|
-
if (isDue && (await isDue(integration, { now })) !== true) {
|
|
77
|
-
result.notDue++;
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
await this._send(name, { integrationId: integration.id });
|
|
81
|
-
result.dispatched++;
|
|
82
|
-
} catch (error) {
|
|
83
|
-
result.errors++;
|
|
84
|
-
this.logger.error(
|
|
85
|
-
`[ScheduledSync] Failed to dispatch sync for ${name} integration ${integration?.id}:`,
|
|
86
|
-
error.message
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
_send(integrationName, data) {
|
|
93
|
-
return this.queueProvider.send(
|
|
94
|
-
{ integrationName, event: ONGOING_SYNC_EVENT, data },
|
|
95
|
-
this.queueId
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
module.exports = { DispatchOngoingSyncs, ONGOING_SYNC_EVENT };
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
const Boom = require('@hapi/boom');
|
|
2
|
-
const { SCOPES, resolveScope } = require('../../modules/scope-resolution');
|
|
3
|
-
const {
|
|
4
|
-
integrationOwnerKind,
|
|
5
|
-
resolveIntegrationOwner,
|
|
6
|
-
} = require('../../modules/scope-resolution');
|
|
7
|
-
const {
|
|
8
|
-
describeRecord,
|
|
9
|
-
unscopedRowsBelongHere,
|
|
10
|
-
} = require('../../modules/scope-visibility');
|
|
11
|
-
const {
|
|
12
|
-
bindingScopeSetting,
|
|
13
|
-
readIntegrationOwnership,
|
|
14
|
-
} = require('../../modules/scope-config');
|
|
15
|
-
const { DEFAULT_SCOPE_POLICY } = require('../../modules/scope-policy');
|
|
16
|
-
const { entityTypeNameOf } = require('../../modules/entity-type-registry');
|
|
17
|
-
|
|
18
|
-
const notFound = (entityId) =>
|
|
19
|
-
Boom.notFound(`Entity ${entityId} not found`, { code: 'ENTITY_NOT_FOUND' });
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Who owns an integration about to be created, and in which organization
|
|
23
|
-
* (ADR-056 §3, §3.3, §3.6). Shared by the v1 and v2 create paths. Nothing is
|
|
24
|
-
* persisted before this has answered, so a refusal leaves no row behind.
|
|
25
|
-
*
|
|
26
|
-
* Every entity must be one the caller may use in the active organization. A
|
|
27
|
-
* missing one, another organization's and a `global` one are the same 404, so
|
|
28
|
-
* ids cannot be probed. When the integration declares a scope (a binding's
|
|
29
|
-
* `scope` or `Definition.entities[key].scope`, or `Definition.ownership.scope`)
|
|
30
|
-
* the entity's scope must be in it. Levels below the integration (entity type,
|
|
31
|
-
* app, default) decide where a connection is CREATED and do not restrict what
|
|
32
|
-
* an integration may bind, so an app that adds a registry scope later does not
|
|
33
|
-
* break integrations that already use older connections.
|
|
34
|
-
*
|
|
35
|
-
* The owner follows the entities (all organization: the organization;
|
|
36
|
-
* otherwise the person) unless `Definition.ownership.owner` says. Every entity
|
|
37
|
-
* must then be of the owner's own scope: an integration is loaded by its owner,
|
|
38
|
-
* who must own what it uses, so an integration that mixes an organization
|
|
39
|
-
* entity with a person's own, or that names an owner that does not match, is
|
|
40
|
-
* refused with a 400 here, before anything is written.
|
|
41
|
-
*/
|
|
42
|
-
class ResolveIntegrationOwnership {
|
|
43
|
-
/**
|
|
44
|
-
* @param {Object} params
|
|
45
|
-
* @param {import('../../modules/repositories/module-repository-interface').ModuleRepositoryInterface} params.moduleRepository
|
|
46
|
-
* @param {Array<Function>} params.integrationClasses
|
|
47
|
-
* @param {Object} [params.scopePolicy] - from createScopePolicy
|
|
48
|
-
*/
|
|
49
|
-
constructor({ moduleRepository, integrationClasses, scopePolicy }) {
|
|
50
|
-
this.moduleRepository = moduleRepository;
|
|
51
|
-
this.integrationClasses = integrationClasses || [];
|
|
52
|
-
this.scopePolicy = scopePolicy || DEFAULT_SCOPE_POLICY;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* @param {Object} params
|
|
57
|
-
* @param {Array<string|number>} params.entityIds
|
|
58
|
-
* @param {Object} params.config - `{ type }`
|
|
59
|
-
* @param {Object} params.user - authenticated User
|
|
60
|
-
* @returns {Promise<{ownerId: string, organizationId: (string|null), ownerKind: string, adoptUnscoped: boolean}>}
|
|
61
|
-
*/
|
|
62
|
-
async execute({ entityIds, config, user }) {
|
|
63
|
-
const ctx = this.scopePolicy.accessContext(user);
|
|
64
|
-
const integrationClass = this.integrationClasses.find(
|
|
65
|
-
(IntegrationClass) => IntegrationClass.Definition?.name === config?.type
|
|
66
|
-
);
|
|
67
|
-
const ownership = readIntegrationOwnership(integrationClass);
|
|
68
|
-
|
|
69
|
-
const entities = await this.loadEntities(entityIds);
|
|
70
|
-
const scoped = entities.map((entity) => ({
|
|
71
|
-
entity,
|
|
72
|
-
scope: this.acceptedScope({ entity, ctx, integrationClass, ownership }),
|
|
73
|
-
}));
|
|
74
|
-
|
|
75
|
-
const ownerKind = integrationOwnerKind(
|
|
76
|
-
scoped.map(({ scope }) => scope),
|
|
77
|
-
ownership.owner
|
|
78
|
-
);
|
|
79
|
-
this.assertOneScope(scoped, ownerKind, config?.type);
|
|
80
|
-
return {
|
|
81
|
-
...resolveIntegrationOwner(ownerKind, ctx),
|
|
82
|
-
ownerKind,
|
|
83
|
-
// Whether an existing integration with no organization (written
|
|
84
|
-
// before scoping) is this caller's to reuse. An organization-owned
|
|
85
|
-
// one is unambiguous: its owner is the organization, whoever asks
|
|
86
|
-
// and whatever their own legacy pointer says. A person's is theirs
|
|
87
|
-
// only where scope-visibility says (their legacy organization).
|
|
88
|
-
adoptUnscoped:
|
|
89
|
-
ownerKind === SCOPES.ORGANIZATION ||
|
|
90
|
-
unscopedRowsBelongHere(ctx, 'integration'),
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Every entity must be of the integration owner's own scope (see the class comment). */
|
|
95
|
-
assertOneScope(scoped, ownerKind, type) {
|
|
96
|
-
for (const { entity, scope } of scoped) {
|
|
97
|
-
if (scope === ownerKind) continue;
|
|
98
|
-
// The load-time check (resolveOwnershipConfig) only sees declared
|
|
99
|
-
// scopes; this one sees the entities actually named.
|
|
100
|
-
if (ownerKind === SCOPES.ORGANIZATION) {
|
|
101
|
-
throw Boom.badRequest(
|
|
102
|
-
"An organization-owned integration cannot use an 'individual' entity: a shared integration must not run on one person's credential",
|
|
103
|
-
{
|
|
104
|
-
code: 'ORG_INTEGRATION_INDIVIDUAL_ENTITY',
|
|
105
|
-
details: { entityId: String(entity.id) },
|
|
106
|
-
}
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
throw Boom.badRequest(
|
|
110
|
-
`Integration '${type}' would be owned by the person but uses the organization entity ${entity.id}. An integration must use entities of one scope: all of the person's, or all of the organization's.`,
|
|
111
|
-
{
|
|
112
|
-
code: 'MIXED_ENTITY_SCOPES',
|
|
113
|
-
details: { entityId: String(entity.id), scope },
|
|
114
|
-
}
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async loadEntities(entityIds) {
|
|
120
|
-
const ids = (entityIds || []).map(String);
|
|
121
|
-
if (ids.length === 0) return [];
|
|
122
|
-
let found;
|
|
123
|
-
try {
|
|
124
|
-
found = await this.moduleRepository.findEntitiesByIds(ids);
|
|
125
|
-
} catch (error) {
|
|
126
|
-
// A malformed id (e.g. not an integer on PostgreSQL) is "not found".
|
|
127
|
-
if (/Invalid ID|ObjectId|Malformed/i.test(error?.message || '')) {
|
|
128
|
-
throw notFound(ids[0]);
|
|
129
|
-
}
|
|
130
|
-
throw error;
|
|
131
|
-
}
|
|
132
|
-
const byId = new Map((found || []).filter(Boolean).map((e) => [String(e.id), e]));
|
|
133
|
-
return ids.map((id) => {
|
|
134
|
-
if (!byId.has(id)) throw notFound(id);
|
|
135
|
-
return byId.get(id);
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** The entity's scope for this caller, or a refusal. */
|
|
140
|
-
acceptedScope({ entity, ctx, integrationClass, ownership }) {
|
|
141
|
-
const described = describeRecord(entity, ctx, 'entity');
|
|
142
|
-
if (!described) throw notFound(entity.id);
|
|
143
|
-
const { scope } = described;
|
|
144
|
-
|
|
145
|
-
const declared = this.declaredScope({ entity, integrationClass, ownership });
|
|
146
|
-
if (declared && !declared.allow.includes(scope)) {
|
|
147
|
-
throw Boom.badRequest(
|
|
148
|
-
`Entity ${entity.id} is ${scope}-scoped, but this integration binds ${entity.moduleName} at ${declared.allow.join(' or ')} scope`,
|
|
149
|
-
{
|
|
150
|
-
code: 'ENTITY_SCOPE_MISMATCH',
|
|
151
|
-
details: { entityId: String(entity.id), scope, allowed: declared.allow },
|
|
152
|
-
}
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
return scope;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* What the integration itself declares for this entity's binding (levels 2
|
|
160
|
-
* and 3 only), or null when it declares nothing.
|
|
161
|
-
*/
|
|
162
|
-
declaredScope({ entity, integrationClass, ownership }) {
|
|
163
|
-
const Definition = integrationClass?.Definition;
|
|
164
|
-
const key = Object.keys(Definition?.modules || {}).find(
|
|
165
|
-
(candidate) =>
|
|
166
|
-
entityTypeNameOf(Definition.modules[candidate]?.definition) ===
|
|
167
|
-
entity.moduleName
|
|
168
|
-
);
|
|
169
|
-
const resolved = resolveScope({
|
|
170
|
-
binding: key === undefined ? undefined : bindingScopeSetting(Definition, key),
|
|
171
|
-
integration: ownership.scope,
|
|
172
|
-
});
|
|
173
|
-
return resolved.decidedBy === 'default' ? null : resolved;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
module.exports = { ResolveIntegrationOwnership };
|
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,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,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The error `findEntityById` throws for an id with no row, and the one
|
|
3
|
-
* predicate that recognises it (as integration-not-found.js does for
|
|
4
|
-
* integrations). The adapters build the error here and use cases match it
|
|
5
|
-
* here, so a missing entity can be told from any other failure without
|
|
6
|
-
* widening a regex.
|
|
7
|
-
*
|
|
8
|
-
* Why it matters: a use case that answers 404 for another tenant's entity must
|
|
9
|
-
* answer the same 404 for an id that does not exist. Otherwise the status code
|
|
10
|
-
* says which ids exist (ADR-052 §3, ADR-056 §3.6).
|
|
11
|
-
*/
|
|
12
|
-
const MESSAGE = /^Entity .+ not found$/;
|
|
13
|
-
|
|
14
|
-
const entityNotFoundError = (id) => new Error(`Entity ${id} not found`);
|
|
15
|
-
|
|
16
|
-
const isEntityNotFound = (error) => MESSAGE.test(error?.message || '');
|
|
17
|
-
|
|
18
|
-
module.exports = { entityNotFoundError, isEntityNotFound };
|