@friggframework/core 2.0.0--canary.682.3c8b68a.0 → 2.0.0--canary.685.022c3c3.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 +23 -0
- package/README.md +1 -1
- package/credential/use-cases/credential-operations-for-user.js +110 -25
- package/credential/use-cases/reauthorize-credential-for-user.js +15 -8
- package/handlers/app-definition-loader.js +9 -0
- package/index.js +4 -0
- package/integrations/integration-router.js +7 -5
- package/integrations/use-cases/integration-operations-for-user.js +16 -6
- package/management-api/management-api-config.js +70 -4
- package/management-api/openapi/components.js +1 -0
- package/management-api/openapi/v2-operations.js +6 -3
- package/management-api/v2/dependencies.js +2 -0
- package/management-api/v2/dto.js +3 -0
- package/management-api/v2/handlers/credentials.js +6 -2
- package/modules/repositories/entity-not-found.js +18 -0
- package/modules/repositories/module-repository-documentdb.js +3 -2
- package/modules/repositories/module-repository-mongo.js +2 -1
- package/modules/repositories/module-repository-postgres.js +2 -1
- package/modules/repositories/module-repository.js +2 -1
- package/modules/scope-config.js +23 -1
- package/modules/scope-policy.js +34 -3
- package/modules/scope-resolution.js +11 -0
- package/modules/scope-visibility.js +63 -8
- package/modules/use-cases/authorize-entity-for-user.js +4 -2
- package/modules/use-cases/entity-operations-for-user.js +9 -4
- package/modules/use-cases/execute-entity-proxy-request.js +50 -18
- package/modules/use-cases/get-entity-module-for-user.js +9 -2
- package/modules/use-cases/manage-access.js +21 -0
- package/modules/use-cases/multi-step-authorization.js +4 -1
- package/package.json +5 -5
- package/types/modules/index.d.ts +42 -0
- package/types/user/index.d.ts +7 -0
- package/user/use-cases/authenticate-user.js +7 -1
- package/user/user.js +24 -1
package/types/modules/index.d.ts
CHANGED
|
@@ -68,12 +68,49 @@ declare module "@friggframework/core" {
|
|
|
68
68
|
owner?: "derive" | "individual" | "organization";
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/** One operation a module or the app lets the entity proxy forward (ADR-052). */
|
|
72
|
+
export interface ProxyAllowRule {
|
|
73
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "*";
|
|
74
|
+
/** Relative to the module's base URL. `:param` is one segment; a trailing `/**` is a subtree. */
|
|
75
|
+
path: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** `appDefinition.managementApi.proxy`: the entity proxy, POST /api/v2/entities/:id/proxy (ADR-052). */
|
|
79
|
+
export interface ManagementApiProxyConfig {
|
|
80
|
+
/** Mount the route. Default false. */
|
|
81
|
+
enable?: boolean;
|
|
82
|
+
/** Upstream timeout in ms; can only be lowered from 25000. */
|
|
83
|
+
timeoutMs?: number;
|
|
84
|
+
/** Largest request body in bytes; default 1 MiB, at most 10 MiB. */
|
|
85
|
+
maxRequestBytes?: number;
|
|
86
|
+
/** Largest response body in bytes; can only be lowered from 5 MiB. */
|
|
87
|
+
maxResponseBytes?: number;
|
|
88
|
+
/**
|
|
89
|
+
* Entity scopes the proxy accepts (ADR-056 §3.5). Default `['individual']`.
|
|
90
|
+
* Add `'organization'` to let a member with use access proxy through the
|
|
91
|
+
* organization's shared credential. `global` is never accepted. An unknown,
|
|
92
|
+
* empty or repeated value throws ManagementApiConfigError at load.
|
|
93
|
+
*/
|
|
94
|
+
scopes?: Array<Exclude<EntityScope, "global">>;
|
|
95
|
+
/** Per-module allow-list override; `'module'` uses the module's own list. */
|
|
96
|
+
modules?: Record<string, { allow: "module" | ProxyAllowRule[] }>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** `appDefinition.managementApi` (ADR-052, ADR-053). */
|
|
100
|
+
export interface ManagementApiConfig {
|
|
101
|
+
/** Serve the deprecated v1 routes. Default true. */
|
|
102
|
+
v1?: boolean;
|
|
103
|
+
proxy?: ManagementApiProxyConfig;
|
|
104
|
+
}
|
|
105
|
+
|
|
71
106
|
export interface AppDefinition {
|
|
72
107
|
integrations: any[];
|
|
73
108
|
/** App-level entity type registry (ADR-059). */
|
|
74
109
|
entities?: AppDefinitionEntities;
|
|
75
110
|
/** Who owns what a person connects (ADR-056). */
|
|
76
111
|
ownership?: AppOwnership;
|
|
112
|
+
/** Management API versions and the entity proxy (ADR-052, ADR-053, ADR-056 §3.5). */
|
|
113
|
+
managementApi?: ManagementApiConfig;
|
|
77
114
|
[key: string]: unknown;
|
|
78
115
|
}
|
|
79
116
|
|
|
@@ -138,6 +175,11 @@ declare module "@friggframework/core" {
|
|
|
138
175
|
appDefinition?: Pick<AppDefinition, "entities" | "integrations">
|
|
139
176
|
): EntityTypeRegistry;
|
|
140
177
|
|
|
178
|
+
/** Invalid `managementApi` setting (today: `proxy.scopes`), thrown at app-definition load. */
|
|
179
|
+
export class ManagementApiConfigError extends Error {
|
|
180
|
+
code: "MANAGEMENT_API_CONFIG_INVALID";
|
|
181
|
+
}
|
|
182
|
+
|
|
141
183
|
/** Invalid `entities` registry. `code` is always `ENTITY_REGISTRY_INVALID`. */
|
|
142
184
|
export class EntityRegistryError extends Error {
|
|
143
185
|
code: "ENTITY_REGISTRY_INVALID";
|
package/types/user/index.d.ts
CHANGED
|
@@ -62,6 +62,13 @@ declare module "@friggframework/core/user" {
|
|
|
62
62
|
* at startup. `strictMembership` wins when both are set.
|
|
63
63
|
*/
|
|
64
64
|
strictUserValidation?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Roles (the adopter token's role claim) that may manage organization-level
|
|
67
|
+
* things (ADR-056 §3). Default `['ADMIN']`; a token with no role claim is
|
|
68
|
+
* `MEMBER`, never admin; a shared-secret (`x-frigg` header) caller needs no
|
|
69
|
+
* role. Compared case-insensitively. Validated at load.
|
|
70
|
+
*/
|
|
71
|
+
orgAdminRoles?: string[];
|
|
65
72
|
authModes?: UserAuthModes;
|
|
66
73
|
jwtConfig?: AdopterJwtConfig;
|
|
67
74
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const Boom = require('@hapi/boom');
|
|
2
2
|
const { getLogger } = require('../../logs');
|
|
3
|
+
const { AUTH_SOURCES } = require('../user');
|
|
3
4
|
|
|
4
5
|
const log = getLogger('frigg.user.authentication');
|
|
5
6
|
|
|
@@ -68,10 +69,13 @@ class AuthenticateUser {
|
|
|
68
69
|
// Validate the API key (authentication)
|
|
69
70
|
await this.authenticateWithSharedSecret.execute(apiKey);
|
|
70
71
|
// Get user from x-frigg headers (authorization)
|
|
71
|
-
|
|
72
|
+
const user = await this.getUserFromXFriggHeaders.execute(
|
|
72
73
|
appUserId,
|
|
73
74
|
appOrgId
|
|
74
75
|
);
|
|
76
|
+
// The app's own backend: trusted to manage organization things.
|
|
77
|
+
user.setAuthSource(AUTH_SOURCES.SHARED_SECRET);
|
|
78
|
+
return user;
|
|
75
79
|
}
|
|
76
80
|
log.debug('No x-frigg-api-key header, skipping shared secret', {
|
|
77
81
|
eventName: `${log.name}.shared_secret_skipped`,
|
|
@@ -90,6 +94,7 @@ class AuthenticateUser {
|
|
|
90
94
|
// Detect JWT format (3 parts separated by dots)
|
|
91
95
|
if (token && token.split('.').length === 3) {
|
|
92
96
|
const user = await this.getUserFromAdopterJwt.execute(token);
|
|
97
|
+
user.setAuthSource(AUTH_SOURCES.ADOPTER_JWT);
|
|
93
98
|
// Validate x-frigg headers match JWT claims if present
|
|
94
99
|
if (appUserId || appOrgId) {
|
|
95
100
|
this.validateUserMatch(user, appUserId, appOrgId);
|
|
@@ -106,6 +111,7 @@ class AuthenticateUser {
|
|
|
106
111
|
const user = await this.getUserFromBearerToken.execute(
|
|
107
112
|
req.headers.authorization
|
|
108
113
|
);
|
|
114
|
+
user.setAuthSource(AUTH_SOURCES.BEARER_TOKEN);
|
|
109
115
|
// Validate x-frigg headers match token user if present
|
|
110
116
|
if (appUserId || appOrgId) {
|
|
111
117
|
this.validateUserMatch(user, appUserId, appOrgId);
|
package/user/user.js
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
const bcrypt = require('bcryptjs');
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* How the request that produced a `User` proved who it is. Set by
|
|
5
|
+
* AuthenticateUser, never inferred from a missing value. Organization roles
|
|
6
|
+
* (ADR-056 §3, §3.6) depend on it: a shared-secret caller is the app's own
|
|
7
|
+
* backend and is trusted to manage; a person with a token has the role the
|
|
8
|
+
* token carries, or `MEMBER`.
|
|
9
|
+
*/
|
|
10
|
+
const AUTH_SOURCES = Object.freeze({
|
|
11
|
+
SHARED_SECRET: 'shared-secret',
|
|
12
|
+
ADOPTER_JWT: 'adopter-jwt',
|
|
13
|
+
BEARER_TOKEN: 'bearer-token',
|
|
14
|
+
});
|
|
15
|
+
|
|
3
16
|
/**
|
|
4
17
|
* Represents a user in the system. The User class is a domain entity,
|
|
5
18
|
* @class User
|
|
@@ -21,6 +34,8 @@ class User {
|
|
|
21
34
|
// Organization role from an adopter token claim (ADR-056 §2). Per request:
|
|
22
35
|
// the role stored on a membership is only what the first token carried.
|
|
23
36
|
this.orgRole = null;
|
|
37
|
+
// How this request authenticated (one of AUTH_SOURCES); null until AuthenticateUser says.
|
|
38
|
+
this.authSource = null;
|
|
24
39
|
|
|
25
40
|
this.config = {
|
|
26
41
|
primary,
|
|
@@ -84,6 +99,14 @@ class User {
|
|
|
84
99
|
return this.config.individualUserRequired;
|
|
85
100
|
}
|
|
86
101
|
|
|
102
|
+
getAuthSource() {
|
|
103
|
+
return this.authSource;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
setAuthSource(authSource) {
|
|
107
|
+
this.authSource = authSource ?? null;
|
|
108
|
+
}
|
|
109
|
+
|
|
87
110
|
getOrgRole() {
|
|
88
111
|
return this.orgRole;
|
|
89
112
|
}
|
|
@@ -149,4 +172,4 @@ class User {
|
|
|
149
172
|
}
|
|
150
173
|
}
|
|
151
174
|
|
|
152
|
-
module.exports = { User };
|
|
175
|
+
module.exports = { User, AUTH_SOURCES };
|