@aotter/mantle 0.1.3 → 0.1.5-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -20
- package/dist/cli/generate-cloudflare.d.ts +7 -0
- package/dist/cli/generate-cloudflare.d.ts.map +1 -0
- package/dist/cli/generate-cloudflare.js +123 -0
- package/dist/cli/generate-cloudflare.js.map +1 -0
- package/dist/cli/generate-project.d.ts +25 -0
- package/dist/cli/generate-project.d.ts.map +1 -0
- package/dist/cli/generate-project.js +282 -0
- package/dist/cli/generate-project.js.map +1 -0
- package/dist/cli/generate-sites.d.ts +8 -0
- package/dist/cli/generate-sites.d.ts.map +1 -0
- package/dist/cli/generate-sites.js +273 -0
- package/dist/cli/generate-sites.js.map +1 -0
- package/dist/cli/generate.d.ts.map +1 -1
- package/dist/cli/generate.js +121 -10
- package/dist/cli/generate.js.map +1 -1
- package/dist/cli/main.d.ts +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +11 -11
- package/dist/cli/skills.js +1 -1
- package/dist/cli/skills.js.map +1 -1
- package/docs/adr/0001-four-atom-manifest-model.md +3 -2
- package/docs/adr/0013-agent-provisioned-consumer-projects.md +4 -4
- package/docs/adr/0016-site-semantic-layer.md +5 -4
- package/docs/adr/0018-core-starters-repository-boundary.md +1 -1
- package/docs/adr/0021-retire-starter-scaffolding.md +1 -1
- package/docs/adr/0026-progressive-generate.md +36 -0
- package/docs/adr/0027-scheduled-procedure-triggers.md +31 -0
- package/docs/adr/0028-schema-ttl-expiry.md +33 -0
- package/docs/adr/README.md +7 -4
- package/docs/agent-prompts.md +41 -78
- package/docs/assets/mantle-admin-developer.png +0 -0
- package/docs/examples/README.md +1 -1
- package/docs/examples/host-chatgpt-sites/README.md +29 -12
- package/docs/examples/host-chatgpt-sites/drizzle/0003_store_instance_id.sql +1 -0
- package/docs/examples/host-chatgpt-sites/drizzle/0004_mantle.sql +10 -0
- package/docs/examples/host-chatgpt-sites/drizzle/0005_mantle.sql +24 -0
- package/docs/examples/host-chatgpt-sites/drizzle/meta/_journal.json +1 -1
- package/docs/examples/host-chatgpt-sites/drizzle/meta/mantle-state.json +60 -0
- package/docs/examples/host-chatgpt-sites/package.json +1 -1
- package/docs/examples/host-chatgpt-sites/public/site.css +1 -1
- package/docs/examples/host-chatgpt-sites/scripts/build.mjs +2 -1
- package/docs/examples/host-chatgpt-sites/scripts/check.mjs +2 -0
- package/docs/examples/host-chatgpt-sites/scripts/migration.mjs +42 -8
- package/docs/examples/host-chatgpt-sites/src/chatgpt-auth.ts +9 -6
- package/docs/examples/host-chatgpt-sites/src/index.ts +1 -1
- package/docs/examples/host-chatgpt-sites/src/web.ts +1 -1
- package/docs/examples/host-local-admin-otp/package.json +2 -2
- package/docs/examples/host-minimal-worker/README.md +5 -5
- package/docs/examples/host-minimal-worker/package.json +2 -2
- package/docs/handbook/{sites → chatgpt-sites}/equipment-checkout.md +1 -1
- package/docs/handbook/chatgpt-sites/generated-app.md +84 -0
- package/docs/handbook/{sites → chatgpt-sites}/index.md +35 -13
- package/docs/handbook/cloudflare/authentication.md +19 -2
- package/docs/handbook/cloudflare/bindings.md +4 -11
- package/docs/handbook/cloudflare/chatgpt-sites.md +6 -6
- package/docs/handbook/cloudflare/conventional-worker.md +7 -3
- package/docs/handbook/cloudflare/deploy-and-operate.md +4 -3
- package/docs/handbook/cloudflare/media-r2.md +2 -2
- package/docs/handbook/concepts/four-atoms.md +1 -1
- package/docs/handbook/concepts/lifecycle-and-locales.md +1 -1
- package/docs/handbook/concepts/mcp-and-agents.md +24 -4
- package/docs/handbook/concepts/procedures-and-triggers.md +1 -1
- package/docs/handbook/concepts/runtime-and-adapters.md +8 -1
- package/docs/handbook/concepts/views.md +1 -1
- package/docs/handbook/guides/agent-setup.md +8 -7
- package/docs/handbook/navigation.json +7 -6
- package/docs/handbook/reference/diagnostics.md +5 -0
- package/docs/handbook/reference/procedure.md +52 -1
- package/docs/handbook/reference/schema.md +27 -1
- package/docs/handbook/reference/surface.md +3 -3
- package/docs/handbook/reference/trigger.md +21 -1
- package/docs/handbook/releases/index.md +81 -1
- package/docs/handbook/start/overview.md +6 -3
- package/docs/handbook/start/project-and-cli.md +103 -119
- package/docs/handbook/start/quickstart-admin.md +6 -4
- package/docs/handbook/start/quickstart-worker.md +17 -11
- package/docs/release-process.md +3 -2
- package/docs/sealed-pipeline-ownership.md +1 -1
- package/docs/skills/develop/SKILL.md +258 -0
- package/docs/skills/integrate/SKILL.md +60 -0
- package/docs/skills/media-gc/SKILL.md +85 -0
- package/docs/skills/plugin/SKILL.md +132 -0
- package/docs/skills/provision/SKILL.md +194 -0
- package/docs/skills/theme/SKILL.md +77 -0
- package/docs/skills/update/SKILL.md +51 -0
- package/docs/spec-only-host-adoption.md +1 -1
- package/package.json +19 -18
- package/skills/README.md +28 -21
- package/skills/develop/SKILL.md +16 -8
- package/skills/install/SKILL.md +103 -128
- package/skills/integrate/SKILL.md +60 -0
- package/skills/media-gc/SKILL.md +2 -2
- package/skills/plugin/SKILL.md +2 -2
- package/skills/provision/SKILL.md +3 -3
- package/skills/theme/SKILL.md +2 -2
- package/skills/update/SKILL.md +2 -2
- package/templates/chatgpt-sites/auth.ts +66 -0
- package/templates/chatgpt-sites/build.mjs +14 -0
- package/templates/chatgpt-sites/env.ts +1 -0
- package/templates/chatgpt-sites/mcp.ts +48 -0
- package/templates/chatgpt-sites/public-mcp.ts +29 -0
- package/templates/chatgpt-sites/smoke-local.mjs +20 -0
- package/skills/.keep +0 -0
- /package/docs/handbook/{sites → chatgpt-sites}/host-reference.md +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"fingerprint": "9ef85f9933e22328140e924045820af6be2a6985ed8ecc19b63a4f2a37826618",
|
|
3
|
+
"schemas": [
|
|
4
|
+
{
|
|
5
|
+
"apiVersion": "cms.mantle.aotter.net/v1",
|
|
6
|
+
"kind": "Schema",
|
|
7
|
+
"metadata": {
|
|
8
|
+
"name": "articles"
|
|
9
|
+
},
|
|
10
|
+
"spec": {
|
|
11
|
+
"title": "文章",
|
|
12
|
+
"schema": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"required": [
|
|
15
|
+
"title"
|
|
16
|
+
],
|
|
17
|
+
"properties": {
|
|
18
|
+
"title": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"title": "標題",
|
|
21
|
+
"maxLength": 160
|
|
22
|
+
},
|
|
23
|
+
"summary": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"title": "摘要",
|
|
26
|
+
"maxLength": 1000
|
|
27
|
+
},
|
|
28
|
+
"body": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"title": "內容",
|
|
31
|
+
"x-mcp-hint": "markdown"
|
|
32
|
+
},
|
|
33
|
+
"coverAssetId": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"title": "封面圖片",
|
|
36
|
+
"x-mantle-ref": "media_assets",
|
|
37
|
+
"x-mcp-hint": "media-image"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"lifecycle": "publishing",
|
|
42
|
+
"uniqueIndexes": [],
|
|
43
|
+
"indexes": [],
|
|
44
|
+
"searchableFields": [],
|
|
45
|
+
"localized": false
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
"appliedMigrationIds": [
|
|
50
|
+
"0001-init",
|
|
51
|
+
"0002-media-assets",
|
|
52
|
+
"0003-pending-media-uploads",
|
|
53
|
+
"0004-native-schema-storage",
|
|
54
|
+
"0005-store-instance-id",
|
|
55
|
+
"0006-managed-runtime-version",
|
|
56
|
+
"0007-schedule-run-observations"
|
|
57
|
+
],
|
|
58
|
+
"lastIndex": 5,
|
|
59
|
+
"canonicalVersion": "0007-schedule-run-observations"
|
|
60
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"mantle-sites-reference","version":"0.0.1","private":true,"type":"module","scripts":{"build":"node scripts/build.mjs","dev":"wrangler dev","generate":"mantle generate","check":"tsc --noEmit","test":"node --experimental-strip-types scripts/check.mjs"},"dependencies":{"@aotter/mantle":"latest","@aotter/mantle-admin":"latest","@aotter/mantle-admin-ui":"latest","@aotter/mantle-cloudflare":"latest","@aotter/mantle-web":"latest","hono":"^4.9.0","micromark":"4.0.2","zod":"^4.0.0"},"devDependencies":{"@cloudflare/workers-types":"*","esbuild":"^0.28.0","typescript":"^5.9.0","wrangler":"^4.0.0"}}
|
|
1
|
+
{"name":"mantle-sites-reference","version":"0.0.1","private":true,"type":"module","scripts":{"build":"node scripts/build.mjs","dev":"wrangler dev","generate":"mantle generate","check":"tsc --noEmit","test":"node --experimental-strip-types scripts/check.mjs","migrate:generate":"node scripts/migration.mjs","migrate:check":"node scripts/migration.mjs --check"},"dependencies":{"@aotter/mantle":"latest","@aotter/mantle-admin":"latest","@aotter/mantle-admin-ui":"latest","@aotter/mantle-cloudflare":"latest","@aotter/mantle-web":"latest","hono":"^4.9.0","micromark":"4.0.2","zod":"^4.0.0"},"devDependencies":{"@cloudflare/workers-types":"*","esbuild":"^0.28.0","typescript":"^5.9.0","wrangler":"^4.0.0"}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
:root{font-family:Inter,"Noto Sans TC",ui-sans-serif,system-ui,sans-serif;color:#172320;background:#f7f8f4;font-synthesis:none}*{box-sizing:border-box}body{margin:0}a{color:inherit;text-decoration:none}a:hover{text-decoration:underline}.topbar{height:78px;max-width:1440px;margin:auto;padding:0 clamp(24px,5vw,80px);display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #d9dfd8}.brand{display:flex;align-items:center;gap:12px;font-size:18px;font-weight:760;letter-spacing:-.04em}.brand em{color:#95aa8c;font-style:normal}.mark{width:37px;height:37px;border-radius:11px;background:#183c30;color:#d9eadb;display:grid;place-items:center;font-size:19px;font-family:Georgia,serif}.topbar nav{display:flex;gap:28px;font-size:14px;color:#43564e}.hero{max-width:1440px;margin:auto;padding:100px clamp(24px,8vw,128px) 116px}.hero.small{padding-bottom:75px}.eyebrow,.feature-index{font-size:11px;letter-spacing:.19em;font-weight:750;color:#68806f}.hero h1,.article-page h1{font-family:Georgia,"Noto Serif TC",serif;font-weight:500;letter-spacing:-.055em;line-height:1.09;margin:26px 0 28px;font-size:clamp(48px,6.1vw,94px)}.dot{color:#a2b888}.hero-copy{max-width:620px;color:#52665a;font-size:18px;line-height:1.8}.actions{display:flex;align-items:center;gap:30px;margin-top:38px}.button{display:inline-flex;justify-content:space-between;gap:44px;min-width:184px;padding:17px 19px;border-radius:7px;color:#fff;background:#214f3c;font-size:14px;font-weight:650}.button:hover{background:#173c2d;text-decoration:none}.text-link{font-size:14px;color:#315640}.feature-grid{max-width:1440px;margin:auto;padding:0 clamp(24px,8vw,128px) 110px;display:grid;grid-template-columns:repeat(3,1fr);gap:18px}.feature-grid>div{border-top:1px solid #bfcac0;padding-top:24px}.feature-grid h2{font-family:Georgia,"Noto Serif TC",serif;font-size:29px;font-weight:500;margin:29px 0 10px}.feature-grid p{font-size:14px;line-height:1.75;color:#597064;max-width:270px}.content-wrap{max-width:1184px;margin:0 auto;padding:0 24px 100px}.article-row{padding:25px 0;border-top:1px solid #cbd5ca;display:grid;grid-template-columns:160px 1fr 36px;gap:24px;align-items:start}.article-row:last-child{border-bottom:1px solid #cbd5ca}.article-number{font-size:12px;color:#708477;margin-top:11px}.article-row h2{font-family:Georgia,"Noto Serif TC",serif;font-size:clamp(24px,3vw,35px);font-weight:500;letter-spacing:-.035em;margin:0 0 8px}.article-row p{font-size:14px;color:#617468;line-height:1.65;margin:0}.article-row>span{color:#739279;font-size:25px}.empty{min-height:220px;padding:52px 0}.empty h2{font-size:27px;font-family:Georgia,"Noto Serif TC",serif;font-weight:500}.empty p{color:#66796c}.pagination{margin:26px 0}.article-page{max-width:900px;padding:75px 30px 140px;margin:auto}.back{font-size:13px;color:#5c7768}.article-page article{margin-top:75px}.article-page h1{font-size:clamp(44px,5vw,75px)}.lede{font-size:21px;line-height:1.75;color:#52685a}.prose{border-top:1px solid #cbd5ca;margin-top:45px;padding-top:28px;line-height:1.95;font-size:17px;color:#33473c}.prose p{margin:0 0 1.2em}
|
|
1
|
+
:root{font-family:Inter,"Noto Sans TC",ui-sans-serif,system-ui,sans-serif;color:#172320;background:#f7f8f4;font-synthesis:none}*{box-sizing:border-box}body{margin:0}a{color:inherit;text-decoration:none}a:hover{text-decoration:underline}.topbar{height:78px;max-width:1440px;margin:auto;padding:0 clamp(24px,5vw,80px);display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #d9dfd8}.brand{display:flex;align-items:center;gap:12px;font-size:18px;font-weight:760;letter-spacing:-.04em}.brand em{color:#95aa8c;font-style:normal}.mark{width:37px;height:37px;border-radius:11px;background:#183c30;color:#d9eadb;display:grid;place-items:center;font-size:19px;font-family:Georgia,serif}.topbar nav{display:flex;gap:28px;font-size:14px;color:#43564e}.hero{max-width:1440px;margin:auto;padding:100px clamp(24px,8vw,128px) 116px}.hero.small{padding-bottom:75px}.eyebrow,.feature-index{font-size:11px;letter-spacing:.19em;font-weight:750;color:#68806f}.hero h1,.article-page h1{font-family:Georgia,"Noto Serif TC",serif;font-weight:500;letter-spacing:-.055em;line-height:1.09;margin:26px 0 28px;font-size:clamp(48px,6.1vw,94px)}.dot{color:#a2b888}.hero-copy{max-width:620px;color:#52665a;font-size:18px;line-height:1.8}.actions{display:flex;align-items:center;gap:30px;margin-top:38px}.button{display:inline-flex;justify-content:space-between;gap:44px;min-width:184px;padding:17px 19px;border-radius:7px;color:#fff;background:#214f3c;font-size:14px;font-weight:650}.button:hover{background:#173c2d;text-decoration:none}.text-link{font-size:14px;color:#315640}.feature-grid{max-width:1440px;margin:auto;padding:0 clamp(24px,8vw,128px) 110px;display:grid;grid-template-columns:repeat(3,1fr);gap:18px}.feature-grid>div{border-top:1px solid #bfcac0;padding-top:24px}.feature-grid h2{font-family:Georgia,"Noto Serif TC",serif;font-size:29px;font-weight:500;margin:29px 0 10px}.feature-grid p{font-size:14px;line-height:1.75;color:#597064;max-width:270px}.content-wrap{max-width:1184px;margin:0 auto;padding:0 24px 100px}.article-row{padding:25px 0;border-top:1px solid #cbd5ca;display:grid;grid-template-columns:160px 1fr 36px;gap:24px;align-items:start}.article-row:last-child{border-bottom:1px solid #cbd5ca}.article-number{font-size:12px;color:#708477;margin-top:11px}.article-row h2{font-family:Georgia,"Noto Serif TC",serif;font-size:clamp(24px,3vw,35px);font-weight:500;letter-spacing:-.035em;margin:0 0 8px}.article-row p{font-size:14px;color:#617468;line-height:1.65;margin:0}.article-row>span{color:#739279;font-size:25px}.empty{min-height:220px;padding:52px 0}.empty h2{font-size:27px;font-family:Georgia,"Noto Serif TC",serif;font-weight:500}.empty p{color:#66796c}.pagination{margin:26px 0}.article-page{max-width:900px;padding:75px 30px 140px;margin:auto}.back{font-size:13px;color:#5c7768}.article-page article{margin-top:75px}.article-page h1{font-size:clamp(44px,5vw,75px)}.lede{font-size:21px;line-height:1.75;color:#52685a}.prose{border-top:1px solid #cbd5ca;margin-top:45px;padding-top:28px;line-height:1.95;font-size:17px;color:#33473c}.prose p{margin:0 0 1.2em}footer{border-top:1px solid #d5ddd3;max-width:1440px;margin:auto;padding:28px clamp(24px,5vw,80px);display:flex;justify-content:space-between;font-size:12px;color:#788b7d}@media(max-width:700px){.topbar{height:68px}.topbar nav{gap:16px;font-size:12px}.hero{padding-top:74px;padding-bottom:70px}.hero-copy{font-size:16px}.feature-grid{grid-template-columns:1fr;gap:26px}.article-row{grid-template-columns:1fr 26px;gap:10px}.article-number{grid-column:1/-1;margin:0}.article-page{padding-top:55px}.article-page article{margin-top:50px}footer{gap:20px}}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { build } from 'esbuild';
|
|
2
|
-
import { cp, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { cp, mkdir, rm } from 'node:fs/promises';
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
|
+
await rm('dist',{recursive:true,force:true});
|
|
4
5
|
execFileSync('node',['node_modules/@aotter/mantle/dist/cli/main.js','generate'],{stdio:'inherit'});
|
|
5
6
|
// Reject older published SDKs that cannot advertise the mounted MCP endpoints.
|
|
6
7
|
execFileSync('node',['node_modules/typescript/bin/tsc','--noEmit'],{stdio:'inherit'});
|
|
@@ -76,6 +76,8 @@ assert.match(publicPage,new RegExp(mediaUrl));
|
|
|
76
76
|
assert.doesNotMatch(publicPage,/href="javascript:/);
|
|
77
77
|
assert.match(publicPage,/<meta property="og:image"/);
|
|
78
78
|
assert.match(publicPage,/<link rel="canonical"/);
|
|
79
|
+
assert.match(publicPage,/<link rel="alternate" type="text\/markdown"/);
|
|
80
|
+
assert.doesNotMatch(publicPage,/<a\b[^>]*href="[^"]+\.md(?:[?#][^"]*)?"/);
|
|
79
81
|
assert.match(publicPage,/application\/ld\+json/);
|
|
80
82
|
assert.match(await (await call(`/articles/${entry.id}.md`)).text(),/Published body from Mantle/);
|
|
81
83
|
assert.match(await (await call('/articles')).text(),new RegExp(entry.id));
|
|
@@ -1,9 +1,43 @@
|
|
|
1
|
-
//
|
|
2
|
-
import { buildSqliteMigrationArtifact,
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
await
|
|
1
|
+
// Append a reviewed D1 migration only after the previous one is applied locally.
|
|
2
|
+
import { buildSqliteMigrationArtifact, renderSqliteManagedMigration } from '@aotter/mantle/runtime';
|
|
3
|
+
import { parseManifestSources, ValidateManifestsUseCase } from '@aotter/mantle/spec';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { readFile, readdir, writeFile } from 'node:fs/promises';
|
|
6
|
+
const statePath='drizzle/meta/mantle-state.json';
|
|
7
|
+
const state=JSON.parse(await readFile(statePath,'utf8'));
|
|
8
|
+
const files=(await readdir('manifests')).filter(name=>/\.ya?ml$/i.test(name)).sort();
|
|
9
|
+
const parsed=parseManifestSources({sources:await Promise.all(files.map(async name=>({sourceId:name,text:await readFile(`manifests/${name}`,'utf8')})))});
|
|
10
|
+
if(!parsed.ok)throw Error(`Invalid manifest: ${parsed.diagnostics.map(d=>d.message).join('; ')}`);
|
|
11
|
+
const validated=ValidateManifestsUseCase.run({parsed:parsed.value});
|
|
12
|
+
if(validated.errorCount)throw Error(`Invalid manifest: ${validated.diagnostics.filter(d=>d.severity==='error').map(d=>d.message).join('; ')}`);
|
|
13
|
+
const target=parsed.value.entries.map(entry=>entry.manifest).filter(manifest=>manifest.kind==='Schema');
|
|
14
|
+
const artifact=await buildSqliteMigrationArtifact(state.schemas,target,{appliedMigrationIds:state.appliedMigrationIds});
|
|
15
|
+
if(artifact.sourceFingerprint!==state.fingerprint)throw Error('The saved migration source fingerprint is invalid. Restore the applied state before generating.');
|
|
16
|
+
if(artifact.destructive)throw Error('Destructive Schema change needs a reviewed migration; no D1 file was written.');
|
|
17
|
+
const unchanged=artifact.sourceFingerprint===artifact.targetFingerprint&&artifact.migrations.length===0;
|
|
18
|
+
const fingerprint=JSON.parse(await readFile('src/storage-fingerprint.json','utf8'));
|
|
19
|
+
if(fingerprint!==state.fingerprint)throw Error('Generated fingerprint and migration source disagree. Restore the reviewed migration state.');
|
|
20
|
+
if(process.argv.includes('--check')){
|
|
21
|
+
if(!unchanged)throw Error('Pending Schema or runtime migration. Run node scripts/migration.mjs and review the new D1 file.');
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
const output=execFileSync('node',['node_modules/wrangler/bin/wrangler.js','d1','execute','mantle-sites-reference-local','--local','--command','SELECT fingerprint FROM _mantle_storage_state WHERE id = 1','--json'],{encoding:'utf8'});
|
|
25
|
+
const active=JSON.parse(output)[0]?.results?.[0]?.fingerprint;
|
|
26
|
+
if(active!==state.fingerprint)throw Error(`Local D1 fingerprint ${active??'(missing)'} does not match the last migration ${state.fingerprint}. Apply pending D1 migrations first.`);
|
|
27
|
+
const versionOutput=execFileSync('node',['node_modules/wrangler/bin/wrangler.js','d1','execute','mantle-sites-reference-local','--local','--command',"SELECT name FROM sqlite_schema WHERE type='table' AND name='_mantle_managed_runtime_state'",'--json'],{encoding:'utf8'});
|
|
28
|
+
const versionTable=JSON.parse(versionOutput)[0]?.results?.[0]?.name;
|
|
29
|
+
const version=versionTable?(JSON.parse(execFileSync('node',['node_modules/wrangler/bin/wrangler.js','d1','execute','mantle-sites-reference-local','--local','--command','SELECT canonical_version FROM _mantle_managed_runtime_state WHERE id = 1','--json'],{encoding:'utf8'}))[0]?.results?.[0]?.canonical_version??null):null;
|
|
30
|
+
if(version!==null&&version!==state.canonicalVersion)throw Error(`Local D1 runtime version ${version} does not match migration source ${state.canonicalVersion}.`);
|
|
31
|
+
if(version===null&&state.canonicalVersion!=='0005-store-instance-id')throw Error('Local D1 is missing the managed runtime version marker. Apply pending migrations first.');
|
|
32
|
+
if(unchanged){console.log('D1 migrations are current.');process.exit(0);}
|
|
33
|
+
const sql=renderSqliteManagedMigration(artifact,{canonicalVersion:state.canonicalVersion});
|
|
34
|
+
const index=state.lastIndex+1;
|
|
35
|
+
const path=`drizzle/${String(index).padStart(4,'0')}_mantle.sql`;
|
|
36
|
+
const journalPath='drizzle/meta/_journal.json';
|
|
37
|
+
const journal=JSON.parse(await readFile(journalPath,'utf8'));
|
|
38
|
+
if(journal.entries.at(-1)?.idx!==state.lastIndex)throw Error('Drizzle journal does not match the saved migration state.');
|
|
39
|
+
await writeFile(path,sql,{flag:'wx'});
|
|
40
|
+
await writeFile(journalPath,JSON.stringify({...journal,entries:[...journal.entries,{idx:index,version:'6',when:Date.now(),tag:`${String(index).padStart(4,'0')}_mantle`,breakpoints:true}]})+'\n');
|
|
41
|
+
await writeFile(statePath,JSON.stringify({fingerprint:artifact.targetFingerprint,canonicalVersion:artifact.targetCanonicalVersion,schemas:target,appliedMigrationIds:[...new Set([...state.appliedMigrationIds,...artifact.migrations.map(m=>m.id)])],lastIndex:index},null,2)+'\n');
|
|
9
42
|
await writeFile('src/storage-fingerprint.json',JSON.stringify(artifact.targetFingerprint)+'\n');
|
|
43
|
+
console.log(`Review ${path}, then apply it with wrangler d1 migrations apply mantle-sites-reference-local --local.`);
|
|
@@ -24,13 +24,16 @@ export function createChatGPTAuth(env: Env): AdminAuth {
|
|
|
24
24
|
basePath:'/api/auth', methods:[],
|
|
25
25
|
async getSession(request) {
|
|
26
26
|
const who=readIdentity(request); if(!who)return null;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
let row=await db.prepare('SELECT * FROM sites_users WHERE id=?').bind(who.id).first<UserRow>();
|
|
28
|
+
if(!row){
|
|
29
|
+
// Claim an invitation before inserting; a stable identity keeps its existing role if its email later changes.
|
|
30
|
+
await db.prepare('UPDATE sites_users SET id=?,name=?,signed_in=1 WHERE email=? AND signed_in=0').bind(who.id,who.name,who.email).run();
|
|
31
|
+
await db.prepare('INSERT OR IGNORE INTO sites_users (id,email,name,role,signed_in) VALUES (?,?,?,?,1)')
|
|
32
|
+
.bind(who.id,who.email,who.name,who.email===env.OWNER_EMAIL?.trim().toLowerCase()?'owner':null).run();
|
|
33
|
+
row=await db.prepare('SELECT * FROM sites_users WHERE id=?').bind(who.id).first<UserRow>();
|
|
34
|
+
}
|
|
32
35
|
if(!row)return null;
|
|
33
|
-
return {session:{id:`sites:${who.id}`},user:{
|
|
36
|
+
return {session:{id:`sites:${who.id}`},user:{id:row.id,email:row.email,name:row.name,role:row.role,roleCurrent:true}};
|
|
34
37
|
},
|
|
35
38
|
getUserRole:roleFor,
|
|
36
39
|
async handler(request) {
|
|
@@ -23,7 +23,7 @@ function assemble(env:Env) {
|
|
|
23
23
|
mountWeb(app,get);
|
|
24
24
|
mountMcp(app,get,auth);
|
|
25
25
|
mountMedia(app,auth,env);
|
|
26
|
-
app.get('/health',async()=>{await get();return Response.json({ok:true,storage:'D1',auth:'ChatGPT Sites'
|
|
26
|
+
app.get('/health',async()=>{await get();return Response.json({ok:true,storage:'D1',auth:'ChatGPT Sites'});});
|
|
27
27
|
app.get('/admin/sign-in',async c=>{
|
|
28
28
|
if(c.req.header('cookie')?.split(';').some(v=>v.trim()==='mantle-sites-signout=1')) {
|
|
29
29
|
c.header('Set-Cookie','mantle-sites-signout=; Path=/admin/sign-in; HttpOnly; SameSite=Strict; Max-Age=0'+(env.PUBLIC_ORIGIN.startsWith('https:')?'; Secure':''));
|
|
@@ -26,7 +26,7 @@ export function mountWeb(app:Hono<{Bindings:Env}>,get:()=>Promise<MantleAdminRun
|
|
|
26
26
|
const title=String(entry.data.title??site.title);
|
|
27
27
|
const image=coverUrl?`<figure class="article-cover"><img src="${escape(coverUrl)}" alt="${escape(cover?.alt||title)}" loading="eager"></figure>`:'';
|
|
28
28
|
const body=micromark(String(entry.data.body??''),{allowDangerousHtml:false,allowDangerousProtocol:false});
|
|
29
|
-
return c.html(shell(`${title}|${site.title}`,renderSeoTagsHtml(seo),`<main class="article-page"><a class="back" href="/articles">← 返回文章</a><article><p class="eyebrow">ARTICLE · ${new Date(entry.updatedAt).toLocaleDateString('zh-TW',{timeZone:'UTC'})}</p><h1>${escape(title)}</h1>${entry.data.summary?`<p class="lede">${escape(entry.data.summary)}</p>`:''}${image}<div class="prose">${body}</div
|
|
29
|
+
return c.html(shell(`${title}|${site.title}`,renderSeoTagsHtml(seo),`<main class="article-page"><a class="back" href="/articles">← 返回文章</a><article><p class="eyebrow">ARTICLE · ${new Date(entry.updatedAt).toLocaleDateString('zh-TW',{timeZone:'UTC'})}</p><h1>${escape(title)}</h1>${entry.data.summary?`<p class="lede">${escape(entry.data.summary)}</p>`:''}${image}<div class="prose">${body}</div></article></main>`));});
|
|
30
30
|
// ponytail: one discovery page is enough for this lab; expose cursor pages when published articles exceed the SDK page limit.
|
|
31
31
|
app.get('/llms.txt',async c=>{const {site,web}=await load();const page=await web.composeLlmsTxt.execute({collections:['articles'],locale:null,site,pathFor:e=>articlePath(e.id)});const body=page?.body??`# ${site.title}\n\n文章索引:${site.origin}/articles\n`;return c.text(body,200,{'Content-Type':'text/plain; charset=utf-8'});});
|
|
32
32
|
// ponytail: first 1000 published URLs; add sitemap index parts only when the catalogue reaches that ceiling.
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
"aws4fetch": "^1.0.20"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@cloudflare/workers-types": "
|
|
24
|
+
"@cloudflare/workers-types": "5.20260904.1",
|
|
25
25
|
"typescript": "^6.0.3",
|
|
26
|
-
"wrangler": "
|
|
26
|
+
"wrangler": "4.129.0"
|
|
27
27
|
},
|
|
28
28
|
"packageManager": "pnpm@9.15.0"
|
|
29
29
|
}
|
|
@@ -9,15 +9,15 @@ Schema/View is example business data; `mantle generate` never invents it.
|
|
|
9
9
|
|
|
10
10
|
For your own project, author package.json, manifests, Worker/provider config
|
|
11
11
|
and TypeScript settings for your requirements. Pin all selected `@aotter/mantle*`
|
|
12
|
-
dependencies to the same intended release.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
disposable copy.
|
|
12
|
+
dependencies to the same intended release. The checked-in `latest` values are
|
|
13
|
+
placeholders for Core's disposable release test; resolve and pin one exact
|
|
14
|
+
version before installing a copied application.
|
|
16
15
|
|
|
17
16
|
Outside the SDK workspace, with Node 22+ and pnpm 9+:
|
|
18
17
|
|
|
19
18
|
```sh
|
|
20
|
-
pnpm
|
|
19
|
+
MANTLE_VERSION=$(pnpm view @aotter/mantle@latest version)
|
|
20
|
+
pnpm add --save-exact "@aotter/mantle@$MANTLE_VERSION" "@aotter/mantle-cloudflare@$MANTLE_VERSION"
|
|
21
21
|
pnpm check
|
|
22
22
|
pnpm dev
|
|
23
23
|
```
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"aws4fetch": "^1.0.20"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"@cloudflare/workers-types": "
|
|
21
|
+
"@cloudflare/workers-types": "5.20260904.1",
|
|
22
22
|
"typescript": "^6.0.3",
|
|
23
|
-
"wrangler": "
|
|
23
|
+
"wrangler": "4.129.0"
|
|
24
24
|
},
|
|
25
25
|
"packageManager": "pnpm@9.15.0"
|
|
26
26
|
}
|
|
@@ -208,7 +208,7 @@ because the underlying host composition or a different provider succeeded.
|
|
|
208
208
|
|
|
209
209
|
```text
|
|
210
210
|
Extend my existing ChatGPT Site with Mantle equipment checkout. Read the matching
|
|
211
|
-
SDK's docs/handbook/sites/index.md, equipment-checkout.md and host-reference.md.
|
|
211
|
+
SDK's docs/handbook/chatgpt-sites/index.md, equipment-checkout.md and host-reference.md.
|
|
212
212
|
Preserve current content, audience and R2 media. This guide is a design, not an
|
|
213
213
|
installed app: implement and test the missing manifests, handlers and routes.
|
|
214
214
|
Start with one identified kit and one fixed half-day slot per request. Include
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Generate a blank Mantle app for ChatGPT Sites, apply D1 migrations, and verify local Admin and MCP.
|
|
3
|
+
---
|
|
4
|
+
# Generate a ChatGPT Sites app
|
|
5
|
+
|
|
6
|
+
`mantle generate --host chatgpt-sites` creates a blank home, a D1-backed Runtime,
|
|
7
|
+
public API and MCP, and Mantle Admin. It adds no article Schema, media bucket, or
|
|
8
|
+
demo content. Edit `src/home.ts` and add your own manifests and handlers as needed.
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
mkdir my-site && cd my-site
|
|
12
|
+
npm init -y
|
|
13
|
+
npm pkg set type=module
|
|
14
|
+
npm install --save-exact @aotter/mantle
|
|
15
|
+
npx mantle generate --host chatgpt-sites
|
|
16
|
+
npm install
|
|
17
|
+
npx mantle generate
|
|
18
|
+
npm run build
|
|
19
|
+
npx wrangler d1 migrations apply my-site --local
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The first generate declares the exact Mantle package versions it needs; the
|
|
23
|
+
second runs after those packages are installed. Review `drizzle/0000_mantle.sql`
|
|
24
|
+
before applying it. The D1 name comes from `wrangler.jsonc`. Generation does not
|
|
25
|
+
apply a migration, provision a Site, or publish one. `npm run build` removes the
|
|
26
|
+
old `dist` before copying the current migrations and Admin assets.
|
|
27
|
+
|
|
28
|
+
For local Admin, copy `.dev.vars.example` to `.dev.vars`, set `OWNER_EMAIL` to
|
|
29
|
+
the email of the ChatGPT account that will own this Site, and keep
|
|
30
|
+
`PUBLIC_ORIGIN` equal to the local URL. Run `npm run dev`.
|
|
31
|
+
The plain local browser has no ChatGPT Sites identity, so it will redirect to
|
|
32
|
+
Sites sign-in and cannot prove owner access. To exercise the identity contract
|
|
33
|
+
locally, set `MANTLE_TEST_OWNER_EMAIL` to the same email and run
|
|
34
|
+
`npm run smoke:local`. That script refuses non-loopback
|
|
35
|
+
targets. Its identity headers are a local simulation; the production Worker
|
|
36
|
+
contains no identity-injection path and must run behind Sites' trusted
|
|
37
|
+
identity-stripping dispatcher. The generated Wrangler config disables the
|
|
38
|
+
independent `workers.dev` URL; do not add a direct Worker route. A passing
|
|
39
|
+
local smoke does not verify a deployed ChatGPT sign-in.
|
|
40
|
+
|
|
41
|
+
Without `OWNER_EMAIL`, sign-in grants no Mantle staff role. If the intended
|
|
42
|
+
owner signed in before configuration, their existing `sites_users` row remains
|
|
43
|
+
unprivileged. Configure the owner email and deliberately assign `role='owner'`
|
|
44
|
+
to that account's exact email and `chatgpt:` ID in D1, or reset a disposable
|
|
45
|
+
local database and sign in again. Mantle never gives ownership to the first
|
|
46
|
+
visitor.
|
|
47
|
+
|
|
48
|
+
The anonymous public MCP endpoint is `/api/mcp`; it discovers only public
|
|
49
|
+
manifest capabilities. `/api/mcp/staff` uses the current Sites browser session
|
|
50
|
+
and a Mantle staff role. Admin advertises these endpoints, and Admin WebMCP
|
|
51
|
+
works in supporting browsers. The staff endpoint is not a remote OAuth MCP
|
|
52
|
+
connector. A remote agent needs a separately implemented verified OAuth flow.
|
|
53
|
+
|
|
54
|
+
When a Schema changes, run `npx mantle generate --check` to detect drift, then
|
|
55
|
+
`npx mantle generate`. Review the newly appended SQL and apply pending D1
|
|
56
|
+
migrations before deploying code that expects the new Schema. Previous
|
|
57
|
+
migration files remain untouched; an unapplied migration keeps the old database
|
|
58
|
+
fingerprint, and managed boot refuses to use an older database. Sites owns
|
|
59
|
+
`.openai/hosting.json` project metadata and production D1 migration delivery.
|
|
60
|
+
|
|
61
|
+
Replacing a Schema's `uniqueIndexes` tuple requires an explicit reviewed
|
|
62
|
+
migration. With the old local D1 migration applied, edit the Manifest and run
|
|
63
|
+
`npx mantle generate --review-unique-indexes --check` to inspect the proposed
|
|
64
|
+
source/target fingerprints, affected tuples and checksum without writing. Then
|
|
65
|
+
run `npx mantle generate --review-unique-indexes`, review the new
|
|
66
|
+
`drizzle/meta/000N_mantle.review.json` and matching SQL, and apply that SQL
|
|
67
|
+
through the normal Sites D1 migration path. The CLI checks **local D1 only**
|
|
68
|
+
for duplicate target tuples and reports a count and sample before writing;
|
|
69
|
+
inspect production data separately before applying there. SQLite also refuses
|
|
70
|
+
to create the new unique index if production has conflicts; do
|
|
71
|
+
not deploy the target Worker until the migration has succeeded. Old Workers
|
|
72
|
+
cannot safely reactivate after the old uniqueness guarantee is removed.
|
|
73
|
+
This option is limited to managed Sites SQLite and unique tuple changes;
|
|
74
|
+
field conversions and runtime-managed CF boot still reject destructive
|
|
75
|
+
changes.
|
|
76
|
+
|
|
77
|
+
For a complete article, media, and publishing example, use the
|
|
78
|
+
[Sites reference](../../examples/host-chatgpt-sites/README.md).
|
|
79
|
+
|
|
80
|
+
## Source
|
|
81
|
+
|
|
82
|
+
- [Sites generator](../../../packages/mantle/src/cli/generate-sites.ts)
|
|
83
|
+
- [Generated Sites templates](../../../packages/mantle/templates/chatgpt-sites)
|
|
84
|
+
- [Runnable Sites reference](../../examples/host-chatgpt-sites/README.md)
|
|
@@ -16,6 +16,10 @@ repository. Sites owns hosting, visitor access, and ChatGPT sign-in; Mantle owns
|
|
|
16
16
|
the content model, staff roles, and publishing workflow. For platform access and
|
|
17
17
|
sharing options, see [OpenAI's Sites documentation](https://learn.chatgpt.com/docs/sites).
|
|
18
18
|
|
|
19
|
+
For a new project, start with the [blank generated app](./generated-app.md).
|
|
20
|
+
The article reference below demonstrates a complete editorial Site with R2
|
|
21
|
+
media after you choose that content model.
|
|
22
|
+
|
|
19
23
|
## What you get
|
|
20
24
|
|
|
21
25
|
| Capability | In the Mantle Sites reference |
|
|
@@ -34,12 +38,17 @@ boundaries.
|
|
|
34
38
|
|
|
35
39
|
## Start with the supported SDK
|
|
36
40
|
|
|
37
|
-
You need Node.js 22
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
You need Node.js 22+ and Sites access with D1. Start with the
|
|
42
|
+
[blank generated app](./generated-app.md) to choose your own content model;
|
|
43
|
+
it requires no R2 bucket. The article reference also needs R2 for cover
|
|
44
|
+
images. Build and smoke-test locally before deployment, and keep your
|
|
45
|
+
application outside the SDK checkout.
|
|
46
|
+
|
|
47
|
+
## Follow the article reference when it fits
|
|
41
48
|
|
|
42
|
-
|
|
49
|
+
The steps below apply to the optional prebuilt article example. A new blank
|
|
50
|
+
generated Site has no article Schema, media bucket or content; add those only
|
|
51
|
+
when your requirements call for them.
|
|
43
52
|
|
|
44
53
|
1. **Prepare the application.** Follow the
|
|
45
54
|
[runnable reference](../../examples/host-chatgpt-sites/README.md#install-and-run).
|
|
@@ -56,7 +65,8 @@ a working example; adapt its fields and lifecycle to your team's content.
|
|
|
56
65
|
Markdown body. Upload an image in the media library and select it as the
|
|
57
66
|
article's cover. The draft's public article URL should return 404.
|
|
58
67
|
5. **Publish and read.** Publish the article and open `/articles`. Follow its
|
|
59
|
-
link to check the body and cover, then
|
|
68
|
+
link to check the body and cover, then confirm the page head advertises
|
|
69
|
+
its `.md` version with `rel="alternate"` and `type="text/markdown"`. For a public
|
|
60
70
|
Site, repeat this while signed out. A restricted Site still enforces its
|
|
61
71
|
audience settings before visitors reach these pages.
|
|
62
72
|
6. **Verify control.** Unpublish the article and confirm its detail page returns
|
|
@@ -67,23 +77,34 @@ For later content edits, return to Mantle Admin. Changes to the application's
|
|
|
67
77
|
schema, code, or deployment settings follow the reference's migration and Sites
|
|
68
78
|
deployment workflow.
|
|
69
79
|
|
|
80
|
+
## Maintain content with an agent
|
|
81
|
+
|
|
82
|
+
The owner can use Admin WebMCP in a browser that supports it to let an agent
|
|
83
|
+
discover staff tools for drafts, publishing, and other permitted operations.
|
|
84
|
+
Open `/admin/dev/docs/webmcp` to see that browser surface and
|
|
85
|
+
`/admin/dev/docs/mcp` to see the Site's `/api/mcp` public read-only endpoint and `/api/mcp/staff`
|
|
86
|
+
staff endpoint. The latter uses the current Sites browser session and checks
|
|
87
|
+
the Mantle staff role on each request. It is useful to same-origin browser
|
|
88
|
+
code, but is not a remote OAuth MCP connector for a desktop agent. A remote
|
|
89
|
+
staff connector needs its own verified OAuth flow; see the [host reference](./host-reference.md#remote-mcp-is-a-separate-gate).
|
|
90
|
+
|
|
70
91
|
## Ask your agent to set it up
|
|
71
92
|
|
|
72
|
-
Install the Mantle
|
|
73
|
-
|
|
93
|
+
Install the Mantle skill and exact SDK version, then describe your audience
|
|
94
|
+
and content requirements:
|
|
74
95
|
|
|
75
96
|
```text
|
|
76
97
|
Build a content site with ChatGPT Sites and Mantle. Read
|
|
77
|
-
handbook/
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
sign-in
|
|
98
|
+
handbook/chatgpt-sites/generated-app.md from the installed Mantle docs, and
|
|
99
|
+
start from the blank generated Site when that CLI supports it. Use the article
|
|
100
|
+
reference only if its content model matches my requirements. Include ChatGPT
|
|
101
|
+
sign-in and Mantle staff roles; add cover uploads only if I request media.
|
|
81
102
|
Run the local checks and show me a draft-to-publish walkthrough before
|
|
82
103
|
deploying through Sites. Keep remote staff OAuth MCP outside this scope.
|
|
83
104
|
```
|
|
84
105
|
|
|
85
106
|
For business rules beyond content, add: "use custom `ref` handlers and
|
|
86
|
-
application-owned tables as described in handbook/sites/equipment-checkout.md."
|
|
107
|
+
application-owned tables as described in handbook/chatgpt-sites/equipment-checkout.md."
|
|
87
108
|
|
|
88
109
|
## Beyond content: operational workflows
|
|
89
110
|
|
|
@@ -100,6 +121,7 @@ evidence from the equipment and Slack work still required in your application.
|
|
|
100
121
|
## Integration details
|
|
101
122
|
|
|
102
123
|
- [Host reference](./host-reference.md): identity, migrations, R2 routes, MCP, and deployment checks.
|
|
124
|
+
- [Generated blank app](./generated-app.md): local composition, D1 migration, Admin, and MCP.
|
|
103
125
|
- [Equipment checkout](./equipment-checkout.md): custom business rules and external API delivery without payments.
|
|
104
126
|
- [Runnable application](../../examples/host-chatgpt-sites/README.md): installation, build artifacts, and smoke tests.
|
|
105
127
|
- [Conventional Cloudflare Worker](../cloudflare/conventional-worker.md): the separate path for a Worker you deploy directly.
|
|
@@ -7,11 +7,28 @@ Conventional Auth is chosen by one variable, `MANTLE_AUTH_MODE`, and fails close
|
|
|
7
7
|
|
|
8
8
|
## Local Admin: email OTP
|
|
9
9
|
|
|
10
|
-
The
|
|
10
|
+
The generated CF app writes `.dev.vars.example`. Copy it to `.dev.vars` for
|
|
11
|
+
local use, then set `MANTLE_AUTH_MODE=local-otp`, the real `ADMIN_EMAIL`, a
|
|
12
|
+
random `BETTER_AUTH_SECRET`, and the exact loopback `PUBLIC_ORIGIN`. It configures
|
|
13
|
+
its email-OTP auth factory only for a
|
|
14
|
+
loopback origin. If you author the Worker yourself, pass `auth` to
|
|
15
|
+
`createMantleWorker` with `email-otp`, `ConsoleEmailSender`, and
|
|
16
|
+
`bootstrapOwner.match: "email"`; that factory does not read the conventional
|
|
17
|
+
mode matrix. The one-time code is printed in wrangler logs. See
|
|
18
|
+
[Quickstart: local Admin](../start/quickstart-admin.md). `ConsoleEmailSender`
|
|
19
|
+
is for `wrangler dev` only; production needs a real sender. Match
|
|
20
|
+
`PUBLIC_ORIGIN` to wrangler's actual `127.0.0.1:8787` origin or Better Auth
|
|
21
|
+
will reject OTP with `INVALID_ORIGIN`.
|
|
22
|
+
|
|
23
|
+
For production, the generated Worker's local OTP branch is disabled outside
|
|
24
|
+
loopback. Configure the conventional `self-managed` or `hosted` Auth mode,
|
|
25
|
+
including its required provider credentials and `ADMIN_GITHUB_LOGIN`, instead
|
|
26
|
+
of editing the CLI-owned Worker. An application that needs production email
|
|
27
|
+
delivery should author its own Worker Auth factory and real `EmailSender`.
|
|
11
28
|
|
|
12
29
|
With `auth` set, the mode matrix below is not read. Core still owns `/admin` and `/api/auth/*`.
|
|
13
30
|
|
|
14
|
-
For production email OTP, keep the custom `createAuth()` factory, replace
|
|
31
|
+
For production email OTP in a manually authored Worker, keep the custom `createAuth()` factory, replace
|
|
15
32
|
`ConsoleEmailSender` with the application's production `EmailSender`, and keep
|
|
16
33
|
`bootstrapOwner: { match: "email", value: <owner email> }`. Store the sender
|
|
17
34
|
credentials and `BETTER_AUTH_SECRET` as Worker secrets. If the application has
|
|
@@ -186,26 +186,19 @@ the interface is one `record(event)` method.
|
|
|
186
186
|
"triggers": { "crons": ["*/5 * * * *"] }
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
-
A `
|
|
189
|
+
A `schedule` Trigger declares the Procedure, and the Worker export reuses the same runtime as `fetch`:
|
|
190
190
|
|
|
191
191
|
```ts
|
|
192
|
-
import { bindMantle } from "../.mantle/generated/mantle.js";
|
|
193
|
-
|
|
194
192
|
const worker = createMantleWorker<Env>({ plan, handlers });
|
|
195
193
|
|
|
196
194
|
export default {
|
|
197
195
|
fetch: worker.fetch,
|
|
198
|
-
|
|
199
|
-
const mantle = bindMantle(await worker.getRuntime(env));
|
|
200
|
-
const result = await mantle.procedures.sweepExpiredOrders(
|
|
201
|
-
{ now: Date.now() },
|
|
202
|
-
{ user: null, staff: null, env, waitUntil: (p) => ctx.waitUntil(p) },
|
|
203
|
-
);
|
|
204
|
-
if (!result.ok) throw new Error(`sweep failed: ${result.diagnostic.code}`);
|
|
205
|
-
},
|
|
196
|
+
scheduled: worker.scheduled,
|
|
206
197
|
} satisfies ExportedHandler<Env>;
|
|
207
198
|
```
|
|
208
199
|
|
|
200
|
+
Register the exact enabled `cron` expression in `wrangler.jsonc`; Mantle does not create provider triggers. The Procedure takes `{}` input and sees `ctx.schedule.id` for retry-safe idempotency. See [Trigger](../reference/trigger.md#schedule-source).
|
|
201
|
+
|
|
209
202
|
## Durable Objects (application-owned)
|
|
210
203
|
|
|
211
204
|
Durable Objects are not a Core primitive. Use one as an application-owned coordinator when a decision must be serialized, for example reserving inventory exactly once. Export the class from the Worker entry, then bind and migrate it:
|
|
@@ -4,26 +4,26 @@ description: "Mantle on ChatGPT Sites now has its own integration guide."
|
|
|
4
4
|
# Mantle on ChatGPT Sites
|
|
5
5
|
|
|
6
6
|
ChatGPT Sites has a dedicated section in the Mantle handbook. Start with
|
|
7
|
-
[Mantle on ChatGPT Sites](../sites/index.md) for installation, supported
|
|
7
|
+
[Mantle on ChatGPT Sites](../chatgpt-sites/index.md) for installation, supported
|
|
8
8
|
capabilities, and your first published article. This page preserves earlier
|
|
9
9
|
links to the host checklist.
|
|
10
10
|
|
|
11
11
|
## First deployment
|
|
12
12
|
|
|
13
|
-
See [the host deployment checklist](../sites/host-reference.md#first-deployment).
|
|
13
|
+
See [the host deployment checklist](../chatgpt-sites/host-reference.md#first-deployment).
|
|
14
14
|
|
|
15
15
|
## Media with only an R2 binding
|
|
16
16
|
|
|
17
|
-
See [R2 media integration](../sites/host-reference.md#media-with-only-an-r2-binding).
|
|
17
|
+
See [R2 media integration](../chatgpt-sites/host-reference.md#media-with-only-an-r2-binding).
|
|
18
18
|
|
|
19
19
|
## Remote MCP is a separate gate
|
|
20
20
|
|
|
21
|
-
See [MCP support and authentication](../sites/host-reference.md#remote-mcp-is-a-separate-gate).
|
|
21
|
+
See [MCP support and authentication](../chatgpt-sites/host-reference.md#remote-mcp-is-a-separate-gate).
|
|
22
22
|
|
|
23
23
|
## Capability boundaries
|
|
24
24
|
|
|
25
|
-
See [host capability boundaries](../sites/host-reference.md#capability-boundaries).
|
|
25
|
+
See [host capability boundaries](../chatgpt-sites/host-reference.md#capability-boundaries).
|
|
26
26
|
|
|
27
27
|
## Source
|
|
28
28
|
|
|
29
|
-
See [integration sources](../sites/host-reference.md#source).
|
|
29
|
+
See [integration sources](../chatgpt-sites/host-reference.md#source).
|
|
@@ -3,7 +3,10 @@ description: Assemble a Mantle Worker with createMantleWorker, add handlers and
|
|
|
3
3
|
---
|
|
4
4
|
# The conventional Worker
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
New applications can start with `mantle generate --host cf`, which writes a
|
|
7
|
+
blank home, Worker entry and selected Admin/API/MCP wiring. This page covers
|
|
8
|
+
the lower-level `createMantleWorker` facade for applications that author their
|
|
9
|
+
own Worker entry: its options, extension seam, readiness and reserved paths.
|
|
7
10
|
|
|
8
11
|
## Minimal entry
|
|
9
12
|
|
|
@@ -14,7 +17,7 @@ import { plan } from "../.mantle/generated/mantle.js";
|
|
|
14
17
|
export default createMantleWorker({ plan, cacheScope: "my-site-production" });
|
|
15
18
|
```
|
|
16
19
|
|
|
17
|
-
`plan` is the sealed plan that `mantle generate` writes to `.mantle/generated/mantle.ts`. With only `plan`,
|
|
20
|
+
`plan` is the sealed plan that `mantle generate` writes to `.mantle/generated/mantle.ts`. With only `plan`, this manually assembled Worker serves public View REST, HTTP Triggers, Admin, Auth, OAuth and MCP. It renders no public pages and `/` is a 404; see [Public web](./public-web.md). A newly generated full CF project wires a separate editable blank home. Local Admin with email OTP replaces Auth construction; see [Quickstart: local Admin](../start/quickstart-admin.md). Admin still requires the `ASSETS` binding.
|
|
18
21
|
|
|
19
22
|
## Options
|
|
20
23
|
|
|
@@ -64,9 +67,10 @@ If `auth.ready` rejects, the memoized assembly is evicted so the next request re
|
|
|
64
67
|
const worker = createMantleWorker<Env>({ plan });
|
|
65
68
|
worker.fetch(request, env, ctx); // HTTP entry
|
|
66
69
|
worker.getRuntime(env); // the runtime fetch uses, resolved after auth.ready
|
|
70
|
+
worker.scheduled(controller, env, ctx); // declared scheduled Procedure Triggers
|
|
67
71
|
```
|
|
68
72
|
|
|
69
|
-
Queue
|
|
73
|
+
Queue handlers call `worker.getRuntime(env)` and then generated `bindMantle(runtime)` so they reuse the assembled runtime. Scheduled Procedure Triggers use `worker.scheduled` with expressions registered in Wrangler. See [Bindings](./bindings.md#cron-triggers), [Trigger](../reference/trigger.md#schedule-source), and [Deferred hooks](./deferred-hooks-queues.md).
|
|
70
74
|
|
|
71
75
|
## Readiness rule
|
|
72
76
|
|
|
@@ -10,7 +10,7 @@ This page is the production checklist for a Mantle Worker on Cloudflare: what to
|
|
|
10
10
|
- Pin every `@aotter/mantle*` package to one exact version and commit the lockfile. Install with `pnpm install --frozen-lockfile` (or `npm ci`) from then on. See [Project and CLI](../start/project-and-cli.md).
|
|
11
11
|
- Set `PUBLIC_ORIGIN` to the real HTTPS origin, without a trailing slash. It drives canonical URLs, `.md` mirrors, `llms.txt`, the MCP resource and the OAuth callback. If a static documentation build also emits absolute URLs, give it the same value.
|
|
12
12
|
- Set the production D1 `database_id` (and `account_id` if your deployment needs it) in `wrangler.jsonc`. A local `database_name` is not a production identifier, and local D1 is not production data.
|
|
13
|
-
- Choose production Auth:
|
|
13
|
+
- Choose production Auth: a generated CF app uses `MANTLE_AUTH_MODE=self-managed` or `hosted` with `ADMIN_GITHUB_LOGIN` and the matching provider credentials. Local `local-otp` works only on loopback. If you author your own Worker Auth factory, replace console email delivery with a real `EmailSender`. Store secrets with `wrangler secret put`. See [Authentication](./authentication.md).
|
|
14
14
|
- Keep `compatibility_flags: ["nodejs_compat", "global_fetch_strictly_public"]`.
|
|
15
15
|
- Enable observability:
|
|
16
16
|
|
|
@@ -47,7 +47,8 @@ wrangler deploy
|
|
|
47
47
|
|
|
48
48
|
## Post-deploy verification
|
|
49
49
|
|
|
50
|
-
Probe the deployed origin, not `wrangler dev
|
|
50
|
+
Probe the deployed origin, not `wrangler dev`. Run only the probes for surfaces
|
|
51
|
+
the application mounts and configures:
|
|
51
52
|
|
|
52
53
|
| Probe | Expect |
|
|
53
54
|
|---|---|
|
|
@@ -55,7 +56,7 @@ Probe the deployed origin, not `wrangler dev`:
|
|
|
55
56
|
| `GET /<locale>/<segment>/<slug>` and `GET /<locale>/<segment>/<slug>.md` | `200` HTML and Markdown for a published entry |
|
|
56
57
|
| `GET /llms.txt`, `GET /sitemap.xml`, `GET /robots.txt` | `200` |
|
|
57
58
|
| `GET /<locale>/<segment>/does-not-exist` | `404` from your `notFoundRenderer` |
|
|
58
|
-
| `
|
|
59
|
+
| `POST /mcp/staff` without credentials | `401` with `WWW-Authenticate`; `GET /mcp` is `405` |
|
|
59
60
|
| `GET /admin` | Sign-in page; sign in with the `ADMIN_GITHUB_LOGIN` account |
|
|
60
61
|
|
|
61
62
|
Then check the cache: a second anonymous `GET` of a public page should show `cf-cache-status: HIT`; publish a change in Admin and the next request should be a `MISS`. Sample latency with:
|