@aotter/mantle 0.2.0-alpha.10 → 0.2.0-alpha.11

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 (121) hide show
  1. package/dist/auth/adapter.js +1 -28
  2. package/dist/auth/admin.js +2 -5
  3. package/dist/auth/caller.js +0 -27
  4. package/dist/auth/consent.js +100 -0
  5. package/dist/auth/hardening.js +0 -7
  6. package/dist/auth/identity.js +0 -11
  7. package/dist/auth/oauth.js +0 -32
  8. package/dist/auth/resource-seeds.js +0 -2
  9. package/dist/auth/route-http.js +33 -0
  10. package/dist/auth/routes.js +5 -151
  11. package/dist/build/compiler.d.ts +12 -611
  12. package/dist/build/compiler.js +3 -7029
  13. package/dist/bun/assets.js +0 -2
  14. package/dist/domain/adminOrder.js +0 -2
  15. package/dist/domain/columns.js +0 -15
  16. package/dist/domain/describe/chunk-SGMRVDGE.js +1 -0
  17. package/dist/domain/describe/index.js +1 -115
  18. package/dist/domain/describe/target-hash.js +1 -12
  19. package/dist/domain/entry.js +0 -14
  20. package/dist/domain/generated-tools.js +0 -1
  21. package/dist/domain/input.js +0 -2
  22. package/dist/domain/paging.js +0 -17
  23. package/dist/domain/recipes.js +0 -2
  24. package/dist/domain/userReferences.js +0 -1
  25. package/dist/domain/wireScalars.js +0 -6
  26. package/dist/infrastructure/adminEntries.js +9 -6
  27. package/dist/infrastructure/adminPaging.js +0 -2
  28. package/dist/infrastructure/artifacts.js +0 -4
  29. package/dist/infrastructure/authSchema.js +0 -9
  30. package/dist/infrastructure/bundle.js +0 -29
  31. package/dist/infrastructure/cli.js +0 -7
  32. package/dist/infrastructure/generate.js +0 -14
  33. package/dist/infrastructure/generateAuth.js +1 -9
  34. package/dist/infrastructure/help.js +0 -5
  35. package/dist/infrastructure/init.js +0 -4
  36. package/dist/infrastructure/inputs.js +0 -9
  37. package/dist/infrastructure/mounts.js +0 -3
  38. package/dist/infrastructure/wrangler.js +0 -6
  39. package/dist/kernel/authorization.js +0 -1
  40. package/dist/kernel/binds.js +1 -11
  41. package/dist/kernel/caller-identity.d.ts +0 -3
  42. package/dist/kernel/caller-identity.js +0 -15
  43. package/dist/kernel/caller-mode.d.ts +4 -0
  44. package/dist/kernel/caller-mode.js +14 -0
  45. package/dist/kernel/contracts.d.ts +1 -9
  46. package/dist/kernel/contracts.js +1 -42
  47. package/dist/kernel/cron.js +0 -4
  48. package/dist/kernel/dialect/common.js +0 -3
  49. package/dist/kernel/dialect/postgres.js +0 -2
  50. package/dist/kernel/dialect/sqlite.js +1 -6
  51. package/dist/kernel/drain.js +2 -8
  52. package/dist/kernel/driver/bun-sqlite.js +0 -4
  53. package/dist/kernel/driver/d1.js +0 -1
  54. package/dist/kernel/driver/postgres.js +0 -15
  55. package/dist/kernel/driver/transaction-context.js +0 -1
  56. package/dist/kernel/email.js +0 -4
  57. package/dist/kernel/host-access.js +2 -3
  58. package/dist/kernel/host-caller.d.ts +1 -1
  59. package/dist/kernel/host-caller.js +3 -6
  60. package/dist/kernel/json-body.js +0 -2
  61. package/dist/kernel/mcp-apps.js +0 -5
  62. package/dist/kernel/mcp-call.js +0 -1
  63. package/dist/kernel/mcp-handler.js +2 -26
  64. package/dist/kernel/mcp.js +0 -5
  65. package/dist/kernel/media.js +0 -18
  66. package/dist/kernel/paging.js +1 -9
  67. package/dist/kernel/publishing.js +1 -2
  68. package/dist/kernel/r2-media.js +0 -7
  69. package/dist/kernel/rest-dispatch.js +0 -1
  70. package/dist/kernel/rest-router.js +2 -3
  71. package/dist/kernel/rest.js +0 -3
  72. package/dist/kernel/scalars.js +1 -9
  73. package/dist/kernel/search.js +0 -1
  74. package/dist/kernel/site.js +0 -2
  75. package/dist/kernel/store.js +1 -26
  76. package/dist/kernel/surface-failure.d.ts +10 -0
  77. package/dist/kernel/surface-failure.js +29 -0
  78. package/dist/kernel/surface.js +0 -3
  79. package/dist/kernel/sweep.js +0 -1
  80. package/dist/kernel/translations.js +0 -1
  81. package/dist/kernel/user-caller.js +0 -6
  82. package/dist/kernel/user-fields.d.ts +1 -1
  83. package/dist/kernel/user-fields.js +1 -11
  84. package/dist/kernel/validation.js +0 -8
  85. package/dist/kernel/web-gateway.js +0 -2
  86. package/dist/kernel/web-head.js +0 -1
  87. package/dist/kernel/web-index.js +0 -1
  88. package/dist/kernel/web-response.js +0 -1
  89. package/dist/kernel/web-serve.js +97 -0
  90. package/dist/kernel/web.d.ts +1 -1
  91. package/dist/kernel/web.js +10 -91
  92. package/dist/kernel/where.js +0 -6
  93. package/dist/usecase/adminBindings.js +28 -21
  94. package/dist/usecase/adminClient.js +0 -21
  95. package/dist/usecase/adminDescriptors.js +2 -9
  96. package/dist/usecase/generate.js +0 -6
  97. package/dist/usecase/handlers.js +0 -3
  98. package/dist/usecase/mcp-media.js +0 -1
  99. package/dist/usecase/mcp.js +0 -4
  100. package/dist/usecase/paging.js +0 -2
  101. package/dist/usecase/publishing.js +0 -4
  102. package/dist/usecase/rest.js +0 -1
  103. package/dist/usecase/site.js +0 -1
  104. package/dist/usecase/stores.js +1 -13
  105. package/dist/usecase/tool-version.js +0 -3
  106. package/dist/usecase/translations.js +0 -2
  107. package/dist/usecase/validation.js +0 -20
  108. package/docs/examples/README.md +1 -1
  109. package/docs/examples/app-shell/package.json +3 -3
  110. package/docs/examples/app-shell/src/routes.ts +3 -1
  111. package/docs/examples/app-shell/src/service.ts +2 -1
  112. package/docs/handbook/cloudflare/service-entry.md +3 -0
  113. package/docs/handbook/examples/hub.md +1 -1
  114. package/docs/handbook/guides/agent-setup.md +4 -4
  115. package/docs/handbook/guides/public-pages.md +6 -0
  116. package/docs/handbook/reference/admin-descriptors.md +4 -1
  117. package/docs/handbook/reference/schema.md +1 -1
  118. package/docs/handbook/releases/index.md +3 -3
  119. package/package.json +2 -2
  120. package/skills/README.md +1 -1
  121. package/dist/domain/describe/chunk-ZVXBGP7Y.js +0 -424
