@connextar/house 0.18.0 → 0.19.1

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 (91) hide show
  1. package/README.md +673 -8
  2. package/dist/admin/access.d.ts +134 -0
  3. package/dist/admin/access.d.ts.map +1 -0
  4. package/dist/admin/access.js +157 -0
  5. package/dist/admin/access.js.map +1 -0
  6. package/dist/admin/audit.d.ts +130 -0
  7. package/dist/admin/audit.d.ts.map +1 -0
  8. package/dist/admin/audit.js +132 -0
  9. package/dist/admin/audit.js.map +1 -0
  10. package/dist/admin/bootstrap.d.ts +80 -0
  11. package/dist/admin/bootstrap.d.ts.map +1 -0
  12. package/dist/admin/bootstrap.js +106 -0
  13. package/dist/admin/bootstrap.js.map +1 -0
  14. package/dist/admin/client-index.d.ts +7 -0
  15. package/dist/admin/client-index.d.ts.map +1 -0
  16. package/dist/admin/client-index.js +6 -0
  17. package/dist/admin/client-index.js.map +1 -0
  18. package/dist/admin/client.d.ts +72 -0
  19. package/dist/admin/client.d.ts.map +1 -0
  20. package/dist/admin/client.js +90 -0
  21. package/dist/admin/client.js.map +1 -0
  22. package/dist/admin/index.d.ts +13 -0
  23. package/dist/admin/index.d.ts.map +1 -0
  24. package/dist/admin/index.js +13 -0
  25. package/dist/admin/index.js.map +1 -0
  26. package/dist/admin/proof.d.ts +34 -0
  27. package/dist/admin/proof.d.ts.map +1 -0
  28. package/dist/admin/proof.js +30 -0
  29. package/dist/admin/proof.js.map +1 -0
  30. package/dist/admin/second-factor-index.d.ts +8 -0
  31. package/dist/admin/second-factor-index.d.ts.map +1 -0
  32. package/dist/admin/second-factor-index.js +8 -0
  33. package/dist/admin/second-factor-index.js.map +1 -0
  34. package/dist/admin/second-factor.d.ts +156 -0
  35. package/dist/admin/second-factor.d.ts.map +1 -0
  36. package/dist/admin/second-factor.js +271 -0
  37. package/dist/admin/second-factor.js.map +1 -0
  38. package/dist/admin/testing-index.d.ts +6 -0
  39. package/dist/admin/testing-index.d.ts.map +1 -0
  40. package/dist/admin/testing-index.js +6 -0
  41. package/dist/admin/testing-index.js.map +1 -0
  42. package/dist/admin/testing.d.ts +84 -0
  43. package/dist/admin/testing.d.ts.map +1 -0
  44. package/dist/admin/testing.js +223 -0
  45. package/dist/admin/testing.js.map +1 -0
  46. package/dist/admin/view-as.d.ts +209 -0
  47. package/dist/admin/view-as.d.ts.map +1 -0
  48. package/dist/admin/view-as.js +251 -0
  49. package/dist/admin/view-as.js.map +1 -0
  50. package/dist/guides/guide-figure-zoom.d.ts +33 -8
  51. package/dist/guides/guide-figure-zoom.d.ts.map +1 -1
  52. package/dist/guides/guide-figure-zoom.js +34 -9
  53. package/dist/guides/guide-figure-zoom.js.map +1 -1
  54. package/dist/guides/library.d.ts +22 -10
  55. package/dist/guides/library.d.ts.map +1 -1
  56. package/dist/guides/library.js +3 -0
  57. package/dist/guides/library.js.map +1 -1
  58. package/dist/markdown/guide.d.ts +12 -5
  59. package/dist/markdown/guide.d.ts.map +1 -1
  60. package/dist/markdown/guide.js +9 -5
  61. package/dist/markdown/guide.js.map +1 -1
  62. package/dist/release/changelog.d.ts +19 -0
  63. package/dist/release/changelog.d.ts.map +1 -1
  64. package/dist/release/changelog.js +37 -6
  65. package/dist/release/changelog.js.map +1 -1
  66. package/dist/release/index.d.ts +1 -1
  67. package/dist/release/index.d.ts.map +1 -1
  68. package/dist/release/index.js +1 -1
  69. package/dist/release/index.js.map +1 -1
  70. package/dist/whats-new/body.d.ts +81 -0
  71. package/dist/whats-new/body.d.ts.map +1 -0
  72. package/dist/whats-new/body.js +29 -0
  73. package/dist/whats-new/body.js.map +1 -0
  74. package/dist/whats-new/index.d.ts +24 -0
  75. package/dist/whats-new/index.d.ts.map +1 -0
  76. package/dist/whats-new/index.js +24 -0
  77. package/dist/whats-new/index.js.map +1 -0
  78. package/dist/whats-new/seen.d.ts +23 -0
  79. package/dist/whats-new/seen.d.ts.map +1 -0
  80. package/dist/whats-new/seen.js +33 -0
  81. package/dist/whats-new/seen.js.map +1 -0
  82. package/dist/whats-new/select.d.ts +89 -0
  83. package/dist/whats-new/select.d.ts.map +1 -0
  84. package/dist/whats-new/select.js +117 -0
  85. package/dist/whats-new/select.js.map +1 -0
  86. package/dist/whats-new/use-whats-new.d.ts +36 -0
  87. package/dist/whats-new/use-whats-new.d.ts.map +1 -0
  88. package/dist/whats-new/use-whats-new.js +81 -0
  89. package/dist/whats-new/use-whats-new.js.map +1 -0
  90. package/docs/ReleaseProcess.md +12 -0
  91. package/package.json +21 -1
