lecodes-cli 0.19.0 → 0.19.2
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/dist/index.js +730 -235
- package/package.json +14 -13
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk-types.json +1 -1
- package/src/api.ts +302 -0
- package/src/browserAuth.ts +87 -0
- package/src/cmgenTool.ts +104 -0
- package/src/commands/app.ts +891 -0
- package/src/commands/appAndroid.ts +603 -0
- package/src/commands/appDesktop.ts +326 -0
- package/src/commands/appDesktopMac.ts +470 -0
- package/src/commands/appIcon.ts +187 -0
- package/src/commands/appShared.ts +448 -0
- package/src/commands/appTemplates.ts +883 -0
- package/src/commands/appTemplatesAndroid.ts +599 -0
- package/src/commands/appTemplatesGradlew.ts +9 -0
- package/src/commands/assets.ts +28 -0
- package/src/commands/clone.ts +60 -0
- package/src/commands/compile.ts +253 -0
- package/src/commands/create.ts +59 -0
- package/src/commands/design.ts +863 -0
- package/src/commands/designTemplates.ts +11 -0
- package/src/commands/desktop.ts +122 -0
- package/src/commands/dev.ts +214 -0
- package/src/commands/diff.ts +59 -0
- package/src/commands/init.ts +191 -0
- package/src/commands/install.ts +198 -0
- package/src/commands/lightmap.ts +290 -0
- package/src/commands/link.ts +147 -0
- package/src/commands/login.ts +24 -0
- package/src/commands/navmesh.ts +225 -0
- package/src/commands/pn.ts +305 -0
- package/src/commands/projectTemplates.ts +129 -0
- package/src/commands/pull.ts +109 -0
- package/src/commands/push.ts +173 -0
- package/src/commands/render.ts +414 -0
- package/src/commands/scene.ts +148 -0
- package/src/commands/shaders.ts +157 -0
- package/src/commands/status.ts +29 -0
- package/src/commands/test.ts +370 -0
- package/src/commands/thumbs.ts +178 -0
- package/src/commands/types.ts +87 -0
- package/src/commands/update.ts +190 -0
- package/src/compile/assetIcons.ts +214 -0
- package/src/compile/collect.ts +46 -0
- package/src/compile/collectLocal.ts +37 -0
- package/src/compile/designCompile.ts +104 -0
- package/src/compile/fonts.ts +156 -0
- package/src/compile/headlessBundle.ts +129 -0
- package/src/compile/nativeStack.ts +21 -0
- package/src/compile/projectCompile.ts +109 -0
- package/src/compile/sceneCompile.ts +113 -0
- package/src/compile/screenEntry.ts +127 -0
- package/src/compile/shaders.ts +245 -0
- package/src/config.ts +42 -0
- package/src/designMeta.ts +35 -0
- package/src/desktopRenderer.ts +532 -0
- package/src/desktopScript.ts +276 -0
- package/src/dev/clientTemplates.ts +160 -0
- package/src/dev/devServer.ts +292 -0
- package/src/dev/wsServer.ts +144 -0
- package/src/distRoot.ts +20 -0
- package/src/ignore.ts +163 -0
- package/src/index.ts +491 -0
- package/src/lecodes-3d-editor.d.ts +41 -0
- package/src/lecodes-assets.d.ts +7 -0
- package/src/lecodes-design.d.ts +191 -0
- package/src/lecodes-renderer.d.ts +131 -0
- package/src/localFiles.ts +144 -0
- package/src/manifest.ts +46 -0
- package/src/matcTool.ts +137 -0
- package/src/peers.ts +40 -0
- package/src/project.ts +61 -0
- package/src/projectEnv.ts +94 -0
- package/src/qrcode-terminal.d.ts +9 -0
- package/src/releases.ts +125 -0
- package/src/serverDiff.ts +78 -0
- package/src/textDiff.ts +103 -0
- package/src/types.ts +0 -0
- package/src/util.ts +146 -0
- package/runtime/sdk/animate/animate.ts +0 -238
- package/runtime/sdk/animate/bezier.ts +0 -138
- package/runtime/sdk/animate/easings.ts +0 -126
- package/runtime/sdk/canvas/Canvas.ts +0 -305
- package/runtime/sdk/compile/aspectMacro.ts +0 -86
- package/runtime/sdk/compile/assetIconMacro.ts +0 -384
- package/runtime/sdk/compile/assetMacro.ts +0 -146
- package/runtime/sdk/compile/assetName.ts +0 -50
- package/runtime/sdk/compile/bundler.ts +0 -287
- package/runtime/sdk/compile/compileProject.ts +0 -129
- package/runtime/sdk/compile/detectEntry.ts +0 -128
- package/runtime/sdk/compile/fontMacro.ts +0 -459
- package/runtime/sdk/compile/fontRegistry.ts +0 -78
- package/runtime/sdk/compile/header.ts +0 -67
- package/runtime/sdk/compile/index.ts +0 -100
- package/runtime/sdk/compile/libraryImports.ts +0 -52
- package/runtime/sdk/compile/liteMaterial.ts +0 -247
- package/runtime/sdk/compile/sceneEditor.ts +0 -88
- package/runtime/sdk/compile/serverSplit.ts +0 -233
- package/runtime/sdk/compile/serverTypes.ts +0 -227
- package/runtime/sdk/compile/sfnt.ts +0 -98
- package/runtime/sdk/compile/shaderSchema.ts +0 -202
- package/runtime/sdk/compile/shaderTargets.ts +0 -81
- package/runtime/sdk/compile/sourcemap.ts +0 -25
- package/runtime/sdk/core/Aspect.ts +0 -568
- package/runtime/sdk/core/InspectorUI.ts +0 -212
- package/runtime/sdk/core/StateMachine.ts +0 -308
- package/runtime/sdk/core/color.ts +0 -66
- package/runtime/sdk/core/compWrite.ts +0 -42
- package/runtime/sdk/core/events.ts +0 -38
- package/runtime/sdk/core/fields.ts +0 -120
- package/runtime/sdk/core/registry.ts +0 -23
- package/runtime/sdk/core/signals.ts +0 -277
- package/runtime/sdk/core/time.ts +0 -84
- package/runtime/sdk/g2/Camera2D.ts +0 -40
- package/runtime/sdk/g2/CharacterController2D.ts +0 -276
- package/runtime/sdk/g2/Node2D.ts +0 -267
- package/runtime/sdk/g2/OneWay2D.ts +0 -66
- package/runtime/sdk/g2/Physics2D.ts +0 -346
- package/runtime/sdk/g2/Scene2D.ts +0 -209
- package/runtime/sdk/g2/Shape2D.ts +0 -259
- package/runtime/sdk/g2/Sprite.ts +0 -89
- package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
- package/runtime/sdk/g2/SpriteSheet.ts +0 -166
- package/runtime/sdk/g2/Texture2D.ts +0 -47
- package/runtime/sdk/g2/Tilemap.ts +0 -41
- package/runtime/sdk/g2/Tileset.ts +0 -71
- package/runtime/sdk/g2/Trigger2D.ts +0 -77
- package/runtime/sdk/g2/autotile.ts +0 -433
- package/runtime/sdk/g2/cells.ts +0 -91
- package/runtime/sdk/g2/defineScene2d.ts +0 -381
- package/runtime/sdk/g2/groups2d.ts +0 -106
- package/runtime/sdk/g2/loop.ts +0 -50
- package/runtime/sdk/g2/scenarios2d.ts +0 -69
- package/runtime/sdk/g2/touch.ts +0 -83
- package/runtime/sdk/gl/Camera.ts +0 -160
- package/runtime/sdk/gl/CameraPlace.ts +0 -52
- package/runtime/sdk/gl/CharacterController.ts +0 -238
- package/runtime/sdk/gl/Gearbox.ts +0 -212
- package/runtime/sdk/gl/Geometry.ts +0 -279
- package/runtime/sdk/gl/IK.ts +0 -193
- package/runtime/sdk/gl/InstancedMesh.ts +0 -132
- package/runtime/sdk/gl/Light.ts +0 -107
- package/runtime/sdk/gl/Lightmap.ts +0 -256
- package/runtime/sdk/gl/Material.ts +0 -260
- package/runtime/sdk/gl/Mesh.ts +0 -83
- package/runtime/sdk/gl/Model.ts +0 -124
- package/runtime/sdk/gl/NavAgent.ts +0 -337
- package/runtime/sdk/gl/NavMesh.ts +0 -397
- package/runtime/sdk/gl/Node.ts +0 -350
- package/runtime/sdk/gl/Noise.ts +0 -30
- package/runtime/sdk/gl/Particles.ts +0 -676
- package/runtime/sdk/gl/Physics.ts +0 -222
- package/runtime/sdk/gl/Plane.ts +0 -53
- package/runtime/sdk/gl/Ragdoll.ts +0 -270
- package/runtime/sdk/gl/Ray.ts +0 -16
- package/runtime/sdk/gl/Scene.ts +0 -486
- package/runtime/sdk/gl/Shape.ts +0 -409
- package/runtime/sdk/gl/Terrain.ts +0 -1092
- package/runtime/sdk/gl/Texture.ts +0 -63
- package/runtime/sdk/gl/Trigger.ts +0 -45
- package/runtime/sdk/gl/Vehicle.ts +0 -473
- package/runtime/sdk/gl/Wheel.ts +0 -240
- package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
- package/runtime/sdk/gl/animation/Animator.ts +0 -120
- package/runtime/sdk/gl/animation/Layer.ts +0 -31
- package/runtime/sdk/gl/animation/Loop.ts +0 -30
- package/runtime/sdk/gl/animation/Playback.ts +0 -44
- package/runtime/sdk/gl/animation/core.ts +0 -336
- package/runtime/sdk/gl/controls.ts +0 -95
- package/runtime/sdk/gl/physicsEvents.ts +0 -20
- package/runtime/sdk/gl/scenarios.ts +0 -291
- package/runtime/sdk/gl/state.ts +0 -6
- package/runtime/sdk/gl/terrainMesh.ts +0 -219
- package/runtime/sdk/gl/touch.ts +0 -68
- package/runtime/sdk/inject.ts +0 -197
- package/runtime/sdk/math/Mathf.ts +0 -118
- package/runtime/sdk/math/mat4.ts +0 -278
- package/runtime/sdk/math/quat.ts +0 -232
- package/runtime/sdk/math/vec.ts +0 -255
- package/runtime/sdk/net/codec.ts +0 -119
- package/runtime/sdk/net/core.ts +0 -384
- package/runtime/sdk/net/index.ts +0 -181
- package/runtime/sdk/net/replication.ts +0 -622
- package/runtime/sdk/plugins/camera.ts +0 -81
- package/runtime/sdk/plugins/geolocation.ts +0 -123
- package/runtime/sdk/plugins/oauth.ts +0 -61
- package/runtime/sdk/plugins/permission.ts +0 -7
- package/runtime/sdk/plugins/push.ts +0 -132
- package/runtime/sdk/plugins/qr.ts +0 -73
- package/runtime/sdk/plugins/service.ts +0 -47
- package/runtime/sdk/runtime/app.ts +0 -101
- package/runtime/sdk/runtime/appEvents.ts +0 -54
- package/runtime/sdk/runtime/channel.ts +0 -61
- package/runtime/sdk/runtime/clipboard.ts +0 -20
- package/runtime/sdk/runtime/datetime.ts +0 -329
- package/runtime/sdk/runtime/device.ts +0 -293
- package/runtime/sdk/runtime/fetch.ts +0 -77
- package/runtime/sdk/runtime/files.ts +0 -23
- package/runtime/sdk/runtime/input.ts +0 -175
- package/runtime/sdk/runtime/media.ts +0 -111
- package/runtime/sdk/runtime/misc.ts +0 -16
- package/runtime/sdk/runtime/net.ts +0 -36
- package/runtime/sdk/runtime/rpc.ts +0 -218
- package/runtime/sdk/runtime/service.ts +0 -83
- package/runtime/sdk/runtime/share.ts +0 -9
- package/runtime/sdk/runtime/storage.ts +0 -13
- package/runtime/sdk/runtime/touch.ts +0 -76
- package/runtime/sdk/scene/defineScene.ts +0 -1353
- package/runtime/sdk/scene/editorPlugins.ts +0 -110
- package/runtime/sdk/scene/gizmos.ts +0 -148
- package/runtime/sdk/scene/grammar.ts +0 -120
- package/runtime/sdk/scene/material.ts +0 -188
- package/runtime/sdk/server/auth/appConfig.ts +0 -12
- package/runtime/sdk/server/auth/global.ts +0 -80
- package/runtime/sdk/server/auth/host.ts +0 -318
- package/runtime/sdk/server/auth/models.ts +0 -83
- package/runtime/sdk/server/auth/types.ts +0 -50
- package/runtime/sdk/server/channel.ts +0 -56
- package/runtime/sdk/server/context.ts +0 -36
- package/runtime/sdk/server/db/defineDb.ts +0 -237
- package/runtime/sdk/server/db/fields.ts +0 -132
- package/runtime/sdk/server/db/httpTransport.ts +0 -93
- package/runtime/sdk/server/db/index.ts +0 -7
- package/runtime/sdk/server/db/marci/query.ts +0 -412
- package/runtime/sdk/server/db/types.ts +0 -202
- package/runtime/sdk/server/errors.ts +0 -12
- package/runtime/sdk/server/host.ts +0 -74
- package/runtime/sdk/server/inject.ts +0 -13
- package/runtime/sdk/server/runtime.ts +0 -133
- package/runtime/sdk/server/validate.ts +0 -87
- package/runtime/sdk/ui/NativeView.ts +0 -142
- package/runtime/sdk/ui/UI.ts +0 -39
- package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
- package/runtime/sdk/ui/UIButton.ts +0 -101
- package/runtime/sdk/ui/UIContainer.ts +0 -60
- package/runtime/sdk/ui/UIImage.ts +0 -83
- package/runtime/sdk/ui/UIInput.ts +0 -185
- package/runtime/sdk/ui/UIModal.ts +0 -139
- package/runtime/sdk/ui/UINode.ts +0 -830
- package/runtime/sdk/ui/UIPager.ts +0 -362
- package/runtime/sdk/ui/UIPopover.ts +0 -100
- package/runtime/sdk/ui/UIScreen.ts +0 -123
- package/runtime/sdk/ui/UIScrollable.ts +0 -87
- package/runtime/sdk/ui/UISpacer.ts +0 -14
- package/runtime/sdk/ui/UITabs.ts +0 -236
- package/runtime/sdk/ui/UIText.ts +0 -51
- package/runtime/sdk/ui/UIVideo.ts +0 -88
- package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
- package/runtime/sdk/ui/UIWidget.ts +0 -127
- package/runtime/sdk/ui/fonts.ts +0 -13
- package/runtime/sdk/ui/presentable.ts +0 -117
- package/runtime/sdk/ui/router.ts +0 -132
- package/runtime/sdk/ui/theme.ts +0 -84
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The HOST side of auth (docs/backend-plan.md §3.4): sessions + tokens, the email-code flow, OAuth
|
|
3
|
-
* binding. Runs where the runner imports `sdk/server` (the project worker), against the project's own
|
|
4
|
-
* db handle (`server.dbs[0]` — `User`/`Session` are ordinary models, see ./models.ts). Installed on
|
|
5
|
-
* `globalThis.__lecodesAuth` for the bundle's `auth` global (./global.ts); `resolve(ctx)` runs before
|
|
6
|
-
* every endpoint / channel join so `auth.user` / `auth.session` are plain synchronous reads.
|
|
7
|
-
*
|
|
8
|
-
* Tokens: 32 random bytes (base64url) held by the client transport; only `sha256(token)` is stored.
|
|
9
|
-
* A request without a valid token gets a fresh guest session and the token travels back in the
|
|
10
|
-
* envelope (`session`); sign-in/out ROTATE the token the same way. OAuth binds without rotation (the
|
|
11
|
-
* browser completes the flow, the app can't receive a new token) — see `oauthComplete`.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { RequestContext } from "../context"
|
|
15
|
-
import type { AuthBinding } from "../db/types"
|
|
16
|
-
import { ApiError } from "../errors"
|
|
17
|
-
import type { AuthProvider } from "./models"
|
|
18
|
-
import type { AuthOps, AuthState, AuthUser, OAuthCredential, OAuthProfile } from "./types"
|
|
19
|
-
|
|
20
|
-
export type AuthMail = { to: string, code: string, appName: string }
|
|
21
|
-
|
|
22
|
-
export type AuthHostConfig = {
|
|
23
|
-
/** The project's db (`server.dbs[0]`, any `defineDb` handle); `null` = no database → `auth.*` throws a clear error. */
|
|
24
|
-
db: { readonly $auth: AuthBinding, readonly $schema: string } | null
|
|
25
|
-
/** HMAC key for OAuth nonces (runner-wide secret). */
|
|
26
|
-
secret: string
|
|
27
|
-
/** `app.json` `auth.providers` (the compiler embeds them; the runner reads them off the manifest). */
|
|
28
|
-
providers: AuthProvider[]
|
|
29
|
-
appName?: string
|
|
30
|
-
/** `<publicUrl>/p/<key>` — OAuth start links are minted under it; absent = OAuth unavailable. */
|
|
31
|
-
publicBase?: string
|
|
32
|
-
/** Nonce scope (the runner's project key): a nonce minted for one project can't bind a session of another. */
|
|
33
|
-
scope?: string
|
|
34
|
-
/** Sends the sign-in code (the platform mailer). Absent = codes are only echoed to the console. */
|
|
35
|
-
mail?: (msg: AuthMail) => Promise<void>
|
|
36
|
-
/** Echo every code to the server console (the draft slot: the developer is the only user). */
|
|
37
|
-
echoCodes?: boolean
|
|
38
|
-
/** Verifies a NATIVE-SDK credential with the provider (runner-side, knows the platform's app ids) → profile.
|
|
39
|
-
* Absent = `auth.oauth.signIn` is unavailable. Throw `ApiError` for a bad credential. */
|
|
40
|
-
verifyOAuth?: (provider: Exclude<AuthProvider, "email">, credential: OAuthCredential) => Promise<OAuthProfile>
|
|
41
|
-
/** Server console sink (default console). */
|
|
42
|
-
log?: (level: "warn" | "info", text: string) => void
|
|
43
|
-
now?: () => number
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const CODE_TTL_MS = 10 * 60_000
|
|
47
|
-
const CODE_MAX_ATTEMPTS = 5
|
|
48
|
-
const SEND_MIN_GAP_MS = 30_000
|
|
49
|
-
const SEND_MAX_PER_10MIN = 3
|
|
50
|
-
const NONCE_TTL_MS = 10 * 60_000
|
|
51
|
-
const CACHE_TTL_MS = 60_000
|
|
52
|
-
const SEEN_UPDATE_MS = 60_000
|
|
53
|
-
|
|
54
|
-
const enc = new TextEncoder()
|
|
55
|
-
const b64url = (bytes: ArrayBuffer | Uint8Array): string => {
|
|
56
|
-
const u = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)
|
|
57
|
-
let s = ""
|
|
58
|
-
for (const b of u) s += String.fromCharCode(b)
|
|
59
|
-
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
|
|
60
|
-
}
|
|
61
|
-
const hex = (bytes: ArrayBuffer): string => [...new Uint8Array(bytes)].map(b => b.toString(16).padStart(2, "0")).join("")
|
|
62
|
-
export const sha256 = async (text: string): Promise<string> => hex(await crypto.subtle.digest("SHA-256", enc.encode(text)))
|
|
63
|
-
const randomToken = (): string => b64url(crypto.getRandomValues(new Uint8Array(32)))
|
|
64
|
-
const randomCode = (): string => String(crypto.getRandomValues(new Uint32Array(1))[0] % 1_000_000).padStart(6, "0")
|
|
65
|
-
|
|
66
|
-
const hmac = async (secret: string, text: string): Promise<string> => {
|
|
67
|
-
const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"])
|
|
68
|
-
return b64url(await crypto.subtle.sign("HMAC", key, enc.encode(text)))
|
|
69
|
-
}
|
|
70
|
-
/** OAuth nonce: `<sessionId>.<expiresAt>.<hmac>` — stateless, bound to a session (and a `scope`: the project), short-lived. */
|
|
71
|
-
export const signNonce = async (secret: string, sessionId: number, expiresAt: number, scope = ""): Promise<string> =>
|
|
72
|
-
`${sessionId}.${expiresAt}.${await hmac(secret, `${scope}|${sessionId}.${expiresAt}`)}`
|
|
73
|
-
export const verifyNonce = async (secret: string, nonce: string, now = Date.now(), scope = ""): Promise<{ sessionId: number } | null> => {
|
|
74
|
-
const [sid, exp, sig] = nonce.split(".")
|
|
75
|
-
const sessionId = Number(sid), expiresAt = Number(exp)
|
|
76
|
-
if (!Number.isInteger(sessionId) || !Number.isFinite(expiresAt) || !sig) return null
|
|
77
|
-
if (expiresAt < now) return null
|
|
78
|
-
if (sig !== await hmac(secret, `${scope}|${sid}.${exp}`)) return null
|
|
79
|
-
return { sessionId }
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
83
|
-
const normalizeEmail = (email: unknown): string => {
|
|
84
|
-
const e = typeof email === "string" ? email.trim().toLowerCase() : ""
|
|
85
|
-
if (!e || e.length > 254 || !EMAIL_RE.test(e)) throw new ApiError(400, "Invalid email")
|
|
86
|
-
return e
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Built-in user shape from a row (custom scalar fields ride along at runtime). */
|
|
90
|
-
const toUser = (row: any): AuthUser => row as AuthUser
|
|
91
|
-
|
|
92
|
-
export type AuthHost = {
|
|
93
|
-
/** Resolve the bearer token of `ctx` into `ctx.auth` (minting a guest session when needed). No db → leaves it undefined. */
|
|
94
|
-
resolve(ctx: RequestContext): Promise<void>
|
|
95
|
-
ops: AuthOps
|
|
96
|
-
/** Install `ops` for the bundle's `auth` global (`globalThis.__lecodesAuth`). */
|
|
97
|
-
install(): void
|
|
98
|
-
/**
|
|
99
|
-
* OAuth callback: bind the session named by `nonce` to the user identified by `profile` (existing
|
|
100
|
-
* `<provider>Id`, else a verified-email match, else a new user). Runs in the worker; the runner's
|
|
101
|
-
* broker route calls it with what the provider returned.
|
|
102
|
-
*/
|
|
103
|
-
oauthComplete(provider: Exclude<AuthProvider, "email">, nonce: string, profile: OAuthProfile): Promise<{ userId: number }>
|
|
104
|
-
/** Test hook / diagnostics: forget cached sessions and users. */
|
|
105
|
-
clearCache(): void
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export const createAuthHost = (cfg: AuthHostConfig): AuthHost => {
|
|
109
|
-
const db = cfg.db as any
|
|
110
|
-
const binding = cfg.db?.$auth ?? null
|
|
111
|
-
const hasUser = !!binding?.user
|
|
112
|
-
const now = cfg.now ?? (() => Date.now())
|
|
113
|
-
const log = cfg.log ?? ((level, text) => (level === "warn" ? console.warn(text) : console.info(text)))
|
|
114
|
-
const providers = new Set(cfg.providers)
|
|
115
|
-
|
|
116
|
-
// token → session (60 s), user id → user row (60 s). Cleared on every mutation — correctness over reuse.
|
|
117
|
-
const sessionCache = new Map<string, { sessionId: number, userId: number | null, at: number, seenAt: number }>()
|
|
118
|
-
const userCache = new Map<number, { user: AuthUser | null, at: number }>()
|
|
119
|
-
const clearCache = () => { sessionCache.clear(); userCache.clear() }
|
|
120
|
-
|
|
121
|
-
const requireDb = () => {
|
|
122
|
-
if (!db) throw new Error("auth: this project has no database — sessions need a defineDb()")
|
|
123
|
-
return db
|
|
124
|
-
}
|
|
125
|
-
const requireUserModel = (what: string) => {
|
|
126
|
-
if (!hasUser) throw new Error(`${what} needs a user model: export const User = auth.model.user({...}) in your schema`)
|
|
127
|
-
}
|
|
128
|
-
const stateOf = (ctx: RequestContext): AuthState => {
|
|
129
|
-
const s = ctx.auth as AuthState | undefined
|
|
130
|
-
if (!s) throw new Error("auth: this project has no database — sessions need a defineDb()")
|
|
131
|
-
return s
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const sessionSelect = () => (hasUser ? { id: true, revoked: true, lastSeenAt: true, user: { id: true } } : { id: true, revoked: true, lastSeenAt: true })
|
|
135
|
-
const loadUser = async (userId: number | null): Promise<AuthUser | null> => {
|
|
136
|
-
if (userId === null || !hasUser) return null
|
|
137
|
-
const c = userCache.get(userId)
|
|
138
|
-
if (c && now() - c.at < CACHE_TTL_MS) return c.user
|
|
139
|
-
const row = await db.user.where({ id: userId }).first()
|
|
140
|
-
const user = row ? toUser(row) : null
|
|
141
|
-
userCache.set(userId, { user, at: now() })
|
|
142
|
-
return user
|
|
143
|
-
}
|
|
144
|
-
const mint = async (ctx: RequestContext): Promise<AuthState> => {
|
|
145
|
-
const token = randomToken()
|
|
146
|
-
const { id } = await db.session.insert({ tokenHash: await sha256(token) })
|
|
147
|
-
const state: AuthState = { sessionId: id, userId: null, user: null, issuedToken: token }
|
|
148
|
-
sessionCache.set(token, { sessionId: id, userId: null, at: now(), seenAt: now() })
|
|
149
|
-
ctx.auth = state
|
|
150
|
-
ctx.sessionToken = token
|
|
151
|
-
return state
|
|
152
|
-
}
|
|
153
|
-
const rotate = async (ctx: RequestContext): Promise<void> => {
|
|
154
|
-
const s = stateOf(ctx)
|
|
155
|
-
const token = randomToken()
|
|
156
|
-
await db.session.update(s.sessionId, { tokenHash: await sha256(token) })
|
|
157
|
-
clearCache()
|
|
158
|
-
s.issuedToken = token
|
|
159
|
-
ctx.sessionToken = token
|
|
160
|
-
}
|
|
161
|
-
const bind = async (ctx: RequestContext, userId: number | null, opts: { rotate: boolean }): Promise<AuthUser | null> => {
|
|
162
|
-
const s = stateOf(ctx)
|
|
163
|
-
await db.session.update(s.sessionId, { user: userId === null ? null : { $connect: { id: userId } } })
|
|
164
|
-
clearCache()
|
|
165
|
-
s.userId = userId
|
|
166
|
-
s.user = await loadUser(userId)
|
|
167
|
-
if (opts.rotate) await rotate(ctx)
|
|
168
|
-
return s.user
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const resolve = async (ctx: RequestContext): Promise<void> => {
|
|
172
|
-
if (!db) return
|
|
173
|
-
const token = ctx.sessionToken
|
|
174
|
-
if (token) {
|
|
175
|
-
const c = sessionCache.get(token)
|
|
176
|
-
if (c && now() - c.at < CACHE_TTL_MS) {
|
|
177
|
-
ctx.auth = { sessionId: c.sessionId, userId: c.userId, user: await loadUser(c.userId) }
|
|
178
|
-
return
|
|
179
|
-
}
|
|
180
|
-
const row = await db.session.where({ tokenHash: await sha256(token) }).select(sessionSelect()).first()
|
|
181
|
-
if (row && !row.revoked) {
|
|
182
|
-
const userId: number | null = hasUser && row.user ? row.user.id : null
|
|
183
|
-
if (now() - (row.lastSeenAt ?? 0) > SEEN_UPDATE_MS) db.session.update(row.id, { lastSeenAt: now() }).then(undefined, () => {})
|
|
184
|
-
sessionCache.set(token, { sessionId: row.id, userId, at: now(), seenAt: now() })
|
|
185
|
-
ctx.auth = { sessionId: row.id, userId, user: await loadUser(userId) }
|
|
186
|
-
return
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
await mint(ctx)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// ── email codes ──
|
|
193
|
-
const sends = new Map<string, number[]>() // email → send timestamps (per worker; attempts persist on the row)
|
|
194
|
-
const codeHash = (sessionId: number, code: string) => sha256(`${sessionId}:${code}`)
|
|
195
|
-
|
|
196
|
-
const sendCode = async (ctx: RequestContext, rawEmail: string): Promise<void> => {
|
|
197
|
-
requireDb()
|
|
198
|
-
if (!providers.has("email")) throw new Error(`auth.email is not enabled — add "email" to auth.providers in app.json`)
|
|
199
|
-
requireUserModel("auth.email")
|
|
200
|
-
const s = stateOf(ctx)
|
|
201
|
-
const email = normalizeEmail(rawEmail)
|
|
202
|
-
const t = now()
|
|
203
|
-
const recent = (sends.get(email) ?? []).filter(x => t - x < 10 * 60_000)
|
|
204
|
-
if (recent.length && t - recent[recent.length - 1] < SEND_MIN_GAP_MS) throw new ApiError(429, `Try again in ${Math.ceil((SEND_MIN_GAP_MS - (t - recent[recent.length - 1])) / 1000)} s`)
|
|
205
|
-
if (recent.length >= SEND_MAX_PER_10MIN) throw new ApiError(429, "Too many codes requested — try again later")
|
|
206
|
-
const code = randomCode()
|
|
207
|
-
await db.session.update(s.sessionId, { pendingEmail: email, codeHash: await codeHash(s.sessionId, code), codeExpires: t + CODE_TTL_MS, codeAttempts: 0 })
|
|
208
|
-
recent.push(t); sends.set(email, recent)
|
|
209
|
-
const appName = cfg.appName || "the app"
|
|
210
|
-
if (cfg.mail) {
|
|
211
|
-
try { await cfg.mail({ to: email, code, appName }) }
|
|
212
|
-
catch (e) { log("warn", `[auth] could not send the sign-in code to ${email}: ${(e as Error).message}`) }
|
|
213
|
-
}
|
|
214
|
-
if (!cfg.mail || cfg.echoCodes) log("info", `[auth] sign-in code for ${email}: ${code}${cfg.mail ? "" : " (no mailer configured — the code was not emailed)"}`)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const verify = async (ctx: RequestContext, rawEmail: string, code: string): Promise<AuthUser> => {
|
|
218
|
-
requireDb()
|
|
219
|
-
requireUserModel("auth.email")
|
|
220
|
-
const s = stateOf(ctx)
|
|
221
|
-
const email = normalizeEmail(rawEmail)
|
|
222
|
-
const row = await db.session.where({ id: s.sessionId }).select({ pendingEmail: true, codeHash: true, codeExpires: true, codeAttempts: true }).first()
|
|
223
|
-
if (!row?.codeHash || row.pendingEmail !== email || !row.codeExpires || row.codeExpires < now()) throw new ApiError(400, "Code expired — request a new one")
|
|
224
|
-
if (row.codeAttempts >= CODE_MAX_ATTEMPTS) throw new ApiError(429, "Too many attempts — request a new code")
|
|
225
|
-
if (typeof code !== "string" || await codeHash(s.sessionId, code.trim()) !== row.codeHash) {
|
|
226
|
-
await db.session.update(s.sessionId, { codeAttempts: { $increment: 1 } })
|
|
227
|
-
throw new ApiError(400, "Invalid code")
|
|
228
|
-
}
|
|
229
|
-
const existing = await db.user.where({ email }).first()
|
|
230
|
-
const userId: number = existing ? existing.id : (await db.user.insert({ email })).id
|
|
231
|
-
await db.session.update(s.sessionId, { pendingEmail: null, codeHash: null, codeExpires: null, codeAttempts: 0 })
|
|
232
|
-
const user = await bind(ctx, userId, { rotate: true })
|
|
233
|
-
return user!
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// ── oauth ──
|
|
237
|
-
const oauthUrl = async (ctx: RequestContext, provider: Exclude<AuthProvider, "email">): Promise<string> => {
|
|
238
|
-
requireDb()
|
|
239
|
-
if (!providers.has(provider)) throw new Error(`auth.oauth: "${provider}" is not enabled — add it to auth.providers in app.json`)
|
|
240
|
-
requireUserModel("auth.oauth")
|
|
241
|
-
if (!cfg.publicBase) throw new Error("auth.oauth: not available in this environment (no public runner URL)")
|
|
242
|
-
const s = stateOf(ctx)
|
|
243
|
-
const nonce = await signNonce(cfg.secret, s.sessionId, now() + NONCE_TTL_MS, cfg.scope)
|
|
244
|
-
return `${cfg.publicBase}/auth/${provider}/start?s=${encodeURIComponent(nonce)}`
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** The user a provider identity maps to: by `<provider>Id`, else a verified-email match (linked), else created. */
|
|
248
|
-
const userForProfile = async (provider: Exclude<AuthProvider, "email">, profile: OAuthProfile): Promise<number> => {
|
|
249
|
-
const idField = `${provider}Id`
|
|
250
|
-
if (!profile.sub) throw new ApiError(502, `${provider} returned no account id`)
|
|
251
|
-
let user = await db.user.where({ [idField]: profile.sub }).first()
|
|
252
|
-
if (!user && profile.email && profile.emailVerified !== false) {
|
|
253
|
-
const byEmail = await db.user.where({ email: profile.email.toLowerCase() }).first()
|
|
254
|
-
if (byEmail) { await db.user.update(byEmail.id, { [idField]: profile.sub }); user = { ...byEmail, [idField]: profile.sub } }
|
|
255
|
-
}
|
|
256
|
-
if (!user) {
|
|
257
|
-
const data: Record<string, unknown> = { [idField]: profile.sub }
|
|
258
|
-
if (profile.email && profile.emailVerified !== false) data.email = profile.email.toLowerCase()
|
|
259
|
-
if (profile.name) data.name = profile.name
|
|
260
|
-
if (profile.avatar) data.avatar = profile.avatar
|
|
261
|
-
const { id } = await db.user.insert(data)
|
|
262
|
-
user = { id }
|
|
263
|
-
}
|
|
264
|
-
return user.id
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const oauthComplete = async (provider: Exclude<AuthProvider, "email">, nonce: string, profile: OAuthProfile): Promise<{ userId: number }> => {
|
|
268
|
-
requireDb()
|
|
269
|
-
requireUserModel("auth.oauth")
|
|
270
|
-
const n = await verifyNonce(cfg.secret, nonce, now(), cfg.scope)
|
|
271
|
-
if (!n) throw new ApiError(400, "This sign-in link has expired — start again from the app")
|
|
272
|
-
const session = await db.session.where({ id: n.sessionId }).select({ id: true, revoked: true, user: { id: true } }).first()
|
|
273
|
-
if (!session || session.revoked) throw new ApiError(400, "This sign-in link is no longer valid — start again from the app")
|
|
274
|
-
const userId = await userForProfile(provider, profile)
|
|
275
|
-
// A session already signed in as someone else is not re-bound by a stray link (fixation guard).
|
|
276
|
-
if (session.user && session.user.id !== userId) throw new ApiError(409, "This device is already signed in as another user — sign out first")
|
|
277
|
-
if (!session.user) await db.session.update(session.id, { user: { $connect: { id: userId } } })
|
|
278
|
-
clearCache()
|
|
279
|
-
return { userId }
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Native-SDK credential (client plugin OAuth.signIn) → verified with the provider → bound to THIS session, rotated
|
|
283
|
-
// like any in-band sign-in (the same client receives the new token).
|
|
284
|
-
const oauthSignIn = async (ctx: RequestContext, provider: Exclude<AuthProvider, "email">, credential: OAuthCredential): Promise<AuthUser> => {
|
|
285
|
-
requireDb()
|
|
286
|
-
if (!providers.has(provider)) throw new Error(`auth.oauth: "${provider}" is not enabled — add it to auth.providers in app.json`)
|
|
287
|
-
requireUserModel("auth.oauth")
|
|
288
|
-
if (!cfg.verifyOAuth) throw new Error("auth.oauth.signIn: not available in this environment (no provider verification configured)")
|
|
289
|
-
if (!credential || typeof credential !== "object" || (typeof credential.accessToken !== "string" && typeof credential.idToken !== "string")) throw new ApiError(400, "A provider credential (accessToken or idToken) is required")
|
|
290
|
-
const profile = await cfg.verifyOAuth(provider, credential)
|
|
291
|
-
// Apple only sends the name on the first sign-in — the app forwards what the sheet returned
|
|
292
|
-
if (!profile.name && typeof credential.name === "string" && credential.name.trim()) profile.name = credential.name.trim()
|
|
293
|
-
const userId = await userForProfile(provider, profile)
|
|
294
|
-
return (await bind(ctx, userId, { rotate: true }))!
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const ops: AuthOps = {
|
|
298
|
-
email: { sendCode, verify },
|
|
299
|
-
oauth: { url: oauthUrl, signIn: oauthSignIn },
|
|
300
|
-
signIn: async (ctx, userId) => {
|
|
301
|
-
requireDb(); requireUserModel("auth.signIn")
|
|
302
|
-
const row = await db.user.where({ id: userId }).first()
|
|
303
|
-
if (!row) throw new ApiError(404, "No such user")
|
|
304
|
-
return (await bind(ctx, userId, { rotate: true }))!
|
|
305
|
-
},
|
|
306
|
-
signOut: async (ctx) => { requireDb(); await bind(ctx, null, { rotate: true }) },
|
|
307
|
-
sessions: (ctx) => {
|
|
308
|
-
requireDb()
|
|
309
|
-
const s = stateOf(ctx)
|
|
310
|
-
return s.userId !== null && hasUser ? db.session.where({ user: { id: s.userId } }) : db.session.where({ id: s.sessionId })
|
|
311
|
-
},
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
return {
|
|
315
|
-
resolve, ops, oauthComplete, clearCache,
|
|
316
|
-
install: () => { (globalThis as any).__lecodesAuth = ops },
|
|
317
|
-
}
|
|
318
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `auth.model.user({...})` / `auth.model.session()` — the ONLY place auth touches the schema
|
|
3
|
-
* (docs/backend-plan.md §3.4 / decision 10). Both return ordinary `model(...)`s: the developer registers
|
|
4
|
-
* them in `defineDb({ User, Session, … })` under exactly those keys (other models relate to them with
|
|
5
|
-
* `t.one("User")` / `t.one("Session")`), and `defineDb` adds `Session` itself when it isn't declared —
|
|
6
|
-
* a device session exists from the first request, before any login, so every project db has one.
|
|
7
|
-
*
|
|
8
|
-
* Provider identity columns (`googleId`, `appleId`) follow `app.json` `auth.providers`, which the
|
|
9
|
-
* compiler hands the server bundle through ./appConfig.ts (a module it replaces at compile time).
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { t, type Field } from "../db/fields"
|
|
13
|
-
import type { Fields, Model } from "../db/types"
|
|
14
|
-
import { app, type AppConfig, type AuthProvider } from "./appConfig"
|
|
15
|
-
|
|
16
|
-
export type { AppConfig, AuthProvider }
|
|
17
|
-
|
|
18
|
-
const seam = globalThis as unknown as { __lecodesApp?: AppConfig }
|
|
19
|
-
/** The project's `app.json` subset: the compiled-in module, else the test/tooling seam, else empty. */
|
|
20
|
-
export const appConfig = (): AppConfig => (Object.keys(app).length ? app : seam.__lecodesApp ?? {})
|
|
21
|
-
export const enabledProviders = (): AuthProvider[] => appConfig().auth?.providers ?? []
|
|
22
|
-
|
|
23
|
-
/** The model name the auth runtime binds to. */
|
|
24
|
-
export const AUTH_USER_MODEL = "User"
|
|
25
|
-
export const AUTH_SESSION_MODEL = "Session"
|
|
26
|
-
|
|
27
|
-
/** Marker on the two auth models (`model()` objects are otherwise plain). */
|
|
28
|
-
export type AuthModelKind = "user" | "session"
|
|
29
|
-
export type AuthModel<F extends Fields> = Model<F> & { readonly __auth: AuthModelKind }
|
|
30
|
-
|
|
31
|
-
// ───────────────────────────── field sets ─────────────────────────────
|
|
32
|
-
|
|
33
|
-
/** Built-in `User` fields (what `auth.user` is typed with). */
|
|
34
|
-
export const userFields = () => ({
|
|
35
|
-
email: t.string().optional().unique(),
|
|
36
|
-
name: t.string().optional(),
|
|
37
|
-
avatar: t.string().optional(),
|
|
38
|
-
/** Plain string column, `"user"` by default — `auth.requireUser({ role: "admin" })` is field equality. */
|
|
39
|
-
role: t.string().default("user"),
|
|
40
|
-
createdAt: t.date().default("now"),
|
|
41
|
-
})
|
|
42
|
-
export type UserFields = ReturnType<typeof userFields>
|
|
43
|
-
|
|
44
|
-
/** `<provider>Id` identity columns for the OAuth providers enabled in app.json. */
|
|
45
|
-
const providerFields = (): Record<string, Field<any, any>> => {
|
|
46
|
-
const out: Record<string, Field<any, any>> = {}
|
|
47
|
-
for (const p of enabledProviders()) if (p !== "email") out[`${p}Id`] = t.string().optional().unique()
|
|
48
|
-
return out
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Built-in `Session` fields. `user` is added by `defineDb` when a `User` model exists (schema-time, so a
|
|
52
|
-
* db without an auth user model has sessions without a user column). */
|
|
53
|
-
export const sessionFields = () => ({
|
|
54
|
-
/** sha256 of the bearer token the device holds — the token itself is never stored. */
|
|
55
|
-
tokenHash: t.string().unique(),
|
|
56
|
-
createdAt: t.date().default("now"),
|
|
57
|
-
lastSeenAt: t.date().default("now"),
|
|
58
|
-
revoked: t.bool().default(false),
|
|
59
|
-
// email-code sign-in state lives on the requesting session (no code table)
|
|
60
|
-
pendingEmail: t.string().optional(),
|
|
61
|
-
codeHash: t.string().optional(),
|
|
62
|
-
codeExpires: t.date().optional(),
|
|
63
|
-
codeAttempts: t.int().default(0),
|
|
64
|
-
})
|
|
65
|
-
export type SessionFields = ReturnType<typeof sessionFields> & { user: ReturnType<typeof sessionUserField> }
|
|
66
|
-
export const sessionUserField = () => t.one(AUTH_USER_MODEL).optional().onDelete("cascade")
|
|
67
|
-
|
|
68
|
-
// ───────────────────────────── factories ─────────────────────────────
|
|
69
|
-
|
|
70
|
-
// Built inline (not via defineDb's `model()`) to keep this module import-free of defineDb, which imports us.
|
|
71
|
-
const authModel = <F extends Fields>(kind: AuthModelKind, fields: F): AuthModel<F> =>
|
|
72
|
-
({ fields, __model: true, __auth: kind }) as AuthModel<F>
|
|
73
|
-
|
|
74
|
-
/** `auth.model.user({...extra})` — the built-in user (+ provider ids from app.json) plus the developer's fields. */
|
|
75
|
-
export const authUserModel = <E extends Fields = {}>(extra?: E): AuthModel<UserFields & E> =>
|
|
76
|
-
authModel("user", { ...userFields(), ...providerFields(), ...(extra ?? {}) } as UserFields & E)
|
|
77
|
-
|
|
78
|
-
/** `auth.model.session({...extra})` — declare it only to relate to sessions (`t.one("Session")`) or to add fields. */
|
|
79
|
-
export const authSessionModel = <E extends Fields = {}>(extra?: E): AuthModel<SessionFields & E> =>
|
|
80
|
-
authModel("session", { ...sessionFields(), ...(extra ?? {}) } as unknown as SessionFields & E)
|
|
81
|
-
|
|
82
|
-
export const isAuthModel = (m: unknown, kind?: AuthModelKind): m is AuthModel<any> =>
|
|
83
|
-
typeof m === "object" && m !== null && "__auth" in m && (kind === undefined || (m as AuthModel<any>).__auth === kind)
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared shapes of the auth layer (docs/backend-plan.md §3.4). Two sides meet through them:
|
|
3
|
-
* - the BUNDLE side (`auth` global, ./global.ts) — what `*.server.ts` code calls;
|
|
4
|
-
* - the HOST side (./host.ts) — sessions, email codes and OAuth binding, run by the runner with the
|
|
5
|
-
* project's db handle and the platform's secrets. Installed on `globalThis.__lecodesAuth`.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { RequestContext } from "../context"
|
|
9
|
-
import type { AuthProvider } from "./models"
|
|
10
|
-
|
|
11
|
-
/** The built-in user row — what `auth.user` is typed with (custom fields exist at runtime; select them via `db.user`). */
|
|
12
|
-
export type AuthUser = {
|
|
13
|
-
id: number
|
|
14
|
-
email: string | null
|
|
15
|
-
name: string | null
|
|
16
|
-
avatar: string | null
|
|
17
|
-
role: string
|
|
18
|
-
createdAt: number
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Per-request auth state, resolved by the host before the endpoint runs (`RequestContext.auth`). */
|
|
22
|
-
export type AuthState = {
|
|
23
|
-
sessionId: number
|
|
24
|
-
userId: number | null
|
|
25
|
-
user: AuthUser | null
|
|
26
|
-
/** A token to hand back to the client in this response (a new guest session, or a rotation). */
|
|
27
|
-
issuedToken?: string
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** What an OAuth provider hands the runner after a successful web sign-in. */
|
|
31
|
-
export type OAuthProfile = { sub: string, email?: string | null, emailVerified?: boolean, name?: string | null, avatar?: string | null }
|
|
32
|
-
/** A credential a NATIVE provider SDK produced in the app (client plugin `OAuth.signIn`): an access token
|
|
33
|
-
* (Yandex, VK ID) or an OpenID identity token (Google, Apple). Verified server-side, never trusted as-is. */
|
|
34
|
-
export type OAuthCredential = { accessToken?: string, idToken?: string, name?: string | null, email?: string | null }
|
|
35
|
-
|
|
36
|
-
/** Host operations the bundle's `auth` global forwards to (all request-scoped through `ctx`). */
|
|
37
|
-
export type AuthOps = {
|
|
38
|
-
email: {
|
|
39
|
-
sendCode(ctx: RequestContext, email: string): Promise<void>
|
|
40
|
-
verify(ctx: RequestContext, email: string, code: string): Promise<AuthUser>
|
|
41
|
-
}
|
|
42
|
-
oauth: {
|
|
43
|
-
url(ctx: RequestContext, provider: Exclude<AuthProvider, "email">): Promise<string>
|
|
44
|
-
signIn(ctx: RequestContext, provider: Exclude<AuthProvider, "email">, credential: OAuthCredential): Promise<AuthUser>
|
|
45
|
-
}
|
|
46
|
-
signIn(ctx: RequestContext, userId: number): Promise<AuthUser>
|
|
47
|
-
signOut(ctx: RequestContext): Promise<void>
|
|
48
|
-
/** `db.session` filtered to the current user's sessions (or just this device's, when signed out). */
|
|
49
|
-
sessions(ctx: RequestContext): any
|
|
50
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Server side of channels (docs/backend-plan.md §3.3): typed pub/sub, server → client only.
|
|
3
|
-
*
|
|
4
|
-
* type ChatEvents = { message: { from: string; text: string }; typing: { from: string } }
|
|
5
|
-
* export const chat = channel<ChatEvents>({ onJoin: room => { auth.requireUser() } })
|
|
6
|
-
* chat.publish(room, "message", { from, text })
|
|
7
|
-
*
|
|
8
|
-
* A channel's identity is its export (`<path>#<name>`), assigned by the runtime when the bundle is
|
|
9
|
-
* loaded (`loadServerModules`); `publish` before that is an error. Delivery goes through the
|
|
10
|
-
* `globalThis.__lecodesPublish` seam the runner installs (same reason as ./context.ts: the bundle
|
|
11
|
-
* carries its own SDK copy).
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export type ChannelEvents = Record<string, unknown>
|
|
15
|
-
|
|
16
|
-
export type ChannelOptions = {
|
|
17
|
-
/** Called when a client subscribes to `topic`; throw (e.g. `ApiError(403)`) to refuse. */
|
|
18
|
-
onJoin?: (topic: string) => void | Promise<void>
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface Channel<E extends ChannelEvents> {
|
|
22
|
-
/** @internal set by the runtime: `<path>#<export>` */
|
|
23
|
-
__id: string | null
|
|
24
|
-
/** @internal */
|
|
25
|
-
readonly __channel: true
|
|
26
|
-
readonly options: ChannelOptions
|
|
27
|
-
/** Deliver `event` with `payload` to every subscriber of `topic` (at-most-once). */
|
|
28
|
-
publish<K extends keyof E & string>(topic: string, event: K, payload: E[K]): void
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
type Publisher = (channelId: string, topic: string, event: string, payload: unknown) => void
|
|
32
|
-
const seam = globalThis as unknown as { __lecodesPublish?: Publisher }
|
|
33
|
-
|
|
34
|
-
/** Host hook (runner / test harness): where `publish` delivers to. */
|
|
35
|
-
export const setChannelPublisher = (publisher: Publisher | null) => {
|
|
36
|
-
if (publisher) seam.__lecodesPublish = publisher
|
|
37
|
-
else delete seam.__lecodesPublish
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export const channel = <E extends ChannelEvents>(options: ChannelOptions = {}): Channel<E> => {
|
|
41
|
-
const ch: Channel<E> = {
|
|
42
|
-
__id: null,
|
|
43
|
-
__channel: true,
|
|
44
|
-
options,
|
|
45
|
-
publish(topic, event, payload) {
|
|
46
|
-
if (!ch.__id) throw new Error("channel.publish: the channel is not registered — export it from a *.server.ts file")
|
|
47
|
-
const publish = seam.__lecodesPublish
|
|
48
|
-
if (!publish) throw new Error("channel.publish: no host publisher installed")
|
|
49
|
-
publish(ch.__id, topic, event, payload)
|
|
50
|
-
},
|
|
51
|
-
}
|
|
52
|
-
return ch
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export const isChannel = (v: unknown): v is Channel<any> =>
|
|
56
|
-
typeof v === "object" && v !== null && (v as Channel<any>).__channel === true
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Request context for server code. The runner keeps the current request in an AsyncLocalStorage
|
|
3
|
-
* and exposes it through `globalThis.__lecodesRequest` — a *global* seam on purpose: the compiled
|
|
4
|
-
* server bundle carries its own copy of this module (chisel bundles the SDK in), so the host can't
|
|
5
|
-
* hand it a value by import. `request` reads through that seam; outside a request (module load,
|
|
6
|
-
* timers after the response) every member is undefined.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export type RequestContext = {
|
|
10
|
-
/** Endpoint id `<path>#<export>` (or the channel id for `onJoin`). */
|
|
11
|
-
id: string
|
|
12
|
-
headers: Record<string, string>
|
|
13
|
-
ip?: string
|
|
14
|
-
/** Bearer session token as sent by the client transport (raw; auth resolves it). */
|
|
15
|
-
sessionToken?: string
|
|
16
|
-
/** Set by the auth runtime after resolving the token — read via `auth.*`, not here. */
|
|
17
|
-
auth?: unknown
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
type Provider = () => RequestContext | undefined
|
|
21
|
-
const seam = globalThis as unknown as { __lecodesRequest?: Provider }
|
|
22
|
-
|
|
23
|
-
/** Host hook (runner / test harness): install the current-request provider. */
|
|
24
|
-
export const setRequestProvider = (provider: Provider | null) => {
|
|
25
|
-
if (provider) seam.__lecodesRequest = provider
|
|
26
|
-
else delete seam.__lecodesRequest
|
|
27
|
-
}
|
|
28
|
-
/** @internal */
|
|
29
|
-
export const currentRequest = (): RequestContext | undefined => seam.__lecodesRequest?.()
|
|
30
|
-
|
|
31
|
-
/** The current request (server global). Undefined members outside a request. */
|
|
32
|
-
export const request = {
|
|
33
|
-
get id(): string | undefined { return currentRequest()?.id },
|
|
34
|
-
get headers(): Record<string, string> { return currentRequest()?.headers ?? {} },
|
|
35
|
-
get ip(): string | undefined { return currentRequest()?.ip },
|
|
36
|
-
}
|