lecodes-sdk 2.0.3 → 2.0.5

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 (84) hide show
  1. package/README.md +104 -76
  2. package/dist/global.d.ts +3 -5
  3. package/dist/host.d.ts +3 -0
  4. package/dist/types/inject.d.ts +4 -4
  5. package/dist/types/net/codec.d.ts +3 -2
  6. package/dist/types/net/core.d.ts +10 -1
  7. package/dist/types/net/index.d.ts +22 -5
  8. package/dist/types/net/replication.d.ts +23 -2
  9. package/dist/types/runtime/device.d.ts +7 -0
  10. package/dist/types/runtime/rpc.d.ts +11 -17
  11. package/dist/types/runtime/wire.d.ts +53 -0
  12. package/dist/types/server/auth/api.d.ts +42 -0
  13. package/dist/types/server/auth/appConfig.d.ts +1 -5
  14. package/dist/types/server/auth/models.d.ts +119 -70
  15. package/dist/types/server/auth/types.d.ts +19 -43
  16. package/dist/types/server/channel.d.ts +57 -19
  17. package/dist/types/server/context.d.ts +2 -2
  18. package/dist/types/server/db/defineDb.d.ts +10 -0
  19. package/dist/types/server/db/index.d.ts +1 -1
  20. package/dist/types/server/db/types.d.ts +76 -6
  21. package/dist/types/server/inject.d.ts +0 -1
  22. package/dist/types/ui/UINode.d.ts +19 -5
  23. package/dist/types/ui/UIScreen.d.ts +1 -0
  24. package/dist/types/ui/UITabs.d.ts +8 -6
  25. package/dist/types/ui/theme.d.ts +48 -13
  26. package/dist/types/version.d.ts +1 -1
  27. package/dist/types.json +1 -1
  28. package/package.json +4 -2
  29. package/prompts/README.md +1 -1
  30. package/prompts/design.md +19 -19
  31. package/prompts/dist/2d-game.md +45 -31
  32. package/prompts/dist/3d-app.md +45 -31
  33. package/prompts/dist/ar-app.md +45 -31
  34. package/prompts/dist/design.md +25 -24
  35. package/prompts/dist/ui-app.md +45 -31
  36. package/prompts/ui-design.md +6 -5
  37. package/prompts/ui.md +25 -22
  38. package/src/animate/tween/read.ts +146 -0
  39. package/src/bridges/device.d.ts +9 -0
  40. package/src/bridges/tree.d.ts +5 -0
  41. package/src/canvas/gen/cssColor.ts +1 -1
  42. package/src/canvas/gen/recorder.ts +1 -1
  43. package/src/canvas/gen/spec.ts +1 -1
  44. package/src/chisel.ts +1 -1
  45. package/src/compile/bundler.ts +6 -0
  46. package/src/compile/compileProject.ts +3 -1
  47. package/src/compile/index.ts +3 -1
  48. package/src/compile/serverSplit.ts +58 -11
  49. package/src/compile/serverTypes.ts +189 -8
  50. package/src/host.d.ts +3 -0
  51. package/src/inject.ts +7 -7
  52. package/src/net/codec.ts +19 -10
  53. package/src/net/core.ts +18 -5
  54. package/src/net/index.ts +30 -9
  55. package/src/net/replication.ts +63 -28
  56. package/src/runtime/device.ts +12 -0
  57. package/src/runtime/rpc.ts +101 -40
  58. package/src/runtime/wire.ts +35 -0
  59. package/src/server/auth/api.ts +94 -0
  60. package/src/server/auth/appConfig.ts +2 -3
  61. package/src/server/auth/host.ts +244 -174
  62. package/src/server/auth/models.ts +45 -62
  63. package/src/server/auth/types.ts +19 -34
  64. package/src/server/channel.ts +97 -29
  65. package/src/server/channelHub.ts +153 -0
  66. package/src/server/context.ts +2 -2
  67. package/src/server/db/defineDb.ts +96 -36
  68. package/src/server/db/index.ts +1 -1
  69. package/src/server/db/types.ts +76 -8
  70. package/src/server/host.ts +25 -10
  71. package/src/server/inject.ts +2 -2
  72. package/src/server/runtime.ts +34 -12
  73. package/src/ui/UINode.ts +22 -5
  74. package/src/ui/UIScreen.ts +5 -0
  75. package/src/ui/UITabs.ts +19 -17
  76. package/src/ui/styleColor.ts +10 -1
  77. package/src/ui/theme.ts +96 -41
  78. package/src/version.ts +1 -1
  79. package/tests/helpers/fakeTree.ts +1 -0
  80. package/dist/types/plugins/oauth.d.ts +0 -25
  81. package/dist/types/server/auth/global.d.ts +0 -56
  82. package/src/plugins/oauth.ts +0 -61
  83. package/src/server/auth/global.ts +0 -80
  84. package/tests/helpers/memoryMarci.ts +0 -124