@@ -4,7 +4,6 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "nod
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { build } from "esbuild";
6
6
  import { INPUTS_FILE, authTableSql, extraRoles, generatedTarget, staleInputs } from "./inputs.js";
7
- /** Only static imports run at startup; dynamic imports remain separate uploaded modules. */
8
7
  export function startupGraph(metafile, entry) {
9
8
  const visited = new Set();
10
9
  const visit = (name) => {
@@ -25,23 +24,19 @@ export function startupGraph(metafile, entry) {
25
24
  const refused = [
26
25
  [/(?:^|\/)node_modules\/(?:better-auth(?:\/|$)|@better-auth\/)/, "Load it through authLoader(() => import(...)) to keep Better Auth out of startup."],
27
26
  [/(?:^|\/)node_modules\/@modelcontextprotocol\//, "Load MCP through a request-time import(...)."],
28
- // the compiler: the package's own generator layers (installed, or workspace dist/source) and its parser, YAML and SQL dependencies
29
27
  [/(?:^|\/)node_modules\/(?:@aotter\/mantle\/dist\/(?:build|infrastructure)\/|yaml\/|pgsql-deparser\/|libpg-query\/)|(?:^|\/)mantle\/(?:dist|src)\/(?:build|infrastructure)\//, "Generator code must not run in a Worker; import runtime entry points such as @aotter/mantle/rest."],
30
- // Admin's UI is built into <assets>/admin by the app's admin.tsx; the Worker serves it, never runs it.
31
28
  [/(?:^|\/)node_modules\/@aotter\/mantle-admin\/dist\/admin-app\/|(?:^|\/)mantle-admin\/(?:dist\/admin-app|admin\/src)\//, "Admin's UI (@aotter/mantle-admin/app, /hooks) runs in the browser: import it only from src/admin.tsx, which `mantle bundle` builds into the assets directory."],
32
29
  ];
33
30
  export function guardStartup(metafile, entry) {
34
31
  for (const name of startupGraph(metafile, entry)) {
35
32
  const output = metafile.outputs[name];
36
33
  for (const input of Object.keys(output.inputs)) {
37
- // shortcut: vendored auth outside node_modules is not recognized; use package metadata if vendoring is supported.
38
34
  const hit = refused.find(([pattern]) => pattern.test(input.replaceAll("\\", "/")));
39
35
  if (hit)
40
36
  throw new Error(`Startup module ${name} contains ${input}. ${hit[1]}`);
41
37
  }
42
38
  }
43
39
  }
44
- /** The physical spelling of a path, including a nonexistent tail below its nearest existing ancestor. */
45
40
  const physical = (input) => {
46
41
  let found = resolve(input);
47
42
  const tail = [];
@@ -54,33 +49,23 @@ const physical = (input) => {
54
49
  }
55
50
  return resolve(realpathSync(found), ...tail);
56
51
  };
57
- /** Whether `inner` is `outer` or inside it. */
58
52
  const within = (outer, inner) => {
59
53
  const path = relative(physical(outer), physical(inner));
60
54
  return path === "" || (path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path));
61
55
  };
62
- /** A directory a build empties must not be the working directory or hold the source entry. */
63
56
  function assertCleanable(label, dir, entry) {
64
57
  if (physical(dir) === physical(process.cwd()))
65
58
  throw new Error(`${label} must not be the working directory: it is cleaned before building.`);
66
59
  if (within(dir, entry))
67
60
  throw new Error(`${label} must not contain the source entry: it is cleaned before building.`);
68
61
  }
69
- /** The app's own modules only (packages external): enough to find its generated output and what it imports. */
70
62
  export const ownModules = async (entry) => (await build({ entryPoints: [entry], bundle: true, write: false, metafile: true, packages: "external", format: "esm", platform: "neutral", logLevel: "silent", absWorkingDir: process.cwd() })).metafile;
71
- /**
72
- * `mantle bundle <entry> <outdir> [--assets <dir>]`: the Worker into `<outdir>/worker`, and Admin into `<assets>/admin`
73
- * (default `<outdir>/client`) when the Worker serves it (`@aotter/mantle-admin/server`; `/mcp` and the other subpaths alone do not).
74
- * A server whose generated output records a `bun-*` target runs as TypeScript, so only its Admin is built.
75
- */
76
63
  export async function bundleApp(entry, outdir, assets = join(outdir, "client")) {
77
64
  const source = realpathSync(entry);
78
- // what is cleaned lies under these two, so neither may be the working directory or hold the source
79
65
  assertCleanable("Bundle outdir", outdir, source);
80
66
  assertCleanable("The assets directory", assets, source);
81
67
  const own = await ownModules(entry);
82
68
  if (generatedDirs(own).some(dir => generatedTarget(join(dir, "generated.ts"))?.startsWith("bun-"))) {
83
- // Packages are external here, so only the app's own imports of the Admin server are seen; one reached through another package is not.
84
69
  if (Object.values(own.inputs).some(input => input.imports.some(imported => /^@aotter\/mantle-admin\/server(?:\/|$)/.test(imported.path))))
85
70
  await bundleAdmin(source, outdir, join(assets, "admin"));
86
71
  else
@@ -88,7 +73,6 @@ export async function bundleApp(entry, outdir, assets = join(outdir, "client"))
88
73
  return;
89
74
  }
90
75
  const worker = join(outdir, "worker");
91
- // Wrangler uploads every module under the Worker directory and every file under the assets directory.
92
76
  if (within(worker, assets) || within(assets, worker))
93
77
  throw new Error(`The assets directory ${assets} must not overlap the Worker directory ${worker}.`);
94
78
  const metafile = await bundle(entry, worker);
@@ -96,12 +80,6 @@ export async function bundleApp(entry, outdir, assets = join(outdir, "client"))
96
80
  await bundleAdmin(source, outdir, join(assets, "admin"));
97
81
  }
98
82
  }
99
- /**
100
- * Admin from `<dirname(entry)>/admin.tsx`, or the built-in entry; an optional `admin.mcp.tsx` selects the host-safe graph.
101
- * Uses `@aotter/mantle-admin/build` as the app installed
102
- * it. The built-in entry is skipped when the installed Admin and its direct dependencies are unchanged since the last
103
- * build; an application entry always builds, because what it imports is only known by building it.
104
- */
105
83
  async function bundleAdmin(entry, outdir, out) {
106
84
  const source = join(dirname(entry), "admin.tsx"), custom = existsSync(source);
107
85
  const mcpSource = join(dirname(entry), "admin.mcp.tsx"), customMcp = existsSync(mcpSource);
@@ -115,7 +93,6 @@ async function bundleAdmin(entry, outdir, out) {
115
93
  throw new Error(`The Worker serves Admin, but @aotter/mantle-admin/build does not resolve from ${relative(process.cwd(), entry)}. Install @aotter/mantle-admin at the version of @aotter/mantle-admin/server.`);
116
94
  }
117
95
  const stamp = join(outdir, ".admin-inputs");
118
- // the installed path too: a patched or linked Admin keeps its version but not its path
119
96
  const manifest = realpathSync(lookup(require, "@aotter/mantle-admin") ?? require.resolve("@aotter/mantle-admin/package.json"));
120
97
  const key = custom || customMcp ? undefined : JSON.stringify({ out: resolve(out), path: dirname(manifest), packages: installed(manifest) });
121
98
  if (key && existsSync(join(out, "index.html")) && existsSync(join(out, "mcp-app.html")) && existsSync(stamp) && readFileSync(stamp, "utf8") === key) {
@@ -128,9 +105,7 @@ async function bundleAdmin(entry, outdir, out) {
128
105
  if (key)
129
106
  writeFileSync(stamp, key);
130
107
  }
131
- /** The package.json a bare import of `name` finds, read from disk (`exports` may hide it, and require caches realpaths). */
132
108
  const lookup = (require, name) => require.resolve.paths(name)?.map(dir => join(dir, name, "package.json")).find(file => existsSync(file));
133
- /** The package's version and the installed version of each of its dependencies and peers (React comes from the application). */
134
109
  function installed(manifest) {
135
110
  const { name, version, dependencies = {}, peerDependencies = {} } = JSON.parse(readFileSync(manifest, "utf8"));
136
111
  const require = createRequire(manifest);
@@ -141,7 +116,6 @@ function installed(manifest) {
141
116
  }
142
117
  return versions;
143
118
  }
144
- /** The generated directories among a build's inputs, refusing stale ones. */
145
119
  function generatedDirs(metafile) {
146
120
  const dirs = [...new Set(Object.keys(metafile.inputs).map(input => resolve(input, "..")).filter(dir => existsSync(resolve(dir, INPUTS_FILE))))];
147
121
  for (const dir of dirs) {
@@ -164,15 +138,12 @@ export async function bundle(entry, outdir) {
164
138
  if (args.pluginData?.optionalPeer)
165
139
  return;
166
140
  const resolved = await builder.resolve(args.path, { importer: args.importer, resolveDir: args.resolveDir, kind: args.kind, pluginData: { optionalPeer: true } });
167
- // Unselected request paths may reference absent peers; installed peers still bundle normally.
168
141
  return resolved.errors.length ? { path: args.path, external: true } : resolved;
169
142
  });
170
143
  } }],
171
144
  });
172
145
  if (Object.keys(result.metafile.inputs).some(input => /(?:^|\/)mantle\/(?:src|dist)\/testing\//.test(resolve(input).replaceAll("\\", "/"))))
173
146
  throw new Error("@aotter/mantle/testing is only for tests and cannot be bundled for production.");
174
- // A build is what `wrangler dev` and deploys run: generated code must match the manifests it came from.
175
- // Any bundled module from a generated directory finds its fingerprint (type-only imports leave generated.ts out).
176
147
  generatedDirs(result.metafile);
177
148
  const appSources = Object.keys(result.metafile.inputs).filter(input => !/node_modules|(?:^|\/)generated\/|^\.\.\//.test(input) && /\.[cm]?[jt]s$/.test(input)).map(input => [input, readFileSync(input, "utf8")]);
178
149
  for (const [input, source] of appSources) {
@@ -7,7 +7,6 @@ import { parseArgs } from "node:util";
7
7
  import { configureWrangler } from "./wrangler.js";
8
8
  import { writeArtifacts } from "./artifacts.js";
9
9
  import { INPUTS_FILE, inputsFingerprint } from "./inputs.js";
10
- /** Option modules are often .ts, which older Node rejects with a bare "Unknown file extension". */
11
10
  async function load(path) {
12
11
  try {
13
12
  return await import(pathToFileURL(resolve(path)).href);
@@ -25,7 +24,6 @@ try {
25
24
  diff: { type: "string" }, check: { type: "boolean" }, admin: { type: "boolean" }, site: { type: "string" }, auth: { type: "string", multiple: true }, wrangler: { type: "string" }, entry: { type: "string", default: "src/worker.ts" }, outdir: { type: "string", default: "dist" }, assets: { type: "string" }, "dry-run": { type: "boolean" }, help: { type: "boolean", short: "h" },
26
25
  } });
27
26
  const named = ["bundle", "generate", "describe", "help", "init"].includes(positionals[0]) ? positionals.shift() : undefined;
28
- // `-h` prints usage and never runs a command; bare `mantle` starts at help, but options with no positionals stay a usage error.
29
27
  const command = values.help ? "usage" : named ?? (process.argv.length > 2 ? "generate" : "help");
30
28
  if (command === "usage")
31
29
  console.log(`${usage}\n\nStart with mantle help: the installed docs, one topic at a time.`);
@@ -68,13 +66,10 @@ try {
68
66
  const [target, destination, ...paths] = positionals;
69
67
  if (!target || !targets.includes(target) || !destination || !paths.length)
70
68
  throw new Error(usage);
71
- // A shell leaves an unmatched glob as is; say what is missing instead of ENOENT.
72
69
  for (const path of paths)
73
70
  if (/[*?]/.test(path) && !existsSync(path))
74
71
  throw new Error(`No manifest matches ${path}. Add one (mantle help four-atoms, mantle help manifest), then run generate again.`);
75
72
  const sources = await Promise.all(paths.map(async (path) => ({ sourceId: path, text: await readFile(path, "utf8") })));
76
- // Read plain options without constructing Better Auth, which can touch the database.
77
- // One --auth per Better Auth instance; their tables are merged into one migration.
78
73
  const auth = await Promise.all((values.auth ?? []).map(async (path) => {
79
74
  const loaded = await load(path);
80
75
  const options = loaded.authOptions?.(new Proxy({}, { get: () => undefined })) ?? loaded.auth?.options;
@@ -93,7 +88,6 @@ try {
93
88
  console.warn(`warning: ${warning}`);
94
89
  const read = [...paths, ...(values.auth ?? []), ...(values.site ? [values.site] : [])];
95
90
  const changed = await writeArtifacts(destination, { ...result.files, [INPUTS_FILE]: inputsFingerprint(destination, read, target) }, values.check);
96
- // After writing: the entry's imports of generated modules only resolve once they exist.
97
91
  for (const warning of await (await import("./mounts.js")).unmountedWarnings(values.entry, destination, result.files))
98
92
  console.warn(`warning: ${warning}`);
99
93
  if (target.startsWith("cloudflare-")) {
@@ -107,7 +101,6 @@ try {
107
101
  throw new Error(`Generated output is stale. Run mantle generate with the same options:\n${changed.join("\n")}`);
108
102
  if (!values.check)
109
103
  console.log(`Commit generated output and Wrangler config edits; keep dist/ untracked. Add to .gitattributes: ${JSON.stringify(relative(process.cwd(), resolve(destination)).replaceAll("\\", "/") + "/**")} linguist-generated=true`);
110
- // A contract, not a to-do: the CLI does not read the app's handlers; tsc checks them against Handlers.
111
104
  if (!values.check && result.refProcedures.length)
112
105
  console.log(`Ref Procedures need these Handlers keys (tsc --noEmit checks them; pass them at store(db, caller, { handlers })): ${result.refProcedures.map(name => JSON.stringify(name)).join(", ")}`);
113
106
  }
@@ -1,5 +1,4 @@
1
1
  import { describeApp } from "../domain/describe/index.js";
2
- /** Build-time assembly only. None of these imports enter generated applications. */
3
2
  import { compilePlan, viewOutputs, sqliteCompiler, postgresCompiler, d1Dialect, postgresDialect, lowerPlan, sqliteSchema, postgresSchema } from "../build/compiler.js";
4
3
  import { wireReturning, wireScalars } from "../domain/wireScalars.js";
5
4
  import { boundaryType, outputType } from "../domain/columns.js";
@@ -12,12 +11,7 @@ import { publishingSql } from "../usecase/publishing.js";
12
11
  import { searchDiagnostics } from "../usecase/search.js";
13
12
  import { generate } from "../usecase/generate.js";
14
13
  import { generatedFiles } from "../domain/entry.js";
15
- /** Core's own new-entry-id lowering for a dialect, printed once as an SQL expression: the Store's inserts carry the same one its compiled INSERTs do. */
16
14
  export const coreNewId = (dialect, schemas) => dialect.print({ SelectStmt: { targetList: [{ ResTarget: { val: dialect.lowering.newId() } }], limitOption: "LIMIT_OPTION_DEFAULT", op: "SETOP_NONE" } }, schemas).replace(/^SELECT /, "");
17
- // JSON, date-time and date inputs are driver-ready text. Declaring their parameter as text makes every driver send it verbatim:
18
- // Bun.SQL would JSON-encode a string bound to jsonb a second time, and postgres.js turns one bound to timestamptz or date into a
19
- // JavaScript Date, which loses microseconds. Core prints `CAST($n AS <type>)`; the PostgreSQL dialect spells what replaces it (the
20
- // same cast the Store writes with). Any other use of such a parameter is refused.
21
15
  const CORE_CAST = { json: "jsonb", timestamptz: "timestamptz", date: "date" };
22
16
  function castInputs(statement, path, refusals) {
23
17
  let sql = statement.sql;
@@ -32,23 +26,18 @@ function castInputs(statement, path, refusals) {
32
26
  }
33
27
  Object.assign(statement, { sql });
34
28
  }
35
- /** Operational records are added in Admin only through a Procedure placed with `uiSchema.collectionAction`; name the Schemas left without one. */
36
29
  function adminCreateWarnings(plan) {
37
30
  const placed = new Set(Object.values(plan.procedures).map(p => p.uiSchema?.["collectionAction"]).filter((name) => typeof name === "string").map(name => name.toLowerCase()));
38
31
  return Object.values(plan.schemas).filter(s => {
39
32
  const required = new Set(s.schema.required ?? []);
40
- // Folded children open from their parent; translations from their language tabs.
41
33
  const folded = Object.entries(s.schema.properties ?? {}).some(([name, property]) => required.has(name) && property?.["x-mantle-ref"] != null);
42
- // Field-scoped rows belong to their members, who create them.
43
34
  return !s.publishing && s.schema.readOnly !== true && !s.scope && !s.translates && !folded && !placed.has(s.name.toLowerCase());
44
35
  }).map(s => `Schema '${s.name}' is operational and writable, but no Procedure places a create action on it, so Admin cannot add its records. Add a Procedure with uiSchema.collectionAction: ${s.name} and an admin MCP Trigger, or mark the Schema readOnly.${s.userScopes?.length ? ` That Procedure takes ${s.userScopes.map(u => `'${u.column}'`).join(', ')} as an optional input for callers holding { all: true } (field scope in the Schema reference).` : ''}`).concat(Object.values(plan.schemas)
45
- // A create action's Procedure writes the field itself; only Admin's generic create needs the default.
46
36
  .filter(s => s.schema.readOnly !== true && !placed.has(s.name.toLowerCase()))
47
37
  .flatMap(s => Object.entries(s.schema.properties ?? {})
48
38
  .filter(([name, property]) => property?.readOnly === true && (s.schema.required ?? []).includes(name) && property.default === undefined)
49
39
  .map(([name]) => `Schema '${s.name}' field '${name}' is required and readOnly without a default, so Admin's generic create cannot fill it. Give it a default (Admin stores it on create), or create the records through a Procedure with uiSchema.collectionAction: ${s.name}.`)));
50
40
  }
51
- /** Warn about SQL's case folding without changing the authored output names. */
52
41
  function outputNameWarnings(plan) {
53
42
  const warnings = [];
54
43
  const inspect = (kind, name, stmts) => {
@@ -116,8 +105,6 @@ export async function generateFromSources(request, schema) {
116
105
  const refusals = [], paging = {};
117
106
  const lower = (boundary) => lowerPlan(compiled.plan, dialect, {
118
107
  pageQuery(ast, name) {
119
- // Core visits public/caller before trusted. Prove the real caller equality prefixes;
120
- // a trusted all-scope read does not acquire a fictional scope predicate.
121
108
  if (!Object.hasOwn(paging, name))
122
109
  paging[name] = provePaging(compiled.plan, ast);
123
110
  return ast;
@@ -172,7 +159,6 @@ export async function generateFromSources(request, schema) {
172
159
  !["Every reachable visibility mode must have canonical lowered SQL.", "Both caller and trusted SQL must be lowered."].includes(d.message));
173
160
  return { ok: false, diagnostics: [...refusals, ...diagnostics] };
174
161
  }
175
- // A Procedure the runtime cannot lower names only that; the lowering's own refusal says why.
176
162
  if (!result.ok)
177
163
  return { ...result, diagnostics: result.diagnostics.map(d => {
178
164
  const why = d.message === "Both caller and trusted SQL must be lowered." ? warnings.find(w => w.includes(`${d.path} (caller)`) || w.includes(`${d.path} (admin)`)) : undefined;
@@ -1,7 +1,6 @@
1
1
  import { generateFromSources as generate } from "./generate.js";
2
2
  import { admin, anonymous, jwt, phoneNumber, siwe, twoFactor } from "better-auth/plugins";
3
3
  import { authSchemaSql } from "./authSchema.js";
4
- /** One or more Better Auth instances (each with its own table names); their DDL is merged. */
5
4
  export function generateFromSources(request) {
6
5
  const instances = request.auth ? [request.auth].flat() : [];
7
6
  return generate(request, instances.length ? () => authSql(instances, request.target.endsWith("postgres") ? "postgres" : "sqlite") : undefined);
@@ -9,29 +8,23 @@ export function generateFromSources(request) {
9
8
  function authSql(instances, dialect) {
10
9
  for (const options of instances) {
11
10
  const ids = (options.plugins ?? []).map(plugin => plugin.id);
12
- // Offline OAuth grants outlive sessions; without the revocation plugin a password reset or ban would not end them. mcp() spreads oauthProvider, id included.
13
- if (ids.includes("oauth-provider") && !ids.includes("mantle-oauth-grant-revocation") /* oauthGrantRevocation().id */)
11
+ if (ids.includes("oauth-provider") && !ids.includes("mantle-oauth-grant-revocation") )
14
12
  throw new Error("Better Auth has an OAuth provider (oauthProvider or mcp) without oauthGrantRevocation(): add it from @aotter/mantle/auth/oauth to the plugins, so a password reset, an admin ban or session revocation ends that user's OAuth grants.");
15
13
  }
16
14
  if (instances.length > 1)
17
15
  refuseSharedPluginMappings(instances);
18
16
  const statements = [...new Set(instances.flatMap(options => authSchemaSql(options, dialect)))];
19
- // CREATE TABLE IF NOT EXISTS would silently keep the first of two different definitions.
20
- // Identical statements are already merged, so a repeated table name means two different definitions.
21
17
  const tables = statements.map(statement => /^CREATE TABLE IF NOT EXISTS ("(?:[^"]|"")+")/.exec(statement)?.[1]).filter(Boolean);
22
18
  const twice = tables.find((table, i) => tables.indexOf(table) !== i);
23
19
  if (twice)
24
20
  throw new Error(`Better Auth instances define table ${twice} differently; give each instance its own table names (user.modelName, session.modelName, ...).`);
25
21
  return statements;
26
22
  }
27
- // Plugins whose factory hands back one module-level schema object (mergeSchema writes `schema` options into it); no options leaves it untouched.
28
23
  const sharedSchema = {
29
24
  jwt: () => jwt().schema, admin: () => admin().schema, anonymous: () => anonymous().schema,
30
25
  "phone-number": () => phoneNumber({}).schema, siwe: () => siwe({}).schema, "two-factor": () => twoFactor().schema,
31
26
  };
32
- // Plugins that share field objects (device-authorization) or copy per call (oauth-provider, listed on request) are only checked for equal resolved names.
33
27
  const checkedPlugins = new Set([...Object.keys(sharedSchema), "device-authorization", "oauth-provider"]);
34
- /** Names a plugin's tables and fields resolve to, keyed `table` / `table.field`; only mappings that differ from the key are listed. */
35
28
  function mapped(schema) {
36
29
  const names = new Map();
37
30
  for (const [table, model] of Object.entries(schema)) {
@@ -45,7 +38,6 @@ function mapped(schema) {
45
38
  }
46
39
  return names;
47
40
  }
48
- /** Better Auth plugin factories share their schema object, so differing plugin field/model mappings across instances in one Worker override each other. */
49
41
  function refuseSharedPluginMappings(instances) {
50
42
  const byPlugin = new Map();
51
43
  instances.forEach((options, index) => (options.plugins ?? []).forEach(plugin => {
@@ -1,9 +1,7 @@
1
- /** `mantle help [topic]`: a projection of the installed docs/handbook/navigation.json; the docs stay the only source. */
2
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
3
2
  import { basename, dirname, relative, resolve } from "node:path";
4
3
  import { fileURLToPath } from "node:url";
5
4
  export const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
6
- /** Packed: <package>/docs. Workspace: the repository's docs/, which prepack copies in. */
7
5
  export function docsRoot() {
8
6
  const root = [resolve(packageRoot, "docs"), resolve(packageRoot, "../../docs")].find(path => existsSync(resolve(path, "handbook/navigation.json")));
9
7
  if (!root)
@@ -13,7 +11,6 @@ export function docsRoot() {
13
11
  export function topics(root = docsRoot()) {
14
12
  const nav = JSON.parse(readFileSync(resolve(root, "handbook/navigation.json"), "utf8"));
15
13
  const pages = nav.groups.flatMap(group => group.items.map(item => ({ name: item.link.slice(1), title: item.text, file: resolve(root, `handbook${item.link}.md`), group: group.text })));
16
- // Pages the sidebar leaves out (acceptance, release notes) are still topics.
17
14
  const listed = new Set(pages.map(page => page.file));
18
15
  const unlisted = readdirSync(resolve(root, "handbook"), { recursive: true }).filter(path => path.endsWith(".md")).sort()
19
16
  .map(path => resolve(root, "handbook", path)).filter(file => !listed.has(file))
@@ -25,7 +22,6 @@ export function topics(root = docsRoot()) {
25
22
  return [...pages, ...unlisted, ...skills, { name: "examples", title: "Examples and how to combine them", file: resolve(examples, "README.md"), group: "Examples" },
26
23
  ...shipped.map(entry => ({ name: `examples/${entry.name}`, title: `Example: ${entry.name}`, file: resolve(examples, entry.name, "README.md"), group: "Examples" }))];
27
24
  }
28
- /** A full topic name, or its last segment when only one topic ends with it. */
29
25
  function find(all, name) {
30
26
  const exact = all.find(topic => topic.name === name);
31
27
  if (exact)
@@ -41,7 +37,6 @@ export function help(name, root = docsRoot()) {
41
37
  throw new Error(`No help topic ${JSON.stringify(name)}.${near.length ? `\nDid you mean:\n${near.join("\n")}` : ""}\nRun mantle help topics for the list.`);
42
38
  }
43
39
  const text = readFileSync(topic.file, "utf8").replace(/^---\n[\s\S]*?\n---\n/, "").trim();
44
- // Links to other topics become commands, so an agent can follow them without knowing the docs layout.
45
40
  const linked = [...new Set([...text.matchAll(/\]\(([^)#\s]+\.md)(?:#[^)]*)?\)/g)].map(match => resolve(dirname(topic.file), match[1])))]
46
41
  .flatMap(file => all.filter(t => t.file === file && t !== topic));
47
42
  const index = all.indexOf(topic), next = all[index + 1]?.group === topic.group ? all[index + 1] : undefined;
@@ -1,8 +1,6 @@
1
- /** `mantle init`: copy the installed app shell once. The app owns every file afterwards; init never overwrites one. */
2
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
3
2
  import { dirname, join, relative, resolve } from "node:path";
4
3
  import { docsRoot, packageRoot } from "./help.js";
5
- // Local state a docs checkout may hold after someone ran the shell in place; sync-package-docs excludes the same names.
6
4
  const LOCAL = new Set(["node_modules", ".wrangler", ".dev.vars", "pnpm-lock.yaml", "generated", "dist"]);
7
5
  function files(directory) {
8
6
  return readdirSync(directory, { withFileTypes: true }).flatMap(entry => LOCAL.has(entry.name) ? []
@@ -12,7 +10,6 @@ export function init(destination, dryRun = false) {
12
10
  const shell = resolve(docsRoot(), "examples/app-shell");
13
11
  const sdk = JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf8"));
14
12
  const target = resolve(destination);
15
- // A packed shell carries `gitignore` (npm drops .gitignore); a workspace checkout has .gitignore itself.
16
13
  const all = files(shell).map(file => file === "gitignore" ? ".gitignore" : file);
17
14
  const source = (file) => file === ".gitignore" && !existsSync(join(shell, file)) ? join(shell, "gitignore") : join(shell, file);
18
15
  const existing = all.filter(file => existsSync(join(target, file)));
@@ -21,7 +18,6 @@ export function init(destination, dryRun = false) {
21
18
  const notes = [];
22
19
  for (const file of all) {
23
20
  let text = readFileSync(source(file), "utf8");
24
- // Pin the shell's Mantle packages to the installed SDK.
25
21
  if (file === "package.json") {
26
22
  const pkg = JSON.parse(text);
27
23
  for (const name of Object.keys(pkg.dependencies))
@@ -1,23 +1,17 @@
1
- /** `mantle generate` fingerprints its inputs next to the output; `mantle bundle` refuses output whose inputs changed since. */
2
1
  import { createHash } from "node:crypto";
3
2
  import { existsSync, readFileSync } from "node:fs";
4
3
  import { dirname, relative, resolve } from "node:path";
5
4
  export const INPUTS_FILE = ".mantle-inputs.json";
6
- /** The installed `@aotter/mantle` version (dist/infrastructure → package root). */
7
5
  const MANTLE_VERSION = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
8
- // Line endings are normalized so a CRLF checkout (core.autocrlf) matches the LF fingerprint.
9
6
  const digest = (path) => existsSync(path) ? createHash("sha256").update(readFileSync(path, "utf8").replaceAll("\r\n", "\n")).digest("hex") : null;
10
- /** The target and the manifests and option modules (`--auth`, `--site`) one generate run read, by path relative to its output. */
11
7
  export function inputsFingerprint(destination, paths, target) {
12
8
  const inputs = Object.fromEntries(paths.map(path => [relative(resolve(destination), resolve(path)).replaceAll("\\", "/"), digest(path)]));
13
9
  return JSON.stringify({ mantle: MANTLE_VERSION, target, inputs }, null, 2) + "\n";
14
10
  }
15
- /** The generate target recorded beside `generatedModule` (undefined without a fingerprint, or one written before targets were recorded). */
16
11
  export function generatedTarget(generatedModule) {
17
12
  const file = resolve(dirname(generatedModule), INPUTS_FILE);
18
13
  return existsSync(file) ? JSON.parse(readFileSync(file, "utf8")).target : undefined;
19
14
  }
20
- /** Why the generated output that holds `generatedModule` no longer matches its inputs (empty when it does, or when it has no fingerprint). */
21
15
  export function staleInputs(generatedModule) {
22
16
  const directory = dirname(generatedModule), file = resolve(directory, INPUTS_FILE);
23
17
  if (!existsSync(file))
@@ -29,13 +23,10 @@ export function staleInputs(generatedModule) {
29
23
  stale.push(`${relative(process.cwd(), resolve(directory, path))} changed`);
30
24
  return stale;
31
25
  }
32
- /** Better Auth roles an app declares beyond `admin` and `user` (`<name>: ac.newRole(...)`), for a one-line design note. */
33
26
  export function extraRoles(source) {
34
27
  return [...source.matchAll(/["']?([A-Za-z][\w-]*)["']?\s*:\s*ac\.newRole\(/g)].map(m => m[1]).filter(role => role !== "admin" && role !== "user");
35
28
  }
36
- /** Better Auth tables an app's own code queries with SQL (`FROM "user"`, `JOIN session` ...), for a one-line note. */
37
29
  export function authTableSql(source) {
38
- // Only lines that read like SQL statements, so prose such as "take into account" is not a finding.
39
30
  const sql = source.split("\n").filter(line => /\b(?:SELECT|INSERT|UPDATE|DELETE)\b/.test(line)).join("\n");
40
31
  return [...new Set([...sql.matchAll(/\b(?:FROM|JOIN|UPDATE|INTO)\s+"?(user|session|account|verification)"?(?=[\s;),`'"]|$)/gi)].map(m => m[1].toLowerCase()))];
41
32
  }
@@ -1,4 +1,3 @@
1
- /** Warn when a surface the manifests select is generated but the app entry never imports it, so it is never served. */
2
1
  import { existsSync } from "node:fs";
3
2
  import { relative, resolve } from "node:path";
4
3
  import { ownModules } from "./bundle.js";
@@ -7,7 +6,6 @@ const SURFACES = [
7
6
  { module: "mcp.ts", selected: files => "mcp.ts" in files, what: "MCP tools", topic: "service-entry" },
8
7
  { module: "admin-bindings.ts", selected: files => "admin-bindings.ts" in files, what: "Admin bindings", topic: "native-admin" },
9
8
  ];
10
- /** Follows relative static and dynamic imports from the entry; packages stay external, so nothing needs to be installed. */
11
9
  export async function unmountedWarnings(entry, destination, files) {
12
10
  const selected = SURFACES.filter(surface => surface.selected(files));
13
11
  if (!selected.length || !existsSync(entry))
@@ -17,7 +15,6 @@ export async function unmountedWarnings(entry, destination, files) {
17
15
  inputs = Object.keys((await ownModules(entry)).inputs).map(path => resolve(path));
18
16
  }
19
17
  catch (error) {
20
- // This probe has none of mantle bundle's loaders or conditions, so a failure here says nothing about the entry.
21
18
  const first = error.errors?.[0]?.text ?? String(error);
22
19
  return [`skipped checking which generated surfaces ${entry} serves: could not follow its imports (${first}).`];
23
20
  }
@@ -4,7 +4,6 @@ import { basename, dirname, join, relative, resolve } from "node:path";
4
4
  import { isDeepStrictEqual } from "node:util";
5
5
  import { applyEdits, modify, parse } from "jsonc-parser";
6
6
  const requiredFlags = ["nodejs_compat", "global_fetch_strictly_public"];
7
- /** Newest date the project's installed workerd supports, as create-cloudflare picks it; a fixed date before Wrangler is installed. */
8
7
  function compatibilityDate(dir) {
9
8
  try {
10
9
  let from = join(dir, "package.json");
@@ -17,7 +16,6 @@ function compatibilityDate(dir) {
17
16
  catch { }
18
17
  return "2026-09-28";
19
18
  }
20
- /** `assets`: with `--admin`, the static assets directory `mantle bundle` builds Admin into (`<assets>/admin`). */
21
19
  export function configureWrangler(configPath, entry, outdir, generated, check = false, assets, publicFetchRequired = false) {
22
20
  const configDir = dirname(resolve(configPath));
23
21
  const local = (path) => relative(configDir, resolve(path)).replaceAll("\\", "/") || ".";
@@ -49,13 +47,11 @@ export function configureWrangler(configPath, entry, outdir, generated, check =
49
47
  throw new Error(`${configPath} must be a valid JSONC object.`);
50
48
  const indent = text.match(/\n([ \t]+)"/)?.[1] ?? " ";
51
49
  const formattingOptions = { insertSpaces: !indent.includes("\t"), tabSize: indent.length, eol: text.includes("\r\n") ? "\r\n" : "\n" };
52
- // Merge, never replace: the user's own module rules and build options survive.
53
50
  const { rules: [rule], build: buildKeys, ...top } = keys;
54
51
  const edit = (path, value) => {
55
52
  if (!isDeepStrictEqual(path.reduce((v, k) => v?.[k], config), value))
56
53
  text = applyEdits(text, modify(text, path, value, { formattingOptions }));
57
54
  };
58
- // Public routing is the default; an explicit private origin remains app-owned without OAuth/CIMD.
59
55
  const ensureFlags = (path, flags) => {
60
56
  if (flags !== undefined && (!Array.isArray(flags) || !flags.every(flag => typeof flag === "string")))
61
57
  throw new Error(`${configPath}: ${path.join(".")} must be an array of strings.`);
@@ -71,7 +67,6 @@ export function configureWrangler(configPath, entry, outdir, generated, check =
71
67
  edit([...path, -1], flag);
72
68
  };
73
69
  ensureFlags(["compatibility_flags"], config.compatibility_flags);
74
- // Wrangler environment arrays replace the top-level flags rather than inheriting them.
75
70
  for (const [name, environment] of Object.entries(config.env ?? {})) {
76
71
  if (environment?.compatibility_flags !== undefined)
77
72
  ensureFlags(["env", name, "compatibility_flags"], environment.compatibility_flags);
@@ -88,7 +83,6 @@ export function configureWrangler(configPath, entry, outdir, generated, check =
88
83
  edit([...path, "binding"], "ASSETS");
89
84
  if (ownsDefaults && current?.run_worker_first === undefined)
90
85
  edit([...path, "run_worker_first"], true);
91
- // The shell fetches index.html itself; asset canonicalization would redirect it back to /admin/ forever.
92
86
  if (current?.html_handling === undefined && (current?.run_worker_first === true || (ownsDefaults && current?.run_worker_first === undefined)))
93
87
  edit([...path, "html_handling"], "none");
94
88
  };
@@ -1,5 +1,4 @@
1
1
  import { ContractError } from "./contracts.js";
2
- /** Rejections deny; synchronous application failures remain internal errors. */
3
2
  export async function authorized(authorize, target, caller) {
4
3
  let result;
5
4
  try {
@@ -1,12 +1,4 @@
1
- /**
2
- * Core's bind recipes (`BindSpec` in the policy rewriter), evaluated for one call. Generated Views, generated
3
- * Procedures and the Store all bind through this one function. Input recipes carry the authored field name; `version` reads the Store's lock from `input`.
4
- */
5
1
  import { deny, isAdmin } from "./contracts.js";
6
- /**
7
- * A declared date-time or date as the engine stores it, and a JSON value as its text (a string, such as a boundary's
8
- * already converted value, is bound as it is); any other value (and `null`) is bound as it is.
9
- */
10
2
  export const encode = (d, type, v) => type === "timestamptz" ? (v == null ? null : d.timestamp(v)) : type === "date" ? (v == null ? null : d.date(v))
11
3
  : type === "json" && v != null && typeof v !== "string" ? JSON.stringify(v) : v;
12
4
  export function binds(d, recipes, caller, input, ms = 0) {
@@ -36,14 +28,12 @@ export function binds(d, recipes, caller, input, ms = 0) {
36
28
  case "input":
37
29
  out.push(encode(d, r.type, input[r.name]));
38
30
  break;
39
- // Only apps whose plan uses caller values import the version-checked capability; it refuses an unknown recipe, and without it this throws before SQL.
40
31
  default: out.push(d.userRefs(r, caller, input));
41
32
  }
42
33
  }
43
34
  return out;
44
35
  }
45
- /** Every step's binds, evaluated for a Procedure. */
46
36
  export function bound(d, steps, caller, input) {
47
- const ms = Date.now(); // one clock for the whole Procedure
37
+ const ms = Date.now();
48
38
  return steps.map(s => ({ sql: s.sql, binds: binds(d, s.binds, caller, input, ms), row: s.row }));
49
39
  }
@@ -18,7 +18,4 @@ export declare const isAdminCaller: (caller: Caller) => caller is Extract<Caller
18
18
  }> & {
19
19
  readonly admin: true;
20
20
  };
21
- export declare function modeOf(caller: Caller): "caller" | "trusted";
22
- /** Only branded host identities may carry freshness authority. */
23
- export declare function checkFresh(caller: Caller, readOnly: boolean, target?: string): void;
24
21
  export {};
@@ -1,22 +1,7 @@
1
- import { ContractError } from "./contract-error.js";
2
1
  export const identities = /* @__PURE__ */ new WeakMap();
3
2
  export const isHostCaller = (caller) => identities.get(caller)?.host === true;
4
3
  export const rolesOf = (caller) => identities.get(caller)?.roles ?? [];
5
- // Unbranded callers get a fresh empty map; mutating it cannot grant authority.
6
4
  export const fieldsOf = (caller) => identities.get(caller)?.fields ?? {};
7
5
  export const valueFor = (caller, field) => identities.get(caller)?.fields[field];
8
6
  export const hasRole = (caller, role) => rolesOf(caller).includes(role);
9
7
  export const isAdminCaller = (caller) => !!identities.get(caller)?.admin;
10
- export function modeOf(caller) {
11
- const kind = caller.kind;
12
- if (kind === 'system')
13
- return 'trusted';
14
- if (kind === 'user' || kind === 'anonymous' || kind === 'host' && isHostCaller(caller))
15
- return 'caller';
16
- throw new ContractError('AUTH_DENIED', 'Unknown caller');
17
- }
18
- /** Only branded host identities may carry freshness authority. */
19
- export function checkFresh(caller, readOnly, target) {
20
- if (!readOnly && isHostCaller(caller) && "authenticatedAs" in caller && caller.authenticatedAs === "snapshot")
21
- throw new ContractError("STALE_CALLER", "Snapshot callers cannot write.", target, { status: 403 });
22
- }
@@ -0,0 +1,4 @@
1
+ import type { Caller } from "./contracts.js";
2
+ export declare function modeOf(caller: Caller): "caller" | "trusted";
3
+ /** Only branded host identities may carry freshness authority. */
4
+ export declare function checkFresh(caller: Caller, readOnly: boolean, target?: string): void;
@@ -0,0 +1,14 @@
1
+ import { ContractError } from "./contract-error.js";
2
+ import { isHostCaller } from "./caller-identity.js";
3
+ export function modeOf(caller) {
4
+ const kind = caller.kind;
5
+ if (kind === 'system')
6
+ return 'trusted';
7
+ if (kind === 'user' || kind === 'anonymous' || kind === 'host' && isHostCaller(caller))
8
+ return 'caller';
9
+ throw new ContractError('AUTH_DENIED', 'Unknown caller');
10
+ }
11
+ export function checkFresh(caller, readOnly, target) {
12
+ if (!readOnly && isHostCaller(caller) && "authenticatedAs" in caller && caller.authenticatedAs === "snapshot")
13
+ throw new ContractError("STALE_CALLER", "Snapshot callers cannot write.", target, { status: 403 });
14
+ }
@@ -132,12 +132,4 @@ export interface Postgres {
132
132
  }
133
133
  /** What a read or a single statement needs: a transaction handle suits it too. */
134
134
  export type Unsafe = Pick<Postgres, "unsafe">;
135
- /** A cookie is ambient authority; bearer credentials deliberately supplied by the app are not. */
136
- export declare function checkOrigin(request: Request, caller: Caller): void;
137
- /** A resolver's ContractError (e.g. UNAUTHENTICATED for an expired token) keeps its status; anything else is an application failure. */
138
- export declare function surfaceCaller<Env>(request: Request, env: Env, resolve: (request: Request, env: Env, authority: {
139
- readonly readOnly: boolean;
140
- }) => Caller | Promise<Caller>, readOnly?: boolean): Promise<Caller>;
141
- /** The client sees the redacted wire form; the log gets the surface (a route or tool, never headers) and the original message, stack and cause. */
142
- export declare function surfaceError(error: unknown, where?: string): ReturnType<typeof wire>;
143
- export declare function surfaceFailure(error: unknown, challenge?: string, where?: string): Response;
135
+ export { checkOrigin, surfaceCaller, surfaceError, surfaceFailure } from "./surface-failure.js";