@trylight/app 0.3.9 → 0.3.12
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 +5 -3
- package/dist/auth/plaza.d.ts +4 -3
- package/dist/auth/plaza.js +38 -4
- package/dist/auth/problems.js +4 -0
- package/dist/client.d.ts +11 -2
- package/dist/client.js +11 -0
- package/dist/entities.d.ts +2 -0
- package/dist/platform/contract.d.ts +2 -0
- package/dist/platform/contract.js +2 -0
- package/dist/server/entities.js +35 -14
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +2 -2
- package/dist/server/session.d.ts +5 -2
- package/dist/server/session.js +8 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,8 +8,8 @@ its tables and their typed client, its door, its contracts and its chat.
|
|
|
8
8
|
| Import | What |
|
|
9
9
|
|---|---|
|
|
10
10
|
| `@trylight/app/schema` | `entity(name, columns, access)`: a Drizzle table plus `id`, `created_at`, `updated_at`, `created_by`, `field_sources`; `lightChanges` (the audit table); `lightImages` (the app's copy of its logos and photos); `fieldSource` (the `{url, evidence}` schema) |
|
|
11
|
-
| `@trylight/app` | `createApp<typeof contracts>()` → `app.table(entity)` (`list/get/create/update/delete`, `useList/useInfiniteList/useGet/useCreate/useUpdate/useDelete`, typed by the entity passed; `useList(...).rows` is never undefined), `app.contracts` (`useInvocation`), and `imageSrc(url)` (where a logo or photo loads from) |
|
|
12
|
-
| `@trylight/app/server` | `createLightServer({ schema, contracts })` → `light.db` (Drizzle), `light.session(request)`, `light.signIn(email, password)`, and the route handlers `light.entities`, `light.image`, `light.invoke.handler`, `light.chat` |
|
|
11
|
+
| `@trylight/app` | `createApp<typeof contracts>()` → `app.table(entity)` (`list/get/create/update/delete`, `useList/useInfiniteList/useCount/useGet/useCreate/useUpdate/useDelete`, typed by the entity passed; `useList(...).rows` is never undefined), `app.contracts` (`useInvocation`), and `imageSrc(url)` (where a logo or photo loads from) |
|
|
12
|
+
| `@trylight/app/server` | `createLightServer({ schema, contracts })` → `light.db` (Drizzle), `light.session(request)`, `light.signIn(email, password, code?)`, and the route handlers `light.entities`, `light.image`, `light.invoke.handler`, `light.chat` |
|
|
13
13
|
| `@trylight/app/platform` | the door for server functions, framing, analytics/error policies, the pane bridge, `fetchThroughRestart` (`createStart`'s `serverFns.fetch`) |
|
|
14
14
|
| `@trylight/app/vite` | `lightDb()`: the dev server syncs `db/schema` into the database on every change |
|
|
15
15
|
| `light-db` (bin) | `sync`, `migrate`, `check`: the app's `pnpm db:*` scripts |
|
|
@@ -37,7 +37,9 @@ new ones load (`isSwitching`), sorts on the server, and prefetches the
|
|
|
37
37
|
sibling views it is given once it settles. A write refetches every page a
|
|
38
38
|
list has loaded, one after another, before its mutation settles; the
|
|
39
39
|
change itself shows at once. `search` matches words in any
|
|
40
|
-
text column, case-insensitively, on the server.
|
|
40
|
+
text column, case-insensitively, on the server. `useCount({ where, search })` is how
|
|
41
|
+
many rows the same list would hold, counted on the server (a paged list's tab
|
|
42
|
+
total), and refetched after every write like the rest.
|
|
41
43
|
|
|
42
44
|
Joins and aggregates stay typed server functions over `light.db`.
|
|
43
45
|
|
package/dist/auth/plaza.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export type PlazaSession = {
|
|
|
25
25
|
* (the platform unreachable, the platform answering nonsense), so the route can report them.
|
|
26
26
|
*/
|
|
27
27
|
export type SignInFailure = {
|
|
28
|
-
readonly kind: "credentials" | "not_a_member" | "throttled";
|
|
28
|
+
readonly kind: "credentials" | "not_a_member" | "unverified" | "throttled" | "code_required" | "wrong_code" | "two_factor_not_set_up";
|
|
29
29
|
readonly message: string;
|
|
30
30
|
} | {
|
|
31
31
|
readonly kind: "unavailable";
|
|
@@ -44,9 +44,10 @@ export type SignInResult = ({
|
|
|
44
44
|
};
|
|
45
45
|
/**
|
|
46
46
|
* Check an email and password with the platform, and get back who the person is and
|
|
47
|
-
* the session the platform signed for them.
|
|
47
|
+
* the session the platform signed for them. `code` is from their authenticator app,
|
|
48
|
+
* sent once the platform has asked for it.
|
|
48
49
|
*/
|
|
49
|
-
export declare function signInWithPlaza(email: string, password: string): Promise<SignInResult>;
|
|
50
|
+
export declare function signInWithPlaza(email: string, password: string, code?: string): Promise<SignInResult>;
|
|
50
51
|
export type PasswordResetResult = {
|
|
51
52
|
readonly ok: true;
|
|
52
53
|
} | {
|
package/dist/auth/plaza.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
2
|
import { miiAuthConfig } from "./config.js";
|
|
3
|
-
import { PLATFORM_AUTH_PATHS } from "../platform/contract.js";
|
|
3
|
+
import { EMAIL_NOT_VERIFIED_REFUSAL, PLATFORM_AUTH_PATHS } from "../platform/contract.js";
|
|
4
4
|
/**
|
|
5
5
|
* Check an email and password with the platform, and get back who the person is and
|
|
6
|
-
* the session the platform signed for them.
|
|
6
|
+
* the session the platform signed for them. `code` is from their authenticator app,
|
|
7
|
+
* sent once the platform has asked for it.
|
|
7
8
|
*/
|
|
8
|
-
export async function signInWithPlaza(email, password) {
|
|
9
|
+
export async function signInWithPlaza(email, password, code) {
|
|
9
10
|
const { plazaUrl, appId } = miiAuthConfig();
|
|
10
11
|
let response;
|
|
11
12
|
try {
|
|
12
13
|
response = await fetch(`${plazaUrl}${PLATFORM_AUTH_PATHS.signIn}`, {
|
|
13
14
|
method: "POST",
|
|
14
15
|
headers: { "Content-Type": "application/json" },
|
|
15
|
-
body: JSON.stringify({ appId, email, password }),
|
|
16
|
+
body: JSON.stringify({ appId, email, password, code: typedCode.parse(code) }),
|
|
16
17
|
cache: "no-store"
|
|
17
18
|
});
|
|
18
19
|
}
|
|
@@ -46,6 +47,12 @@ export async function signInWithPlaza(email, password) {
|
|
|
46
47
|
failure: failureFor(response.status, await response.text())
|
|
47
48
|
};
|
|
48
49
|
}
|
|
50
|
+
/** A blank code field is no code: the platform refuses an empty one outright. */
|
|
51
|
+
const typedCode = z
|
|
52
|
+
.string()
|
|
53
|
+
.trim()
|
|
54
|
+
.transform((typed) => (typed === "" ? undefined : typed))
|
|
55
|
+
.optional();
|
|
49
56
|
/** `returnTo` is this app's login, where the platform's reset page sends the person afterwards. */
|
|
50
57
|
export async function requestPasswordResetWithPlaza(email, returnTo) {
|
|
51
58
|
const { plazaUrl, appId } = miiAuthConfig();
|
|
@@ -155,11 +162,33 @@ function asSignedIn(body) {
|
|
|
155
162
|
const { session, ...identity } = read.data;
|
|
156
163
|
return { identity, session };
|
|
157
164
|
}
|
|
165
|
+
// A 403 is also "not a member"; the refusal's code tells them apart.
|
|
166
|
+
const unverifiedRefusal = z.object({ error: z.literal(EMAIL_NOT_VERIFIED_REFUSAL) });
|
|
167
|
+
// Not every answer is JSON: a proxy's error page, say.
|
|
168
|
+
function jsonIn(body) {
|
|
169
|
+
try {
|
|
170
|
+
return JSON.parse(body);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
158
176
|
function failureFor(status, body) {
|
|
177
|
+
const answer = jsonIn(body);
|
|
178
|
+
const refusal = codeRefusal.safeParse(answer);
|
|
179
|
+
if (refusal.success) {
|
|
180
|
+
return { kind: refusal.data.error, message: refusal.data.detail };
|
|
181
|
+
}
|
|
159
182
|
if (status === 401) {
|
|
160
183
|
return { kind: "credentials", message: "Wrong email or password." };
|
|
161
184
|
}
|
|
162
185
|
if (status === 403) {
|
|
186
|
+
if (unverifiedRefusal.safeParse(answer).success) {
|
|
187
|
+
return {
|
|
188
|
+
kind: "unverified",
|
|
189
|
+
message: "Confirm your email first: we sent you a link."
|
|
190
|
+
};
|
|
191
|
+
}
|
|
163
192
|
return {
|
|
164
193
|
kind: "not_a_member",
|
|
165
194
|
// Said plainly: they have just proved they own the account, so this is
|
|
@@ -179,3 +208,8 @@ function failureFor(status, body) {
|
|
|
179
208
|
cause: new Error(`Platform sign-in answered ${status}: ${body.slice(0, 300)}`)
|
|
180
209
|
};
|
|
181
210
|
}
|
|
211
|
+
// The refusals the platform words itself: it knows the lockout and the setup path.
|
|
212
|
+
const codeRefusal = z.object({
|
|
213
|
+
error: z.enum(["code_required", "wrong_code", "two_factor_not_set_up", "throttled"]),
|
|
214
|
+
detail: z.string().min(1)
|
|
215
|
+
});
|
package/dist/auth/problems.js
CHANGED
|
@@ -13,6 +13,10 @@ export const SIGN_IN_PROBLEMS = {
|
|
|
13
13
|
title: "That account can't open this app",
|
|
14
14
|
body: "The password was right, but the account isn't in this app's org. Ask whoever runs it to add you."
|
|
15
15
|
},
|
|
16
|
+
unverified: {
|
|
17
|
+
title: "Confirm your email first",
|
|
18
|
+
body: "We emailed you a link. Open it, then sign in here again."
|
|
19
|
+
},
|
|
16
20
|
deactivated: {
|
|
17
21
|
title: "Your access here has been turned off",
|
|
18
22
|
body: "Ask whoever runs this app to turn it back on."
|
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type QueryKey } from "@tanstack/react-query";
|
|
2
2
|
import type { Contracts } from "./invoke/server.js";
|
|
3
|
-
import { EntityError, type ListOptions, type NewRow, type Patch, type Row } from "./entities.js";
|
|
3
|
+
import { EntityError, type CountOptions, type ListOptions, type NewRow, type Patch, type Row } from "./entities.js";
|
|
4
4
|
import type { Entity } from "./schema.js";
|
|
5
5
|
declare module "@tanstack/react-query" {
|
|
6
6
|
interface Register {
|
|
@@ -10,7 +10,7 @@ declare module "@tanstack/react-query" {
|
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
-
export { EntityError, LIST_LIMIT, type ListOptions, type NewRow, type Sort, type Patch, type Row, type Where } from "./entities.js";
|
|
13
|
+
export { EntityError, LIST_LIMIT, type CountOptions, type ListOptions, type NewRow, type Sort, type Patch, type Row, type Where } from "./entities.js";
|
|
14
14
|
export type { FieldSource, FieldSources } from "./schema.js";
|
|
15
15
|
export { siteOf } from "./schema.js";
|
|
16
16
|
export { imageSrc } from "./images.js";
|
|
@@ -81,6 +81,7 @@ declare function tableClient<T extends Entity>(entity: T): {
|
|
|
81
81
|
*/
|
|
82
82
|
key: string[];
|
|
83
83
|
list: (options?: ListOptions<Row<T>>) => Promise<Row<T>[]>;
|
|
84
|
+
count: (options?: CountOptions<Row<T>>) => Promise<number>;
|
|
84
85
|
get: (id: string) => Promise<Row<T>>;
|
|
85
86
|
create: (data: NewRow<T>) => Promise<Row<T>>;
|
|
86
87
|
update: (id: string, patch: Patch<T>) => Promise<Row<T>>;
|
|
@@ -695,6 +696,14 @@ declare function tableClient<T extends Entity>(entity: T): {
|
|
|
695
696
|
isEnabled: boolean;
|
|
696
697
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
697
698
|
};
|
|
699
|
+
/**
|
|
700
|
+
* How many rows match, counted on the server, so a paged list's tab can
|
|
701
|
+
* show its total; refetched after any write to this table. New options
|
|
702
|
+
* keep the last count until theirs lands.
|
|
703
|
+
*
|
|
704
|
+
* const drafts = app.table(posts).useCount({ where: { status: "draft" } });
|
|
705
|
+
*/
|
|
706
|
+
useCount: (options?: CountOptions<Row<T>>) => import("@tanstack/react-query").UseQueryResult<number, EntityError>;
|
|
698
707
|
/** One row; `null` waits (nothing open yet). */
|
|
699
708
|
useGet: (id: string | null) => import("@tanstack/react-query").UseQueryResult<T["$inferSelect"] extends infer T_1 ? { [K in keyof T_1]: Date extends T_1[K] ? string | Exclude<T_1[K], T_1[K] & Date> : T_1[K]; } : never, EntityError>;
|
|
700
709
|
useCreate: () => import("@tanstack/react-query").UseMutationResult<T["$inferSelect"] extends infer T_1 ? { [K in keyof T_1]: Date extends T_1[K] ? string | Exclude<T_1[K], T_1[K] & Date> : T_1[K]; } : never, EntityError, NewRow<T>, unknown>;
|
package/dist/client.js
CHANGED
|
@@ -132,10 +132,12 @@ function tableClient(entity) {
|
|
|
132
132
|
all,
|
|
133
133
|
list: (options) => [...all, "list", options],
|
|
134
134
|
pages: (options) => [...all, "pages", options],
|
|
135
|
+
count: (options) => [...all, "count", options],
|
|
135
136
|
get: (id) => [...all, "get", id],
|
|
136
137
|
write: [...all, "write"]
|
|
137
138
|
};
|
|
138
139
|
const list = (options = {}) => call("GET", `${base}?q=${encodeURIComponent(JSON.stringify(options))}`);
|
|
140
|
+
const count = (options = {}) => call("GET", `${base}?count=${encodeURIComponent(JSON.stringify(options))}`);
|
|
139
141
|
const get = (id) => call("GET", `${base}/${id}`);
|
|
140
142
|
const create = (data) => call("POST", base, json(data));
|
|
141
143
|
const update = (id, patch) => call("PATCH", `${base}/${id}`, json(patch));
|
|
@@ -187,6 +189,7 @@ function tableClient(entity) {
|
|
|
187
189
|
*/
|
|
188
190
|
key: keys.all,
|
|
189
191
|
list,
|
|
192
|
+
count,
|
|
190
193
|
get,
|
|
191
194
|
create,
|
|
192
195
|
update,
|
|
@@ -272,6 +275,14 @@ function tableClient(entity) {
|
|
|
272
275
|
};
|
|
273
276
|
return { ...query, ...view };
|
|
274
277
|
},
|
|
278
|
+
/**
|
|
279
|
+
* How many rows match, counted on the server, so a paged list's tab can
|
|
280
|
+
* show its total; refetched after any write to this table. New options
|
|
281
|
+
* keep the last count until theirs lands.
|
|
282
|
+
*
|
|
283
|
+
* const drafts = app.table(posts).useCount({ where: { status: "draft" } });
|
|
284
|
+
*/
|
|
285
|
+
useCount: (options = {}) => useQuery({ queryKey: keys.count(options), queryFn: () => count(options), placeholderData: keepPreviousData }),
|
|
275
286
|
/** One row; `null` waits (nothing open yet). */
|
|
276
287
|
useGet: (id) => useQuery({
|
|
277
288
|
queryKey: keys.get(id),
|
package/dist/entities.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export type ListOptions<R> = {
|
|
|
27
27
|
limit?: number;
|
|
28
28
|
offset?: number;
|
|
29
29
|
};
|
|
30
|
+
/** What `useCount` takes: the rows a list with the same `where` and `search` would hold. */
|
|
31
|
+
export type CountOptions<R> = Pick<ListOptions<R>, "where" | "search">;
|
|
30
32
|
/** A refused or failed entities call, with the route's own reason. */
|
|
31
33
|
export declare class EntityError extends Error {
|
|
32
34
|
readonly status: number;
|
|
@@ -16,6 +16,8 @@ export declare const PLATFORM_AUTH_PATHS: {
|
|
|
16
16
|
/** "Continue with Google / Microsoft": the platform signs the person in and returns with a one-time code. */
|
|
17
17
|
socialSignIn: string;
|
|
18
18
|
};
|
|
19
|
+
/** The platform's sign-in refusing a right password: the login's address is unconfirmed (a 403, like "not a member"). */
|
|
20
|
+
export declare const EMAIL_NOT_VERIFIED_REFUSAL = "email_not_verified";
|
|
19
21
|
/** An app's server calling the platform as the app, for a visitor with no session: which app, and its secret. */
|
|
20
22
|
export declare const APP_CALLER_HEADERS: {
|
|
21
23
|
appId: string;
|
|
@@ -22,6 +22,8 @@ export const PLATFORM_AUTH_PATHS = {
|
|
|
22
22
|
/** "Continue with Google / Microsoft": the platform signs the person in and returns with a one-time code. */
|
|
23
23
|
socialSignIn: "/api/app-auth/social"
|
|
24
24
|
};
|
|
25
|
+
/** The platform's sign-in refusing a right password: the login's address is unconfirmed (a 403, like "not a member"). */
|
|
26
|
+
export const EMAIL_NOT_VERIFIED_REFUSAL = "email_not_verified";
|
|
25
27
|
/** An app's server calling the platform as the app, for a visitor with no session: which app, and its secret. */
|
|
26
28
|
export const APP_CALLER_HEADERS = { appId: "x-app-id", secret: "x-app-secret" };
|
|
27
29
|
/** The server env that secret is provisioned in. */
|
package/dist/server/entities.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The one route behind `app.table(...)`: list, get, create, update and delete
|
|
1
|
+
// The one route behind `app.table(...)`: list, count, get, create, update and delete
|
|
2
2
|
// on any entity table, each request checked against the table's own Zod
|
|
3
3
|
// schema and the app's door. Each write names its person for the table's
|
|
4
4
|
// trigger, which records the change in light_changes (src/db/audit.ts), which
|
|
@@ -82,7 +82,10 @@ export function createEntitiesHandler(options) {
|
|
|
82
82
|
if (method === "GET") {
|
|
83
83
|
if (!mayRead(session))
|
|
84
84
|
return refuse(401, "not_signed_in", "Sign in to use this app.");
|
|
85
|
-
|
|
85
|
+
if (id !== null)
|
|
86
|
+
return answer(() => get(db, table, id));
|
|
87
|
+
const counted = url.searchParams.get("count");
|
|
88
|
+
return counted === null ? list(db, table, url) : count(db, table, counted);
|
|
86
89
|
}
|
|
87
90
|
if (table.entity.$access.write === "agents") {
|
|
88
91
|
return refuse(403, "agents_only", `${table.name} is written by the org's agents, not from the app.`);
|
|
@@ -133,6 +136,10 @@ function tableRules(entity) {
|
|
|
133
136
|
.filter(([name, c]) => !(name in PLATFORM_COLUMNS) && SEARCHED_TYPES.has(c.columnType))
|
|
134
137
|
.map(([, c]) => c);
|
|
135
138
|
const match = Object.fromEntries(Object.entries(createSelectSchema(entity).shape).map(([key, value]) => [key, z.union([value, z.array(value)])]));
|
|
139
|
+
const filters = {
|
|
140
|
+
where: z.object(match).partial().strict().default({}),
|
|
141
|
+
search: z.string().trim().max(SEARCH_MAX).default("")
|
|
142
|
+
};
|
|
136
143
|
return {
|
|
137
144
|
name: getTableName(entity),
|
|
138
145
|
entity,
|
|
@@ -143,13 +150,13 @@ function tableRules(entity) {
|
|
|
143
150
|
update: writable(createUpdateSchema(entity).shape),
|
|
144
151
|
list: z
|
|
145
152
|
.object({
|
|
146
|
-
|
|
147
|
-
search: z.string().trim().max(SEARCH_MAX).default(""),
|
|
153
|
+
...filters,
|
|
148
154
|
sort: z.literal([...names, ...names.map((name) => `-${name}`)]).default("-created_at"),
|
|
149
155
|
limit: z.int().min(1).max(LIST_LIMIT).default(LIST_LIMIT),
|
|
150
156
|
offset: z.int().min(0).default(0)
|
|
151
157
|
})
|
|
152
|
-
.strict()
|
|
158
|
+
.strict(),
|
|
159
|
+
count: z.object(filters).strict()
|
|
153
160
|
};
|
|
154
161
|
}
|
|
155
162
|
async function list(db, table, url) {
|
|
@@ -159,6 +166,28 @@ async function list(db, table, url) {
|
|
|
159
166
|
return refuse(400, "invalid", z.prettifyError(query.error));
|
|
160
167
|
const { where, search, sort, limit, offset } = query.data;
|
|
161
168
|
const order = sort.startsWith("-") ? desc(table.column(sort.slice(1))) : asc(table.column(sort));
|
|
169
|
+
const rows = await db
|
|
170
|
+
.select()
|
|
171
|
+
.from(table.entity)
|
|
172
|
+
.where(and(...matching(table, where, search)))
|
|
173
|
+
// The id breaks ties, so paging with offset never repeats or skips a row.
|
|
174
|
+
.orderBy(order, asc(table.id))
|
|
175
|
+
.limit(limit)
|
|
176
|
+
.offset(offset);
|
|
177
|
+
return Response.json(rows);
|
|
178
|
+
}
|
|
179
|
+
/** How many rows a list with the same `where` and `search` would hold, without fetching them. */
|
|
180
|
+
async function count(db, table, asked) {
|
|
181
|
+
const query = table.count.safeParse(readJson(asked));
|
|
182
|
+
if (!query.success)
|
|
183
|
+
return refuse(400, "invalid", z.prettifyError(query.error));
|
|
184
|
+
const rows = await db
|
|
185
|
+
.select({ count: sql `count(*)::int` })
|
|
186
|
+
.from(table.entity)
|
|
187
|
+
.where(and(...matching(table, query.data.where, query.data.search)));
|
|
188
|
+
return Response.json(exactlyOne(rows).count);
|
|
189
|
+
}
|
|
190
|
+
function matching(table, where, search) {
|
|
162
191
|
const conditions = Object.entries(where).map(([name, value]) => {
|
|
163
192
|
const column = table.column(name);
|
|
164
193
|
if (value === null)
|
|
@@ -172,15 +201,7 @@ async function list(db, table, url) {
|
|
|
172
201
|
const pattern = `%${word.replace(/[\\%_]/g, (c) => `\\${c}`)}%`;
|
|
173
202
|
conditions.push(or(...table.searched.map((column) => ilike(column, pattern))) ?? sql `false`);
|
|
174
203
|
}
|
|
175
|
-
|
|
176
|
-
.select()
|
|
177
|
-
.from(table.entity)
|
|
178
|
-
.where(and(...conditions))
|
|
179
|
-
// The id breaks ties, so paging with offset never repeats or skips a row.
|
|
180
|
-
.orderBy(order, asc(table.id))
|
|
181
|
-
.limit(limit)
|
|
182
|
-
.offset(offset);
|
|
183
|
-
return Response.json(rows);
|
|
204
|
+
return conditions;
|
|
184
205
|
}
|
|
185
206
|
/** Members-only: someone signed in; anyone with the link (and a local dev server): anyone. */
|
|
186
207
|
function mayRead(session) {
|
package/dist/server/index.d.ts
CHANGED
|
@@ -13,8 +13,8 @@ export declare function createLightServer<S extends Record<string, unknown>, C e
|
|
|
13
13
|
session: (request: Request) => Promise<import("./session.js").SessionState>;
|
|
14
14
|
/** Whether sign-in is configured (a bare local dev server has none). */
|
|
15
15
|
configured: () => boolean;
|
|
16
|
-
/** Email and password, checked by the platform; the session to set as a cookie, or why not. */
|
|
17
|
-
signIn: (email: string, password: string) => Promise<import("./session.js").SignIn>;
|
|
16
|
+
/** Email and password (and the authenticator code once asked for), checked by the platform; the session to set as a cookie, or why not. */
|
|
17
|
+
signIn: (email: string, password: string, code?: string) => Promise<import("./session.js").SignIn>;
|
|
18
18
|
requestPasswordReset: (email: string, returnTo: string) => Promise<import("./session.js").PasswordResetRequest>;
|
|
19
19
|
/** The app's database, typed by its schema: `light.db.select().from(companies)`. */
|
|
20
20
|
readonly db: PostgresJsDatabase<S>;
|
package/dist/server/index.js
CHANGED
|
@@ -36,8 +36,8 @@ export function createLightServer(options) {
|
|
|
36
36
|
session,
|
|
37
37
|
/** Whether sign-in is configured (a bare local dev server has none). */
|
|
38
38
|
configured: () => env().configured,
|
|
39
|
-
/** Email and password, checked by the platform; the session to set as a cookie, or why not. */
|
|
40
|
-
signIn: (email, password) => signIn(auth, email, password),
|
|
39
|
+
/** Email and password (and the authenticator code once asked for), checked by the platform; the session to set as a cookie, or why not. */
|
|
40
|
+
signIn: (email, password, code) => signIn(auth, email, password, code),
|
|
41
41
|
requestPasswordReset: (email, returnTo) => requestPasswordReset(email, returnTo),
|
|
42
42
|
/** The app's database, typed by its schema: `light.db.select().from(companies)`. */
|
|
43
43
|
get db() {
|
package/dist/server/session.d.ts
CHANGED
|
@@ -10,12 +10,15 @@ export type SignIn = {
|
|
|
10
10
|
ok: true;
|
|
11
11
|
token: string;
|
|
12
12
|
expiresAt: Date;
|
|
13
|
-
}
|
|
13
|
+
}
|
|
14
|
+
/** `needsCode`: show a field for the authenticator code and send it with the password again. */
|
|
15
|
+
| {
|
|
14
16
|
ok: false;
|
|
15
17
|
message: string;
|
|
18
|
+
needsCode: boolean;
|
|
16
19
|
};
|
|
17
20
|
/** The login form's whole job: the platform checks the password, the app starts a session. */
|
|
18
|
-
export declare function signIn(auth: MiiAuthCore, email: string, password: string): Promise<SignIn>;
|
|
21
|
+
export declare function signIn(auth: MiiAuthCore, email: string, password: string, code?: string): Promise<SignIn>;
|
|
19
22
|
export type PasswordResetRequest = {
|
|
20
23
|
ok: true;
|
|
21
24
|
} | {
|
package/dist/server/session.js
CHANGED
|
@@ -21,15 +21,17 @@ export async function readSession(auth, request) {
|
|
|
21
21
|
return { user: { id, name, email, avatarUrl, role }, mode };
|
|
22
22
|
}
|
|
23
23
|
/** The login form's whole job: the platform checks the password, the app starts a session. */
|
|
24
|
-
export async function signIn(auth, email, password) {
|
|
24
|
+
export async function signIn(auth, email, password, code) {
|
|
25
25
|
if (!env().configured)
|
|
26
|
-
return { ok: false, message: "Sign-in is not configured." };
|
|
27
|
-
const result = await signInWithPlaza(email, password);
|
|
28
|
-
if (!result.ok)
|
|
29
|
-
|
|
26
|
+
return { ok: false, message: "Sign-in is not configured.", needsCode: false };
|
|
27
|
+
const result = await signInWithPlaza(email, password, code);
|
|
28
|
+
if (!result.ok) {
|
|
29
|
+
const { kind, message } = result.failure;
|
|
30
|
+
return { ok: false, message, needsCode: kind === "code_required" || kind === "wrong_code" };
|
|
31
|
+
}
|
|
30
32
|
const member = await auth.upsertMemberFromIdentity(result.identity);
|
|
31
33
|
if (member.status !== "active") {
|
|
32
|
-
return { ok: false, message: "Your account is not active in this workspace." };
|
|
34
|
+
return { ok: false, message: "Your account is not active in this workspace.", needsCode: false };
|
|
33
35
|
}
|
|
34
36
|
const session = await auth.issueSession(member.id, result.session);
|
|
35
37
|
return { ok: true, ...session };
|
package/package.json
CHANGED