@happyvertical/smrt-users 0.51.36 → 0.52.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 +1 -0
- package/agents/mobile-auth.md +45 -0
- package/agents/oidc-provisioning.md +97 -0
- package/agents/permissions.md +186 -0
- package/agents/retention.md +52 -0
- package/agents/terminal-auth-testing.md +32 -0
- package/dist/__tests__/helpers/oidc-test-server.d.ts +1 -1
- package/dist/__tests__/helpers/oidc-test-server.d.ts.map +1 -1
- package/dist/chunks/{TerminalAuthService-D9OXHSHy.js → TerminalAuthService-Br64vHtd.js} +4 -3
- package/dist/chunks/TerminalAuthService-Br64vHtd.js.map +1 -0
- package/dist/collections/CliAuthApproveLimitCollection.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/manifest.json +72 -84
- package/dist/smrt-knowledge.json +42 -6
- package/dist/sveltekit.js +1 -1
- package/package.json +14 -13
- package/dist/chunks/TerminalAuthService-D9OXHSHy.js.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -12,6 +12,7 @@ are not prerequisites for unrelated user-package work.
|
|
|
12
12
|
| `src/services/PermissionResolver.ts` | permission precedence, inherited memberships, guards, RLS, role seeding | [agents/permissions.md](agents/permissions.md) |
|
|
13
13
|
| `src/services/OidcLoginService.ts`, collections and OIDC handlers | identity reconciliation, transaction boundaries, migration readiness | [agents/oidc-provisioning.md](agents/oidc-provisioning.md) |
|
|
14
14
|
| `src/services/MobileAuthService.ts` | mobile handshake, bearer sessions, bootstrap extension boundary | [agents/mobile-auth.md](agents/mobile-auth.md) |
|
|
15
|
+
| `src/services/TerminalAuthService.ts`, `src/collections/CliAuthApproveLimitCollection.ts` | terminal approval concurrency, shared attempt budget, retry-safe database fixtures | [agents/terminal-auth-testing.md](agents/terminal-auth-testing.md) |
|
|
15
16
|
| `src/retention.ts` | expired session/token/CLI-auth reaping and retention sweep wiring | [agents/retention.md](agents/retention.md) |
|
|
16
17
|
|
|
17
18
|
## Models and authority
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Mobile authentication
|
|
2
|
+
|
|
3
|
+
Read for `src/services/MobileAuthService.ts` and `/sveltekit` mobile handlers.
|
|
4
|
+
|
|
5
|
+
`createMobileAuthHandlers(options)` (from `/sveltekit`) returns the mountable
|
|
6
|
+
server side of the KMP mobile contract: `authStart` (`POST auth/start`,
|
|
7
|
+
server-brokered PKCE via `OidcLoginService`), `authComplete`
|
|
8
|
+
(`POST auth/complete`, code + echoed `state`/`codeVerifier` → bearer
|
|
9
|
+
session), `session.GET`/`session.DELETE` (bootstrap/logout), and
|
|
10
|
+
`guard`/`withSession` — the bearer middleware for app-owned mobile routes.
|
|
11
|
+
Core logic lives in `MobileAuthService` (framework-agnostic; exported from
|
|
12
|
+
the package root).
|
|
13
|
+
|
|
14
|
+
- **Bearer = session id** (same convention as `TerminalAuthService`); 401
|
|
15
|
+
bodies are `{ error, code }` and drive the mobile client's re-auth flow.
|
|
16
|
+
- **Stateless handshake**: the OAuth `state` is an HMAC-signed token
|
|
17
|
+
(secret: `stateSecret` ?? provider `clientSecret`) carrying
|
|
18
|
+
nonce/provider/createdAt — full ID-token nonce verification with no
|
|
19
|
+
server-side pending state. The `codeVerifier` never enters a URL: it is
|
|
20
|
+
client-held per the frozen contract.
|
|
21
|
+
- **Wire DTOs** come from `@happyvertical/smrt-mobile-contract`
|
|
22
|
+
(`MobileAuthStartRequest` etc.) — one owning package for the Kotlin,
|
|
23
|
+
Swift, and TypeScript shapes (compile-checked descriptors + parity test).
|
|
24
|
+
- **Tenant options** honor `Role.inheritsToDescendants` (#1867): direct
|
|
25
|
+
ACTIVE memberships plus descendants of flagged memberships (nearest
|
|
26
|
+
flagged ancestor labels the option; any direct row pins; inactive direct
|
|
27
|
+
rows exclude). Session binding defaults to the first DIRECT tenant —
|
|
28
|
+
override with `resolveTenantId`.
|
|
29
|
+
- **Hooks**: `resolveUser` (invite-gating; default provisions via
|
|
30
|
+
`getOrCreateFromOidc`), `resolveTenantId`, `buildExtras` (bootstrap
|
|
31
|
+
`extras`; model JSON must use `toPublicJSON({ permissions })` — #1822).
|
|
32
|
+
`buildExtras` is the only app-domain bootstrap extension point. Never emit
|
|
33
|
+
app fields such as `dashboard` at the response top level: they are outside
|
|
34
|
+
`MobileSessionBootstrap` and the Kotlin decoder ignores them.
|
|
35
|
+
- **Guard** wraps `withSessionPermissionContext`, so
|
|
36
|
+
`assertOperationPermission`, tenancy context, and Postgres RLS all see the
|
|
37
|
+
bearer caller; `OperationPermissionError` maps to 403 with a
|
|
38
|
+
machine-readable `reason`.
|
|
39
|
+
- **Uploads**: `resolveMobileUploadDedupKey` + the documented contract in
|
|
40
|
+
`docs/content/architecture/mobile-upload-contract.md` (`clientCaptureId`
|
|
41
|
+
field, `Idempotency-Key` header fallback); domain ingestion stays
|
|
42
|
+
app-side. Framework-model writes ride `sync/apply`, not this path.
|
|
43
|
+
- Configure `redirectUris` in production — RFC 8252 scheme rules always
|
|
44
|
+
apply, but the allow list is the defense against redirecting authorization
|
|
45
|
+
responses to attacker-controlled URIs.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# OIDC provisioning
|
|
2
|
+
|
|
3
|
+
Read for changes to `src/collections/UserCollection.ts`, `src/services/OidcLoginService.ts`,
|
|
4
|
+
OIDC handlers, or identity migrations. The canonical scenario matrix is
|
|
5
|
+
`packages/profiles/src/testing/oidcProvisioningDecisionMatrix.ts`.
|
|
6
|
+
|
|
7
|
+
- **OIDC `email_verified` is enforced.** `UserCollection.getOrCreateFromOidc`
|
|
8
|
+
refuses to provision a user when the IdP explicitly returns
|
|
9
|
+
`email_verified: false` (opt out with `{ allowUnverifiedEmail: true }`). An
|
|
10
|
+
absent claim makes no assertion and is not enforced.
|
|
11
|
+
- **RFC 9207 response issuer is exact.** OIDC callbacks validate a supplied
|
|
12
|
+
`iss` against discovered metadata with exact string comparison before trusting
|
|
13
|
+
a code or error. If metadata advertises issuer-response support, `iss` is
|
|
14
|
+
required.
|
|
15
|
+
- **Verified-email Profile reuse is fail-closed.** The typed canonical scenarios
|
|
16
|
+
live in
|
|
17
|
+
`packages/profiles/src/testing/oidcProvisioningDecisionMatrix.ts`; both
|
|
18
|
+
package suites execute that matrix and public docs reference it rather than
|
|
19
|
+
maintaining another behavioral table. Default provisioning reuses only one
|
|
20
|
+
unowned, global `Person`. Tenant-scoped, non-Person, duplicate-email,
|
|
21
|
+
and already-owned matches fail before User/session creation. An existing
|
|
22
|
+
issuer/subject link without a User must still be the unique global Person for
|
|
23
|
+
the current verified claim email; once owned, the stable issuer/subject link
|
|
24
|
+
reuses its canonical Person and owner.
|
|
25
|
+
Issuer and subject are opaque, case-sensitive identifiers; preserve their
|
|
26
|
+
exact value and use trim only to reject blank claims.
|
|
27
|
+
- **`resolveProfile` is the application reconciliation boundary.** The
|
|
28
|
+
SvelteKit handlers, `OidcLoginService`, and `getOrCreateFromOidc` accept the
|
|
29
|
+
same hook inside the provisioning transaction. The service/handler path
|
|
30
|
+
supplies protocol-validated claims; direct collection callers must validate
|
|
31
|
+
and trust their claim source before calling `getOrCreateFromOidc`.
|
|
32
|
+
Token/userinfo merging keeps `email` and `email_verified` paired to the same
|
|
33
|
+
claim source; verification is never borrowed across sources.
|
|
34
|
+
Resolver reads/writes use the supplied `db`, and the hook must be idempotent
|
|
35
|
+
because a concurrent unique-key conflict can retry it. `undefined` chooses
|
|
36
|
+
the secure default and `null` rejects, including exact issuer/subject reuse.
|
|
37
|
+
For a new identity, a supplied Profile is still validated as the unique,
|
|
38
|
+
unowned global Person for the verified email. For an exact existing identity,
|
|
39
|
+
it must be the already-linked Profile and cannot rebind identity authority;
|
|
40
|
+
stable-link owner and canonical-Person checks still apply. The hook receives a
|
|
41
|
+
separate frozen claims snapshot; internal retry and persistence state is not
|
|
42
|
+
exposed for mutation.
|
|
43
|
+
- **Owned first binding requires `authorizeProfileOwner`.** An invitation or
|
|
44
|
+
approval workflow may explicitly return both its pre-provisioned canonical
|
|
45
|
+
global `Person` and existing approved `User` from this transaction-bound
|
|
46
|
+
hook. `undefined` keeps the secure `profile_owned` default and `null`
|
|
47
|
+
rejects. SMRT treats only the selected IDs as input, reloads them in the
|
|
48
|
+
provisioning transaction, and requires `email_verified === true`, the unique
|
|
49
|
+
canonical global Person for the normalized claim email, exactly one owner,
|
|
50
|
+
that owner as the selected User, and the same normalized User email. The
|
|
51
|
+
hook runs before identity/User/session creation and may be retried, so use
|
|
52
|
+
only its supplied `db` and `users` handles and keep application authorization
|
|
53
|
+
idempotent. Never authorize from email matching alone. Existing exact
|
|
54
|
+
identities cannot be rebound; when `resolveProfile` is also present both
|
|
55
|
+
hooks must select the same Profile.
|
|
56
|
+
- **OIDC first login is atomic.** The Profile, `OidcIdentity`, and User are one
|
|
57
|
+
transaction. The database arbiters are `OidcIdentity.identityKey`,
|
|
58
|
+
private `oidc_profile_email_reservations.email_key`, `User.emailKey`, and the
|
|
59
|
+
unique `User.profileId`; local callbacks acquire exact issuer/subject and normalized
|
|
60
|
+
email locks in deterministic order so changed email claims also serialize.
|
|
61
|
+
SQLite and DuckDB callbacks additionally serialize every root-handle statement
|
|
62
|
+
the coordinator owns per database URL — `_smrt_backfills` initialization, the
|
|
63
|
+
transaction, and the post-commit rebind — because one adapter multiplexes a
|
|
64
|
+
single native connection and cannot safely overlap unrelated root
|
|
65
|
+
transactions. Never overlap two statements on one such handle, inside a
|
|
66
|
+
transaction or not: rebind and owner/email candidate reads are sequential,
|
|
67
|
+
never `Promise.all`. Owner-authorized DuckDB callbacks use that same
|
|
68
|
+
root-handle serialization;
|
|
69
|
+
PostgreSQL deadlock and serialization errors use a bounded transaction retry.
|
|
70
|
+
Newly provisioned Profiles use non-semantic per-profile slugs so equal IdP
|
|
71
|
+
display names cannot trigger a natural-key upsert;
|
|
72
|
+
run
|
|
73
|
+
`smrt db:status`, `smrt db:migrate`, then `smrt db:status` before deployment.
|
|
74
|
+
Stop or upgrade old writers first. Before migration, group
|
|
75
|
+
non-null `users.profile_id` values, then reconcile duplicates. After
|
|
76
|
+
migration, run public `backfillProfileEmailKeys(db)`,
|
|
77
|
+
`backfillUserEmailKeys(db)`, then `backfillLegacyUserProfiles(db)` from one
|
|
78
|
+
deploy process. The first two use the shared
|
|
79
|
+
TypeScript `normalizeIdentityEmail()` implementation transactionally and are
|
|
80
|
+
idempotent; the User backfill fails before writes if normalized duplicates
|
|
81
|
+
remain. The legacy Profile backfill transactionally creates canonical global
|
|
82
|
+
Persons for null/blank User links without creating OIDC identities or
|
|
83
|
+
inferring ownership; reconcile any same-email Profile or reservation before
|
|
84
|
+
retrying. Its marker records a completed pass but never skips newly imported
|
|
85
|
+
legacy Users on a later run. Every OIDC path requires the Profile email-key
|
|
86
|
+
readiness marker; creating a User or checking User email uniqueness
|
|
87
|
+
additionally requires the User marker. A stable issuer/subject with an
|
|
88
|
+
existing owning User skips only the User email-key lookup and marker. Full
|
|
89
|
+
scans remain in the explicit deploy step; guarded runtime paths use indexed
|
|
90
|
+
keys and validate only returned candidates. Multiple null links remain valid.
|
|
91
|
+
Legacy race keys backfill only after canonical validation. Pass a root
|
|
92
|
+
database on adapters such as DuckDB that cannot create nested savepoints.
|
|
93
|
+
Root adapters must expose `beginTransaction`; transaction-only handles are
|
|
94
|
+
ambiguous and fail closed before provisioning writes. Caller-owned
|
|
95
|
+
transactions never run `_smrt_backfills` DDL and require that table to
|
|
96
|
+
already exist; use the root database when initialization or recovery is
|
|
97
|
+
needed.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Permissions and tenant inheritance
|
|
2
|
+
|
|
3
|
+
Read for `src/services/PermissionResolver.ts`, operation guards, role seeding,
|
|
4
|
+
RLS, and tenant-hierarchy changes. Integration examples live in README's
|
|
5
|
+
manifest-derived permission catalog and PostgreSQL RLS sections.
|
|
6
|
+
|
|
7
|
+
## Membership selection
|
|
8
|
+
|
|
9
|
+
1. A direct target-tenant membership pins resolution: active resolves normally;
|
|
10
|
+
pending/suspended returns empty. Never union direct and ancestor roles.
|
|
11
|
+
2. Otherwise walk hierarchyPath nearest-first for the nearest active ancestor
|
|
12
|
+
membership whose role has inheritsToDescendants. Skip inactive/unflagged
|
|
13
|
+
ancestors; they neither grant nor block. No qualifying ancestor means empty.
|
|
14
|
+
3. Run later layers against the target tenant. Only target-tenant groups apply;
|
|
15
|
+
membership overrides travel with the chosen ancestor membership.
|
|
16
|
+
inheritedFromTenantId reports that ancestor, null for direct resolution.
|
|
17
|
+
|
|
18
|
+
Verify hierarchyPath link-by-link against loaded parentTenantId rows before
|
|
19
|
+
trusting it: excess depth, self-reference, duplicates or inconsistent paths
|
|
20
|
+
fail closed. Bound traversal by MAX_TENANT_HIERARCHY_DEPTH. Tenant status is not
|
|
21
|
+
consulted, matching direct resolution. Long-lived user/tenant caches must
|
|
22
|
+
invalidate ancestor membership changes and inheritsToDescendants flips;
|
|
23
|
+
request-scoped caches do not survive those requests.
|
|
24
|
+
|
|
25
|
+
`loadSessionContext().tenantAuthorization` is authoritative for required-tenant
|
|
26
|
+
consumers; membership null can mean inherited authority.
|
|
27
|
+
|
|
28
|
+
4. If no membership resolved at all, apply the opt-in ancestor-read policy
|
|
29
|
+
(smrt#2939): the user's ACTIVE memberships on VERIFIED DESCENDANTS of the
|
|
30
|
+
target, whose role slug is declared in `permissions.ancestorRead.roles` and
|
|
31
|
+
within `maxDepth` hops, contribute `<collection>.read` for declared
|
|
32
|
+
collections, intersected with BOTH that role's own catalog grants (so a
|
|
33
|
+
descendant-side tenant GRANT, group role, or membership GRANT cannot widen
|
|
34
|
+
it) AND the principal's effective permissions in that descendant (so its
|
|
35
|
+
DENY cascade and membership DENY apply). Only a SYSTEM
|
|
36
|
+
role (`tenantId` null, `isSystem: true`) matches a declared slug: slugs are
|
|
37
|
+
not unique across a hierarchy, so a tenant-scoped custom role of the same
|
|
38
|
+
name must not opt its tenant in. Off by default;
|
|
39
|
+
read-only; never lateral; never reached when a direct or inherited
|
|
40
|
+
membership already decided. The target tenant's DENY still subtracts.
|
|
41
|
+
`ancestorReadFromTenantIds` reports the contributing descendants. The grant
|
|
42
|
+
carries no membership, so `membershipId`/`tenantAuthorization.membershipId`
|
|
43
|
+
stay null and a consumer requiring a non-empty membership still refuses.
|
|
44
|
+
|
|
45
|
+
## Permission precedence
|
|
46
|
+
|
|
47
|
+
Apply these layers in order, later layers overriding earlier ones:
|
|
48
|
+
|
|
49
|
+
1. Ancestor TenantPermissionOverride cascade (GRANT adds, DENY removes).
|
|
50
|
+
2. Selected membership role permissions.
|
|
51
|
+
3. Target-tenant group-role permissions.
|
|
52
|
+
4. Tenant cascade's net DENY removes role/group grants.
|
|
53
|
+
5. Membership GRANT may re-add a tenant-denied slug.
|
|
54
|
+
6. Membership DENY removes last and always wins.
|
|
55
|
+
|
|
56
|
+
The tenant block is the cascade's net result, not the union of historical DENYs:
|
|
57
|
+
a child's more-specific GRANT can override a parent DENY. Use a direct child
|
|
58
|
+
membership or tenant DENY to attenuate inherited role authority.
|
|
59
|
+
`getGroupIdsForTenant(userId, tenantId)` is required; getGroupIds is cross-tenant.
|
|
60
|
+
|
|
61
|
+
## Hierarchies and seeding
|
|
62
|
+
|
|
63
|
+
- `Tenant.save()` derives hierarchyPath/hierarchyLevel from the REAL
|
|
64
|
+
parentTenantId chain on every save (never from an ancestor's stored path),
|
|
65
|
+
refuses missing parents, cycles and over-depth before writing, and
|
|
66
|
+
re-materializes descendants by raw column update (so STI subclass columns
|
|
67
|
+
are never clobbered). createChild/moveToParent/makeRoot are thin wrappers.
|
|
68
|
+
`materializeTenantHierarchy(db, { dryRun })` / `smrt
|
|
69
|
+
db:materialize-tenant-hierarchy` backfills legacy rows: idempotent, one
|
|
70
|
+
transaction, refuses a broken chain without writing (#3036). A NEW or
|
|
71
|
+
CHANGED parent link is authority-bearing, so `Tenant.save()` first loads the
|
|
72
|
+
parent through TenantCollection (the caller's tenancy scope) and refuses an
|
|
73
|
+
invisible one on every path — helpers, `create({ parentTenantId })`, or a
|
|
74
|
+
plain assignment + save; system context bypasses as usual, and an unchanged
|
|
75
|
+
link is never re-checked. getTree(rootId?)
|
|
76
|
+
returns UI structure. Maximum depth is 10.
|
|
77
|
+
- Tenant override cascade requires parent cascadePermissions and child
|
|
78
|
+
inheritPermissions. These flags do not gate the independent, per-role
|
|
79
|
+
inheritsToDescendants membership flow.
|
|
80
|
+
- `permissions.ancestorRead` in the `users` package config declares upward,
|
|
81
|
+
read-only visibility: `{ roles, collections, maxDepth? }`. Both lists are
|
|
82
|
+
required and non-empty and `maxDepth >= 1`, else the policy is OFF — there is
|
|
83
|
+
no partially valid declaration. `isAncestorReadableSlug()` is the single place
|
|
84
|
+
the read-only bound is enforced; only a two-segment `<collection>.<action>`
|
|
85
|
+
slug whose action normalizes to `read` on a declared collection passes.
|
|
86
|
+
`PermissionResolver.create(options, { ancestorReadPolicy })` binds one
|
|
87
|
+
resolver; `null` forces it off. It grants the OPERATION at the ancestor, never
|
|
88
|
+
row visibility — sibling rows stay scoped by the tenancy interceptor and RLS.
|
|
89
|
+
- seedSystemRoles({ inheritsToDescendants: ['owner', 'admin'] }) flags listed
|
|
90
|
+
slugs additively, never unflags omitted ones, and rejects unknown slugs.
|
|
91
|
+
Default seeds are exact-tenant.
|
|
92
|
+
- RolePermissionCollection.seedRolePermissions() or
|
|
93
|
+
seedSystemRoles({ seedPermissions: true }) maps owner/admin to all catalog
|
|
94
|
+
permissions, member to ordinary-resource read/create, viewer to read. Member
|
|
95
|
+
create excludes identity/RBAC/security resources and their joins/overrides.
|
|
96
|
+
Seeding is additive/idempotent; removal requires prune: true.
|
|
97
|
+
- When a package adds built-in self-personalization permissions after role
|
|
98
|
+
creation, explicitly call seedDefaultRolePersonalizationPermissions(). It
|
|
99
|
+
upgrades owner/admin/member/viewer idempotently and never grants custom roles.
|
|
100
|
+
|
|
101
|
+
## Guards and RLS
|
|
102
|
+
|
|
103
|
+
PermissionCatalogService derives collection.action slugs from manifests,
|
|
104
|
+
including custom actions; list/get map to read. Guards derive a class's slug
|
|
105
|
+
through the same resolution (explicit `collection`, manifest entry, then its
|
|
106
|
+
STI base's), so an STI subtype resolves its STI base's collection
|
|
107
|
+
(`Network extends Tenant` -> `tenants.update`) in every build, including a
|
|
108
|
+
bundle decorated before its manifest loads (#3125). Hand-written mutations in form
|
|
109
|
+
handlers, endpoints, CLI and jobs use assertOperationPermission(). It requires
|
|
110
|
+
catalog presence then resolves permissions, throwing fail-closed by default.
|
|
111
|
+
Use onDeny: 'return', checkOperationPermission or hasOperationPermission only
|
|
112
|
+
when the caller handles a structured/boolean denial.
|
|
113
|
+
|
|
114
|
+
Generated SvelteKit write routes (POST/PUT/PATCH/DELETE CRUD and mutating
|
|
115
|
+
custom actions) enforce the same `collection.action` slug (custom actions use
|
|
116
|
+
`collection.<method>`; actions hosted on a collection class use the item collection, which the catalog now emits) after authentication: the slug must be in the request's
|
|
117
|
+
session permission snapshot (`locals.permissions`/`permissionSet`/
|
|
118
|
+
`smrtPermissions` or `locals.tenantContext.permissions`), otherwise 403. Only
|
|
119
|
+
`locals.tenantContext.superAdminBypass` skips it; `api.public: true` stays open.
|
|
120
|
+
|
|
121
|
+
Resource-anchored guards receive the resource tenant ID. When it differs from
|
|
122
|
+
the session tenant, omit the session membership so the resolver selects the
|
|
123
|
+
appropriate authority; mismatched supplied membership/tenant fails closed.
|
|
124
|
+
Inherited root-admin authority can then authorize descendant resources without
|
|
125
|
+
application-side membership fan-out.
|
|
126
|
+
|
|
127
|
+
Resolution cost must not scale with catalog size in objects (#3047): role →
|
|
128
|
+
permission ids and ids → slugs are `list({ select })` projection reads (the
|
|
129
|
+
collections' beforeList interceptors still run; no RolePermission/Permission
|
|
130
|
+
is hydrated), so an owner mapped to the whole catalog costs two queries and
|
|
131
|
+
no per-permission objects. Keep new resolver reads projections when only ids
|
|
132
|
+
or slugs are needed, and do not add public collection methods for them — each
|
|
133
|
+
becomes a custom-action slug in the manifest-derived catalog. The resolver's
|
|
134
|
+
collections never inherit `defaultListLimit`/`maxListLimit` from the caller's
|
|
135
|
+
options, and id-list reads chunk at `IN_LIST_CHUNK_SIZE` with an explicit
|
|
136
|
+
limit: a truncated authorization read would narrow the result silently.
|
|
137
|
+
`issue-3047-permission-resolution-cost-postgres.test.ts` pins the hydration
|
|
138
|
+
budget and a golden equivalence matrix recorded on the hydrating resolver.
|
|
139
|
+
|
|
140
|
+
The same rule holds for every framework-internal authorization read (#3048):
|
|
141
|
+
collections that back a decision — the resolver's, the ResourceGrant exact-tuple
|
|
142
|
+
read in `checkResourceOperationPermission()` (DENY wins only if every row is
|
|
143
|
+
read), and TenantService's owned-tenant quota count — are created through
|
|
144
|
+
`withoutListBounds()` (`src/services/authorization-read-options.ts`), which
|
|
145
|
+
strips only the caller's list bounds. Use it for any new decision read;
|
|
146
|
+
`issue-3048-authorization-list-bounds-postgres.test.ts` pins it.
|
|
147
|
+
|
|
148
|
+
PermissionResolver runs every resolution inside smrt-tenancy's system context
|
|
149
|
+
(#3036). Its reads are deliberately cross-tenant and framework-owned — the
|
|
150
|
+
ancestor TenantPermissionOverride batch, ancestor memberships for
|
|
151
|
+
inheritsToDescendants, descendant memberships for ancestor-read — and are keyed
|
|
152
|
+
by the explicit (userId, tenantId); with those classes registered tenant-scoped
|
|
153
|
+
(`autoFilter`) the consumer's filter otherwise threw TenantIsolationError or
|
|
154
|
+
silently narrowed them. It returns slugs and ids only, never rows, and the
|
|
155
|
+
caller's own reads stay filtered. Do not move a resolver read back under the
|
|
156
|
+
ambient tenant; the Postgres regression suite
|
|
157
|
+
`issue-3036-hierarchy-resolution-postgres.test.ts` pins this. Because nothing
|
|
158
|
+
backstops those reads any more, the tenant-override cascade verifies its chain
|
|
159
|
+
too: it uses hierarchyPath only when it agrees link-by-link with
|
|
160
|
+
parentTenantId, otherwise walks the real parent links (so a forged path cannot
|
|
161
|
+
cascade an unrelated tenant in, and a never-materialized row still gets its real
|
|
162
|
+
ancestors' DENYs — and GRANTs), and throws TenantHierarchyError on a broken
|
|
163
|
+
real chain: a per-request failure for that tenant's whole subtree, which
|
|
164
|
+
`smrt db:materialize-tenant-hierarchy --dry-run` reports ahead of time (see
|
|
165
|
+
README "Upgrading an existing fleet").
|
|
166
|
+
`getTenantInheritanceChain()` returns Tenant rows, so it is NOT run in system
|
|
167
|
+
context; it reports the same verified chain the cascade applies when the
|
|
168
|
+
caller can read every ancestor. Otherwise it never reports PARENT_NOT_FOUND
|
|
169
|
+
for a row the caller simply cannot see: an interceptor refusal surfaces as
|
|
170
|
+
TenantIsolationError, and a silently filtered row (e.g. RLS) ends the chain at
|
|
171
|
+
the nearest readable ancestor. Descendant path rewrites are raw column updates recorded through
|
|
172
|
+
`bumpChangeFeed`; `updated_at` is intentionally not bumped (derived columns;
|
|
173
|
+
every save recomputes them). They are not atomic with the moving row's save —
|
|
174
|
+
an interrupted subtree fails closed until re-saved or backfilled.
|
|
175
|
+
|
|
176
|
+
System context and super-admin bypass are honored. Pass
|
|
177
|
+
allowSuperAdminBypass: false for money or separation-of-duties operations that
|
|
178
|
+
require an explicit grant.
|
|
179
|
+
|
|
180
|
+
generatePostgresPermissionSql/applyPostgresPermissionPolicies enforce generated
|
|
181
|
+
RLS using smrt.permissions and smrt.tenant_id, installed with set_config by
|
|
182
|
+
withSessionPermissionContext. This bounds REST, MCP and in-process database
|
|
183
|
+
access once principal context is set. Child-tenant sessions receive resolved
|
|
184
|
+
inherited permissions automatically, but RLS row filtering remains bound to the
|
|
185
|
+
session tenant: a root session's app-level guard authorization does not itself
|
|
186
|
+
permit child rows through RLS. Preserve this distinction.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# smrt-users/credential retention
|
|
2
|
+
|
|
3
|
+
Module semantics for `src/retention.ts` and the three `deleteExpired()` paths
|
|
4
|
+
it drives. Package orientation, the permission model, and the security rules
|
|
5
|
+
that apply before editing anything live in [../AGENTS.md](../AGENTS.md) — read
|
|
6
|
+
that first.
|
|
7
|
+
|
|
8
|
+
## Credential retention (#2375)
|
|
9
|
+
|
|
10
|
+
`SessionCollection.deleteExpired()`,
|
|
11
|
+
`UsersMagicLinkTokenCollection.deleteExpired()` and
|
|
12
|
+
`UsersCliAuthRequestCollection.deleteExpired()` all existed, and all waited for
|
|
13
|
+
an application to remember to call them. Expired credential rows are the worst
|
|
14
|
+
kind of unbounded growth: worthless the moment they expire, and exactly the
|
|
15
|
+
rows an attacker would like to still find in the table.
|
|
16
|
+
|
|
17
|
+
`src/retention.ts` contributes the three to the framework retention sweep in
|
|
18
|
+
`@happyvertical/smrt-core`, so `smrt db:prune` and a running jobs `TaskRunner`
|
|
19
|
+
reap them.
|
|
20
|
+
|
|
21
|
+
- **The entry point registers on import.** `src/index.ts` calls
|
|
22
|
+
`registerUserRetentionTasks()`, so any process that loaded this package
|
|
23
|
+
contributes the tasks — including `smrt db:prune`, which imports the package
|
|
24
|
+
optionally for exactly that reason. Registering is not scheduling: nothing is
|
|
25
|
+
deleted until something runs a sweep.
|
|
26
|
+
- **Task names** are `users-sessions`, `users-magic-link-tokens`,
|
|
27
|
+
`users-cli-auth-requests` — prefixed with the package's short name because
|
|
28
|
+
the retention registry is one process-global namespace. Opt one out with
|
|
29
|
+
`runRetentionSweep(db, { tasks: { 'users-sessions': false } })` or
|
|
30
|
+
`smrt db:prune --skip users-sessions`; `unregisterUserRetentionTasks()`
|
|
31
|
+
removes all three.
|
|
32
|
+
- **There is no retention window to configure.** An expired credential has
|
|
33
|
+
nothing worth retaining, so each task deletes only already-expired rows. An
|
|
34
|
+
application that keeps expired sessions for audit should opt the task out and
|
|
35
|
+
archive them itself.
|
|
36
|
+
- **All three are a single counted DELETE**, not a hydrate-and-delete loop.
|
|
37
|
+
They now run unattended on a timer, and a per-row delete that throws part-way
|
|
38
|
+
leaves the rest of the expired rows un-reaped — the #1400 reasoning that
|
|
39
|
+
already applied to sessions, now applied to the other two. Counting first
|
|
40
|
+
also gives a usable figure where `rowCount` is not populated, and is what
|
|
41
|
+
makes `{ dryRun }` preview the same predicate rather than an estimate. The
|
|
42
|
+
count and the delete are not one transaction, so the figure is approximate
|
|
43
|
+
under concurrent writers.
|
|
44
|
+
- **`expiresAt` is `@field({ indexed: true })`** on `Session`,
|
|
45
|
+
`UsersMagicLinkToken` and `UsersCliAuthRequest`: the prune predicate scans
|
|
46
|
+
that column on every pass.
|
|
47
|
+
- **CLI bearer handoff is single-use.** An approved request becomes `consumed`
|
|
48
|
+
and clears its `sessionId` when one poller wins the exchange. The CLI
|
|
49
|
+
retention task deletes pending requests past their TTL, requests already
|
|
50
|
+
marked `expired` by lazy expiry, and consumed history; it retains approved
|
|
51
|
+
requests until exchange so a near-expiry approval cannot orphan its bearer
|
|
52
|
+
session.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Terminal approval storage and regression matrix — #3076
|
|
2
|
+
|
|
3
|
+
The failed-approval budget is shared storage, keyed by unique `user_id`.
|
|
4
|
+
Each UPSERT candidate uses a unique private slug, so concurrent first inserts do
|
|
5
|
+
not also collide on the inherited `(slug, context)` index. Existing rows require
|
|
6
|
+
no migration: their slug is not used for lookup or authorization. The guarded
|
|
7
|
+
UPSERT continues to reserve at most the configured count across service instances
|
|
8
|
+
and processes. Unknown database failures propagate; they are not authentication
|
|
9
|
+
successes or silently converted to a rate-limit response.
|
|
10
|
+
|
|
11
|
+
Risk: high (terminal authentication and shared rate-limit authority). No public
|
|
12
|
+
API, schema, session, tenant or approval contract changes.
|
|
13
|
+
|
|
14
|
+
| Behavior / trigger | Positive | Negative / failure | Actor / executor | Runtime / edge | Validation |
|
|
15
|
+
|---|---|---|---|---|---|
|
|
16
|
+
| First reservation candidates | Distinct private insert slugs, one user budget | Third reservation denied at limit2; old code deterministically repeats slug | Same user, real SQL observed without replacing executor | SQLite; independent PostgreSQL service connections | users `test`, `test:postgres` |
|
|
17
|
+
| Concurrent invalid approvals | Three attempts reach ordinary auth denial | Ten rejected, exactly seven typed rate limits; zero attacker sessions | One attacker/user+tenant, two instances, atomic UPSERT | SQLite and PostgreSQL18 | users `test`, `test:postgres` |
|
|
18
|
+
| Concurrent approval/exchange | Eight approvals share one session, one of eight exchanges succeeds | Seven exchanges expired, consumed row clears session ID | UUID user/tenant, owning services | PostgreSQL; SQLite existing lifecycle coverage | users `test`, `test:postgres` |
|
|
19
|
+
| Window/release | Successful attempts release their reservation | Old-window release cannot erase new-window failure; cleanup failure cannot reverse committed approval | Existing storage executor, no retry change | SQLite | users `test` |
|
|
20
|
+
| Fixture retry/reseed | Two complete fixture cycles use the same emails | No duplicate email from prior committed fixture rows | Test-owned UUIDs, explicit cleanup through public database interface | PostgreSQL; intentionally independent connections outside rollback | users `test:postgres` |
|
|
21
|
+
|
|
22
|
+
Before the repair, the candidate regression fails (one slug instead of two),
|
|
23
|
+
and the second PostgreSQL fixture cycle fails its fixed-email insert. Actual CI
|
|
24
|
+
also captured a PostgreSQL duplicate `(slug, context)` error during concurrent
|
|
25
|
+
first reservations. The fixture cleans only its own IDs in FK order, then rolls
|
|
26
|
+
back its isolated handle; it must not replace real connections with one shared
|
|
27
|
+
transaction, weaken count assertions, or disable retries.
|
|
28
|
+
|
|
29
|
+
Run documented users build, typecheck (including Svelte), test, test:postgres and
|
|
30
|
+
verify:pack gates. PostgreSQL runs use the canonical disposable-database wrapper.
|
|
31
|
+
Root knowledge checks follow all producers. No new dialect is introduced; the
|
|
32
|
+
terminal auth regression suite declares SQLite and PostgreSQL coverage.
|
|
@@ -36,7 +36,7 @@ export interface OidcTestServer {
|
|
|
36
36
|
* `(tenant_id, slug, context, _meta_type)` since smrt#2360 — keep this DDL in
|
|
37
37
|
* step with the framework schema or `save()`'s ON CONFLICT cannot bind.
|
|
38
38
|
*/
|
|
39
|
-
export declare const OIDC_USERS_TEST_SCHEMA = "\nCREATE TABLE IF NOT EXISTS \"_smrt_backfills\" (\n \"name\" TEXT PRIMARY KEY NOT NULL,\n \"applied_at\" TIMESTAMP DEFAULT current_timestamp,\n \"description\" TEXT,\n \"package_name\" TEXT\n);\n\nCREATE TABLE IF NOT EXISTS \"profile_types\" (\n \"id\" TEXT PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"_meta_type\" TEXT NOT NULL,\n \"_meta_data\" JSON,\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" TEXT,\n \"name\" TEXT DEFAULT '',\n \"description\" TEXT\n);\nCREATE UNIQUE INDEX IF NOT EXISTS \"profile_types_slug_context_meta_type_idx\" ON \"profile_types\" (\"tenant_id\", \"slug\", \"context\", \"_meta_type\");\nCREATE INDEX IF NOT EXISTS \"profile_types_meta_type_idx\" ON \"profile_types\" (\"_meta_type\");\n\nCREATE TABLE IF NOT EXISTS \"profiles\" (\n \"id\" TEXT PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"_meta_type\" TEXT NOT NULL,\n \"_meta_data\" JSON,\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" TEXT,\n \"type_id\" TEXT,\n \"email\" TEXT,\n \"email_key\" TEXT,\n \"name\" TEXT DEFAULT '',\n \"description\" TEXT\n);\nCREATE UNIQUE INDEX IF NOT EXISTS \"profiles_slug_context_meta_type_idx\" ON \"profiles\" (\"tenant_id\", \"slug\", \"context\", \"_meta_type\");\nCREATE INDEX IF NOT EXISTS \"profiles_meta_type_idx\" ON \"profiles\" (\"_meta_type\");\nCREATE INDEX IF NOT EXISTS \"profiles_email_key_idx\" ON \"profiles\" (\"email_key\");\n\nCREATE TABLE IF NOT EXISTS \"oidc_profile_email_reservations\" (\n \"id\" TEXT PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"profile_id\" TEXT NOT NULL,\n \"email_key\" TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS \"oidc_profile_email_reservations_slug_context_idx\" ON \"oidc_profile_email_reservations\" (\"slug\", \"context\");\nCREATE UNIQUE INDEX IF NOT EXISTS \"oidc_profile_email_reservations_profile_id_idx\" ON \"oidc_profile_email_reservations\" (\"profile_id\");\nCREATE UNIQUE INDEX IF NOT EXISTS \"oidc_profile_email_reservations_email_key_idx\" ON \"oidc_profile_email_reservations\" (\"email_key\");\n\nCREATE TABLE IF NOT EXISTS \"oidc_identities\" (\n \"id\" TEXT PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"profile_id\" TEXT NOT NULL,\n \"provider\" TEXT DEFAULT '',\n \"issuer\" TEXT DEFAULT '',\n \"subject\" TEXT DEFAULT '',\n \"identity_key\" TEXT,\n \"email\" TEXT DEFAULT '',\n \"last_used_at\" TIMESTAMP\n);\nCREATE UNIQUE INDEX IF NOT EXISTS \"oidc_identities_slug_context_idx\" ON \"oidc_identities\" (\"slug\", \"context\");\nCREATE UNIQUE INDEX IF NOT EXISTS \"oidc_identities_identity_key_idx\" ON \"oidc_identities\" (\"identity_key\");\n\nCREATE TABLE IF NOT EXISTS \"users\" (\n \"id\" TEXT PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"profile_id\" TEXT DEFAULT '',\n \"email\" TEXT DEFAULT '',\n \"email_key\" TEXT,\n \"status\" TEXT,\n \"last_login_at\" TIMESTAMP\n);\nCREATE UNIQUE INDEX IF NOT EXISTS \"users_slug_context_idx\" ON \"users\" (\"slug\", \"context\");\nCREATE UNIQUE INDEX IF NOT EXISTS \"users_profile_id_idx\" ON \"users\" (\"profile_id\");\nCREATE UNIQUE INDEX IF NOT EXISTS \"users_email_key_idx\" ON \"users\" (\"email_key\");\n\nCREATE TABLE IF NOT EXISTS \"sessions\" (\n \"id\" TEXT PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"user_id\" TEXT DEFAULT '',\n \"tenant_id\" TEXT,\n \"status\" TEXT,\n \"expires_at\" TIMESTAMP,\n \"user_agent\" TEXT DEFAULT '',\n \"ip_address\" TEXT DEFAULT '',\n \"last_accessed_at\" TIMESTAMP,\n \"data\" JSON DEFAULT '{}'\n);\nCREATE UNIQUE INDEX IF NOT EXISTS \"sessions_slug_context_idx\" ON \"sessions\" (\"slug\", \"context\");\n";
|
|
39
|
+
export declare const OIDC_USERS_TEST_SCHEMA = "\nCREATE TABLE IF NOT EXISTS \"_smrt_backfills\" (\n \"name\" TEXT PRIMARY KEY NOT NULL,\n \"applied_at\" TIMESTAMP DEFAULT current_timestamp,\n \"description\" TEXT,\n \"package_name\" TEXT\n);\n\nCREATE TABLE IF NOT EXISTS \"profile_types\" (\n \"id\" TEXT PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"_meta_type\" TEXT NOT NULL,\n \"_meta_data\" JSON,\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" TEXT,\n \"name\" TEXT DEFAULT '',\n \"description\" TEXT\n);\nCREATE UNIQUE INDEX IF NOT EXISTS \"profile_types_slug_context_meta_type_idx\" ON \"profile_types\" (\"tenant_id\", \"slug\", \"context\", \"_meta_type\");\nCREATE INDEX IF NOT EXISTS \"profile_types_meta_type_idx\" ON \"profile_types\" (\"_meta_type\");\n\nCREATE TABLE IF NOT EXISTS \"profiles\" (\n \"id\" TEXT PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"_meta_type\" TEXT NOT NULL,\n \"_meta_data\" JSON,\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"tenant_id\" TEXT,\n \"type_id\" TEXT,\n \"email\" TEXT,\n \"email_key\" TEXT,\n \"name\" TEXT DEFAULT '',\n \"description\" TEXT,\n \"is_public\" BOOLEAN DEFAULT FALSE\n);\nCREATE UNIQUE INDEX IF NOT EXISTS \"profiles_slug_context_meta_type_idx\" ON \"profiles\" (\"tenant_id\", \"slug\", \"context\", \"_meta_type\");\nCREATE INDEX IF NOT EXISTS \"profiles_meta_type_idx\" ON \"profiles\" (\"_meta_type\");\nCREATE INDEX IF NOT EXISTS \"profiles_email_key_idx\" ON \"profiles\" (\"email_key\");\n\nCREATE TABLE IF NOT EXISTS \"oidc_profile_email_reservations\" (\n \"id\" TEXT PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"profile_id\" TEXT NOT NULL,\n \"email_key\" TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS \"oidc_profile_email_reservations_slug_context_idx\" ON \"oidc_profile_email_reservations\" (\"slug\", \"context\");\nCREATE UNIQUE INDEX IF NOT EXISTS \"oidc_profile_email_reservations_profile_id_idx\" ON \"oidc_profile_email_reservations\" (\"profile_id\");\nCREATE UNIQUE INDEX IF NOT EXISTS \"oidc_profile_email_reservations_email_key_idx\" ON \"oidc_profile_email_reservations\" (\"email_key\");\n\nCREATE TABLE IF NOT EXISTS \"oidc_identities\" (\n \"id\" TEXT PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"profile_id\" TEXT NOT NULL,\n \"provider\" TEXT DEFAULT '',\n \"issuer\" TEXT DEFAULT '',\n \"subject\" TEXT DEFAULT '',\n \"identity_key\" TEXT,\n \"email\" TEXT DEFAULT '',\n \"last_used_at\" TIMESTAMP\n);\nCREATE UNIQUE INDEX IF NOT EXISTS \"oidc_identities_slug_context_idx\" ON \"oidc_identities\" (\"slug\", \"context\");\nCREATE UNIQUE INDEX IF NOT EXISTS \"oidc_identities_identity_key_idx\" ON \"oidc_identities\" (\"identity_key\");\n\nCREATE TABLE IF NOT EXISTS \"users\" (\n \"id\" TEXT PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"profile_id\" TEXT DEFAULT '',\n \"email\" TEXT DEFAULT '',\n \"email_key\" TEXT,\n \"status\" TEXT,\n \"last_login_at\" TIMESTAMP\n);\nCREATE UNIQUE INDEX IF NOT EXISTS \"users_slug_context_idx\" ON \"users\" (\"slug\", \"context\");\nCREATE UNIQUE INDEX IF NOT EXISTS \"users_profile_id_idx\" ON \"users\" (\"profile_id\");\nCREATE UNIQUE INDEX IF NOT EXISTS \"users_email_key_idx\" ON \"users\" (\"email_key\");\n\nCREATE TABLE IF NOT EXISTS \"sessions\" (\n \"id\" TEXT PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"user_id\" TEXT DEFAULT '',\n \"tenant_id\" TEXT,\n \"status\" TEXT,\n \"expires_at\" TIMESTAMP,\n \"user_agent\" TEXT DEFAULT '',\n \"ip_address\" TEXT DEFAULT '',\n \"last_accessed_at\" TIMESTAMP,\n \"data\" JSON DEFAULT '{}'\n);\nCREATE UNIQUE INDEX IF NOT EXISTS \"sessions_slug_context_idx\" ON \"sessions\" (\"slug\", \"context\");\n";
|
|
40
40
|
/** Mark a freshly-created, empty test schema ready for indexed OIDC reads. */
|
|
41
41
|
export declare function prepareOidcEmailKeyBackfills(db: DatabaseInterface): Promise<void>;
|
|
42
42
|
/** Close every server started by this harness (call from `afterAll`). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oidc-test-server.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/oidc-test-server.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,qCAAqC,CAAC;AAK7C,MAAM,WAAW,qBAAqB;IACpC,oCAAoC,CAAC,EAAE,OAAO,CAAC;IAC/C,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,aAAa,EAAE,KAAK,CAAC;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,eAAe,CAAA;KAAE,CAAC,CAAC;IAC1E,gBAAgB,EAAE,KAAK,CAAC;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrD;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,
|
|
1
|
+
{"version":3,"file":"oidc-test-server.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/oidc-test-server.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,qCAAqC,CAAC;AAK7C,MAAM,WAAW,qBAAqB;IACpC,oCAAoC,CAAC,EAAE,OAAO,CAAC;IAC/C,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,aAAa,EAAE,KAAK,CAAC;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,eAAe,CAAA;KAAE,CAAC,CAAC;IAC1E,gBAAgB,EAAE,KAAK,CAAC;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrD;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,g/IAyGlC,CAAC;AAEF,8EAA8E;AAC9E,wBAAsB,4BAA4B,CAChD,EAAE,EAAE,iBAAiB,GACpB,OAAO,CAAC,IAAI,CAAC,CAUf;AAaD,yEAAyE;AACzE,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAEzD;AAiBD,wBAAsB,eAAe,CACnC,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,cAAc,CAAC,CAgIzB"}
|