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
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `db.auth` — the BUNDLE side of sign-in, what `*.server.ts` code calls on a db made with
3
+ * `defineDb({...}).withAuth({ model: "User" })`:
4
+ *
5
+ * const db = defineDb({ User, Post }).withAuth({ model: "User" })
6
+ *
7
+ * export async function register(login: string, password: string, name: string) {
8
+ * await db.auth.signUpWithPassword(login, password, { name })
9
+ * }
10
+ * export async function me() { return db.auth.user().select({ id: true, name: true }) }
11
+ * export async function removePost(id: number) {
12
+ * await db.auth.requireUser({ role: "admin" }).select({ id: true })
13
+ * …
14
+ * }
15
+ *
16
+ * The platform knows a request's session and the ID of its user (the host resolved the token before
17
+ * the endpoint ran). Everything else about a user is a row of the project's own model, and is READ:
18
+ * `user()` / `requireUser()` are queries over that model — typed by it, run when awaited — narrowed to
19
+ * the user of the request. Sign-in operations forward to the host's `AuthOps` on
20
+ * `globalThis.__lecodesAuth` (a seam: the compiled bundle carries its own SDK copy).
21
+ */
22
+ /** The collections of a db with auth, by role (defineDb.ts). */
23
+ export type AuthCollections = {
24
+ model: string;
25
+ user: any;
26
+ session: any;
27
+ identity: any;
28
+ };
29
+ export declare const createAuthApi: (tables: () => AuthCollections) => {
30
+ user: () => any;
31
+ requireUser: (where?: Record<string, unknown>) => any;
32
+ readonly sessionId: number;
33
+ signUpWithPassword: (login: string, password: string, data?: Record<string, unknown>) => Promise<void>;
34
+ signInWithPassword: (login: string, password: string) => Promise<void>;
35
+ setPassword: (password: string) => Promise<void>;
36
+ sendCode: (email: string) => Promise<void>;
37
+ signInWithCode: (email: string, code: string) => Promise<void>;
38
+ signIn: (userId: number) => Promise<void>;
39
+ signOut: () => Promise<void>;
40
+ readonly sessions: any;
41
+ readonly identities: any;
42
+ };
@@ -1,16 +1,12 @@
1
1
  /**
2
- * The project's `app.json` subset the server runtime reads (name, auth providers).
2
+ * The project's `app.json` subset the server runtime reads: the app's name (the sign-in mail).
3
3
  *
4
4
  * This module is a placeholder the COMPILER REPLACES: `compileServerBundle({ app })` puts a module with the
5
5
  * real values at this same path in chisel's file map (project files win over SDK files), so every SDK module
6
6
  * that imports `app` gets the project's config as a plain dependency — no globals, no evaluation-order games.
7
7
  * Outside a compiled bundle (tests, tooling) `globalThis.__lecodesApp` can stand in.
8
8
  */
9
- export type AuthProvider = "email" | "google" | "apple" | "yandex" | "vk";
10
9
  export type AppConfig = {
11
10
  name?: string;
12
- auth?: {
13
- providers?: AuthProvider[];
14
- };
15
11
  };
16
12
  export declare const app: AppConfig;
