lecodes-sdk 2.0.4 → 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 +2 -5
- package/dist/host.d.ts +3 -0
- package/dist/types/inject.d.ts +3 -3
- 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 +3 -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/bridges/device.d.ts +9 -0
- package/src/bridges/tree.d.ts +5 -0
- 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 +6 -6
- 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
|
@@ -1,31 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `
|
|
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
|
-
*
|
|
9
|
-
*
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
export
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
+
codeExpires: import("../db").Field<number, {
|
|
47
71
|
kind: "scalar";
|
|
48
|
-
scalar: "
|
|
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
|
-
|
|
56
|
-
role: Field<string, {
|
|
79
|
+
codeAttempts: import("../db").Field<number, {
|
|
57
80
|
kind: "scalar";
|
|
58
|
-
scalar: "
|
|
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
|
-
|
|
66
|
-
kind: "
|
|
67
|
-
scalar: "
|
|
68
|
-
optional:
|
|
69
|
-
hasDefault:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
200
|
+
createdAt: import("../db").Field<number, {
|
|
139
201
|
kind: "scalar";
|
|
140
|
-
scalar: "
|
|
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
|
|
3
|
-
* - the BUNDLE side (`auth
|
|
4
|
-
* - the HOST side (./host.ts) — sessions,
|
|
5
|
-
* project's db handle
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
/**
|
|
27
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
*
|
|
5
|
-
* export const
|
|
6
|
-
*
|
|
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
|
|
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
|
-
|
|
14
|
-
export type
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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,
|
|
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
|
|
27
|
-
export declare const
|
|
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
|
|
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
|
|
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";
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Naming: `S` = the schema record (model name → Model), `N` = a model name, `F` = a fields record.
|
|
8
8
|
*/
|
|
9
9
|
import type { Field, FieldMeta } from "./fields";
|
|
10
|
+
import type { IdentityPublicFields, SessionPublicFields } from "../auth/models";
|
|
10
11
|
import type { CompareNumValue, CompareRefListValue, CompareRefValue, CompareStrValue, CompareValue, CustomSearchValue, FullTextSearch, JsonCondition, JsonPathWhere, Op, PrimitiveListUpdate, Query as MarciQuery, RefListUpdate, RefListUpdateOrdered, RefUpdate, RefUpdateStruct, Rows, Sub, UpdateNumValue, VectorSearch, WhereValue } from "./marci/query";
|
|
11
12
|
export type Fields = Record<string, Field<any, any>>;
|
|
12
13
|
export type Model<F extends Fields = Fields> = {
|
|
@@ -86,7 +87,7 @@ type StructSelect<S, F extends Fields> = {
|
|
|
86
87
|
[K in keyof F]?: FieldSelect<S, F[K]>;
|
|
87
88
|
};
|
|
88
89
|
type ScalarWhere<T, M extends FieldMeta> = Nullable<M, CompareValue<T>> | (IsNum<M> extends true ? CompareNumValue<T> : M["scalar"] extends "string" ? CompareStrValue<T> : never);
|
|
89
|
-
type FieldWhere<S, Fld, M extends FieldMeta = MetaOf<Fld>> = M["kind"] extends "one" ? CompareRefValue<Where<S, ModelName<S, Fld>> | null> : M["kind"] extends "many" | "list" ? CompareRefListValue<Where<S, ModelName<S, Fld>>> : M["kind"] extends "struct" ? CompareRefValue<WhereValue<StructWhereFields<S, StructFieldsOf<Fld>>> | null> : M["kind"] extends "json" ? JsonPathWhere | JsonCondition : M["array"] extends true ?
|
|
90
|
+
type FieldWhere<S, Fld, M extends FieldMeta = MetaOf<Fld>> = M["kind"] extends "one" ? CompareRefValue<Where<S, ModelName<S, Fld>> | null> : M["kind"] extends "many" | "list" ? CompareRefListValue<Where<S, ModelName<S, Fld>>> : M["kind"] extends "struct" ? CompareRefValue<WhereValue<StructWhereFields<S, StructFieldsOf<Fld>>> | null> : M["kind"] extends "json" ? JsonPathWhere | JsonCondition : M["array"] extends true ? TsOf<Fld>[] : ScalarWhere<TsOf<Fld>, M> | CustomSearchValue<FullTextSearch> | CustomSearchValue<VectorSearch>;
|
|
90
91
|
type WhereFieldsOf<S, F extends Fields> = {
|
|
91
92
|
[K in keyof F]?: FieldWhere<S, F[K]>;
|
|
92
93
|
};
|
|
@@ -95,9 +96,15 @@ type IdWhere<S, N extends keyof S> = {
|
|
|
95
96
|
};
|
|
96
97
|
export type Where<S, N extends keyof S> = WhereValue<WhereFieldsOf<S, FieldsOf<S[N]>> & IdWhere<S, N>>;
|
|
97
98
|
type StructWhereFields<S, F extends Fields> = WhereFieldsOf<S, F>;
|
|
99
|
+
type OrderKeys<S, N extends keyof S> = (ScalarKeys<FieldsOf<S[N]>> | keyof Id<S, N>) & keyof Row<S, N>;
|
|
100
|
+
/** ONE field and its direction: the engine sorts by a single field, so a second key is a type error. */
|
|
98
101
|
export type Order<S, N extends keyof S> = {
|
|
99
|
-
[K in
|
|
100
|
-
|
|
102
|
+
[K in OrderKeys<S, N>]: {
|
|
103
|
+
[P in K]: "asc" | "desc";
|
|
104
|
+
} & {
|
|
105
|
+
[P in Exclude<OrderKeys<S, N>, K>]?: never;
|
|
106
|
+
};
|
|
107
|
+
}[OrderKeys<S, N>];
|
|
101
108
|
/** The query object — marcidb's wire form: a select shape plus the `$`-clauses. */
|
|
102
109
|
export type QueryObject<S, N extends keyof S> = Select<S, N> & {
|
|
103
110
|
$where?: Where<S, N>;
|
|
@@ -140,8 +147,14 @@ export type Query<S, N extends keyof S, Sel = ScalarSelect<S, N>> = MarciQuery<T
|
|
|
140
147
|
export type Result<S, N extends keyof S, Q> = Rows<Types<S, N>, Q>;
|
|
141
148
|
/** `db.<model>` — the root query of a model. */
|
|
142
149
|
export type Collection<S, N extends keyof S> = Query<S, N>;
|
|
143
|
-
|
|
144
|
-
|
|
150
|
+
/** The platform's models a project's own may point at (`t.one("Session")`) — see ../auth/models.ts. */
|
|
151
|
+
type PlatformModels = {
|
|
152
|
+
Session: Model<SessionPublicFields>;
|
|
153
|
+
};
|
|
154
|
+
/** What a db's queries are typed against: the project's models and what they may point at. */
|
|
155
|
+
type Full<S> = S & PlatformModels;
|
|
156
|
+
type DbBase<S extends Schema> = {
|
|
157
|
+
[N in keyof S & string as Uncapitalize<N>]: Query<Full<S>, N>;
|
|
145
158
|
} & {
|
|
146
159
|
/** Atomic batch: all ops commit or none. `ref("0.id")` feeds a generated id into a later op. */
|
|
147
160
|
$transaction<P extends readonly Op<any>[]>(ops: [...P]): Promise<{
|
|
@@ -150,8 +163,65 @@ export type Db<S extends Schema> = {
|
|
|
150
163
|
/** The `.marci` schema text this db syncs with. */
|
|
151
164
|
readonly $schema: string;
|
|
152
165
|
};
|
|
166
|
+
export type Db<S extends Schema> = DbBase<S> & {
|
|
167
|
+
/**
|
|
168
|
+
* Sign-in for this database: `model` is the project's model of a user. Adds the platform's `Session`
|
|
169
|
+
* and `Identity` models beside it and gives the db its `auth`. Part of the definition — call it on
|
|
170
|
+
* `defineDb(...)` itself, before the db is used. Every field of the user model must be optional or
|
|
171
|
+
* have a default: the platform creates the row the first time someone signs in.
|
|
172
|
+
*/
|
|
173
|
+
withAuth<N extends keyof S & string>(options: {
|
|
174
|
+
model: N;
|
|
175
|
+
}): DbWithAuth<S, N>;
|
|
176
|
+
};
|
|
177
|
+
export type DbWithAuth<S extends Schema, N extends keyof S & string> = DbBase<S> & {
|
|
178
|
+
readonly auth: Auth<S, N>;
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* The user of the current request, as a query over the project's user model: `select(shape)` picks the
|
|
182
|
+
* fields (id + scalars without it), `await` runs it. Nothing happens until it is awaited — a query that
|
|
183
|
+
* is never awaited checks nothing, which is why the compiler refuses one.
|
|
184
|
+
*/
|
|
185
|
+
export interface UserQuery<S, N extends keyof S, Sel, Nil> extends PromiseLike<Result<S, N, Sel> | Nil> {
|
|
186
|
+
select<Q extends Select<S, N>>(shape: Q): UserQuery<S, N, Q, Nil>;
|
|
187
|
+
}
|
|
188
|
+
type AuthTables = {
|
|
189
|
+
Session: Model<SessionPublicFields>;
|
|
190
|
+
Identity: Model<IdentityPublicFields>;
|
|
191
|
+
};
|
|
192
|
+
/** `db.auth` — see ../auth/api.ts. */
|
|
193
|
+
export type Auth<S extends Schema, N extends keyof S & string> = {
|
|
194
|
+
/** The signed-in user, or `null` for a guest. */
|
|
195
|
+
user(): UserQuery<Full<S>, N, ScalarSelect<Full<S>, N>, null>;
|
|
196
|
+
/**
|
|
197
|
+
* The signed-in user; a guest is a 401. With `where` it is a check of access too: a user the filter
|
|
198
|
+
* does not match is a 403 — `await db.auth.requireUser({ role: "admin" }).select({ id: true })`.
|
|
199
|
+
*/
|
|
200
|
+
requireUser(where?: Where<Full<S>, N>): UserQuery<Full<S>, N, ScalarSelect<Full<S>, N>, never>;
|
|
201
|
+
/** The device's session — there from the first request, before any sign-in (a guest's cart points at it). */
|
|
202
|
+
readonly sessionId: number;
|
|
203
|
+
/** A new account: `login` is an email or a name (3–32 letters, digits, `. _ -`), `data` the fields of the
|
|
204
|
+
* user's row. 409 when the login is taken. Signs this device in. */
|
|
205
|
+
signUpWithPassword(login: string, password: string, data?: Insert<Full<S>, N>): Promise<void>;
|
|
206
|
+
/** Signs this device in; 401 on any failure (rate-limited per login and ip). */
|
|
207
|
+
signInWithPassword(login: string, password: string): Promise<void>;
|
|
208
|
+
/** Sets the current user's password; every other device is signed out. */
|
|
209
|
+
setPassword(password: string): Promise<void>;
|
|
210
|
+
/** Emails a 6-digit code (always resolves — no "email exists" oracle; rate-limited). */
|
|
211
|
+
sendCode(email: string): Promise<void>;
|
|
212
|
+
/** Checks the code sent to this device, finds or creates the user, signs this device in. */
|
|
213
|
+
signInWithCode(email: string, code: string): Promise<void>;
|
|
214
|
+
/** For a person the project verified itself (a provider's sign-in, Telegram initData, SSO): binds this device to the user. */
|
|
215
|
+
signIn(userId: number): Promise<void>;
|
|
216
|
+
/** The device stays, as a guest. */
|
|
217
|
+
signOut(): Promise<void>;
|
|
218
|
+
/** The current user's devices, as a query: `await`, `.select()`, `.where(...).updateMany({ revoked: true })`. */
|
|
219
|
+
readonly sessions: Query<AuthTables, "Session">;
|
|
220
|
+
/** How the current user signs in, as a query: provider (`"email"` | `"login"`), subject, verifiedAt. */
|
|
221
|
+
readonly identities: Query<AuthTables, "Identity">;
|
|
222
|
+
};
|
|
153
223
|
type RelKinds = "one" | "many" | "list";
|
|
154
|
-
type CheckField<S, Fld> = MetaOf<Fld>["kind"] extends RelKinds ? MetaOf<Fld>["model"] extends keyof S ? Fld : `Unknown model "${MetaOf<Fld>["model"]}" — not a key of defineDb({...})` : Fld;
|
|
224
|
+
type CheckField<S, Fld> = MetaOf<Fld>["kind"] extends RelKinds ? MetaOf<Fld>["model"] extends keyof S | keyof PlatformModels ? Fld : `Unknown model "${MetaOf<Fld>["model"]}" — not a key of defineDb({...})` : Fld;
|
|
155
225
|
/** Intersected with `S` in `defineDb`'s parameter: an unknown relation target fails to type-check on that field. */
|
|
156
226
|
export type ValidateRefs<S extends Schema> = {
|
|
157
227
|
[N in keyof S]: {
|