@@ -0,0 +1,223 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { join, sep } from "node:path";
3
+ export class AdminGuardCoverageError extends Error {
4
+ problems;
5
+ constructor(problems) {
6
+ super(`Admin guard coverage — ${problems.length} problem${problems.length === 1 ? "" : "s"}:\n - ${problems.join("\n - ")}`);
7
+ this.name = "AdminGuardCoverageError";
8
+ this.problems = problems;
9
+ }
10
+ }
11
+ const METHODS = "GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS";
12
+ const HANDLER = new RegExp(String.raw `export\s+async\s+function\s+(${METHODS})\s*\(`, "g");
13
+ const UNREADABLE = [
14
+ new RegExp(String.raw `export\s+(?:const|let|var)\s+(${METHODS})\b`, "g"),
15
+ new RegExp(String.raw `export\s+function\s+(${METHODS})\b`, "g"),
16
+ new RegExp(String.raw `export\s*\{[^}]*\b(${METHODS})\b[^}]*\}`, "g"),
17
+ new RegExp(String.raw `export\s+default\b`, "g"),
18
+ ];
19
+ const SOURCE = /\.(?:[cm]?[jt]sx?)$/;
20
+ const TEST = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
21
+ const ROUTE = /^route\.[cm]?[jt]sx?$/;
22
+ const DEFAULT_PAGE = /^page\.[jt]sx?$/;
23
+ /** Source with its comments blanked out, strings and template literals left as they are. */
24
+ export function stripComments(source) {
25
+ let out = "";
26
+ let i = 0;
27
+ let quote = null;
28
+ while (i < source.length) {
29
+ const char = source[i];
30
+ const next = source[i + 1];
31
+ if (quote) {
32
+ out += char;
33
+ if (char === "\\") {
34
+ out += next ?? "";
35
+ i += 2;
36
+ continue;
37
+ }
38
+ if (char === quote)
39
+ quote = null;
40
+ i += 1;
41
+ continue;
42
+ }
43
+ if (char === "\\") {
44
+ out += char + (next ?? "");
45
+ i += 2;
46
+ continue;
47
+ }
48
+ if (char === '"' || char === "'" || char === "`") {
49
+ quote = char;
50
+ out += char;
51
+ i += 1;
52
+ continue;
53
+ }
54
+ if (char === "/" && next === "/") {
55
+ while (i < source.length && source[i] !== "\n")
56
+ i += 1;
57
+ continue;
58
+ }
59
+ if (char === "/" && next === "*") {
60
+ const end = source.indexOf("*/", i + 2);
61
+ const stop = end === -1 ? source.length : end + 2;
62
+ // Keep the line breaks, so nothing after the comment moves line.
63
+ out += source.slice(i, stop).replace(/[^\n]/g, " ");
64
+ i = stop;
65
+ continue;
66
+ }
67
+ out += char;
68
+ i += 1;
69
+ }
70
+ return out;
71
+ }
72
+ function filesUnder(root, dir, match) {
73
+ const full = join(root, dir);
74
+ if (!existsSync(full))
75
+ return [];
76
+ const out = [];
77
+ for (const name of readdirSync(full).sort()) {
78
+ if (name === "node_modules" || name.startsWith("."))
79
+ continue;
80
+ const path = join(dir, name);
81
+ if (statSync(join(root, path)).isDirectory())
82
+ out.push(...filesUnder(root, path, match));
83
+ else if (match(name))
84
+ out.push(path);
85
+ }
86
+ return out;
87
+ }
88
+ const posix = (path) => path.split(sep).join("/");
89
+ const names = (guard) => (typeof guard === "string" ? [guard] : [...guard]);
90
+ /** The sweep, answering every problem it found rather than stopping at the first. */
91
+ export function checkAdminGuardCoverage(options) {
92
+ const root = options.root ?? process.cwd();
93
+ const apiGuards = names(options.apiGuard);
94
+ const pageGuards = names(options.pageGuard);
95
+ const exempt = options.exempt ?? {};
96
+ const pageFile = options.pageFile ?? DEFAULT_PAGE;
97
+ const problems = [];
98
+ const read = (file) => readFileSync(join(root, file), "utf8");
99
+ const calls = (name, guards) => guards.some((guard) => name === guard || name.endsWith(`.${guard}`));
100
+ for (const dir of [...options.apiDirs, ...options.pageDirs]) {
101
+ if (!existsSync(join(root, dir)))
102
+ problems.push(`${dir}: not found — the sweep would check nothing there`);
103
+ }
104
+ const routes = [
105
+ ...new Set([...options.apiDirs, ...options.pageDirs].flatMap((dir) => filesUnder(root, dir, (n) => ROUTE.test(n)))),
106
+ ]
107
+ .map(posix)
108
+ .sort();
109
+ const handlers = [];
110
+ const usedExemptions = new Set();
111
+ for (const route of routes) {
112
+ const code = stripComments(read(route));
113
+ for (const pattern of UNREADABLE) {
114
+ for (const match of code.matchAll(pattern)) {
115
+ problems.push(`${route}: \`${match[0].replace(/\s+/g, " ").trim()}\` — export handlers as \`export async function ${match[1] ?? "GET"}(…)\` so the sweep can read them`);
116
+ }
117
+ }
118
+ const found = [...code.matchAll(HANDLER)];
119
+ if (found.length === 0)
120
+ problems.push(`${route}: no \`export async function <METHOD>(…)\` handler found`);
121
+ found.forEach((handler, index) => {
122
+ const name = `${route} ${handler[1]}`;
123
+ handlers.push(name);
124
+ if (Object.hasOwn(exempt, name)) {
125
+ usedExemptions.add(name);
126
+ return;
127
+ }
128
+ const end = found[index + 1]?.index ?? code.length;
129
+ const body = code.slice(handler.index, end);
130
+ const first = /\bawait\s+([\w$.]+)\s*\(/.exec(body)?.[1];
131
+ if (!first)
132
+ problems.push(`${name}: never awaits ${apiGuards.join(" or ")}`);
133
+ else if (!calls(first, apiGuards)) {
134
+ problems.push(`${name}: the first await is ${first}(), not ${apiGuards.join(" or ")}()`);
135
+ }
136
+ });
137
+ }
138
+ for (const name of Object.keys(exempt)) {
139
+ if (!usedExemptions.has(name))
140
+ problems.push(`exempt "${name}": no such handler — remove the exemption`);
141
+ }
142
+ const pages = [...new Set(options.pageDirs.flatMap((dir) => filesUnder(root, dir, (n) => pageFile.test(n))))]
143
+ .map(posix)
144
+ .sort();
145
+ const pageCall = new RegExp(String.raw `\bawait\s+(?:[\w$]+\.)*(?:${pageGuards.map(escape).join("|")})\s*\(`);
146
+ for (const page of pages) {
147
+ if (!pageCall.test(stripComments(read(page))))
148
+ problems.push(`${page}: does not await ${pageGuards.join(" or ")}()`);
149
+ }
150
+ for (const file of [...options.apiDirs, ...options.pageDirs].flatMap((dir) => filesUnder(root, dir, (n) => SOURCE.test(n) && !TEST.test(n)))) {
151
+ if (/^\s*(?:\/\/[^\n]*\n\s*|\/\*[\s\S]*?\*\/\s*)*["']use server["']/.test(read(file))) {
152
+ problems.push(`${posix(file)}: a "use server" file is an endpoint the route sweep cannot see — make it a route`);
153
+ }
154
+ }
155
+ let flagReaders = [];
156
+ if (options.flagReaders) {
157
+ const { pattern, dirs, allowed, ignore } = options.flagReaders;
158
+ const probe = new RegExp(pattern.source, pattern.flags.replace(/g|y/g, ""));
159
+ flagReaders = [...new Set(dirs.flatMap((dir) => filesUnder(root, dir, (n) => SOURCE.test(n) && !TEST.test(n))))]
160
+ .map(posix)
161
+ .filter((file) => !ignore?.(file))
162
+ .filter((file) => probe.test(read(file)))
163
+ .sort();
164
+ for (const file of flagReaders) {
165
+ if (!Object.hasOwn(allowed, file)) {
166
+ problems.push(`${file}: mentions the admin flag (${pattern}) and is not on the allow-list — check it goes through the guard, then list it with why`);
167
+ }
168
+ }
169
+ for (const file of Object.keys(allowed)) {
170
+ if (!flagReaders.includes(file))
171
+ problems.push(`${file}: is on the flag allow-list but no longer mentions it — remove it`);
172
+ }
173
+ }
174
+ const minimum = { routes: 1, handlers: 1, pages: 1, ...options.minimum };
175
+ if (options.apiDirs.length && routes.length < minimum.routes) {
176
+ problems.push(`found ${routes.length} admin route file(s), expected at least ${minimum.routes}`);
177
+ }
178
+ if (options.apiDirs.length && handlers.length < minimum.handlers) {
179
+ problems.push(`found ${handlers.length} admin handler(s), expected at least ${minimum.handlers}`);
180
+ }
181
+ if (options.pageDirs.length && pages.length < minimum.pages) {
182
+ problems.push(`found ${pages.length} admin page(s), expected at least ${minimum.pages}`);
183
+ }
184
+ return { routes, handlers, pages, flagReaders, problems };
185
+ }
186
+ function escape(text) {
187
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
188
+ }
189
+ /** The sweep, throwing every problem at once. Answers what it checked, so a test can say how much. */
190
+ export function assertAdminGuardCoverage(options) {
191
+ const report = checkAdminGuardCoverage(options);
192
+ if (report.problems.length)
193
+ throw new AdminGuardCoverageError(report.problems);
194
+ return report;
195
+ }
196
+ /** A cookie store in memory, shaped like Next's `cookies()`, for testing an app's "view as". */
197
+ export function memoryCookieJar(initial = {}) {
198
+ const values = new Map(Object.entries(initial));
199
+ const attributes = new Map();
200
+ const jar = {
201
+ get(name) {
202
+ const value = values.get(name);
203
+ return value === undefined ? undefined : { name, value };
204
+ },
205
+ set(name, value, options) {
206
+ values.set(name, value);
207
+ attributes.set(name, options);
208
+ },
209
+ delete(name) {
210
+ values.delete(name);
211
+ attributes.delete(name);
212
+ },
213
+ /** What `set` was given for a cookie. */
214
+ options(name) {
215
+ return attributes.get(name);
216
+ },
217
+ has(name) {
218
+ return values.has(name);
219
+ },
220
+ };
221
+ return jar;
222
+ }
223
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.js","sourceRoot":"","sources":["../../src/admin/testing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,209 @@
1
+ import { type AdminAccess, type AdminRefusal } from "./access.js";
2
+ import { type AdminAuditScope, type AdminAuditSink, type AuditLogger, type RequestMeta } from "./audit.js";
3
+ /**
4
+ * "View as" — seeing the product through one person's account, AltEd's rules made safe by default.
5
+ *
6
+ * Support needs the shape of what somebody sees, not just their rows. An administrator can already read every record,
7
+ * so a view grants no new access: it changes which screens render, not what may be read. Five rules make it safe:
8
+ *
9
+ * 1. **Read-only.** Every write is refused while viewing, except the app's allow-list of writes that happen as a side
10
+ * effect of reading (`viewAsWriteGuard`). What reaches outside the database — Stripe, email, a rate-limit bucket —
11
+ * is refused in the route with `refuseWhileViewingAs`.
12
+ * 2. **Short.** The cookie lapses on its own (60 minutes by default), and the expiry is inside the signed value, so the
13
+ * server decides rather than the browser.
14
+ * 3. **The administrator's, and only while they are one.** The cookie names the administrator who started it, and is
15
+ * honoured only while the real session passes `adminAccess` — checked on every request, so a demotion, a revoked
16
+ * session or a sign-in without the second factor ends the view at once. A cookie is a browser's, not a session's,
17
+ * and `signOut` does not clear it: the next person to sign in on a shared machine inherits nothing.
18
+ * 4. **Never another administrator.** Their view is this one; viewing as them would let one administrator act with
19
+ * another's standing. Checked when it starts and on every read, so a promotion mid-view ends it.
20
+ * 5. **Recorded.** Starting and stopping are both audit events naming the administrator and the account.
21
+ *
22
+ * **Signed.** AltEd's cookie was unsigned by design — honoured only for an administrator, who could start any view
23
+ * anyway. That holds only while every reader checks the binding the same way; signing it costs one HMAC and makes a
24
+ * forged or edited cookie worthless whoever reads it.
25
+ */
26
+ /** The cookie store this writes to — Next's `cookies()` from `next/headers` fits it as it is. */
27
+ export interface CookieJar {
28
+ get(name: string): {
29
+ value: string;
30
+ } | undefined;
31
+ set(name: string, value: string, options?: ViewAsCookieOptions): unknown;
32
+ delete(name: string): unknown;
33
+ }
34
+ export interface ViewAsCookieOptions {
35
+ httpOnly: boolean;
36
+ sameSite: "lax" | "strict";
37
+ secure: boolean;
38
+ path: string;
39
+ maxAge: number;
40
+ }
41
+ export interface ViewAsOptions {
42
+ cookieName: string;
43
+ /** At least 32 characters. The app's `BETTER_AUTH_SECRET` serves; the cookie name keeps the signature its own. */
44
+ secret: string;
45
+ /** How long a view lasts before it lapses on its own. Default 60. */
46
+ minutes?: number;
47
+ /** The request's cookie store: `cookies` from `next/headers`, or anything shaped like it. */
48
+ cookies: () => CookieJar | Promise<CookieJar>;
49
+ /** The app's `adminAccess`: the real session must pass it to start a view, and for the view to be honoured. */
50
+ access: AdminAccess;
51
+ /** Where start and stop are recorded. */
52
+ audit?: AdminAuditSink;
53
+ /** A name for the audit events when the caller does not pass one. */
54
+ names?: (userId: string) => string | null | undefined | Promise<string | null | undefined>;
55
+ /** The actor's standing in the audit events. Default `"admin"`. */
56
+ standing?: string;
57
+ /** The audit events' scope. Default platform. */
58
+ scope?: AdminAuditScope;
59
+ /** Default: `process.env.NODE_ENV === "production"`. */
60
+ secure?: boolean;
61
+ /** Default `"/"`. */
62
+ path?: string;
63
+ /** For the refusals `start` answers with. */
64
+ words?: {
65
+ self?: string;
66
+ targetIsAdmin?: string;
67
+ };
68
+ logger?: AuditLogger;
69
+ /** For tests. */
70
+ now?: () => number;
71
+ }
72
+ /** A view in force. */
73
+ export interface ViewingAs {
74
+ targetId: string;
75
+ adminId: string;
76
+ expiresAt: Date;
77
+ }
78
+ /** The administrator behind the request — the real session, never the account on screen. */
79
+ export interface RealAdmin {
80
+ id: string;
81
+ sessionId: string;
82
+ name?: string | null;
83
+ }
84
+ export type ViewAsStartRefusal = {
85
+ ok: false;
86
+ reason: "self" | "target-is-admin";
87
+ message: string;
88
+ status: 400;
89
+ } | AdminRefusal;
90
+ export type ViewAsStartResult = {
91
+ ok: true;
92
+ view: ViewingAs;
93
+ minutes: number;
94
+ } | ViewAsStartRefusal;
95
+ export interface ViewAs {
96
+ /** Start viewing as `target`, for this administrator. Refused unless their real session passes `adminAccess`. */
97
+ start(admin: RealAdmin, target: string | {
98
+ id: string;
99
+ name?: string | null;
100
+ }, meta?: RequestMeta): Promise<ViewAsStartResult>;
101
+ /**
102
+ * End the view. Works for anybody signed in — while a view is in force the request presents as the person viewed,
103
+ * who is not an administrator and could not otherwise end it. Recorded when it was this administrator's view.
104
+ */
105
+ stop(realAdmin?: {
106
+ id: string;
107
+ name?: string | null;
108
+ } | null, meta?: RequestMeta): Promise<{
109
+ stopped: ViewingAs | null;
110
+ }>;
111
+ /** The view in force for this administrator, or null. Never writes, so a server component can call it. */
112
+ read(realAdmin: RealAdmin | null | undefined): Promise<ViewingAs | null>;
113
+ readonly cookieName: string;
114
+ readonly minutes: number;
115
+ }
116
+ export declare function viewAs(options: ViewAsOptions): ViewAs;
117
+ /** Whoever the request presents as: AltEd's and Quickie's `impersonatedBy`, or a `viewingAs`. */
118
+ export interface MaybeViewing {
119
+ name?: string | null;
120
+ impersonatedBy?: unknown;
121
+ viewingAs?: {
122
+ name?: string | null;
123
+ } | boolean | null;
124
+ }
125
+ export interface RefuseWhileViewingAsOptions {
126
+ /** Throw the app's own error class: `(message) => new ForbiddenError(message)`. Default `AdminRefusalError`. */
127
+ makeError?: (message: string) => Error;
128
+ }
129
+ /** The sentence a refusal under "view as" says. */
130
+ export declare function viewingAsMessage(name: string | null | undefined, what: string): string;
131
+ /**
132
+ * Refuse, under "view as", a request that acts for the viewed account where the write guard cannot see it: a Stripe
133
+ * call, an email, a rate-limit bucket. Call it first in the route, before anything is spent.
134
+ */
135
+ export declare function refuseWhileViewingAs(user: MaybeViewing | null | undefined, what: string, options?: RefuseWhileViewingAsOptions): void;
136
+ /** The view this request is in, as the app's request-scoped actor has it: `{ name }`, `true`, or null when not. */
137
+ export type ViewingLookup = () => {
138
+ name?: string | null;
139
+ } | boolean | null | undefined | Promise<{
140
+ name?: string | null;
141
+ } | boolean | null | undefined>;
142
+ export interface ViewingAsGateOptions {
143
+ /** The same lookup the write guard takes — the app's request actor's `viewingAs`. */
144
+ getViewing: ViewingLookup;
145
+ makeError?: (message: string) => Error;
146
+ }
147
+ export interface ViewingAsGate {
148
+ isViewingAs(): Promise<boolean>;
149
+ /** Throws under "view as". Fail-closed: a lookup that throws refuses too. */
150
+ assertNotViewingAs(what: string): Promise<void>;
151
+ }
152
+ /**
153
+ * The fail-closed default for what reaches outside the database: call `assertNotViewingAs` once inside the app's own
154
+ * `sendEmail`, `sendSms`, Stripe, GoCardless, Xero and AI wrappers, and every route that uses them is covered —
155
+ * including the next one written. Quickie had to add `refuseWhileViewingAs` to thirty handlers by hand.
156
+ *
157
+ * It reads the app's request-scoped actor rather than keeping a store of its own. An `AsyncLocalStorage` set where the
158
+ * view is resolved — inside `getCurrentUser`, an awaited callee — does not reach the route's later calls: a store
159
+ * entered in a callee after its first await (the session read) never propagates back to the caller's continuation
160
+ * (`view-as.test.ts` pins it), and wrapping
161
+ * every handler in `withViewingAs(…, fn)` is the per-route step this exists to remove. Quickie's and AltEd's actor
162
+ * is keyed on the request itself (Next's `headers()` object), which every call in the request shares.
163
+ */
164
+ export declare function viewingAsGate(options: ViewingAsGateOptions): ViewingAsGate;
165
+ /** Every Prisma write operation. A read is never refused — that is the whole point of a view. */
166
+ export declare const PRISMA_WRITE_OPERATIONS: ReadonlySet<string>;
167
+ /** `allow` from a map of model → operations: `allowWhileViewing({ Session: ["update", "updateMany"] })`. */
168
+ export declare function allowWhileViewing(list: Record<string, readonly string[]>): (model: string | undefined, operation: string) => boolean;
169
+ /** The arguments Prisma hands a `$allOperations` query extension. */
170
+ export interface PrismaOperationParams {
171
+ model?: string;
172
+ operation: string;
173
+ args: unknown;
174
+ query: (args: unknown) => Promise<unknown>;
175
+ }
176
+ export interface ViewAsWriteGuardOptions {
177
+ /** Which operations are writes. Default: {@link PRISMA_WRITE_OPERATIONS}. */
178
+ isWrite?: (operation: string) => boolean;
179
+ /**
180
+ * The writes that happen as a side effect of reading, and so stay allowed: Better Auth touching the session row,
181
+ * the rate limiter counting, the audit trail recording the view itself. Everything else is refused.
182
+ */
183
+ allow: (model: string | undefined, operation: string) => boolean;
184
+ /** The view this request is in, from the app's request actor: `{ name }`, `true`, or null when not viewing. */
185
+ getViewing: ViewingLookup;
186
+ makeError?: (message: string) => Error;
187
+ /** The extension's name. Default `"view-as-write-guard"`. */
188
+ name?: string;
189
+ }
190
+ /**
191
+ * Make "view as" read-only at the client: `prisma.$extends(viewAsWriteGuard({ … }))`. Every write that arrives while
192
+ * the request is viewing as somebody is refused, whichever handler made it, unless it is on the allow-list.
193
+ *
194
+ * The rule is inverted from "block what is audited", which AltEd learnt by running it: deleting an account ran
195
+ * `session.deleteMany` and `passkey.deleteMany` before it touched the user row, so the deletion was refused —
196
+ * correctly — after signing the family out of every device. So everything is refused except what is allowed.
197
+ *
198
+ * Typed structurally, so this package imports no Prisma. If an app's Prisma types object to it, wrap it:
199
+ * `Prisma.defineExtension(viewAsWriteGuard({ … }))`.
200
+ */
201
+ export declare function viewAsWriteGuard(options: ViewAsWriteGuardOptions): {
202
+ name: string;
203
+ query: {
204
+ $allModels: {
205
+ $allOperations({ model, operation, args, query }: PrismaOperationParams): Promise<unknown>;
206
+ };
207
+ };
208
+ };
209
+ //# sourceMappingURL=view-as.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view-as.d.ts","sourceRoot":"","sources":["../../src/admin/view-as.ts"],"names":[],"mappings":"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"}