@friggframework/core 2.0.0--canary.684.0da024b.0 → 2.0.0--canary.679.a2bcc2f.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/CLAUDE.md +0 -2
  2. package/README.md +0 -105
  3. package/application/commands/integration-commands.js +2 -2
  4. package/application/commands/user-commands.js +5 -17
  5. package/credential/use-cases/credential-operations-for-user.js +25 -110
  6. package/credential/use-cases/reauthorize-credential-for-user.js +8 -15
  7. package/database/documentdb-utils.js +1 -6
  8. package/generated/prisma-mongodb/edge.js +4 -15
  9. package/generated/prisma-mongodb/index-browser.js +1 -12
  10. package/generated/prisma-mongodb/index.d.ts +86 -2027
  11. package/generated/prisma-mongodb/index.js +4 -15
  12. package/generated/prisma-mongodb/package.json +1 -1
  13. package/generated/prisma-mongodb/schema.prisma +0 -47
  14. package/generated/prisma-mongodb/wasm.js +4 -15
  15. package/generated/prisma-postgresql/edge.js +4 -15
  16. package/generated/prisma-postgresql/index-browser.js +1 -12
  17. package/generated/prisma-postgresql/index.d.ts +63 -2162
  18. package/generated/prisma-postgresql/index.js +4 -15
  19. package/generated/prisma-postgresql/package.json +1 -1
  20. package/generated/prisma-postgresql/schema.prisma +0 -44
  21. package/generated/prisma-postgresql/wasm.js +4 -15
  22. package/handlers/app-definition-loader.js +1 -15
  23. package/index.js +0 -18
  24. package/integrations/integration-router.js +73 -115
  25. package/integrations/repositories/integration-repository-documentdb.js +3 -35
  26. package/integrations/repositories/integration-repository-interface.js +1 -17
  27. package/integrations/repositories/integration-repository-mongo.js +76 -59
  28. package/integrations/repositories/integration-repository-postgres.js +76 -62
  29. package/integrations/tests/doubles/test-integration-repository.js +1 -12
  30. package/integrations/use-cases/create-integration.js +20 -55
  31. package/integrations/use-cases/get-integrations-for-user.js +40 -80
  32. package/integrations/use-cases/index.js +1 -5
  33. package/integrations/use-cases/integration-operations-for-user.js +24 -55
  34. package/management-api/management-api-config.js +0 -3
  35. package/management-api/openapi/components.js +0 -40
  36. package/management-api/openapi/v2-operations.js +1 -13
  37. package/management-api/v2/dependencies.js +15 -40
  38. package/management-api/v2/dto.js +0 -20
  39. package/management-api/v2/handlers/authorization.js +0 -1
  40. package/management-api/v2/handlers/credentials.js +2 -6
  41. package/management-api/v2/handlers/entities.js +7 -10
  42. package/management-api/v2/handlers/integrations.js +3 -6
  43. package/management-api/v2/request.js +1 -32
  44. package/modules/index.js +0 -8
  45. package/modules/repositories/module-repository-documentdb.js +4 -48
  46. package/modules/repositories/module-repository-interface.js +2 -19
  47. package/modules/repositories/module-repository-mongo.js +73 -65
  48. package/modules/repositories/module-repository-postgres.js +73 -64
  49. package/modules/use-cases/entity-operations-for-user.js +13 -50
  50. package/modules/use-cases/entity-types.js +7 -31
  51. package/modules/use-cases/execute-entity-proxy-request.js +0 -15
  52. package/modules/use-cases/get-entities-for-user.js +5 -16
  53. package/modules/use-cases/get-entity-module-for-user.js +3 -23
  54. package/modules/use-cases/get-entity-options-by-id.js +3 -9
  55. package/modules/use-cases/get-module.js +3 -9
  56. package/modules/use-cases/multi-step-authorization.js +12 -39
  57. package/modules/use-cases/process-authorization-callback.js +18 -52
  58. package/modules/use-cases/refresh-entity-options.js +3 -9
  59. package/modules/use-cases/test-module-auth.js +3 -9
  60. package/package.json +5 -6
  61. package/prisma-mongodb/schema.prisma +0 -47
  62. package/prisma-postgresql/schema.prisma +0 -44
  63. package/types/index.d.ts +0 -1
  64. package/types/modules/index.d.ts +0 -87
  65. package/user/repositories/user-repository-documentdb.js +2 -27
  66. package/user/repositories/user-repository-interface.js +0 -18
  67. package/user/repositories/user-repository-mongo.js +0 -24
  68. package/user/repositories/user-repository-postgres.js +0 -18
  69. package/user/use-cases/authenticate-user.js +1 -7
  70. package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
  71. package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
  72. package/user/user.js +1 -64
  73. package/database/organization-where.js +0 -18
  74. package/database/unique-violation.js +0 -25
  75. package/integrations/repositories/integration-not-found.js +0 -14
  76. package/integrations/use-cases/resolve-integration-ownership.js +0 -177
  77. package/modules/entity-scope.js +0 -15
  78. package/modules/scope-config.js +0 -192
  79. package/modules/scope-policy.js +0 -112
  80. package/modules/scope-resolution.js +0 -288
  81. package/modules/scope-visibility.js +0 -239
  82. package/modules/tests/doubles/ownership-matching.js +0 -18
  83. package/modules/use-cases/authorize-entity-for-user.js +0 -61
  84. package/modules/use-cases/create-entity-from-credential.js +0 -113
  85. package/modules/use-cases/manage-access.js +0 -21
  86. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
  87. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
  88. package/types/user/index.d.ts +0 -134
  89. package/user/adopter-jwt-config.js +0 -177
  90. package/user/adopter-jwt-errors.js +0 -59
  91. package/user/adopter-token-verifier.js +0 -122
  92. package/user/app-user-helpers.js +0 -97
  93. package/user/create-user-authentication.js +0 -64
  94. package/user/membership.js +0 -51
  95. package/user/repositories/organization-membership-record.js +0 -19
  96. package/user/repositories/organization-membership-repository-documentdb.js +0 -106
  97. package/user/repositories/organization-membership-repository-factory.js +0 -42
  98. package/user/repositories/organization-membership-repository-interface.js +0 -57
  99. package/user/repositories/organization-membership-repository-mongo.js +0 -13
  100. package/user/repositories/organization-membership-repository-postgres.js +0 -29
  101. package/user/repositories/organization-membership-repository-prisma.js +0 -81
  102. package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
  103. package/user/use-cases/ensure-organization-membership.js +0 -92
  104. package/user/use-cases/find-individual-users-by-organization.js +0 -54
