@friggframework/core 2.0.0--canary.686.3e27a92.0 → 2.0.0--canary.663.8de669c.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 (184) hide show
  1. package/CLAUDE.md +0 -28
  2. package/README.md +0 -105
  3. package/application/commands/integration-commands.js +10 -20
  4. package/application/commands/scheduler-commands.js +7 -11
  5. package/application/commands/user-commands.js +5 -17
  6. package/application/index.js +2 -6
  7. package/core/Worker.js +7 -31
  8. package/core/create-handler.js +0 -9
  9. package/credential/repositories/credential-repository-documentdb.js +0 -34
  10. package/credential/repositories/credential-repository-interface.js +0 -15
  11. package/credential/repositories/credential-repository-mongo.js +0 -23
  12. package/credential/repositories/credential-repository-postgres.js +0 -28
  13. package/database/config.js +24 -48
  14. package/database/documentdb-utils.js +1 -6
  15. package/database/encryption/encryption-config.js +31 -10
  16. package/database/encryption/encryption-schema-registry.js +12 -93
  17. package/database/prisma.js +2 -4
  18. package/generated/prisma-mongodb/edge.js +4 -43
  19. package/generated/prisma-mongodb/index-browser.js +1 -40
  20. package/generated/prisma-mongodb/index.d.ts +111 -4739
  21. package/generated/prisma-mongodb/index.js +4 -43
  22. package/generated/prisma-mongodb/package.json +1 -1
  23. package/generated/prisma-mongodb/schema.prisma +0 -92
  24. package/generated/prisma-mongodb/wasm.js +4 -43
  25. package/generated/prisma-postgresql/edge.js +4 -43
  26. package/generated/prisma-postgresql/index-browser.js +1 -40
  27. package/generated/prisma-postgresql/index.d.ts +33 -4966
  28. package/generated/prisma-postgresql/index.js +4 -43
  29. package/generated/prisma-postgresql/package.json +1 -1
  30. package/generated/prisma-postgresql/schema.prisma +0 -87
  31. package/generated/prisma-postgresql/wasm.js +4 -43
  32. package/handlers/app-definition-loader.js +19 -121
  33. package/handlers/app-handler-helpers.js +3 -25
  34. package/handlers/backend-utils.js +25 -31
  35. package/handlers/middleware/admin-auth.js +1 -14
  36. package/handlers/routers/auth.js +10 -46
  37. package/handlers/routers/health.js +4 -13
  38. package/handlers/routers/integration-defined-routers.js +92 -144
  39. package/handlers/routers/integration-webhook-routers.js +64 -102
  40. package/handlers/workers/integration-defined-workers.js +2 -5
  41. package/index.js +1 -65
  42. package/infrastructure/scheduler/index.js +6 -19
  43. package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
  44. package/integrations/index.js +0 -1
  45. package/integrations/integration-router.js +96 -131
  46. package/integrations/repositories/integration-repository-documentdb.js +3 -35
  47. package/integrations/repositories/integration-repository-interface.js +1 -17
  48. package/integrations/repositories/integration-repository-mongo.js +76 -59
  49. package/integrations/repositories/integration-repository-postgres.js +76 -62
  50. package/integrations/tests/doubles/test-integration-repository.js +1 -12
  51. package/integrations/use-cases/create-integration.js +20 -55
  52. package/integrations/use-cases/get-integrations-for-user.js +40 -80
  53. package/integrations/use-cases/index.js +1 -7
  54. package/integrations/utils/map-integration-dto.js +14 -43
  55. package/modules/index.js +0 -14
  56. package/modules/module-factory.js +0 -12
  57. package/modules/module.js +2 -53
  58. package/modules/repositories/module-repository-documentdb.js +6 -51
  59. package/modules/repositories/module-repository-interface.js +2 -19
  60. package/modules/repositories/module-repository-mongo.js +74 -67
  61. package/modules/repositories/module-repository-postgres.js +74 -66
  62. package/modules/repositories/module-repository.js +1 -2
  63. package/modules/requester/requester.js +2 -27
  64. package/modules/use-cases/get-entities-for-user.js +13 -26
  65. package/modules/use-cases/get-entity-options-by-id.js +4 -10
  66. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  67. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  68. package/modules/use-cases/get-module.js +4 -10
  69. package/modules/use-cases/process-authorization-callback.js +19 -53
  70. package/modules/use-cases/refresh-entity-options.js +4 -10
  71. package/modules/use-cases/test-module-auth.js +4 -10
  72. package/package.json +5 -6
  73. package/prisma-mongodb/schema.prisma +0 -92
  74. package/prisma-postgresql/schema.prisma +0 -87
  75. package/queues/index.js +0 -25
  76. package/types/index.d.ts +0 -2
  77. package/user/repositories/user-repository-documentdb.js +2 -27
  78. package/user/repositories/user-repository-interface.js +0 -18
  79. package/user/repositories/user-repository-mongo.js +0 -24
  80. package/user/repositories/user-repository-postgres.js +0 -18
  81. package/user/use-cases/authenticate-user.js +1 -7
  82. package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
  83. package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
  84. package/user/user.js +1 -64
  85. package/credential/use-cases/credential-operations-for-user.js +0 -202
  86. package/credential/use-cases/reauthorize-credential-for-user.js +0 -73
  87. package/database/organization-where.js +0 -18
  88. package/database/unique-violation.js +0 -25
  89. package/extensions/bootstrap-runner.js +0 -43
  90. package/extensions/extension-loader.js +0 -187
  91. package/extensions/index.js +0 -41
  92. package/extensions/initialize-app.js +0 -123
  93. package/extensions/route-mounter.js +0 -103
  94. package/extensions/schema-composer.js +0 -188
  95. package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -216
  96. package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
  97. package/integrations/repositories/integration-not-found.js +0 -14
  98. package/integrations/schedule-config.js +0 -80
  99. package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
  100. package/integrations/use-cases/integration-operations-for-user.js +0 -256
  101. package/integrations/use-cases/resolve-integration-ownership.js +0 -177
  102. package/management-api/create-management-api-router.js +0 -49
  103. package/management-api/error-response.js +0 -116
  104. package/management-api/management-api-config.js +0 -151
  105. package/management-api/meta-router.js +0 -76
  106. package/management-api/openapi/build-openapi.js +0 -144
  107. package/management-api/openapi/components.js +0 -408
  108. package/management-api/openapi/v1-operations.js +0 -83
  109. package/management-api/openapi/v2-operations.js +0 -264
  110. package/management-api/route-registry.js +0 -411
  111. package/management-api/test-support/http-client.js +0 -39
  112. package/management-api/use-cases/get-management-api-meta.js +0 -57
  113. package/management-api/v1-deprecation.js +0 -120
  114. package/management-api/v2/create-v2-router.js +0 -85
  115. package/management-api/v2/dependencies.js +0 -281
  116. package/management-api/v2/dto.js +0 -143
  117. package/management-api/v2/handlers/authorization.js +0 -94
  118. package/management-api/v2/handlers/credentials.js +0 -33
  119. package/management-api/v2/handlers/entities.js +0 -77
  120. package/management-api/v2/handlers/integrations.js +0 -128
  121. package/management-api/v2/handlers/proxy.js +0 -34
  122. package/management-api/v2/request.js +0 -85
  123. package/management-api/v2/test-support/fake-dependencies.js +0 -103
  124. package/modules/domain/authorization-session.js +0 -144
  125. package/modules/entity-scope.js +0 -15
  126. package/modules/entity-type-registry.js +0 -311
  127. package/modules/proxy/proxy-errors.js +0 -20
  128. package/modules/proxy/proxy-path.js +0 -179
  129. package/modules/proxy/proxy-policy.js +0 -248
  130. package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
  131. package/modules/repositories/authorization-session-repository-factory.js +0 -31
  132. package/modules/repositories/authorization-session-repository-interface.js +0 -47
  133. package/modules/repositories/authorization-session-repository-prisma.js +0 -87
  134. package/modules/repositories/entity-not-found.js +0 -18
  135. package/modules/requester/proxy-request.js +0 -244
  136. package/modules/scope-config.js +0 -192
  137. package/modules/scope-policy.js +0 -112
  138. package/modules/scope-resolution.js +0 -288
  139. package/modules/scope-visibility.js +0 -247
  140. package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
  141. package/modules/tests/doubles/ownership-matching.js +0 -18
  142. package/modules/use-cases/authorize-entity-for-user.js +0 -61
  143. package/modules/use-cases/create-entity-from-credential.js +0 -113
  144. package/modules/use-cases/entity-operations-for-user.js +0 -162
  145. package/modules/use-cases/entity-types.js +0 -139
  146. package/modules/use-cases/execute-entity-proxy-request.js +0 -286
  147. package/modules/use-cases/get-authorization-requirements.js +0 -100
  148. package/modules/use-cases/get-entity-module-for-user.js +0 -113
  149. package/modules/use-cases/manage-access.js +0 -21
  150. package/modules/use-cases/multi-step-authorization.js +0 -329
  151. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
  152. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
  153. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
  154. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
  155. package/providers/index.js +0 -23
  156. package/providers/resolve-provider.js +0 -93
  157. package/providers/runtime-detection.js +0 -124
  158. package/queues/providers/index.js +0 -11
  159. package/queues/providers/netlify-background-provider.js +0 -150
  160. package/queues/providers/qstash-queue-provider.js +0 -219
  161. package/queues/providers/sqs-queue-provider.js +0 -70
  162. package/queues/queue-client-interface.js +0 -55
  163. package/queues/queue-message-auth.js +0 -193
  164. package/queues/queue-provider-factory.js +0 -101
  165. package/queues/queue-provider.js +0 -65
  166. package/queues/sqs-queue-client.js +0 -81
  167. package/types/modules/index.d.ts +0 -197
  168. package/types/user/index.d.ts +0 -134
  169. package/user/adopter-jwt-config.js +0 -177
  170. package/user/adopter-jwt-errors.js +0 -59
  171. package/user/adopter-token-verifier.js +0 -122
  172. package/user/app-user-helpers.js +0 -97
  173. package/user/create-user-authentication.js +0 -64
  174. package/user/membership.js +0 -51
  175. package/user/repositories/organization-membership-record.js +0 -19
  176. package/user/repositories/organization-membership-repository-documentdb.js +0 -106
  177. package/user/repositories/organization-membership-repository-factory.js +0 -42
  178. package/user/repositories/organization-membership-repository-interface.js +0 -57
  179. package/user/repositories/organization-membership-repository-mongo.js +0 -13
  180. package/user/repositories/organization-membership-repository-postgres.js +0 -29
  181. package/user/repositories/organization-membership-repository-prisma.js +0 -81
  182. package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
  183. package/user/use-cases/ensure-organization-membership.js +0 -92
  184. package/user/use-cases/find-individual-users-by-organization.js +0 -54
