@friggframework/core 2.0.0--canary.689.386395c.0 → 2.0.0--canary.690.d043f23.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 +28 -30
- package/README.md +105 -0
- package/application/commands/integration-commands.js +20 -68
- package/application/commands/scheduler-commands.js +11 -7
- package/application/commands/user-commands.js +17 -5
- package/application/index.js +6 -2
- package/core/Worker.js +31 -7
- package/core/create-handler.js +9 -0
- package/credential/repositories/credential-repository-documentdb.js +34 -0
- package/credential/repositories/credential-repository-interface.js +15 -0
- package/credential/repositories/credential-repository-mongo.js +23 -0
- package/credential/repositories/credential-repository-postgres.js +28 -0
- package/credential/use-cases/credential-operations-for-user.js +202 -0
- package/credential/use-cases/reauthorize-credential-for-user.js +73 -0
- package/database/config.js +48 -24
- package/database/documentdb-utils.js +2 -0
- package/database/encryption/encryption-schema-registry.js +93 -12
- package/database/organization-where.js +18 -0
- package/database/prisma.js +4 -2
- package/database/unique-violation.js +25 -0
- package/errors/index.js +0 -4
- package/extensions/bootstrap-runner.js +43 -0
- package/extensions/extension-loader.js +167 -0
- package/extensions/index.js +41 -0
- package/extensions/initialize-app.js +123 -0
- package/extensions/route-mounter.js +103 -0
- package/extensions/schema-composer.js +188 -0
- package/generated/prisma-mongodb/edge.js +43 -4
- package/generated/prisma-mongodb/index-browser.js +40 -1
- package/generated/prisma-mongodb/index.d.ts +4761 -133
- package/generated/prisma-mongodb/index.js +43 -4
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +92 -0
- package/generated/prisma-mongodb/wasm.js +43 -4
- package/generated/prisma-postgresql/edge.js +43 -4
- package/generated/prisma-postgresql/index-browser.js +40 -1
- package/generated/prisma-postgresql/index.d.ts +4961 -28
- package/generated/prisma-postgresql/index.js +43 -4
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +87 -0
- package/generated/prisma-postgresql/wasm.js +43 -4
- package/handlers/WEBHOOKS.md +0 -29
- package/handlers/app-definition-loader.js +118 -19
- package/handlers/app-handler-helpers.js +25 -3
- package/handlers/backend-utils.js +31 -25
- package/handlers/middleware/admin-auth.js +14 -1
- package/handlers/routers/auth.js +46 -10
- package/handlers/routers/health.js +11 -4
- package/handlers/routers/integration-defined-routers.js +144 -92
- package/handlers/routers/integration-webhook-routers.js +102 -64
- package/handlers/workers/integration-defined-workers.js +5 -2
- package/index.js +63 -3
- package/infrastructure/scheduler/index.js +19 -6
- package/infrastructure/scheduler/netlify-scheduler-adapter.js +216 -0
- package/infrastructure/scheduler/scheduled-job-repository.js +118 -0
- package/infrastructure/scheduler/scheduler-service-factory.js +55 -7
- package/integrations/index.js +1 -0
- package/integrations/integration-base.js +0 -29
- package/integrations/integration-router.js +131 -96
- package/integrations/repositories/integration-mapping-repository-documentdb.js +0 -50
- package/integrations/repositories/integration-mapping-repository-interface.js +0 -15
- package/integrations/repositories/integration-mapping-repository-mongo.js +0 -29
- package/integrations/repositories/integration-mapping-repository-postgres.js +0 -30
- package/integrations/repositories/integration-mapping-repository.js +0 -29
- package/integrations/repositories/integration-not-found.js +14 -0
- package/integrations/repositories/integration-repository-documentdb.js +35 -35
- package/integrations/repositories/integration-repository-interface.js +17 -31
- package/integrations/repositories/integration-repository-mongo.js +60 -129
- package/integrations/repositories/integration-repository-postgres.js +62 -134
- package/integrations/repositories/process-repository-mongo.js +1 -1
- 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 +256 -0
- package/integrations/use-cases/resolve-integration-ownership.js +177 -0
- package/integrations/utils/map-integration-dto.js +43 -14
- package/management-api/create-management-api-router.js +49 -0
- package/management-api/error-response.js +116 -0
- package/management-api/management-api-config.js +151 -0
- package/management-api/meta-router.js +57 -0
- package/management-api/openapi/build-openapi.js +144 -0
- package/management-api/openapi/components.js +414 -0
- package/management-api/openapi/v1-operations.js +83 -0
- package/management-api/openapi/v2-operations.js +264 -0
- package/management-api/route-registry.js +294 -0
- package/management-api/test-support/http-client.js +39 -0
- package/management-api/use-cases/get-management-api-meta.js +57 -0
- package/management-api/v1-deprecation.js +120 -0
- package/management-api/v2/create-v2-router.js +85 -0
- package/management-api/v2/dependencies.js +281 -0
- package/management-api/v2/dto.js +143 -0
- package/management-api/v2/handlers/authorization.js +94 -0
- package/management-api/v2/handlers/credentials.js +33 -0
- package/management-api/v2/handlers/entities.js +77 -0
- package/management-api/v2/handlers/integrations.js +128 -0
- package/management-api/v2/handlers/proxy.js +34 -0
- package/management-api/v2/request.js +85 -0
- package/management-api/v2/test-support/fake-dependencies.js +103 -0
- package/modules/domain/authorization-session.js +144 -0
- package/modules/entity-scope.js +15 -0
- package/modules/entity-type-registry.js +311 -0
- package/modules/index.js +14 -0
- package/modules/module-factory.js +12 -0
- package/modules/module.js +53 -2
- package/modules/proxy/proxy-bases.js +169 -0
- package/modules/proxy/proxy-errors.js +31 -0
- package/modules/proxy/proxy-path.js +183 -0
- package/modules/proxy/proxy-policy.js +265 -0
- package/modules/repositories/authorization-session-repository-documentdb.js +112 -0
- package/modules/repositories/authorization-session-repository-factory.js +31 -0
- package/modules/repositories/authorization-session-repository-interface.js +47 -0
- package/modules/repositories/authorization-session-repository-prisma.js +87 -0
- package/modules/repositories/entity-not-found.js +18 -0
- package/modules/repositories/module-repository-documentdb.js +51 -6
- package/modules/repositories/module-repository-interface.js +19 -2
- package/modules/repositories/module-repository-mongo.js +67 -74
- package/modules/repositories/module-repository-postgres.js +66 -74
- package/modules/repositories/module-repository.js +2 -1
- package/modules/requester/proxy-request.js +245 -0
- package/modules/requester/requester.js +27 -2
- package/modules/scope-config.js +192 -0
- package/modules/scope-policy.js +112 -0
- package/modules/scope-resolution.js +288 -0
- package/modules/scope-visibility.js +247 -0
- package/modules/tests/doubles/in-memory-authorization-session-repository.js +45 -0
- package/modules/tests/doubles/ownership-matching.js +18 -0
- package/modules/use-cases/authorize-entity-for-user.js +61 -0
- package/modules/use-cases/create-entity-from-credential.js +113 -0
- package/modules/use-cases/entity-operations-for-user.js +162 -0
- package/modules/use-cases/entity-types.js +139 -0
- package/modules/use-cases/execute-entity-proxy-request.js +331 -0
- package/modules/use-cases/get-authorization-requirements.js +100 -0
- package/modules/use-cases/get-entities-for-user.js +26 -13
- package/modules/use-cases/get-entity-module-for-user.js +113 -0
- package/modules/use-cases/get-entity-options-by-id.js +10 -4
- 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 +10 -4
- package/modules/use-cases/manage-access.js +21 -0
- package/modules/use-cases/multi-step-authorization.js +329 -0
- package/modules/use-cases/process-authorization-callback.js +53 -19
- package/modules/use-cases/refresh-entity-options.js +10 -4
- package/modules/use-cases/test-module-auth.js +10 -4
- package/package.json +6 -5
- package/prisma-mongodb/schema.prisma +92 -0
- package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +28 -0
- package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
- package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
- package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +28 -0
- package/prisma-postgresql/schema.prisma +87 -0
- package/providers/index.js +13 -0
- package/providers/resolve-provider.js +93 -0
- package/queues/index.js +25 -0
- package/queues/providers/index.js +11 -0
- package/queues/providers/netlify-background-provider.js +150 -0
- package/queues/providers/qstash-queue-provider.js +199 -0
- package/queues/providers/sqs-queue-provider.js +70 -0
- package/queues/queue-client-interface.js +55 -0
- package/queues/queue-message-auth.js +187 -0
- package/queues/queue-provider-factory.js +101 -0
- package/queues/queue-provider.js +65 -0
- package/queues/sqs-queue-client.js +81 -0
- package/types/index.d.ts +2 -0
- package/types/modules/index.d.ts +214 -0
- package/types/user/index.d.ts +134 -0
- package/user/adopter-jwt-config.js +177 -0
- package/user/adopter-jwt-errors.js +59 -0
- package/user/adopter-token-verifier.js +122 -0
- package/user/app-user-helpers.js +97 -0
- package/user/create-user-authentication.js +64 -0
- package/user/membership.js +51 -0
- package/user/repositories/organization-membership-record.js +19 -0
- package/user/repositories/organization-membership-repository-documentdb.js +106 -0
- package/user/repositories/organization-membership-repository-factory.js +42 -0
- package/user/repositories/organization-membership-repository-interface.js +57 -0
- package/user/repositories/organization-membership-repository-mongo.js +13 -0
- package/user/repositories/organization-membership-repository-postgres.js +29 -0
- package/user/repositories/organization-membership-repository-prisma.js +81 -0
- package/user/repositories/user-repository-documentdb.js +27 -2
- package/user/repositories/user-repository-interface.js +18 -0
- package/user/repositories/user-repository-mongo.js +24 -0
- package/user/repositories/user-repository-postgres.js +18 -0
- package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
- package/user/use-cases/authenticate-user.js +7 -1
- package/user/use-cases/ensure-organization-membership.js +92 -0
- package/user/use-cases/find-individual-users-by-organization.js +54 -0
- package/user/use-cases/get-user-from-adopter-jwt.js +95 -124
- package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
- package/user/user.js +64 -1
- package/errors/mapping-already-exists-error.js +0 -14
- package/integrations/use-cases/add-entities-to-integration.js +0 -73
- package/integrations/use-cases/remove-entities-from-integration.js +0 -38
- package/integrations/utils/assert-entity-ids-input.js +0 -23
package/CLAUDE.md
CHANGED
|
@@ -206,41 +206,11 @@ packages/core/
|
|
|
206
206
|
- `delete-integration-for-user.js` - Remove integration
|
|
207
207
|
- `get-integration-instance.js` - Load integration with modules
|
|
208
208
|
- `load-integration-context.js` - Full integration context loading
|
|
209
|
-
- `add-entities-to-integration.js` / `remove-entities-from-integration.js` - Attach or detach entities on an existing integration
|
|
210
209
|
|
|
211
210
|
**Repositories**:
|
|
212
211
|
- `integration-repository-factory.js` - Creates database-specific repositories
|
|
213
212
|
- `integration-repository-mongo.js` - MongoDB implementation
|
|
214
213
|
- `integration-repository-postgres.js` - PostgreSQL implementation
|
|
215
|
-
- `addEntitiesToIntegration(integrationId, entityIds)` /
|
|
216
|
-
`removeEntitiesFromIntegration(integrationId, entityIds)` change the entity
|
|
217
|
-
set of an existing integration (all three adapters; `null` for an unknown
|
|
218
|
-
integration). Reach them through the `addEntitiesToIntegration` /
|
|
219
|
-
`removeEntitiesFromIntegration` commands: the add use case
|
|
220
|
-
(`add-entities-to-integration.js`) rejects unknown entities
|
|
221
|
-
(`ENTITIES_NOT_FOUND`, 404) and entities of another user
|
|
222
|
-
(`ENTITY_USER_MISMATCH`, 403). Re-adding an attached entity or removing a
|
|
223
|
-
detached one changes nothing; a detached entity is not deleted.
|
|
224
|
-
`integration-repository-entities-parity.test.js` runs every adapter against
|
|
225
|
-
real databases when `INTEGRATION_ENTITIES_PARITY_MONGO_URL` /
|
|
226
|
-
`INTEGRATION_ENTITIES_PARITY_POSTGRES_URL` are set. The DocumentDB adapter
|
|
227
|
-
writes only `Integration.entityIds` (as its `createIntegration` does), not
|
|
228
|
-
`Entity.integrationIds`.
|
|
229
|
-
**One credential, many accounts**: when one credential covers several
|
|
230
|
-
provider accounts, give each account its own entity (externalId = account
|
|
231
|
-
id) and attach them all to one integration; webhooks then route with
|
|
232
|
-
`findIntegrationByEntityExternalId`. Three traps:
|
|
233
|
-
(1) `IntegrationBase` keys modules by module name, so `this.<moduleKey>`
|
|
234
|
-
holds only the last entity's module: pick the account's entity from
|
|
235
|
-
`this.entities` by `externalId`.
|
|
236
|
-
(2) `findIntegrationByEntityExternalId` counts every entity with that
|
|
237
|
-
externalId, attached or not: reuse an existing entity (`findEntity`) when an
|
|
238
|
-
account comes back, or delete it when it is revoked, never create a second
|
|
239
|
-
one.
|
|
240
|
-
(3) `createIntegration` reuses an integration only for the exact same entity
|
|
241
|
-
set, so after an attach a second `createIntegration` with the original
|
|
242
|
-
entities creates a new integration: re-authorization must update the
|
|
243
|
-
existing integration, not create one.
|
|
244
214
|
- `integration-mapping-repository-*.js` - Mapping data persistence.
|
|
245
215
|
`queryMappings(integrationId, { where, orderBy, skip, take, omit })`
|
|
246
216
|
returns `{ mappings, total }`: one filtered, ordered page of an
|
|
@@ -362,12 +332,14 @@ class MyIntegration extends IntegrationBase {
|
|
|
362
332
|
- Password-based (bcrypt hashed)
|
|
363
333
|
- Token-based (Bearer tokens)
|
|
364
334
|
- App-based (external app user IDs)
|
|
335
|
+
- Adopter JWT (`authModes.adopterJwt`, `jwtConfig`; `jose`, JWKS or HMAC; see `README.md`)
|
|
365
336
|
|
|
366
337
|
**Use Cases**:
|
|
367
338
|
- `login-user.js` - User authentication
|
|
368
339
|
- `create-individual-user.js` - Create personal account
|
|
369
340
|
- `create-organization-user.js` - Create business account
|
|
370
341
|
- `get-user-from-bearer-token.js` - Token authentication
|
|
342
|
+
- `get-user-from-adopter-jwt.js` - Adopter JWT verification, find-or-create (ADR-056 §2); `adopter-jwt-config.js` validates `jwtConfig` at load
|
|
371
343
|
|
|
372
344
|
**Repositories**:
|
|
373
345
|
- `user-repository-factory.js` - Creates database-specific repositories
|
|
@@ -530,6 +502,29 @@ The single source of the max receive count is
|
|
|
530
502
|
devtools integration builder uses it for the queue's `RedrivePolicy` and sets
|
|
531
503
|
it as `FRIGG_QUEUE_MAX_RECEIVE_COUNT` on the queue worker function.
|
|
532
504
|
|
|
505
|
+
### Management API v2 notes (`/management-api`)
|
|
506
|
+
|
|
507
|
+
- Handlers in `management-api/v2/handlers/` call use cases only; dependencies are wired in
|
|
508
|
+
`management-api/v2/dependencies.js`. Errors are `{ error: { code, message, details? } }`.
|
|
509
|
+
- `appDefinition.managementApi` is resolved by `resolveManagementApiConfig`, which `loadAppDefinition`
|
|
510
|
+
also calls so a bad value stops the app at load. Numeric proxy limits fall back to their defaults;
|
|
511
|
+
`proxy.scopes` (ADR-056 §3.5, default `['individual']`) never does: an unknown scope, `'global'`, a
|
|
512
|
+
non-list, an empty list or a repeated scope throws `ManagementApiConfigError`
|
|
513
|
+
(`MANAGEMENT_API_CONFIG_INVALID`).
|
|
514
|
+
- **Entity proxy** (`ExecuteEntityProxyRequest`): visibility in the active organization comes from
|
|
515
|
+
`GetEntityModuleForUser` (404), then `assertProxyable` reads the same `{ scope, access }` description
|
|
516
|
+
the v2 lists return: `grantsUse(access)` (`use` or `manage`, from `modules/scope-visibility.js`) or 403
|
|
517
|
+
`PROXY_NOT_ALLOWED` `reason: 'access'`, and the scope must be in `proxy.scopes` or 403 `reason: 'scope'`.
|
|
518
|
+
Do not add a second ownership rule for the proxy; change the description instead.
|
|
519
|
+
- **Not visible is 404, and so is missing.** Module repositories throw `entityNotFoundError(id)`
|
|
520
|
+
(`modules/repositories/entity-not-found.js`) for a missing row and use cases match it with
|
|
521
|
+
`isEntityNotFound`. A 500 for a missing id next to a 404 for another tenant's id tells a caller which
|
|
522
|
+
ids exist. A test double that resolves `findEntityById` to `null` hides this: no adapter does that.
|
|
523
|
+
- **Tenant isolation on real PostgreSQL**: `management-api/v2/tenant-isolation.postgres.test.js`, run with
|
|
524
|
+
`FRIGG_TEST_POSTGRES_URL=<a database it may wipe>`. It applies the migrations, truncates every table
|
|
525
|
+
before each test, and drives real adopter-JWT auth through the real router and Prisma repositories.
|
|
526
|
+
Without the variable it is skipped (and says so), not passed. See `docs/guides/entity-scopes.md`.
|
|
527
|
+
|
|
533
528
|
### 8. Error Handling (`/errors`)
|
|
534
529
|
|
|
535
530
|
**Purpose**: Standardized error types with proper HTTP semantics.
|
|
@@ -780,6 +775,9 @@ Use test doubles from `@friggframework/test` package for consistent mocking.
|
|
|
780
775
|
- `SECRET_ARN` - AWS Secrets Manager ARN for auto-injection
|
|
781
776
|
- `FRIGG_LOG_LEVEL` - Minimum log level (`TRACE` … `FATAL`, default `INFO`; `DEBUG` on local runs)
|
|
782
777
|
- `FRIGG_QUEUE_MAX_RECEIVE_COUNT` - Set by devtools on integration queue workers whose queue the stack owns; feeds `delivery.maxReceiveCount`
|
|
778
|
+
- `FRIGG_QUEUE_SIGNING_SECRET` - Required for HTTP-delivered queues (Netlify Background Functions). `NetlifyBackgroundProvider` signs each message (HMAC-SHA256 over `timestamp.rawBody`, `x-frigg-queue-signature` / `x-frigg-queue-timestamp`) and `verifyEvent()` refuses anything unsigned, tampered, signed with another secret or older than 5 minutes. Unset → `send()` throws and every event is refused (`queues/queue-message-auth.js`)
|
|
779
|
+
- `FRIGG_QUEUE_ALLOW_UNSIGNED` - `true` accepts unsigned queue messages when no secret is set, outside production only (`STAGE` dev/test/local, `NODE_ENV` development/test, or `NETLIFY_DEV`); ignored in production
|
|
780
|
+
- `QSTASH_CURRENT_SIGNING_KEY` / `QSTASH_NEXT_SIGNING_KEY` - Required with the QStash queue: `QStashQueueProvider.verifyEvent()` checks the `Upstash-Signature` JWT with them
|
|
783
781
|
|
|
784
782
|
## Version Information
|
|
785
783
|
|
package/README.md
CHANGED
|
@@ -305,6 +305,7 @@ Comprehensive user authentication and authorization system supporting both indiv
|
|
|
305
305
|
- **Password-based**: Traditional username/password authentication
|
|
306
306
|
- **Token-based**: Bearer token authentication with session management
|
|
307
307
|
- **App-based**: External app user ID authentication (passwordless)
|
|
308
|
+
- **Adopter token (JWT)**: A token issued by your own identity system, verified by Frigg and safe to send from a browser (see below)
|
|
308
309
|
|
|
309
310
|
**Usage:**
|
|
310
311
|
```javascript
|
|
@@ -352,6 +353,110 @@ console.log('Individual user:', user.getIndividualUser());
|
|
|
352
353
|
console.log('Organization user:', user.getOrganizationUser());
|
|
353
354
|
```
|
|
354
355
|
|
|
356
|
+
#### Adopter token (JWT) authentication
|
|
357
|
+
|
|
358
|
+
For a multi-tenant product (ADR-056 §2), let the browser send the token your identity system
|
|
359
|
+
already issues. Frigg verifies the signature and claims, then finds or creates the individual
|
|
360
|
+
user and the organization user the token names. Unlike `sharedSecret` (`x-frigg-*` headers), a
|
|
361
|
+
token cannot name a user other than the one it was issued for, so it is safe in a browser.
|
|
362
|
+
|
|
363
|
+
```javascript
|
|
364
|
+
// backend/index.js
|
|
365
|
+
const appDefinition = {
|
|
366
|
+
user: {
|
|
367
|
+
primary: 'organization',
|
|
368
|
+
individualUserRequired: true,
|
|
369
|
+
organizationUserRequired: true,
|
|
370
|
+
authModes: { adopterJwt: true, friggToken: false, sharedSecret: false },
|
|
371
|
+
jwtConfig: {
|
|
372
|
+
jwksUri: 'https://login.example.com/.well-known/jwks.json',
|
|
373
|
+
issuer: 'https://login.example.com/',
|
|
374
|
+
audience: 'my-frigg-api', // required with jwksUri
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
The client sends `Authorization: Bearer <token>`. `x-frigg-appuserid` / `x-frigg-apporgid`, if
|
|
381
|
+
also sent, must match the token or the request is refused with 403. That match rule lives in
|
|
382
|
+
`user/use-cases/authenticate-user.js` (`validateUserMatch`), not in the token verifier. A bearer
|
|
383
|
+
value that is not a three-part JWT is not offered to the verifier at all; it falls through to
|
|
384
|
+
`friggToken` (or is a 401 when that mode is off).
|
|
385
|
+
|
|
386
|
+
| `jwtConfig` field | Default | Meaning |
|
|
387
|
+
|---|---|---|
|
|
388
|
+
| `jwksUri` | | Remote JWKS (rotating keys, one per verifier, cached by jose). Must be `https`; `http` only for `localhost`, `127.0.0.0/8` or `::1`. Set exactly one of this and `secret`. |
|
|
389
|
+
| `secret` | | Shared HMAC secret. Read it from an environment variable. |
|
|
390
|
+
| `issuer` | not checked | Required `iss`. With `jwksUri`, a startup warning is logged when it is unset. |
|
|
391
|
+
| `audience` | not checked with `secret`; **required with `jwksUri`** | Accepted `aud` (a string or a list). |
|
|
392
|
+
| `algorithms` | `['ES256','RS256']` with `jwksUri`, `['HS256']` with `secret` | Allow-list. HS* is only accepted with `secret`, never with `jwksUri`. The older single `algorithm` is a deprecated alias; setting both is an error. |
|
|
393
|
+
| `clockToleranceSec` | `30` | Skew allowed on `exp` and `nbf`. |
|
|
394
|
+
| `userIdClaim` | `sub` | App user id (required in the token). |
|
|
395
|
+
| `orgIdClaim` | `org_id` | App organization id (optional). |
|
|
396
|
+
| `roleClaim` | `org_role` | Exposed as `user.getOrgRole()` for the request, and stored as the `orgRole` of a person's membership the first time they join that organization (`MEMBER` when the token names none). For permissions the token's role wins; a token with none uses the role stored on the person's membership of the active organization, and is a member when there is none: it cannot create or change organization-level entities or integrations. `user.orgAdminRoles` (default `['ADMIN']`) names the roles that can. Frigg bearer tokens are not gated unless `user.orgAdminGateBearerTokens: true`. |
|
|
397
|
+
|
|
398
|
+
Rules worth knowing:
|
|
399
|
+
|
|
400
|
+
- Tokens must carry `exp`. `alg: none` and any algorithm outside the allow-list are refused.
|
|
401
|
+
- Startup fails (`loadAppDefinition` throws) when `adopterJwt` is on and the config is unusable:
|
|
402
|
+
neither or both of `jwksUri` and `secret`, a `jwksUri` that is not https (or loopback http),
|
|
403
|
+
no `audience` with `jwksUri`, or an algorithm list that does not fit the key type.
|
|
404
|
+
- Failures are `401` with a stable `data.reason`: `token_expired`, `token_not_yet_valid`,
|
|
405
|
+
`token_invalid_signature`, `token_wrong_issuer`, `token_wrong_audience`,
|
|
406
|
+
`token_missing_user_claim`, `token_malformed`. The message never contains the token or its claims.
|
|
407
|
+
Any other verification failure, such as an unreachable or unresolvable JWKS, is `503`
|
|
408
|
+
`jwks_unavailable`, not a 401, so clients do not drop good sessions.
|
|
409
|
+
- **The token's organization is the request's active organization**, and a person can belong to
|
|
410
|
+
several (ADR-056 §3.6). The first time a person presents a token for an organization they are
|
|
411
|
+
not a member of, Frigg adds an `OrganizationMembership` and the request runs as that
|
|
412
|
+
organization (`user.getActiveOrganizationId()`; `user.getId()` under `primary: 'organization'`).
|
|
413
|
+
Nothing is moved: `IndividualUser.organizationId` is a legacy pointer, set only on a person's
|
|
414
|
+
first link and read as an implicit membership. The same applies to `x-frigg-apporgid` with
|
|
415
|
+
`sharedSecret`. With `user.strictMembership: true` those headers cannot add anyone to an organization
|
|
416
|
+
that already existed (400 for a non-member, a brand-new `appUserId` included); only an organization
|
|
417
|
+
the request itself creates is allowed. `strictUserValidation` is a deprecated alias with that same
|
|
418
|
+
meaning. See
|
|
419
|
+
[Organization memberships](../../docs/guides/organization-memberships.md). A token naming a second
|
|
420
|
+
organization is no longer a `403` (`multi_org_unsupported` is gone).
|
|
421
|
+
- Two first logins for the same new person or organization, even in different organizations, both
|
|
422
|
+
succeed: a create that loses to a unique constraint finds the winner's row.
|
|
423
|
+
|
|
424
|
+
#### Entity scopes and ownership (ADR-056 §3)
|
|
425
|
+
|
|
426
|
+
Who owns an entity (a connected account) or an integration follows its **scope**, not `user.primary`:
|
|
427
|
+
|
|
428
|
+
| Scope | Owned by | Created with |
|
|
429
|
+
|---|---|---|
|
|
430
|
+
| `individual` | the person (`individualUser`) | the active organization (`organizationId`), or none under `individualAcross: 'all'` |
|
|
431
|
+
| `organization` | the active organization's user | that organization |
|
|
432
|
+
| `global` | nobody (the app owner) | none; refused by the tenant API (admin routes only) |
|
|
433
|
+
|
|
434
|
+
The scope of a new connection is the first of: the request (`scope` on `POST /api/v2/authorize`, only from
|
|
435
|
+
the deciding level's `allow` list), the integration binding (`Definition.modules[key].scope` or
|
|
436
|
+
`Definition.entities[key].scope`), `Definition.ownership.scope`, the entity type
|
|
437
|
+
(`appDefinition.entities[name].scope`), `appDefinition.ownership.scope`, then `organization` for an
|
|
438
|
+
organization-primary app and `individual` otherwise. A setting is `'organization'` or
|
|
439
|
+
`{ default: 'individual', allow: ['individual', 'organization'] }`.
|
|
440
|
+
|
|
441
|
+
```javascript
|
|
442
|
+
const appDefinition = {
|
|
443
|
+
user: { primary: 'individual', authModes: { adopterJwt: true } },
|
|
444
|
+
ownership: { scope: 'individual', individualAcross: 'organization' }, // the defaults
|
|
445
|
+
entities: {
|
|
446
|
+
google: { definition: google, scope: 'individual' },
|
|
447
|
+
xero: { definition: xero, scope: { default: 'organization', allow: ['organization', 'individual'] } },
|
|
448
|
+
},
|
|
449
|
+
integrations: [Sync], // Sync.Definition.ownership = { owner: 'derive' }
|
|
450
|
+
};
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
`GET /api/v2/entities` and `/integrations` return, for the active organization, its own
|
|
454
|
+
rows and the caller's, each with `scope`, `owner`, `organizationId` and `access`, and take `?scope=` and
|
|
455
|
+
`?owner=me|org`. An individual entity is only visible and usable in the organization it was connected in
|
|
456
|
+
(or in all of them, with `individualAcross: 'all'`). Rows written before this existed keep working: see
|
|
457
|
+
[Entity scopes](../../docs/guides/entity-scopes.md), which also covers the schema (`organizationId` on
|
|
458
|
+
`Entity` and `Integration`, no `scope` column) and what is deferred.
|
|
459
|
+
|
|
355
460
|
### 8. Lambda Utilities (`/lambda`)
|
|
356
461
|
|
|
357
462
|
AWS Lambda-specific utilities and helpers.
|
|
@@ -18,7 +18,7 @@ const {
|
|
|
18
18
|
ListIntegrationsByEntityExternalIdUseCase,
|
|
19
19
|
} = require('../../integrations/use-cases/list-integrations-by-entity-external-id');
|
|
20
20
|
const {
|
|
21
|
-
|
|
21
|
+
GetIntegrationsByUserId,
|
|
22
22
|
} = require('../../integrations/use-cases/get-integrations-for-user');
|
|
23
23
|
const {
|
|
24
24
|
CreateIntegration,
|
|
@@ -30,14 +30,9 @@ const {
|
|
|
30
30
|
PatchIntegrationConfig,
|
|
31
31
|
} = require('../../integrations/use-cases/patch-integration-config');
|
|
32
32
|
const {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
RemoveEntitiesFromIntegration,
|
|
37
|
-
} = require('../../integrations/use-cases/remove-entities-from-integration');
|
|
38
|
-
const {
|
|
39
|
-
getModulesDefinitionFromIntegrationClasses,
|
|
40
|
-
} = require('../../integrations/utils/map-integration-dto');
|
|
33
|
+
getEntityTypeRegistry,
|
|
34
|
+
getEntityTypeDefinitions,
|
|
35
|
+
} = require('../../modules/entity-type-registry');
|
|
41
36
|
|
|
42
37
|
const ERROR_CODE_MAP = {
|
|
43
38
|
ENTITY_NOT_FOUND: 401,
|
|
@@ -46,9 +41,6 @@ const ERROR_CODE_MAP = {
|
|
|
46
41
|
EXTERNAL_ID_REQUIRED: 400,
|
|
47
42
|
TYPE_REQUIRED: 400,
|
|
48
43
|
INTEGRATION_RECORD_NOT_FOUND: 404,
|
|
49
|
-
INVALID_INTEGRATION_DATA: 400,
|
|
50
|
-
ENTITIES_NOT_FOUND: 404,
|
|
51
|
-
ENTITY_USER_MISMATCH: 403,
|
|
52
44
|
};
|
|
53
45
|
|
|
54
46
|
function mapErrorToResponse(error) {
|
|
@@ -60,7 +52,16 @@ function mapErrorToResponse(error) {
|
|
|
60
52
|
};
|
|
61
53
|
}
|
|
62
54
|
|
|
63
|
-
|
|
55
|
+
/**
|
|
56
|
+
* @param {Object} [params]
|
|
57
|
+
* @param {Function} [params.integrationClass]
|
|
58
|
+
* @param {Array<object>} [params.moduleDefinitions] - the app's offered module
|
|
59
|
+
* definitions (`loadAppDefinition().moduleDefinitions`). Integration code
|
|
60
|
+
* calls this from a constructor with no app definition in hand, so when it
|
|
61
|
+
* is omitted the commands hydrate with this integration's own modules only
|
|
62
|
+
* (a registry built from `{ integrations: [integrationClass] }`).
|
|
63
|
+
*/
|
|
64
|
+
function createIntegrationCommands({ integrationClass, moduleDefinitions } = {}) {
|
|
64
65
|
// Always use Frigg's default repositories and use cases
|
|
65
66
|
const integrationRepository = createIntegrationRepository();
|
|
66
67
|
|
|
@@ -117,13 +118,13 @@ function createIntegrationCommands({ integrationClass } = {}) {
|
|
|
117
118
|
|
|
118
119
|
const moduleRepository = createModuleRepository();
|
|
119
120
|
|
|
120
|
-
const moduleDefinitions = getModulesDefinitionFromIntegrationClasses([
|
|
121
|
-
integrationClass,
|
|
122
|
-
]);
|
|
123
|
-
|
|
124
121
|
const moduleFactory = new ModuleFactory({
|
|
125
122
|
moduleRepository,
|
|
126
|
-
moduleDefinitions
|
|
123
|
+
moduleDefinitions:
|
|
124
|
+
moduleDefinitions ??
|
|
125
|
+
getEntityTypeDefinitions(
|
|
126
|
+
getEntityTypeRegistry({ integrations: [integrationClass] })
|
|
127
|
+
),
|
|
127
128
|
});
|
|
128
129
|
|
|
129
130
|
const loadIntegrationContextUseCase = new LoadIntegrationContextUseCase({
|
|
@@ -151,7 +152,7 @@ function createIntegrationCommands({ integrationClass } = {}) {
|
|
|
151
152
|
moduleRepository,
|
|
152
153
|
});
|
|
153
154
|
|
|
154
|
-
const getIntegrationsForUserUseCase = new
|
|
155
|
+
const getIntegrationsForUserUseCase = new GetIntegrationsByUserId({
|
|
155
156
|
integrationRepository,
|
|
156
157
|
integrationClasses: [integrationClass],
|
|
157
158
|
moduleFactory,
|
|
@@ -172,16 +173,6 @@ function createIntegrationCommands({ integrationClass } = {}) {
|
|
|
172
173
|
integrationRepository,
|
|
173
174
|
});
|
|
174
175
|
|
|
175
|
-
const addEntitiesToIntegrationUseCase = new AddEntitiesToIntegration({
|
|
176
|
-
integrationRepository,
|
|
177
|
-
moduleRepository,
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
const removeEntitiesFromIntegrationUseCase =
|
|
181
|
-
new RemoveEntitiesFromIntegration({
|
|
182
|
-
integrationRepository,
|
|
183
|
-
});
|
|
184
|
-
|
|
185
176
|
return {
|
|
186
177
|
findIntegrationById,
|
|
187
178
|
listIntegrations,
|
|
@@ -326,45 +317,6 @@ function createIntegrationCommands({ integrationClass } = {}) {
|
|
|
326
317
|
}
|
|
327
318
|
},
|
|
328
319
|
|
|
329
|
-
/**
|
|
330
|
-
* Attach entities to an existing integration. Every entity must
|
|
331
|
-
* exist and belong to the integration's user; entities that are
|
|
332
|
-
* already attached are left as they are.
|
|
333
|
-
* @param {Object} params
|
|
334
|
-
* @param {string} params.integrationId - Integration ID
|
|
335
|
-
* @param {Array<string>} params.entityIds - Entity IDs to attach
|
|
336
|
-
* @returns {Promise<Object>} Updated integration
|
|
337
|
-
*/
|
|
338
|
-
async addEntitiesToIntegration({ integrationId, entityIds }) {
|
|
339
|
-
try {
|
|
340
|
-
return await addEntitiesToIntegrationUseCase.execute(
|
|
341
|
-
integrationId,
|
|
342
|
-
entityIds
|
|
343
|
-
);
|
|
344
|
-
} catch (error) {
|
|
345
|
-
return mapErrorToResponse(error);
|
|
346
|
-
}
|
|
347
|
-
},
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Detach entities from an integration. Entities that are not
|
|
351
|
-
* attached are ignored; detached entities are not deleted.
|
|
352
|
-
* @param {Object} params
|
|
353
|
-
* @param {string} params.integrationId - Integration ID
|
|
354
|
-
* @param {Array<string>} params.entityIds - Entity IDs to detach
|
|
355
|
-
* @returns {Promise<Object>} Updated integration
|
|
356
|
-
*/
|
|
357
|
-
async removeEntitiesFromIntegration({ integrationId, entityIds }) {
|
|
358
|
-
try {
|
|
359
|
-
return await removeEntitiesFromIntegrationUseCase.execute(
|
|
360
|
-
integrationId,
|
|
361
|
-
entityIds
|
|
362
|
-
);
|
|
363
|
-
} catch (error) {
|
|
364
|
-
return mapErrorToResponse(error);
|
|
365
|
-
}
|
|
366
|
-
},
|
|
367
|
-
|
|
368
320
|
/**
|
|
369
321
|
* Delete an integration by ID
|
|
370
322
|
* @param {string} integrationId - Integration ID to delete
|
|
@@ -121,17 +121,19 @@ function createSchedulerCommands({ integrationName, schedulerService }) {
|
|
|
121
121
|
throw error;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
// An adapter with its own notion of a queue target (Netlify:
|
|
125
|
+
// the worker function) resolves it; otherwise the target is
|
|
126
|
+
// an SQS queue and its URL is required.
|
|
127
|
+
const service = getSchedulerService();
|
|
128
|
+
const resolvesOwnTarget =
|
|
129
|
+
typeof service?.toQueueResourceId === 'function';
|
|
130
|
+
|
|
131
|
+
if (!queueUrl && !resolvesOwnTarget) {
|
|
125
132
|
const error = new Error('queueUrl is required');
|
|
126
133
|
error.code = 'INVALID_JOB_DATA';
|
|
127
134
|
throw error;
|
|
128
135
|
}
|
|
129
136
|
|
|
130
|
-
// Derive ARN from URL (business logic - transformation)
|
|
131
|
-
const queueArn = deriveArnFromQueueUrl(queueUrl);
|
|
132
|
-
|
|
133
|
-
// Get scheduler service (via DI or factory)
|
|
134
|
-
const service = getSchedulerService();
|
|
135
137
|
if (!service) {
|
|
136
138
|
console.warn(
|
|
137
139
|
`[${integrationName}] Scheduler not configured, skipping job schedule`
|
|
@@ -157,7 +159,9 @@ function createSchedulerCommands({ integrationName, schedulerService }) {
|
|
|
157
159
|
const result = await service.scheduleOneTime({
|
|
158
160
|
scheduleName: jobId,
|
|
159
161
|
scheduleAt: scheduledAt,
|
|
160
|
-
queueResourceId:
|
|
162
|
+
queueResourceId: resolvesOwnTarget
|
|
163
|
+
? service.toQueueResourceId(queueUrl)
|
|
164
|
+
: deriveArnFromQueueUrl(queueUrl),
|
|
161
165
|
payload: sqsPayload,
|
|
162
166
|
});
|
|
163
167
|
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
const {
|
|
2
2
|
createUserRepository,
|
|
3
3
|
} = require('../../user/repositories/user-repository-factory');
|
|
4
|
+
const {
|
|
5
|
+
createOrganizationMembershipRepository,
|
|
6
|
+
} = require('../../user/repositories/organization-membership-repository-factory');
|
|
7
|
+
const {
|
|
8
|
+
FindIndividualUsersByOrganization,
|
|
9
|
+
} = require('../../user/use-cases/find-individual-users-by-organization');
|
|
4
10
|
|
|
5
11
|
const ERROR_CODE_MAP = {
|
|
6
12
|
USER_NOT_FOUND: 404,
|
|
@@ -26,6 +32,12 @@ function mapErrorToResponse(error) {
|
|
|
26
32
|
*/
|
|
27
33
|
function createUserCommands() {
|
|
28
34
|
const userRepository = createUserRepository();
|
|
35
|
+
const findIndividualUsersByOrganization =
|
|
36
|
+
new FindIndividualUsersByOrganization({
|
|
37
|
+
userRepository,
|
|
38
|
+
organizationMembershipRepository:
|
|
39
|
+
createOrganizationMembershipRepository(),
|
|
40
|
+
});
|
|
29
41
|
|
|
30
42
|
return {
|
|
31
43
|
/**
|
|
@@ -170,7 +182,8 @@ function createUserCommands() {
|
|
|
170
182
|
},
|
|
171
183
|
|
|
172
184
|
/**
|
|
173
|
-
* Find all individual users
|
|
185
|
+
* Find all individual users who belong to an organization user: those with
|
|
186
|
+
* a membership and those the legacy organizationId pointer places there.
|
|
174
187
|
* @param {string} organizationUserId - Organization user ID to search for
|
|
175
188
|
* @returns {Promise<Object[]>} Array of individual user objects (empty if none)
|
|
176
189
|
*/
|
|
@@ -182,10 +195,9 @@ function createUserCommands() {
|
|
|
182
195
|
throw error;
|
|
183
196
|
}
|
|
184
197
|
|
|
185
|
-
const users =
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
);
|
|
198
|
+
const users = await findIndividualUsersByOrganization.execute(
|
|
199
|
+
organizationUserId
|
|
200
|
+
);
|
|
189
201
|
|
|
190
202
|
return (users || []).map((user) => ({
|
|
191
203
|
id: user._id?.toString() || user.id,
|
package/application/index.js
CHANGED
|
@@ -17,6 +17,7 @@ const { createUsageCommands } = require('./commands/usage-commands');
|
|
|
17
17
|
*
|
|
18
18
|
* @param {Object} params
|
|
19
19
|
* @param {Object} params.integrationClass - Integration class (required)
|
|
20
|
+
* @param {Array<object>} [params.moduleDefinitions] - the app's offered module definitions; defaults to the integration's own modules
|
|
20
21
|
* @returns {Object} Unified commands object with all CRUD operations
|
|
21
22
|
*
|
|
22
23
|
* @example
|
|
@@ -24,9 +25,12 @@ const { createUsageCommands } = require('./commands/usage-commands');
|
|
|
24
25
|
* const user = await commands.createUser({ username: 'user@example.com' });
|
|
25
26
|
* const credential = await commands.createCredential({ userId: user.id, ... });
|
|
26
27
|
*/
|
|
27
|
-
function createFriggCommands({ integrationClass }) {
|
|
28
|
+
function createFriggCommands({ integrationClass, moduleDefinitions }) {
|
|
28
29
|
// All commands use Frigg's default repositories and use cases
|
|
29
|
-
const integrationCommands = createIntegrationCommands({
|
|
30
|
+
const integrationCommands = createIntegrationCommands({
|
|
31
|
+
integrationClass,
|
|
32
|
+
moduleDefinitions,
|
|
33
|
+
});
|
|
30
34
|
|
|
31
35
|
const userCommands = createUserCommands();
|
|
32
36
|
|
package/core/Worker.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const { SQSClient, GetQueueUrlCommand, SendMessageCommand } = require('@aws-sdk/client-sqs');
|
|
2
1
|
const _ = require('lodash');
|
|
3
2
|
const { RequiredPropertyError } = require('../errors');
|
|
4
3
|
const { get } = require('../assertions');
|
|
@@ -6,18 +5,44 @@ const { readQueueDelivery } = require('../queues/queue-delivery');
|
|
|
6
5
|
const { runMessageScope } = require('./invocation-scope');
|
|
7
6
|
const { getLogger } = require('../logs');
|
|
8
7
|
|
|
9
|
-
const sqs = new SQSClient({ region: process.env.AWS_REGION });
|
|
10
8
|
const log = getLogger('frigg.worker');
|
|
11
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Worker - queue message producer/consumer base class.
|
|
12
|
+
*
|
|
13
|
+
* Subclass and override _run(). The queue transport is behind
|
|
14
|
+
* QueueClientInterface and can be injected (friggframework/frigg#545):
|
|
15
|
+
* `new MyWorker({ queueClient })`. Unlike #545 it is not required: without
|
|
16
|
+
* one the worker uses an SqsQueueClient configured exactly as before
|
|
17
|
+
* (AWS_REGION), created on first use rather than at module load, so
|
|
18
|
+
* requiring core constructs no AWS client and a Netlify/QStash worker that
|
|
19
|
+
* only calls _run() never touches SQS.
|
|
20
|
+
*/
|
|
12
21
|
class Worker {
|
|
22
|
+
/**
|
|
23
|
+
* @param {Object} [options]
|
|
24
|
+
* @param {import('../queues/queue-client-interface').QueueClientInterface} [options.queueClient]
|
|
25
|
+
*/
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
this._queueClient = options.queueClient || null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_getQueueClient() {
|
|
31
|
+
if (!this._queueClient) {
|
|
32
|
+
const { SqsQueueClient } = require('../queues/sqs-queue-client');
|
|
33
|
+
this._queueClient = new SqsQueueClient({
|
|
34
|
+
clientConfig: { region: process.env.AWS_REGION },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return this._queueClient;
|
|
38
|
+
}
|
|
39
|
+
|
|
13
40
|
async getQueueURL(params) {
|
|
14
41
|
// Passing params in because there will be multiple QueueNames
|
|
15
42
|
// let params = {
|
|
16
43
|
// QueueName: process.env.QueueName
|
|
17
44
|
// };
|
|
18
|
-
|
|
19
|
-
const data = await sqs.send(command);
|
|
20
|
-
return data.QueueUrl;
|
|
45
|
+
return this._getQueueClient().getQueueUrl(params);
|
|
21
46
|
}
|
|
22
47
|
|
|
23
48
|
async run(params, context = {}) {
|
|
@@ -91,8 +116,7 @@ class Worker {
|
|
|
91
116
|
}
|
|
92
117
|
|
|
93
118
|
async sendAsyncSQSMessage(params) {
|
|
94
|
-
const
|
|
95
|
-
const data = await sqs.send(command);
|
|
119
|
+
const data = await this._getQueueClient().sendMessage(params);
|
|
96
120
|
return data.MessageId;
|
|
97
121
|
}
|
|
98
122
|
|
package/core/create-handler.js
CHANGED
|
@@ -81,6 +81,15 @@ const createHandler = (optionByName = {}) => {
|
|
|
81
81
|
if (shouldUseDatabase) {
|
|
82
82
|
const { connectPrisma } = require('../database/prisma');
|
|
83
83
|
await connectPrisma();
|
|
84
|
+
|
|
85
|
+
// Extension bootstraps (e.g. db-credentials swapping a
|
|
86
|
+
// module's env for its DB loader) run once per
|
|
87
|
+
// process, after the connect and before any request
|
|
88
|
+
// builds a module. AWS and Netlify both reach here.
|
|
89
|
+
const {
|
|
90
|
+
initializeApp,
|
|
91
|
+
} = require('../extensions/initialize-app');
|
|
92
|
+
await initializeApp();
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
// Helps reuse the database connection. Lowers response times.
|
|
@@ -186,6 +186,40 @@ class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
|
|
|
186
186
|
return this._mapCredential(decryptedCredential);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
/**
|
|
190
|
+
* All credentials owned by a user, newest first.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} userId - User ID
|
|
193
|
+
* @returns {Promise<Array<Object>>} Credentials with string IDs
|
|
194
|
+
*/
|
|
195
|
+
async findCredentialsByUserId(userId) {
|
|
196
|
+
const userObjectId = toObjectId(userId);
|
|
197
|
+
if (!userObjectId) return [];
|
|
198
|
+
const docs = await findManyDrained(
|
|
199
|
+
this.prisma,
|
|
200
|
+
'Credential',
|
|
201
|
+
{ userId: userObjectId },
|
|
202
|
+
{ sort: { createdAt: -1 } }
|
|
203
|
+
);
|
|
204
|
+
const credentials = [];
|
|
205
|
+
for (const doc of docs) {
|
|
206
|
+
const decrypted = await this.encryptionService.decryptFields(
|
|
207
|
+
'Credential',
|
|
208
|
+
doc
|
|
209
|
+
);
|
|
210
|
+
credentials.push({
|
|
211
|
+
...this._mapCredential(decrypted),
|
|
212
|
+
id: fromObjectId(doc._id),
|
|
213
|
+
userId: fromObjectId(doc.userId) ?? doc.userId,
|
|
214
|
+
externalId: doc.externalId ?? null,
|
|
215
|
+
authIsValid: doc.authIsValid ?? null,
|
|
216
|
+
createdAt: doc.createdAt ?? null,
|
|
217
|
+
updatedAt: doc.updatedAt ?? null,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return credentials;
|
|
221
|
+
}
|
|
222
|
+
|
|
189
223
|
async updateCredential(credentialId, updates) {
|
|
190
224
|
const objectId = toObjectId(credentialId);
|
|
191
225
|
if (!objectId) return null;
|
|
@@ -80,6 +80,21 @@ class CredentialRepositoryInterface {
|
|
|
80
80
|
);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* All credentials owned by a user, newest first.
|
|
85
|
+
*
|
|
86
|
+
* @param {string|number} userId - User ID (string from application layer)
|
|
87
|
+
* @returns {Promise<Array<Object>>} Credentials with string IDs, `createdAt`
|
|
88
|
+
* and `updatedAt`, and the (decrypted) data fields spread in, as
|
|
89
|
+
* findCredentialById returns them. Empty when the user has none.
|
|
90
|
+
* @abstract
|
|
91
|
+
*/
|
|
92
|
+
async findCredentialsByUserId(userId) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
'Method findCredentialsByUserId must be implemented by subclass'
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
83
98
|
/**
|
|
84
99
|
* Update a credential by ID
|
|
85
100
|
*
|
|
@@ -188,6 +188,29 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
188
188
|
};
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
/**
|
|
192
|
+
* All credentials owned by a user, newest first.
|
|
193
|
+
*
|
|
194
|
+
* @param {string} userId - User ID
|
|
195
|
+
* @returns {Promise<Array<Object>>} Credentials with string IDs
|
|
196
|
+
*/
|
|
197
|
+
async findCredentialsByUserId(userId) {
|
|
198
|
+
if (!userId) return [];
|
|
199
|
+
const credentials = await this.prisma.credential.findMany({
|
|
200
|
+
where: { userId: String(userId) },
|
|
201
|
+
orderBy: { createdAt: 'desc' },
|
|
202
|
+
});
|
|
203
|
+
return credentials.map((credential) => ({
|
|
204
|
+
...(credential.data || {}),
|
|
205
|
+
id: credential.id,
|
|
206
|
+
userId: credential.userId,
|
|
207
|
+
externalId: credential.externalId,
|
|
208
|
+
authIsValid: credential.authIsValid,
|
|
209
|
+
createdAt: credential.createdAt,
|
|
210
|
+
updatedAt: credential.updatedAt,
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
|
|
191
214
|
/**
|
|
192
215
|
* Update a credential by ID
|
|
193
216
|
* Replaces: Credential.findByIdAndUpdate(credentialId, { $set: updates })
|