@happyvertical/smrt-users 0.39.16 → 0.40.0
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/AGENTS.md +14 -0
- package/README.md +54 -3
- package/dist/chunks/{TerminalAuthService-BnNKbnGB.js → TerminalAuthService-BpA7-JpM.js} +70 -22
- package/dist/chunks/TerminalAuthService-BpA7-JpM.js.map +1 -0
- package/dist/collections/UserCollection.d.ts +33 -7
- package/dist/collections/UserCollection.d.ts.map +1 -1
- package/dist/collections/index.d.ts +1 -1
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/manifest.json +34 -34
- package/dist/services/OidcLoginService.d.ts +7 -1
- package/dist/services/OidcLoginService.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +6 -6
- package/dist/sveltekit/index.d.ts +8 -2
- package/dist/sveltekit/index.d.ts.map +1 -1
- package/dist/sveltekit.js +1 -1
- package/dist/sveltekit.js.map +1 -1
- package/package.json +9 -9
- package/dist/chunks/TerminalAuthService-BnNKbnGB.js.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -268,6 +268,19 @@ the package root).
|
|
|
268
268
|
stable-link owner and canonical-Person checks still apply. The hook receives a
|
|
269
269
|
separate frozen claims snapshot; internal retry and persistence state is not
|
|
270
270
|
exposed for mutation.
|
|
271
|
+
- **Owned first binding requires `authorizeProfileOwner`.** An invitation or
|
|
272
|
+
approval workflow may explicitly return both its pre-provisioned canonical
|
|
273
|
+
global `Person` and existing approved `User` from this transaction-bound
|
|
274
|
+
hook. `undefined` keeps the secure `profile_owned` default and `null`
|
|
275
|
+
rejects. SMRT treats only the selected IDs as input, reloads them in the
|
|
276
|
+
provisioning transaction, and requires `email_verified === true`, the unique
|
|
277
|
+
canonical global Person for the normalized claim email, exactly one owner,
|
|
278
|
+
that owner as the selected User, and the same normalized User email. The
|
|
279
|
+
hook runs before identity/User/session creation and may be retried, so use
|
|
280
|
+
only its supplied `db` and `users` handles and keep application authorization
|
|
281
|
+
idempotent. Never authorize from email matching alone. Existing exact
|
|
282
|
+
identities cannot be rebound; when `resolveProfile` is also present both
|
|
283
|
+
hooks must select the same Profile.
|
|
271
284
|
- **OIDC first login is atomic.** The Profile, `OidcIdentity`, and User are one
|
|
272
285
|
transaction. The database arbiters are `OidcIdentity.identityKey`,
|
|
273
286
|
private `oidc_profile_email_reservations.email_key`, `User.emailKey`, and the
|
|
@@ -275,6 +288,7 @@ the package root).
|
|
|
275
288
|
email locks in deterministic order so changed email claims also serialize.
|
|
276
289
|
SQLite and DuckDB callbacks additionally serialize transactions per database
|
|
277
290
|
URL because one adapter cannot safely overlap unrelated root transactions;
|
|
291
|
+
owner-authorized DuckDB callbacks use that same root-handle serialization;
|
|
278
292
|
PostgreSQL deadlock and serialization errors use a bounded transaction retry.
|
|
279
293
|
Newly provisioned Profiles use non-semantic per-profile slugs so equal IdP
|
|
280
294
|
display names cannot trigger a natural-key upsert;
|
package/README.md
CHANGED
|
@@ -381,8 +381,10 @@ rebinding, ownership/collision failures, readiness, retries, adapter support,
|
|
|
381
381
|
public errors, and permitted Profile/OIDC identity/User/session creation. For a
|
|
382
382
|
new identity, the Users path is deliberately fail-closed before User or session
|
|
383
383
|
creation unless the selected Profile is the one safe, unowned global `Person`
|
|
384
|
-
allowed by that matrix. An
|
|
385
|
-
|
|
384
|
+
allowed by that matrix. An owned Profile still returns `profile_owned` unless
|
|
385
|
+
the application explicitly supplies the owner authorization described below.
|
|
386
|
+
An exact issuer/subject link may instead continue to its already-owned canonical
|
|
387
|
+
global `Person`, but it cannot be rebound.
|
|
386
388
|
|
|
387
389
|
Canonical Profile failures use `CanonicalPersonProfileError` from
|
|
388
390
|
`@happyvertical/smrt-profiles`, with codes `ambiguous_email`, `email_mismatch`,
|
|
@@ -433,6 +435,52 @@ still apply. The resolver receives a separate frozen claims snapshot; retry
|
|
|
433
435
|
locks, identity lookups, and persistence retain SMRT's immutable internal
|
|
434
436
|
snapshot.
|
|
435
437
|
|
|
438
|
+
An invitation or approval workflow that pre-provisions both the canonical
|
|
439
|
+
global `Person` and its approved owning `User` can authorize the first identity
|
|
440
|
+
binding with `authorizeProfileOwner`:
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
// src/routes/auth/[provider]/callback/+server.ts
|
|
444
|
+
import { ProfileCollection } from '@happyvertical/smrt-profiles';
|
|
445
|
+
import { createOidcCallbackHandler } from '@happyvertical/smrt-users/sveltekit';
|
|
446
|
+
|
|
447
|
+
export const GET = createOidcCallbackHandler({
|
|
448
|
+
db: { type: 'postgres', url: process.env.DATABASE_URL! },
|
|
449
|
+
authorizeProfileOwner: async ({ claims, db, users }) => {
|
|
450
|
+
// This application record is the authorization decision. Select by its
|
|
451
|
+
// approved IDs; do not authorize an account from matching email alone.
|
|
452
|
+
const approval = await findApprovedOidcUser({
|
|
453
|
+
db,
|
|
454
|
+
email: claims.email,
|
|
455
|
+
});
|
|
456
|
+
if (!approval) return undefined; // preserve SMRT's secure default
|
|
457
|
+
|
|
458
|
+
const profiles = await ProfileCollection.create({ db });
|
|
459
|
+
const [profile, user] = await Promise.all([
|
|
460
|
+
profiles.get({ id: approval.profileId }),
|
|
461
|
+
users.get({ id: approval.userId }),
|
|
462
|
+
]);
|
|
463
|
+
if (!profile || !user) return null; // explicitly reject stale approval
|
|
464
|
+
return { profile, user };
|
|
465
|
+
},
|
|
466
|
+
successRedirect: '/dashboard',
|
|
467
|
+
});
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
The authorizer runs after protocol validation and inside the provisioning
|
|
471
|
+
transaction. It receives frozen normalized claims, the transaction-bound `db`,
|
|
472
|
+
and a `UserCollection` bound to that same transaction. Return both selected
|
|
473
|
+
objects only after application authorization; `undefined` uses the fail-closed
|
|
474
|
+
default and `null` rejects. SMRT reloads and verifies the selected IDs rather
|
|
475
|
+
than trusting the returned objects: `email_verified` must be exactly `true`,
|
|
476
|
+
the Profile must be the unique canonical global `Person` for the claim email,
|
|
477
|
+
exactly one User must own it, and that User must have the same normalized email.
|
|
478
|
+
An exact issuer/subject cannot be rebound. Identity creation and login remain
|
|
479
|
+
atomic, and a race retry may invoke the authorizer again, so its reads and
|
|
480
|
+
writes must use only the supplied handles and be idempotent. Supplying both
|
|
481
|
+
`resolveProfile` and `authorizeProfileOwner` is allowed only when they select
|
|
482
|
+
the same Profile.
|
|
483
|
+
|
|
436
484
|
When userinfo supplies a missing email, its `email_verified` value travels with
|
|
437
485
|
that email as one source-bound pair. SMRT never borrows a verification flag
|
|
438
486
|
from the ID token for a userinfo address, or from userinfo for an ID-token
|
|
@@ -459,7 +507,9 @@ locks in deterministic order, including when the same subject presents changed
|
|
|
459
507
|
email claims on independent database handles. SQLite and DuckDB also acquire a
|
|
460
508
|
database-URL transaction lock because one adapter cannot safely overlap
|
|
461
509
|
unrelated root transactions; PostgreSQL deadlock and serialization failures use
|
|
462
|
-
a bounded transaction retry.
|
|
510
|
+
a bounded transaction retry. Owner-authorized binding uses the same contract:
|
|
511
|
+
pass the DuckDB root handle and let SMRT serialize the callback transaction.
|
|
512
|
+
New OIDC Profiles use non-semantic unique slugs,
|
|
463
513
|
so equal IdP display names cannot overwrite one another through SMRT's
|
|
464
514
|
natural-key upsert.
|
|
465
515
|
Existing installations must run `smrt db:status`, `smrt db:migrate`, then
|
|
@@ -613,6 +663,7 @@ TenantService supports three modes: `flexible` (no auto-create), `personal` (aut
|
|
|
613
663
|
| `OidcLoginService` | Generic OIDC authorization-code login with PKCE for Kanidm, Dex, and other standards-compliant providers. |
|
|
614
664
|
| `backfillUserEmailKeys` | Idempotently populate durable normalized-email keys after migrating legacy Users; fails closed on duplicates. |
|
|
615
665
|
| `OidcProfileResolver` | Transaction-bound pre-provision hook for application identity reconciliation. |
|
|
666
|
+
| `OidcProfileOwnerAuthorizer` | Transaction-bound application authorization for binding a first identity to an existing canonical Profile and its sole approved User owner. |
|
|
616
667
|
| `NormalizedOidcClaims` | Frozen resolver claims with required normalized `email`. |
|
|
617
668
|
| `withSessionPermissionContext()` | Loads a session, optionally enters tenancy context, and exposes a request-scoped database/permission context. |
|
|
618
669
|
| `getCurrentSessionPermissionContext()`, `getRequestScopedDatabase()` | Read the active request/session context inside app code. |
|