@happyvertical/smrt-users 0.38.10 → 0.38.12

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 CHANGED
@@ -151,6 +151,47 @@ await destroySessionCookie(event, { db });
151
151
  await switchSessionTenant(event, tenantId, { db });
152
152
  ```
153
153
 
154
+ ## Mobile `/api/mobile` Handlers (ADR 0001 Phase 3.5, #1748)
155
+
156
+ `createMobileAuthHandlers(options)` (from `/sveltekit`) returns the mountable
157
+ server side of the KMP mobile contract: `authStart` (`POST auth/start`,
158
+ server-brokered PKCE via `OidcLoginService`), `authComplete`
159
+ (`POST auth/complete`, code + echoed `state`/`codeVerifier` → bearer
160
+ session), `session.GET`/`session.DELETE` (bootstrap/logout), and
161
+ `guard`/`withSession` — the bearer middleware for app-owned mobile routes.
162
+ Core logic lives in `MobileAuthService` (framework-agnostic; exported from
163
+ the package root).
164
+
165
+ - **Bearer = session id** (same convention as `TerminalAuthService`); 401
166
+ bodies are `{ error, code }` and drive the mobile client's re-auth flow.
167
+ - **Stateless handshake**: the OAuth `state` is an HMAC-signed token
168
+ (secret: `stateSecret` ?? provider `clientSecret`) carrying
169
+ nonce/provider/createdAt — full ID-token nonce verification with no
170
+ server-side pending state. The `codeVerifier` never enters a URL: it is
171
+ client-held per the frozen contract.
172
+ - **Wire DTOs** come from `@happyvertical/smrt-mobile-contract`
173
+ (`MobileAuthStartRequest` etc.) — one owning package for the Kotlin,
174
+ Swift, and TypeScript shapes (compile-checked descriptors + parity test).
175
+ - **Tenant options** honor `Role.inheritsToDescendants` (#1867): direct
176
+ ACTIVE memberships plus descendants of flagged memberships (nearest
177
+ flagged ancestor labels the option; any direct row pins; inactive direct
178
+ rows exclude). Session binding defaults to the first DIRECT tenant —
179
+ override with `resolveTenantId`.
180
+ - **Hooks**: `resolveUser` (invite-gating; default provisions via
181
+ `getOrCreateFromOidc`), `resolveTenantId`, `buildExtras` (bootstrap
182
+ `extras`; model JSON must use `toPublicJSON({ permissions })` — #1822).
183
+ - **Guard** wraps `withSessionPermissionContext`, so
184
+ `assertOperationPermission`, tenancy context, and Postgres RLS all see the
185
+ bearer caller; `OperationPermissionError` maps to 403 with a
186
+ machine-readable `reason`.
187
+ - **Uploads**: `resolveMobileUploadDedupKey` + the documented contract in
188
+ `docs/content/architecture/mobile-upload-contract.md` (`clientCaptureId`
189
+ field, `Idempotency-Key` header fallback); domain ingestion stays
190
+ app-side. Framework-model writes ride `sync/apply`, not this path.
191
+ - Configure `redirectUris` in production — RFC 8252 scheme rules always
192
+ apply, but the allow list is the defense against redirecting authorization
193
+ responses to attacker-controlled URIs.
194
+
154
195
  ## Security (S5 #1400)
155
196
 
156
197
  - **Generated REST/MCP surface is READ-ONLY for every RBAC/identity model.**