@aotter/mantle 0.2.0-alpha.8 → 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/README.md +26 -3
- package/dist/auth/adapter.js +10 -34
- package/dist/auth/admin.js +2 -5
- package/dist/auth/caller.js +0 -27
- package/dist/auth/consent.js +100 -0
- package/dist/auth/hardening.js +0 -7
- package/dist/auth/identity.js +0 -11
- package/dist/auth/oauth.js +0 -32
- package/dist/auth/resource-seeds.js +0 -2
- package/dist/auth/route-http.js +33 -0
- package/dist/auth/routes.js +8 -153
- package/dist/build/compiler.d.ts +19 -609
- package/dist/build/compiler.js +3 -6896
- package/dist/build/spec/domain/model/SqlIr.d.ts +2 -0
- package/dist/build/spec/domain/model/UserFields.d.ts +1 -0
- package/dist/bun/assets.js +0 -2
- package/dist/domain/adminOrder.js +0 -2
- package/dist/domain/columns.js +7 -20
- package/dist/domain/describe/chunk-SGMRVDGE.js +1 -0
- package/dist/domain/describe/index.js +1 -115
- package/dist/domain/describe/target-hash.js +1 -12
- package/dist/domain/entry.js +0 -14
- package/dist/domain/generated-tools.js +0 -1
- package/dist/domain/input.js +0 -2
- package/dist/domain/paging.js +0 -17
- package/dist/domain/recipes.js +10 -7
- package/dist/domain/userReferences.js +0 -1
- package/dist/domain/wireScalars.js +0 -6
- package/dist/infrastructure/adminEntries.js +10 -7
- package/dist/infrastructure/adminPaging.js +55 -57
- package/dist/infrastructure/artifacts.js +0 -4
- package/dist/infrastructure/authSchema.js +0 -9
- package/dist/infrastructure/bundle.js +17 -32
- package/dist/infrastructure/cli.js +25 -11
- package/dist/infrastructure/generate.js +18 -17
- package/dist/infrastructure/generateAuth.js +48 -5
- package/dist/infrastructure/help.js +55 -0
- package/dist/infrastructure/init.js +50 -0
- package/dist/infrastructure/inputs.js +0 -9
- package/dist/infrastructure/mounts.js +22 -0
- package/dist/infrastructure/wrangler.js +18 -9
- package/dist/kernel/authorization.js +0 -1
- package/dist/kernel/binds.d.ts +16 -7
- package/dist/kernel/binds.js +4 -10
- package/dist/kernel/caller-identity.d.ts +0 -3
- package/dist/kernel/caller-identity.js +0 -15
- package/dist/kernel/caller-mode.d.ts +4 -0
- package/dist/kernel/caller-mode.js +14 -0
- package/dist/kernel/contracts.d.ts +7 -11
- package/dist/kernel/contracts.js +8 -44
- package/dist/kernel/cron.js +0 -4
- package/dist/kernel/dialect/common.d.ts +0 -1
- package/dist/kernel/dialect/common.js +0 -3
- package/dist/kernel/dialect/postgres.js +0 -2
- package/dist/kernel/dialect/sqlite.js +1 -6
- package/dist/kernel/drain.js +2 -8
- package/dist/kernel/driver/bun-sqlite.js +0 -4
- package/dist/kernel/driver/d1.js +0 -1
- package/dist/kernel/driver/postgres.js +3 -17
- package/dist/kernel/driver/transaction-context.js +0 -1
- package/dist/kernel/email.js +0 -4
- package/dist/kernel/host-access.js +2 -3
- package/dist/kernel/host-caller.d.ts +1 -1
- package/dist/kernel/host-caller.js +3 -6
- package/dist/kernel/json-body.js +0 -2
- package/dist/kernel/mcp-apps.js +1 -6
- package/dist/kernel/mcp-call.js +0 -1
- package/dist/kernel/mcp-handler.js +3 -27
- package/dist/kernel/mcp.d.ts +5 -1
- package/dist/kernel/mcp.js +2 -6
- package/dist/kernel/media.js +0 -18
- package/dist/kernel/paging.js +1 -9
- package/dist/kernel/publishing.js +3 -4
- package/dist/kernel/r2-media.js +0 -7
- package/dist/kernel/rest-dispatch.js +0 -1
- package/dist/kernel/rest-router.js +2 -3
- package/dist/kernel/rest.js +0 -3
- package/dist/kernel/scalars.js +4 -11
- package/dist/kernel/search.js +0 -1
- package/dist/kernel/site.js +0 -2
- package/dist/kernel/store.d.ts +11 -1
- package/dist/kernel/store.js +21 -34
- package/dist/kernel/surface-failure.d.ts +10 -0
- package/dist/kernel/surface-failure.js +29 -0
- package/dist/kernel/surface.js +0 -3
- package/dist/kernel/sweep.js +0 -1
- package/dist/kernel/translations.js +0 -1
- package/dist/kernel/user-caller.js +2 -8
- package/dist/kernel/user-fields.d.ts +3 -2
- package/dist/kernel/user-fields.js +9 -9
- package/dist/kernel/validation.js +0 -8
- package/dist/kernel/web-gateway.js +0 -2
- package/dist/kernel/web-head.js +0 -1
- package/dist/kernel/web-index.js +39 -0
- package/dist/kernel/web-response.js +0 -1
- package/dist/kernel/web-serve.js +97 -0
- package/dist/kernel/web.d.ts +1 -1
- package/dist/kernel/web.js +10 -125
- package/dist/kernel/where.js +0 -6
- package/dist/usecase/adminBindings.js +30 -21
- package/dist/usecase/adminClient.js +0 -21
- package/dist/usecase/adminDescriptors.js +2 -9
- package/dist/usecase/generate.js +7 -12
- package/dist/usecase/handlers.js +0 -3
- package/dist/usecase/mcp-media.js +0 -1
- package/dist/usecase/mcp.js +0 -4
- package/dist/usecase/paging.js +1 -3
- package/dist/usecase/publishing.js +1 -5
- package/dist/usecase/rest.js +0 -1
- package/dist/usecase/site.js +0 -2
- package/dist/usecase/stores.js +4 -14
- package/dist/usecase/tool-version.js +0 -3
- package/dist/usecase/translations.js +0 -2
- package/dist/usecase/validation.js +8 -20
- package/docs/examples/README.md +13 -5
- package/docs/examples/app-shell/README.md +43 -8
- package/docs/examples/app-shell/accept.mjs +91 -0
- package/docs/examples/app-shell/gitignore +7 -0
- package/docs/examples/app-shell/package.json +7 -6
- package/docs/examples/app-shell/smoke-kit.mjs +29 -15
- package/docs/examples/app-shell/src/auth.ts +3 -2
- package/docs/examples/app-shell/src/routes.ts +6 -3
- package/docs/examples/app-shell/src/service.ts +4 -3
- package/docs/examples/club-events/README.md +7 -2
- package/docs/examples/club-events/manifests/club-events.yaml +22 -0
- package/docs/examples/club-events/smoke.mjs +7 -1
- package/docs/examples/club-events/src/handlers.ts +1 -1
- package/docs/examples/purchasing/README.md +12 -3
- package/docs/examples/purchasing/manifests/purchasing.yaml +36 -3
- package/docs/examples/purchasing/smoke.mjs +14 -0
- package/docs/examples/purchasing/src/handlers.ts +1 -1
- package/docs/examples/recipes/README.md +5 -3
- package/docs/handbook/cloudflare/authentication.md +21 -4
- package/docs/handbook/cloudflare/chatgpt-sites.md +4 -2
- package/docs/handbook/cloudflare/deploy-and-operate.md +24 -1
- package/docs/handbook/cloudflare/media-r2.md +2 -3
- package/docs/handbook/cloudflare/service-entry.md +17 -4
- package/docs/handbook/concepts/runtime-and-adapters.md +1 -1
- package/docs/handbook/examples/hub.md +1 -1
- package/docs/handbook/guides/agent-setup.md +15 -10
- package/docs/handbook/guides/agent-workflow-acceptance.md +232 -0
- package/docs/handbook/guides/local-testing.md +40 -0
- package/docs/handbook/guides/mcp-apps.md +78 -1
- package/docs/handbook/guides/native-admin.md +29 -6
- package/docs/handbook/guides/native-execution.md +5 -4
- package/docs/handbook/guides/oauth.md +58 -0
- package/docs/handbook/guides/public-pages.md +44 -4
- package/docs/handbook/guides/sql-patterns.md +266 -0
- package/docs/handbook/guides/typed-queries.md +173 -0
- package/docs/handbook/navigation.json +16 -0
- package/docs/handbook/reference/admin-descriptors.md +4 -1
- package/docs/handbook/reference/authorization.md +1 -1
- package/docs/handbook/reference/diagnostics.md +6 -1
- package/docs/handbook/reference/procedure.md +5 -5
- package/docs/handbook/reference/schema.md +10 -4
- package/docs/handbook/reference/sql.md +64 -0
- package/docs/handbook/reference/surface.md +11 -4
- package/docs/handbook/reference/trigger.md +9 -0
- package/docs/handbook/reference/view.md +11 -3
- package/docs/handbook/releases/index.md +27 -6
- package/docs/handbook/start/agents.md +45 -0
- package/docs/handbook/start/project-and-cli.md +15 -1
- package/docs/handbook/start/quickstart-worker.md +1 -1
- package/docs/skills/develop/SKILL.md +44 -16
- package/docs/skills/integrate/SKILL.md +4 -9
- package/docs/skills/provision/SKILL.md +15 -12
- package/docs/skills/update/SKILL.md +21 -18
- package/package.json +24 -4
- package/skills/README.md +1 -1
- package/skills/mantle/SKILL.md +48 -107
- package/dist/domain/describe/chunk-ZVXBGP7Y.js +0 -424
- package/docs/skills/develop/mcp-app.md +0 -39
- package/skills/.keep +0 -0
- package/skills/mantle/references/local-testing.md +0 -15
- package/skills/mantle/references/web.md +0 -41
- package/skills/mantle/references/website.md +0 -49
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, relative, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
export const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
5
|
+
export function docsRoot() {
|
|
6
|
+
const root = [resolve(packageRoot, "docs"), resolve(packageRoot, "../../docs")].find(path => existsSync(resolve(path, "handbook/navigation.json")));
|
|
7
|
+
if (!root)
|
|
8
|
+
throw new Error(`Mantle docs are missing from ${packageRoot}; reinstall @aotter/mantle.`);
|
|
9
|
+
return root;
|
|
10
|
+
}
|
|
11
|
+
export function topics(root = docsRoot()) {
|
|
12
|
+
const nav = JSON.parse(readFileSync(resolve(root, "handbook/navigation.json"), "utf8"));
|
|
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 })));
|
|
14
|
+
const listed = new Set(pages.map(page => page.file));
|
|
15
|
+
const unlisted = readdirSync(resolve(root, "handbook"), { recursive: true }).filter(path => path.endsWith(".md")).sort()
|
|
16
|
+
.map(path => resolve(root, "handbook", path)).filter(file => !listed.has(file))
|
|
17
|
+
.map(file => ({ name: relative(resolve(root, "handbook"), file).replaceAll("\\", "/").slice(0, -3), title: readFileSync(file, "utf8").match(/^# (.+)$/m)?.[1] ?? basename(file), file, group: "More pages" }));
|
|
18
|
+
const skills = (existsSync(resolve(root, "skills")) ? readdirSync(resolve(root, "skills")) : []).filter(name => existsSync(resolve(root, "skills", name, "SKILL.md")))
|
|
19
|
+
.map(name => ({ name: `skills/${name}`, title: `Package skill: ${name}`, file: resolve(root, "skills", name, "SKILL.md"), group: "Package skills" }));
|
|
20
|
+
const examples = resolve(root, "examples");
|
|
21
|
+
const shipped = readdirSync(examples, { withFileTypes: true }).filter(entry => entry.isDirectory() && existsSync(resolve(examples, entry.name, "README.md")));
|
|
22
|
+
return [...pages, ...unlisted, ...skills, { name: "examples", title: "Examples and how to combine them", file: resolve(examples, "README.md"), group: "Examples" },
|
|
23
|
+
...shipped.map(entry => ({ name: `examples/${entry.name}`, title: `Example: ${entry.name}`, file: resolve(examples, entry.name, "README.md"), group: "Examples" }))];
|
|
24
|
+
}
|
|
25
|
+
function find(all, name) {
|
|
26
|
+
const exact = all.find(topic => topic.name === name);
|
|
27
|
+
if (exact)
|
|
28
|
+
return exact;
|
|
29
|
+
const tail = all.filter(topic => basename(topic.name) === name);
|
|
30
|
+
return tail.length === 1 ? tail[0] : undefined;
|
|
31
|
+
}
|
|
32
|
+
export function help(name, root = docsRoot()) {
|
|
33
|
+
const all = topics(root);
|
|
34
|
+
const topic = find(all, name ?? "start/agents");
|
|
35
|
+
if (!topic) {
|
|
36
|
+
const near = all.filter(t => name && t.name.includes(name)).map(t => ` mantle help ${t.name}`);
|
|
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.`);
|
|
38
|
+
}
|
|
39
|
+
const text = readFileSync(topic.file, "utf8").replace(/^---\n[\s\S]*?\n---\n/, "").trim();
|
|
40
|
+
const linked = [...new Set([...text.matchAll(/\]\(([^)#\s]+\.md)(?:#[^)]*)?\)/g)].map(match => resolve(dirname(topic.file), match[1])))]
|
|
41
|
+
.flatMap(file => all.filter(t => t.file === file && t !== topic));
|
|
42
|
+
const index = all.indexOf(topic), next = all[index + 1]?.group === topic.group ? all[index + 1] : undefined;
|
|
43
|
+
const lines = [text, "", `Source: ${relative(process.cwd(), topic.file) || topic.file}`];
|
|
44
|
+
if (next)
|
|
45
|
+
lines.push(`Next: mantle help ${next.name} (${next.title})`);
|
|
46
|
+
if (linked.length)
|
|
47
|
+
lines.push("Linked:", ...linked.map(t => ` mantle help ${t.name} (${t.title})`));
|
|
48
|
+
if (!name)
|
|
49
|
+
lines.push("", topicList(all));
|
|
50
|
+
return lines.join("\n");
|
|
51
|
+
}
|
|
52
|
+
export function topicList(all = topics()) {
|
|
53
|
+
const groups = [...new Set(all.map(topic => topic.group))];
|
|
54
|
+
return ["Topics:", ...groups.flatMap(group => [` ${group}`, ...all.filter(t => t.group === group).map(t => ` ${t.name.padEnd(34)} ${t.title}`)])].join("\n");
|
|
55
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { docsRoot, packageRoot } from "./help.js";
|
|
4
|
+
const LOCAL = new Set(["node_modules", ".wrangler", ".dev.vars", "pnpm-lock.yaml", "generated", "dist"]);
|
|
5
|
+
function files(directory) {
|
|
6
|
+
return readdirSync(directory, { withFileTypes: true }).flatMap(entry => LOCAL.has(entry.name) ? []
|
|
7
|
+
: entry.isDirectory() ? files(join(directory, entry.name)).map(file => join(entry.name, file)) : [entry.name]);
|
|
8
|
+
}
|
|
9
|
+
export function init(destination, dryRun = false) {
|
|
10
|
+
const shell = resolve(docsRoot(), "examples/app-shell");
|
|
11
|
+
const sdk = JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf8"));
|
|
12
|
+
const target = resolve(destination);
|
|
13
|
+
const all = files(shell).map(file => file === "gitignore" ? ".gitignore" : file);
|
|
14
|
+
const source = (file) => file === ".gitignore" && !existsSync(join(shell, file)) ? join(shell, "gitignore") : join(shell, file);
|
|
15
|
+
const existing = all.filter(file => existsSync(join(target, file)));
|
|
16
|
+
if (existing.length)
|
|
17
|
+
throw new Error(`mantle init writes the app shell once and never overwrites. These files already exist in ${target}:\n${existing.map(f => ` ${f}`).join("\n")}\nMove them aside or run mantle init in an empty directory; to use generated code inside an existing app, skip init and run mantle help typed-queries.`);
|
|
18
|
+
const notes = [];
|
|
19
|
+
for (const file of all) {
|
|
20
|
+
let text = readFileSync(source(file), "utf8");
|
|
21
|
+
if (file === "package.json") {
|
|
22
|
+
const pkg = JSON.parse(text);
|
|
23
|
+
for (const name of Object.keys(pkg.dependencies))
|
|
24
|
+
if (name.startsWith("@aotter/mantle"))
|
|
25
|
+
pkg.dependencies[name] = sdk.version;
|
|
26
|
+
text = JSON.stringify(pkg, null, 2) + "\n";
|
|
27
|
+
}
|
|
28
|
+
if (!file.endsWith(".md"))
|
|
29
|
+
text.split("\n").forEach((line, index) => { if (line.includes("AGENT:"))
|
|
30
|
+
notes.push(` ${file}:${index + 1}: ${line.slice(line.indexOf("AGENT:") + 6).trim()}`); });
|
|
31
|
+
if (dryRun)
|
|
32
|
+
continue;
|
|
33
|
+
mkdirSync(dirname(join(target, file)), { recursive: true });
|
|
34
|
+
writeFileSync(join(target, file), text, { flag: "wx" });
|
|
35
|
+
}
|
|
36
|
+
if (!dryRun)
|
|
37
|
+
mkdirSync(join(target, "manifests"), { recursive: true });
|
|
38
|
+
const where = relative(process.cwd(), target) || ".";
|
|
39
|
+
return [
|
|
40
|
+
`${dryRun ? "Would write" : "Wrote"} the Mantle ${sdk.version} app shell to ${where}:`,
|
|
41
|
+
...all.map(file => ` ${file}`), " manifests/",
|
|
42
|
+
"",
|
|
43
|
+
"AGENT: the app must fill in these before it is done (each is marked in the file):",
|
|
44
|
+
" manifests/*.yaml: the app's Schemas, Views, Procedures and Triggers (mantle help four-atoms)",
|
|
45
|
+
...notes,
|
|
46
|
+
"",
|
|
47
|
+
`Next: cd ${where} && pnpm install; then follow README.md. mantle help explains each step;`,
|
|
48
|
+
"mantle help examples shows the patterns to take for each need. Accept with mantle help agent-workflow-acceptance before reporting done.",
|
|
49
|
+
].join("\n");
|
|
50
|
+
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { relative, resolve } from "node:path";
|
|
3
|
+
import { ownModules } from "./bundle.js";
|
|
4
|
+
const SURFACES = [
|
|
5
|
+
{ module: "rest.ts", selected: files => files["rest.ts"]?.includes("pattern: new RegExp") ?? false, what: "REST routes", topic: "service-entry" },
|
|
6
|
+
{ module: "mcp.ts", selected: files => "mcp.ts" in files, what: "MCP tools", topic: "service-entry" },
|
|
7
|
+
{ module: "admin-bindings.ts", selected: files => "admin-bindings.ts" in files, what: "Admin bindings", topic: "native-admin" },
|
|
8
|
+
];
|
|
9
|
+
export async function unmountedWarnings(entry, destination, files) {
|
|
10
|
+
const selected = SURFACES.filter(surface => surface.selected(files));
|
|
11
|
+
if (!selected.length || !existsSync(entry))
|
|
12
|
+
return [];
|
|
13
|
+
let inputs;
|
|
14
|
+
try {
|
|
15
|
+
inputs = Object.keys((await ownModules(entry)).inputs).map(path => resolve(path));
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
const first = error.errors?.[0]?.text ?? String(error);
|
|
19
|
+
return [`skipped checking which generated surfaces ${entry} serves: could not follow its imports (${first}).`];
|
|
20
|
+
}
|
|
21
|
+
return selected.filter(surface => !inputs.includes(resolve(destination, surface.module))).map(surface => `${surface.what} are generated in ${relative(process.cwd(), resolve(destination, surface.module))}, but ${entry} never imports it, so no request reaches them. Mount it in the entry (mantle help ${surface.topic}), or pass --entry if the app starts elsewhere.`);
|
|
22
|
+
}
|
|
@@ -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("\\", "/") || ".";
|
|
@@ -36,7 +34,7 @@ export function configureWrangler(configPath, entry, outdir, generated, check =
|
|
|
36
34
|
throw new Error("--check requires a JSON or JSONC Wrangler config; TOML generation only prints manual edits.");
|
|
37
35
|
console.log(`Leave ${configPath} unchanged. Add these Wrangler TOML keys (include nodejs_compat and global_fetch_strictly_public in every compatibility_flags override):\n` +
|
|
38
36
|
`main = ${JSON.stringify(keys.main)}\nno_bundle = true\nfind_additional_modules = true\nbase_dir = ${JSON.stringify(worker)}\ncompatibility_flags = ${JSON.stringify(requiredFlags)}\n\n` +
|
|
39
|
-
(assets === undefined ? "" : `[assets]\ndirectory = ${JSON.stringify(local(assets))}\n\n`) +
|
|
37
|
+
(assets === undefined ? "" : `[assets]\ndirectory = ${JSON.stringify(local(assets))}\nbinding = "ASSETS"\nrun_worker_first = true\nhtml_handling = "none"\n\n`) +
|
|
40
38
|
`[[rules]]\ntype = "ESModule"\nglobs = ["**/*.js"]\nfallthrough = true\n\n[build]\ncommand = ${JSON.stringify(keys.build.command)}\nwatch_dir = ${JSON.stringify(keys.build.watch_dir)}`);
|
|
41
39
|
return false;
|
|
42
40
|
}
|
|
@@ -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);
|
|
@@ -80,9 +75,23 @@ export function configureWrangler(configPath, entry, outdir, generated, check =
|
|
|
80
75
|
edit([key], value);
|
|
81
76
|
for (const [key, value] of Object.entries(buildKeys))
|
|
82
77
|
edit(["build", key], value);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
if (assets !== undefined) {
|
|
79
|
+
const wireAssets = (path, current) => {
|
|
80
|
+
edit([...path, "directory"], local(assets));
|
|
81
|
+
const ownsDefaults = current === undefined || current?.binding === undefined;
|
|
82
|
+
if (ownsDefaults && current?.binding === undefined)
|
|
83
|
+
edit([...path, "binding"], "ASSETS");
|
|
84
|
+
if (ownsDefaults && current?.run_worker_first === undefined)
|
|
85
|
+
edit([...path, "run_worker_first"], true);
|
|
86
|
+
if (current?.html_handling === undefined && (current?.run_worker_first === true || (ownsDefaults && current?.run_worker_first === undefined)))
|
|
87
|
+
edit([...path, "html_handling"], "none");
|
|
88
|
+
};
|
|
89
|
+
wireAssets(["assets"], config.assets);
|
|
90
|
+
for (const [name, environment] of Object.entries(config.env ?? {})) {
|
|
91
|
+
if (environment?.assets !== undefined)
|
|
92
|
+
wireAssets(["env", name, "assets"], environment.assets);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
86
95
|
if (!(config.rules ?? []).some((r) => isDeepStrictEqual(r, rule)))
|
|
87
96
|
edit(config.rules === undefined ? ["rules"] : ["rules", -1], config.rules === undefined ? [rule] : rule);
|
|
88
97
|
const changed = !exists || text !== original;
|
package/dist/kernel/binds.d.ts
CHANGED
|
@@ -18,10 +18,19 @@ export type Recipe = {
|
|
|
18
18
|
} | {
|
|
19
19
|
readonly k: "const";
|
|
20
20
|
readonly value: unknown;
|
|
21
|
+
}
|
|
22
|
+
/** A field-scoped Schema's scope column on an SQL INSERT: the Store's insert rule (autofill one value, refuse a value outside the caller's set). */
|
|
23
|
+
| {
|
|
24
|
+
readonly k: "scoped";
|
|
25
|
+
readonly column: string;
|
|
26
|
+
readonly field: string;
|
|
27
|
+
readonly type: "string" | "number";
|
|
28
|
+
readonly name?: string;
|
|
29
|
+
readonly value?: string | number;
|
|
21
30
|
} | {
|
|
22
31
|
readonly k: "input";
|
|
23
|
-
readonly name: string; /** Only a date-time
|
|
24
|
-
readonly type?: "timestamptz" | "date";
|
|
32
|
+
readonly name: string; /** Only a date-time, a date or JSON is converted, so only those carry a type. */
|
|
33
|
+
readonly type?: "timestamptz" | "date" | "json";
|
|
25
34
|
};
|
|
26
35
|
/** One printed statement and how to bind it. `row`: it must affect exactly one row. */
|
|
27
36
|
export interface Step {
|
|
@@ -36,15 +45,15 @@ export type Bound = {
|
|
|
36
45
|
};
|
|
37
46
|
/** The dialect functions the recipes in use call; generated code passes only those. */
|
|
38
47
|
export type Values = Partial<Pick<Dialect, "now" | "cutoff" | "timestamp" | "date">> & {
|
|
39
|
-
readonly user?: (recipe: Extract<Recipe, {
|
|
40
|
-
k: "user";
|
|
41
|
-
}>, caller: Caller) => unknown;
|
|
42
48
|
readonly scopeInsert?: Dialect['scopeInsert'];
|
|
43
|
-
readonly userRefs?: (recipe: Recipe, caller: Caller) => unknown;
|
|
49
|
+
readonly userRefs?: (recipe: Recipe, caller: Caller, input?: Readonly<Record<string, unknown>>) => unknown;
|
|
44
50
|
/** Present only in apps with user fields: keeps, per caller, one printed form of each Schema scope term. */
|
|
45
51
|
readonly scopedAll?: <T>(value: T, caller: Caller) => T;
|
|
46
52
|
};
|
|
47
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* 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
|
|
55
|
+
* already converted value, is bound as it is); any other value (and `null`) is bound as it is.
|
|
56
|
+
*/
|
|
48
57
|
export declare const encode: (d: Values, type: string | undefined, v: unknown) => unknown;
|
|
49
58
|
export declare function binds(d: Values, recipes: readonly Recipe[], caller: Caller, input?: Readonly<Record<string, unknown>>, ms?: number): unknown[];
|
|
50
59
|
/** Every step's binds, evaluated for a Procedure. */
|
package/dist/kernel/binds.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
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
|
-
|
|
2
|
+
export const encode = (d, type, v) => type === "timestamptz" ? (v == null ? null : d.timestamp(v)) : type === "date" ? (v == null ? null : d.date(v))
|
|
3
|
+
: type === "json" && v != null && typeof v !== "string" ? JSON.stringify(v) : v;
|
|
8
4
|
export function binds(d, recipes, caller, input, ms = 0) {
|
|
9
5
|
const out = [];
|
|
10
6
|
for (const r of recipes) {
|
|
@@ -32,14 +28,12 @@ export function binds(d, recipes, caller, input, ms = 0) {
|
|
|
32
28
|
case "input":
|
|
33
29
|
out.push(encode(d, r.type, input[r.name]));
|
|
34
30
|
break;
|
|
35
|
-
|
|
36
|
-
default: out.push(d.userRefs(r, caller));
|
|
31
|
+
default: out.push(d.userRefs(r, caller, input));
|
|
37
32
|
}
|
|
38
33
|
}
|
|
39
34
|
return out;
|
|
40
35
|
}
|
|
41
|
-
/** Every step's binds, evaluated for a Procedure. */
|
|
42
36
|
export function bound(d, steps, caller, input) {
|
|
43
|
-
const ms = Date.now();
|
|
37
|
+
const ms = Date.now();
|
|
44
38
|
return steps.map(s => ({ sql: s.sql, binds: binds(d, s.binds, caller, input, ms), row: s.row }));
|
|
45
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
|
+
}
|
|
@@ -108,10 +108,14 @@ export type Output = {
|
|
|
108
108
|
};
|
|
109
109
|
/** Deny unless `allowed`: a user is AUTH_DENIED, anyone else UNAUTHENTICATED. */
|
|
110
110
|
export declare function deny(caller: Caller, allowed: boolean, path: string): void;
|
|
111
|
-
/**
|
|
111
|
+
/** A row statement that changed no row cannot tell why without leaking whether the row exists, so it says how to recover. */
|
|
112
|
+
export declare const ROW_CONFLICT = "No row matched this change: it does not exist or is not visible to you, it changed since it was read, or its state no longer allows this change. Read it again before deciding whether to retry.";
|
|
113
|
+
/** CONFLICT for a write the database refused: a unique-index clash (`fields.kind` "unique"), or a row statement that did not change exactly one row ("row"). */
|
|
112
114
|
export declare const conflict: (path: string, unique: boolean, cause?: unknown) => ContractError;
|
|
113
115
|
/** A Schema `checks` violation (Mantle's SQLite check trigger, or PostgreSQL SQLSTATE 23514) is a CONFLICT naming the check. */
|
|
114
|
-
export declare const checkConflict: (path: string, error: unknown
|
|
116
|
+
export declare const checkConflict: (path: string, error: unknown) => ContractError | undefined;
|
|
117
|
+
/** One message on every target, built from the parsed fields; the driver's own text stays in `cause`. */
|
|
118
|
+
export declare const schemaCheckFailed: (path: string, cause: unknown, schema: string | undefined, check: string | undefined) => ContractError;
|
|
115
119
|
/** One result of `unsafe`: the rows (typed by the generated function), plus the affected count. */
|
|
116
120
|
export type PostgresResult = any[] & {
|
|
117
121
|
readonly count: number | null;
|
|
@@ -128,12 +132,4 @@ export interface Postgres {
|
|
|
128
132
|
}
|
|
129
133
|
/** What a read or a single statement needs: a transaction handle suits it too. */
|
|
130
134
|
export type Unsafe = Pick<Postgres, "unsafe">;
|
|
131
|
-
|
|
132
|
-
export declare function checkOrigin(request: Request, caller: Caller): void;
|
|
133
|
-
/** A resolver's ContractError (e.g. UNAUTHENTICATED for an expired token) keeps its status; anything else is an application failure. */
|
|
134
|
-
export declare function surfaceCaller<Env>(request: Request, env: Env, resolve: (request: Request, env: Env, authority: {
|
|
135
|
-
readonly readOnly: boolean;
|
|
136
|
-
}) => Caller | Promise<Caller>, readOnly?: boolean): Promise<Caller>;
|
|
137
|
-
/** 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. */
|
|
138
|
-
export declare function surfaceError(error: unknown, where?: string): ReturnType<typeof wire>;
|
|
139
|
-
export declare function surfaceFailure(error: unknown, challenge?: string, where?: string): Response;
|
|
135
|
+
export { checkOrigin, surfaceCaller, surfaceError, surfaceFailure } from "./surface-failure.js";
|
package/dist/kernel/contracts.js
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
|
-
/** Boundary primitives only. No plan, SQL interpreter or resource ownership. */
|
|
2
1
|
import { ContractError } from "./contract-error.js";
|
|
3
2
|
export { ContractError } from "./contract-error.js";
|
|
4
3
|
import { hasRole, isAdminCaller as isAdmin } from "./caller-identity.js";
|
|
5
|
-
/** Gateway mode or the one execution prop carrying forwarded authority. */
|
|
6
4
|
export const forwarded = (ctx, gateway) => !!(gateway || ctx?.props && Object.hasOwn(ctx.props, "mantle"));
|
|
7
|
-
/** Privilege never follows a member role name. Only the original verified identity carries the admin layer. */
|
|
8
5
|
export { isAdmin };
|
|
9
|
-
/** Both authenticated layers pass ctx.user; the host layer still needs its original brand. */
|
|
10
6
|
export const isUser = (caller) => caller.kind === "user" || isAdmin(caller);
|
|
11
7
|
export const hasAdminRole = (caller, name) => isAdmin(caller) && hasRole(caller, name);
|
|
12
|
-
/** Only the core transport failures; other codes carry their own status, and unknown codes remain server errors. */
|
|
13
8
|
export const statusOf = (code) => ({ __proto__: null,
|
|
14
9
|
INPUT_VALIDATION_FAILED: 400, UNAUTHENTICATED: 401, AUTH_DENIED: 403, NOT_FOUND: 404,
|
|
15
10
|
METHOD_NOT_ALLOWED: 405, CONFLICT: 409, OUTCOME_UNKNOWN: 503,
|
|
16
11
|
}[code] ?? 500);
|
|
17
|
-
/** Redact native errors at the selected boundary; the bare query preserves their identity. */
|
|
18
12
|
export function wire(error) {
|
|
19
13
|
const status = error instanceof ContractError ? error.status ?? statusOf(error.code) : 500;
|
|
20
14
|
if (status !== 500) {
|
|
@@ -46,47 +40,17 @@ export function wireRows(rows, fields = []) {
|
|
|
46
40
|
return result;
|
|
47
41
|
});
|
|
48
42
|
}
|
|
49
|
-
/** Deny unless `allowed`: a user is AUTH_DENIED, anyone else UNAUTHENTICATED. */
|
|
50
43
|
export function deny(caller, allowed, path) {
|
|
51
44
|
if (!allowed)
|
|
52
45
|
throw new ContractError("subject" in caller ? "AUTH_DENIED" : "UNAUTHENTICATED", "Authorization predicate not satisfied.", path);
|
|
53
46
|
}
|
|
54
|
-
|
|
55
|
-
export const conflict = (path, unique, cause) => new ContractError("CONFLICT", unique ? "A unique constraint of the Schema was violated." :
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (
|
|
47
|
+
export const ROW_CONFLICT = "No row matched this change: it does not exist or is not visible to you, it changed since it was read, or its state no longer allows this change. Read it again before deciding whether to retry.";
|
|
48
|
+
export const conflict = (path, unique, cause) => new ContractError("CONFLICT", unique ? "A unique constraint of the Schema was violated." : ROW_CONFLICT, path, { cause, fields: { kind: unique ? "unique" : "row" } });
|
|
49
|
+
export const checkConflict = (path, error) => {
|
|
50
|
+
const message = error?.message ?? "", marker = /MANTLE_CHECK ([^:\n]+): (.+)/.exec(message);
|
|
51
|
+
if (!/MANTLE_CHECK /.test(message))
|
|
59
52
|
return undefined;
|
|
60
|
-
|
|
61
|
-
return new ContractError("CONFLICT", `A check of the Schema failed: ${/MANTLE_CHECK [^\n'"]+/.exec(message)?.[0] ?? message}`, path, { cause: error });
|
|
53
|
+
return schemaCheckFailed(path, error, marker?.[1], marker?.[2]?.replace(/: SQLITE_CONSTRAINT.*$/, ""));
|
|
62
54
|
};
|
|
63
|
-
|
|
64
|
-
export
|
|
65
|
-
if (!("credential" in caller) || caller.credential !== "session" || ["GET", "HEAD", "OPTIONS"].includes(request.method))
|
|
66
|
-
return;
|
|
67
|
-
const site = request.headers.get("sec-fetch-site"), origin = request.headers.get("origin");
|
|
68
|
-
if ((!site && !origin) || (site && site !== "same-origin" && site !== "none") || (origin && origin !== new URL(request.url).origin))
|
|
69
|
-
throw new ContractError("AUTH_DENIED", "Cross-origin session mutation rejected.", "request:origin");
|
|
70
|
-
}
|
|
71
|
-
/** A resolver's ContractError (e.g. UNAUTHENTICATED for an expired token) keeps its status; anything else is an application failure. */
|
|
72
|
-
export async function surfaceCaller(request, env, resolve, readOnly = false) {
|
|
73
|
-
try {
|
|
74
|
-
return await resolve(request, env, { readOnly });
|
|
75
|
-
}
|
|
76
|
-
catch (cause) {
|
|
77
|
-
if (cause instanceof ContractError)
|
|
78
|
-
throw cause;
|
|
79
|
-
throw new Error("Caller resolution failed.", { cause });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
/** 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. */
|
|
83
|
-
export function surfaceError(error, where = "surface") {
|
|
84
|
-
const result = wire(error);
|
|
85
|
-
if (result.status === 500)
|
|
86
|
-
console.error(`[mantle ${where}] unhandled failure`, error);
|
|
87
|
-
return result;
|
|
88
|
-
}
|
|
89
|
-
export function surfaceFailure(error, challenge, where) {
|
|
90
|
-
const result = surfaceError(error, where);
|
|
91
|
-
return Response.json({ error: result.error }, { status: result.status, headers: { "cache-control": "private, no-store", ...(challenge && [401, 403].includes(result.status) && result.error.path !== "request:origin" ? { "www-authenticate": challenge } : {}) } });
|
|
92
|
-
}
|
|
55
|
+
export const schemaCheckFailed = (path, cause, schema, check) => new ContractError("CONFLICT", schema && check ? `A check of the Schema failed: ${schema}: ${check}` : "A check of the Schema failed.", path, { cause, fields: { kind: "check", schema, check } });
|
|
56
|
+
export { checkOrigin, surfaceCaller, surfaceError, surfaceFailure } from "./surface-failure.js";
|
package/dist/kernel/cron.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
/** SDK-owned POSIX to Cloudflare cron conversion, copied verbatim from Core's CloudflareCron so Workers don't pull Core's D1 driver; Core is retired with 0.2.0, so this copy becomes the only one. */
|
|
2
1
|
const PART = /^(\*|\d+|[A-Za-z]{3})(?:-(\d+|[A-Za-z]{3}))?(?:\/(\d+))?$/;
|
|
3
2
|
const MONTHS = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
|
|
4
3
|
const DAYS = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
|
|
5
|
-
/** label, lowest, highest, names (by value from `lowest`) */
|
|
6
4
|
const FIELDS = [["minute", 0, 59, null], ["hour", 0, 23, null], ["day of month", 1, 31, null], ["month", 1, 12, MONTHS], ["weekday", 0, 6, DAYS]];
|
|
7
5
|
export function toCloudflareCron(cron) {
|
|
8
6
|
const refuse = (why) => {
|
|
@@ -14,7 +12,6 @@ export function toCloudflareCron(cron) {
|
|
|
14
12
|
const weekday = [];
|
|
15
13
|
fields.forEach((field, i) => {
|
|
16
14
|
const [label, min, max, names] = FIELDS[i];
|
|
17
|
-
// a value as a number: a name counts as its number, and a number must be written plainly (`01` and `1` are one value)
|
|
18
15
|
const value = (token) => {
|
|
19
16
|
if (/^\d+$/.test(token)) {
|
|
20
17
|
if (token.length > 1 && token.startsWith("0"))
|
|
@@ -51,7 +48,6 @@ export function toCloudflareCron(cron) {
|
|
|
51
48
|
if (i === 4)
|
|
52
49
|
weekday.push(...out);
|
|
53
50
|
});
|
|
54
|
-
// POSIX runs on either day when both are restricted; Cloudflare does not document that rule, so only one may be
|
|
55
51
|
if (fields[2] !== "*" && fields[4] !== "*")
|
|
56
52
|
refuse("restrict the day of month or the weekday, not both");
|
|
57
53
|
return [...fields.slice(0, 4), weekday.join(",")].join(" ");
|
|
@@ -10,7 +10,6 @@ export declare const likeEscape = " ESCAPE '\\'";
|
|
|
10
10
|
*/
|
|
11
11
|
export interface Dialect {
|
|
12
12
|
readonly userRefs?: import('../binds.js').Values['userRefs'];
|
|
13
|
-
readonly user?: import('../binds.js').Values['user'];
|
|
14
13
|
readonly anyOf?: (column: string, placeholder: string, type: 'string' | 'number') => string;
|
|
15
14
|
readonly scopeInsert?: (scopes: readonly {
|
|
16
15
|
readonly column: string;
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
/** What SQLite and PostgreSQL spell identically, defined once; each dialect module re-exports it. */
|
|
2
|
-
/** A (possibly schema-qualified) name, quoted. */
|
|
3
1
|
export const ident = (name) => name.split(".").map(part => `"${part.replaceAll('"', '""')}"`).join(".");
|
|
4
|
-
/** A literal piece of text for a LIKE pattern: `\`, `%` and `_` match themselves. Pair with `likeEscape`. */
|
|
5
2
|
export const escapeLike = (value) => value.replace(/[\\%_]/g, "\\$&");
|
|
6
3
|
export const likeEscape = " ESCAPE '\\'";
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** PostgreSQL: `$n` placeholders, time as timestamptz (ISO text keeps every microsecond). */
|
|
2
1
|
import { isoDate, isoTimestamptz } from "../scalars.js";
|
|
3
2
|
import { searchLike } from "../search.js";
|
|
4
3
|
import { escapeLike, ident, likeEscape } from "./common.js";
|
|
@@ -13,6 +12,5 @@ export const cutoff = (ms, seconds) => new Date(ms - seconds * 1000);
|
|
|
13
12
|
export const timestamp = isoTimestamptz;
|
|
14
13
|
export const date = isoDate;
|
|
15
14
|
export const cast = (placeholder, type) => type === "timestamptz" || type === "date" ? `CAST(CAST(${placeholder} AS text) AS ${type})` : type === "json" ? `CAST(CAST(${placeholder} AS text) AS jsonb)` : placeholder;
|
|
16
|
-
// ponytail: preserve Core's ILIKE scan; add pg_trgm only when measured search latency needs it.
|
|
17
15
|
export const search = (_table, fields, alias, query) => searchLike(fields.map(field => `${alias}.${ident(field)}`), query, "ILIKE");
|
|
18
16
|
export const dialect = { param, ident, escapeLike, likeEscape, like, search, wireTimestamp, wireDate, now, cutoff, timestamp, date, cast, anyOf };
|
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
import { ContractError } from "../contracts.js";
|
|
2
|
-
/** SQLite and Cloudflare D1: `?n` placeholders, time as microseconds and dates as days since the epoch. */
|
|
3
2
|
import { encodeDate, encodeTimestamptz } from "../scalars.js";
|
|
4
3
|
import { searchLike } from "../search.js";
|
|
5
4
|
import { escapeLike, ident, likeEscape } from "./common.js";
|
|
6
5
|
export { ident, escapeLike, likeEscape };
|
|
7
6
|
export const param = (n) => `?${n}`;
|
|
8
7
|
export const anyOf = (column, placeholder) => `${column} IN (SELECT value FROM json_each(${placeholder}))`;
|
|
9
|
-
export const like = () => "LIKE";
|
|
10
|
-
/** A write that broke a UNIQUE index or the id's UNIQUE (bun:sqlite and D1 word it the same). */
|
|
8
|
+
export const like = () => "LIKE";
|
|
11
9
|
export const unique = (error) => /UNIQUE constraint failed/.test(String(error?.message));
|
|
12
|
-
/** Positive rollback evidence only. Plain/network/I/O errors are deliberately uncertain. */
|
|
13
10
|
export function definitive(error) {
|
|
14
11
|
if (error instanceof ContractError)
|
|
15
12
|
return error.code === "CONFLICT";
|
|
16
13
|
const e = error;
|
|
17
|
-
// Bun before 1.4 omits `code` on statement errors; its SQLiteError still carries the native errno.
|
|
18
14
|
const native = typeof e?.code === "string" ? e.code.startsWith("SQLITE_") : e?.name === "SQLiteError";
|
|
19
15
|
return native && typeof e.errno === "number" && [1, 19, 20, 25].includes(e.errno & 255);
|
|
20
16
|
}
|
|
21
|
-
// floor, not SQLite's truncating `/` and `%`: a time before 1970 is a negative number of microseconds
|
|
22
17
|
export const wireTimestamp = (v) => ["strftime('%Y-%m-%dT%H:%M:%S', ((", v, ") - ((", v, ") % 1000000 + 1000000) % 1000000) / 1000000, 'unixepoch') || '.' || substr('00000' || (((", v, ") % 1000000 + 1000000) % 1000000), -6) || 'Z'"];
|
|
23
18
|
export const wireDate = (v) => ["date((", v, ") * 86400, 'unixepoch')"];
|
|
24
19
|
export const now = (ms) => ms * 1000;
|
package/dist/kernel/drain.js
CHANGED
|
@@ -1,25 +1,19 @@
|
|
|
1
|
-
/** Request-time only: no Worker startup path imports this. */
|
|
2
|
-
/** Read and discard what is left of a request body, at most 1 MiB and 1 s. A response sent over an unread POST body makes local
|
|
3
|
-
* `wrangler dev` break the connection, and the next request fails with "Network connection lost". */
|
|
4
1
|
async function drain(request) {
|
|
5
2
|
let reader;
|
|
6
3
|
const timer = setTimeout(() => reader?.cancel().catch(() => { }), 1000);
|
|
7
4
|
try {
|
|
8
|
-
reader = request.body?.getReader();
|
|
5
|
+
reader = request.body?.getReader();
|
|
9
6
|
for (let read = 0, chunk; reader && !(chunk = await reader.read()).done;)
|
|
10
7
|
if ((read += chunk.value.byteLength) > 1024 * 1024) {
|
|
11
8
|
await reader.cancel();
|
|
12
9
|
return;
|
|
13
10
|
}
|
|
14
11
|
}
|
|
15
|
-
catch {
|
|
12
|
+
catch { }
|
|
16
13
|
finally {
|
|
17
14
|
clearTimeout(timer);
|
|
18
15
|
}
|
|
19
16
|
}
|
|
20
|
-
/** A request handler that drains the body once it has answered or thrown. `undefined` and `null` mean the request is not this
|
|
21
|
-
* handler's, so the next handler can still read it. A handler that answers and reads the request later (in `waitUntil`) must
|
|
22
|
-
* read or lock the body before answering. */
|
|
23
17
|
export const draining = (handle) => async (...args) => {
|
|
24
18
|
let response;
|
|
25
19
|
try {
|