package/src/ui/UITabs.ts CHANGED
@@ -12,14 +12,15 @@ import type { UIChildArg } from "./UINode"
12
12
  // config without screens, mounted per-file with a literal tab id. They share the constants and bar
13
13
  // geometry in this file, so a designed mockup and the built app render identically.
14
14
 
15
- /** Active/inactive/bar colors ride the theme — one `theme({ primaryColor, mutedColor, tabbarBg })`
16
- * call restyles the bar app-wide; the fallbacks keep an unthemed app looking right. */
17
- const ACTIVE = "var(--primaryColor, #5b8cff)"
18
- const INACTIVE = "var(--mutedColor, #8a919e)"
19
- const BAR_BG = "var(--tabbarBg, #15171c)"
20
- const BAR_BORDER = "var(--tabbarBorder, #23262e)"
21
- const SCREEN_BG = "var(--screenBg, #101114)"
22
- const BADGE_BG = "var(--badgeColor, #ff453a)"
15
+ /** The bar reads the theme's ROLES and nothing of its own — `theme({ accent, textMuted, surface,
16
+ * border })` colors it with the rest of the app; `tabs.bar.theme({...})` gives one bar other
17
+ * values. The fallbacks keep an unthemed app looking right: a dark bar (on the theme's `bg`
18
+ * when only that is set), a hairline that reads on any background. */
19
+ const ACTIVE = "var(--accent, #5b8cff)"
20
+ const INACTIVE = "var(--textMuted, #8a919e)"
21
+ const BAR_BG = "var(--surface, var(--bg, #15171c))"
22
+ const BAR_BORDER = "var(--border, #80808040)"
23
+ const BADGE_BG = "#ff453a"
23
24
 
