@owlmeans/server-auth-identity 0.1.18-rc.3 → 0.1.18-rc.31

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 (44) hide show
  1. package/README.md +229 -57
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/server-auth-identity/SKILL.md +191 -74
  4. package/build/consts.d.ts +4 -0
  5. package/build/consts.d.ts.map +1 -1
  6. package/build/consts.js +7 -0
  7. package/build/consts.js.map +1 -1
  8. package/build/events.d.ts +9 -0
  9. package/build/events.d.ts.map +1 -0
  10. package/build/events.js +29 -0
  11. package/build/events.js.map +1 -0
  12. package/build/helper.d.ts.map +1 -1
  13. package/build/helper.js +13 -1
  14. package/build/helper.js.map +1 -1
  15. package/build/index.d.ts +2 -0
  16. package/build/index.d.ts.map +1 -1
  17. package/build/index.js +2 -0
  18. package/build/index.js.map +1 -1
  19. package/build/resolver.d.ts +3 -0
  20. package/build/resolver.d.ts.map +1 -0
  21. package/build/resolver.js +133 -0
  22. package/build/resolver.js.map +1 -0
  23. package/build/resource.d.ts +11 -1
  24. package/build/resource.d.ts.map +1 -1
  25. package/build/resource.js +30 -4
  26. package/build/resource.js.map +1 -1
  27. package/build/service.d.ts.map +1 -1
  28. package/build/service.js +130 -25
  29. package/build/service.js.map +1 -1
  30. package/build/types.d.ts +78 -4
  31. package/build/types.d.ts.map +1 -1
  32. package/package.json +10 -9
  33. package/src/consts.ts +8 -0
  34. package/src/events.ts +39 -0
  35. package/src/helper.ts +13 -1
  36. package/src/index.ts +2 -0
  37. package/src/resolver.ts +165 -0
  38. package/src/resource.ts +36 -6
  39. package/src/service.ts +146 -28
  40. package/src/types.ts +82 -4
  41. package/tests/context.ts +98 -0
  42. package/tests/events.spec.ts +81 -0
  43. package/tests/linking.spec.ts +133 -0
  44. package/tsconfig.json +1 -1
package/README.md CHANGED
@@ -1,97 +1,269 @@
1
1
  # @owlmeans/server-auth-identity
2
2
 
3
- Mongo-backed local identity resources and provider account linking for OwlMeans server applications.
4
-
5
- ## Overview
6
-
7
- - Registers three Mongo resources: local accounts, local profiles, and provider credentials.
8
- - Provides `IdentityLinkingService` to map external provider profile details into an OwlMeans `AuthPayload`.
9
- - Keeps external provider identity separate from local product identity and authorization scopes.
10
- - Designed to work with `@owlmeans/server-auth`, `@owlmeans/server-oidc-rp`, and product-specific module gates.
3
+ The Mongo-backed local identity store behind provider logins. An application uses it when it owns
4
+ its users and organizations: a Google, OIDC, email-OTP or supervisor login is mapped onto one local
5
+ account, profile and credential, and the organization entity registry turns the slug on a token into
6
+ a stable entity id. It is not used where identity and authorization are delegated to an external IAM
7
+ whose grants decide access (use `@owlmeans/server-oidc-rp`'s guard and gate, or
8
+ `@owlmeans/server-iam`), and it does not verify bearer tokens (that is `@owlmeans/server-auth`).
11
9
 
12
10
  ## Installation
13
11
 
14
12
  ```bash
15
- bun add @owlmeans/server-auth-identity
13
+ bun add @owlmeans/server-auth-identity@^0.1.18-rc.31
16
14
  ```
17
15
 
