@astralarium/repo-auth 0.1.0 → 0.2.0

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/dist/request.d.ts CHANGED
@@ -9,8 +9,8 @@ export declare const AUTH_BASE_PATH = "/api/auth";
9
9
  export declare const AUTHORIZE_PATHS: ReadonlySet<string>;
10
10
  /** Endpoints `app`'s client calls: session, social sign-in, each
11
11
  * provider's callback and sign-out; account linking with `linkSocial`;
12
- * email sign-in and sign-up with `devEmailSignIn`. */
13
- export declare function authPaths({ providers, linkSocial, devEmailSignIn, }: Pick<ShellAuthApp<AdditionalFields, SessionLike>, "providers" | "linkSocial" | "devEmailSignIn">): ReadonlySet<string>;
12
+ * dev sign-in's endpoints with `devSignIn`. */
13
+ export declare function authPaths({ providers, linkSocial, devSignIn, }: Pick<ShellAuthApp<AdditionalFields, SessionLike>, "providers" | "linkSocial" | "devSignIn">): ReadonlySet<string>;
14
14
  /** A per-key limiter: Cloudflare's rate limiting binding. */
15
15
  export interface RateLimiter {
16
16
  limit(options: {
package/dist/request.js CHANGED
@@ -4,13 +4,15 @@ export const AUTH_BASE_PATH = "/api/auth";
4
4
  export const AUTHORIZE_PATHS = new Set(["/sign-in/social", "/link-social"].map(path => `${AUTH_BASE_PATH}${path}`));
5
5
  /** Endpoints `app`'s client calls: session, social sign-in, each
6
6
  * provider's callback and sign-out; account linking with `linkSocial`;
7
- * email sign-in and sign-up with `devEmailSignIn`. */
7
+ * dev sign-in's endpoints with `devSignIn`. */
8
8
  export function authPaths({
9
9
  providers,
10
10
  linkSocial,
11
- devEmailSignIn
11
+ devSignIn
12
12
  }) {
13
- return new Set(["/get-session", "/sign-in/social", ...(linkSocial ? ["/link-social"] : []), ...(providers?.ids ?? []).map(id => `/callback/${id}`), "/sign-out", ...(devEmailSignIn ? ["/sign-in/email", "/sign-up/email"] : [])].map(path => `${AUTH_BASE_PATH}${path}`));
13
+ return new Set(["/get-session", "/sign-in/social", ...(linkSocial ? ["/link-social"] : []), ...(providers?.ids ?? []).map(id => `/callback/${id}`), "/sign-out", ...Object.values(devSignIn?.endpoints ?? {}).map(({
14
+ path
15
+ }) => path)].map(path => `${AUTH_BASE_PATH}${path}`));
14
16
  }
15
17
  /** Whether `request` carries a JSON object without `idToken`. ID-token
16
18
  * sign-in skips state, PKCE and nonce; authorization takes the redirect
@@ -0,0 +1,6 @@
1
+ /** TanStack Start bindings of shell-auth, client-safe: the layout route
2
+ * context carrying the viewer's session, return paths, and server
3
+ * function middleware. */
4
+ export * from "./middleware.ts";
5
+ export * from "./return-path.ts";
6
+ export * from "./viewer.ts";
@@ -0,0 +1,6 @@
1
+ /** TanStack Start bindings of shell-auth, client-safe: the layout route
2
+ * context carrying the viewer's session, return paths, and server
3
+ * function middleware. */
4
+ export * from "./middleware.js";
5
+ export * from "./return-path.js";
6
+ export * from "./viewer.js";
@@ -0,0 +1,28 @@
1
+ import type { SessionLike } from "../types.ts";
2
+ /** Runs `fn` with the app's auth instance, its queries scoped to the
3
+ * request (`withShellAuth`). */
4
+ export type WithAuth<Auth> = <T>(fn: (auth: Auth) => Promise<T>) => Promise<T>;
5
+ /** What `viewerSession` reads of an auth instance: `shellAuth`'s
6
+ * projected `get-session`. */
7
+ export interface ViewerAuth {
8
+ api: {
9
+ getSession(context: {
10
+ headers: Headers;
11
+ returnHeaders: true;
12
+ }): Promise<{
13
+ headers: Headers;
14
+ response: unknown;
15
+ }>;
16
+ };
17
+ $Infer: {
18
+ Session: SessionLike;
19
+ };
20
+ }
21
+ /**
22
+ * Server function handler: the request's projected session, `null` signed
23
+ * out, read through `withAuth`; the response carries the read's cookies
24
+ * and is `private, no-store`. Apps declare the GET server function itself,
25
+ * as Start compiles top-level declarations alone:
26
+ * `createServerFn({ method: "GET" }).handler(viewerSession(withAuth))`.
27
+ */
28
+ export declare function viewerSession<Auth extends ViewerAuth>(withAuth: WithAuth<Auth>): () => Promise<Auth["$Infer"]["Session"] | null>;
@@ -0,0 +1,29 @@
1
+ /** Server half of shell-auth's TanStack Start bindings: the viewer's
2
+ * session read. */
3
+ import { createServerOnlyFn } from "@tanstack/react-start";
4
+ import { appendResponseHeader, getRequestHeaders, setResponseHeader } from "@tanstack/react-start/server";
5
+ /** `auth`'s session of the request, `null` signed out. The response
6
+ * carries the read's `Set-Cookie` values (cookie cache refresh, session
7
+ * extension) and depends on the request, so shared caches skip it. */
8
+ const readViewerSession = createServerOnlyFn(async auth => {
9
+ setResponseHeader("cache-control", "private, no-store");
10
+ const {
11
+ headers,
12
+ response
13
+ } = await auth.api.getSession({
14
+ headers: getRequestHeaders(),
15
+ returnHeaders: true
16
+ });
17
+ for (const cookie of headers.getSetCookie()) appendResponseHeader("set-cookie", cookie);
18
+ return response;
19
+ });
20
+ /**
21
+ * Server function handler: the request's projected session, `null` signed
22
+ * out, read through `withAuth`; the response carries the read's cookies
23
+ * and is `private, no-store`. Apps declare the GET server function itself,
24
+ * as Start compiles top-level declarations alone:
25
+ * `createServerFn({ method: "GET" }).handler(viewerSession(withAuth))`.
26
+ */
27
+ export function viewerSession(withAuth) {
28
+ return () => withAuth(auth => readViewerSession(auth));
29
+ }
@@ -0,0 +1,33 @@
1
+ /** Plain-text body of the 401 `Response` answering a signed-out call of
2
+ * an `authedMiddleware` server function. */
3
+ export declare const UNAUTHENTICATED = "unauthenticated";
4
+ /** Whether `error` is a 401 `Response`, as a signed-out call of an
5
+ * `authedMiddleware` server function rejects with. */
6
+ export declare function isUnauthenticated(error: unknown): error is Response;
7
+ /** Adds `sql`: one client over `connectionString()` per server function
8
+ * call; its one connection opens at the first query and closes when the
9
+ * handler resolves. The connection string reads per call: bindings are
10
+ * absent at static prerender. `connectionString` lands in the client
11
+ * build: a `createServerOnlyFn` drops its imports there. */
12
+ export declare function dbMiddleware(connectionString: () => string): import("@tanstack/react-start").FunctionMiddlewareAfterServer<{}, unknown, undefined, {
13
+ sql: import("postgres").Sql<{}>;
14
+ }, undefined, undefined, undefined>;
15
+ /** Adds the viewer's `session` from `readSession` (`viewerSession`'s
16
+ * server function), `null` signed out. The read opens its own
17
+ * connection (`withAuth`'s scope), closed before the handler runs; the
18
+ * cookie cache answers most reads with none. Handlers querying `sql`
19
+ * add `dbMiddleware` beside it. */
20
+ export declare function sessionMiddleware<Session>(readSession: () => Promise<Session | null>): import("@tanstack/react-start").FunctionMiddlewareAfterServer<{}, unknown, undefined, {
21
+ session: Awaited<Session> | null;
22
+ }, undefined, undefined, undefined>;
23
+ /** A `sessionMiddleware` over sessions of `Session`. */
24
+ export type SessionMiddleware<Session> = ReturnType<typeof sessionMiddleware<Session>>;
25
+ /** `session`'s context with a signed-in `session`, opening no
26
+ * connection of its own. A signed-out call throws a 401 `Response` with
27
+ * body `UNAUTHENTICATED`; Start rejects the caller's call with that
28
+ * `Response`, in the browser and during SSR (`isUnauthenticated`). */
29
+ export declare function authedMiddleware<Session>(session: SessionMiddleware<Session>): import("@tanstack/react-start").FunctionMiddlewareAfterServer<{}, readonly [import("@tanstack/react-start").FunctionMiddlewareAfterServer<{}, unknown, undefined, {
30
+ session: Awaited<Session> | null;
31
+ }, undefined, undefined, undefined>], undefined, {
32
+ session: NonNullable<Awaited<Session>>;
33
+ }, undefined, undefined, undefined>;
@@ -0,0 +1,70 @@
1
+ /** Server function middleware of shell-auth: a database client per call,
2
+ * the viewer's session, and signed-in calls alone. Client-importable:
3
+ * this package lists `@tanstack/react-start` as a peer, so Start's Vite
4
+ * plugin compiles it, and the client compile strips `.server()` bodies
5
+ * and their imports. */
6
+ import { withSql } from "@astralarium/repo-postgres/sql";
7
+ import { createMiddleware } from "@tanstack/react-start";
8
+ /** Plain-text body of the 401 `Response` answering a signed-out call of
9
+ * an `authedMiddleware` server function. */
10
+ export const UNAUTHENTICATED = "unauthenticated";
11
+ /** Whether `error` is a 401 `Response`, as a signed-out call of an
12
+ * `authedMiddleware` server function rejects with. */
13
+ export function isUnauthenticated(error) {
14
+ return error instanceof Response && error.status === 401;
15
+ }
16
+ /** Adds `sql`: one client over `connectionString()` per server function
17
+ * call; its one connection opens at the first query and closes when the
18
+ * handler resolves. The connection string reads per call: bindings are
19
+ * absent at static prerender. `connectionString` lands in the client
20
+ * build: a `createServerOnlyFn` drops its imports there. */
21
+ export function dbMiddleware(connectionString) {
22
+ return createMiddleware({
23
+ type: "function"
24
+ }).server(({
25
+ next
26
+ }) => withSql(connectionString(), sql => next({
27
+ context: {
28
+ sql
29
+ }
30
+ })));
31
+ }
32
+ /** Adds the viewer's `session` from `readSession` (`viewerSession`'s
33
+ * server function), `null` signed out. The read opens its own
34
+ * connection (`withAuth`'s scope), closed before the handler runs; the
35
+ * cookie cache answers most reads with none. Handlers querying `sql`
36
+ * add `dbMiddleware` beside it. */
37
+ export function sessionMiddleware(readSession) {
38
+ return createMiddleware({
39
+ type: "function"
40
+ }).server(async ({
41
+ next
42
+ }) => next({
43
+ context: {
44
+ session: await readSession()
45
+ }
46
+ }));
47
+ }
48
+ /** `session`'s context with a signed-in `session`, opening no
49
+ * connection of its own. A signed-out call throws a 401 `Response` with
50
+ * body `UNAUTHENTICATED`; Start rejects the caller's call with that
51
+ * `Response`, in the browser and during SSR (`isUnauthenticated`). */
52
+ export function authedMiddleware(session) {
53
+ return createMiddleware({
54
+ type: "function"
55
+ }).middleware([session]).server(({
56
+ next,
57
+ context
58
+ }) => {
59
+ if (!context.session)
60
+ // eslint-disable-next-line @typescript-eslint/only-throw-error -- Start answers a thrown `Response` as is
61
+ throw new Response(UNAUTHENTICATED, {
62
+ status: 401
63
+ });
64
+ return next({
65
+ context: {
66
+ session: context.session
67
+ }
68
+ });
69
+ });
70
+ }
@@ -0,0 +1,16 @@
1
+ /** Return paths: where a page sends the visitor once done, kept on this
2
+ * site. */
3
+ /** `url` resolved against `base`; null when it fails to parse. Stands in
4
+ * for `URL.parse`, which postdates the Vite browser baseline (Safari
5
+ * 16.4). */
6
+ export declare function parseUrl(url: string, base?: string): URL | null;
7
+ /** `url`'s normalized path, query and fragment when the browser resolves
8
+ * it as an origin-relative path; undefined when it or its normalized path
9
+ * (`//host`, from dot segments) names a host. */
10
+ export declare function ifUrlOkay(url?: string | null): string | undefined;
11
+ /** Search of a page returning to `r` once done; `validateSearch`. `r` is
12
+ * `ifUrlOkay`'s origin-relative path; an unsafe or non-string `r`
13
+ * drops. */
14
+ export declare function validateReturnSearch(search: Record<string, unknown>): {
15
+ r?: string;
16
+ };
@@ -0,0 +1,32 @@
1
+ /** Return paths: where a page sends the visitor once done, kept on this
2
+ * site. */
3
+ /** `url` resolved against `base`; null when it fails to parse. Stands in
4
+ * for `URL.parse`, which postdates the Vite browser baseline (Safari
5
+ * 16.4). */
6
+ export function parseUrl(url, base) {
7
+ try {
8
+ return new URL(url, base);
9
+ } catch {
10
+ return null;
11
+ }
12
+ }
13
+ /** Stand-in origin for resolving paths. */
14
+ const PATH_BASE = "http://path.invalid";
15
+ /** `url`'s normalized path, query and fragment when the browser resolves
16
+ * it as an origin-relative path; undefined when it or its normalized path
17
+ * (`//host`, from dot segments) names a host. */
18
+ export function ifUrlOkay(url) {
19
+ if (!url?.startsWith("/")) return;
20
+ const resolved = parseUrl(url, PATH_BASE);
21
+ if (resolved?.origin !== PATH_BASE || resolved.pathname.startsWith("//")) return;
22
+ return resolved.pathname + resolved.search + resolved.hash;
23
+ }
24
+ /** Search of a page returning to `r` once done; `validateSearch`. `r` is
25
+ * `ifUrlOkay`'s origin-relative path; an unsafe or non-string `r`
26
+ * drops. */
27
+ export function validateReturnSearch(search) {
28
+ const r = typeof search.r === "string" ? ifUrlOkay(search.r) : undefined;
29
+ return r === undefined ? {} : {
30
+ r
31
+ };
32
+ }
@@ -0,0 +1,8 @@
1
+ import type { ViewerContext } from "../client.ts";
2
+ /**
3
+ * `beforeLoad` of the layout route above every page reading the session:
4
+ * `{ session }` from `fetchViewer` (`viewerSession`'s server function) on
5
+ * the server, dehydrated with the match; `undefined` in the browser,
6
+ * whose live session store holds it (`watchViewerRouter`).
7
+ */
8
+ export declare function viewerRouteContext<Session>(fetchViewer: () => Promise<Session | null>): () => Promise<ViewerContext<Session>>;
@@ -0,0 +1,15 @@
1
+ /** The layout route context carrying the viewer's session in a TanStack
2
+ * Start app. */
3
+ import { createIsomorphicFn } from "@tanstack/react-start";
4
+ /**
5
+ * `beforeLoad` of the layout route above every page reading the session:
6
+ * `{ session }` from `fetchViewer` (`viewerSession`'s server function) on
7
+ * the server, dehydrated with the match; `undefined` in the browser,
8
+ * whose live session store holds it (`watchViewerRouter`).
9
+ */
10
+ export function viewerRouteContext(fetchViewer) {
11
+ const read = createIsomorphicFn().server(() => fetchViewer());
12
+ return async () => ({
13
+ session: await read()
14
+ });
15
+ }
@@ -1,9 +1,10 @@
1
1
  /** Test support: a deployed `auth.*` schema checked against an app's auth
2
2
  * instance, endpoint conformance (unserved paths, ID-token refusal,
3
3
  * anonymous sessions), cookies out of auth responses, a rate-limit probe,
4
- * social sign-in starts, session reads held on a lock, `auth.users`
5
- * rows inserted directly, and a fake OpenID Connect provider. */
6
- import type { Sql, TransactionSql } from "@astralarium/repo-postgres/sql";
4
+ * social sign-in starts, session reads held on a lock, `testUtils`
5
+ * sessions, `auth.users` rows inserted directly, and a fake OpenID
6
+ * Connect provider. */
7
+ import { type Sql, type TransactionSql } from "@astralarium/repo-postgres/sql";
7
8
  import { authPaths } from "../request.ts";
8
9
  import { type JsonPost } from "./json-post.ts";
9
10
  export * from "./oidc.ts";
@@ -108,13 +109,22 @@ export interface BlockedSessionReadOptions {
108
109
  timeout?: number;
109
110
  }
110
111
  /**
111
- * Runs `fn` while a connection over `db` holds `auth.session` exclusively
112
- * and `readSession` (a database session read, e.g. `get-session` with a
113
- * session cookie and `disableCookieCache`) waits on the lock. The lock
114
- * releases as `fn` settles; rejects when no read reaches it within
115
- * `timeout`.
112
+ * Runs `fn` while a transaction over `db` holds `auth.session` exclusively
113
+ * (`holdTransaction`) and `readSession` (a database session read, e.g.
114
+ * `get-session` with a session cookie and `disableCookieCache`) waits on
115
+ * it. The lock releases as `fn` settles; rejects when no read reaches it
116
+ * within `timeout`.
116
117
  */
117
118
  export declare function withBlockedSessionRead<R, T>(db: string, readSession: () => Promise<R>, fn: (blocked: BlockedSessionRead<R>) => T | Promise<T>, { applicationName, timeout }?: BlockedSessionReadOptions): Promise<T>;
119
+ /** What `userSession` reads of an auth instance: one with better-auth's
120
+ * `testUtils` plugin. */
121
+ interface TestUtilsAuth {
122
+ $context: Promise<object>;
123
+ }
124
+ /** A `cookie` header carrying a new session of user `userId`, signed in
125
+ * through `auth`'s `testUtils` login in a `withShellAuth` scope over
126
+ * `db`. */
127
+ export declare function userSession(db: string, auth: TestUtilsAuth, userId: string): Promise<string>;
118
128
  /** Inserts an `auth.users` row: a fresh verified email, created now,
119
129
  * `fields` adding or overriding columns (the app's `additionalFields`).
120
130
  * Resolves to the new user's id. */
@@ -1,4 +1,11 @@
1
- import { Client } from "pg";
1
+ /** Test support: a deployed `auth.*` schema checked against an app's auth
2
+ * instance, endpoint conformance (unserved paths, ID-token refusal,
3
+ * anonymous sessions), cookies out of auth responses, a rate-limit probe,
4
+ * social sign-in starts, session reads held on a lock, `testUtils`
5
+ * sessions, `auth.users` rows inserted directly, and a fake OpenID
6
+ * Connect provider. */
7
+ import { sql as pooledSql } from "@astralarium/repo-postgres/sql";
8
+ import { holdTransaction } from "@astralarium/repo-postgres/testkit";
2
9
  import { z } from "zod";
3
10
  import { withShellAuth } from "../index.server.js";
4
11
  import { AUTH_BASE_PATH, AUTHORIZE_PATHS, authPaths } from "../request.js";
@@ -134,59 +141,83 @@ export async function socialSignIn(request, {
134
141
  return new URL(url);
135
142
  }
136
143
  /**
137
- * Runs `fn` while a connection over `db` holds `auth.session` exclusively
138
- * and `readSession` (a database session read, e.g. `get-session` with a
139
- * session cookie and `disableCookieCache`) waits on the lock. The lock
140
- * releases as `fn` settles; rejects when no read reaches it within
141
- * `timeout`.
144
+ * Runs `fn` while a transaction over `db` holds `auth.session` exclusively
145
+ * (`holdTransaction`) and `readSession` (a database session read, e.g.
146
+ * `get-session` with a session cookie and `disableCookieCache`) waits on
147
+ * it. The lock releases as `fn` settles; rejects when no read reaches it
148
+ * within `timeout`.
142
149
  */
143
150
  export async function withBlockedSessionRead(db, readSession, fn, {
144
151
  applicationName,
145
152
  timeout = 10_000
146
153
  } = {}) {
147
- const sql = new Client({
148
- connectionString: db
149
- });
150
- await sql.connect();
154
+ const client = pooledSql(db);
151
155
  try {
152
- await sql.query("BEGIN");
153
- await sql.query("LOCK TABLE auth.session IN ACCESS EXCLUSIVE MODE");
154
- const response = readSession();
155
- // Rejections surface through `fn`; a timeout leaves them unobserved.
156
- response.catch(() => undefined);
157
- const deadline = Date.now() + timeout;
158
- for (;;) {
159
- // Activity stats are snapshotted per transaction.
160
- await sql.query("SELECT pg_stat_clear_snapshot()");
161
- // pg_locks and pg_stat_activity.usename are visible across roles.
162
- const {
163
- rows
164
- } = await sql.query(`SELECT a.pid, a.usename, r.rolsuper
165
- FROM pg_locks l
166
- JOIN pg_stat_activity a ON a.pid = l.pid
167
- JOIN pg_roles r ON r.rolname = a.usename
168
- WHERE a.datname = current_database()
169
- AND ($1::text IS NULL OR a.application_name = $1)
170
- AND l.relation = 'auth.session'::regclass AND NOT l.granted
171
- AND l.mode = 'AccessShareLock'`, [applicationName ?? null]);
172
- const [row] = rows;
173
- if (row) {
174
- return await fn({
175
- pid: row.pid,
176
- role: row.usename,
177
- superuser: row.rolsuper,
178
- response
179
- });
180
- }
181
- if (Date.now() > deadline) {
182
- throw new Error(`no session read waited on auth.session in ${timeout} ms`);
156
+ const held = await holdTransaction(hold => client.begin(async tx => {
157
+ await tx`LOCK TABLE auth.session IN ACCESS EXCLUSIVE MODE`;
158
+ await hold(tx);
159
+ }));
160
+ try {
161
+ const response = readSession();
162
+ // Rejections surface through `fn`; a timeout leaves them unobserved.
163
+ response.catch(() => undefined);
164
+ const deadline = Date.now() + timeout;
165
+ for (;;) {
166
+ // pg_locks and pg_stat_activity.usename are visible across roles.
167
+ const [row] = await client`
168
+ SELECT a.pid, a.usename, r.rolsuper
169
+ FROM pg_locks l
170
+ JOIN pg_stat_activity a ON a.pid = l.pid
171
+ JOIN pg_roles r ON r.rolname = a.usename
172
+ WHERE l.relation = 'auth.session'::regclass AND NOT l.granted
173
+ AND l.mode = 'AccessShareLock'
174
+ AND ${held.holder}::integer = ANY(pg_blocking_pids(l.pid))
175
+ AND (${applicationName ?? null}::text IS NULL
176
+ OR a.application_name = ${applicationName ?? null})
177
+ `;
178
+ if (row) {
179
+ return await fn({
180
+ pid: row.pid,
181
+ role: row.usename,
182
+ superuser: row.rolsuper,
183
+ response
184
+ });
185
+ }
186
+ if (Date.now() > deadline) {
187
+ throw new Error(`no session read waited on auth.session in ${timeout} ms`);
188
+ }
189
+ await new Promise(resolve => setTimeout(resolve, 50));
183
190
  }
184
- await new Promise(resolve => setTimeout(resolve, 50));
191
+ } finally {
192
+ held.release();
193
+ await held.done;
185
194
  }
186
195
  } finally {
187
- await sql.end();
196
+ await client.end();
188
197
  }
189
198
  }
199
+ /** Whether `context` carries `testUtils`' helpers. */
200
+ function hasTestHelpers(context) {
201
+ return "test" in context;
202
+ }
203
+ /** A `cookie` header carrying a new session of user `userId`, signed in
204
+ * through `auth`'s `testUtils` login in a `withShellAuth` scope over
205
+ * `db`. */
206
+ export function userSession(db, auth, userId) {
207
+ return withShellAuth(db, async () => {
208
+ const context = await auth.$context;
209
+ if (!hasTestHelpers(context)) throw new Error("userSession: the auth instance lacks testUtils");
210
+ const {
211
+ cookies
212
+ } = await context.test.login({
213
+ userId
214
+ });
215
+ return cookies.map(({
216
+ name,
217
+ value
218
+ }) => `${name}=${value}`).join("; ");
219
+ });
220
+ }
190
221
  /** Inserts an `auth.users` row: a fresh verified email, created now,
191
222
  * `fields` adding or overriding columns (the app's `additionalFields`).
192
223
  * Resolves to the new user's id. */
@@ -26,16 +26,55 @@ export interface SessionSource {
26
26
  start(): Promise<string>;
27
27
  end(): Promise<void>;
28
28
  }
29
- /** A throwaway email user signed up through the app at `origin` (apps
30
- * with `devEmailSignIn`), deleted from `db` at `end`. */
31
- export declare function emailSession(request: AppRequester, { origin, db }: {
32
- origin: string;
33
- db: string;
34
- }): SessionSource;
29
+ /** Dev sign-in through the app at `origin` as `accountId`
30
+ * (`ShellAuthApp.devSignIn`): its answer. */
31
+ export declare function devSignIn(request: AppRequester, origin: string, accountId: string): Promise<{
32
+ status: number;
33
+ headers: {
34
+ get(name: string): string | null;
35
+ getSetCookie(): string[];
36
+ };
37
+ text(): Promise<string>;
38
+ body: {
39
+ cancel(): Promise<void>;
40
+ } | null;
41
+ }>;
42
+ /** A session `devSignInAs` signed in. */
43
+ export interface DevSignedIn {
44
+ /** The session's user. */
45
+ userId: string;
46
+ /** `cookie` header of the response's cookies. */
47
+ cookie: string;
48
+ /** The response's `Set-Cookie` values. */
49
+ setCookies: string[];
50
+ }
51
+ /** Signs in through the app's dev sign-in at `origin` as `accountId`;
52
+ * rejects naming any answer other than 200. */
53
+ export declare function devSignInAs(request: AppRequester, origin: string, accountId: string): Promise<DevSignedIn>;
54
+ /** The session and user ids `cookie` carries, read past the cookie
55
+ * cache. */
56
+ export declare function sessionOf(request: AppRequester, cookie: string): Promise<{
57
+ session: {
58
+ id: string;
59
+ };
60
+ user: {
61
+ id: string;
62
+ };
63
+ }>;
64
+ /** A session signed in through the app's dev sign-in at `origin` as a
65
+ * fresh `devAccountId()` (apps with `devSignIn`); its new user is deleted
66
+ * from `db` at `end`. */
67
+ export declare function devSignInSession(request: AppRequester, { origin, db, devAccountId, }: Pick<DevAuthSuite, "origin" | "db" | "devAccountId">): SessionSource;
35
68
  /** What `testDevAuth` runs against. */
36
69
  export interface DevAuthSuite {
37
70
  /** The signed-in visitor. */
38
71
  session: SessionSource;
72
+ /** The app's origin; dev sign-in posts from it. */
73
+ origin: string;
74
+ /** A fresh account id of the app's `devSignIn` provider: unheld by any
75
+ * account, its `newUser` email by any user. The suite deletes each user
76
+ * dev sign-in answers for one. */
77
+ devAccountId: () => string;
39
78
  /** Connection string of the app's database. */
40
79
  db: string;
41
80
  /** Postgres role the app queries as. */
@@ -55,11 +94,12 @@ export interface DevAuthSuite {
55
94
  }
56
95
  /**
57
96
  * Tests of `request`'s dev app serving auth over its Postgres: the database
58
- * role, public pages independent of the viewer, provider callbacks on
59
- * each local origin, and the rate limit, last: it exhausts the client's
60
- * budget for the window.
97
+ * role, public pages independent of the viewer, dev sign-in, provider
98
+ * callbacks on each local origin, and the rate limit, last: it exhausts
99
+ * the client's budget for the window. Users dev sign-in creates are
100
+ * deleted from `db` after.
61
101
  */
62
- export declare function testDevAuth(request: AppRequester, { session, db, role, publicPages, provider, localOrigins, rateLimit, }: DevAuthSuite): void;
102
+ export declare function testDevAuth(request: AppRequester, { session, origin, devAccountId, db, role, publicPages, provider, localOrigins, rateLimit, }: DevAuthSuite): void;
63
103
  /** What `testProductionAuth` runs against. */
64
104
  export interface ProductionAuthSuite<Fields extends AdditionalFields, Projected extends SessionLike> {
65
105
  /** The app's public origin. */
@@ -78,8 +118,8 @@ export interface ProductionAuthSuite<Fields extends AdditionalFields, Projected
78
118
  }
79
119
  /**
80
120
  * Tests of `request`'s production app serving auth, against `app` built for
81
- * production (`devEmailSignIn` off): its schema, 404s outside the served
82
- * endpoints, ID-token refusal, the anonymous session, a failed provider
121
+ * production (`devSignIn` off): its schema, 404s outside the served
122
+ * endpoints and at dev sign-in, ID-token refusal, the anonymous session, a failed provider
83
123
  * callback landing on the error page, and each provider's sign-in with its
84
124
  * runtime client and callback.
85
125
  */