@aotter/mantle 0.2.0-alpha.9 → 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 +9 -3
- package/dist/auth/adapter.js +1 -28
- 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 +5 -151
- 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 +2 -30
- package/dist/infrastructure/cli.js +25 -11
- package/dist/infrastructure/generate.js +9 -16
- package/dist/infrastructure/generateAuth.js +1 -9
- 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 +0 -6
- package/dist/kernel/authorization.js +0 -1
- package/dist/kernel/binds.d.ts +16 -4
- 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 +6 -10
- package/dist/kernel/contracts.js +5 -41
- package/dist/kernel/cron.js +0 -4
- 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 +2 -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 +0 -5
- package/dist/kernel/mcp-call.js +0 -1
- package/dist/kernel/mcp-handler.js +2 -26
- package/dist/kernel/mcp.js +0 -5
- 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 +1 -9
- 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 +0 -1
- 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 -91
- 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 +0 -4
- package/dist/usecase/rest.js +0 -1
- package/dist/usecase/site.js +0 -1
- 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 +0 -20
- package/docs/examples/README.md +13 -5
- package/docs/examples/app-shell/README.md +38 -7
- 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/routes.ts +6 -3
- package/docs/examples/app-shell/src/service.ts +2 -1
- package/docs/examples/club-events/README.md +7 -2
- package/docs/examples/club-events/manifests/club-events.yaml +15 -0
- package/docs/examples/club-events/smoke.mjs +7 -1
- package/docs/examples/purchasing/README.md +12 -3
- package/docs/examples/purchasing/manifests/purchasing.yaml +21 -4
- package/docs/examples/purchasing/smoke.mjs +14 -0
- package/docs/examples/recipes/README.md +5 -3
- package/docs/handbook/cloudflare/service-entry.md +3 -0
- 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 +113 -103
- package/docs/handbook/guides/local-testing.md +40 -0
- package/docs/handbook/guides/native-admin.md +25 -6
- package/docs/handbook/guides/native-execution.md +5 -4
- 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 +138 -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 +5 -2
- 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 +6 -0
- package/docs/handbook/start/quickstart-worker.md +1 -1
- package/docs/skills/develop/SKILL.md +6 -5
- package/docs/skills/update/SKILL.md +2 -0
- package/package.json +23 -3
- package/skills/README.md +1 -1
- package/skills/mantle/SKILL.md +39 -120
- package/dist/domain/describe/chunk-ZVXBGP7Y.js +0 -424
- package/skills/mantle/references/local-testing.md +0 -20
- package/skills/mantle/references/web.md +0 -41
- package/skills/mantle/references/website.md +0 -51
|
@@ -1,424 +0,0 @@
|
|
|
1
|
-
// src/build/spec/domain/model/ManifestGrammar.ts
|
|
2
|
-
function resolveLocalizedText(value, preferred, canonical2) {
|
|
3
|
-
if (value == null) return null;
|
|
4
|
-
if (typeof value === "string") return value;
|
|
5
|
-
if (Object.prototype.hasOwnProperty.call(value, preferred)) {
|
|
6
|
-
return value[preferred];
|
|
7
|
-
}
|
|
8
|
-
if (canonical2 && Object.prototype.hasOwnProperty.call(value, canonical2)) {
|
|
9
|
-
return value[canonical2];
|
|
10
|
-
}
|
|
11
|
-
const firstKey = Object.keys(value)[0];
|
|
12
|
-
return firstKey !== void 0 ? value[firstKey] : null;
|
|
13
|
-
}
|
|
14
|
-
var INPUT_RULE_KEYWORDS = ["dependentRequired", "anyOf", "if", "then", "else"];
|
|
15
|
-
|
|
16
|
-
// src/build/spec/domain/service/RootRefChain.ts
|
|
17
|
-
function lookupLocalRef(root, ref) {
|
|
18
|
-
if (typeof ref !== "string" || !ref.startsWith("#/")) return void 0;
|
|
19
|
-
let value = root;
|
|
20
|
-
try {
|
|
21
|
-
for (const key of decodeURIComponent(ref.slice(2)).split("/").map((key2) => key2.replace(/~1/g, "/").replace(/~0/g, "~"))) value = value && typeof value === "object" && Object.hasOwn(value, key) ? value[key] : void 0;
|
|
22
|
-
} catch {
|
|
23
|
-
return void 0;
|
|
24
|
-
}
|
|
25
|
-
return value && typeof value === "object" ? value : void 0;
|
|
26
|
-
}
|
|
27
|
-
function rootRefChain(input) {
|
|
28
|
-
const chain = [];
|
|
29
|
-
for (let schema = input; schema !== void 0; schema = lookupLocalRef(input, schema.$ref)) {
|
|
30
|
-
if (chain.includes(schema)) return void 0;
|
|
31
|
-
chain.push(schema);
|
|
32
|
-
if (schema.$ref === void 0) return chain;
|
|
33
|
-
}
|
|
34
|
-
return void 0;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// src/build/spec/domain/service/McpToolNaming.ts
|
|
38
|
-
function mcpToolNameSegment(name) {
|
|
39
|
-
return name.toLowerCase().replace(/-/g, "_");
|
|
40
|
-
}
|
|
41
|
-
function toolTitle(name) {
|
|
42
|
-
const words = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[-_.\s]+/g, " ").trim().toLowerCase();
|
|
43
|
-
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
44
|
-
}
|
|
45
|
-
var viewAnnotations = { readOnlyHint: true, destructiveHint: false };
|
|
46
|
-
function procedureAnnotations(p) {
|
|
47
|
-
const readOnlyHint = p.mcp?.readOnlyHint ?? false;
|
|
48
|
-
return { readOnlyHint, destructiveHint: !readOnlyHint && ("ref" in p.handler || overwrites(p.handler.sql.stmts)), ...p.mcp };
|
|
49
|
-
}
|
|
50
|
-
var overwrites = (node) => !!node && typeof node === "object" && ("DeleteStmt" in node || "UpdateStmt" in node || node.onConflictClause?.action === "ONCONFLICT_UPDATE" || Object.values(node).some(overwrites));
|
|
51
|
-
function mcpTools(plan, surface, locale = "en") {
|
|
52
|
-
const text = (t) => resolveLocalizedText(t, locale, "en") ?? void 0;
|
|
53
|
-
const tools = /* @__PURE__ */ new Map();
|
|
54
|
-
const add = (tool) => {
|
|
55
|
-
const prior = tools.get(tool.name);
|
|
56
|
-
if (prior && (prior.kind !== tool.kind || prior.source !== tool.source)) throw new TypeError(`MCP tool '${tool.name}' comes from both '${prior.source}' and '${tool.source}'.`);
|
|
57
|
-
tools.set(tool.name, tool);
|
|
58
|
-
};
|
|
59
|
-
for (const t of Object.values(plan.triggers)) {
|
|
60
|
-
if (t.source.kind !== "mcp" || t.source.surface !== surface) continue;
|
|
61
|
-
const p = plan.procedures[t.procedure];
|
|
62
|
-
const title = text(p.title);
|
|
63
|
-
add({
|
|
64
|
-
name: mcpToolNameSegment(t.procedure),
|
|
65
|
-
kind: "procedure",
|
|
66
|
-
source: t.procedure,
|
|
67
|
-
title: title ?? toolTitle(t.procedure),
|
|
68
|
-
description: text(p.description) ?? title ?? t.procedure,
|
|
69
|
-
inputSchema: p.input,
|
|
70
|
-
// structuredContent must be an object
|
|
71
|
-
...p.output.type === "object" ? { outputSchema: p.output } : {},
|
|
72
|
-
annotations: procedureAnnotations(p),
|
|
73
|
-
requires: p.requires
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
for (const [name, v] of Object.entries(plan.views)) {
|
|
77
|
-
if (v.surface !== surface) continue;
|
|
78
|
-
const chain = (v.input ? rootRefChain(v.input) ?? [v.input] : []).reverse();
|
|
79
|
-
const props = Object.assign({}, ...chain.map((s) => s.properties ?? {}));
|
|
80
|
-
const required = [...new Set(chain.flatMap((s) => s.required ?? []))];
|
|
81
|
-
if ("limit" in props || "cursor" in props) throw new TypeError(`View '${name}' declares an input named limit or cursor; the MCP tool reserves both for paging.`);
|
|
82
|
-
const rules = chain.map((s) => Object.fromEntries(INPUT_RULE_KEYWORDS.flatMap((k) => s[k] === void 0 ? [] : [[k, s[k]]]))).filter((r) => Object.keys(r).length);
|
|
83
|
-
const defs = v.input ? referencedDefs(v.input, props) : {};
|
|
84
|
-
const title = text(v.title);
|
|
85
|
-
add({
|
|
86
|
-
name: mcpToolNameSegment(name),
|
|
87
|
-
kind: "view",
|
|
88
|
-
source: name,
|
|
89
|
-
title: title ?? toolTitle(name),
|
|
90
|
-
description: text(v.description) ?? title ?? name,
|
|
91
|
-
inputSchema: {
|
|
92
|
-
type: "object",
|
|
93
|
-
properties: { ...props, limit: { type: "integer", minimum: 1, maximum: 500 }, cursor: { type: "string" } },
|
|
94
|
-
...required.length ? { required } : {},
|
|
95
|
-
...rules.length > 1 ? { allOf: rules } : rules[0],
|
|
96
|
-
...Object.keys(defs).length ? { $defs: defs } : {}
|
|
97
|
-
},
|
|
98
|
-
annotations: viewAnnotations,
|
|
99
|
-
requires: v.requires
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
return [...tools.values()];
|
|
103
|
-
}
|
|
104
|
-
function referencedDefs(root, node, found = {}) {
|
|
105
|
-
if (!node || typeof node !== "object") return found;
|
|
106
|
-
const ref = node.$ref, [head, segment] = typeof ref === "string" && ref.startsWith("#/") ? decoded(ref.slice(2)).split("/") : [];
|
|
107
|
-
const key = head === "$defs" ? segment?.replace(/~1/g, "/").replace(/~0/g, "~") : void 0;
|
|
108
|
-
if (key !== void 0 && !Object.hasOwn(found, key) && root.$defs && Object.hasOwn(root.$defs, key)) {
|
|
109
|
-
found[key] = root.$defs[key];
|
|
110
|
-
referencedDefs(root, found[key], found);
|
|
111
|
-
}
|
|
112
|
-
for (const child of Object.values(node)) referencedDefs(root, child, found);
|
|
113
|
-
return found;
|
|
114
|
-
}
|
|
115
|
-
var decoded = (pointer) => {
|
|
116
|
-
try {
|
|
117
|
-
return decodeURIComponent(pointer);
|
|
118
|
-
} catch {
|
|
119
|
-
return "";
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
// src/build/spec/domain/service/PlanFingerprint.ts
|
|
124
|
-
function canonical(v) {
|
|
125
|
-
if (Array.isArray(v)) return `[${v.map(canonical).join(",")}]`;
|
|
126
|
-
if (v && typeof v === "object")
|
|
127
|
-
return `{${Object.keys(v).sort().filter((k) => v[k] !== void 0).map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`).join(",")}}`;
|
|
128
|
-
return JSON.stringify(v) ?? "null";
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// src/domain/generated-tools.ts
|
|
132
|
-
var publishingTransitions = [["publish", ["draft"], "published"], ["unpublish", ["published", "archived"], "draft"], ["archive", ["draft", "published"], "archived"]];
|
|
133
|
-
var publishingInput = { type: "object", properties: { id: { type: "string", minLength: 1 }, expectedVersion: { type: "integer", minimum: 1 } }, required: ["id", "expectedVersion"], additionalProperties: false };
|
|
134
|
-
function mediaDefinitions() {
|
|
135
|
-
const define = (name, description, inputSchema, action, readOnlyHint = false) => ({ name, description, inputSchema, action, annotations: { readOnlyHint, destructiveHint: action === "delete", openWorldHint: true } });
|
|
136
|
-
const string = { type: "string" };
|
|
137
|
-
const texts = { alt: { type: "string", maxLength: 1e3 }, caption: { type: "string", maxLength: 1e3 } };
|
|
138
|
-
const input = (properties, required = []) => ({ type: "object", properties, required, additionalProperties: false });
|
|
139
|
-
return [
|
|
140
|
-
define("get_media_upload_policy", "Read declared image purpose MIME slots and byte caps before preparing variants or requesting an upload. Optimization runs locally, never in the host.", input({}), "policy", true),
|
|
141
|
-
define("create_media_upload", "Create short-lived PUT capabilities for image variants. Read site media purpose policy first; optimize images locally. Obtain attachment bytes yourself, then PUT each variant with its requiredHeaders and call commit_media_upload. Never store upload URLs as published images or ask the user to run terminal commands.", input({ filename: { type: "string", maxLength: 255 }, purpose: string, variants: { type: "array", minItems: 1, items: input({ mimeType: string, byteSize: { type: "integer", minimum: 1 }, role: { type: "string", enum: ["primary", "alternate", "fallback"] } }, ["mimeType", "byteSize", "role"]) }, ...texts }, ["filename", "purpose", "variants"]), "create"),
|
|
142
|
-
define("commit_media_upload", "Verify all uploaded variants and commit one permanent media asset. Store the returned asset id in fields referencing media_assets.id, never a temporary upload URL. Unknown or expired upload groups require a new create_media_upload.", input({ uploadGroupId: string, ...texts }, ["uploadGroupId"]), "commit"),
|
|
143
|
-
define("list_media_assets", "List committed image assets, newest first, with their permanent variants and alt/caption. Search matches id, alt or caption; reuse only a returned cursor.", input({ limit: { type: "integer", minimum: 1, maximum: 500 }, cursor: string, search: string }), "list", true),
|
|
144
|
-
define("get_media_asset", "Read one committed media asset and its permanent variant URLs.", input({ id: string }, ["id"]), "get", true),
|
|
145
|
-
define("update_media_asset", "Update committed asset alt or caption; omitted fields remain unchanged and empty strings clear text.", input({ id: string, ...texts }, ["id"]), "update"),
|
|
146
|
-
define("delete_media_asset", "Delete an asset's objects and library record. Check application references first. A partial failure can be retried using the same id.", input({ id: string }, ["id"]), "delete")
|
|
147
|
-
];
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// src/domain/describe/targets.ts
|
|
151
|
-
function inspectSql(value) {
|
|
152
|
-
const schemas = /* @__PURE__ */ new Set(), readSchemas = /* @__PURE__ */ new Set();
|
|
153
|
-
const writes = [];
|
|
154
|
-
let readOnly = true;
|
|
155
|
-
const walk = (v) => {
|
|
156
|
-
if (!v || typeof v !== "object") return;
|
|
157
|
-
const n = v;
|
|
158
|
-
if (n.RangeVar?.mantle === "table") {
|
|
159
|
-
schemas.add(n.RangeVar.relname.toLowerCase());
|
|
160
|
-
readSchemas.add(n.RangeVar.relname.toLowerCase());
|
|
161
|
-
}
|
|
162
|
-
for (const kind of ["InsertStmt", "UpdateStmt", "DeleteStmt"]) if (n[kind]) {
|
|
163
|
-
readOnly = false;
|
|
164
|
-
if (n[kind].relation?.relname) writes.push({ schema: n[kind].relation.relname.toLowerCase(), operation: kind === "InsertStmt" ? "create" : kind === "UpdateStmt" ? "update" : "delete" });
|
|
165
|
-
if (n[kind].relation?.relname) {
|
|
166
|
-
schemas.add(n[kind].relation.relname.toLowerCase());
|
|
167
|
-
if (kind !== "InsertStmt") readSchemas.add(n[kind].relation.relname.toLowerCase());
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
Object.values(n).forEach(walk);
|
|
171
|
-
};
|
|
172
|
-
walk(value);
|
|
173
|
-
return { schemas: [...schemas].sort(), readSchemas: [...readSchemas].sort(), readOnly, writes };
|
|
174
|
-
}
|
|
175
|
-
function mergeRestrict(policies, conjunctive) {
|
|
176
|
-
return Object.fromEntries([...new Set(policies.flatMap((p) => Object.keys(p)))].sort().map((schema) => {
|
|
177
|
-
const rows = policies.map((p) => p[schema] ?? { terms: [] });
|
|
178
|
-
const terms = [...new Map(rows.flatMap((p) => p.terms).map((t) => [canonical(t), t])).values()].filter((t) => conjunctive || rows.every((p) => p.terms.some((v) => canonical(v) === canonical(t))));
|
|
179
|
-
const owner = rows.find((p) => p.owner)?.owner;
|
|
180
|
-
return [schema, { ...owner && (conjunctive || rows.every((p) => p.owner === owner)) ? { owner } : {}, terms }];
|
|
181
|
-
}));
|
|
182
|
-
}
|
|
183
|
-
function inspectBehavior(plan, ir, opaque, requires, restrict, writes) {
|
|
184
|
-
const inspected = inspectSql(ir);
|
|
185
|
-
if (writes) {
|
|
186
|
-
inspected.writes = [...writes];
|
|
187
|
-
inspected.schemas.push(...writes.map((w) => w.schema));
|
|
188
|
-
inspected.readSchemas.push(...writes.filter((w) => w.operation !== "create").map((w) => w.schema));
|
|
189
|
-
}
|
|
190
|
-
const dependencies = [], policies = [restrict ?? {}];
|
|
191
|
-
const procedures = /* @__PURE__ */ new Set(), hooks = /* @__PURE__ */ new Set();
|
|
192
|
-
const visit = (name) => {
|
|
193
|
-
if (procedures.has(name)) return;
|
|
194
|
-
procedures.add(name);
|
|
195
|
-
const p = plan.procedures[name];
|
|
196
|
-
if (!p) throw new TypeError(`Unknown dependency '${name}'.`);
|
|
197
|
-
dependencies.push({ procedure: name, declaration: p });
|
|
198
|
-
policies.push(p.restrict ?? {});
|
|
199
|
-
opaque ||= "ref" in p.handler;
|
|
200
|
-
const g = inspectSql("sql" in p.handler ? p.handler.sql.stmts : void 0);
|
|
201
|
-
inspected.schemas.push(...g.schemas);
|
|
202
|
-
inspected.readSchemas.push(...g.readSchemas);
|
|
203
|
-
inspected.writes.push(...g.writes);
|
|
204
|
-
inspected.readOnly &&= g.readOnly;
|
|
205
|
-
if (p.requires?.guard) visit(p.requires.guard.procedure);
|
|
206
|
-
};
|
|
207
|
-
if (requires?.guard) visit(requires.guard.procedure);
|
|
208
|
-
for (; ; ) {
|
|
209
|
-
const before = hooks.size;
|
|
210
|
-
for (const [id, t] of Object.entries(plan.triggers).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
211
|
-
if (t.source.kind !== "lifecycle" || hooks.has(id)) continue;
|
|
212
|
-
const source = t.source;
|
|
213
|
-
if (!opaque && !inspected.writes.some((w) => w.schema === source.schema.toLowerCase() && source.on.some((on) => on.endsWith(`_${w.operation}`)))) continue;
|
|
214
|
-
hooks.add(id);
|
|
215
|
-
dependencies.push({ trigger: id, source });
|
|
216
|
-
visit(t.procedure);
|
|
217
|
-
}
|
|
218
|
-
if (hooks.size === before) break;
|
|
219
|
-
}
|
|
220
|
-
const related = (names) => {
|
|
221
|
-
const all = new Set(opaque ? Object.keys(plan.schemas) : names);
|
|
222
|
-
for (const name of all) {
|
|
223
|
-
const parent = plan.schemas[name]?.translates?.parent.toLowerCase();
|
|
224
|
-
if (parent) all.add(parent);
|
|
225
|
-
}
|
|
226
|
-
return [...all].sort();
|
|
227
|
-
};
|
|
228
|
-
return {
|
|
229
|
-
...inspected,
|
|
230
|
-
schemas: related(inspected.schemas),
|
|
231
|
-
readSchemas: related(inspected.readSchemas),
|
|
232
|
-
opaque,
|
|
233
|
-
restrict: mergeRestrict(policies, true),
|
|
234
|
-
dependencies
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
var objectInput = { type: "object", properties: {} };
|
|
238
|
-
var pathTemplate = (path) => path.replace(/\{([^}]+)\}/g, ":$1");
|
|
239
|
-
var adminRoutes = [
|
|
240
|
-
"GET /me",
|
|
241
|
-
"GET /site",
|
|
242
|
-
"GET /collections",
|
|
243
|
-
"GET /views-manifest",
|
|
244
|
-
"GET /operations",
|
|
245
|
-
"GET /webmcp",
|
|
246
|
-
"POST /webmcp/:tool",
|
|
247
|
-
"GET /bootstrap",
|
|
248
|
-
"GET /views/:name/export",
|
|
249
|
-
"GET /views/:name",
|
|
250
|
-
"GET /view-batch",
|
|
251
|
-
"GET /developer/views/:name",
|
|
252
|
-
"POST /operations/:name",
|
|
253
|
-
"GET /developer-console",
|
|
254
|
-
"GET /collections/:name/statistics",
|
|
255
|
-
"GET /entries",
|
|
256
|
-
"GET /entries/export",
|
|
257
|
-
"GET /entries/:id",
|
|
258
|
-
"POST /entries",
|
|
259
|
-
"PATCH /entries/:id",
|
|
260
|
-
"POST /entries/:id/publish",
|
|
261
|
-
"POST /entries/:id/unpublish",
|
|
262
|
-
"POST /entries/:id/archive",
|
|
263
|
-
"DELETE /entries/:id",
|
|
264
|
-
"GET /site-settings",
|
|
265
|
-
"PATCH /site-settings",
|
|
266
|
-
"POST /media/uploads",
|
|
267
|
-
"POST /media/uploads/:groupId/commit",
|
|
268
|
-
"GET /media",
|
|
269
|
-
"GET /media/:id",
|
|
270
|
-
"PATCH /media/:id",
|
|
271
|
-
"DELETE /media/:id",
|
|
272
|
-
"GET /admin",
|
|
273
|
-
"GET /members",
|
|
274
|
-
"PATCH /admin/:id/role",
|
|
275
|
-
"POST /admin/invitations",
|
|
276
|
-
"DELETE /admin/invitations/:id"
|
|
277
|
-
];
|
|
278
|
-
function targetDefinitions(plan) {
|
|
279
|
-
const out = [];
|
|
280
|
-
const tools = [...mcpTools(plan, "public"), ...mcpTools(plan, "admin")];
|
|
281
|
-
for (const [kind, entries] of [["view", plan.views], ["procedure", plan.procedures]]) {
|
|
282
|
-
for (const [name, p] of Object.entries(entries).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
283
|
-
const view = kind === "view" ? plan.views[name] : void 0;
|
|
284
|
-
const proc = kind === "procedure" ? plan.procedures[name] : void 0;
|
|
285
|
-
const ir = view?.stmts ?? (proc && "sql" in proc.handler ? proc.handler.sql.stmts : void 0);
|
|
286
|
-
const inspected = inspectBehavior(plan, ir, !!proc && "ref" in proc.handler, p.requires, p.restrict);
|
|
287
|
-
const tool = tools.find((t) => t.kind === kind && t.source === name);
|
|
288
|
-
const title = resolveLocalizedText(p.title, "en", "en") ?? name;
|
|
289
|
-
const triggers = Object.entries(plan.triggers).filter(([, t]) => kind === "procedure" && t.procedure === name).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([id, t]) => ({ id, source: t.source }));
|
|
290
|
-
const surfaces = [.../* @__PURE__ */ new Set([
|
|
291
|
-
...view ? [view.surface, "admin", ...view.surface === "public" ? ["rest"] : []] : [],
|
|
292
|
-
...triggers.map((t) => t.source.kind === "mcp" ? t.source.surface : t.source.kind === "http" ? "rest" : t.source.kind)
|
|
293
|
-
])].sort();
|
|
294
|
-
if (!surfaces.length) surfaces.push("internal");
|
|
295
|
-
out.push({
|
|
296
|
-
id: `${kind}:${name}`,
|
|
297
|
-
name: tool?.name ?? mcpToolNameSegment(name),
|
|
298
|
-
kind,
|
|
299
|
-
surface: view?.surface ?? (surfaces.includes("public") || surfaces.includes("rest") ? "public" : surfaces.includes("admin") ? "admin" : "internal"),
|
|
300
|
-
surfaces,
|
|
301
|
-
triggers,
|
|
302
|
-
title,
|
|
303
|
-
description: resolveLocalizedText(p.description, "en", "en") ?? title,
|
|
304
|
-
inputSchema: tool?.inputSchema ?? view?.input ?? proc?.input ?? objectInput,
|
|
305
|
-
annotations: tool?.annotations ?? {},
|
|
306
|
-
requires: p.requires,
|
|
307
|
-
restrict: inspected.restrict,
|
|
308
|
-
schemas: inspected.schemas,
|
|
309
|
-
readSchemas: inspected.readSchemas,
|
|
310
|
-
readOnly: !inspected.opaque && inspected.readOnly,
|
|
311
|
-
opaque: inspected.opaque,
|
|
312
|
-
trustedOnly: !!proc?.trustedOnly,
|
|
313
|
-
variants: [],
|
|
314
|
-
behavior: { declaration: p, dependencies: inspected.dependencies }
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
const addRoute = (id, dependencies, inputSchema = objectInput) => {
|
|
319
|
-
const admin = id.startsWith("admin:");
|
|
320
|
-
out.push({
|
|
321
|
-
id,
|
|
322
|
-
name: id.replace(/[^a-zA-Z0-9_-]/g, "_"),
|
|
323
|
-
title: id,
|
|
324
|
-
description: id,
|
|
325
|
-
kind: admin ? "admin" : "rest",
|
|
326
|
-
surface: admin ? "admin" : "public",
|
|
327
|
-
surfaces: [admin ? "admin" : "rest"],
|
|
328
|
-
triggers: dependencies.flatMap((d) => d.triggers),
|
|
329
|
-
inputSchema,
|
|
330
|
-
annotations: {},
|
|
331
|
-
requires: admin ? { auth: { all: ["ctx.admin"] } } : dependencies[0]?.requires,
|
|
332
|
-
restrict: mergeRestrict(dependencies.map((d) => d.restrict), false),
|
|
333
|
-
schemas: [...new Set(dependencies.flatMap((d) => d.schemas))].sort(),
|
|
334
|
-
readSchemas: [...new Set(dependencies.flatMap((d) => d.readSchemas))].sort(),
|
|
335
|
-
readOnly: dependencies.length > 0 && dependencies.every((d) => d.readOnly) && (!admin || id.startsWith("admin:GET ") || id === "admin:POST /operations/:name" || id === "admin:POST /webmcp/:tool"),
|
|
336
|
-
opaque: dependencies.some((d) => d.opaque),
|
|
337
|
-
trustedOnly: dependencies.some((d) => d.trustedOnly),
|
|
338
|
-
variants: dependencies,
|
|
339
|
-
behavior: dependencies.map((d) => ({ id: d.id, behavior: d.behavior, surfaces: d.surfaces, triggers: d.triggers, input: d.inputSchema, requires: d.requires, restrict: d.restrict, trustedOnly: d.trustedOnly }))
|
|
340
|
-
});
|
|
341
|
-
};
|
|
342
|
-
const declarations = [...out];
|
|
343
|
-
const adminOperations = new Set(mcpTools(plan, "admin").filter((t) => t.kind === "procedure").map((t) => `procedure:${t.source}`));
|
|
344
|
-
for (const d of declarations) if (d.kind === "view" && d.surfaces.includes("public")) addRoute(`rest:GET /api/views/${d.id.slice(5)}`, [d], d.inputSchema);
|
|
345
|
-
for (const [, t] of Object.entries(plan.triggers).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) if (t.source.kind === "http") {
|
|
346
|
-
const d = declarations.find((d2) => d2.id === `procedure:${t.procedure}`);
|
|
347
|
-
addRoute(`rest:${t.source.method} ${pathTemplate(t.source.path)}`, [d], d.inputSchema);
|
|
348
|
-
}
|
|
349
|
-
const generated = [];
|
|
350
|
-
for (const [schema, s] of Object.entries(plan.schemas).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) if (s.publishing) for (const [verb, from, to] of publishingTransitions) {
|
|
351
|
-
addRoute(`procedure:${s.name}.${verb}`, [], publishingInput);
|
|
352
|
-
const d = out.at(-1);
|
|
353
|
-
const inspected = inspectBehavior(plan, void 0, false, void 0, void 0, [{ schema, operation: verb === "publish" ? "publish" : "update" }]);
|
|
354
|
-
Object.assign(d, {
|
|
355
|
-
kind: "procedure",
|
|
356
|
-
name: mcpToolNameSegment(`${verb}_${s.name}`),
|
|
357
|
-
surface: "admin",
|
|
358
|
-
surfaces: ["admin"],
|
|
359
|
-
requires: { auth: { all: ["ctx.admin"] } },
|
|
360
|
-
schemas: [.../* @__PURE__ */ new Set([schema, ...inspected.schemas])].sort(),
|
|
361
|
-
readSchemas: [.../* @__PURE__ */ new Set([schema, ...inspected.readSchemas])].sort(),
|
|
362
|
-
opaque: inspected.opaque,
|
|
363
|
-
behavior: { schema, verb, from, to, dependencies: inspected.dependencies }
|
|
364
|
-
});
|
|
365
|
-
generated.push(d);
|
|
366
|
-
}
|
|
367
|
-
for (const tool of mediaDefinitions()) {
|
|
368
|
-
addRoute(`admin:tool:${tool.name}`, [], tool.inputSchema);
|
|
369
|
-
const d = out.at(-1);
|
|
370
|
-
Object.assign(d, { name: tool.name, readOnly: tool.annotations.readOnlyHint, opaque: !tool.annotations.readOnlyHint, annotations: tool.annotations, behavior: { tool, schemas: plan.schemas } });
|
|
371
|
-
generated.push(d);
|
|
372
|
-
}
|
|
373
|
-
for (const route of adminRoutes) {
|
|
374
|
-
const deps = route.includes("/views/:") || route === "GET /view-batch" ? declarations.filter((d2) => d2.kind === "view" && (route.includes("/developer/") || plan.views[d2.id.slice(5)]?.surface === "admin")) : route.includes("/operations/:") ? declarations.filter((d2) => adminOperations.has(d2.id)) : route === "POST /webmcp/:tool" ? declarations.filter((d2) => mcpTools(plan, "admin").some((t) => `${t.kind}:${t.source}` === d2.id)).concat(generated) : [];
|
|
375
|
-
const generic = route.includes("/entries") || route.includes("/statistics") || route === "GET /bootstrap";
|
|
376
|
-
addRoute(`admin:${route}`, deps, deps.length && route !== "GET /view-batch" ? { ...objectInput, anyOf: deps.map((d2) => ({ type: "object", properties: { [route.includes("/webmcp/") ? "tool" : "name"]: { type: "string", const: route.includes("/webmcp/") ? d2.name : d2.id.slice(d2.kind === "view" ? 5 : 10) } } })) } : objectInput);
|
|
377
|
-
const d = out.at(-1);
|
|
378
|
-
if (generic) {
|
|
379
|
-
const operation = route === "POST /entries" ? "create" : route === "DELETE /entries/:id" ? "delete" : route.startsWith("GET ") ? void 0 : route.endsWith("/publish") ? "publish" : "update";
|
|
380
|
-
const inspected = inspectBehavior(plan, void 0, false, void 0, void 0, operation ? Object.keys(plan.schemas).map((schema) => ({ schema, operation })) : []);
|
|
381
|
-
d.schemas = Object.keys(plan.schemas).sort();
|
|
382
|
-
d.readSchemas = d.schemas;
|
|
383
|
-
d.opaque = inspected.opaque;
|
|
384
|
-
d.behavior = { schemas: plan.schemas, dependencies: inspected.dependencies };
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
return [...new Map(out.map((d) => [d.id, d])).values()].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// src/domain/describe/target-hash.ts
|
|
391
|
-
async function hash(value) {
|
|
392
|
-
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonical(value)));
|
|
393
|
-
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
394
|
-
}
|
|
395
|
-
function hashDefinition(plan, d) {
|
|
396
|
-
return hash({
|
|
397
|
-
id: d.id,
|
|
398
|
-
surfaces: d.surfaces,
|
|
399
|
-
triggers: d.triggers,
|
|
400
|
-
behavior: d.behavior,
|
|
401
|
-
input: d.inputSchema,
|
|
402
|
-
requires: d.requires,
|
|
403
|
-
restrict: d.restrict,
|
|
404
|
-
dialect: plan.dialect,
|
|
405
|
-
schemas: Object.fromEntries(d.schemas.map((name) => [name, plan.schemas[name]])),
|
|
406
|
-
userFields: plan.access?.userFields,
|
|
407
|
-
readOnly: d.readOnly,
|
|
408
|
-
opaque: d.opaque,
|
|
409
|
-
trustedOnly: d.trustedOnly
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
async function targetHash(plan, targetId) {
|
|
413
|
-
const target = targetDefinitions(plan).find((d) => d.id === targetId);
|
|
414
|
-
if (!target) throw new TypeError(`Unknown target '${targetId}'.`);
|
|
415
|
-
return hashDefinition(plan, target);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
export {
|
|
419
|
-
canonical,
|
|
420
|
-
targetDefinitions,
|
|
421
|
-
hash,
|
|
422
|
-
hashDefinition,
|
|
423
|
-
targetHash
|
|
424
|
-
};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# Local HTTP acceptance
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
Generation with `--check` and typechecking prove compilation, not handler execution. Before reporting local readiness or deploying, run the application's existing dev command with isolated local data and a console/test email sender. Use a small application-owned Node `assert` + `fetch` script; no browser, deployment or helper server runner is required.
|
|
5
|
-
|
|
6
|
-
Better Auth limits each email-OTP path to 3 requests/minute per IP; `authHardening` stores rate limits in the database. Local tests signing in several users should send a distinct `cf-connecting-ip` header per user (with that header configured in `ipAddressHeaders`), or wait out the window. With the console sender, the OTP appears in the Wrangler log.
|
|
7
|
-
|
|
8
|
-
- Exercise actual HTTP Triggers for every custom Procedure handler: valid input/output, invalid input, anonymous refusal on protected operations, authorized synthetic caller and expected stored result. A status below 500 alone is not success; assert the expected status and JSON envelope/body. Use only the generated contract's routes, inputs and permissions.
|
|
9
|
-
- For an app-owned `fetch`, exercise its real public routes, not just dist files. Assert HTML content type, visible content and metadata/canonical/JSON-LD with no JavaScript execution, sitemap, robots and an unknown URL returning 404. Create a local synthetic draft and assert it is hidden anonymously; publish, edit and delete it through authorized operations and check the page and sitemap after each change. Reuse the application's existing smoke-test pattern and clean up test rows/server in `finally`.
|
|
10
|
-
- If the app serves Admin, run `npm run build` and then `node --test generated/admin-mcp-app.check.mjs` (the examples name it `npm test`): the generated `generated/admin-mcp-app.check.mjs` loads the Admin MCP App in an MCP host and fails on a network request, console error or exception. It needs `playwright-core` (a devDependency) and Chrome (`npx playwright-core install chromium` or `MANTLE_TEST_CHROMIUM`); with no browser it fails, so report it as blocked rather than skipped. See `docs/handbook/guides/mcp-apps.md`.
|
|
11
|
-
- Keep caller authority explicit: use supplied `resolveCaller` and caller-scoped Store/Procedure APIs from the installed service-entry docs. Use the actual local host capabilities; local tests do not prove remote identity/provider wiring.
|
|
12
|
-
- Account for each declared interface separately: verified with the command/identity/result, intentionally not exposed, or deferred/unverified with a reason. Generation proves declaration and artifacts, not route mounting. An early Admin preview or a WebMCP-only application may defer remote MCP; preserve that pending item in the application's existing README, handoff or test report, without a new status/configuration format.
|
|
13
|
-
- For selected remote MCP, use the installed `docs/examples/app-shell/smoke-kit.mjs` `assertMcpDiscovery(endpoint, { tools, headers })` in the application's smoke scenario. Derive `tools` from the generated `mcp.ts` exports, or the pure static `developerConsole.interfaces.callable` in generated `admin-descriptors.ts`; filter to the selected surface and explicitly select the expected permitted subset when application authorization narrows it. Include app-composed tools when present. Do not count `describe.targets` or reconstruct authorization rules in the harness. Assert initialized discovery and all `tools/list` pages; a 404, HTML 200, protocol error or missing expected tools must fail. Keep anonymous/member denial assertions separate. Discovery never needs a business-write `tools/call`.
|
|
14
|
-
- Supply test credentials from the application's normal authorized sign-in/test setup (the smoke kit's console OTP sign-in is valid). The discovery assertion only receives credentials: it must not acquire credentials itself, scrape other sessions or manufacture authority. Report local cookie-session discovery, OAuth Bearer/client integration and browser WebMCP as separate evidence. Cookie or WebMCP success does not prove remote OAuth/ChatGPT wiring; missing credentials leave that case unverified, not passed. Do not enable remote routes or OAuth merely to make acceptance pass.
|
|
15
|
-
- Record commands and passed/failed/blocked cases for the exact commit. Fix failures before source admission; rerun relevant checks after changing source. Packing does not run these checks and an agent's checklist is not server build attestation.
|
|
16
|
-
|
|
17
|
-
Independent acceptance for every user-facing workflow follows the installed
|
|
18
|
-
`docs/handbook/guides/agent-workflow-acceptance.md`. Scripted smoke is separate
|
|
19
|
-
evidence; browser or host rendering is the acceptance surface for UI and MCP App
|
|
20
|
-
deliverables.
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# Generated public websites
|
|
2
|
-
|
|
3
|
-
Use this flow only when the installed package README documents concrete native
|
|
4
|
-
queries and generated `web.ts`.
|
|
5
|
-
Earlier Core versions use their own installed handbook. Read the installed
|
|
6
|
-
`docs/handbook/reference/surface.md` and `docs/examples/generated-web/README.md` under `node_modules/@aotter/mantle/` before writing
|
|
7
|
-
routes; the installed version is authoritative.
|
|
8
|
-
|
|
9
|
-
Mount `web()` when the application serves public HTML and needs `llms.txt`, a
|
|
10
|
-
sitemap or Markdown alternates. Import the generated `web.ts`; pass the native
|
|
11
|
-
`db`, a verified Caller (or the app's resolver), the configured public `origin`,
|
|
12
|
-
`title`, `description` and `pages`. Mount generated `rest.ts` for public View and
|
|
13
|
-
HTTP Trigger APIs. Neither module renders the application's HTML.
|
|
14
|
-
|
|
15
|
-
Each page template's `{parameter}` names exactly match its public View inputs;
|
|
16
|
-
both its View and list View return these parameters. A list View accepts empty
|
|
17
|
-
input. `markdown` names a textual column in the detail View. Supply the same
|
|
18
|
-
SQL-visible data to the HTML renderer through generated Views or the typed Store.
|
|
19
|
-
Never add a frontend status filter: generated SQL owns publishing, scope and TTL.
|
|
20
|
-
|
|
21
|
-
In the Fetch entry, return a handled REST/web response, then render HTML for the
|
|
22
|
-
remaining page routes. Insert `await site.head(request, env)` inside `<head>`:
|
|
23
|
-
it supplies canonical and Markdown alternate tags from the current View row,
|
|
24
|
-
using the configured origin rather than the request host. Escape all other
|
|
25
|
-
application text. An unavailable page needs an application-owned 404; an empty
|
|
26
|
-
`head()` result alone does not render that response.
|
|
27
|
-
|
|
28
|
-
Verify locally before deploying:
|
|
29
|
-
|
|
30
|
-
1. Generate, typecheck, implement every listed ref handler and typecheck again.
|
|
31
|
-
2. Walk the public list's complete cursor chain. Its rows equal the `llms.txt`
|
|
32
|
-
entries and sitemap detail URLs below the documented index caps.
|
|
33
|
-
3. Request each rendered page and assert its canonical uses the configured
|
|
34
|
-
origin and its `.md` alternate returns current Markdown. Request hidden
|
|
35
|
-
drafts, expired/scoped-out rows and malformed paths; they must not leak data.
|
|
36
|
-
4. Check GET/HEAD, 404 `no-store` and escaped HTML. Importing no web entry should
|
|
37
|
-
keep web rendering code out of the application's bundle.
|
|
38
|
-
|
|
39
|
-
The current caps are 5,000 scanned rows/10 queries/1 MiB for `llms.txt`, and
|
|
40
|
-
50,000 rows/100 queries/50 MiB for sitemap. Translation-aware indexes and
|
|
41
|
-
hreflang are a documented follow-up; do not claim them from `locales[0]`.
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# Complete website defaults
|
|
2
|
-
|
|
3
|
-
These are default generation and acceptance requirements for a complete website,
|
|
4
|
-
unless the user explicitly changes the scope. They do not add Manifest keys or
|
|
5
|
-
change Core's optional composition. Read the installed version's docs and the
|
|
6
|
-
selected host's frontend kit before choosing APIs. Native applications use
|
|
7
|
-
their installed `docs/handbook/guides/native-admin.md` and the
|
|
8
|
-
[web contract](web.md).
|
|
9
|
-
|
|
10
|
-
- For self-hosted applications, build Admin with `mantle bundle` and serve it as
|
|
11
|
-
the installed `docs/handbook/guides/native-admin.md` describes. The application
|
|
12
|
-
owns auth, OTP delivery and verified Caller wiring; use a console/test sender
|
|
13
|
-
locally and configure production email for deployment. If a host already
|
|
14
|
-
supplies identity/Admin, reuse its explicit contract and reserved routes.
|
|
15
|
-
Add visitor sign-in where needed. Backend permissions enforce roles and data
|
|
16
|
-
access; host membership alone is not application admin authority.
|
|
17
|
-
- Integrate image selection/upload into content editing through the existing
|
|
18
|
-
Admin media library and the application's selected storage. Follow the pinned Core upload/PUT/commit
|
|
19
|
-
contract and `x-mantle-ref: media_assets` plus `x-mcp-hint: media-image`; store committed asset IDs, resolve permanent variants in the frontend, never use an ID as an image URL. The application's `/mcp/admin` supplies `get_media_upload_policy` → `create_media_upload` → direct HTTP PUT → `commit_media_upload` when its shared media adapter is wired. Follow the installed media authentication contract; host project credentials are not application credentials. Upload capability URLs are temporary,
|
|
20
|
-
not committed public images; anonymous visitors must not gain admin media access.
|
|
21
|
-
- Generate an accessible, responsive public frontend by reading the installed
|
|
22
|
-
generated `web.ts` and REST contracts; browser code on static hosts uses
|
|
23
|
-
same-origin fetch, not a server surface imported into the browser. Do not install the historical standalone
|
|
24
|
-
`@aotter/mantle-web` or claim Core supplies an HTML renderer. The application-owned Fetch entry renders HTML; follow the selected host's static/template restrictions.
|
|
25
|
-
- Public content follows the installed `docs/handbook/guides/public-pages.md`
|
|
26
|
-
(the one list of head tags, JSON-LD, languages, Markdown, sitemap, robots and
|
|
27
|
-
`llms.txt`). For public content on static hosts disable SPA fallback and check unknown URLs
|
|
28
|
-
return 404. Follow the selected host's permanent origin contract. Missing
|
|
29
|
-
final origin blocks origin-dependent checks.
|
|
30
|
-
Structured data must match visible content; do not fabricate facts or promise
|
|
31
|
-
ranking or AI citation.
|
|
32
|
-
- Verify that content publication, edits and deletion update the crawlable page
|
|
33
|
-
and sitemap. If static generation requires a rebuild, explain the publishing
|
|
34
|
-
dependency; a dynamic blog is blocked until that path exists. Do not silently
|
|
35
|
-
substitute build-time sample content for live tenant content.
|
|
36
|
-
|
|
37
|
-
Follow the [local HTTP acceptance](local-testing.md) before reporting local
|
|
38
|
-
readiness: inspect HTML without JavaScript, check metadata/sitemap and anonymous
|
|
39
|
-
draft/permission refusal, and exercise OTP and editing with isolated synthetic
|
|
40
|
-
accounts. Local readiness does not require publication, a hosted tenant or a
|
|
41
|
-
candidate preview. Scripted local acceptance does not replace the installed
|
|
42
|
-
independent acceptance guide, which is required before reporting the pages done.
|
|
43
|
-
Report production email, public origin and real object-store upload/commit checks
|
|
44
|
-
separately when local capabilities cannot exercise them.
|
|
45
|
-
|
|
46
|
-
For an explicitly requested deployment, use the selected host's native workflow
|
|
47
|
-
and any preview it provides. Verify disabled preview capabilities on an already
|
|
48
|
-
authorized live target; synthetic writes and cleanup stay within that scope.
|
|
49
|
-
Pending production checks block a production-readiness claim, not an otherwise
|
|
50
|
-
passing local-development handoff. Preserve preview isolation and permissions;
|
|
51
|
-
report passed, failed and blocked checks for each environment separately.
|