@friggframework/core 2.0.0--canary.681.2df9ef0.0 → 2.0.0--canary.682.cbca9e2.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/README.md +36 -0
- package/application/commands/integration-commands.js +20 -10
- package/application/index.js +6 -2
- package/database/encryption/encryption-schema-registry.js +7 -0
- package/database/organization-where.js +18 -0
- package/generated/prisma-mongodb/edge.js +5 -3
- package/generated/prisma-mongodb/index-browser.js +2 -0
- package/generated/prisma-mongodb/index.d.ts +115 -2
- package/generated/prisma-mongodb/index.js +5 -3
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +15 -0
- package/generated/prisma-mongodb/wasm.js +5 -3
- package/generated/prisma-postgresql/edge.js +5 -3
- package/generated/prisma-postgresql/index-browser.js +2 -0
- package/generated/prisma-postgresql/index.d.ts +131 -2
- package/generated/prisma-postgresql/index.js +5 -3
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +15 -0
- package/generated/prisma-postgresql/wasm.js +5 -3
- package/handlers/app-definition-loader.js +25 -8
- package/handlers/backend-utils.js +31 -25
- package/handlers/routers/health.js +1 -4
- package/handlers/workers/integration-defined-workers.js +5 -2
- package/index.js +10 -2
- package/integrations/index.js +0 -4
- package/integrations/integration-router.js +99 -52
- package/integrations/repositories/integration-not-found.js +14 -0
- package/integrations/repositories/integration-repository-documentdb.js +34 -3
- package/integrations/repositories/integration-repository-interface.js +17 -1
- package/integrations/repositories/integration-repository-mongo.js +59 -76
- package/integrations/repositories/integration-repository-postgres.js +62 -76
- package/integrations/tests/doubles/test-integration-repository.js +12 -1
- package/integrations/use-cases/create-integration.js +55 -20
- package/integrations/use-cases/get-integrations-for-user.js +80 -40
- package/integrations/use-cases/index.js +5 -1
- package/integrations/use-cases/integration-operations-for-user.js +40 -19
- package/integrations/use-cases/resolve-integration-ownership.js +177 -0
- package/integrations/utils/map-integration-dto.js +1 -15
- package/management-api/management-api-config.js +3 -0
- package/management-api/openapi/components.js +46 -1
- package/management-api/openapi/v2-operations.js +14 -1
- package/management-api/v2/dependencies.js +56 -14
- package/management-api/v2/dto.js +17 -0
- package/management-api/v2/handlers/authorization.js +1 -0
- package/management-api/v2/handlers/entities.js +10 -7
- package/management-api/v2/handlers/integrations.js +6 -3
- package/management-api/v2/request.js +32 -1
- package/modules/entity-scope.js +15 -0
- package/modules/entity-type-registry.js +311 -0
- package/modules/index.js +14 -0
- package/modules/repositories/module-repository-documentdb.js +41 -1
- package/modules/repositories/module-repository-interface.js +19 -2
- package/modules/repositories/module-repository-mongo.js +64 -73
- package/modules/repositories/module-repository-postgres.js +63 -73
- package/modules/scope-config.js +155 -0
- package/modules/scope-policy.js +80 -0
- package/modules/scope-resolution.js +277 -0
- package/modules/scope-visibility.js +165 -0
- package/modules/tests/doubles/ownership-matching.js +18 -0
- package/modules/use-cases/authorize-entity-for-user.js +59 -0
- package/modules/use-cases/entity-operations-for-user.js +42 -10
- package/modules/use-cases/entity-types.js +62 -14
- package/modules/use-cases/execute-entity-proxy-request.js +15 -0
- package/modules/use-cases/get-entities-for-user.js +16 -5
- package/modules/use-cases/get-entity-module-for-user.js +23 -3
- package/modules/use-cases/get-entity-options-by-id.js +5 -2
- package/modules/use-cases/get-module.js +5 -2
- package/modules/use-cases/multi-step-authorization.js +36 -12
- package/modules/use-cases/process-authorization-callback.js +52 -18
- package/modules/use-cases/refresh-entity-options.js +5 -2
- package/modules/use-cases/test-module-auth.js +5 -2
- package/package.json +5 -5
- package/prisma-mongodb/schema.prisma +15 -0
- package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
- package/prisma-postgresql/schema.prisma +15 -0
- package/types/index.d.ts +1 -0
- package/types/modules/index.d.ts +145 -0
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
const { Module } = require('../module');
|
|
2
2
|
const { ModuleConstants } = require('../ModuleConstants');
|
|
3
|
+
const { pickExistingEntity } = require('../scope-resolution');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What an entity belongs to when the caller says nothing: the person's own,
|
|
7
|
+
* outside any organization.
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_TENANCY = Object.freeze({
|
|
10
|
+
scope: 'individual',
|
|
11
|
+
organizationId: null,
|
|
12
|
+
legacyOrgId: null,
|
|
13
|
+
});
|
|
3
14
|
const { getLogger } = require('../../logs');
|
|
4
15
|
|
|
5
16
|
const log = getLogger('frigg.modules.authorization_callback');
|
|
@@ -30,7 +41,17 @@ class ProcessAuthorizationCallback {
|
|
|
30
41
|
this.integrationRepository = integrationRepository;
|
|
31
42
|
}
|
|
32
43
|
|
|
33
|
-
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} userId - the OWNER of what this creates (ADR-056 §3.2): the
|
|
46
|
+
* person for `individual`, the organization's user for `organization`.
|
|
47
|
+
* Not necessarily the caller; callers resolve it from the scope.
|
|
48
|
+
* @param {string} entityType
|
|
49
|
+
* @param {Object} params - what the module's authorization needs
|
|
50
|
+
* @param {{scope: string, organizationId: (string|null), legacyOrgId: (string|null)}} [tenancy]
|
|
51
|
+
* the entity's scope and organization (§3.6); default: the person's own,
|
|
52
|
+
* with no organization
|
|
53
|
+
*/
|
|
54
|
+
async execute(userId, entityType, params, tenancy = DEFAULT_TENANCY) {
|
|
34
55
|
log.debug('Authorization callback started', {
|
|
35
56
|
eventName: `${log.name}.started`,
|
|
36
57
|
userId,
|
|
@@ -57,10 +78,7 @@ class ProcessAuthorizationCallback {
|
|
|
57
78
|
userId,
|
|
58
79
|
entityType
|
|
59
80
|
);
|
|
60
|
-
const existingEntity =
|
|
61
|
-
existingEntities && existingEntities.length > 0
|
|
62
|
-
? existingEntities[0]
|
|
63
|
-
: null;
|
|
81
|
+
const existingEntity = pickExistingEntity(existingEntities, tenancy);
|
|
64
82
|
|
|
65
83
|
if (existingEntity) {
|
|
66
84
|
log.debug('Existing entity found', {
|
|
@@ -156,7 +174,8 @@ class ProcessAuthorizationCallback {
|
|
|
156
174
|
const persistedEntity = await this.findOrCreateEntity(
|
|
157
175
|
entityDetails,
|
|
158
176
|
entityType,
|
|
159
|
-
module.credential.id
|
|
177
|
+
module.credential.id,
|
|
178
|
+
tenancy
|
|
160
179
|
);
|
|
161
180
|
log.debug('Authorization callback completed', {
|
|
162
181
|
eventName: `${log.name}.completed`,
|
|
@@ -233,7 +252,12 @@ class ProcessAuthorizationCallback {
|
|
|
233
252
|
module.credential = persisted;
|
|
234
253
|
}
|
|
235
254
|
|
|
236
|
-
async findOrCreateEntity(
|
|
255
|
+
async findOrCreateEntity(
|
|
256
|
+
entityDetails,
|
|
257
|
+
moduleName,
|
|
258
|
+
credentialId,
|
|
259
|
+
tenancy = DEFAULT_TENANCY
|
|
260
|
+
) {
|
|
237
261
|
const { identifiers, details } = entityDetails;
|
|
238
262
|
|
|
239
263
|
// Support both 'user' and 'userId' field names from module definitions
|
|
@@ -247,11 +271,16 @@ class ProcessAuthorizationCallback {
|
|
|
247
271
|
);
|
|
248
272
|
}
|
|
249
273
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
274
|
+
// The same provider account in two organizations is two entities
|
|
275
|
+
// (ADR-056 §3.6), so the match is per organization.
|
|
276
|
+
const existingEntity = pickExistingEntity(
|
|
277
|
+
await this.moduleRepository.findEntities({
|
|
278
|
+
externalId: identifiers.externalId,
|
|
279
|
+
user: userId,
|
|
280
|
+
moduleName: moduleName,
|
|
281
|
+
}),
|
|
282
|
+
tenancy
|
|
283
|
+
);
|
|
255
284
|
|
|
256
285
|
if (existingEntity) {
|
|
257
286
|
// Repoint the entity's credentialId when re-auth produced a
|
|
@@ -282,12 +311,17 @@ class ProcessAuthorizationCallback {
|
|
|
282
311
|
return existingEntity;
|
|
283
312
|
}
|
|
284
313
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
314
|
+
// `organizationId` is not part of the entity data: that carries provider
|
|
315
|
+
// fields, and one that happens to be called organizationId is theirs.
|
|
316
|
+
return await this.moduleRepository.createEntity(
|
|
317
|
+
{
|
|
318
|
+
...identifiers,
|
|
319
|
+
...details,
|
|
320
|
+
moduleName: moduleName,
|
|
321
|
+
credential: credentialId,
|
|
322
|
+
},
|
|
323
|
+
{ organizationId: tenancy.organizationId ?? null }
|
|
324
|
+
);
|
|
291
325
|
}
|
|
292
326
|
}
|
|
293
327
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { Module } = require('../module');
|
|
2
|
+
const { DEFAULT_SCOPE_POLICY } = require('../scope-policy');
|
|
2
3
|
|
|
3
4
|
class RefreshEntityOptions {
|
|
4
5
|
/**
|
|
@@ -6,9 +7,11 @@ class RefreshEntityOptions {
|
|
|
6
7
|
* @param {import('../repositories/module-repository-interface').ModuleRepositoryInterface} params.moduleRepository
|
|
7
8
|
* @param {} params.moduleDefinitions
|
|
8
9
|
*/
|
|
9
|
-
constructor({ moduleRepository, moduleDefinitions }) {
|
|
10
|
+
constructor({ moduleRepository, moduleDefinitions, scopePolicy }) {
|
|
10
11
|
this.moduleRepository = moduleRepository;
|
|
11
12
|
this.moduleDefinitions = moduleDefinitions;
|
|
13
|
+
// Who may load an entity follows its scope (ADR-056 §3.6).
|
|
14
|
+
this.scopePolicy = scopePolicy || DEFAULT_SCOPE_POLICY;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
/**
|
|
@@ -36,7 +39,7 @@ class RefreshEntityOptions {
|
|
|
36
39
|
|
|
37
40
|
// Validate entity ownership
|
|
38
41
|
const isOwned = typeof userIdOrUser === 'object' && userIdOrUser?.ownsUserId
|
|
39
|
-
?
|
|
42
|
+
? this.scopePolicy.canUseEntity(userIdOrUser, entity)
|
|
40
43
|
: entity.userId?.toString() === userId?.toString();
|
|
41
44
|
|
|
42
45
|
if (!isOwned) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { Module } = require('../module');
|
|
2
|
+
const { DEFAULT_SCOPE_POLICY } = require('../scope-policy');
|
|
2
3
|
|
|
3
4
|
class TestModuleAuth {
|
|
4
5
|
/**
|
|
@@ -6,9 +7,11 @@ class TestModuleAuth {
|
|
|
6
7
|
* @param {import('../repositories/module-repository-interface').ModuleRepositoryInterface} params.moduleRepository - Repository for module data operations.
|
|
7
8
|
* @param {Array<Object>} params.moduleDefinitions - Array of module definitions.
|
|
8
9
|
*/
|
|
9
|
-
constructor({ moduleRepository, moduleDefinitions }) {
|
|
10
|
+
constructor({ moduleRepository, moduleDefinitions, scopePolicy }) {
|
|
10
11
|
this.moduleRepository = moduleRepository;
|
|
11
12
|
this.moduleDefinitions = moduleDefinitions;
|
|
13
|
+
// Who may load an entity follows its scope (ADR-056 §3.6).
|
|
14
|
+
this.scopePolicy = scopePolicy || DEFAULT_SCOPE_POLICY;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
/**
|
|
@@ -35,7 +38,7 @@ class TestModuleAuth {
|
|
|
35
38
|
|
|
36
39
|
// Validate entity ownership
|
|
37
40
|
const isOwned = typeof userIdOrUser === 'object' && userIdOrUser?.ownsUserId
|
|
38
|
-
?
|
|
41
|
+
? this.scopePolicy.canUseEntity(userIdOrUser, entity)
|
|
39
42
|
: entity.userId?.toString() === userId?.toString();
|
|
40
43
|
|
|
41
44
|
if (!isOwned) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.
|
|
4
|
+
"version": "2.0.0--canary.682.cbca9e2.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@friggframework/eslint-config": "2.0.0--canary.
|
|
52
|
-
"@friggframework/prettier-config": "2.0.0--canary.
|
|
53
|
-
"@friggframework/test": "2.0.0--canary.
|
|
51
|
+
"@friggframework/eslint-config": "2.0.0--canary.682.cbca9e2.0",
|
|
52
|
+
"@friggframework/prettier-config": "2.0.0--canary.682.cbca9e2.0",
|
|
53
|
+
"@friggframework/test": "2.0.0--canary.682.cbca9e2.0",
|
|
54
54
|
"@prisma/client": "^6.19.3",
|
|
55
55
|
"@types/lodash": "4.17.15",
|
|
56
56
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -90,5 +90,5 @@
|
|
|
90
90
|
"publishConfig": {
|
|
91
91
|
"access": "public"
|
|
92
92
|
},
|
|
93
|
-
"gitHead": "
|
|
93
|
+
"gitHead": "cbca9e22cf6af8765f83f03b2e6d3700d1e149b5"
|
|
94
94
|
}
|
|
@@ -134,6 +134,14 @@ model Entity {
|
|
|
134
134
|
moduleName String?
|
|
135
135
|
externalId String?
|
|
136
136
|
|
|
137
|
+
// Organization this entity was connected in (ADR-056 §3.6): the active
|
|
138
|
+
// organization for `individual`, the owner for `organization`, null for
|
|
139
|
+
// `global`. Null on an `individual` entity is also what every row written
|
|
140
|
+
// before this column existed has, and what `individualAcross: 'all'` writes.
|
|
141
|
+
// Deliberately not a relation: a deleted organization must leave its rows
|
|
142
|
+
// invisible, not turn them into "unscoped" ones (SetNull would do that).
|
|
143
|
+
organizationId String? @db.ObjectId
|
|
144
|
+
|
|
137
145
|
data Json @default("{}")
|
|
138
146
|
|
|
139
147
|
createdAt DateTime @default(now())
|
|
@@ -150,6 +158,7 @@ model Entity {
|
|
|
150
158
|
associationObjects AssociationObject[]
|
|
151
159
|
|
|
152
160
|
@@index([userId])
|
|
161
|
+
@@index([userId, organizationId])
|
|
153
162
|
@@index([externalId])
|
|
154
163
|
@@index([moduleName])
|
|
155
164
|
@@index([credentialId])
|
|
@@ -167,6 +176,11 @@ model Integration {
|
|
|
167
176
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
168
177
|
status IntegrationStatus @default(IN_CREATION)
|
|
169
178
|
|
|
179
|
+
// Organization this integration was created in (ADR-056 §3.6): the active
|
|
180
|
+
// organization at create time. Null for rows that predate the column. Not a
|
|
181
|
+
// relation, for the same reason as Entity.organizationId.
|
|
182
|
+
organizationId String? @db.ObjectId
|
|
183
|
+
|
|
170
184
|
// Configuration and version
|
|
171
185
|
config Json? // Integration configuration object
|
|
172
186
|
version String?
|
|
@@ -191,6 +205,7 @@ model Integration {
|
|
|
191
205
|
processes Process[]
|
|
192
206
|
|
|
193
207
|
@@index([userId])
|
|
208
|
+
@@index([userId, organizationId])
|
|
194
209
|
@@index([status])
|
|
195
210
|
@@map("Integration")
|
|
196
211
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- Entity and Integration carry the organization they were created in (ADR-056 §3.6).
|
|
2
|
+
-- Nullable, no default and no backfill: a row written before this migration keeps
|
|
3
|
+
-- NULL, and the domain (modules/entity-scope.js) reads NULL as "predates scoping"
|
|
4
|
+
-- (docs/guides/entity-scopes.md). No scope column: the owner's user type says it.
|
|
5
|
+
-- Deliberately no foreign key: a deleted organization must leave its rows
|
|
6
|
+
-- invisible, and ON DELETE SET NULL would turn them into unscoped ones.
|
|
7
|
+
--
|
|
8
|
+
-- Release note: the two CREATE INDEX statements below are plain (not
|
|
9
|
+
-- CONCURRENTLY, which Prisma migrations cannot run inside a transaction). Each
|
|
10
|
+
-- takes a SHARE lock on its table while it builds, so writes to "Entity" and
|
|
11
|
+
-- "Integration" wait for it. Both tables are small in a typical Frigg
|
|
12
|
+
-- deployment; on a very large one, create the indexes by hand first with
|
|
13
|
+
-- CREATE INDEX CONCURRENTLY IF NOT EXISTS under the same names, then deploy.
|
|
14
|
+
|
|
15
|
+
-- AlterTable
|
|
16
|
+
ALTER TABLE "Entity" ADD COLUMN "organizationId" INTEGER;
|
|
17
|
+
|
|
18
|
+
-- AlterTable
|
|
19
|
+
ALTER TABLE "Integration" ADD COLUMN "organizationId" INTEGER;
|
|
20
|
+
|
|
21
|
+
-- CreateIndex
|
|
22
|
+
CREATE INDEX "Entity_userId_organizationId_idx" ON "Entity"("userId", "organizationId");
|
|
23
|
+
|
|
24
|
+
-- CreateIndex
|
|
25
|
+
CREATE INDEX "Integration_userId_organizationId_idx" ON "Integration"("userId", "organizationId");
|
|
@@ -131,6 +131,14 @@ model Entity {
|
|
|
131
131
|
moduleName String?
|
|
132
132
|
externalId String?
|
|
133
133
|
|
|
134
|
+
// Organization this entity was connected in (ADR-056 §3.6): the active
|
|
135
|
+
// organization for `individual`, the owner for `organization`, null for
|
|
136
|
+
// `global`. Null on an `individual` entity is also what every row written
|
|
137
|
+
// before this column existed has, and what `individualAcross: 'all'` writes.
|
|
138
|
+
// Deliberately not a relation: a deleted organization must leave its rows
|
|
139
|
+
// invisible, not turn them into "unscoped" ones (SetNull would do that).
|
|
140
|
+
organizationId Int?
|
|
141
|
+
|
|
134
142
|
data Json @default("{}")
|
|
135
143
|
|
|
136
144
|
createdAt DateTime @default(now())
|
|
@@ -144,6 +152,7 @@ model Entity {
|
|
|
144
152
|
associationObjects AssociationObject[]
|
|
145
153
|
|
|
146
154
|
@@index([userId])
|
|
155
|
+
@@index([userId, organizationId])
|
|
147
156
|
@@index([externalId])
|
|
148
157
|
@@index([moduleName])
|
|
149
158
|
@@index([credentialId])
|
|
@@ -160,6 +169,11 @@ model Integration {
|
|
|
160
169
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
161
170
|
status IntegrationStatus @default(IN_CREATION)
|
|
162
171
|
|
|
172
|
+
// Organization this integration was created in (ADR-056 §3.6): the active
|
|
173
|
+
// organization at create time. Null for rows that predate the column. Not a
|
|
174
|
+
// relation, for the same reason as Entity.organizationId.
|
|
175
|
+
organizationId Int?
|
|
176
|
+
|
|
163
177
|
// Configuration and version
|
|
164
178
|
config Json? // Integration configuration object
|
|
165
179
|
version String?
|
|
@@ -183,6 +197,7 @@ model Integration {
|
|
|
183
197
|
processes Process[]
|
|
184
198
|
|
|
185
199
|
@@index([userId])
|
|
200
|
+
@@index([userId, organizationId])
|
|
186
201
|
@@index([status])
|
|
187
202
|
}
|
|
188
203
|
|
package/types/index.d.ts
CHANGED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
declare module "@friggframework/core" {
|
|
2
|
+
/** Where an entity type comes from: the app's `entities` map or an integration binding it. */
|
|
3
|
+
export type EntityTypeSource =
|
|
4
|
+
| { kind: "app" }
|
|
5
|
+
| { kind: "integration"; name: string };
|
|
6
|
+
|
|
7
|
+
export type EntityScope = "individual" | "organization" | "global";
|
|
8
|
+
|
|
9
|
+
/** App-level settings carried from the `entities` entry; empty for an inferred-only type. */
|
|
10
|
+
export interface EntityTypeSettings {
|
|
11
|
+
scope?: EntityScope | { default?: EntityScope; allow?: EntityScope[] };
|
|
12
|
+
proxy?: Record<string, unknown>;
|
|
13
|
+
display?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface EntityTypeEntry {
|
|
17
|
+
/** The API module definition. */
|
|
18
|
+
definition: any;
|
|
19
|
+
/** Frozen. `{ kind: 'app' }` first when registered, then each binding integration. */
|
|
20
|
+
sources: readonly EntityTypeSource[];
|
|
21
|
+
/** Frozen. */
|
|
22
|
+
settings: Readonly<EntityTypeSettings>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Entity type name (the definition's moduleName) to its entry. */
|
|
26
|
+
export type EntityTypeRegistry = Map<string, EntityTypeEntry>;
|
|
27
|
+
|
|
28
|
+
/** One `appDefinition.entities` entry. The key must equal the definition's moduleName (and getName() when defined). */
|
|
29
|
+
export interface EntityRegistration extends EntityTypeSettings {
|
|
30
|
+
definition: any;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AppDefinitionEntities {
|
|
34
|
+
/** false turns inference off: every module an integration binds must be registered. Default true. */
|
|
35
|
+
$infer?: boolean;
|
|
36
|
+
[entityType: string]: EntityRegistration | boolean | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* One level's setting: a scope, or a default plus the scopes a request may
|
|
41
|
+
* choose from (ADR-056 §3.1). `global` is never chosen by a tenant request.
|
|
42
|
+
*/
|
|
43
|
+
export type ScopeSetting =
|
|
44
|
+
| EntityScope
|
|
45
|
+
| { default?: EntityScope; allow?: EntityScope[] };
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `appDefinition.ownership` (ADR-056 §3, §3.6). Resolution order for a new
|
|
49
|
+
* connection: request, binding, integration, entity type (`entities[name].scope`),
|
|
50
|
+
* this `scope`, then `organization` for an organization-primary app and
|
|
51
|
+
* `individual` otherwise.
|
|
52
|
+
*/
|
|
53
|
+
export interface AppOwnership {
|
|
54
|
+
scope?: ScopeSetting;
|
|
55
|
+
/**
|
|
56
|
+
* `'organization'` (default): an individual entity belongs to the organization
|
|
57
|
+
* it was connected in. `'all'`: one connection per person across every
|
|
58
|
+
* organization, stored with no `organizationId`.
|
|
59
|
+
*/
|
|
60
|
+
individualAcross?: "organization" | "all";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `Definition.ownership` on an integration (ADR-056 §3.1, §3.3). */
|
|
64
|
+
export interface IntegrationOwnership {
|
|
65
|
+
/** Scope every binding of this integration accepts unless it sets its own. */
|
|
66
|
+
scope?: ScopeSetting;
|
|
67
|
+
/** `'derive'` (default): organization when every entity it uses is organization-scoped. */
|
|
68
|
+
owner?: "derive" | "individual" | "organization";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface AppDefinition {
|
|
72
|
+
integrations: any[];
|
|
73
|
+
/** App-level entity type registry (ADR-059). */
|
|
74
|
+
entities?: AppDefinitionEntities;
|
|
75
|
+
/** Who owns what a person connects (ADR-056). */
|
|
76
|
+
ownership?: AppOwnership;
|
|
77
|
+
[key: string]: unknown;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Whose a record is and what the caller may do with it (ADR-056 §3.4). */
|
|
81
|
+
export interface RecordTenancy {
|
|
82
|
+
scope: Exclude<EntityScope, "global">;
|
|
83
|
+
owner: { type: Exclude<EntityScope, "global">; id: string };
|
|
84
|
+
/** Null for an individual entity connected outside an organization, or under `individualAcross: 'all'`. */
|
|
85
|
+
organizationId: string | null;
|
|
86
|
+
/** `manage`: create, reauthorize, delete. `use`: bind it, proxy through it. `view`: metadata only. */
|
|
87
|
+
access: "manage" | "use" | "view";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Which scope a connection gets, and from which level of the order (ADR-056 §3.1). */
|
|
91
|
+
export interface ResolvedScope {
|
|
92
|
+
scope: EntityScope;
|
|
93
|
+
/** Where `scope` came from. `request` when the request chose within the allowed list. */
|
|
94
|
+
source: "request" | "binding" | "integration" | "entityType" | "app" | "default";
|
|
95
|
+
/** The level that decided what a request may choose from. */
|
|
96
|
+
decidedBy: "binding" | "integration" | "entityType" | "app" | "default";
|
|
97
|
+
allow: EntityScope[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* First match wins, each level overriding the ones below it. A request may
|
|
102
|
+
* choose only from the deciding level's `allow`; `global` never comes from a
|
|
103
|
+
* tenant request. Throws a Boom 400/403 for a request scope that is not allowed.
|
|
104
|
+
*/
|
|
105
|
+
export function resolveScope(levels: {
|
|
106
|
+
request?: EntityScope;
|
|
107
|
+
binding?: ScopeSetting;
|
|
108
|
+
integration?: ScopeSetting;
|
|
109
|
+
entityType?: ScopeSetting;
|
|
110
|
+
app?: ScopeSetting;
|
|
111
|
+
primary?: "individual" | "organization";
|
|
112
|
+
}): ResolvedScope;
|
|
113
|
+
|
|
114
|
+
/** Every binding of every integration with its resolved scope (for tooling that wants to show the chain). */
|
|
115
|
+
export function listBindingScopes(
|
|
116
|
+
appDefinition: Pick<AppDefinition, "integrations" | "ownership">,
|
|
117
|
+
entityTypeRegistry: EntityTypeRegistry,
|
|
118
|
+
primary?: "individual" | "organization"
|
|
119
|
+
): Array<{
|
|
120
|
+
integration: string;
|
|
121
|
+
binding: string;
|
|
122
|
+
moduleName: string | null;
|
|
123
|
+
scope: EntityScope;
|
|
124
|
+
source: ResolvedScope["source"];
|
|
125
|
+
owner: "derive" | "individual" | "organization";
|
|
126
|
+
}>;
|
|
127
|
+
|
|
128
|
+
/** Invalid ownership or scope setting, thrown at app-definition load. `code` is always `OWNERSHIP_CONFIG_INVALID`. */
|
|
129
|
+
export class OwnershipConfigError extends Error {
|
|
130
|
+
code: "OWNERSHIP_CONFIG_INVALID";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Builds the registry from `entities` plus the modules integrations bind.
|
|
135
|
+
* Throws EntityRegistryError on an invalid registry.
|
|
136
|
+
*/
|
|
137
|
+
export function getEntityTypeRegistry(
|
|
138
|
+
appDefinition?: Pick<AppDefinition, "entities" | "integrations">
|
|
139
|
+
): EntityTypeRegistry;
|
|
140
|
+
|
|
141
|
+
/** Invalid `entities` registry. `code` is always `ENTITY_REGISTRY_INVALID`. */
|
|
142
|
+
export class EntityRegistryError extends Error {
|
|
143
|
+
code: "ENTITY_REGISTRY_INVALID";
|
|
144
|
+
}
|
|
145
|
+
}
|