@@ -1,106 +0,0 @@
1
- const { prisma } = require('../../database/prisma');
2
- const {
3
- toObjectId,
4
- fromObjectId,
5
- findOne,
6
- findManyDrained,
7
- updateOne,
8
- } = require('../../database/documentdb-utils');
9
- const { DUPLICATE_KEY_CODE } = require('../../database/unique-violation');
10
- const {
11
- OrganizationMembershipRepositoryInterface,
12
- } = require('./organization-membership-repository-interface');
13
- const { toMembership } = require('./organization-membership-record');
14
- const { DEFAULT_ORG_ROLE } = require('../membership');
15
-
16
- const COLLECTION = 'OrganizationMembership';
17
-
18
- const parseDate = (value) => {
19
- if (!value) return undefined;
20
- const date = new Date(value.$date ?? value);
21
- return Number.isNaN(date.getTime()) ? undefined : date;
22
- };
23
-
24
- /**
25
- * Organization membership persistence for DocumentDB through raw commands.
26
- * Nothing in a membership is encrypted.
27
- */
28
- class OrganizationMembershipRepositoryDocumentDB extends OrganizationMembershipRepositoryInterface {
29
- constructor() {
30
- super();
31
- this.prisma = prisma;
32
- }
33
-
34
- async findMembership(individualUserId, organizationUserId) {
35
- const doc = await this._findDoc(individualUserId, organizationUserId);
36
- return doc ? this._toMembership(doc) : null;
37
- }
38
-
39
- async addMembership(individualUserId, organizationUserId, options = {}) {
40
- // $setOnInsert: an existing membership is left exactly as it is.
41
- const result = await updateOne(
42
- this.prisma,
43
- COLLECTION,
44
- this._pair(individualUserId, organizationUserId),
45
- {
46
- $setOnInsert: {
47
- orgRole: options.orgRole ?? DEFAULT_ORG_ROLE,
48
- createdAt: new Date(),
49
- },
50
- },
51
- { upsert: true }
52
- );
53
- // The loser of a first-login race can hit the unique pair; the row
54
- // exists then, so only other write errors are failures.
55
- const writeError = result?.writeErrors?.[0];
56
- if (writeError && writeError.code !== DUPLICATE_KEY_CODE) {
57
- throw new Error(
58
- `Adding organization membership failed: ${writeError.errmsg} (code: ${writeError.code})`
59
- );
60
- }
61
- const doc = await this._findDoc(individualUserId, organizationUserId);
62
- if (!doc) {
63
- throw new Error('Organization membership was not stored');
64
- }
65
- return this._toMembership(doc);
66
- }
67
-
68
- async listMembershipsForOrganization(organizationUserId) {
69
- // Drained: findMany returns only the first cursor batch (~101 documents),
70
- // which would silently drop members of a large organization.
71
- const docs = await findManyDrained(
72
- this.prisma,
73
- COLLECTION,
74
- { organizationUserId: toObjectId(organizationUserId) },
75
- { sort: { createdAt: 1 } }
76
- );
77
- return docs.map((doc) => this._toMembership(doc));
78
- }
79
-
80
- _pair(individualUserId, organizationUserId) {
81
- return {
82
- individualUserId: toObjectId(individualUserId),
83
- organizationUserId: toObjectId(organizationUserId),
84
- };
85
- }
86
-
87
- async _findDoc(individualUserId, organizationUserId) {
88
- return findOne(
89
- this.prisma,
90
- COLLECTION,
91
- this._pair(individualUserId, organizationUserId)
92
- );
93
- }
94
-
95
- _toMembership(doc) {
96
- return toMembership({
97
- id: fromObjectId(doc._id),
98
- individualUserId: fromObjectId(doc.individualUserId),
99
- organizationUserId: fromObjectId(doc.organizationUserId),
100
- orgRole: doc.orgRole ?? DEFAULT_ORG_ROLE,
101
- createdAt: parseDate(doc.createdAt),
102
- });
103
- }
104
- }
105
-
106
- module.exports = { OrganizationMembershipRepositoryDocumentDB };
@@ -1,42 +0,0 @@
1
- const databaseConfig = require('../../database/config');
2
- const {
3
- OrganizationMembershipRepositoryMongo,
4
- } = require('./organization-membership-repository-mongo');
5
- const {
6
- OrganizationMembershipRepositoryPrisma,
7
- } = require('./organization-membership-repository-prisma');
8
- const {
9
- OrganizationMembershipRepositoryPostgres,
10
- } = require('./organization-membership-repository-postgres');
11
- const {
12
- OrganizationMembershipRepositoryDocumentDB,
13
- } = require('./organization-membership-repository-documentdb');
14
-
15
- /**
16
- * Membership repository for the configured database. Every method returns
17
- * String ids whatever the database stores.
18
- *
19
- * @returns {import('./organization-membership-repository-interface').OrganizationMembershipRepositoryInterface}
20
- */
21
- function createOrganizationMembershipRepository() {
22
- switch (databaseConfig.DB_TYPE) {
23
- case 'mongodb':
24
- return new OrganizationMembershipRepositoryMongo();
25
- case 'postgresql':
26
- return new OrganizationMembershipRepositoryPostgres();
27
- case 'documentdb':
28
- return new OrganizationMembershipRepositoryDocumentDB();
29
- default:
30
- throw new Error(
31
- `Unsupported DB_TYPE: ${databaseConfig.DB_TYPE}. Supported values: 'mongodb', 'documentdb', 'postgresql'`
32
- );
33
- }
34
- }
35
-
36
- module.exports = {
37
- createOrganizationMembershipRepository,
38
- OrganizationMembershipRepositoryPrisma,
39
- OrganizationMembershipRepositoryMongo,
40
- OrganizationMembershipRepositoryPostgres,
41
- OrganizationMembershipRepositoryDocumentDB,
42
- };
@@ -1,57 +0,0 @@
1
- /**
2
- * Organization Membership Repository Interface (ADR-056 §3.6).
3
- *
4
- * Atomic persistence of "this person belongs to that organization": a row or
5
- * nothing. No policy and no reading of `IndividualUser.organizationId`: the
6
- * legacy pointer counts as an implicit membership, and that rule lives in the
7
- * domain (`user/membership.js`), applied by the use cases.
8
- *
9
- * Every method returns String ids whatever the database stores.
10
- *
11
- * @abstract
12
- */
13
- class OrganizationMembershipRepositoryInterface {
14
- /**
15
- * @param {string|number} individualUserId
16
- * @param {string|number} organizationUserId
17
- * @returns {Promise<Object|null>} The stored membership, or null.
18
- * @abstract
19
- */
20
- async findMembership(individualUserId, organizationUserId) {
21
- throw new Error('Method findMembership must be implemented by subclass');
22
- }
23
-
24
- /**
25
- * Adds a membership if there is none; never changes an existing one.
26
- * Idempotent, and safe against a concurrent identical call: both resolve
27
- * to the same row.
28
- *
29
- * @param {string|number} individualUserId
30
- * @param {string|number} organizationUserId
31
- * @param {Object} [options]
32
- * @param {string|null} [options.orgRole] Stored only when the row is
33
- * created; defaults to `MEMBER`.
34
- * @returns {Promise<Object>} The stored membership.
35
- * @abstract
36
- */
37
- async addMembership(individualUserId, organizationUserId, options) {
38
- throw new Error('Method addMembership must be implemented by subclass');
39
- }
40
-
41
- /**
42
- * Stored memberships of an organization, oldest first. Rows only: people
43
- * who are in the organization through the legacy pointer alone are not
44
- * here.
45
- *
46
- * @param {string|number} organizationUserId
47
- * @returns {Promise<Object[]>}
48
- * @abstract
49
- */
50
- async listMembershipsForOrganization(organizationUserId) {
51
- throw new Error(
52
- 'Method listMembershipsForOrganization must be implemented by subclass'
53
- );
54
- }
55
- }
56
-
57
- module.exports = { OrganizationMembershipRepositoryInterface };
@@ -1,13 +0,0 @@
1
- const {
2
- OrganizationMembershipRepositoryPrisma,
3
- } = require('./organization-membership-repository-prisma');
4
-
5
- /** Organization membership persistence for MongoDB: ids are ObjectId strings, used as they are. */
6
- class OrganizationMembershipRepositoryMongo extends OrganizationMembershipRepositoryPrisma {
7
- /** @protected */
8
- _toDbId(id) {
9
- return id;
10
- }
11
- }
12
-
13
- module.exports = { OrganizationMembershipRepositoryMongo };
@@ -1,29 +0,0 @@
1
- const {
2
- OrganizationMembershipRepositoryPrisma,
3
- } = require('./organization-membership-repository-prisma');
4
-
5
- /**
6
- * Organization membership persistence for PostgreSQL. The database keys are
7
- * Int, the application's are strings, so ids are converted in and the record
8
- * mapper stringifies them out.
9
- */
10
- class OrganizationMembershipRepositoryPostgres extends OrganizationMembershipRepositoryPrisma {
11
- /**
12
- * Strict: digits only. `parseInt` would turn '12abc' into 12 and query
13
- * the wrong person.
14
- * @protected
15
- */
16
- _toDbId(id) {
17
- const text = typeof id === 'number' ? String(id) : id;
18
- if (typeof text !== 'string' || !/^\d+$/.test(text)) {
19
- throw new Error(`Invalid ID: ${id} cannot be converted to integer`);
20
- }
21
- const parsed = Number(text);
22
- if (!Number.isSafeInteger(parsed)) {
23
- throw new Error(`Invalid ID: ${id} cannot be converted to integer`);
24
- }
25
- return parsed;
26
- }
27
- }
28
-
29
- module.exports = { OrganizationMembershipRepositoryPostgres };
@@ -1,81 +0,0 @@
1
- const { prisma } = require('../../database/prisma');
2
- const { isUniqueViolation } = require('../../database/unique-violation');
3
- const {
4
- OrganizationMembershipRepositoryInterface,
5
- } = require('./organization-membership-repository-interface');
6
- const { toMembership } = require('./organization-membership-record');
7
-
8
- /**
9
- * Organization membership persistence through the Prisma client. Both schemas
10
- * declare the same model; they differ only in the id type, which the subclass
11
- * supplies through `_toDbId` (MongoDB: ObjectId strings, PostgreSQL: Int).
12
- *
13
- * @abstract
14
- */
15
- class OrganizationMembershipRepositoryPrisma extends OrganizationMembershipRepositoryInterface {
16
- constructor(prismaClient = prisma) {
17
- super();
18
- this.prisma = prismaClient;
19
- }
20
-
21
- /**
22
- * @protected
23
- * @abstract
24
- */
25
- _toDbId(id) {
26
- throw new Error('Method _toDbId must be implemented by subclass');
27
- }
28
-
29
- async findMembership(individualUserId, organizationUserId) {
30
- const row = await this._findRow(individualUserId, organizationUserId);
31
- return row ? toMembership(row) : null;
32
- }
33
-
34
- async addMembership(individualUserId, organizationUserId, options = {}) {
35
- const pair = this._pair(individualUserId, organizationUserId);
36
- try {
37
- const row = await this.prisma.organizationMembership.upsert({
38
- where: { individualUserId_organizationUserId: pair },
39
- // `undefined` lets the column default (MEMBER) apply.
40
- create: { ...pair, orgRole: options.orgRole ?? undefined },
41
- update: {},
42
- });
43
- return toMembership(row);
44
- } catch (error) {
45
- // Two first logins can both miss the row and both try to create it.
46
- // The loser's insert hits the unique pair; the row is there now.
47
- if (!isUniqueViolation(error)) throw error;
48
- const row = await this._findRow(individualUserId, organizationUserId);
49
- if (!row) throw error;
50
- return toMembership(row);
51
- }
52
- }
53
-
54
- async listMembershipsForOrganization(organizationUserId) {
55
- const rows = await this.prisma.organizationMembership.findMany({
56
- where: { organizationUserId: this._toDbId(organizationUserId) },
57
- orderBy: { createdAt: 'asc' },
58
- });
59
- return rows.map(toMembership);
60
- }
61
-
62
- _pair(individualUserId, organizationUserId) {
63
- return {
64
- individualUserId: this._toDbId(individualUserId),
65
- organizationUserId: this._toDbId(organizationUserId),
66
- };
67
- }
68
-
69
- async _findRow(individualUserId, organizationUserId) {
70
- return this.prisma.organizationMembership.findUnique({
71
- where: {
72
- individualUserId_organizationUserId: this._pair(
73
- individualUserId,
74
- organizationUserId
75
- ),
76
- },
77
- });
78
- }
79
- }
80
-
81
- module.exports = { OrganizationMembershipRepositoryPrisma };
@@ -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 };