@owlmeans/server-auth-identity 0.1.18-rc.41 → 0.1.18-rc.43
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 +77 -42
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-auth-identity/SKILL.md +168 -199
- package/build/consts.d.ts +11 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +13 -0
- package/build/consts.js.map +1 -1
- package/build/events.d.ts.map +1 -1
- package/build/events.js +22 -10
- package/build/events.js.map +1 -1
- package/build/groups.d.ts +23 -0
- package/build/groups.d.ts.map +1 -0
- package/build/groups.js +50 -0
- package/build/groups.js.map +1 -0
- package/build/helper.d.ts +6 -1
- package/build/helper.d.ts.map +1 -1
- package/build/helper.js +6 -2
- package/build/helper.js.map +1 -1
- package/build/identity.d.ts +49 -0
- package/build/identity.d.ts.map +1 -0
- package/build/identity.js +205 -0
- package/build/identity.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/build/native.d.ts +14 -0
- package/build/native.d.ts.map +1 -0
- package/build/native.js +12 -0
- package/build/native.js.map +1 -0
- package/build/resolver.d.ts.map +1 -1
- package/build/resolver.js +52 -30
- package/build/resolver.js.map +1 -1
- package/build/resource.d.ts +3 -0
- package/build/resource.d.ts.map +1 -1
- package/build/resource.js +19 -14
- package/build/resource.js.map +1 -1
- package/build/service.d.ts +6 -2
- package/build/service.d.ts.map +1 -1
- package/build/service.js +80 -211
- package/build/service.js.map +1 -1
- package/build/types.d.ts +154 -24
- package/build/types.d.ts.map +1 -1
- package/package.json +7 -6
- package/src/consts.ts +16 -0
- package/src/events.ts +29 -10
- package/src/groups.ts +67 -0
- package/src/helper.ts +8 -2
- package/src/identity.ts +240 -0
- package/src/index.ts +2 -0
- package/src/native.ts +18 -0
- package/src/resolver.ts +65 -36
- package/src/resource.ts +19 -14
- package/src/service.ts +86 -224
- package/src/types.ts +163 -24
- package/tests/context.ts +188 -79
- package/tests/events.spec.ts +58 -55
- package/tests/identity.spec.ts +148 -0
- package/tests/linking.spec.ts +120 -90
- package/tests/resolver.spec.ts +112 -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.43
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
## Concepts
|
|
@@ -19,17 +19,23 @@ bun add @owlmeans/server-auth-identity@^0.1.18-rc.41
|
|
|
19
19
|
never on the wire), `slug` (renameable `entitySlug`, the only value a token carries),
|
|
20
20
|
`formerSlugs` that keep resolving after a rename, a frozen `iamKey`, and `names` minted once for
|
|
21
21
|
systems that cannot be renamed.
|
|
22
|
-
- **Account (`IdentityAccount`)** — one per person,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
- **Account (`IdentityAccount`)** — one per person: a unique, lower-case `email`, a display `name`,
|
|
23
|
+
and `entityId`, the personal organization created with it.
|
|
24
|
+
- **Credentials (`IdentityCredentials`)** — one row per sign-in method of an account (`accountId`),
|
|
25
|
+
keyed by `"{type}:{service}:{providerSub}"`.
|
|
26
|
+
- **Profile row (`IdentityProfile`)** — one person in one organization for one app (`service`):
|
|
27
|
+
`profileId` (computed by `profileIdOf(service, accountId)`), `owner`, `role`, `scopes`,
|
|
28
|
+
`permissions`, `groups`. The row in the account's own organization is the app's PRIMARY row.
|
|
29
|
+
- **Groups (`OrgGroup`)** — an app's groups, kept in the organization record, unique per
|
|
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).
|
|
33
|
+
- **Linking service** — `IdentityLinkingService` signs a provider login into the deployment's own
|
|
34
|
+
app and returns an `AuthPayload`.
|
|
29
35
|
- **Entity resolver** — the `EntityResolverService` registered under `ENTITY_RESOLVER`. Registering
|
|
30
36
|
it tells the server boundary that this deployment has organizations.
|
|
31
|
-
- **Identity events** — `identityEvents(ctx)` returns the `IdentityEventsService
|
|
32
|
-
`
|
|
37
|
+
- **Identity events** — `identityEvents(ctx)` returns the `IdentityEventsService`:
|
|
38
|
+
`onProfileCreated` runs once per (account, app), `onEntityCreated` once per registration.
|
|
33
39
|
|
|
34
40
|
## Usage
|
|
35
41
|
|
|
@@ -62,7 +68,8 @@ import { appendAuthIdentityResources, AUTH_IDENTITY_DB_ALIAS } from '@owlmeans/s
|
|
|
62
68
|
|
|
63
69
|
appendMongo<C, T>(context)
|
|
64
70
|
appendAuthService<C, T>(context)
|
|
65
|
-
|
|
71
|
+
// MY_APP: this deployment's own app key — the `service` of the rows its sign-in writes
|
|
72
|
+
appendAuthIdentityResources(context, AUTH_IDENTITY_DB_ALIAS, { service: MY_APP })
|
|
66
73
|
context.registerService(makeMyAppGate())
|
|
67
74
|
```
|
|
68
75
|
|
|
@@ -89,7 +96,9 @@ export const makeMyAppGate = (alias: string = MY_APP_GATE): GateService => {
|
|
|
89
96
|
|
|
90
97
|
const profile = await ctx.resource<IdentityProfileResource>(AUTH_IDENTITY_PROFILE)
|
|
91
98
|
.load({ entityId, profileId: req.auth.profileId })
|
|
92
|
-
|
|
99
|
+
// The store also holds other apps' rows of the same organization — only this app's count.
|
|
100
|
+
if (profile == null || profile.service !== MY_APP || profile.disabled === true
|
|
101
|
+
|| (profile.expiresAt != null && new Date(profile.expiresAt) < new Date())) {
|
|
93
102
|
throw new AuthForbidden('profile')
|
|
94
103
|
}
|
|
95
104
|
|
|
@@ -119,7 +128,7 @@ const api = handlers<Context>()
|
|
|
119
128
|
|
|
120
129
|
export const members = api.request(memberProtocols.list, async (request, context) => {
|
|
121
130
|
const { items } = await context.resource<IdentityProfileResource>(AUTH_IDENTITY_PROFILE)
|
|
122
|
-
.list({ entityId: requireEntityKey(request) }, { sort: [{ field: 'createdAt', order: 'desc' }] })
|
|
131
|
+
.list({ entityId: requireEntityKey(request), service: MY_APP }, { size: 0, sort: [{ field: 'createdAt', order: 'desc' }] })
|
|
123
132
|
|
|
124
133
|
return items.map(({ profileId, role, name }) => ({ profileId, role, name }))
|
|
125
134
|
})
|
|
@@ -140,29 +149,42 @@ const details = {
|
|
|
140
149
|
userId: providerSub, // the provider's subject claim
|
|
141
150
|
}
|
|
142
151
|
|
|
143
|
-
// Returning login:
|
|
152
|
+
// Returning login: the credential, its account, this app's row in the account's organization.
|
|
144
153
|
let payload = await linking.getLinkedProfile(details)
|
|
145
|
-
//
|
|
154
|
+
// Otherwise: the account by the verified address (registered if new), the method attached to it,
|
|
155
|
+
// and this app's owner row in the person's own organization.
|
|
146
156
|
payload ??= await linking.linkProfile(details, { username: 'person@example.org' })
|
|
147
|
-
// payload: { type, role, userId, profileId, entitySlug, scopes }
|
|
157
|
+
// payload: { type, role, userId: accountId, profileId, entitySlug, scopes }
|
|
148
158
|
```
|
|
149
159
|
|
|
150
|
-
###
|
|
160
|
+
### Rows for a hosted app
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { ensureAccount, ensureProfile } from '@owlmeans/server-auth-identity'
|
|
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 })
|
|
167
|
+
// a hosted app's rows carry `scopes: []`; a row in another organization brings the primary row with it
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Provision when a person becomes a user of the app
|
|
151
171
|
|
|
152
172
|
```ts
|
|
153
173
|
import { identityEvents } from '@owlmeans/server-auth-identity'
|
|
154
174
|
|
|
155
175
|
// in makeContext, after appendAuthIdentityResources
|
|
156
|
-
identityEvents(context)?.
|
|
176
|
+
identityEvents(context)?.onProfileCreated(async (event, ctx) => {
|
|
177
|
+
if (event.service !== MY_APP) return
|
|
157
178
|
await ctx.service<PlanService>(PLAN_SERVICE).grantStarterPlan(event.entityId)
|
|
158
179
|
})
|
|
159
180
|
```
|
|
160
181
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
sign-in
|
|
182
|
+
`onProfileCreated` carries `entityId`, `entitySlug`, `accountId`, `profileId`, `service` and
|
|
183
|
+
`owner`, and fires once per (account, app), when its primary row is created. `onEntityCreated`
|
|
184
|
+
carries `entityId`, `entitySlug`, `iamKey`, `accountId`, `profileId`, `username`, the login `type`
|
|
185
|
+
and `service`, the owner row's app as `profileService`, and `createdAt`; `linkProfile` fires it when
|
|
186
|
+
the sign-in registered the person. Listeners run in order and are awaited; one that throws is logged
|
|
187
|
+
and never fails the sign-in, so anything a listener provisions needs its own backfill.
|
|
166
188
|
|
|
167
189
|
### Rename an organization and mint a durable name
|
|
168
190
|
|
|
@@ -187,12 +209,17 @@ const namespace = await resolver.mintName(entityId, 'namespace', entity => `my-a
|
|
|
187
209
|
|
|
188
210
|
| Symbol | Kind | Purpose |
|
|
189
211
|
|---|---|---|
|
|
190
|
-
| `appendAuthIdentityResources(context, dbAlias?)` | function | Register the four resources, the linking service
|
|
212
|
+
| `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 |
|
|
191
218
|
| `makeOrgEntityResource(dbAlias?)` | function | Mongo resource for `OrgEntity` |
|
|
192
219
|
| `makeIdentityAccountResource(dbAlias?)` | function | Mongo resource for `IdentityAccount` |
|
|
193
220
|
| `makeIdentityProfileResource(dbAlias?)` | function | Mongo resource for `IdentityProfile` |
|
|
194
221
|
| `makeIdentityCredentialsResource(dbAlias?)` | function | Mongo resource for `IdentityCredentials` |
|
|
195
|
-
| `makeIdentityLinkingService()` | function | The `IdentityLinkingService` implementation |
|
|
222
|
+
| `makeIdentityLinkingService({ service? })` | function | The `IdentityLinkingService` implementation for one app |
|
|
196
223
|
| `makeEntityResolverService(alias = ENTITY_RESOLVER)` | function | The `EntityResolverService` implementation, cached 30 s per resolved name |
|
|
197
224
|
| `makeIdentityEventsService(alias = AUTH_IDENTITY_EVENTS)` | function | The `IdentityEventsService` implementation (lazy) |
|
|
198
225
|
| `identityEvents(ctx, alias?)` | function | The registered events service, or `null` |
|
|
@@ -206,21 +233,25 @@ const namespace = await resolver.mintName(entityId, 'namespace', entity => `my-a
|
|
|
206
233
|
| `AUTH_IDENTITY_EVENTS` | const | `'auth-identity:events'` — identity-events service alias |
|
|
207
234
|
| `AUTH_IDENTITY_DB_ALIAS` | const | `'auth-identity'` — suggested db config alias |
|
|
208
235
|
| `AUTH_IDENTITY_ORG_ENTITY_COLLECTION`, `AUTH_IDENTITY_ACCOUNT_COLLECTION`, `AUTH_IDENTITY_PROFILE_COLLECTION`, `AUTH_IDENTITY_CREDENTIALS_COLLECTION` | const | Colon-free Mongo collection base names |
|
|
236
|
+
| `DEFAULT_APP_SERVICE` | const | `'app'` — the own app key when none is passed |
|
|
209
237
|
| `MAX_ENTITY_SLUG_ATTEMPTS` | const | `8` — word slugs tried before minting gives up |
|
|
238
|
+
| `MAX_ACCOUNT_KEY_ATTEMPTS`, `MAX_GUARDED_UPDATE_ATTEMPTS`, `PROFILE_DIGEST_LENGTH` | const | Retry bounds; the `profileId` digest length |
|
|
210
239
|
| `LOGIN_SERVICE_PREFIX`, `EXTERNAL_KEY_DELIMITER` | const | `'service'` and `':'` — the derived-key grammar |
|
|
211
240
|
|
|
212
241
|
### Types
|
|
213
242
|
|
|
214
243
|
| Symbol | Kind | Purpose |
|
|
215
244
|
|---|---|---|
|
|
216
|
-
| `OrgEntity` | type | `id`, `slug`, `formerSlugs?`, `iamKey`, `names?`, `createdAt`, `updatedAt?` |
|
|
217
|
-
| `
|
|
218
|
-
| `
|
|
219
|
-
| `
|
|
245
|
+
| `OrgEntity` | type | `id`, `slug`, `formerSlugs?`, `iamKey`, `title?`, `names?`, `groups?`, `createdAt`, `updatedAt?` |
|
|
246
|
+
| `OrgGroup`, `OrgGroupBundle`, `OrgGroupFilter` | type | An app's group in an organization and its permission bundles |
|
|
247
|
+
| `IdentityAccount` | type | `id`, `credential`, `email`, `name`, `entityId`, `scopes` |
|
|
248
|
+
| `IdentityProfile` | type | `id`, `profileId`, `userId`, `service`, `entityId`, `owner?`, `role`, `scopes`, `permissions`, `groups?`, `home?`, `disabled?`, `managed?` |
|
|
249
|
+
| `IdentityCredentials` | type | `AuthCredentials` + `accountId` (`profileId?` reserved) |
|
|
250
|
+
| `EnsureAccountArgs`, `EnsuredAccount`, `EnsureProfileArgs`, `IdentityResourcesOptions` | type | The primitives' arguments and results |
|
|
220
251
|
| `IdentityLinkingService` | type | `getLinkedProfile`, `linkProfile`, `linkCredentials`, `unlinkCredentials`, `getOwnerProfiles`, `getOwnerCredentials` |
|
|
221
|
-
| `AccountMeta` | type | `{ username
|
|
222
|
-
| `IdentityEventsService` | type | `onEntityCreated
|
|
223
|
-
| `EntityCreatedEvent`, `
|
|
252
|
+
| `AccountMeta` | type | `{ username }` — the verified address |
|
|
253
|
+
| `IdentityEventsService` | type | `onEntityCreated` / `propagateEntityCreated`, `onProfileCreated` / `propagateProfileCreated` |
|
|
254
|
+
| `EntityCreatedEvent`, `ProfileCreatedEvent` and their callbacks | type | The event payloads; `(event, ctx) => Promise<void>` |
|
|
224
255
|
| `OrgEntityResource`, `IdentityAccountResource`, `IdentityProfileResource`, `IdentityCredentialsResource` | type | Typed `MongoResource` aliases |
|
|
225
256
|
| `IdentityConfig`, `IdentityContext` | type | Server config and context shapes |
|
|
226
257
|
| `GoogleUserInfo` | type | Google userinfo claims |
|
|
@@ -228,12 +259,14 @@ const namespace = await resolver.mintName(entityId, 'namespace', entity => `my-a
|
|
|
228
259
|
|
|
229
260
|
## Key derivation
|
|
230
261
|
|
|
231
|
-
- Account `credential` — a unique 16-character Base58
|
|
232
|
-
- Account
|
|
233
|
-
-
|
|
234
|
-
|
|
262
|
+
- Account `credential` — a unique 16-character Base58 key.
|
|
263
|
+
- Account `email` — trimmed, lower-case, unique.
|
|
264
|
+
- Account / profile `entityId`, profile `userId` and `home`, credentials `accountId` — record ids,
|
|
265
|
+
declared ObjectId references; a first registration creates the organization first.
|
|
266
|
+
- Profile `profileId` — `profileIdOf(service, accountId)`: `"{service}:"` + 22 Base58 characters of
|
|
267
|
+
`sha256("{accountId}:{service}")`. Computed, the same on every row of one (account, app).
|
|
235
268
|
- Credentials `userId` — `"{type}:{service}:{providerSub}"`; unrelated to the profile's `userId`.
|
|
236
|
-
- Credentials
|
|
269
|
+
- Credentials `credential` — `"service:{type}:{service}"`.
|
|
237
270
|
|
|
238
271
|
## Common pitfalls
|
|
239
272
|
|
|
@@ -244,10 +277,12 @@ const namespace = await resolver.mintName(entityId, 'namespace', entity => `my-a
|
|
|
244
277
|
- Without the resolver `request.entity` stays undefined and consumers fall back to the slug — call
|
|
245
278
|
`appendAuthIdentityResources` (or register the resolver) in every service that serves organization
|
|
246
279
|
data.
|
|
247
|
-
-
|
|
248
|
-
|
|
249
|
-
-
|
|
250
|
-
|
|
280
|
+
- `linkProfile` rows get `ALL_SCOPES` and `AuthRole.User`; a hosted app's rows (`ensureProfile`
|
|
281
|
+
default) get `[]`. A gate of the own app must also require `profile.service`.
|
|
282
|
+
- The address is the identity: whoever names an address to `ensureAccount` / `linkProfile` gets that
|
|
283
|
+
person's account, so only a verified address may reach them.
|
|
284
|
+
- Never `update()` an organization record — write fields (the resolver, the group helpers, a guarded
|
|
285
|
+
`collection.updateOne`), or a stale read erases groups and minted names.
|
|
251
286
|
- Provisioning in an `onEntityCreated` listener is best-effort: a throwing listener is logged and the
|
|
252
287
|
sign-in succeeds, so reconcile what it provisions periodically.
|
|
253
288
|
- The resolver caches for 30 seconds, so a rename reaches other replicas within that window; the old
|
|
@@ -273,7 +308,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
273
308
|
your project's skill store (`.agents/skills/`):
|
|
274
309
|
|
|
275
310
|
```sh
|
|
276
|
-
npx @owlmeans/agent-skills@^0.1.18-rc.
|
|
311
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.45
|
|
277
312
|
```
|
|
278
313
|
|
|
279
314
|
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-
|
|
4
|
+
"version": "0.1.18-rc.43",
|
|
5
|
+
"generatedAt": "2026-10-01T23:27:48.721Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|