lecodes-cli 0.16.1 → 0.17.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.
Files changed (40) hide show
  1. package/README.md +27 -4
  2. package/dist/index.js +171621 -382
  3. package/package.json +13 -7
  4. package/runtime/sdk/compile/bundler.ts +10 -2
  5. package/runtime/sdk/compile/compileProject.ts +10 -2
  6. package/runtime/sdk/compile/detectEntry.ts +5 -2
  7. package/runtime/sdk/compile/index.ts +17 -1
  8. package/runtime/sdk/compile/serverSplit.ts +233 -0
  9. package/runtime/sdk/compile/serverTypes.ts +227 -0
  10. package/runtime/sdk/gl/AnimationClip.ts +174 -0
  11. package/runtime/sdk/gl/Animator.ts +642 -0
  12. package/runtime/sdk/gl/CharacterController.ts +6 -1
  13. package/runtime/sdk/gl/IK.ts +170 -0
  14. package/runtime/sdk/gl/Material.ts +1 -1
  15. package/runtime/sdk/gl/ModelAnimation.ts +34 -3
  16. package/runtime/sdk/gl/Node.ts +27 -1
  17. package/runtime/sdk/gl/Scene.ts +291 -289
  18. package/runtime/sdk/gl/state.ts +6 -6
  19. package/runtime/sdk/inject.ts +10 -0
  20. package/runtime/sdk/plugins/oauth.ts +61 -0
  21. package/runtime/sdk/runtime/rpc.ts +218 -0
  22. package/runtime/sdk/server/auth/appConfig.ts +12 -0
  23. package/runtime/sdk/server/auth/global.ts +80 -0
  24. package/runtime/sdk/server/auth/host.ts +318 -0
  25. package/runtime/sdk/server/auth/models.ts +83 -0
  26. package/runtime/sdk/server/auth/types.ts +50 -0
  27. package/runtime/sdk/server/channel.ts +56 -0
  28. package/runtime/sdk/server/context.ts +36 -0
  29. package/runtime/sdk/server/db/defineDb.ts +237 -0
  30. package/runtime/sdk/server/db/fields.ts +132 -0
  31. package/runtime/sdk/server/db/httpTransport.ts +93 -0
  32. package/runtime/sdk/server/db/index.ts +7 -0
  33. package/runtime/sdk/server/db/marci/query.ts +412 -0
  34. package/runtime/sdk/server/db/types.ts +202 -0
  35. package/runtime/sdk/server/errors.ts +12 -0
  36. package/runtime/sdk/server/host.ts +74 -0
  37. package/runtime/sdk/server/inject.ts +13 -0
  38. package/runtime/sdk/server/runtime.ts +133 -0
  39. package/runtime/sdk/server/validate.ts +87 -0
  40. package/runtime/sdk-types.json +1 -1
@@ -34,6 +34,9 @@ export { localStorage } from "./runtime/storage"
34
34
  export { device, type HapticStyle, type MotionOptions } from "./runtime/device"
35
35
  export { Input } from "./runtime/input"
36
36
  export { WebSocket } from "./runtime/net"
37
+ // App-backend transport: the compiler-generated stubs of `*.server.ts` modules call these; user code
38
+ // only ever imports its server functions (docs/backend-plan.md §4).
39
+ export { __rpc, __channel, __serverOnly, RpcError, type ClientChannel } from "./runtime/rpc"
37
40
  export { AudioPlayer, VideoPlayer } from "./runtime/media"
38
41
  export { openFilePicker } from "./runtime/files"
39
42
  export { share } from "./runtime/share"
@@ -115,6 +118,7 @@ export { QRScanner } from "./plugins/qr"
115
118
  export { CameraView, type CameraFacing } from "./plugins/camera"
116
119
  export { Geolocation, type GeoPosition, type GeoOptions, type GeoWatch } from "./plugins/geolocation"
117
120
  export { Push, type PushPayload, type PushStatus, type PushRegisterOptions, type PushEvent } from "./plugins/push"
121
+ export { OAuth, type OAuthCredential, type OAuthProviderName } from "./plugins/oauth"
118
122
  export { Service } from "./plugins/service"
119
123
 
120
124
  // ---- scenes as data (.scene.ts files — see src/scene/defineScene.ts) ----
