@owlmeans/server-auth-identity 0.1.18-rc.21 → 0.1.18-rc.23

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