16
+ ## Concepts
17
+
18
+ - **Organization entity (`OrgEntity`)** — the customer organization: `id` (stable `entityId`,
19
+ never on the wire), `slug` (renameable `entitySlug`, the only value a token carries),
20
+ `formerSlugs` that keep resolving after a rename, a frozen `iamKey`, and `names` minted once for
21
+ systems that cannot be renamed.
22
+ - **Account (`IdentityAccount`)** — one per person, matched on a verified email `name`.
23
+ - **Profile (`IdentityProfile`)** — one person inside one organization entity, with `role`,
24
+ `scopes` and optional `expiresAt`. The durable authorization record.
25
+ - **Credentials (`IdentityCredentials`)** — one provider link per profile, keyed by
26
+ `"{type}:{service}:{providerSub}"`.
27
+ - **Linking service** — `IdentityLinkingService` finds or creates the local identity for provider
28
+ profile details and returns an `AuthPayload`.
29
+ - **Entity resolver** — the `EntityResolverService` registered under `ENTITY_RESOLVER`. Registering
30
+ it tells the server boundary that this deployment has organizations.
31
+ - **Identity events** — `identityEvents(ctx)` returns the `IdentityEventsService`; its
32
+ `onEntityCreated` listeners run once per newly registered organization entity.
33
+
18
34
  ## Usage
19
35
 
20
- Register identity resources in a server context after Mongo and auth services are available:
36
+ ### Register in a context
37
+
38
+ Register after the Mongo service and `appendAuthService`. A dedicated db config under
39
+ `AUTH_IDENTITY_DB_ALIAS` scopes a collection prefix to the identity collections only.
21
40
 
22
- ```typescript
23
- import { appendAuthIdentityResources } from '@owlmeans/server-auth-identity'
41
+ ```ts
42
+ // config.ts
43
+ import { AUTH_IDENTITY_DB_ALIAS, AUTH_IDENTITY_LINKING } from '@owlmeans/server-auth-identity'
24
44
 
25
- appendAuthIdentityResources(context)
45
+ cfg.dbs.push({
46
+ alias: AUTH_IDENTITY_DB_ALIAS,
47
+ service: MONGO_SERVICE,
48
+ host: process.env.MONGO_HOST!,
49
+ schema: 'my-app',
50
+ secret: process.env.MONGO_SECRET!,
51
+ resourcePrefix: 'my-app-', // -> my-app-account, my-app-profile, ...
52
+ })
53
+
54
+ // let the OIDC relying party link provider logins through this store
55
+ cfg.oidc.accountLinkingService = AUTH_IDENTITY_LINKING
26
56
  ```
27
57
 
28
- Link an external provider profile to a local account/profile:
58
+ ```ts
59
+ // context.ts
60
+ import { appendAuthService } from '@owlmeans/server-auth'
61
+ import { appendAuthIdentityResources, AUTH_IDENTITY_DB_ALIAS } from '@owlmeans/server-auth-identity'
29
62
 
30
- ```typescript
31
- import { AUTH_IDENTITY_LINKING } from '@owlmeans/server-auth-identity'
32
- import type { IdentityLinkingService } from '@owlmeans/server-auth-identity'
63
+ appendMongo<C, T>(context)
64
+ appendAuthService<C, T>(context)
65
+ appendAuthIdentityResources(context, AUTH_IDENTITY_DB_ALIAS)
66
+ context.registerService(makeMyAppGate())
67
+ ```
33
68
 
34
- const linking = context.service<IdentityLinkingService>(AUTH_IDENTITY_LINKING)
69
+ ### A product gate over profile scopes
35
70
 