@@ -1,31 +1,55 @@
1
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.
2
+ * The two models sign-in adds to a database — `defineDb({...}).withAuth({ model: "User" })`:
7
3
  *
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).
4
+ * Session one device: the hash of its token, and the user it is signed in as (none = a guest)
5
+ * Identity one way a user signs in: `email:<address>` or `login:<name>` (with the password's hash
6
+ * when one is set) — several per user.
7
+ *
8
+ * They are the PLATFORM's: a project never declares them and never writes to them — it may refer to a
9
+ * session (`t.one("Session")`: a guest's cart) and read both through `db.auth.sessions` /
10
+ * `db.auth.identities`, which leave the secrets out. The user model is the project's own, an ordinary
11
+ * `model({...})`: the platform creates a row of it the first time someone signs in and knows nothing
12
+ * of it but the id.
10
13
  */
11
- import { type Field } from "../db/fields";
12
- import type { Fields, Model } from "../db/types";
13
- import { type AppConfig, type AuthProvider } from "./appConfig";
14
- export type { AppConfig, AuthProvider };
15
- /** The project's `app.json` subset: the compiled-in module, else the test/tooling seam, else empty. */
16
- export declare const appConfig: () => AppConfig;
17
- export declare const enabledProviders: () => AuthProvider[];
18
- /** The model name the auth runtime binds to. */
19
- export declare const AUTH_USER_MODEL = "User";
14
+ export type { AppConfig } from "./appConfig";
15
+ /** What an `Identity` row is of: an email, or a login (a name with no address behind it). */
16
+ export type IdentityProvider = "email" | "login";
20
17
  export declare const AUTH_SESSION_MODEL = "Session";
21
- /** Marker on the two auth models (`model()` objects are otherwise plain). */
22
- export type AuthModelKind = "user" | "session";
23
- export type AuthModel<F extends Fields> = Model<F> & {
24
- readonly __auth: AuthModelKind;
18
+ export declare const AUTH_IDENTITY_MODEL = "Identity";
19
+ /** A session as a project reads it (`db.auth.sessions`, a `t.one("Session")` relation). */
20
+ export declare const sessionPublicFields: () => {
21
+ createdAt: import("../db").Field<number, {
22
+ kind: "scalar";
23
+ scalar: "date";
24
+ optional: false;
25
+ hasDefault: true;
26
+ array: false;
27
+ isId: false;
28
+ model: "";
29
+ }>;
30
+ lastSeenAt: import("../db").Field<number, {
31
+ kind: "scalar";
32
+ scalar: "date";
33
+ optional: false;
34
+ hasDefault: true;
35
+ array: false;
36
+ isId: false;
37
+ model: "";
38
+ }>;
39
+ revoked: import("../db").Field<boolean, {
40
+ kind: "scalar";
41
+ scalar: "bool";
42
+ optional: false;
43
+ hasDefault: true;
44
+ array: false;
45
+ isId: false;
46
+ model: "";
47
+ }>;
25
48
  };
26
- /** Built-in `User` fields (what `auth.user` is typed with). */
27
- export declare const userFields: () => {
28
- email: Field<string, {
49
+ export type SessionPublicFields = ReturnType<typeof sessionPublicFields>;
50
+ /** The whole `Session`; `user` points at the model `withAuth` named. */
51
+ export declare const sessionFields: (userModel: string) => {
52
+ pendingEmail: import("../db").Field<string, {
29
53
  kind: "scalar";
30
54
  scalar: "string";
31
55
  optional: true;
@@ -34,7 +58,7 @@ export declare const userFields: () => {
34
58
  isId: false;
35
59
  model: "";
36
60
  }>;
37
- name: Field<string, {
61
+ codeHash: import("../db").Field<string, {
38
62
  kind: "scalar";
39
63
  scalar: "string";
40
64
  optional: true;
@@ -43,45 +67,34 @@ export declare const userFields: () => {
43
67
  isId: false;
44
68
  model: "";
45
69
  }>;
46
- avatar: Field<string, {
70
+ codeExpires: import("../db").Field<number, {
47
71
  kind: "scalar";
48
- scalar: "string";
72
+ scalar: "date";
49
73
  optional: true;
50
74
  hasDefault: false;
51
75
  array: false;
52
76
  isId: false;
53
77
  model: "";
54
78
  }>;
55
- /** Plain string column, `"user"` by default — `auth.requireUser({ role: "admin" })` is field equality. */
56
- role: Field<string, {
79
+ codeAttempts: import("../db").Field<number, {
57
80
  kind: "scalar";
58
- scalar: "string";
81
+ scalar: "int";
59
82
  optional: false;
60
83
  hasDefault: true;
61
84
  array: false;
62
85
  isId: false;
63
86
  model: "";
64
87
  }>;
65
- createdAt: Field<number, {
66
- kind: "scalar";
67
- scalar: "date";
68
- optional: false;
69
- hasDefault: true;
88
+ user: import("../db").Field<never, {
89
+ kind: "one";
90
+ scalar: "";
91
+ optional: true;
92
+ hasDefault: false;
70
93
  array: false;
71
94
  isId: false;
72
- model: "";
95
+ model: string;
73
96
  }>;
74
- };
75
- export type UserFields = ReturnType<typeof userFields>;
76
- /** Built-in `Session` fields. `user` is added by `defineDb` when a `User` model exists (schema-time, so a
77
- * db without an auth user model has sessions without a user column). */
78
- export declare const sessionFields: () => {
79
- /** sha256 of the bearer token the device holds — the token itself is never stored. */
80
- tokenHash: Field<string, import("../db/fields").Meta<{
81
- kind: "scalar";
82
- scalar: "string";
83
- }>>;
84
- createdAt: Field<number, {
97
+ createdAt: import("../db").Field<number, {
85
98
  kind: "scalar";
86
99
  scalar: "date";
87
100
  optional: false;
@@ -90,7 +103,7 @@ export declare const sessionFields: () => {
90
103
  isId: false;
91
104
  model: "";
92
105
  }>;
93
- lastSeenAt: Field<number, {
106
+ lastSeenAt: import("../db").Field<number, {
94
107
  kind: "scalar";
95
108
  scalar: "date";
96
109
  optional: false;
@@ -99,7 +112,7 @@ export declare const sessionFields: () => {
99
112
  isId: false;
100
113
  model: "";
101
114
  }>;
102
- revoked: Field<boolean, {
115
+ revoked: import("../db").Field<boolean, {
103
116
  kind: "scalar";
104
117
  scalar: "bool";
105
118
  optional: false;
@@ -108,16 +121,54 @@ export declare const sessionFields: () => {
108
121
  isId: false;
109
122
  model: "";
110
123
  }>;
111
- pendingEmail: Field<string, {
124
+ /** sha256 of the bearer token the device holds — the token itself is never stored. */
125
+ tokenHash: import("../db").Field<string, import("../db/fields").Meta<{
112
126
  kind: "scalar";
113
127
  scalar: "string";
128
+ }>>;
129
+ };
130
+ /** A sign-in method as a project reads it (`db.auth.identities`) — no hash. */
131
+ export declare const identityPublicFields: () => {
132
+ /** `"email" | "login"` */
133
+ provider: import("../db").Field<string, import("../db/fields").Meta<{
134
+ kind: "scalar";
135
+ scalar: "string";
136
+ }>>;
137
+ /** What the person types to sign in: the address, or the login. */
138
+ subject: import("../db").Field<string, import("../db/fields").Meta<{
139
+ kind: "scalar";
140
+ scalar: "string";
141
+ }>>;
142
+ /** When the subject was proven (a code). Unset = a password sign-up whose address wasn't confirmed. */
143
+ verifiedAt: import("../db").Field<number, {
144
+ kind: "scalar";
145
+ scalar: "date";
114
146
  optional: true;
115
147
  hasDefault: false;
116
148
  array: false;
117
149
  isId: false;
118
150
  model: "";
119
151
  }>;
120
- codeHash: Field<string, {
152
+ createdAt: import("../db").Field<number, {
153
+ kind: "scalar";
154
+ scalar: "date";
155
+ optional: false;
156
+ hasDefault: true;
157
+ array: false;
158
+ isId: false;
159
+ model: "";
160
+ }>;
161
+ };
162
+ export type IdentityPublicFields = ReturnType<typeof identityPublicFields>;
163
+ /** The whole `Identity`. */
164
+ export declare const identityFields: (userModel: string) => {
165
+ /** `<provider>:<subject>` — marcidb has no composite @unique, this is the one-row-per-identity guarantee. */
166
+ key: import("../db").Field<string, import("../db/fields").Meta<{
167
+ kind: "scalar";
168
+ scalar: "string";
169
+ }>>;
170
+ /** scrypt hash of the password (`email` / `login` identities). */
171
+ hash: import("../db").Field<string, {
121
172
  kind: "scalar";
122
173
  scalar: "string";
123
174
  optional: true;
@@ -126,7 +177,18 @@ export declare const sessionFields: () => {
126
177
  isId: false;
127
178
  model: "";
128
179
  }>;
129
- codeExpires: Field<number, {
180
+ /** `"email" | "login"` */
181
+ provider: import("../db").Field<string, import("../db/fields").Meta<{
182
+ kind: "scalar";
183
+ scalar: "string";
184
+ }>>;
185
+ /** What the person types to sign in: the address, or the login. */
186
+ subject: import("../db").Field<string, import("../db/fields").Meta<{
187
+ kind: "scalar";
188
+ scalar: "string";
189
+ }>>;
190
+ /** When the subject was proven (a code). Unset = a password sign-up whose address wasn't confirmed. */
191
+ verifiedAt: import("../db").Field<number, {
130
192
  kind: "scalar";
131
193
  scalar: "date";
132
194
  optional: true;
@@ -135,30 +197,17 @@ export declare const sessionFields: () => {
135
197
  isId: false;
136
198
  model: "";
137
199
  }>;
138
- codeAttempts: Field<number, {
200
+ createdAt: import("../db").Field<number, {
139
201
  kind: "scalar";
140
- scalar: "int";
202
+ scalar: "date";
141
203
  optional: false;
142
204
  hasDefault: true;
143
205
  array: false;
144
206
  isId: false;
145
207
  model: "";
146
208
  }>;
209
+ user: import("../db").Field<never, import("../db/fields").Meta<{
210
+ kind: "one";
211
+ model: string;
212
+ }>>;
147
213
  };
148
- export type SessionFields = ReturnType<typeof sessionFields> & {
149
- user: ReturnType<typeof sessionUserField>;
150
- };
151
- export declare const sessionUserField: () => Field<never, {
152
- kind: "one";
153
- scalar: "";
154
- optional: true;
155
- hasDefault: false;
156
- array: false;
157
- isId: false;
158
- model: "User";
159
- }>;
160
- /** `auth.model.user({...extra})` — the built-in user (+ provider ids from app.json) plus the developer's fields. */
161
- export declare const authUserModel: <E extends Fields = {}>(extra?: E) => AuthModel<UserFields & E>;
162
- /** `auth.model.session({...extra})` — declare it only to relate to sessions (`t.one("Session")`) or to add fields. */
163
- export declare const authSessionModel: <E extends Fields = {}>(extra?: E) => AuthModel<SessionFields & E>;
164
- export declare const isAuthModel: (m: unknown, kind?: AuthModelKind) => m is AuthModel<any>;
@@ -1,56 +1,32 @@
1
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`.
2
+ * Shared shapes of the auth layer. Two sides meet through them:
3
+ * - the BUNDLE side (`db.auth`, ./api.ts) — what `*.server.ts` code calls;
4
+ * - the HOST side (./host.ts) — sessions, passwords and email codes, run by the runner with the
5
+ * project's db handle. Installed on `globalThis.__lecodesAuth`.
6
6
  */
7
7
  import type { RequestContext } from "../context";
8
- import type { AuthProvider } from "./models";
9
- /** The built-in user row — what `auth.user` is typed with (custom fields exist at runtime; select them via `db.user`). */
10
- export type AuthUser = {
11
- id: number;
12
- email: string | null;
13
- name: string | null;
14
- avatar: string | null;
15
- role: string;
16
- createdAt: number;
17
- };
18
- /** Per-request auth state, resolved by the host before the endpoint runs (`RequestContext.auth`). */
8
+ /** Per-request auth state, resolved by the host before the endpoint runs (`RequestContext.auth`): the
9
+ * device's session and the id of the user it is signed in as. Nothing of the user's row — a project
10
+ * reads that itself (`db.auth.user()`). */
19
11
  export type AuthState = {
20
12
  sessionId: number;
21
13
  userId: number | null;
22
- user: AuthUser | null;
23
14
  /** A token to hand back to the client in this response (a new guest session, or a rotation). */
24
15
  issuedToken?: string;
25
16
  };
26
- /** What an OAuth provider hands the runner after a successful web sign-in. */
27
- export type OAuthProfile = {
28
- sub: string;
29
- email?: string | null;
30
- emailVerified?: boolean;
31
- name?: string | null;
32
- avatar?: string | null;
33
- };
34
- /** A credential a NATIVE provider SDK produced in the app (client plugin `OAuth.signIn`): an access token
35
- * (Yandex, VK ID) or an OpenID identity token (Google, Apple). Verified server-side, never trusted as-is. */
36
- export type OAuthCredential = {
37
- accessToken?: string;
38
- idToken?: string;
39
- name?: string | null;
40
- email?: string | null;
41
- };
42
- /** Host operations the bundle's `auth` global forwards to (all request-scoped through `ctx`). */
17
+ /** Host operations `db.auth` forwards to (all request-scoped through `ctx`). A sign-in resolves once the
18
+ * session is bound; who it is bound to is read through `db.auth.user()`. */
43
19
  export type AuthOps = {
44
- email: {
45
- sendCode(ctx: RequestContext, email: string): Promise<void>;
46
- verify(ctx: RequestContext, email: string, code: string): Promise<AuthUser>;
47
- };
48
- oauth: {
49
- url(ctx: RequestContext, provider: Exclude<AuthProvider, "email">): Promise<string>;
50
- signIn(ctx: RequestContext, provider: Exclude<AuthProvider, "email">, credential: OAuthCredential): Promise<AuthUser>;
51
- };
52
- signIn(ctx: RequestContext, userId: number): Promise<AuthUser>;
20
+ /** `data` = the fields of the new user's row (the project's own model). */
21
+ signUpWithPassword(ctx: RequestContext, login: string, password: string, data?: Record<string, unknown>): Promise<void>;
22
+ signInWithPassword(ctx: RequestContext, login: string, password: string): Promise<void>;
23
+ setPassword(ctx: RequestContext, password: string): Promise<void>;
24
+ sendCode(ctx: RequestContext, email: string): Promise<void>;
25
+ signInWithCode(ctx: RequestContext, email: string, code: string): Promise<void>;
26
+ signIn(ctx: RequestContext, userId: number): Promise<void>;
53
27
  signOut(ctx: RequestContext): Promise<void>;
54
- /** `db.session` filtered to the current user's sessions (or just this device's, when signed out). */
28
+ /** The sessions of the current user (or just this device's, when signed out), as a query. */
55
29
  sessions(ctx: RequestContext): any;
30
+ /** The current user's sign-in methods, as a query — the hash is not in it. */
31
+ identities(ctx: RequestContext): any;
56
32
  };
@@ -1,28 +1,66 @@
1
1
  /**
2
- * Server side of channels (docs/backend-plan.md §3.3): typed pub/sub, server → client only.
2
+ * Channels: the server's messages to the app. A server function is a call from the app to the
3
+ * server; a channel is the other direction, typed by the same export.
3
4
  *
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 })
5
+ * export const postsChannel = channel<Post>() // to everyone listening
6
+ * export const noticesChannel = channel<Notice>() // to each their own
7
+ * .groupBy(async () => (await db.auth.requireUser().select({ id: true })).id)
8
+ *
9
+ * postsChannel.publish(post) // server
10
+ * noticesChannel.publish(userId, notice)
11
+ *
12
+ * postsChannel.subscribe(post => …) // app
13
+ * noticesChannel.subscribe(notice => …)
14
+ *
15
+ * `groupBy` runs on the server when an app subscribes, with what the app passed to `subscribe`
16
+ * before the handler, and answers the group that subscriber is in; `publish(group, message)` reaches
17
+ * that group. `authorize` only decides who may listen. Both refuse by throwing (`ApiError`), and
18
+ * both run in the request scope of the subscriber, so `db.auth` works in them.
7
19
  *
8
20
  * A channel's identity is its export (`<path>#<name>`), assigned by the runtime when the bundle is
9
21
  * 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).
22
+ * `globalThis.__lecodesPublish` seam the host installs (same reason as ./context.ts: the bundle
23
+ * carries its own SDK copy). In the app the export is another object altogether — the proxy
24
+ * `__channel` of src/runtime/rpc.ts — which is why `subscribe` here only throws.
12
25
  */
13
- export type ChannelEvents = Record<string, unknown>;
14
- export type ChannelOptions = {
15
- /** Called when a client subscribes to `topic`; throw (e.g. `ApiError(403)`) to refuse. */
16
- onJoin?: (topic: string) => void | Promise<void>;
26
+ import type { ChannelGroup } from "../runtime/wire";
27
+ export type { ChannelGroup };
28
+ /** What `subscribe` answers: the subscription lives until `close()`. */
29
+ export type ChannelSubscription = {
30
+ close(): void;
17
31
  };
18
- export interface Channel<E extends ChannelEvents> {
19
- readonly options: ChannelOptions;
20
- /** Deliver `event` with `payload` to every subscriber of `topic` (at-most-once). */
21
- publish<K extends keyof E & string>(topic: string, event: K, payload: E[K]): void;
32
+ export type SubscribeOptions = {
33
+ /** The connection dropped and came back: what was published in between is lost — read the state again. */
34
+ reconnect?: () => void;
35
+ /** The server refused the subscription (a hook threw): an `RpcError`, its `status` the hook's.
36
+ * Without it the refusal is logged. The subscription is not over: the server is asked again
37
+ * when the session changes, so this may be called more than once. */
38
+ error?: (e: Error & {
39
+ readonly status: number;
40
+ }) => void;
41
+ };
42
+ /** A channel every subscriber hears alike. */
43
+ export interface Channel<M> {
44
+ /** Who may listen: runs on the server at every subscription, throw to refuse. */
45
+ authorize(check: () => void | Promise<void>): Channel<M>;
46
+ /** Split the subscribers: `group` runs on the server at every subscription — with what the app
47
+ * passed to `subscribe` — and answers the group of that subscriber. Throw to refuse. */
48
+ groupBy<K extends ChannelGroup, A extends unknown[] = []>(group: (...args: A) => K | Promise<K>): GroupedChannel<M, K, A>;
49
+ /** Server: send `message` to every subscriber (at most once — nothing is kept for a socket that is away). */
50
+ publish(message: M): void;
51
+ /** App: listen until `close()`. */
52
+ subscribe(handler: (message: M) => void, options?: SubscribeOptions): ChannelSubscription;
53
+ }
54
+ /** A channel whose subscribers are in groups (`groupBy`). */
55
+ export interface GroupedChannel<M, K extends ChannelGroup, A extends unknown[]> {
56
+ /** Server: send `message` to the subscribers of `group`. */
57
+ publish(group: K, message: M): void;
58
+ /** App: listen until `close()`; the arguments before the handler go to the channel's `groupBy`. */
59
+ subscribe(...args: [...args: A, handler: (message: M) => void, options?: SubscribeOptions]): ChannelSubscription;
22
60
  }
23
- type Publisher = (channelId: string, topic: string, event: string, payload: unknown) => void;
24
- /** Host hook (runner / test harness): where `publish` delivers to. */
61
+ type Publisher = (channelId: string, group: ChannelGroup | null, message: unknown) => void;
62
+ /** Host hook (runner / local backend / test harness): where `publish` delivers to. */
25
63
  export declare const setChannelPublisher: (publisher: Publisher | null) => void;
26
- export declare const channel: <E extends ChannelEvents>(options?: ChannelOptions) => Channel<E>;
27
- export declare const isChannel: (v: unknown) => v is Channel<any>;
28
- export {};
64
+ export declare const isChannelGroup: (v: unknown) => v is ChannelGroup;
65
+ export declare const channel: <M = unknown>() => Channel<M>;
66
+ export declare const isChannel: (v: unknown) => v is ChannelRecord;
@@ -6,13 +6,13 @@
6
6
  * timers after the response) every member is undefined.
7
7
  */
8
8
  export type RequestContext = {
9
- /** Endpoint id `<path>#<export>` (or the channel id for `onJoin`). */
9
+ /** Endpoint id `<path>#<export>` (or the channel id for a subscription's hooks). */
10
10
  id: string;
11
11
  headers: Record<string, string>;
12
12
  ip?: string;
13
13
  /** Bearer session token as sent by the client transport (raw; auth resolves it). */
14
14
  sessionToken?: string;
15
- /** Set by the auth runtime after resolving the token — read via `auth.*`, not here. */
15
+ /** Set by the auth runtime after resolving the token — read via `db.auth`, not here. */
16
16
  auth?: unknown;
17
17
  };
18
18
  type Provider = () => RequestContext | undefined;
@@ -32,5 +32,15 @@ export declare const toMarci: (metas: Record<string, ModelMeta>) => string;
32
32
  export type DefineDbOptions = {
33
33
  transport?: MarciTransport;
34
34
  };
35
+ /**
36
+ * The schema registry: model name = key (`db.user` ← `User`). Relation strings are checked against the
37
+ * keys both at the type level (`ValidateRefs`) and at runtime.
38
+ *
39
+ * A db is SEALED by its first use — a query, a read of its schema, the host taking it after the file
40
+ * has loaded. Until then `.withAuth({ model })` may still change what it holds: it adds the platform's
41
+ * `Session` and `Identity` models (../auth/models.ts) pointing at the project's model of a user, and
42
+ * gives the db its `auth`. So `withAuth` belongs to the definition, written on `defineDb(...)` itself;
43
+ * on a db that has been used it throws.
44
+ */
35
45
  export declare const defineDb: <const S extends Schema>(declared: S & ValidateRefs<S>, options?: DefineDbOptions) => Db<S>;
36
46
  export {};
@@ -2,5 +2,5 @@ export { t } from "./fields";
2
2
  export type { Field, FieldDef, FieldMeta } from "./fields";
3
3
  export { model, defineDb, ref, setDbTransport, toMarci } from "./defineDb";
4
4
  export type { DefineDbOptions } from "./defineDb";
5
- export type { Db, Model, Schema, Row, Id, Insert, Update, Select, Where, Query, QueryObject, Result, Collection, ScalarSelect, Types } from "./types";
5
+ export type { Auth, Db, DbWithAuth, UserQuery, Model, Schema, Row, Id, Insert, Update, Select, Where, Query, QueryObject, Result, Collection, ScalarSelect, Types } from "./types";
6
6
  export type { Op, MarciOp, MarciTransport, JsonValue, Sub } from "./marci/query";