@@ -1,220 +0,0 @@
1
- /* eslint-disable camelcase -- JWT claim names (org_id, org_role) and reason codes are snake_case by spec */
2
- const jose = require('jose');
3
-
4
- /**
5
- * Key material and token signing for adopter-JWT tests. Real keys and real
6
- * signatures: nothing here fakes the verifier.
7
- */
8
- async function createKeys() {
9
- const es = await jose.generateKeyPair('ES256');
10
- const rs = await jose.generateKeyPair('RS256');
11
- const attacker = await jose.generateKeyPair('ES256');
12
- const publicJwks = {
13
- keys: [
14
- {
15
- ...(await jose.exportJWK(es.publicKey)),
16
- kid: 'es-1',
17
- alg: 'ES256',
18
- use: 'sig',
19
- },
20
- {
21
- ...(await jose.exportJWK(rs.publicKey)),
22
- kid: 'rs-1',
23
- alg: 'RS256',
24
- use: 'sig',
25
- },
26
- ],
27
- };
28
- return {
29
- es,
30
- rs,
31
- attacker,
32
- publicJwks,
33
- localJwks: jose.createLocalJWKSet(publicJwks),
34
- hmacSecret: 'a-shared-secret-that-is-long-enough-for-hs256',
35
- };
36
- }
37
-
38
- /**
39
- * @param {Object} claims
40
- * @param {Object} opts
41
- * @param {Object|Uint8Array} opts.key - signing key
42
- * @param {string} [opts.alg='ES256']
43
- * @param {string} [opts.kid]
44
- * @param {string|null} [opts.exp='5m'] - null omits exp
45
- */
46
- async function signToken(
47
- claims,
48
- { key, alg = 'ES256', kid, exp = '5m', iss, aud, nbf } = {}
49
- ) {
50
- const jwt = new jose.SignJWT(claims)
51
- .setProtectedHeader(kid ? { alg, kid } : { alg })
52
- .setIssuedAt();
53
- if (exp !== null) jwt.setExpirationTime(exp);
54
- if (iss) jwt.setIssuer(iss);
55
- if (aud) jwt.setAudience(aud);
56
- if (nbf) jwt.setNotBefore(nbf);
57
- return jwt.sign(key);
58
- }
59
-
60
- const b64url = (value) =>
61
- Buffer.from(JSON.stringify(value)).toString('base64url');
62
-
63
- /** An unsigned token: `alg: none`, empty signature. */
64
- function unsignedToken(claims) {
65
- return `${b64url({ alg: 'none', typ: 'JWT' })}.${b64url({
66
- ...claims,
67
- exp: Math.floor(Date.now() / 1000) + 300,
68
- })}.`;
69
- }
70
-
71
- /**
72
- * Resolves each `wait()` only once `parties` callers have arrived, so a test
73
- * can make concurrent requests all read before any of them writes.
74
- */
75
- function createBarrier(parties) {
76
- let arrived = 0;
77
- let release;
78
- const gate = new Promise((resolve) => {
79
- release = resolve;
80
- });
81
- return {
82
- async wait() {
83
- arrived += 1;
84
- if (arrived >= parties) release();
85
- await gate;
86
- },
87
- };
88
- }
89
-
90
- /**
91
- * Stateful fake of the user repository, built from jest.fn so every call is
92
- * observable. Individuals carry `organizationId`, as the real repositories
93
- * return it. Like the database, it refuses a second individual with the same
94
- * `appUserId` (a Prisma-shaped `P2002`), which is what a first-login race hits.
95
- *
96
- * @param {Object} [options]
97
- * @param {number} [options.findBarrier] Make the first N calls to
98
- * `findIndividualUserByAppUserId` wait for each other (see createBarrier).
99
- */
100
- function createUserRepositoryDouble({
101
- individuals = [],
102
- organizations = [],
103
- findBarrier = 0,
104
- } = {}) {
105
- const individualUsers = new Map(
106
- individuals.map((u) => [u.appUserId, { ...u }])
107
- );
108
- const organizationUsers = new Map(
109
- organizations.map((o) => [o.appOrgId, { ...o }])
110
- );
111
- const barrier = findBarrier > 1 ? createBarrier(findBarrier) : null;
112
- let barrierCalls = 0;
113
- let seq = 0;
114
-
115
- const repo = {
116
- findIndividualUserByAppUserId: jest.fn(async (appUserId) => {
117
- const found = individualUsers.get(appUserId) ?? null;
118
- if (barrier && barrierCalls < findBarrier) {
119
- barrierCalls += 1;
120
- await barrier.wait();
121
- }
122
- return found && { ...found };
123
- }),
124
- findOrganizationUserByAppOrgId: jest.fn(
125
- async (appOrgId) => organizationUsers.get(appOrgId) ?? null
126
- ),
127
- createIndividualUser: jest.fn(async (params) => {
128
- if (individualUsers.has(params.appUserId)) {
129
- throw Object.assign(
130
- new Error('Unique constraint failed on appUserId'),
131
- { code: 'P2002' }
132
- );
133
- }
134
- const created = {
135
- id: `ind-${++seq}`,
136
- organizationId: undefined,
137
- ...params,
138
- };
139
- individualUsers.set(created.appUserId, created);
140
- return { ...created };
141
- }),
142
- createOrganizationUser: jest.fn(async (params) => {
143
- const created = { id: `org-${++seq}`, ...params };
144
- organizationUsers.set(created.appOrgId, created);
145
- return created;
146
- }),
147
- findIndividualUserById: jest.fn(async (id) => {
148
- for (const u of individualUsers.values()) {
149
- if (u.id === id) return { ...u };
150
- }
151
- return null;
152
- }),
153
- // Like the database: sets the pointer only while it is unset.
154
- linkIndividualToOrganizationIfUnset: jest.fn(
155
- async (individualId, organizationId) => {
156
- for (const u of individualUsers.values()) {
157
- if (u.id !== individualId) continue;
158
- const unset =
159
- u.organizationId === undefined ||
160
- u.organizationId === null ||
161
- u.organizationId === '';
162
- if (unset) u.organizationId = organizationId;
163
- return { ...u };
164
- }
165
- return null;
166
- }
167
- ),
168
- };
169
- repo.state = { individualUsers, organizationUsers };
170
- return repo;
171
- }
172
-
173
- /**
174
- * In-memory organization membership repository: atomic, like the real ones. The
175
- * pair is unique and `addMembership` is an upsert. It knows nothing of the
176
- * legacy `organizationId` pointer; the domain (user/membership.js) does.
177
- */
178
- function createMembershipRepositoryDouble() {
179
- const { toMembership } = require('../../repositories/organization-membership-record');
180
- const rows = new Map();
181
- const key = (i, o) => `${i}|${o}`;
182
- let seq = 0;
183
-
184
- const repo = {
185
- findMembership: jest.fn(async (individualId, organizationId) => {
186
- const row = rows.get(key(individualId, organizationId));
187
- return row ? toMembership(row) : null;
188
- }),
189
- addMembership: jest.fn(
190
- async (individualId, organizationId, { orgRole } = {}) => {
191
- const k = key(individualId, organizationId);
192
- if (!rows.has(k)) {
193
- rows.set(k, {
194
- id: `mem-${++seq}`,
195
- individualUserId: individualId,
196
- organizationUserId: organizationId,
197
- orgRole: orgRole ?? 'MEMBER',
198
- createdAt: new Date(),
199
- });
200
- }
201
- return toMembership(rows.get(k));
202
- }
203
- ),
204
- listMembershipsForOrganization: jest.fn(async (organizationId) =>
205
- [...rows.values()]
206
- .filter((r) => r.organizationUserId === organizationId)
207
- .map(toMembership)
208
- ),
209
- };
210
- repo.rows = rows;
211
- return repo;
212
- }
213
-
214
- module.exports = {
215
- createKeys,
216
- signToken,
217
- unsignedToken,
218
- createUserRepositoryDouble,
219
- createMembershipRepositoryDouble,
220
- };
@@ -1,92 +0,0 @@
1
- const { getLogger } = require('../../logs');
2
- const { isPresent, implicitMembership } = require('../membership');
3
-
4
- const log = getLogger('frigg.user.membership');
5
-
6
- /**
7
- * Makes sure a person is a member of the organization a request is acting in
8
- * (ADR-056 §3.6). Shared by the adopter-token and x-frigg header paths.
9
- *
10
- * - Already a member (a stored row, or the legacy `organizationId` pointer
11
- * read as an implicit membership): nothing is written.
12
- * - Not a member: a membership is added, unless the caller asks for existing
13
- * memberships only (`requireExisting`), in which case nothing is written and
14
- * `membership` is null so the caller can refuse.
15
- * - The legacy `organizationId` is set when the person has none (their first
16
- * link) and never changed afterwards. It is a pointer for old readers, not
17
- * the active organization: that is whichever organization the request names.
18
- *
19
- * Adding is an idempotent upsert, so two first logins racing for different
20
- * organizations both succeed.
21
- *
22
- * @class EnsureOrganizationMembership
23
- */
24
- class EnsureOrganizationMembership {
25
- /**
26
- * @param {Object} params
27
- * @param {import('../repositories/user-repository-interface').UserRepositoryInterface} params.userRepository
28
- * @param {import('../repositories/organization-membership-repository-interface').OrganizationMembershipRepositoryInterface} params.organizationMembershipRepository
29
- */
30
- constructor({ userRepository, organizationMembershipRepository }) {
31
- this.userRepository = userRepository;
32
- this.organizationMembershipRepository = organizationMembershipRepository;
33
- }
34
-
35
- /**
36
- * @param {Object} params
37
- * @param {Object|null} params.individual Individual user as the repository returns it.
38
- * @param {Object|null} params.organization The active organization user.
39
- * @param {string|null} [params.orgRole] Role to store with a new membership.
40
- * @param {boolean} [params.requireExisting=false] Refuse to add.
41
- * @returns {Promise<{individual: Object|null, membership: Object|null}>}
42
- * `individual` may be the updated record (legacy pointer set).
43
- */
44
- async execute({
45
- individual,
46
- organization,
47
- orgRole = null,
48
- requireExisting = false,
49
- }) {
50
- // A request that names no organization, or no person (organization-only
51
- // auth), has no membership to check or add.
52
- if (!individual || !organization) {
53
- return { individual, membership: null };
54
- }
55
-
56
- const existing =
57
- (await this.organizationMembershipRepository.findMembership(
58
- individual.id,
59
- organization.id
60
- )) ?? implicitMembership(individual, organization.id);
61
- if (existing) {
62
- return { individual, membership: existing };
63
- }
64
- if (requireExisting) {
65
- return { individual, membership: null };
66
- }
67
-
68
- const membership =
69
- await this.organizationMembershipRepository.addMembership(
70
- individual.id,
71
- organization.id,
72
- { orgRole }
73
- );
74
- log.debug('Organization membership added', {
75
- eventName: `${log.name}.added`,
76
- });
77
-
78
- // The database repeats this condition in the update itself, so a
79
- // concurrent first link by another request cannot be overwritten.
80
- let updated = individual;
81
- if (!isPresent(individual.organizationId)) {
82
- updated =
83
- (await this.userRepository.linkIndividualToOrganizationIfUnset(
84
- individual.id,
85
- organization.id
86
- )) ?? individual;
87
- }
88
- return { individual: updated, membership };
89
- }
90
- }
91
-
92
- module.exports = { EnsureOrganizationMembership };
@@ -1,54 +0,0 @@
1
- const { sameId } = require('../membership');
2
-
3
- /**
4
- * The individual users who belong to an organization (ADR-056 §3.6): the
5
- * people with a stored membership, plus those the legacy
6
- * `IndividualUser.organizationId` pointer places there, each once.
7
- *
8
- * Both repositories stay atomic; the union is composed here. Reading only the
9
- * legacy pointer would list a person in the first organization they ever
10
- * joined and in no other.
11
- *
12
- * @class FindIndividualUsersByOrganization
13
- */
14
- class FindIndividualUsersByOrganization {
15
- /**
16
- * @param {Object} params
17
- * @param {import('../repositories/user-repository-interface').UserRepositoryInterface} params.userRepository
18
- * @param {import('../repositories/organization-membership-repository-interface').OrganizationMembershipRepositoryInterface} params.organizationMembershipRepository
19
- */
20
- constructor({ userRepository, organizationMembershipRepository }) {
21
- this.userRepository = userRepository;
22
- this.organizationMembershipRepository = organizationMembershipRepository;
23
- }
24
-
25
- /**
26
- * @param {string|number} organizationUserId
27
- * @returns {Promise<Object[]>} Individual users, legacy-pointer people first.
28
- */
29
- async execute(organizationUserId) {
30
- const [legacy, memberships] = await Promise.all([
31
- this.userRepository.findIndividualUsersByOrganizationId(
32
- organizationUserId
33
- ),
34
- this.organizationMembershipRepository.listMembershipsForOrganization(
35
- organizationUserId
36
- ),
37
- ]);
38
-
39
- const users = [...(legacy || [])];
40
- for (const { individualUserId } of memberships) {
41
- if (users.some((u) => sameId(u.id ?? u._id, individualUserId))) {
42
- continue;
43
- }
44
- const individual = await this.userRepository.findIndividualUserById(
45
- individualUserId
46
- );
47
- // A membership whose person was deleted is skipped, not an error.
48
- if (individual) users.push(individual);
49
- }
50
- return users;
51
- }
52
- }
53
-
54
- module.exports = { FindIndividualUsersByOrganization };