@happyvertical/smrt-users 0.55.18 → 0.55.19
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 +47 -0
- package/agents/oauth-authorization.md +124 -0
- package/agents/retention.md +18 -0
- package/dist/chunks/{TerminalAuthService-DjGsRtqb.js → TerminalAuthService-DWPeKl3k.js} +2 -2
- package/dist/chunks/TerminalAuthService-DWPeKl3k.js.map +1 -0
- package/dist/collections/OAuthAuthorizationCollection.d.ts +31 -0
- package/dist/collections/OAuthAuthorizationCollection.d.ts.map +1 -0
- package/dist/collections/index.d.ts +1 -0
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +832 -33
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +4590 -2529
- package/dist/models/OAuthAuthorization.d.ts +70 -0
- package/dist/models/OAuthAuthorization.d.ts.map +1 -0
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/retention.d.ts +3 -1
- package/dist/retention.d.ts.map +1 -1
- package/dist/services/OAuthAuthorizationService.d.ts +52 -0
- package/dist/services/OAuthAuthorizationService.d.ts.map +1 -0
- package/dist/services/OAuthAuthorizationStorage.d.ts +39 -0
- package/dist/services/OAuthAuthorizationStorage.d.ts.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/oauth-retention.d.ts +14 -0
- package/dist/services/oauth-retention.d.ts.map +1 -0
- package/dist/services/oauth-transaction.d.ts +4 -0
- package/dist/services/oauth-transaction.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +925 -48
- package/dist/sveltekit/index.d.ts +1 -0
- package/dist/sveltekit/index.d.ts.map +1 -1
- package/dist/sveltekit/oauth-handlers.d.ts +25 -0
- package/dist/sveltekit/oauth-handlers.d.ts.map +1 -0
- package/dist/sveltekit.js +93 -25
- package/dist/sveltekit.js.map +1 -1
- package/package.json +13 -12
- package/dist/chunks/TerminalAuthService-DjGsRtqb.js.map +0 -1
package/README.md
CHANGED
|
@@ -1355,3 +1355,50 @@ against the new constraint. Apply the remaining application schema migrations,
|
|
|
1355
1355
|
run `smrt doctor --db` / `db:status --parity`, deploy the new version to every
|
|
1356
1356
|
writer, then resume traffic. Do not roll back application writers without also
|
|
1357
1357
|
restoring the pre-upgrade schema/database backup.
|
|
1358
|
+
|
|
1359
|
+
## Hosted OAuth authorization
|
|
1360
|
+
|
|
1361
|
+
Use `SmrtOAuthAuthorizationStorage` with the SDK authorization server, and
|
|
1362
|
+
`SmrtOAuthAuthorizationService` to bind consent to existing browser sessions:
|
|
1363
|
+
|
|
1364
|
+
```typescript
|
|
1365
|
+
import { createAuthorizationServer } from '@happyvertical/auth/server';
|
|
1366
|
+
import {
|
|
1367
|
+
SmrtOAuthAuthorizationService,
|
|
1368
|
+
SmrtOAuthAuthorizationStorage,
|
|
1369
|
+
} from '@happyvertical/smrt-users';
|
|
1370
|
+
|
|
1371
|
+
const authorization = await SmrtOAuthAuthorizationService.create({
|
|
1372
|
+
db,
|
|
1373
|
+
scopePermissions: { 'catalog:read': ['catalog.read'] },
|
|
1374
|
+
});
|
|
1375
|
+
const server = createAuthorizationServer({
|
|
1376
|
+
issuer: 'https://app.example/oauth',
|
|
1377
|
+
signingKey,
|
|
1378
|
+
storage: await SmrtOAuthAuthorizationStorage.create({ db }),
|
|
1379
|
+
identity: authorization.identity,
|
|
1380
|
+
scopes: ['catalog:read'],
|
|
1381
|
+
resources: ['https://app.example/mcp'],
|
|
1382
|
+
});
|
|
1383
|
+
// After explicit same-origin browser consent:
|
|
1384
|
+
const parsed = await server.parseAuthorizationRequest(url.searchParams);
|
|
1385
|
+
const redirect = await authorization.approve(server, parsed, sessionId);
|
|
1386
|
+
// On every bearer request, verify cryptography AND live authorization:
|
|
1387
|
+
const payload = await server.verifyAccessToken(token, 'https://app.example/mcp');
|
|
1388
|
+
const context = await authorization.validateAccessTokenClaims(payload);
|
|
1389
|
+
if (!context) throw new Error('Authorization is no longer active.');
|
|
1390
|
+
```
|
|
1391
|
+
|
|
1392
|
+
The SvelteKit export `createOAuthHandlers` supplies optional session-aware
|
|
1393
|
+
consent and account-grant routes. `listGrants(sessionId)` and
|
|
1394
|
+
`revokeGrant(sessionId, grantId)` confine account management to the signed-in
|
|
1395
|
+
subject. Migrate the private `users_oauth_*` tables before enabling the issuer.
|
|
1396
|
+
See [OAuth authorization](agents/oauth-authorization.md) for lifecycle,
|
|
1397
|
+
revocation, permission ceilings and the database test matrix.
|
|
1398
|
+
|
|
1399
|
+
For local SQLite processes sharing a file, configure the storage's optional
|
|
1400
|
+
`recoverDatabase` factory to recreate an invalidated connection and reapply its
|
|
1401
|
+
settings. Instance-only storage fails closed on an invalidated connection; it
|
|
1402
|
+
never silently drops PRAGMAs or temporary state. PostgreSQL does not require
|
|
1403
|
+
this option. The [transaction notes](agents/oauth-authorization.md#recoverable-local-sqlite-connections)
|
|
1404
|
+
show the factory contract.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Hosted OAuth authorization
|
|
2
|
+
|
|
3
|
+
`SmrtOAuthAuthorizationStorage` implements the protocol-neutral storage contract
|
|
4
|
+
from `@happyvertical/auth/server`. Apply normal SMRT migrations before starting
|
|
5
|
+
the server. The six `users_oauth_*` models are sensitive and have no generated
|
|
6
|
+
REST, MCP or CLI operations. Keep their tables on the private application
|
|
7
|
+
connection; public catalog roles must not read them.
|
|
8
|
+
|
|
9
|
+
`SmrtOAuthAuthorizationService.create({ db, scopePermissions })` supplies the
|
|
10
|
+
session boundary. `approve(server, parsedRequest, sessionId)` loads the existing
|
|
11
|
+
session again, checks its active user and tenant membership, verifies every
|
|
12
|
+
scope's configured permissions, and records consent. Existing magic-link,
|
|
13
|
+
password and OIDC sessions work without a separate identity mapping. Parent
|
|
14
|
+
session validity and session permission ceilings are enforced by SessionService.
|
|
15
|
+
Unknown scopes fail closed. An empty permission mapping explicitly permits a
|
|
16
|
+
scope for any valid session. OAuth does not grant super-admin bypass.
|
|
17
|
+
|
|
18
|
+
Pass `service.identity` to the SDK server. It revalidates the stored consent on
|
|
19
|
+
both code exchange and refresh. A permission loss denies the grant rather than
|
|
20
|
+
silently expanding or partially authorizing it. The consent-time permission
|
|
21
|
+
snapshot is an upper bound even if the host later changes its scope mapping.
|
|
22
|
+
|
|
23
|
+
After `server.verifyAccessToken(token, resource)`, always call
|
|
24
|
+
`service.validateAccessTokenClaims(payload)`. A non-null result is a server-only
|
|
25
|
+
SessionContext whose permissions and permissionCeiling are limited to the
|
|
26
|
+
verified token's mapped scopes. Do not serialize that context: it contains the
|
|
27
|
+
session credential. The JWT carries only a non-secret `smrt_grant_id`, subject,
|
|
28
|
+
client, tenant and scopes. JWT signature verification alone does not check live
|
|
29
|
+
session, user, tenant or consent revocation.
|
|
30
|
+
|
|
31
|
+
`listGrants(sessionId)` returns safe account-settings projections.
|
|
32
|
+
`revokeGrant(sessionId, grantId)` checks ownership and atomically revokes the
|
|
33
|
+
consent, outstanding codes, and all refresh descendants. Revoking or replaying a
|
|
34
|
+
managed refresh token also revokes its consent, so existing access tokens fail
|
|
35
|
+
the live grant check. Repeating revocation is safe. A foreign grant returns false.
|
|
36
|
+
|
|
37
|
+
The `/sveltekit` export `createOAuthHandlers({ server, authorization,
|
|
38
|
+
getSessionId })` provides Fetch-compatible `authorize`, `consent`, `grants` and
|
|
39
|
+
`protocol` handlers. The host renders consent, supplies its existing cookie
|
|
40
|
+
lookup, and preserves a validated local return path through magic-link login.
|
|
41
|
+
Consent POST requires an exact same-origin Origin header and `decision=approve`;
|
|
42
|
+
requested OAuth parameters remain on the authorization URL and are revalidated
|
|
43
|
+
by the SDK. Grant revocation POST has the same origin check and takes `grantId`.
|
|
44
|
+
All account/consent responses use private, no-store. No handler authenticates a
|
|
45
|
+
session from OAuth client identity or accepts a browser-submitted user/tenant.
|
|
46
|
+
|
|
47
|
+
## Transaction invariants
|
|
48
|
+
|
|
49
|
+
Code consume uses a conditional write. Refresh rotation and revocation lock a
|
|
50
|
+
stable family row before reading token state; locking the token being replaced
|
|
51
|
+
alone would allow a concurrent revocation to miss an inserted descendant.
|
|
52
|
+
Managed operations lock the consent row first, then the family. Replacement
|
|
53
|
+
insertion, old-token consume and replay revocation share one database executor.
|
|
54
|
+
A failed insert rolls back consume. Revalidated scopes and claims are persisted
|
|
55
|
+
atomically before the SDK exposes the replacement. Undefined claims clear prior
|
|
56
|
+
claims. Narrowing cannot expand scopes, revive or modify a consumed token.
|
|
57
|
+
|
|
58
|
+
Retry only transaction failures classified as SQLite contention or PostgreSQL
|
|
59
|
+
serialization/deadlock, after adapter rollback, at most six retries. Never retry
|
|
60
|
+
protocol callbacks or other external effects. The database adapter must clean up
|
|
61
|
+
failed statements and support independent connections; a process-local mutex is
|
|
62
|
+
not a replica-safety mechanism.
|
|
63
|
+
|
|
64
|
+
## Behavior-to-test contract (#3659)
|
|
65
|
+
|
|
66
|
+
Every row runs in `src/__tests__/oauth-authorization.test.ts` on Node >=26 with
|
|
67
|
+
SQLite and real PostgreSQL. Commands:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
pnpm --filter @happyvertical/smrt-users exec vitest run src/__tests__/oauth-authorization.test.ts
|
|
71
|
+
DATABASE_URL=postgres://postgres@127.0.0.1:55597/smrt_oauth pnpm --filter @happyvertical/smrt-users exec vitest run src/__tests__/oauth-authorization.test.ts
|
|
72
|
+
pnpm --filter @happyvertical/smrt-users typecheck
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
| Invariant / trigger | Positive | Negative / failure | Actor / context | Executor / external edge | Level |
|
|
76
|
+
| --- | --- | --- | --- | --- | --- |
|
|
77
|
+
| Register and consume code | Metadata survives new adapter; one concurrent winner | Wrong client, redirect, resource; expiry and replay | Registered client / subject | Independent DB connections; SDK code contract | Integration |
|
|
78
|
+
| Rotate / replay refresh | One replacement | Reuse revokes every descendant | Same family across replicas | Stable family lock, same transaction | Integration |
|
|
79
|
+
| Rotation rollback / revoke race | Retry after insertion failure works | Duplicate replacement aborts consume; revoke cannot miss descendant | Same family, simultaneous operations | Real rollback and independent executors | Integration |
|
|
80
|
+
| Narrow replacement | Subset and replacement claims persist through subsequent rotation; undefined claims clear old claims | Expansion, hash mismatch, consumed/revoked token | Authenticated client / original family | SDK fail-closed narrowing contract, atomic locked update | Integration |
|
|
81
|
+
| Explicit recovery factory | Recreates configured connection after invalidation | Instance-only fails closed; ordinary storage failure propagates | Storage owner / configured database | Injected failure boundary plus real independent-connection races | Unit + integration |
|
|
82
|
+
| Access revocation | Another adapter sees it; repeats safe | Expired revocation no longer effective | Possessor of verified access token | Unique insert with conflict handling | Integration |
|
|
83
|
+
| Consent/grant ownership | Own grant can list/revoke | Other subject, tenant, scope deny; no session bearer in projection | Owner A/B × grant A/B | Live session + consent reads; atomic cascade | Integration |
|
|
84
|
+
| Live identity and ceiling | Current permission allows | Inactive user/membership, changed session ceiling, destroyed session | Active vs revoked context | SessionService and current RBAC | Integration |
|
|
85
|
+
| Code/live revoke cascade | Valid code exchanges | Membership loss before exchange; refresh revoke kills access | Original subject/tenant | SDK revalidateConsent callback | Integration |
|
|
86
|
+
| Consent snapshot / tenant | Original mapped scope | Later permission mapping expansion; inactive tenant | Owner × current tenant | Stored upper bound plus live permission checks | Integration |
|
|
87
|
+
| Layered session / expiry | Parent and child valid | Parent destroyed or child expired | Person session on device parent | Existing session controls | Integration |
|
|
88
|
+
| Consent route | Same-origin explicit approval | Missing/foreign Origin, denial, revoked session | Browser session / untrusted POST | Fetch request and SDK parsing | Route + DB |
|
|
89
|
+
|
|
90
|
+
This is new functionality, so a base-regression comparison is N/A: base has no
|
|
91
|
+
OAuth authorization storage or session grant API. Positive/negative contract
|
|
92
|
+
cases exercise real persistence rather than mocked SQL. Remote-provider outage
|
|
93
|
+
is N/A: the service has no external identity/provider call. SDK owns malformed
|
|
94
|
+
OAuth protocol, signature, PKCE and redirect parsing tests; hosts own their
|
|
95
|
+
consent UI, login return path and application-specific workspace policy.
|
|
96
|
+
|
|
97
|
+
### Recoverable local SQLite connections
|
|
98
|
+
|
|
99
|
+
Local libsql connections may be invalidated by the SQL adapter after a native
|
|
100
|
+
`SQLITE_BUSY` failure. An instance-only storage adapter fails closed in this
|
|
101
|
+
case; it does not silently reconnect and discard connection settings.
|
|
102
|
+
For recoverable local multi-replica operation, supply `recoverDatabase`:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
const storage = await SmrtOAuthAuthorizationStorage.create({
|
|
106
|
+
db,
|
|
107
|
+
recoverDatabase: async () => {
|
|
108
|
+
const connection = await getDatabase({
|
|
109
|
+
type: 'sqlite', url: configuredFileUrl, dbid: randomUUID(),
|
|
110
|
+
});
|
|
111
|
+
await connection.query('PRAGMA foreign_keys = ON');
|
|
112
|
+
// Reapply any other application-required connection settings here.
|
|
113
|
+
return connection;
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The factory is also used for the initial dedicated storage connection, keeping
|
|
119
|
+
storage recovery independent of a shared application/session connection. It
|
|
120
|
+
must return a migrated connection to the same database and restore every
|
|
121
|
+
required PRAGMA, attachment, temporary table or other per-connection setting.
|
|
122
|
+
It must not create application schema. Known invalidated failures may be retried
|
|
123
|
+
at most six times; ordinary storage failures propagate. PostgreSQL does not
|
|
124
|
+
need this factory. Connection ownership/disposal remains with the host factory.
|
package/agents/retention.md
CHANGED
|
@@ -57,3 +57,21 @@ reap them.
|
|
|
57
57
|
marked `expired` by lazy expiry, and consumed history; it retains approved
|
|
58
58
|
requests until exchange so a near-expiry approval cannot orphan its bearer
|
|
59
59
|
session.
|
|
60
|
+
|
|
61
|
+
## OAuth credential retention
|
|
62
|
+
|
|
63
|
+
`users-oauth-credentials` prunes expired authorization codes and access-token
|
|
64
|
+
revocations. Refresh grants (including consumed or revoked hashes) remain until
|
|
65
|
+
30 days after their own expiry, so replay within their validity period and this
|
|
66
|
+
grace period still revokes the family. Later replay fails as an unknown token.
|
|
67
|
+
This bounds retained rotation history even for continuously active families.
|
|
68
|
+
The sweep locks each candidate family using the same row as token rotation,
|
|
69
|
+
rechecks its descendants, and deletes the family only when none remain. Live
|
|
70
|
+
grants and revocations are never swept. Each family is a separate transaction;
|
|
71
|
+
rolled-back contention retries follow the OAuth transaction policy. If the SQL
|
|
72
|
+
adapter invalidates a connection (for example SQLite SQLITE_BUSY), the sweep
|
|
73
|
+
reports failure; the host must acquire a fresh connection before retrying. It
|
|
74
|
+
never continues on an invalidated connection. Dry runs write nothing and
|
|
75
|
+
report approximate counts under concurrent writes. Clients and consent records
|
|
76
|
+
remain for application-managed lifecycle/audit. Opt-out and unregister work as
|
|
77
|
+
for the other retention tasks. No raw bearer or credential hash is logged.
|