@owlmeans/server-auth-identity 0.1.18-rc.5 → 0.1.18-rc.50
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 +265 -57
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-auth-identity/SKILL.md +194 -98
- package/build/consts.d.ts +15 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +20 -0
- package/build/consts.js.map +1 -1
- package/build/consts.local.d.ts +15 -0
- package/build/consts.local.d.ts.map +1 -0
- package/build/consts.local.js +15 -0
- package/build/consts.local.js.map +1 -0
- package/build/events.d.ts +9 -0
- package/build/events.d.ts.map +1 -0
- package/build/events.js +43 -0
- package/build/events.js.map +1 -0
- package/build/groups/types.d.ts +36 -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 +13 -0
- package/build/groups.d.ts.map +1 -0
- package/build/groups.js +57 -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 +19 -3
- package/build/helper.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 +16 -0
- package/build/identity.d.ts.map +1 -0
- package/build/identity.js +176 -0
- package/build/identity.js.map +1 -0
- package/build/index.d.ts +9 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5 -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/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 +3 -0
- package/build/resolver.d.ts.map +1 -0
- package/build/resolver.js +147 -0
- package/build/resolver.js.map +1 -0
- package/build/resource.d.ts +14 -1
- package/build/resource.d.ts.map +1 -1
- package/build/resource.js +39 -8
- 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 +107 -127
- package/build/service.js.map +1 -1
- package/build/types.d.ts +219 -15
- package/build/types.d.ts.map +1 -1
- package/build/types.local.d.ts +11 -0
- package/build/types.local.d.ts.map +1 -0
- package/build/types.local.js +2 -0
- package/build/types.local.js.map +1 -0
- 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 +12 -9
- package/src/consts.local.ts +15 -0
- package/src/consts.ts +24 -0
- package/src/events.ts +61 -0
- package/src/groups/types.ts +36 -0
- package/src/groups.ts +86 -0
- package/src/helper.ts +21 -3
- package/src/identity/types.ts +38 -0
- package/src/identity.ts +207 -0
- package/src/index.ts +9 -1
- package/src/keys/types.ts +22 -0
- package/src/keys.ts +24 -0
- package/src/resolver.ts +177 -0
- package/src/resource.ts +45 -10
- package/src/service.ts +122 -143
- package/src/types.local.ts +14 -0
- package/src/types.ts +232 -15
- package/src/utils/native/types.ts +17 -0
- package/src/utils/native.ts +17 -0
- package/tests/context.ts +215 -0
- package/tests/events.spec.ts +89 -0
- package/tests/identity.spec.ts +149 -0
- package/tests/linking.spec.ts +164 -0
- package/tests/resolver.spec.ts +149 -0
- package/tsconfig.json +1 -1
- /package/src/{iam.ts → iam/types.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,97 +1,305 @@
|
|
|
1
1
|
# @owlmeans/server-auth-identity
|
|
2
2
|
|
|
3
|
-
Mongo-backed local identity
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
-
|
|
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.50
|
|
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: 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 `identityKeyHelper.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** — `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
|
+
- **Linking service** — `IdentityLinkingService` signs a provider login into the deployment's own
|
|
34
|
+
app and returns an `AuthPayload`.
|
|
35
|
+
- **Entity resolver** — the `EntityResolverService` registered under `ENTITY_RESOLVER`. Registering
|
|
36
|
+
it tells the server boundary that this deployment has organizations.
|
|
37
|
+
- **Identity events** — `identityEvents(ctx)` returns the `IdentityEventsService`:
|
|
38
|
+
`onProfileCreated` runs once per (account, app), `onEntityCreated` once per registration.
|
|
39
|
+
|
|
18
40
|
## Usage
|
|
19
41
|
|
|
20
|
-
Register
|
|
42
|
+
### Register in a context
|
|
43
|
+
|
|
44
|
+
Register after the Mongo service and `appendAuthService`. A dedicated db config under
|
|
45
|
+
`AUTH_IDENTITY_DB_ALIAS` scopes a collection prefix to the identity collections only.
|
|
21
46
|
|
|
22
|
-
```
|
|
23
|
-
|
|
47
|
+
```ts
|
|
48
|
+
// config.ts
|
|
49
|
+
import { AUTH_IDENTITY_DB_ALIAS, AUTH_IDENTITY_LINKING } from '@owlmeans/server-auth-identity'
|
|
24
50
|
|
|
25
|
-
|
|
51
|
+
cfg.dbs.push({
|
|
52
|
+
alias: AUTH_IDENTITY_DB_ALIAS,
|
|
53
|
+
service: MONGO_SERVICE,
|
|
54
|
+
host: process.env.MONGO_HOST!,
|
|
55
|
+
schema: 'my-app',
|
|
56
|
+
secret: process.env.MONGO_SECRET!,
|
|
57
|
+
resourcePrefix: 'my-app-', // -> my-app-account, my-app-profile, ...
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// let the OIDC relying party link provider logins through this store
|
|
61
|
+
cfg.oidc.accountLinkingService = AUTH_IDENTITY_LINKING
|
|
26
62
|
```
|
|
27
63
|
|
|
28
|
-
|
|
64
|
+
```ts
|
|
65
|
+
// context.ts
|
|
66
|
+
import { appendAuthService } from '@owlmeans/server-auth'
|
|
67
|
+
import { appendAuthIdentityResources, AUTH_IDENTITY_DB_ALIAS } from '@owlmeans/server-auth-identity'
|
|
29
68
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
69
|
+
appendMongo<C, T>(context)
|
|
70
|
+
appendAuthService<C, T>(context)
|
|
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 })
|
|
73
|
+
context.registerService(makeMyAppGate())
|
|
74
|
+
```
|
|
33
75
|
|
|
34
|
-
|
|
76
|
+
### A product gate over profile scopes
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { createLazyService } from '@owlmeans/context'
|
|
80
|
+
import type { GateService } from '@owlmeans/entrypoint'
|
|
81
|
+
import { AuthForbidden } from '@owlmeans/auth'
|
|
82
|
+
import { makeEntityScope } from '@owlmeans/auth-common'
|
|
83
|
+
import { AUTH_IDENTITY_PROFILE } from '@owlmeans/server-auth-identity'
|
|
84
|
+
import type { IdentityProfileResource } from '@owlmeans/server-auth-identity'
|
|
35
85
|
|
|
36
|
-
const
|
|
86
|
+
export const makeMyAppGate = (alias: string = MY_APP_GATE): GateService => {
|
|
87
|
+
const service: GateService = createLazyService<GateService>(alias, {
|
|
88
|
+
assert: async (req, _, params) => {
|
|
89
|
+
await service.ready()
|
|
90
|
+
const ctx = service.assertCtx<Config, Context>()
|
|
91
|
+
|
|
92
|
+
const entityId = makeEntityScope(req).entityKeyOf()
|
|
93
|
+
if (req.auth == null || entityId == null) {
|
|
94
|
+
throw new AuthForbidden('auth')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const profile = await ctx.resource<IdentityProfileResource>(AUTH_IDENTITY_PROFILE)
|
|
98
|
+
.load({ entityId, profileId: req.auth.profileId })
|
|
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())) {
|
|
102
|
+
throw new AuthForbidden('profile')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const scopes = req.auth.scopes ?? []
|
|
106
|
+
const required = params.map(param => param.replace(/\{entity\}/g, entityId))
|
|
107
|
+
if (!required.every(scope => scopes.includes('*') || scopes.includes(scope))) {
|
|
108
|
+
throw new AuthForbidden('permission')
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
return service
|
|
114
|
+
}
|
|
37
115
|
```
|
|
38
116
|
|
|
39
|
-
|
|
117
|
+
A protocol uses it with `{ guards: DEFAULT_GUARD, gate: { alias: MY_APP_GATE, params: ['my-app-project-{entity}'] } }`.
|
|
40
118
|
|
|
41
|
-
|
|
119
|
+
### Read an organization's members
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { handlers } from '@owlmeans/server-app'
|
|
123
|
+
import { makeEntityScope } from '@owlmeans/auth-common'
|
|
42
124
|
import { AUTH_IDENTITY_PROFILE } from '@owlmeans/server-auth-identity'
|
|
43
125
|
import type { IdentityProfileResource } from '@owlmeans/server-auth-identity'
|
|
44
126
|
|
|
45
|
-
const
|
|
46
|
-
|
|
127
|
+
const api = handlers<Context>()
|
|
128
|
+
|
|
129
|
+
export const members = api.request(memberProtocols.list, async (request, context) => {
|
|
130
|
+
const { items } = await context.resource<IdentityProfileResource>(AUTH_IDENTITY_PROFILE)
|
|
131
|
+
.list({ entityId: makeEntityScope(request).requireEntityKey(), service: MY_APP }, { size: 0, sort: [{ field: 'createdAt', order: 'desc' }] })
|
|
47
132
|
|
|
48
|
-
|
|
133
|
+
return items.map(({ profileId, role, name }) => ({ profileId, role, name }))
|
|
134
|
+
})
|
|
49
135
|
```
|
|
50
136
|
|
|
51
|
-
|
|
137
|
+
### Link a provider login
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { AUTH_IDENTITY_LINKING } from '@owlmeans/server-auth-identity'
|
|
141
|
+
import type { IdentityLinkingService } from '@owlmeans/server-auth-identity'
|
|
142
|
+
|
|
143
|
+
const linking = context.service<IdentityLinkingService>(AUTH_IDENTITY_LINKING)
|
|
52
144
|
|
|
53
|
-
|
|
145
|
+
const details = {
|
|
146
|
+
type: 'google-oauth', // the AuthenticationType
|
|
147
|
+
service: 'google', // the provider service alias
|
|
148
|
+
clientId: 'google', // the provider client this login came through
|
|
149
|
+
userId: providerSub, // the provider's subject claim
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Returning login: the credential, its account, this app's row in the account's organization.
|
|
153
|
+
let payload = await linking.getLinkedProfile(details)
|
|
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.
|
|
156
|
+
payload ??= await linking.linkProfile(details, { username: 'person@example.org' })
|
|
157
|
+
// payload: { type, role, userId: accountId, profileId, entitySlug, scopes }
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Rows for a hosted app
|
|
54
161
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
- `makeIdentityCredentialsResource(dbAlias?)` - provider credentials link record.
|
|
58
|
-
- `appendAuthIdentityResources(context, dbAlias?)` - registers all resources and `IdentityLinkingService`.
|
|
162
|
+
```ts
|
|
163
|
+
import { identityOf } from '@owlmeans/server-auth-identity'
|
|
59
164
|
|
|
60
|
-
|
|
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 })
|
|
168
|
+
// a hosted app's rows carry `scopes: []`; a row in another organization brings the primary row with it
|
|
169
|
+
```
|
|
61
170
|
|
|
62
|
-
|
|
63
|
-
- `AUTH_IDENTITY_PROFILE` - profile resource alias.
|
|
64
|
-
- `AUTH_IDENTITY_CREDENTIALS` - provider credentials resource alias.
|
|
65
|
-
- `AUTH_IDENTITY_LINKING` - linking service alias.
|
|
171
|
+
### Provision when a person becomes a user of the app
|
|
66
172
|
|
|
67
|
-
|
|
173
|
+
```ts
|
|
174
|
+
import { identityEvents } from '@owlmeans/server-auth-identity'
|
|
68
175
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
176
|
+
// in makeContext, after appendAuthIdentityResources
|
|
177
|
+
identityEvents(context)?.onProfileCreated(async (event, ctx) => {
|
|
178
|
+
if (event.service !== MY_APP) return
|
|
179
|
+
await ctx.service<PlanService>(PLAN_SERVICE).grantStarterPlan(event.entityId)
|
|
180
|
+
})
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`onProfileCreated` carries `entityId`, `entitySlug`, `accountId`, `profileId`, `service` and
|
|
184
|
+
`owner`, and fires once per (account, app), when its primary row is created. `onEntityCreated`
|
|
185
|
+
carries `entityId`, `entitySlug`, `iamKey`, `accountId`, `profileId`, `username`, the login `type`
|
|
186
|
+
and `service`, the owner row's app as `profileService`, and `createdAt`; `linkProfile` fires it when
|
|
187
|
+
the sign-in registered the person. Listeners run in order and are awaited; one that throws is logged
|
|
188
|
+
and never fails the sign-in, so anything a listener provisions needs its own backfill.
|
|
189
|
+
|
|
190
|
+
### Rename an organization and mint a durable name
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import { ENTITY_RESOLVER, makeEntityScope } from '@owlmeans/auth-common'
|
|
194
|
+
import type { EntityResolverService } from '@owlmeans/auth-common'
|
|
73
195
|
|
|
74
|
-
|
|
196
|
+
export const rename = api.body(organizationProtocols.rename, async ({ slug }, context, request) => {
|
|
197
|
+
const resolver = context.service<EntityResolverService>(ENTITY_RESOLVER)
|
|
198
|
+
const entity = await resolver.rename(makeEntityScope(request).requireEntityKey(), slug) // old slug moves to formerSlugs
|
|
75
199
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
- Credentials `userId`: `"{type}:{service}:{providerSub}"`.
|
|
79
|
-
- Credentials `credential`: `"service:{type}:{service}"`.
|
|
200
|
+
return { slug: entity.slug }
|
|
201
|
+
})
|
|
80
202
|
|
|
81
|
-
|
|
203
|
+
// read once, or mint and persist on first ask; later renames leave it alone
|
|
204
|
+
const namespace = await resolver.mintName(entityId, 'namespace', entity => `my-app-${entity.iamKey}`)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## API
|
|
82
208
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
209
|
+
### Functions
|
|
210
|
+
|
|
211
|
+
| Symbol | Kind | Purpose |
|
|
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 |
|
|
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 |
|
|
219
|
+
| `makeOrgEntityResource(dbAlias?)` | function | Mongo resource for `OrgEntity` |
|
|
220
|
+
| `makeIdentityAccountResource(dbAlias?)` | function | Mongo resource for `IdentityAccount` |
|
|
221
|
+
| `makeIdentityProfileResource(dbAlias?)` | function | Mongo resource for `IdentityProfile` |
|
|
222
|
+
| `makeIdentityCredentialsResource(dbAlias?)` | function | Mongo resource for `IdentityCredentials` |
|
|
223
|
+
| `makeIdentityLinkingService({ service? })` | function | The `IdentityLinkingService` implementation for one app |
|
|
224
|
+
| `makeEntityResolverService(alias = ENTITY_RESOLVER)` | function | The `EntityResolverService` implementation, cached 30 s per resolved name |
|
|
225
|
+
| `makeIdentityEventsService(alias = AUTH_IDENTITY_EVENTS)` | function | The `IdentityEventsService` implementation (lazy) |
|
|
226
|
+
| `identityEvents(ctx, alias?)` | function | The registered events service, or `null` |
|
|
227
|
+
|
|
228
|
+
### Constants
|
|
229
|
+
|
|
230
|
+
| Symbol | Kind | Purpose |
|
|
231
|
+
|---|---|---|
|
|
232
|
+
| `AUTH_IDENTITY_ORG_ENTITY`, `AUTH_IDENTITY_ACCOUNT`, `AUTH_IDENTITY_PROFILE`, `AUTH_IDENTITY_CREDENTIALS` | const | Resource aliases (`'auth-identity:…'`), lookup keys only |
|
|
233
|
+
| `AUTH_IDENTITY_LINKING` | const | `'auth-identity:linking'` — linking service alias |
|
|
234
|
+
| `AUTH_IDENTITY_EVENTS` | const | `'auth-identity:events'` — identity-events service alias |
|
|
235
|
+
| `AUTH_IDENTITY_DB_ALIAS` | const | `'auth-identity'` — suggested db config alias |
|
|
236
|
+
| `AUTH_IDENTITY_ORG_ENTITY_COLLECTION`, `AUTH_IDENTITY_ACCOUNT_COLLECTION`, `AUTH_IDENTITY_PROFILE_COLLECTION`, `AUTH_IDENTITY_CREDENTIALS_COLLECTION` | const | Colon-free Mongo collection base names |
|
|
237
|
+
| `DEFAULT_APP_SERVICE` | const | `'app'` — the own app key when none is passed |
|
|
238
|
+
| `MAX_ENTITY_SLUG_ATTEMPTS` | const | `8` — word slugs tried before minting gives up |
|
|
239
|
+
| `MAX_ACCOUNT_KEY_ATTEMPTS`, `MAX_GUARDED_UPDATE_ATTEMPTS`, `PROFILE_DIGEST_LENGTH` | const | Retry bounds; the `profileId` digest length |
|
|
240
|
+
| `LOGIN_SERVICE_PREFIX`, `EXTERNAL_KEY_DELIMITER` | const | `'service'` and `':'` — the derived-key grammar |
|
|
87
241
|
|
|
88
|
-
|
|
242
|
+
### Types
|
|
89
243
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
244
|
+
| Symbol | Kind | Purpose |
|
|
245
|
+
|---|---|---|
|
|
246
|
+
| `OrgEntity` | type | `id`, `slug`, `formerSlugs?`, `iamKey`, `title?`, `names?`, `groups?`, `createdAt`, `updatedAt?` |
|
|
247
|
+
| `OrgGroup`, `OrgGroupBundle`, `OrgGroupFilter` | type | An app's group in an organization and its permission bundles |
|
|
248
|
+
| `IdentityAccount` | type | `id`, `credential`, `email`, `name`, `entityId`, `scopes` |
|
|
249
|
+
| `IdentityProfile` | type | `id`, `profileId`, `userId`, `service`, `entityId`, `owner?`, `role`, `scopes`, `permissions`, `groups?`, `home?`, `disabled?`, `managed?` |
|
|
250
|
+
| `IdentityCredentials` | type | `AuthCredentials` + `accountId` (`profileId?` reserved) |
|
|
251
|
+
| `EnsureAccountArgs`, `EnsuredAccount`, `EnsureProfileArgs`, `IdentityResourcesOptions` | type | The primitives' arguments and results |
|
|
252
|
+
| `IdentityLinkingService` | type | `getLinkedProfile`, `linkProfile`, `linkCredentials`, `unlinkCredentials`, `getOwnerProfiles`, `getOwnerCredentials` |
|
|
253
|
+
| `AccountMeta` | type | `{ username }` — the verified address |
|
|
254
|
+
| `IdentityEventsService` | type | `onEntityCreated` / `propagateEntityCreated`, `onProfileCreated` / `propagateProfileCreated` |
|
|
255
|
+
| `EntityCreatedEvent`, `ProfileCreatedEvent` and their callbacks | type | The event payloads; `(event, ctx) => Promise<void>` |
|
|
256
|
+
| `OrgEntityResource`, `IdentityAccountResource`, `IdentityProfileResource`, `IdentityCredentialsResource` | type | Typed `MongoResource` aliases |
|
|
257
|
+
| `IdentityConfig`, `IdentityContext` | type | Server config and context shapes |
|
|
258
|
+
| `GoogleUserInfo` | type | Google userinfo claims |
|
|
259
|
+
| `EmailIdentityArgs`, `EmailIdentityPayload`, `IdentityIamExtension` | type | Declaration-only seam; nothing in the package consumes them |
|
|
260
|
+
|
|
261
|
+
## Key derivation
|
|
262
|
+
|
|
263
|
+
- Account `credential` — a unique 16-character Base58 key.
|
|
264
|
+
- Account `email` — trimmed, lower-case, unique.
|
|
265
|
+
- Account / profile `entityId`, profile `userId` and `home`, credentials `accountId` — record ids,
|
|
266
|
+
declared ObjectId references; a first registration creates the organization first.
|
|
267
|
+
- Profile `profileId` — `identityKeyHelper.profileIdOf(service, accountId)`: `"{service}:"` + 22 Base58 characters of
|
|
268
|
+
`sha256("{accountId}:{service}")`. Computed, the same on every row of one (account, app).
|
|
269
|
+
- Credentials `userId` — `"{type}:{service}:{providerSub}"`; unrelated to the profile's `userId`.
|
|
270
|
+
- Credentials `credential` — `"service:{type}:{service}"`.
|
|
271
|
+
|
|
272
|
+
## Common pitfalls
|
|
273
|
+
|
|
274
|
+
- `Resource.take()` deletes the record it returns. Use `load(where)` or `list(where)` in gates and
|
|
275
|
+
handlers, never `take()`.
|
|
276
|
+
- Query profiles and organization records by `entityId` from
|
|
277
|
+
`makeEntityScope(request).requireEntityKey()` / `.entityKeyOf()`, never by the slug.
|
|
278
|
+
- Without the resolver `request.entity` stays undefined and consumers fall back to the slug — call
|
|
279
|
+
`appendAuthIdentityResources` (or register the resolver) in every service that serves organization
|
|
280
|
+
data.
|
|
281
|
+
- `linkProfile` rows get `ALL_SCOPES` and `AuthRole.User`; a hosted app's rows (`ensureProfile`
|
|
282
|
+
default) get `[]`. A gate of the own app must also require `profile.service`.
|
|
283
|
+
- The address is the identity: whoever names an address to `ensureAccount` / `linkProfile` gets that
|
|
284
|
+
person's account, so only a verified address may reach them.
|
|
285
|
+
- Never `update()` an organization record — write fields (the resolver, the group helpers, a guarded
|
|
286
|
+
`collection.updateOne`), or a stale read erases groups and minted names.
|
|
287
|
+
- Provisioning in an `onEntityCreated` listener is best-effort: a throwing listener is logged and the
|
|
288
|
+
sign-in succeeds, so reconcile what it provisions periodically.
|
|
289
|
+
- The resolver caches for 30 seconds, so a rename reaches other replicas within that window; the old
|
|
290
|
+
slug keeps resolving meanwhile.
|
|
291
|
+
- `rename` rejects a malformed slug and any slug an entity has ever answered to; `mintSlug` throws
|
|
292
|
+
`entity:slug-exhausted` after `MAX_ENTITY_SLUG_ATTEMPTS`.
|
|
293
|
+
|
|
294
|
+
## Related packages
|
|
295
|
+
|
|
296
|
+
- [`@owlmeans/server-auth`](../server-auth) — bearer verification; canonicalizes the slug through the resolver
|
|
297
|
+
- [`@owlmeans/auth-common`](../auth-common) — `EntityResolverService`, `ENTITY_RESOLVER`, `makeEntityScope` (`requireEntityKey`, `attachEntity`)
|
|
298
|
+
- [`@owlmeans/auth`](../auth) — `AuthPayload`, `AuthRole`, `Profile`, errors
|
|
299
|
+
- [`@owlmeans/server-oidc-rp`](../server-oidc-rp) — calls the linking service during an OAuth callback
|
|
300
|
+
- [`@owlmeans/server-auth-otp`](../server-auth-otp) — email OTP plugin resolving users through the linking service
|
|
301
|
+
- [`@owlmeans/oidc`](../oidc) — `ProviderProfileDetails`
|
|
302
|
+
- [`@owlmeans/mongo-resource`](../mongo-resource) — the Mongo resource implementation
|
|
95
303
|
|
|
96
304
|
<!-- owlmeans:agent-guidance:start -->
|
|
97
305
|
## Agent guidance
|
|
@@ -101,7 +309,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
101
309
|
your project's skill store (`.agents/skills/`):
|
|
102
310
|
|
|
103
311
|
```sh
|
|
104
|
-
npx @owlmeans/agent-skills
|
|
312
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.54
|
|
105
313
|
```
|
|
106
314
|
|
|
107
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-
|
|
4
|
+
"version": "0.1.18-rc.50",
|
|
5
|
+
"generatedAt": "2026-10-15T10:20:01.287Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|