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.
- package/README.md +104 -76
- package/dist/global.d.ts +3 -5
- package/dist/host.d.ts +3 -0
- package/dist/types/inject.d.ts +4 -4
- package/dist/types/net/codec.d.ts +3 -2
- package/dist/types/net/core.d.ts +10 -1
- package/dist/types/net/index.d.ts +22 -5
- package/dist/types/net/replication.d.ts +23 -2
- package/dist/types/runtime/device.d.ts +7 -0
- package/dist/types/runtime/rpc.d.ts +11 -17
- package/dist/types/runtime/wire.d.ts +53 -0
- package/dist/types/server/auth/api.d.ts +42 -0
- package/dist/types/server/auth/appConfig.d.ts +1 -5
- package/dist/types/server/auth/models.d.ts +119 -70
- package/dist/types/server/auth/types.d.ts +19 -43
- package/dist/types/server/channel.d.ts +57 -19
- package/dist/types/server/context.d.ts +2 -2
- package/dist/types/server/db/defineDb.d.ts +10 -0
- package/dist/types/server/db/index.d.ts +1 -1
- package/dist/types/server/db/types.d.ts +76 -6
- package/dist/types/server/inject.d.ts +0 -1
- package/dist/types/ui/UINode.d.ts +19 -5
- package/dist/types/ui/UIScreen.d.ts +1 -0
- package/dist/types/ui/UITabs.d.ts +8 -6
- package/dist/types/ui/theme.d.ts +48 -13
- package/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +4 -2
- package/prompts/README.md +1 -1
- package/prompts/design.md +19 -19
- package/prompts/dist/2d-game.md +45 -31
- package/prompts/dist/3d-app.md +45 -31
- package/prompts/dist/ar-app.md +45 -31
- package/prompts/dist/design.md +25 -24
- package/prompts/dist/ui-app.md +45 -31
- package/prompts/ui-design.md +6 -5
- package/prompts/ui.md +25 -22
- package/src/animate/tween/read.ts +146 -0
- package/src/bridges/device.d.ts +9 -0
- package/src/bridges/tree.d.ts +5 -0
- package/src/canvas/gen/cssColor.ts +1 -1
- package/src/canvas/gen/recorder.ts +1 -1
- package/src/canvas/gen/spec.ts +1 -1
- package/src/chisel.ts +1 -1
- package/src/compile/bundler.ts +6 -0
- package/src/compile/compileProject.ts +3 -1
- package/src/compile/index.ts +3 -1
- package/src/compile/serverSplit.ts +58 -11
- package/src/compile/serverTypes.ts +189 -8
- package/src/host.d.ts +3 -0
- package/src/inject.ts +7 -7
- package/src/net/codec.ts +19 -10
- package/src/net/core.ts +18 -5
- package/src/net/index.ts +30 -9
- package/src/net/replication.ts +63 -28
- package/src/runtime/device.ts +12 -0
- package/src/runtime/rpc.ts +101 -40
- package/src/runtime/wire.ts +35 -0
- package/src/server/auth/api.ts +94 -0
- package/src/server/auth/appConfig.ts +2 -3
- package/src/server/auth/host.ts +244 -174
- package/src/server/auth/models.ts +45 -62
- package/src/server/auth/types.ts +19 -34
- package/src/server/channel.ts +97 -29
- package/src/server/channelHub.ts +153 -0
- package/src/server/context.ts +2 -2
- package/src/server/db/defineDb.ts +96 -36
- package/src/server/db/index.ts +1 -1
- package/src/server/db/types.ts +76 -8
- package/src/server/host.ts +25 -10
- package/src/server/inject.ts +2 -2
- package/src/server/runtime.ts +34 -12
- package/src/ui/UINode.ts +22 -5
- package/src/ui/UIScreen.ts +5 -0
- package/src/ui/UITabs.ts +19 -17
- package/src/ui/styleColor.ts +10 -1
- package/src/ui/theme.ts +96 -41
- package/src/version.ts +1 -1
- package/tests/helpers/fakeTree.ts +1 -0
- package/dist/types/plugins/oauth.d.ts +0 -25
- package/dist/types/server/auth/global.d.ts +0 -56
- package/src/plugins/oauth.ts +0 -61
- package/src/server/auth/global.ts +0 -80
- 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
|
-
/**
|
|
16
|
-
*
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
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).
|
|
46
|
-
*
|
|
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") —
|
|
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", {
|
|
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
|
-
*
|
|
212
|
-
* every bar at once. The built app passes the same keys (plus
|
|
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
|
-
)
|
|
237
|
+
)
|
|
236
238
|
|
|
237
239
|
return { ids, bar, screen }
|
|
238
240
|
}
|
package/src/ui/styleColor.ts
CHANGED
|
@@ -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
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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
|
-
|
|
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]:
|
|
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
|
|
67
|
+
* Merge app theme variables into the live table and get their `var(--key)` accessors back:
|
|
24
68
|
*
|
|
25
69
|
* ```ts
|
|
26
|
-
* export const
|
|
27
|
-
*
|
|
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<
|
|
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
|
-
|
|
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<
|
|
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
|
|
67
|
-
*
|
|
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
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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.
|
|
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 };
|
package/src/plugins/oauth.ts
DELETED
|
@@ -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 }
|