@@ -139,6 +143,12 @@ export { Physics } from "./gl/Physics"
139
143
  export { Trigger } from "./gl/Trigger"
140
144
  export { CharacterController } from "./gl/CharacterController"
141
145
  export { ModelAnimation } from "./gl/ModelAnimation"
146
+ export { AnimationClip } from "./gl/AnimationClip"
147
+ export type { ClipDef, ClipTrackDef, ClipKey } from "./gl/AnimationClip"
148
+ export { Animator } from "./gl/Animator"
149
+ export type { AnimatorPlayOptions } from "./gl/Animator"
150
+ export { IK } from "./gl/IK"
151
+ export type { IKTwoBone, IKLookAt } from "./gl/IK"
142
152
  export { Light } from "./gl/Light"
143
153
  export { Material } from "./gl/Material"
144
154
  export { Geometry } from "./gl/Geometry"
@@ -0,0 +1,61 @@
1
+ // OAuth — native provider sign-in as a typed service plugin (docs/backend-plan.md §3.4). The
2
+ // browser broker (`auth.oauth.url()` on the server + `openURL` in the app) works everywhere; on
3
+ // hosts that ship a provider SDK (Yandex LoginSDK, VK ID, Sign in with Apple, Google Sign-In) the
4
+ // user shouldn't leave the app — the host registers an "oauth" service that runs the native sheet
5
+ // and hands back the provider's CREDENTIAL. The app then calls its own endpoint, which does
6
+ // `auth.oauth.signIn(provider, credential)`: the server verifies the credential with the provider
7
+ // (audience = the platform's app ids) and binds the device session. No token is ever trusted
8
+ // client-side; the credential is opaque to the app.
9
+ //
10
+ // Host-OPTIONAL — gate on `OAuth.isSupported(provider)`; fall back to the browser broker otherwise.
11
+ //
12
+ // Service contract ("oauth"): call `signIn(provider) → OAuthCredential` (rejects "cancelled" /
13
+ // "unavailable"), call `providers() → string[]` (which providers this host has SDKs for).
14
+
15
+ import { ServiceClient } from "../runtime/service"
16
+
17
+ export type OAuthProviderName = "google" | "apple" | "yandex" | "vk"
18
+
19
+ /** What a native provider SDK yields — pass it verbatim to your endpoint / `auth.oauth.signIn`. */
20
+ export type OAuthCredential = {
21
+ provider: OAuthProviderName
22
+ /** OAuth access token (Yandex, VK ID). */
23
+ accessToken?: string
24
+ /** OpenID identity token (Google, Apple). */
25
+ idToken?: string
26
+ /** Apple only sends the name once, on the first sign-in — forward it so the server can store it. */
27
+ name?: string
28
+ email?: string
29
+ }
30
+
31
+ /**
32
+ * Native provider sign-in. `OAuth.isSupported` reports whether this host registered the service;
33
+ * `OAuth.providers()` which providers it has SDKs for. Otherwise open `auth.oauth.url(provider)` (server)
34
+ * with `openURL` — the browser broker.
35
+ */
36
+ // PURE IIFE so an app that never uses it tree-shakes the plugin away (see Geolocation).
37
+ export const OAuth: {
38
+ /** Whether this host registered an "oauth" service. */
39
+ readonly isSupported: boolean
40
+ /** The providers this host can sign in with natively. */
41
+ providers(): Promise<OAuthProviderName[]>
42
+ /** Run the provider's native sheet; resolves with the credential for `auth.oauth.signIn`. Rejects "cancelled" / "unavailable". */
43
+ signIn(provider: OAuthProviderName): Promise<OAuthCredential>
44
+ } = /*#__PURE__*/ (() => {
45
+ let client: ServiceClient | null = null
46
+ const service = (): ServiceClient => client ??= new ServiceClient("oauth")
47
+ const assertSupported = (): void => {
48
+ if (!ServiceClient.isSupported("oauth")) {
49
+ throw new Error("OAuth isn't available on this host — it needs a registered \"oauth\" service; open auth.oauth.url(provider) in the browser instead.")
50
+ }
51
+ }
52
+ return {
53
+ get isSupported() { return ServiceClient.isSupported("oauth") },
54
+ providers: async () => { assertSupported(); const p = await service().call("providers"); return Array.isArray(p) ? p : [] },
55
+ signIn: async (provider) => {
56
+ assertSupported()
57
+ const r = await service().call("signIn", provider)
58
+ return { provider, ...(r ?? {}) }
59
+ },
60
+ }
61
+ })()
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Client transport for the app backend (docs/backend-plan.md §3.2 / §3.3). The compiler replaces
3
+ * every `*.server.ts` module in the client bundle with a stub whose exports are built by the three
4
+ * globals below — user code never calls them; it imports the server function and calls it.
5
+ *
6
+ * export const listNotes = __rpc("<serverUrl>", "notes.server.ts#listNotes")
7
+ * export const chat = __channel("<serverUrl>", "chat.server.ts#chat")
8
+ * export const db = __serverOnly("schema.server.ts#db")
9
+ *
10
+ * Wire (implemented by the runner):
11
+ * POST <serverUrl>/api/<id> body {"args":[…]}, `authorization: Bearer <session>` when known
12
+ * → 200 {"ok":true,"result":…,"session"?:"…"} | {"ok":false,"status":n,"message":"…","session"?:"…"}
13
+ * WS <serverUrl>/ws frames: → {t:"hello",session} {t:"sub",id,ch,topic} {t:"unsub",id}
14
+ * ← {t:"session",session} {t:"ok",id} {t:"err",id,status,message} {t:"ev",ch,topic,event,data}
15
+ * (`ev` is addressed by channel + topic, not by subscription id, so the server can fan one publish out
16
+ * to every socket of a topic natively; the client delivers it to each matching subscription.)
17
+ *
18
+ * The session token is transport-owned (cookie-like): stored under `lecodes.session:<serverUrl>` in
19
+ * localStorage, sent on every call and on the socket, replaced whenever a response carries `session`.
20
+ * User code never sees it — "am I logged in" is an endpoint (`me()`), see the plan.
21
+ */
22
+
23
+ import { fetch } from "./fetch"
24
+ import { localStorage } from "./storage"
25
+ import { WebSocket } from "./net"
26
+
27
+ /** Rejection value of a failed endpoint call: `status` mirrors the server's `ApiError`
28
+ * (0 = the request itself failed: no network / no server). */
29
+ export class RpcError extends Error {
30
+ readonly status: number
31
+ constructor(status: number, message: string) {
32
+ super(message)
33
+ this.name = "RpcError"
34
+ this.status = status
35
+ }
36
+ }
37
+
38
+ // ───────────────────────────── session token ─────────────────────────────
39
+
40
+ const sessionKey = (serverUrl: string) => `lecodes.session:${serverUrl}`
41
+ const getSession = (serverUrl: string): string | null => {
42
+ try { return localStorage.getItem(sessionKey(serverUrl)) } catch { return null }
43
+ }
44
+ const setSession = (serverUrl: string, token: string | null) => {
45
+ try {
46
+ if (token) localStorage.setItem(sessionKey(serverUrl), token)
47
+ else localStorage.removeItem(sessionKey(serverUrl))
48
+ } catch { /* storage-less host: the session lives for this run only */ }
49
+ sockets.get(serverUrl)?.onSessionChanged(token)
50
+ }
51
+
52
+ // ───────────────────────────── endpoints ─────────────────────────────
53
+
54
+ const noServer = (id: string) => new RpcError(0, `Endpoint ${id}: the project has no deployed server (compile with serverUrl)`)
55
+
56
+ /** Build the client proxy of endpoint `id`. Positional args travel as a JSON array. */
57
+ export const __rpc = (serverUrl: string, id: string) => {
58
+ const call = async (...args: unknown[]): Promise<any> => {
59
+ if (!serverUrl) throw noServer(id)
60
+ const headers: Record<string, string> = { "content-type": "application/json" }
61
+ const session = getSession(serverUrl)
62
+ if (session) headers.authorization = `Bearer ${session}`
63
+ let res
64
+ try {
65
+ res = await fetch(`${serverUrl}/api/${encodeURIComponent(id)}`, { method: "POST", headers, body: JSON.stringify({ args }) })
66
+ } catch (e) {
67
+ throw new RpcError(0, `Endpoint ${id}: request failed (${(e as Error)?.message ?? e})`)
68
+ }
69
+ let body: any
70
+ try { body = res.json() } catch { body = null }
71
+ res.dispose()
72
+ if (body && typeof body === "object" && "session" in body) setSession(serverUrl, body.session ?? null)
73
+ if (body && body.ok === true) return body.result
74
+ if (body && body.ok === false) throw new RpcError(body.status ?? res.status, body.message ?? "")
75
+ throw new RpcError(res.status, `Endpoint ${id}: unexpected response (HTTP ${res.status})`)
76
+ }
77
+ return call
78
+ }
79
+
80
+ /** A server-only export (db, models, helpers) referenced from client code: any use throws. */
81
+ export const __serverOnly = (id: string): any =>
82
+ new Proxy(function () {} as any, {
83
+ get: (_t, prop) => { if (prop === "then") return undefined; throw new RpcError(0, `${id} is server-only — it can't be used in client code`) },
84
+ apply: () => { throw new RpcError(0, `${id} is server-only — it can't be used in client code`) },
85
+ })
86
+
87
+ // ───────────────────────────── channels ─────────────────────────────
88
+
89
+ type Handlers = Record<string, ((payload: any) => void) | undefined> & {
90
+ /** The socket dropped and came back: subscriptions were re-sent, but events in between are lost — refetch. */
91
+ reconnect?: () => void
92
+ /** The subscription was refused (`onJoin` threw) or the socket can't connect. */
93
+ error?: (e: RpcError) => void
94
+ }
95
+ type Sub = { id: number, ch: string, topic: string, handlers: Handlers, ready: boolean }
96
+
97
+ const BACKOFF_MS = [1000, 2000, 5000, 10000, 30000]
98
+
99
+ /** One multiplexed socket per server URL, shared by every channel of the app. */
100
+ class ChannelSocket {
101
+ private ws: WebSocket | null = null
102
+ private open = false
103
+ private subs = new Map<number, Sub>()
104
+ private nextId = 1
105
+ private attempts = 0
106
+ private timer: ReturnType<typeof setTimeout> | null = null
107
+ private closedByUs = false
108
+ private everOpened = false
109
+ private serverUrl: string
110
+
111
+ constructor(serverUrl: string) { this.serverUrl = serverUrl }
112
+
113
+ subscribe(ch: string, topic: string, handlers: Handlers): { close(): void } {
114
+ const sub: Sub = { id: this.nextId++, ch, topic, handlers, ready: false }
115
+ this.subs.set(sub.id, sub)
116
+ if (this.open) this.send({ t: "sub", id: sub.id, ch, topic })
117
+ else this.connect()
118
+ return {
119
+ close: () => {
120
+ if (!this.subs.delete(sub.id)) return
121
+ if (this.open) this.send({ t: "unsub", id: sub.id })
122
+ if (this.subs.size === 0) this.shutdown()
123
+ },
124
+ }
125
+ }
126
+
127
+ onSessionChanged(token: string | null) {
128
+ if (this.open) this.send({ t: "hello", session: token })
129
+ }
130
+
131
+ private connect() {
132
+ if (this.ws || this.timer !== null) return
133
+ this.closedByUs = false
134
+ const url = this.serverUrl.replace(/^http/, "ws") + "/ws"
135
+ const session = getSession(this.serverUrl)
136
+ const ws = new WebSocket(url, session ? { authorization: `Bearer ${session}` } : undefined)
137
+ this.ws = ws
138
+ ws.addEventListener("open", () => {
139
+ this.open = true
140
+ this.attempts = 0
141
+ this.send({ t: "hello", session: getSession(this.serverUrl) })
142
+ const reconnected = this.everOpened
143
+ this.everOpened = true
144
+ for (const sub of this.subs.values()) {
145
+ sub.ready = false
146
+ this.send({ t: "sub", id: sub.id, ch: sub.ch, topic: sub.topic })
147
+ if (reconnected) sub.handlers.reconnect?.()
148
+ }
149
+ })
150
+ ws.addEventListener("message", (data) => {
151
+ if (typeof data !== "string") return
152
+ let msg: any
153
+ try { msg = JSON.parse(data) } catch { return }
154
+ this.onFrame(msg)
155
+ })
156
+ const onDown = () => {
157
+ if (this.ws !== ws) return
158
+ this.ws = null
159
+ this.open = false
160
+ if (this.closedByUs || this.subs.size === 0) return
161
+ const delay = BACKOFF_MS[Math.min(this.attempts++, BACKOFF_MS.length - 1)]
162
+ this.timer = setTimeout(() => { this.timer = null; this.connect() }, delay)
163
+ }
164
+ ws.addEventListener("close", onDown)
165
+ ws.addEventListener("error", onDown)
166
+ }
167
+
168
+ private onFrame(msg: any) {
169
+ switch (msg?.t) {
170
+ case "session": setSession(this.serverUrl, msg.session ?? null); break
171
+ case "ok": { const s = this.subs.get(msg.id); if (s) s.ready = true; break }
172
+ case "err": {
173
+ const s = this.subs.get(msg.id)
174
+ if (!s) break
175
+ this.subs.delete(msg.id)
176
+ s.handlers.error?.(new RpcError(msg.status ?? 0, msg.message ?? "subscription refused"))
177
+ break
178
+ }
179
+ case "ev": {
180
+ for (const s of this.subs.values()) {
181
+ if (s.ch === msg.ch && s.topic === msg.topic) s.handlers[msg.event]?.(msg.data)
182
+ }
183
+ break
184
+ }
185
+ }
186
+ }
187
+
188
+ private send(frame: unknown) { this.ws?.send(JSON.stringify(frame)) }
189
+
190
+ private shutdown() {
191
+ this.closedByUs = true
192
+ if (this.timer !== null) { clearTimeout(this.timer); this.timer = null }
193
+ this.ws?.close()
194
+ this.ws = null
195
+ this.open = false
196
+ sockets.delete(this.serverUrl)
197
+ }
198
+ }
199
+
200
+ const sockets = new Map<string, ChannelSocket>()
201
+ const socketFor = (serverUrl: string) => {
202
+ let s = sockets.get(serverUrl)
203
+ if (!s) { s = new ChannelSocket(serverUrl); sockets.set(serverUrl, s) }
204
+ return s
205
+ }
206
+
207
+ export interface ClientChannel<E extends Record<string, unknown> = Record<string, unknown>> {
208
+ /** Subscribe to `topic`; handlers are keyed by event name (+ `reconnect`/`error`). Returns `{ close }`. */
209
+ subscribe(topic: string, handlers: { [K in keyof E]?: (payload: E[K]) => void } & { reconnect?: () => void, error?: (e: RpcError) => void }): { close(): void }
210
+ }
211
+
212
+ /** Build the client proxy of channel `id`. */
213
+ export const __channel = (serverUrl: string, id: string): ClientChannel<any> => ({
214
+ subscribe(topic, handlers) {
215
+ if (!serverUrl) throw noServer(id)
216
+ return socketFor(serverUrl).subscribe(id, topic, handlers as Handlers)
217
+ },
218
+ })
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The project's `app.json` subset the server runtime reads (name, auth providers).
3
+ *
4
+ * This module is a placeholder the COMPILER REPLACES: `compileServerBundle({ app })` puts a module with the
5
+ * real values at this same path in chisel's file map (project files win over SDK files), so every SDK module
6
+ * that imports `app` gets the project's config as a plain dependency — no globals, no evaluation-order games.
7
+ * Outside a compiled bundle (tests, tooling) `globalThis.__lecodesApp` can stand in.
8
+ */
9
+ export type AuthProvider = "email" | "google" | "apple" | "yandex" | "vk"
10
+ export type AppConfig = { name?: string, auth?: { providers?: AuthProvider[] } }
11
+
12
+ export const app: AppConfig = {}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The `auth` server global (docs/backend-plan.md §3.4) — the BUNDLE side. State (`auth.user`,
3
+ * `auth.session`) is read from the current request, which the host resolved before the endpoint ran;
4
+ * operations (`auth.email.*`, `auth.oauth.url`, `signIn/signOut`, `sessions`) forward to the host's
5
+ * `AuthOps` on `globalThis.__lecodesAuth` (a seam: the compiled bundle carries its own SDK copy). Nothing
6
+ * here touches the db or a secret; `auth.model.*` are pure schema factories.
7
+ */
8
+
9
+ import { currentRequest, type RequestContext } from "../context"
10
+ import type { Model, Query } from "../db/types"
11
+ import { ApiError } from "../errors"
12
+ import { authSessionModel, authUserModel, type AuthProvider, type SessionFields, type UserFields } from "./models"
13
+ import type { AuthOps, AuthState, AuthUser, OAuthCredential } from "./types"
14
+
15
+ const seam = globalThis as unknown as { __lecodesAuth?: AuthOps }
16
+ const ops = (): AuthOps => {
17
+ const o = seam.__lecodesAuth
18
+ if (!o) throw new Error("auth: not available — the host did not install the auth runtime (globalThis.__lecodesAuth)")
19
+ return o
20
+ }
21
+ const ctx = (): RequestContext => {
22
+ const c = currentRequest()
23
+ if (!c) throw new Error("auth: no current request — auth.* is only available while handling an endpoint or a channel join")
24
+ return c
25
+ }
26
+ const state = (): AuthState => {
27
+ const c = ctx()
28
+ const s = c.auth as AuthState | undefined
29
+ if (!s) throw new Error("auth: this project has no database — sessions need a defineDb() (auth.model.* are optional, Session is added for you)")
30
+ return s
31
+ }
32
+
33
+ /** The schema of the two built-in models, for typing `auth.sessions` (developer extras aren't visible here). */
34
+ type AuthSchema = { User: Model<UserFields>, Session: Model<SessionFields> }
35
+
36
+ export const auth = {
37
+ /** The signed-in user (built-in fields; custom ones via `db.user`) or `null`. Request-scoped. */
38
+ get user(): AuthUser | null { return state().user },
39
+ /** The device session — exists from the first request, before any login. */
40
+ get session(): { readonly id: number } { return { id: state().sessionId } },
41
+ /**
42
+ * The signed-in user, or `throw new ApiError(401)`; with `match`, every given field must equal the user's
43
+ * (`{ role: "admin" }`, `{ plan: "pro" }`) or it's a 403.
44
+ */
45
+ requireUser(match?: Record<string, unknown>): AuthUser {
46
+ const u = state().user
47
+ if (!u) throw new ApiError(401, "Sign in required")
48
+ if (match) for (const [k, v] of Object.entries(match)) if ((u as Record<string, unknown>)[k] !== v) throw new ApiError(403, "Forbidden")
49
+ return u
50
+ },
51
+ email: {
52
+ /** Emails a 6-digit code to `email` (always resolves — no "email exists" oracle; rate-limited). Needs `"email"` in app.json providers. */
53
+ sendCode: (email: string): Promise<void> => ops().email.sendCode(ctx(), email),
54
+ /** The only email sign-in: checks the code sent to this device session, finds/creates the `User`, binds the session. */
55
+ verify: (email: string, code: string): Promise<AuthUser> => ops().email.verify(ctx(), email, code),
56
+ },
57
+ oauth: {
58
+ /** A one-time sign-in URL for the platform's OAuth broker (`"google" | "apple" | "yandex" | "vk"`), bound to this device session — open it in the browser. */
59
+ url: (provider: Exclude<AuthProvider, "email">): Promise<string> => ops().oauth.url(ctx(), provider),
60
+ /**
61
+ * Sign in with a credential a NATIVE provider SDK produced in the app (client `OAuth.signIn(provider)`):
62
+ * the platform verifies it with the provider, finds/links/creates the `User`, binds this session. The
63
+ * in-app counterpart of the browser broker — same accounts either way.
64
+ */
65
+ signIn: (provider: Exclude<AuthProvider, "email">, credential: OAuthCredential): Promise<AuthUser> => ops().oauth.signIn(ctx(), provider, credential),
66
+ },
67
+ /** Escape hatch for developer-verified identities (Telegram initData, SSO): binds this session to `userId`. */
68
+ signIn: (userId: number): Promise<AuthUser> => ops().signIn(ctx(), userId),
69
+ /** Unbinds the user; the device stays a guest with the same `session.id` (token rotated). */
70
+ signOut: (): Promise<void> => ops().signOut(ctx()),
71
+ /** The current user's sessions ("my devices"): a `db.session` query — `await`, `.select()`, `.where(...).updateMany({ revoked: true })`. */
72
+ get sessions(): Query<AuthSchema, "Session"> { return ops().sessions(ctx()) },
73
+ /** Schema-time factories — see ./models.ts. */
74
+ model: {
75
+ user: authUserModel,
76
+ session: authSessionModel,
77
+ },
78
+ }
79
+
80
+ export type { AuthUser }