@owlmeans/server-auth-identity 0.1.18-rc.46 → 0.1.18-rc.48

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 (67) hide show
  1. package/README.md +24 -23
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/server-auth-identity/SKILL.md +28 -27
  4. package/build/groups/types.d.ts +23 -0
  5. package/build/groups/types.d.ts.map +1 -0
  6. package/build/groups/types.js +2 -0
  7. package/build/groups/types.js.map +1 -0
  8. package/build/groups.d.ts +7 -15
  9. package/build/groups.d.ts.map +1 -1
  10. package/build/groups.js +41 -44
  11. package/build/groups.js.map +1 -1
  12. package/build/iam/types.d.ts +22 -0
  13. package/build/iam/types.d.ts.map +1 -0
  14. package/build/iam/types.js +2 -0
  15. package/build/iam/types.js.map +1 -0
  16. package/build/identity/types.d.ts +35 -0
  17. package/build/identity/types.d.ts.map +1 -0
  18. package/build/identity/types.js +2 -0
  19. package/build/identity/types.js.map +1 -0
  20. package/build/identity.d.ts +9 -40
  21. package/build/identity.d.ts.map +1 -1
  22. package/build/identity.js +156 -185
  23. package/build/identity.js.map +1 -1
  24. package/build/index.d.ts +5 -1
  25. package/build/index.d.ts.map +1 -1
  26. package/build/index.js +1 -1
  27. package/build/index.js.map +1 -1
  28. package/build/keys/types.d.ts +22 -0
  29. package/build/keys/types.d.ts.map +1 -0
  30. package/build/keys/types.js +2 -0
  31. package/build/keys/types.js.map +1 -0
  32. package/build/keys.d.ts +4 -0
  33. package/build/keys.d.ts.map +1 -0
  34. package/build/keys.js +16 -0
  35. package/build/keys.js.map +1 -0
  36. package/build/resolver.js +7 -7
  37. package/build/resolver.js.map +1 -1
  38. package/build/service.d.ts.map +1 -1
  39. package/build/service.js +10 -9
  40. package/build/service.js.map +1 -1
  41. package/build/utils/native/types.d.ts +17 -0
  42. package/build/utils/native/types.d.ts.map +1 -0
  43. package/build/utils/native/types.js +2 -0
  44. package/build/utils/native/types.js.map +1 -0
  45. package/build/utils/native.d.ts +4 -0
  46. package/build/utils/native.d.ts.map +1 -0
  47. package/build/utils/native.js +8 -0
  48. package/build/utils/native.js.map +1 -0
  49. package/package.json +10 -10
  50. package/src/groups/types.ts +23 -0
  51. package/src/groups.ts +55 -49
  52. package/src/identity/types.ts +38 -0
  53. package/src/identity.ts +162 -192
  54. package/src/index.ts +5 -1
  55. package/src/keys/types.ts +22 -0
  56. package/src/keys.ts +24 -0
  57. package/src/resolver.ts +7 -7
  58. package/src/service.ts +10 -9
  59. package/src/utils/native/types.ts +17 -0
  60. package/src/utils/native.ts +17 -0
  61. package/tests/context.ts +10 -10
  62. package/tests/events.spec.ts +5 -4
  63. package/tests/identity.spec.ts +27 -26
  64. package/tests/linking.spec.ts +11 -10
  65. package/tests/resolver.spec.ts +18 -18
  66. package/src/native.ts +0 -18
  67. /package/src/{iam.ts → iam/types.ts} +0 -0