36
- const auth = await linking.linkProfile(providerDetails, { username })
71
+ ```ts
72
+ import { createLazyService } from '@owlmeans/context'
73
+ import type { GateService } from '@owlmeans/entrypoint'
74
+ import { AuthForbidden } from '@owlmeans/auth'
75
+ import { entityKeyOf } from '@owlmeans/auth-common'
76
+ import { AUTH_IDENTITY_PROFILE } from '@owlmeans/server-auth-identity'
77
+ import type { IdentityProfileResource } from '@owlmeans/server-auth-identity'
78
+
79
+ export const makeMyAppGate = (alias: string = MY_APP_GATE): GateService => {
80
+ const service: GateService = createLazyService<GateService>(alias, {
81
+ assert: async (req, _, params) => {
82
+ await service.ready()
83
+ const ctx = service.assertCtx<Config, Context>()
84
+
85
+ const entityId = entityKeyOf(req)
86
+ if (req.auth == null || entityId == null) {
87
+ throw new AuthForbidden('auth')
88
+ }
89
+
90
+ const profile = await ctx.resource<IdentityProfileResource>(AUTH_IDENTITY_PROFILE)
91
+ .load({ entityId, profileId: req.auth.profileId })
92
+ if (profile == null || (profile.expiresAt != null && new Date(profile.expiresAt) < new Date())) {
93
+ throw new AuthForbidden('profile')
94
+ }
95
+
96
+ const scopes = req.auth.scopes ?? []
97
+ const required = params.map(param => param.replace(/\{entity\}/g, entityId))
98
+ if (!required.every(scope => scopes.includes('*') || scopes.includes(scope))) {
99
+ throw new AuthForbidden('permission')
100
+ }
101
+ },
102
+ })
103
+
104
+ return service
105
+ }
37
106
  ```
38
107
 
39
- Read identity profiles without mutating records:
108
+ A protocol uses it with `{ guards: DEFAULT_GUARD, gate: { alias: MY_APP_GATE, params: ['my-app-project-{entity}'] } }`.
109
+
110
+ ### Read an organization's members
40
111
 
41
- ```typescript
112
+ ```ts
113
+ import { handlers } from '@owlmeans/server-app'
114
+ import { requireEntityKey } from '@owlmeans/auth-common'
42
115
  import { AUTH_IDENTITY_PROFILE } from '@owlmeans/server-auth-identity'
43
116
  import type { IdentityProfileResource } from '@owlmeans/server-auth-identity'
44
117
 
45
- const profiles = context.resource<IdentityProfileResource>(AUTH_IDENTITY_PROFILE)
46
- const profile = await profiles.load(profileId, 'profileId')
118
+ const api = handlers<Context>()
119
+
120
+ export const members = api.request(memberProtocols.list, async (request, context) => {
121
+ const { items } = await context.resource<IdentityProfileResource>(AUTH_IDENTITY_PROFILE)
122
+ .list({ entityId: requireEntityKey(request) }, { sort: [{ field: 'createdAt', order: 'desc' }] })
47
123
 
48
- const { items } = await profiles.list({ entityId, profileId } as any)
124
+ return items.map(({ profileId, role, name }) => ({ profileId, role, name }))
125
+ })
49
126
  ```
50
127
 
51
- ## API
128
+ ### Link a provider login
52
129
 
53
- ### Resource Factories
130
+ ```ts
131
+ import { AUTH_IDENTITY_LINKING } from '@owlmeans/server-auth-identity'
132
+ import type { IdentityLinkingService } from '@owlmeans/server-auth-identity'
54
133
 
55
- - `makeIdentityAccountResource(dbAlias?)` - local account record, one per user.
56
- - `makeIdentityProfileResource(dbAlias?)` - local profile record with `entityId`, `role`, `scopes`, and optional expiry.
57
- - `makeIdentityCredentialsResource(dbAlias?)` - provider credentials link record.
58
- - `appendAuthIdentityResources(context, dbAlias?)` - registers all resources and `IdentityLinkingService`.
134
+ const linking = context.service<IdentityLinkingService>(AUTH_IDENTITY_LINKING)
59
135
 
