@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.
- package/README.md +24 -23
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-auth-identity/SKILL.md +28 -27
- package/build/groups/types.d.ts +23 -0
- package/build/groups/types.d.ts.map +1 -0
- package/build/groups/types.js +2 -0
- package/build/groups/types.js.map +1 -0
- package/build/groups.d.ts +7 -15
- package/build/groups.d.ts.map +1 -1
- package/build/groups.js +41 -44
- package/build/groups.js.map +1 -1
- package/build/iam/types.d.ts +22 -0
- package/build/iam/types.d.ts.map +1 -0
- package/build/iam/types.js +2 -0
- package/build/iam/types.js.map +1 -0
- package/build/identity/types.d.ts +35 -0
- package/build/identity/types.d.ts.map +1 -0
- package/build/identity/types.js +2 -0
- package/build/identity/types.js.map +1 -0
- package/build/identity.d.ts +9 -40
- package/build/identity.d.ts.map +1 -1
- package/build/identity.js +156 -185
- package/build/identity.js.map +1 -1
- package/build/index.d.ts +5 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/build/keys/types.d.ts +22 -0
- package/build/keys/types.d.ts.map +1 -0
- package/build/keys/types.js +2 -0
- package/build/keys/types.js.map +1 -0
- package/build/keys.d.ts +4 -0
- package/build/keys.d.ts.map +1 -0
- package/build/keys.js +16 -0
- package/build/keys.js.map +1 -0
- package/build/resolver.js +7 -7
- package/build/resolver.js.map +1 -1
- package/build/service.d.ts.map +1 -1
- package/build/service.js +10 -9
- package/build/service.js.map +1 -1
- package/build/utils/native/types.d.ts +17 -0
- package/build/utils/native/types.d.ts.map +1 -0
- package/build/utils/native/types.js +2 -0
- package/build/utils/native/types.js.map +1 -0
- package/build/utils/native.d.ts +4 -0
- package/build/utils/native.d.ts.map +1 -0
- package/build/utils/native.js +8 -0
- package/build/utils/native.js.map +1 -0
- package/package.json +10 -10
- package/src/groups/types.ts +23 -0
- package/src/groups.ts +55 -49
- package/src/identity/types.ts +38 -0
- package/src/identity.ts +162 -192
- package/src/index.ts +5 -1
- package/src/keys/types.ts +22 -0
- package/src/keys.ts +24 -0
- package/src/resolver.ts +7 -7
- package/src/service.ts +10 -9
- package/src/utils/native/types.ts +17 -0
- package/src/utils/native.ts +17 -0
- package/tests/context.ts +10 -10
- package/tests/events.spec.ts +5 -4
- package/tests/identity.spec.ts +27 -26
- package/tests/linking.spec.ts +11 -10
- package/tests/resolver.spec.ts +18 -18
- package/src/native.ts +0 -18
- /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.
|
|
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
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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:
|
|
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 {
|
|
163
|
+
import { identityOf } from '@owlmeans/server-auth-identity'
|
|
164
164
|
|
|
165
|
-
const
|
|
166
|
-
const
|
|
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,
|
|
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(
|
|
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)` |
|
|
214
|
-
| `ensureAccount(
|
|
215
|
-
| `ensureProfile(
|
|
216
|
-
| `credentialKeyOf(details)`, `credentialOf(
|
|
217
|
-
| `listOrgGroups`,
|
|
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
|
|
276
|
-
`
|
|
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.
|
|
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
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/server-auth-identity",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-10-
|
|
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,
|
|
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.
|
|
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(
|
|
26
|
-
| `ensureProfile(
|
|
27
|
-
| `credentialKeyOf(details)`, `credentialOf(
|
|
28
|
-
| `
|
|
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 {
|
|
124
|
+
import { identityOf } from '@owlmeans/server-auth-identity'
|
|
125
125
|
|
|
126
|
-
const
|
|
127
|
-
const
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 @@
|
|
|
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
|
-
|
|
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
|
package/build/groups.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"groups.d.ts","sourceRoot":"","sources":["../src/groups.ts"],"names":[],"mappings":"
|
|
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 {
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
package/build/groups.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"groups.js","sourceRoot":"","sources":["../src/groups.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;
|
|
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 @@
|
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/identity/types.ts"],"names":[],"mappings":""}
|