package/README.md CHANGED
@@ -10,7 +10,7 @@ whose grants decide access (use `@owlmeans/server-oidc-rp`'s guard and gate, or
10
10
  ## Installation
11
11
 
12
12
  ```bash
13
- bun add @owlmeans/server-auth-identity@^0.1.18-rc.46
13
+ bun add @owlmeans/server-auth-identity@^0.1.18-rc.48
14
14
  ```
15
15
 
16
16
  ## Concepts
@@ -24,12 +24,12 @@ bun add @owlmeans/server-auth-identity@^0.1.18-rc.46
24
24
  - **Credentials (`IdentityCredentials`)** — one row per sign-in method of an account (`accountId`),
25
25
  keyed by `"{type}:{service}:{providerSub}"`.
26
26
  - **Profile row (`IdentityProfile`)** — one person in one organization for one app (`service`):
27
- `profileId` (computed by `profileIdOf(service, accountId)`), `owner`, `role`, `scopes`,
27
+ `profileId` (computed by `identityKeyHelper.profileIdOf(service, accountId)`), `owner`, `role`, `scopes`,
28
28
  `permissions`, `groups`. The row in the account's own organization is the app's PRIMARY row.
29
29
  - **Groups (`OrgGroup`)** — an app's groups, kept in the organization record, unique per
30
30
  (`service`, `key`).
31
- - **Primitives** — `ensureAccount` (find-or-register an account by method, then address) and
32
- `ensureProfile` (find-or-create a row, primary row first).
31
+ - **Primitives** — `identityOf(ctx).ensureAccount` (find-or-register an account by method, then
32
+ address) and `identityOf(ctx).ensureProfile` (find-or-create a row, primary row first).
33
33
  - **Linking service** — `IdentityLinkingService` signs a provider login into the deployment's own
34
34
  app and returns an `AuthPayload`.
35
35
  - **Entity resolver** — the `EntityResolverService` registered under `ENTITY_RESOLVER`. Registering
@@ -79,7 +79,7 @@ context.registerService(makeMyAppGate())
79
79
  import { createLazyService } from '@owlmeans/context'
80
80
  import type { GateService } from '@owlmeans/entrypoint'
81
81
  import { AuthForbidden } from '@owlmeans/auth'
82
- import { entityKeyOf } from '@owlmeans/auth-common'
82
+ import { makeEntityScope } from '@owlmeans/auth-common'
83
83
  import { AUTH_IDENTITY_PROFILE } from '@owlmeans/server-auth-identity'
84
84
  import type { IdentityProfileResource } from '@owlmeans/server-auth-identity'
85
85
 
@@ -89,7 +89,7 @@ export const makeMyAppGate = (alias: string = MY_APP_GATE): GateService => {
89
89
  await service.ready()
90
90
  const ctx = service.assertCtx<Config, Context>()
91
91
 
92
- const entityId = entityKeyOf(req)
92
+ const entityId = makeEntityScope(req).entityKeyOf()
93
93
  if (req.auth == null || entityId == null) {
94
94
  throw new AuthForbidden('auth')
95
95
  }
@@ -120,7 +120,7 @@ A protocol uses it with `{ guards: DEFAULT_GUARD, gate: { alias: MY_APP_GATE, pa
120
120
 
121
121
  ```ts
122
122
  import { handlers } from '@owlmeans/server-app'
123
- import { requireEntityKey } from '@owlmeans/auth-common'
123
+ import { makeEntityScope } from '@owlmeans/auth-common'
124
124
  import { AUTH_IDENTITY_PROFILE } from '@owlmeans/server-auth-identity'
125
125
  import type { IdentityProfileResource } from '@owlmeans/server-auth-identity'
126
126
 
@@ -128,7 +128,7 @@ const api = handlers<Context>()
128
128
 
129
129
  export const members = api.request(memberProtocols.list, async (request, context) => {
130
130
  const { items } = await context.resource<IdentityProfileResource>(AUTH_IDENTITY_PROFILE)
131
- .list({ entityId: requireEntityKey(request), service: MY_APP }, { size: 0, sort: [{ field: 'createdAt', order: 'desc' }] })
131
+ .list({ entityId: makeEntityScope(request).requireEntityKey(), service: MY_APP }, { size: 0, sort: [{ field: 'createdAt', order: 'desc' }] })
132
132
 
133
133
  return items.map(({ profileId, role, name }) => ({ profileId, role, name }))
134
134
  })
@@ -160,10 +160,11 @@ payload ??= await linking.linkProfile(details, { username: 'person@example.org'
160
160
  ### Rows for a hosted app
161
161
 
162
162
  ```ts
163
- import { ensureAccount, ensureProfile } from '@owlmeans/server-auth-identity'
163
+ import { identityOf } from '@owlmeans/server-auth-identity'
164
164
 
165
- const { account } = await ensureAccount(context, { email }, details) // writes no row
166
- const row = await ensureProfile(context, { account, service: clientId, entityId: account.entityId, owner: true })
165
+ const identity = identityOf(context)
166
+ const { account } = await identity.ensureAccount({ email }, details) // writes no row
167
+ const row = await identity.ensureProfile({ account, service: clientId, entityId: account.entityId, owner: true })
167
168
  // a hosted app's rows carry `scopes: []`; a row in another organization brings the primary row with it
168
169
  ```
169
170
 
@@ -189,12 +190,12 @@ and never fails the sign-in, so anything a listener provisions needs its own bac
189
190
  ### Rename an organization and mint a durable name
190
191
 
191
192
  ```ts
192
- import { ENTITY_RESOLVER, requireEntityKey } from '@owlmeans/auth-common'
193
+ import { ENTITY_RESOLVER, makeEntityScope } from '@owlmeans/auth-common'
193
194
  import type { EntityResolverService } from '@owlmeans/auth-common'
194
195
 
195
196
  export const rename = api.body(organizationProtocols.rename, async ({ slug }, context, request) => {
196
197
  const resolver = context.service<EntityResolverService>(ENTITY_RESOLVER)
197
- const entity = await resolver.rename(requireEntityKey(request), slug) // old slug moves to formerSlugs
198
+ const entity = await resolver.rename(makeEntityScope(request).requireEntityKey(), slug) // old slug moves to formerSlugs
198
199
 
199
200
  return { slug: entity.slug }
200
201
  })
@@ -210,11 +211,11 @@ const namespace = await resolver.mintName(entityId, 'namespace', entity => `my-a
210
211
  | Symbol | Kind | Purpose |
211
212
  |---|---|---|
212
213
  | `appendAuthIdentityResources(context, dbAlias?, { service? })` | function | Register the four resources, the linking service for app `service`, the entity resolver and (unless one is registered) the identity-events service |
213
- | `profileIdOf(service, accountId)` | function | The computed profile id of an (account, app) |
214
- | `ensureAccount(ctx, { email, name? }, details?)` | function | The person's account — by method, then address, else registered with a personal organization |
215
- | `ensureProfile(ctx, { account, service, entityId, … })` | function | Find-or-create a row; the primary row first |
216
- | `credentialKeyOf(details)`, `credentialOf(ctx, details)`, `normalizeEmail(email)` | function | A method's credential key and stored row; the stored address form |
217
- | `listOrgGroups`, `putOrgGroup`, `removeOrgGroup` | function | An organization's groups of one app, by guarded field-level writes |
214
+ | `identityKeyHelper.profileIdOf(service, accountId)` | method | The computed profile id of an (account, app) |
215
+ | `identityOf(ctx).ensureAccount({ email, name? }, details?)` | method | The person's account — by method, then address, else registered with a personal organization |
216
+ | `identityOf(ctx).ensureProfile({ account, service, entityId, … })` | method | Find-or-create a row; the primary row first |
217
+ | `identityKeyHelper.credentialKeyOf(details)`, `identityOf(ctx).credentialOf(details)`, `identityKeyHelper.normalizeEmail(email)` | method | A method's credential key and stored row; the stored address form |
218
+ | `orgGroupsOf(ctx).listOrgGroups`, `.putOrgGroup`, `.removeOrgGroup` | method | An organization's groups of one app, by guarded field-level writes |
218
219
  | `makeOrgEntityResource(dbAlias?)` | function | Mongo resource for `OrgEntity` |
219
220
  | `makeIdentityAccountResource(dbAlias?)` | function | Mongo resource for `IdentityAccount` |
220
221
  | `makeIdentityProfileResource(dbAlias?)` | function | Mongo resource for `IdentityProfile` |
@@ -263,7 +264,7 @@ const namespace = await resolver.mintName(entityId, 'namespace', entity => `my-a
263
264
  - Account `email` — trimmed, lower-case, unique.
264
265
  - Account / profile `entityId`, profile `userId` and `home`, credentials `accountId` — record ids,
265
266
  declared ObjectId references; a first registration creates the organization first.
266
- - Profile `profileId` — `profileIdOf(service, accountId)`: `"{service}:"` + 22 Base58 characters of
267
+ - Profile `profileId` — `identityKeyHelper.profileIdOf(service, accountId)`: `"{service}:"` + 22 Base58 characters of
267
268
  `sha256("{accountId}:{service}")`. Computed, the same on every row of one (account, app).
268
269
  - Credentials `userId` — `"{type}:{service}:{providerSub}"`; unrelated to the profile's `userId`.
269
270
  - Credentials `credential` — `"service:{type}:{service}"`.
@@ -272,8 +273,8 @@ const namespace = await resolver.mintName(entityId, 'namespace', entity => `my-a
272
273
 
273
274
  - `Resource.take()` deletes the record it returns. Use `load(where)` or `list(where)` in gates and
274
275
  handlers, never `take()`.
275
- - Query profiles and organization records by `entityId` from `requireEntityKey(request)` /
276
- `entityKeyOf(request)`, never by the slug.
276
+ - Query profiles and organization records by `entityId` from
277
+ `makeEntityScope(request).requireEntityKey()` / `.entityKeyOf()`, never by the slug.
277
278
  - Without the resolver `request.entity` stays undefined and consumers fall back to the slug — call
278
279
  `appendAuthIdentityResources` (or register the resolver) in every service that serves organization
279
280
  data.
@@ -293,7 +294,7 @@ const namespace = await resolver.mintName(entityId, 'namespace', entity => `my-a
293
294
  ## Related packages
294
295
 
295
296
  - [`@owlmeans/server-auth`](../server-auth) — bearer verification; canonicalizes the slug through the resolver
296
- - [`@owlmeans/auth-common`](../auth-common) — `EntityResolverService`, `ENTITY_RESOLVER`, `requireEntityKey`, `attachEntity`
297
+ - [`@owlmeans/auth-common`](../auth-common) — `EntityResolverService`, `ENTITY_RESOLVER`, `makeEntityScope` (`requireEntityKey`, `attachEntity`)
297
298
  - [`@owlmeans/auth`](../auth) — `AuthPayload`, `AuthRole`, `Profile`, errors
298
299
  - [`@owlmeans/server-oidc-rp`](../server-oidc-rp) — calls the linking service during an OAuth callback
299
300
  - [`@owlmeans/server-auth-otp`](../server-auth-otp) — email OTP plugin resolving users through the linking service
@@ -308,7 +309,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
308
309
  your project's skill store (`.agents/skills/`):
309
310
 
310
311
  ```sh
311
- npx @owlmeans/agent-skills@^0.1.18-rc.48
312
+ npx @owlmeans/agent-skills@^0.1.18-rc.51
312
313
  ```
313
314
 
314
315
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/server-auth-identity",
4
- "version": "0.1.18-rc.46",
5
- "generatedAt": "2026-10-05T08:32:19.089Z",
4
+ "version": "0.1.18-rc.48",
5
+ "generatedAt": "2026-10-06T11:45:57.652Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: server-auth-identity
3
- description: How to use @owlmeans/server-auth-identity — the Mongo-backed identity store shared by a deployment's own sign-in and the apps it hosts. One account per e-mail (every sign-in method a credential on it), a personal organization per account, profile rows per (account, app, organization) with a computed profileId, organization groups, the IdentityLinkingService for the deployment's own app, the ensureAccount / ensureProfile primitives, the EntityResolverService with field-level rename/mintName, and the identity-events seam (entity created, profile created). Auto-invoked when importing appendAuthIdentityResources, an identity resource alias, IdentityLinkingService, ensureAccount/ensureProfile/profileIdOf, the org group helpers, the entity resolver or identityEvents.
3
+ description: How to use @owlmeans/server-auth-identity — the Mongo-backed identity store shared by a deployment's own sign-in and the apps it hosts. One account per e-mail (every sign-in method a credential on it), a personal organization per account, profile rows per (account, app, organization) with a computed profileId, organization groups, the IdentityLinkingService for the deployment's own app, the identityOf(ctx) ensureAccount / ensureProfile primitives, the EntityResolverService with field-level rename/mintName, and the identity-events seam (entity created, profile created). Auto-invoked when importing appendAuthIdentityResources, an identity resource alias, IdentityLinkingService, identityOf/identityKeyHelper, the org group helpers (orgGroupsOf), the entity resolver or identityEvents.
4
4
  user-invocable: false
5
5
  ---
6
6
  <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/server-auth-identity
9
9
 
10
10
  **Layer:** Server
11
- **Install:** `"@owlmeans/server-auth-identity": "^0.1.18-rc.46"` in `dependencies`
11
+ **Install:** `"@owlmeans/server-auth-identity": "^0.1.18-rc.48"` in `dependencies`
12
12
 
13
13
  The identity store a deployment owns when it does not delegate identity to an external IAM. It
14
14
  answers *who is this person* (account + credentials), *what are they in this app and organization*
@@ -21,11 +21,11 @@ serves the deployment's own app and every app it hosts (a platform's generated t
21
21
  | Export | Description |
22
22
  |--------|-------------|
23
23
  | `appendAuthIdentityResources(context, dbAlias?, { service? })` | Register the four resources, the linking service for app `service` (default `DEFAULT_APP_SERVICE`), the entity resolver and — unless one is already registered — the identity-events service |
24
- | `profileIdOf(service, accountId)` | The computed profile id of an (account, app) — `"{service}:{22 Base58 chars of sha256}"` |
25
- | `ensureAccount(ctx, { email, name? }, details?)` | The person's account: by the method's credential, then by e-mail, else registered with a personal organization; attaches `details` as a credential |
26
- | `ensureProfile(ctx, { account, service, entityId, owner?, role?, scopes?, permissions?, groups?, managed? })` | Find-or-create the row of (account, app, organization); ensures the primary row first |
27
- | `credentialKeyOf(details)`, `credentialOf(ctx, details)`, `normalizeEmail(email)` | A method's unique credential key; its stored row and account; the stored address form |
28
- | `listOrgGroups(ctx, entityId, service)`, `putOrgGroup(ctx, entityId, group)`, `removeOrgGroup(ctx, entityId, service, key)` | An organization's groups of one app — guarded field-level writes, unique per (`service`, `key`) |
24
+ | `identityKeyHelper.profileIdOf(service, accountId)` | The computed profile id of an (account, app) — `"{service}:{22 Base58 chars of sha256}"` |
25
+ | `identityOf(ctx).ensureAccount({ email, name? }, details?)` | The person's account: by the method's credential, then by e-mail, else registered with a personal organization; attaches `details` as a credential |
26
+ | `identityOf(ctx).ensureProfile({ account, service, entityId, owner?, role?, scopes?, permissions?, groups?, managed? })` | Find-or-create the row of (account, app, organization); ensures the primary row first |
27
+ | `identityKeyHelper.credentialKeyOf(details)`, `identityOf(ctx).credentialOf(details)`, `identityKeyHelper.normalizeEmail(email)` | A method's unique credential key; its stored row and account; the stored address form |
28
+ | `orgGroupsOf(ctx)` — `.listOrgGroups(entityId, service)`, `.putOrgGroup(entityId, group)`, `.removeOrgGroup(entityId, service, key)` | An organization's groups of one app — guarded field-level writes, unique per (`service`, `key`) |
29
29
  | `makeOrgEntityResource`, `makeIdentityAccountResource`, `makeIdentityProfileResource`, `makeIdentityCredentialsResource` | Mongo resource makers (`dbAlias?`) |
30
30
  | `makeIdentityLinkingService({ service? })` | The `IdentityLinkingService` for one app |
31
31
  | `makeEntityResolverService(alias?)` | The `EntityResolverService`, cached 30 s per resolved value (slim references only) |
@@ -49,7 +49,7 @@ serves the deployment's own app and every app it hosts (a platform's generated t
49
49
  | `OrgGroup` | `service`, `key`, `title?`, `managed?`, `permissions`, `bundles?` (`{ filter?, permissions? }`) | Members are the rows naming `key` in `groups`; one app never writes another app's group |
50
50
 
51
51
  **The primary row** of an (account, app) is its row in the account's main organization. It exists
52
- whenever any row of that pair does — `ensureProfile` creates it FIRST, as owner of the personal
52
+ whenever any row of that pair does — `identityOf(ctx).ensureProfile` creates it FIRST, as owner of the personal
53
53
  organization — because it is what says whether the person may use the app at all.
54
54
 
55
55
  **`profileId` is computed, never minted.** Two first sign-ins racing write the same key and the
@@ -77,7 +77,7 @@ Registering the resolver is the signal that the deployment HAS organizations: wi
77
77
  resource's native `collection.updateOne`), never the whole document** — a replace from a stale read
78
78
  erases the groups or names another writer added since. The resolver's `rename` is guarded on the slug
79
79
  it read (`formerSlugs` moves only with it), `mintName` on the name still being unset (first minter
80
- wins, both answer what was stored), `putOrgGroup` replaces the matching element (`groups.$`) or
80
+ wins, both answer what was stored), `orgGroupsOf(ctx).putOrgGroup` replaces the matching element (`groups.$`) or
81
81
  pushes where none matches (`$not: { $elemMatch }`) and retries if a concurrent put won.
82
82
 
83
83
  ## Usage
@@ -107,8 +107,8 @@ payload ??= await linking.linkProfile(details, { username: verifiedEmail })
107
107
 
108
108
  - `getLinkedProfile` — credential → account → the own app's row in the main organization. A method
109
109
  that is linked but has no row of THIS app answers `null`, and the caller's `linkProfile` writes it.
110
- - `linkProfile(details, { username })` — `ensureAccount` with `username` as the e-mail (the login
111
- must have verified it) and `details.username` as the display name, then `ensureProfile` for the own
110
+ - `linkProfile(details, { username })` — `identityOf(ctx).ensureAccount` with `username` as the e-mail (the login
111
+ must have verified it) and `details.username` as the display name, then `identityOf(ctx).ensureProfile` for the own
112
112
  app in the main organization (`owner: true`, `scopes: ['*']`). A second method of a known person
113
113
  adds a credential; a person known from another app gets this app's row in their existing
114
114
  organization.
@@ -121,21 +121,22 @@ payload ??= await linking.linkProfile(details, { username: verifiedEmail })
121
121
  ### Write rows for a hosted app
122
122
 
123
123
  ```typescript
124
- import { ensureAccount, ensureProfile } from '@owlmeans/server-auth-identity'
124
+ import { identityOf } from '@owlmeans/server-auth-identity'
125
125
 
126
- const { account } = await ensureAccount(ctx, { email }, { type: 'email-otp', service: 'email', clientId, userId: email })
127
- const row = await ensureProfile(ctx, { account, service: clientId, entityId: account.entityId, owner: true })
126
+ const identity = identityOf(ctx)
127
+ const { account } = await identity.ensureAccount({ email }, { type: 'email-otp', service: 'email', clientId, userId: email })
128
+ const row = await identity.ensureProfile({ account, service: clientId, entityId: account.entityId, owner: true })
128
129
  ```
129
130
 
130
- `ensureAccount` writes no row — which app a person is a user of is the caller's to say. Hosted-app
131
- rows carry `scopes: []` (the default); never give them the own app's wildcard. `ensureProfile`
131
+ `identityOf(ctx).ensureAccount` writes no row — which app a person is a user of is the caller's to say. Hosted-app
132
+ rows carry `scopes: []` (the default); never give them the own app's wildcard. `identityOf(ctx).ensureProfile`
132
133
  returns an existing row unchanged: it never rewrites owner, groups or grants.
133
134
 
134
135
  ### Read rows in a gate or handler
135
136
 
136
137
  ```typescript
137
138
  const profile = await ctx.resource<IdentityProfileResource>(AUTH_IDENTITY_PROFILE)
138
- .load({ entityId: requireEntityKey(req), profileId: req.auth.profileId })
139
+ .load({ entityId: makeEntityScope(req).requireEntityKey(), profileId: req.auth.profileId })
139
140
  if (profile == null || profile.service !== MY_APP) throw new AuthForbidden('profile')
140
141
  ```
141
142
 
@@ -151,14 +152,14 @@ identityEvents(context)?.onProfileCreated(async (event, ctx) => {
151
152
  ```
152
153
 
153
154
  - **`onProfileCreated`** — `{ entityId, entitySlug, accountId, profileId, service, owner }`, fired
154
- by `ensureProfile` when the PRIMARY row of an (account, app) is created, by the create that won
155
+ by `identityOf(ctx).ensureProfile` when the PRIMARY row of an (account, app) is created, by the create that won
155
156
  only. Once per (account, app); a further organization's row announces nothing. Key app-level
156
157
  provisioning here and filter on `service`.
157
158
  - **`onEntityCreated`** — `EntityCreatedEvent` (`entityId`, `entitySlug`, `iamKey`, `accountId`,
158
159
  `profileId`, `username`, login `type`, provider `service`, the owner row's app as
159
- `profileService`, `createdAt`), fired by `linkProfile` when `ensureAccount` registered the person
160
- (`registered`), after the owner row exists. A caller registering through `ensureAccount` +
161
- `ensureProfile` itself announces with `propagateEntityCreated`.
160
+ `profileService`, `createdAt`), fired by `linkProfile` when `identityOf(ctx).ensureAccount` registered the person
161
+ (`registered`), after the owner row exists. A caller registering through `identityOf(ctx).ensureAccount` +
162
+ `.ensureProfile` itself announces with `propagateEntityCreated`.
162
163
  - Listeners run in registration order, each awaited; a throwing listener is logged and never fails
163
164
  the sign-in, and the next one still runs — so whatever a listener provisions needs a periodic
164
165
  backfill. `identityEvents(ctx)` is `null` where the service is not registered.
@@ -174,7 +175,7 @@ identityEvents(context)?.onProfileCreated(async (event, ctx) => {
174
175
  | One method attached twice at once | credentials (`type`, `userId`, `credential`) unique | Duplicate ignored |
175
176
  | One row created twice at once | profile (`profileId`, `entityId`) unique | Loser loads the winner's row; only the winner announces |
176
177
 
177
- A credential row pointing at a deleted account is removed by `ensureAccount`, so the method can be
178
+ A credential row pointing at a deleted account is removed by `identityOf(ctx).ensureAccount`, so the method can be
178
179
  attached again.
179
180
 
180
181
  ## Resource indexes
@@ -206,13 +207,13 @@ across organizations, not inside one.
206
207
  a guarded `collection.updateOne`.
207
208
  - **`IdentityLinkingService` is compatible with `AccountLinkingService`** of
208
209
  `@owlmeans/server-oidc-rp` but declared independently (acyclic dependencies).
209
- - **The address is the identity.** Whoever calls `ensureAccount` / `linkProfile` with an address is
210
+ - **The address is the identity.** Whoever calls `identityOf(ctx).ensureAccount` / `linkProfile` with an address is
210
211
  handed that person's account, so every caller must have verified it (a verified provider claim, a
211
212
  proven code, a full-trust key).
212
213
  - `mintSlug` throws `entity:slug-exhausted`; `rename` throws `entity:slug-malformed:<slug>`,
213
214
  `entity:slug-taken:<slug>` (a name any entity has ever answered to, or a unique-index race) and
214
215
  `entity:rename-contended:<id>`; `mintName` refuses an empty, dotted or `$` key
215
- (`entity:name-key-malformed:<key>`); `putOrgGroup` throws `UnknownRecordError` for a missing
216
+ (`entity:name-key-malformed:<key>`); `orgGroupsOf(ctx).putOrgGroup` throws `UnknownRecordError` for a missing
216
217
  organization.
217
218
  - The resolver caches every name a hit was found under for 30 s (`OrgEntityRef` only); a rename is
218
219
  visible to other replicas within that window, survivable because the old slug keeps resolving.
@@ -221,7 +222,7 @@ across organizations, not inside one.
221
222
 
222
223
  - **`@owlmeans/server-auth`** — verifies the bearer token; re-reads the row through this package.
223
224
  - **`@owlmeans/auth-common`** — declares `EntityResolverService`, `ENTITY_RESOLVER`, `OrgEntityRef`
224
- and the `entityKeyOf` / `requireEntityKey` / `attachEntity` helpers.
225
+ and `makeEntityScope(req)` with its `entityKeyOf` / `requireEntityKey` / `attachEntity`.
225
226
  - **`@owlmeans/server-oidc-rp`** — the relying party that calls the linking service on a callback.
226
227
  - **`@owlmeans/server-auth-otp`** — the e-mail-OTP plugin resolves its user through a linking service.
227
228
  - **`@owlmeans/iam-integrated`** — the IAM over the same store: hosted apps' rows, groups and grants.
@@ -230,6 +231,6 @@ across organizations, not inside one.
230
231
 
231
232
  - `@owlmeans/auth`, `@owlmeans/auth-common`, `@owlmeans/oidc`, `@owlmeans/context`,
232
233
  `@owlmeans/resource`, `@owlmeans/server-context`
233
- - `@owlmeans/basic-ids` — random keys and word slugs; `@scure/base` + `node:crypto` — `profileIdOf`
234
- - `@owlmeans/mongo-resource` — resources, declared references and indexes, `criteriaToFilter`
234
+ - `@owlmeans/basic-ids` — random keys and word slugs; `@scure/base` + `node:crypto` — `identityKeyHelper.profileIdOf`
235
+ - `@owlmeans/mongo-resource` — resources, declared references and indexes, `mongoCriteriaHelper.criteriaToFilter`
235
236
  - `mongodb` (peer)
@@ -0,0 +1,23 @@
1
+ import type { OrgGroup } from '../types.js';
2
+ /** The groups apps keep in organizations of one context's identity store. */
3
+ export interface OrgGroupHelper {
4
+ /** The groups one app keeps in an organization. */
5
+ listOrgGroups: (entityId: string, service: string) => Promise<OrgGroup[]>;
6
+ /**
7
+ * Create or replace the group (`service`, `key`) of an organization, keeping that pair unique.
8
+ *
9
+ * Two guarded field-level updates rather than a read and a write: replace the element that matches,
10
+ * else push one where none matches. A concurrent put of the same pair makes the push match nothing,
11
+ * and the next pass replaces what it pushed — so the pair can never appear twice, and nothing else
12
+ * in the document (another app's groups, minted names, the slug) is ever written.
13
+ *
14
+ * @throws {UnknownRecordError} when the organization does not exist.
15
+ */
16
+ putOrgGroup: (entityId: string, group: OrgGroup) => Promise<OrgGroup>;
17
+ /**
18
+ * Remove the group (`service`, `key`) from an organization — `true` when it was there. The rows that
19
+ * name it in their `groups` are the caller's to update.
20
+ */
21
+ removeOrgGroup: (entityId: string, service: string, key: string) => Promise<boolean>;
22
+ }
23
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/groups/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAE3C,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAA;IACzE;;;;;;;;;OASG;IACH,WAAW,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IACrE;;;OAGG;IACH,cAAc,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CACrF"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/groups/types.ts"],"names":[],"mappings":""}
package/build/groups.d.ts CHANGED
@@ -1,21 +1,13 @@
1
1
  import type { OrgGroup } from './types.js';
2
+ import type { OrgGroupHelper } from './groups/types.js';
2
3
  import type { Context } from './types.local.js';
3
- /** The groups one app keeps in an organization. */
4
+ export declare const makeOrgGroupHelper: (ctx: Context) => OrgGroupHelper;
5
+ /** The organization groups of a context — one per context. */
6
+ export declare const orgGroupsOf: (target: Context) => OrgGroupHelper;
7
+ /** @deprecated compat:factory-refactor — use `orgGroupsOf(ctx).listOrgGroups(…)` */
4
8
  export declare const listOrgGroups: (ctx: Context, entityId: string, service: string) => Promise<OrgGroup[]>;
5
- /**
6
- * Create or replace the group (`service`, `key`) of an organization, keeping that pair unique.
7
- *
8
- * Two guarded field-level updates rather than a read and a write: replace the element that matches,
9
- * else push one where none matches. A concurrent put of the same pair makes the push match nothing,
10
- * and the next pass replaces what it pushed — so the pair can never appear twice, and nothing else
11
- * in the document (another app's groups, minted names, the slug) is ever written.
12
- *
13
- * @throws {UnknownRecordError} when the organization does not exist.
14
- */
9
+ /** @deprecated compat:factory-refactor — use `orgGroupsOf(ctx).putOrgGroup(…)` */
15
10
  export declare const putOrgGroup: (ctx: Context, entityId: string, group: OrgGroup) => Promise<OrgGroup>;
16
- /**
17
- * Remove the group (`service`, `key`) from an organization — `true` when it was there. The rows that
18
- * name it in their `groups` are the caller's to update.
19
- */
11
+ /** @deprecated compat:factory-refactor — use `orgGroupsOf(ctx).removeOrgGroup(…)` */
20
12
  export declare const removeOrgGroup: (ctx: Context, entityId: string, service: string, key: string) => Promise<boolean>;
21
13
  //# sourceMappingURL=groups.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"groups.d.ts","sourceRoot":"","sources":["../src/groups.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAqB,QAAQ,EAAE,MAAM,YAAY,CAAA;AAG7D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAI/C,mDAAmD;AACnD,eAAO,MAAM,aAAa,QAAe,OAAO,YAAY,MAAM,WAAW,MAAM,KAAG,OAAO,CAAC,QAAQ,EAAE,CACT,CAAA;AAE/F;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,QAAe,OAAO,YAAY,MAAM,SAAS,QAAQ,KAAG,OAAO,CAAC,QAAQ,CA2BnG,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,cAAc,QAAe,OAAO,YAAY,MAAM,WAAW,MAAM,OAAO,MAAM,KAAG,OAAO,CAAC,OAAO,CAQlH,CAAA"}
1
+ {"version":3,"file":"groups.d.ts","sourceRoot":"","sources":["../src/groups.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAqB,QAAQ,EAAE,MAAM,YAAY,CAAA;AAG7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACvD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAE/C,eAAO,MAAM,kBAAkB,QAAS,OAAO,KAAG,cA8CjD,CAAA;AAED,8DAA8D;AAC9D,eAAO,MAAM,WAAW,qCAAyC,CAAA;AAEjE,oFAAoF;AACpF,eAAO,MAAM,aAAa,QAAe,OAAO,YAAY,MAAM,WAAW,MAAM,KAAG,OAAO,CAAC,QAAQ,EAAE,CAC/C,CAAA;AAEzD,kFAAkF;AAClF,eAAO,MAAM,WAAW,QAAe,OAAO,YAAY,MAAM,SAAS,QAAQ,KAAG,OAAO,CAAC,QAAQ,CAC/C,CAAA;AAErD,qFAAqF;AACrF,eAAO,MAAM,cAAc,QAAe,OAAO,YAAY,MAAM,WAAW,MAAM,OAAO,MAAM,KAAG,OAAO,CAAC,OAAO,CACpD,CAAA"}
package/build/groups.js CHANGED
@@ -1,50 +1,47 @@
1
1
  import { UnknownRecordError } from '@owlmeans/resource';
2
+ import { memoHelper } from '@owlmeans/context';
2
3
  import { AUTH_IDENTITY_ORG_ENTITY, MAX_GUARDED_UPDATE_ATTEMPTS } from './consts.js';
3
- import { idFilter } from './native.js';
4
- const entities = (ctx) => ctx.resource(AUTH_IDENTITY_ORG_ENTITY);
5
- /** The groups one app keeps in an organization. */
6
- export const listOrgGroups = async (ctx, entityId, service) => ((await entities(ctx).get(entityId)).groups ?? []).filter(group => group.service === service);
7
- /**
8
- * Create or replace the group (`service`, `key`) of an organization, keeping that pair unique.
9
- *
10
- * Two guarded field-level updates rather than a read and a write: replace the element that matches,
11
- * else push one where none matches. A concurrent put of the same pair makes the push match nothing,
12
- * and the next pass replaces what it pushed — so the pair can never appear twice, and nothing else
13
- * in the document (another app's groups, minted names, the slug) is ever written.
14
- *
15
- * @throws {UnknownRecordError} when the organization does not exist.
16
- */
17
- export const putOrgGroup = async (ctx, entityId, group) => {
18
- if (group.service === '' || group.key === '') {
19
- throw new SyntaxError(`entity:group-malformed:${group.service}:${group.key}`);
20
- }
21
- const res = entities(ctx);
22
- const id = idFilter(res, entityId);
23
- const pair = { service: group.service, key: group.key };
24
- for (let attempt = 0; attempt < MAX_GUARDED_UPDATE_ATTEMPTS; ++attempt) {
25
- const replaced = await res.collection.updateOne({ ...id, groups: { $elemMatch: pair } }, { $set: { 'groups.$': group, updatedAt: new Date() } });
26
- if (replaced.matchedCount > 0) {
27
- return group;
4
+ import { nativeUtils } from './utils/native.js';
5
+ export const makeOrgGroupHelper = (ctx) => {
6
+ const entities = () => ctx.resource(AUTH_IDENTITY_ORG_ENTITY);
7
+ const listOrgGroups = async (entityId, service) => ((await entities().get(entityId)).groups ?? []).filter(group => group.service === service);
8
+ const putOrgGroup = async (entityId, group) => {
9
+ if (group.service === '' || group.key === '') {
10
+ throw new SyntaxError(`entity:group-malformed:${group.service}:${group.key}`);
28
11
  }
29
- const push = { $push: { groups: group }, $set: { updatedAt: new Date() } };
30
- const pushed = await res.collection.updateOne({ ...id, groups: { $not: { $elemMatch: pair } } }, push);
31
- if (pushed.matchedCount > 0) {
32
- return group;
12
+ const res = entities();
13
+ const id = nativeUtils.idFilter(res, entityId);
14
+ const pair = { service: group.service, key: group.key };
15
+ for (let attempt = 0; attempt < MAX_GUARDED_UPDATE_ATTEMPTS; ++attempt) {
16
+ const replaced = await res.collection.updateOne({ ...id, groups: { $elemMatch: pair } }, { $set: { 'groups.$': group, updatedAt: new Date() } });
17
+ if (replaced.matchedCount > 0) {
18
+ return group;
19
+ }
20
+ const push = { $push: { groups: group }, $set: { updatedAt: new Date() } };
21
+ const pushed = await res.collection.updateOne({ ...id, groups: { $not: { $elemMatch: pair } } }, push);
22
+ if (pushed.matchedCount > 0) {
23
+ return group;
24
+ }
25
+ if (await res.load(entityId) == null) {
26
+ throw new UnknownRecordError(entityId);
27
+ }
33
28
  }
34
- if (await res.load(entityId) == null) {
35
- throw new UnknownRecordError(entityId);
36
- }
37
- }
38
- throw new SyntaxError(`entity:group-contended:${group.service}:${group.key}`);
39
- };
40
- /**
41
- * Remove the group (`service`, `key`) from an organization — `true` when it was there. The rows that
42
- * name it in their `groups` are the caller's to update.
43
- */
44
- export const removeOrgGroup = async (ctx, entityId, service, key) => {
45
- const res = entities(ctx);
46
- const pull = { $pull: { groups: { service, key } }, $set: { updatedAt: new Date() } };
47
- const result = await res.collection.updateOne({ ...idFilter(res, entityId), groups: { $elemMatch: { service, key } } }, pull);
48
- return result.matchedCount > 0;
29
+ throw new SyntaxError(`entity:group-contended:${group.service}:${group.key}`);
30
+ };
31
+ const removeOrgGroup = async (entityId, service, key) => {
32
+ const res = entities();
33
+ const pull = { $pull: { groups: { service, key } }, $set: { updatedAt: new Date() } };
34
+ const result = await res.collection.updateOne({ ...nativeUtils.idFilter(res, entityId), groups: { $elemMatch: { service, key } } }, pull);
35
+ return result.matchedCount > 0;
36
+ };
37
+ return { listOrgGroups, putOrgGroup, removeOrgGroup };
49
38
  };
39
+ /** The organization groups of a context — one per context. */
40
+ export const orgGroupsOf = memoHelper.oncePer(makeOrgGroupHelper);
41
+ /** @deprecated compat:factory-refactor — use `orgGroupsOf(ctx).listOrgGroups(…)` */
42
+ export const listOrgGroups = async (ctx, entityId, service) => await orgGroupsOf(ctx).listOrgGroups(entityId, service);
43
+ /** @deprecated compat:factory-refactor — use `orgGroupsOf(ctx).putOrgGroup(…)` */
44
+ export const putOrgGroup = async (ctx, entityId, group) => await orgGroupsOf(ctx).putOrgGroup(entityId, group);
45
+ /** @deprecated compat:factory-refactor — use `orgGroupsOf(ctx).removeOrgGroup(…)` */
46
+ export const removeOrgGroup = async (ctx, entityId, service, key) => await orgGroupsOf(ctx).removeOrgGroup(entityId, service, key);
50
47
  //# sourceMappingURL=groups.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"groups.js","sourceRoot":"","sources":["../src/groups.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"groups.js","sourceRoot":"","sources":["../src/groups.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { AuthPayload } from '@owlmeans/auth';
2
+ /** Find-or-create an end-user identity scoped to an entity by email address.
3
+ *
4
+ * Used by the OTP auth plugin: after code verification, look up whether this
5
+ * email already has a profile under the entity and create one if not.
6
+ */
7
+ export interface EmailIdentityArgs {
8
+ email: string;
9
+ /** The entity (project owner) this login is scoped to */
10
+ entityId?: string;
11
+ /** Human-readable display name (defaults to email) */
12
+ username?: string;
13
+ }
14
+ /** Result matches AuthPayload so it can be embedded directly in the credential. */
15
+ export type EmailIdentityPayload = AuthPayload;
16
+ /** Extension seam for IAM-backed identity operations — Phase 2 fills this */
17
+ export interface IdentityIamExtension {
18
+ /** Inputs and result types for the email find-or-create path */
19
+ EmailIdentityArgs: EmailIdentityArgs;
20
+ EmailIdentityPayload: EmailIdentityPayload;
21
+ }
22
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/iam/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAEjD;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,yDAAyD;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,mFAAmF;AACnF,MAAM,MAAM,oBAAoB,GAAG,WAAW,CAAA;AAE9C,6EAA6E;AAC7E,MAAM,WAAW,oBAAoB;IACnC,gEAAgE;IAChE,iBAAiB,EAAE,iBAAiB,CAAA;IACpC,oBAAoB,EAAE,oBAAoB,CAAA;CAC3C"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/iam/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,35 @@
1
+ import type { ProviderProfileDetails } from '@owlmeans/oidc';
2
+ import type { EnsureAccountArgs, EnsuredAccount, EnsureProfileArgs, IdentityAccount, IdentityCredentials, IdentityProfile } from '../types.js';
3
+ /** A sign-in method's credential row and the account it signs into, if any. */
4
+ export interface LinkedCredential {
5
+ credential: IdentityCredentials;
6
+ account: IdentityAccount | null;
7
+ }
8
+ /** The identity store of one context: accounts, their app rows and their sign-in methods. */
9
+ export interface IdentityHelper {
10
+ /** The credential row of a sign-in method and the account it signs into, if any. */
11
+ credentialOf: (details: ProviderProfileDetails) => Promise<LinkedCredential | null>;
12
+ /**
13
+ * The account of a person — one per e-mail address, whichever method they sign in by.
14
+ *
15
+ * Looked up by the sign-in method's credential first (a returning login), then by the address (a
16
+ * further method of a known person, which is attached as a credential); otherwise registered with a
17
+ * personal organization. Writes no profile row: which app the person is a user of is the caller's
18
+ * to say, through {@link IdentityHelper.ensureProfile}.
19
+ *
20
+ * Every caller must have established the address — a verified provider claim, a proven code, a
21
+ * full-trust key — because whoever names an address here is handed that person's account.
22
+ */
23
+ ensureAccount: (args: EnsureAccountArgs, details?: ProviderProfileDetails) => Promise<EnsuredAccount>;
24
+ /**
25
+ * The row of a person in one organization for one app. Idempotent: an existing row is returned as
26
+ * it is, never rewritten.
27
+ *
28
+ * A row outside the account's main organization brings the PRIMARY row of the same (account, app)
29
+ * with it — created first, as the owner of the personal organization — because the primary row is
30
+ * what says whether the person may use the app at all; a crash between the two must never leave a
31
+ * membership without it.
32
+ */
33
+ ensureProfile: (args: EnsureProfileArgs) => Promise<IdentityProfile>;
34
+ }
35
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/identity/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AAC5D,OAAO,KAAK,EACV,iBAAiB,EAAE,cAAc,EAAE,iBAAiB,EAAE,eAAe,EAAE,mBAAmB,EAAE,eAAe,EAC5G,MAAM,aAAa,CAAA;AAEpB,+EAA+E;AAC/E,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,mBAAmB,CAAA;IAC/B,OAAO,EAAE,eAAe,GAAG,IAAI,CAAA;CAChC;AAED,6FAA6F;AAC7F,MAAM,WAAW,cAAc;IAC7B,oFAAoF;IACpF,YAAY,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;IACnF;;;;;;;;;;OAUG;IACH,aAAa,EAAE,CAAC,IAAI,EAAE,iBAAiB,EAAE,OAAO,CAAC,EAAE,sBAAsB,KAAK,OAAO,CAAC,cAAc,CAAC,CAAA;IACrG;;;;;;;;OAQG;IACH,aAAa,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,eAAe,CAAC,CAAA;CACrE"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/identity/types.ts"],"names":[],"mappings":""}