60
- ### Aliases
136
+ const details = {
137
+ type: 'google-oauth', // the AuthenticationType
138
+ service: 'google', // the provider service alias
139
+ clientId: 'google', // the provider client this login came through
140
+ userId: providerSub, // the provider's subject claim
141
+ }
142
+
143
+ // Returning login: find the credential, then its profile.
144
+ let payload = await linking.getLinkedProfile(details)
145
+ // First login by this method: link it to the person's identity, registering one if new.
146
+ payload ??= await linking.linkProfile(details, { username: 'person@example.org' })
147
+ // payload: { type, role, userId, profileId, entitySlug, scopes }
148
+ ```
61
149
 
62
- - `AUTH_IDENTITY_ACCOUNT` - account resource alias.
63
- - `AUTH_IDENTITY_PROFILE` - profile resource alias.
64
- - `AUTH_IDENTITY_CREDENTIALS` - provider credentials resource alias.
65
- - `AUTH_IDENTITY_LINKING` - linking service alias.
150
+ ### Provision when an organization is created
66
151
 
67
- ### Types
152
+ ```ts
153
+ import { identityEvents } from '@owlmeans/server-auth-identity'
154
+
155
+ // in makeContext, after appendAuthIdentityResources
156
+ identityEvents(context)?.onEntityCreated(async (event, ctx) => {
157
+ await ctx.service<PlanService>(PLAN_SERVICE).grantStarterPlan(event.entityId)
158
+ })
159
+ ```
160
+
161
+ The event carries `entityId`, `entitySlug`, `iamKey`, `accountId`, `profileId`, `username`, the
162
+ login `type` and `service`, and `createdAt`. It fires only when `linkProfile` registers a new
163
+ identity (including `force: true`) — never when a second sign-in method links to an identity that
164
+ exists. Listeners run in order and are awaited; one that throws is logged and never fails the
165
+ sign-in, so anything a listener provisions needs its own backfill.
68
166
 
69
- - `IdentityAccount` - `Profile & ResourceRecord`; `credential` is the generated local entity slug.
70
- - `IdentityProfile` - `Profile & ResourceRecord`; includes `profileId`, `userId?`, `role`, `entityId`, `scopes`, `expiresAt?`.
71
- - `IdentityCredentials` - `AuthCredentials & ResourceRecord`; includes `profileId` and derived provider keys.
72
- - `IdentityLinkingService` - `getLinkedProfile`, `linkProfile`, `linkCredentials`, `getOwnerProfiles`, `getOwnerCredentials`.
167
+ ### Rename an organization and mint a durable name
73
168
 
74
- ## Key Derivation
169
+ ```ts
170
+ import { ENTITY_RESOLVER, requireEntityKey } from '@owlmeans/auth-common'
171
+ import type { EntityResolverService } from '@owlmeans/auth-common'
75
172
 
76
- - Account `credential`: generated Base58 local entity slug.
77
- - Profile `profileId`: `"{type}:{accountId}"`.
78
- - Credentials `userId`: `"{type}:{service}:{providerSub}"`.
79
- - Credentials `credential`: `"service:{type}:{service}"`.
173
+ export const rename = api.body(organizationProtocols.rename, async ({ slug }, context, request) => {
174
+ const resolver = context.service<EntityResolverService>(ENTITY_RESOLVER)
175
+ const entity = await resolver.rename(requireEntityKey(request), slug) // old slug moves to formerSlugs
80
176
 
81
- ## Product-Viable Integration Notes
177
+ return { slug: entity.slug }
178
+ })
82
179
 