24
25
  export type UITabDef = {
25
26
  label: string,
@@ -42,8 +43,9 @@ export type UITabDef = {
42
43
  * ```
43
44
  *
44
45
  * In-tab navigation stays the pager's: `UIPager.push(detail)` from any screen (bar stays);
45
- * `Router.push` opens above the shell (bar covered). Restyle via `theme()` (primaryColor,
46
- * mutedColor, tabbarBg, …) or the `bar` handle; for a fully custom bar build on `UIPager`.
46
+ * `Router.push` opens above the shell (bar covered). The bar is colored by the theme's roles
47
+ * (`accent`, `textMuted`, `surface`, `border`); `tabs.bar.theme({...})` overrides them for this
48
+ * bar alone, the `bar` handle restyles its box; for a fully custom bar build on `UIPager`.
47
49
  */
48
50
  export interface UITabs extends UIScreen {
49
51
  /** The active tab id. */
@@ -56,7 +58,8 @@ export interface UITabs extends UIScreen {
56
58
  badge(id: string, value: number | string | boolean | null): this,
57
59
  /** The pager under the bar — in-tab stacks (`push`/`pop`/`depth`). */
58
60
  readonly pager: UIPager,
59
- /** The bar row (named "tabbar") — for restyling beyond the theme variables. */
61
+ /** The bar row (named "tabbar") — `bar.theme({ accent, textMuted, surface, border })` for this
62
+ * bar's own colors, `bar.style({...})` for its box. */
60
63
  readonly bar: UIRowType,
61
64
  }
62
65
 
@@ -72,8 +75,7 @@ export class TabsElement extends ScreenElement {
72
75
  private readonly _selectListeners: ((id: string, index: number) => void)[] = []
73
76
 
74
77
  constructor(defs: Record<string, UITabDef>) {
75
- super("screen", { bgColor: SCREEN_BG }, [])
76
- this.style({ bgColor: SCREEN_BG })
78
+ super("screen", {}, [])
77
79
  this.ids = Object.keys(defs)
78
80
  if (this.ids.length === 0) console.warn("UITabs: no tabs declared")
79
81
 
@@ -208,9 +210,9 @@ export type TabsHandle<T extends Record<string, TabDef>> = {
208
210
  * export default () => mainTabs.screen("home", [ ...content ])
209
211
  * ```
210
212
  *
211
- * Restyle through the theme — `theme({ primaryColor, mutedColor, tabbarBg, screenBg })` recolors
212
- * every bar at once. The built app passes the same keys (plus each tab's root screen) to `UITabs`,
213
- * which renders the identical bar interactively.
213
+ * The bar reads the theme's roles (`accent`, `textMuted`, `surface`, `border`), the screen its
214
+ * `bg` — one `theme({...})` colors every bar at once. The built app passes the same keys (plus
215
+ * each tab's root screen) to `UITabs`, which renders the identical bar interactively.
214
216
  */
215
217
  export function defineTabs<T extends Record<string, TabDef>>(tabs: T): TabsHandle<T> {
216
218
  const ids = Object.keys(tabs) as (keyof T & string)[]
@@ -232,7 +234,7 @@ export function defineTabs<T extends Record<string, TabDef>>(tabs: T): TabsHandl
232
234
  UIScreen(
233
235
  UIColumn(...children).style({ flexGrow: 1, p: 16, pt: "safe-top" }),
234
236
  bar(active),
235
- ).style({ bgColor: SCREEN_BG })
237
+ )
236
238
 
237
239
  return { ids, bar, screen }
238
240
  }
@@ -30,17 +30,26 @@ export const wireColor = (key: string, v: unknown): unknown => {
30
30
  * `background` is one; a string stays (the core's shorthand tells a color from a width, an image,
31
31
  * a gradient — same grammar) and a number under `border*` is a width. Every other key as is. */
32
32
  export const wireValue = (key: string, v: unknown): unknown => {
33
+ // An SVG source (`asset("./x.svg")`, `assetIcon`) as a background: the wire spelling an image
34
+ // node's src has, "svg:<markup>". (A fetch response / a file goes as it is: the runtime reads
35
+ // its handle.) The source's own tint is an image node's — a background has none.
36
+ if (isSvgSource(v)) return IMAGE_KEYS.has(key) ? "svg:" + v.svg : v
33
37
  if (COLOR_SHORTHAND_KEYS.has(key)) {
34
38
  return key === "background" && (typeof v === "number" || Array.isArray(v)) ? wireColor(key, v) : v
35
39
  }
36
40
  return COLOR_KEYS.has(key) ? wireColor(key, v) : v
37
41
  }
38
42
 
43
+ const IMAGE_KEYS = new Set(["backgroundImage", "bgImage"])
44
+
45
+ const isSvgSource = (v: unknown): v is { svg: string } =>
46
+ typeof v === "object" && v !== null && typeof (v as { svg?: unknown }).svg === "string"
47
+
39
48
  // A nested state block (a `$class`: `$pressed`, `$landscape`, `$checked`, …): a plain object that is
40
49
  // not a host handle — a backgroundImage FetchResponse / File carries `_h` (the runtime's
41
50
  // own rule, presentation/tree.cpp mergeStyleLayer).
42
51
  const isBlock = (v: unknown): v is Record<string, unknown> =>
43
- typeof v === "object" && v !== null && !Array.isArray(v) && !("_h" in v)
52
+ typeof v === "object" && v !== null && !Array.isArray(v) && !("_h" in v) && !isSvgSource(v)
44
53
 
45
54
  /** @internal A style bag → its wire form: {@link wireValue} per key, nested state blocks
46
55
  * recursively. Copy on write: the SAME object comes back when nothing changed. */
package/src/ui/theme.ts CHANGED
@@ -1,30 +1,78 @@
1
- // theme() — app theme variables (docs/ui-theme-plan.md). One global var table lives in the UI
2
- // core; `var(--key)` references resolve at style-apply time, so re-calling theme() re-styles the
3
- // LIVE UI. Bare style tokens (`pt: "comfort-top"`) are keywords that may expand to formulas
1
+ // theme() — app theme variables. One var table lives in the UI core; `var(--key)` references
2
+ // resolve at style-apply time, so re-calling theme() re-styles the LIVE UI. A node may carry
3
+ // variables of its own (`el.theme({...})`): a SCOPE its subtree reads before the app's table.
4
+ // Bare style tokens (`pt: "comfort-top"`) are keywords that may expand to formulas
4
5
  // (max(safe-top, knob)); `var(--comfort-top)` always reads the raw table value.
5
6
 
6
7
  import type { Transition } from "./presentable"
7
8
 
8
9
  export type ThemeValues = Record<string, string | number | null>
9
10
 
11
+ /** The keys the SDK itself reads — the ROLES of a theme. An app's own keys sit beside them in the
12
+ * same call; kit components (`UITabs`) and the defaults of screens and text read these and nothing
13
+ * else, so one palette colors the app and the kit alike. A color is a string, never a number. */
14
+ export type ThemeRoles = {
15
+ /** The background of every screen. */
16
+ bg?: string | null
17
+ /** A raised surface — cards, bars, sheets. The tab bar's background (without it: `bg`). */
18
+ surface?: string | null
19
+ /** Hairlines and outlines. The tab bar's top line. */
20
+ border?: string | null
21
+ /** Text that names no `color` of its own. */
22
+ text?: string | null
23
+ /** Secondary text and inactive controls. The tab bar's inactive tabs. */
24
+ textMuted?: string | null
25
+ /** The brand / active color. The tab bar's active tab. */
26
+ accent?: string | null
27
+ /** The font of text that names no `fontFamily` of its own. */
28
+ fontFamily?: string | null
29
+ }
30
+
31
+ /** What only the app-wide `theme()` takes: the comfort knobs (logical px) and the Router's default
32
+ * `replace` transition. */
33
+ export type ThemeAppKeys = {
34
+ "comfort-top"?: number | string | null
35
+ "comfort-bottom"?: number | string | null
36
+ "comfort-left"?: number | string | null
37
+ "comfort-right"?: number | string | null
38
+ replaceTransition?: Transition | null
39
+ }
40
+
10
41
  /** @internal Navigation defaults the theme owns; the router reads them at call time.
11
42
  * (SDK-side state, deliberately NOT in the host var table — `var(--replaceTransition)`
12
43
  * is not a style value.) */
13
44
  export const _navDefaults: { replace: Transition } = { replace: "none" }
14
45
 
15
- // (Unconstrained: it also maps inputs whose `replaceTransition` key was carved out.)
46
+ /** The `var(--key)` strings of the keys a `theme()` call defined — plain strings, so a helper's
47
+ * `(color = colors.accent)` parameter takes any color. */
16
48
  export type ThemeAccessors<T> = {
17
- readonly [K in keyof T]: K extends string ? `var(--${K})` : never
49
+ readonly [K in Exclude<keyof T, "replaceTransition">]: string
18
50
  }
19
51
 
20
52
  const RESERVED = ["safe-top", "safe-bottom", "safe-left", "safe-right", "vw", "vh", "vmin", "vmax"]
53
+ const APP_ONLY = ["comfort-top", "comfort-bottom", "comfort-left", "comfort-right", "replaceTransition"]
54
+
55
+ // The keys of `values` that go to the var table (a copy only when one is held back).
56
+ const tableValues = (values: Record<string, unknown>, held: (key: string) => boolean): ThemeValues => {
57
+ let send = values as ThemeValues
58
+ for (const key of Object.keys(values)) {
59
+ if (!held(key)) continue
60
+ if (send === values) send = { ...values } as ThemeValues
61
+ delete send[key]
62
+ }
63
+ return send
64
+ }
21
65
 
22
66
  /**
23
- * Merge app theme variables into the live table and get typed `var(--key)` accessors back:
67
+ * Merge app theme variables into the live table and get their `var(--key)` accessors back:
24
68
  *
25
69
  * ```ts
26
- * export const T = theme({ primaryColor: "#0A84FF", cardBg: "#1C1C1E" })
27
- * card.style({ bgColor: T.cardBg }) // T.cardBg === "var(--cardBg)"
70
+ * export const colors = theme({
71
+ * bg: "#F4F6F5", surface: "#FFFFFF", border: "#E4E8E6", // the roles the SDK reads too
72
+ * text: "#131A17", textMuted: "#606B65", accent: "#15A34A",
73
+ * accentSoft: "#E7F6ED", // the app's own keys
74
+ * })
75
+ * card.style({ bgColor: colors.surface }) // colors.surface === "var(--surface)"
28
76
  * ```
29
77
  *
30
78
  * Strings pass through as written (colors — the core parses CSS —, fonts, expressions like
@@ -34,47 +82,54 @@ const RESERVED = ["safe-top", "safe-bottom", "safe-left", "safe-right", "vw", "v
34
82
  * `replaceTransition` — the app-wide default transition for `Router.replace` (consumed SDK-side,
35
83
  * never sent to the host table).
36
84
  */
37
- const setTheme = <T extends Record<string, string | number | Transition | null>>(
85
+ const setTheme = <T extends ThemeRoles & ThemeAppKeys & Record<string, string | number | Transition | null>>(
38
86
  // Only `replaceTransition` may carry a Transition — every other key is a style value.
39
87
  values: T & { [K in Exclude<keyof T, "replaceTransition">]: string | number | null }
40
- ): ThemeAccessors<Omit<T, "replaceTransition">> => {
88
+ ): ThemeAccessors<T> => {
89
+ if ("replaceTransition" in values) _navDefaults.replace = (values.replaceTransition ?? "none") as Transition
90
+ const send = tableValues(values, (key) => {
91
+ if (key === "replaceTransition") return true // SDK-consumed, no accessor: it isn't a style var
92
+ if (!RESERVED.includes(key)) return false
93
+ console.warn(`theme(): "${key}" is an environment value (host-owned) — ignored`)
94
+ return true
95
+ })
41
96
  const accessors: Record<string, string> = {}
42
- let send = values as ThemeValues
43
- const own = () => { if (send === values) send = { ...values } as ThemeValues }
44
- const strip = (key: string) => {
45
- own()
46
- delete send[key]
47
- }
48
- for (const key of Object.keys(values)) {
49
- if (key === "replaceTransition") {
50
- _navDefaults.replace = (values[key] ?? "none") as Transition
51
- strip(key) // SDK-consumed, no accessor: it isn't a style var
52
- continue
53
- }
54
- if (RESERVED.includes(key)) {
55
- console.warn(`theme(): "${key}" is an environment value (host-owned) — ignored`)
56
- strip(key)
57
- continue
58
- }
59
- accessors[key] = `var(--${key})`
60
- }
97
+ for (const key of Object.keys(send)) accessors[key] = `var(--${key})`
61
98
  if (Object.keys(send).length > 0) _creatorTree.setTheme(send)
62
- return accessors as ThemeAccessors<Omit<T, "replaceTransition">>
99
+ return accessors as ThemeAccessors<T>
100
+ }
101
+
102
+ /** @internal `el.theme(values)`: the same variables for ONE node's subtree. */
103
+ export const _setNodeTheme = (id: number, values: Record<string, string | number | null>): void => {
104
+ const send = tableValues(values, (key) => {
105
+ const appOnly = APP_ONLY.includes(key)
106
+ if (!appOnly && !RESERVED.includes(key)) return false
107
+ console.warn(appOnly
108
+ ? `el.theme(): "${key}" is app-wide — set it with theme()`
109
+ : `el.theme(): "${key}" is an environment value (host-owned) — ignored`)
110
+ return true
111
+ })
112
+ if (id !== 0 && Object.keys(send).length > 0) _creatorTree.setNodeTheme(id, send)
63
113
  }
64
114
 
65
115
  /** App theme: callable to merge variables (`theme({...})` — re-calling re-styles the live UI, so
66
- * dark mode is just a second call), with the system vars as static properties
67
- * (`theme.primaryColor`, `theme["comfort-top"]`, …). */
116
+ * dark mode is just a second call), with the roles as static properties (`theme.accent`,
117
+ * `theme["comfort-top"]`, …) for code that has no tokens module to import. */
68
118
  export const theme = Object.assign(setTheme, {
69
- /** The app accent color. Kit components will read it via `var(--primaryColor, <fallback>)`. */
70
- primaryColor: "var(--primaryColor)",
71
- /** Secondary / inactive content color for kit components. */
72
- mutedColor: "var(--mutedColor)",
73
- /** DEFAULT text color — text without an explicit `color` follows it (hosts source their
74
- * default from the theme; unset = the host default, white on dark screens). Set it once for
75
- * a light theme instead of `color: "black"` on every label. */
76
- color: "var(--color)",
77
- /** DEFAULT text font — text without an explicit `fontFamily` follows it. */
119
+ /** The background of every screen. */
120
+ bg: "var(--bg)",
121
+ /** A raised surface — cards, bars, sheets. */
122
+ surface: "var(--surface)",
123
+ /** Hairlines and outlines. */
124
+ border: "var(--border)",
125
+ /** DEFAULT text color — text without a `color` of its own follows it (unset = the host's
126
+ * default: white). Set it once for a light theme instead of a color on every label. */
127
+ text: "var(--text)",
128
+ /** Secondary text and inactive controls. */
129
+ textMuted: "var(--textMuted)",
130
+ /** The brand / active color. */
131
+ accent: "var(--accent)",
132
+ /** DEFAULT text font — text without a `fontFamily` of its own follows it. */
78
133
  fontFamily: "var(--fontFamily)",
79
134
  /** Raw comfort knobs (logical px). The bare `comfort-top` style TOKEN applies the safe-area
80
135
  * formula — these accessors read the knob value itself, for manual composition. */
package/src/version.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  // same manifest). Its MAJOR is the bundle ↔ runtime contract: a bundle's `// sdk:` header line
5
5
  // and a host's embedded version must agree on the major for the host to run the bundle; minor
6
6
  // and patch never gate anything (features are detected, never versioned).
7
- export const SDK_VERSION = "2.0.3"
7
+ export const SDK_VERSION = "2.0.5"
8
8
 
9
9
  /** The major of a semver string, or null when it is not one. */
10
10
  export const sdkMajor = (version: string | null | undefined): number | null => {
@@ -117,6 +117,7 @@ export class FakeTree {
117
117
  if (enabled) n.classes.add(name); else n.classes.delete(name)
118
118
  }
119
119
  setTheme = (values: Record<string, any>): void => { this.rec("setTheme", values) }
120
+ setNodeTheme = (id: number, values: Record<string, any>): void => { this.rec("setNodeTheme", id, values) }
120
121
 
121
122
  // ---- presentation ----------------------------------------------------------------------------------
122
123
  private present(d: Dest, transition: number): void {
@@ -1,25 +0,0 @@
1
- export type OAuthProviderName = "google" | "apple" | "yandex" | "vk";
2
- /** What a native provider SDK yields — pass it verbatim to your endpoint / `auth.oauth.signIn`. */
3
- export type OAuthCredential = {
4
- provider: OAuthProviderName;
5
- /** OAuth access token (Yandex, VK ID). */
6
- accessToken?: string;
7
- /** OpenID identity token (Google, Apple). */
8
- idToken?: string;
9
- /** Apple only sends the name once, on the first sign-in — forward it so the server can store it. */
10
- name?: string;
11
- email?: string;
12
- };
13
- /**
14
- * Native provider sign-in. `OAuth.isSupported` reports whether this host registered the service;
15
- * `OAuth.providers()` which providers it has SDKs for. Otherwise open `auth.oauth.url(provider)` (server)
16
- * with `openURL` — the browser broker.
17
- */
18
- export declare const OAuth: {
19
- /** Whether this host registered an "oauth" service. */
20
- readonly isSupported: boolean;
21
- /** The providers this host can sign in with natively. */
22
- providers(): Promise<OAuthProviderName[]>;
23
- /** Run the provider's native sheet; resolves with the credential for `auth.oauth.signIn`. Rejects "cancelled" / "unavailable". */
24
- signIn(provider: OAuthProviderName): Promise<OAuthCredential>;
25
- };
@@ -1,56 +0,0 @@
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
- import type { Model, Query } from "../db/types";
9
- import { type AuthProvider, type SessionFields, type UserFields } from "./models";
10
- import type { AuthUser, OAuthCredential } from "./types";
11
- /** The schema of the two built-in models, for typing `auth.sessions` (developer extras aren't visible here). */
12
- type AuthSchema = {
13
- User: Model<UserFields>;
14
- Session: Model<SessionFields>;
15
- };
16
- export declare const auth: {
17
- /** The signed-in user (built-in fields; custom ones via `db.user`) or `null`. Request-scoped. */
18
- readonly user: AuthUser | null;
19
- /** The device session — exists from the first request, before any login. */
20
- readonly session: {
21
- readonly id: number;
22
- };
23
- /**
24
- * The signed-in user, or `throw new ApiError(401)`; with `match`, every given field must equal the user's
25
- * (`{ role: "admin" }`, `{ plan: "pro" }`) or it's a 403.
26
- */
27
- requireUser(match?: Record<string, unknown>): AuthUser;
28
- email: {
29
- /** Emails a 6-digit code to `email` (always resolves — no "email exists" oracle; rate-limited). Needs `"email"` in app.json providers. */
30
- sendCode: (email: string) => Promise<void>;
31
- /** The only email sign-in: checks the code sent to this device session, finds/creates the `User`, binds the session. */
32
- verify: (email: string, code: string) => Promise<AuthUser>;
33
- };
34
- oauth: {
35
- /** 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. */
36
- url: (provider: Exclude<AuthProvider, "email">) => Promise<string>;
37
- /**
38
- * Sign in with a credential a NATIVE provider SDK produced in the app (client `OAuth.signIn(provider)`):
39
- * the platform verifies it with the provider, finds/links/creates the `User`, binds this session. The
40
- * in-app counterpart of the browser broker — same accounts either way.
41
- */
42
- signIn: (provider: Exclude<AuthProvider, "email">, credential: OAuthCredential) => Promise<AuthUser>;
43
- };
44
- /** Escape hatch for developer-verified identities (Telegram initData, SSO): binds this session to `userId`. */
45
- signIn: (userId: number) => Promise<AuthUser>;
46
- /** Unbinds the user; the device stays a guest with the same `session.id` (token rotated). */
47
- signOut: () => Promise<void>;
48
- /** The current user's sessions ("my devices"): a `db.session` query — `await`, `.select()`, `.where(...).updateMany({ revoked: true })`. */
49
- readonly sessions: Query<AuthSchema, "Session">;
50
- /** Schema-time factories — see ./models.ts. */
51
- model: {
52
- user: <E extends import("../db/types").Fields = {}>(extra?: E) => import("./models").AuthModel<UserFields & E>;
53
- session: <E extends import("../db/types").Fields = {}>(extra?: E) => import("./models").AuthModel<SessionFields & E>;
54
- };
55
- };
56
- export type { AuthUser };
@@ -1,61 +0,0 @@
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
- })()
@@ -1,80 +0,0 @@
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 }