83
- - Google/OIDC login is only the provider bootstrap path; durable authorization data lives in `IdentityProfile`.
84
- - Backend contexts should register `appendAuthService(context)`, then `appendAuthIdentityResources(context)`, then product gate services.
85
- - Product gates should read `AUTH_IDENTITY_PROFILE`, verify `entityId`, reject expired or blocked profiles, and compare gate params against profile scopes.
86
- - `Resource.pick()` is destructive and deletes the matching record. Never use `pick()` for gate or handler reads; use `load()` or `list()`.
180
+ // read once, or mint and persist on first ask; later renames leave it alone
181
+ const namespace = await resolver.mintName(entityId, 'namespace', entity => `my-app-${entity.iamKey}`)
182
+ ```
87
183
 
88
- ## Related Packages
184
+ ## API
185
+
186
+ ### Functions
187
+
188
+ | Symbol | Kind | Purpose |
189
+ |---|---|---|
190
+ | `appendAuthIdentityResources(context, dbAlias?)` | function | Register the four resources, the linking service, the entity resolver and (unless one is registered) the identity-events service |
191
+ | `makeOrgEntityResource(dbAlias?)` | function | Mongo resource for `OrgEntity` |
192
+ | `makeIdentityAccountResource(dbAlias?)` | function | Mongo resource for `IdentityAccount` |
193
+ | `makeIdentityProfileResource(dbAlias?)` | function | Mongo resource for `IdentityProfile` |
194
+ | `makeIdentityCredentialsResource(dbAlias?)` | function | Mongo resource for `IdentityCredentials` |
195
+ | `makeIdentityLinkingService()` | function | The `IdentityLinkingService` implementation |
196
+ | `makeEntityResolverService(alias = ENTITY_RESOLVER)` | function | The `EntityResolverService` implementation, cached 30 s per resolved name |
197
+ | `makeIdentityEventsService(alias = AUTH_IDENTITY_EVENTS)` | function | The `IdentityEventsService` implementation (lazy) |
198
+ | `identityEvents(ctx, alias?)` | function | The registered events service, or `null` |
199
+
200
+ ### Constants
201
+
202
+ | Symbol | Kind | Purpose |
203
+ |---|---|---|
204
+ | `AUTH_IDENTITY_ORG_ENTITY`, `AUTH_IDENTITY_ACCOUNT`, `AUTH_IDENTITY_PROFILE`, `AUTH_IDENTITY_CREDENTIALS` | const | Resource aliases (`'auth-identity:…'`), lookup keys only |
205
+ | `AUTH_IDENTITY_LINKING` | const | `'auth-identity:linking'` — linking service alias |
206
+ | `AUTH_IDENTITY_EVENTS` | const | `'auth-identity:events'` — identity-events service alias |
207
+ | `AUTH_IDENTITY_DB_ALIAS` | const | `'auth-identity'` — suggested db config alias |
208
+ | `AUTH_IDENTITY_ORG_ENTITY_COLLECTION`, `AUTH_IDENTITY_ACCOUNT_COLLECTION`, `AUTH_IDENTITY_PROFILE_COLLECTION`, `AUTH_IDENTITY_CREDENTIALS_COLLECTION` | const | Colon-free Mongo collection base names |
209
+ | `MAX_ENTITY_SLUG_ATTEMPTS` | const | `8` — word slugs tried before minting gives up |
210
+ | `LOGIN_SERVICE_PREFIX`, `EXTERNAL_KEY_DELIMITER` | const | `'service'` and `':'` — the derived-key grammar |
211
+
212
+ ### Types
89
213
 
90
- - [`@owlmeans/auth`](../auth) - core `AuthPayload`, `AuthRole`, `Profile`, and auth errors.
91
- - [`@owlmeans/server-auth`](../server-auth) - bearer verification and default auth guard.
92
- - [`@owlmeans/server-oidc-rp`](../server-oidc-rp) - provider exchange and account-linking interface compatibility.
93
- - [`@owlmeans/oidc`](../oidc) - `ProviderProfileDetails` and provider config types.
94
- - [`@owlmeans/mongo-resource`](../mongo-resource) - Mongo-backed resource implementation.
214
+ | Symbol | Kind | Purpose |
215
+ |---|---|---|
216
+ | `OrgEntity` | type | `id`, `slug`, `formerSlugs?`, `iamKey`, `names?`, `createdAt`, `updatedAt?` |
217
+ | `IdentityAccount` | type | `Profile` without `entitySlug`, plus `id`, `credential`, `entityId?` |
218
+ | `IdentityProfile` | type | `Profile` without `entitySlug`, plus `id`, `profileId`, `userId?`, `role`, `entityId?`, `expiresAt?` |
219
+ | `IdentityCredentials` | type | `AuthCredentials` + `profileId` |
220
+ | `IdentityLinkingService` | type | `getLinkedProfile`, `linkProfile`, `linkCredentials`, `unlinkCredentials`, `getOwnerProfiles`, `getOwnerCredentials` |
221
+ | `AccountMeta` | type | `{ username, force? }` |
222
+ | `IdentityEventsService` | type | `onEntityCreated(callback)`, `propagateEntityCreated(event)` |
223
+ | `EntityCreatedEvent`, `EntityCreatedCallback` | type | The entity-created payload; `(event, ctx) => Promise<void>` |
224
+ | `OrgEntityResource`, `IdentityAccountResource`, `IdentityProfileResource`, `IdentityCredentialsResource` | type | Typed `MongoResource` aliases |
225
+ | `IdentityConfig`, `IdentityContext` | type | Server config and context shapes |
226
+ | `GoogleUserInfo` | type | Google userinfo claims |
227
+ | `EmailIdentityArgs`, `EmailIdentityPayload`, `IdentityIamExtension` | type | Declaration-only seam; nothing in the package consumes them |
228
+
229
+ ## Key derivation
230
+
231
+ - Account `credential` — a unique 16-character Base58 slug.
232
+ - Account / profile `entityId` — the `OrgEntity` id; a first registration creates the entity first.
233
+ - Profile `userId` — the account's Mongo id, a declared ObjectId reference.
234
+ - Profile `profileId` — `"{type}:{accountId}"`.
235
+ - Credentials `userId` — `"{type}:{service}:{providerSub}"`; unrelated to the profile's `userId`.
236
+ - Credentials / profile `credential` — `"service:{type}:{service}"`; only platform logins carry it.
237
+
238
+ ## Common pitfalls
239
+
240
+ - `Resource.take()` deletes the record it returns. Use `load(where)` or `list(where)` in gates and
241
+ handlers, never `take()`.
242
+ - Query profiles and organization records by `entityId` from `requireEntityKey(request)` /
243
+ `entityKeyOf(request)`, never by the slug.
244
+ - Without the resolver `request.entity` stays undefined and consumers fall back to the slug — call
245
+ `appendAuthIdentityResources` (or register the resolver) in every service that serves organization
246
+ data.
247
+ - First-login profiles get `ALL_SCOPES` and `AuthRole.User`; narrow scopes where finer authorization
248
+ is needed.
249
+ - `linkProfile(details, { username, force: true })` always registers a new identity — use it only
250
+ when a separate identity is intended.
251
+ - Provisioning in an `onEntityCreated` listener is best-effort: a throwing listener is logged and the
252
+ sign-in succeeds, so reconcile what it provisions periodically.
253
+ - The resolver caches for 30 seconds, so a rename reaches other replicas within that window; the old
254
+ slug keeps resolving meanwhile.
255
+ - `rename` rejects a malformed slug and any slug an entity has ever answered to; `mintSlug` throws
256
+ `entity:slug-exhausted` after `MAX_ENTITY_SLUG_ATTEMPTS`.
257
+
258
+ ## Related packages
259
+
260
+ - [`@owlmeans/server-auth`](../server-auth) — bearer verification; canonicalizes the slug through the resolver
261
+ - [`@owlmeans/auth-common`](../auth-common) — `EntityResolverService`, `ENTITY_RESOLVER`, `requireEntityKey`, `attachEntity`
262
+ - [`@owlmeans/auth`](../auth) — `AuthPayload`, `AuthRole`, `Profile`, errors
263
+ - [`@owlmeans/server-oidc-rp`](../server-oidc-rp) — calls the linking service during an OAuth callback
264
+ - [`@owlmeans/server-auth-otp`](../server-auth-otp) — email OTP plugin resolving users through the linking service
265
+ - [`@owlmeans/oidc`](../oidc) — `ProviderProfileDetails`
266
+ - [`@owlmeans/mongo-resource`](../mongo-resource) — the Mongo resource implementation
95
267
 
96
268
  <!-- owlmeans:agent-guidance:start -->
97
269
  ## Agent guidance
@@ -101,7 +273,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
101
273
  your project's skill store (`.agents/skills/`):
102
274
 
103
275
  ```sh
104
- npx @owlmeans/agent-skills
276
+ npx @owlmeans/agent-skills@^0.1.18-rc.31
105
277
  ```
106
278
 
107
279
  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.0",
5
- "generatedAt": "2026-08-16T22:20:50.498Z",
4
+ "version": "0.1.18-rc.31",
5
+ "generatedAt": "2026-09-21T21:54:22.942Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {