@aotter/mantle 0.1.4 → 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.
Files changed (67) hide show
  1. package/README.md +16 -16
  2. package/dist/cli/generate-cloudflare.d.ts +7 -0
  3. package/dist/cli/generate-cloudflare.d.ts.map +1 -0
  4. package/dist/cli/generate-cloudflare.js +123 -0
  5. package/dist/cli/generate-cloudflare.js.map +1 -0
  6. package/dist/cli/generate-project.d.ts +25 -0
  7. package/dist/cli/generate-project.d.ts.map +1 -0
  8. package/dist/cli/generate-project.js +282 -0
  9. package/dist/cli/generate-project.js.map +1 -0
  10. package/dist/cli/generate-sites.d.ts +8 -0
  11. package/dist/cli/generate-sites.d.ts.map +1 -0
  12. package/dist/cli/generate-sites.js +273 -0
  13. package/dist/cli/generate-sites.js.map +1 -0
  14. package/dist/cli/generate.d.ts.map +1 -1
  15. package/dist/cli/generate.js +121 -10
  16. package/dist/cli/generate.js.map +1 -1
  17. package/dist/cli/main.d.ts +1 -1
  18. package/dist/cli/main.d.ts.map +1 -1
  19. package/dist/cli/main.js +10 -10
  20. package/docs/adr/0021-retire-starter-scaffolding.md +1 -1
  21. package/docs/adr/0026-progressive-generate.md +36 -0
  22. package/docs/adr/0027-scheduled-procedure-triggers.md +31 -0
  23. package/docs/adr/0028-schema-ttl-expiry.md +33 -0
  24. package/docs/adr/README.md +5 -2
  25. package/docs/agent-prompts.md +40 -78
  26. package/docs/examples/host-chatgpt-sites/README.md +1 -1
  27. package/docs/examples/host-chatgpt-sites/drizzle/0004_mantle.sql +10 -0
  28. package/docs/examples/host-chatgpt-sites/drizzle/0005_mantle.sql +24 -0
  29. package/docs/examples/host-chatgpt-sites/drizzle/meta/_journal.json +1 -1
  30. package/docs/examples/host-chatgpt-sites/drizzle/meta/mantle-state.json +60 -0
  31. package/docs/examples/host-chatgpt-sites/package.json +1 -1
  32. package/docs/examples/host-chatgpt-sites/scripts/migration.mjs +42 -8
  33. package/docs/examples/host-chatgpt-sites/src/chatgpt-auth.ts +9 -6
  34. package/docs/examples/host-local-admin-otp/package.json +2 -2
  35. package/docs/examples/host-minimal-worker/package.json +2 -2
  36. package/docs/handbook/chatgpt-sites/generated-app.md +84 -0
  37. package/docs/handbook/chatgpt-sites/index.md +21 -11
  38. package/docs/handbook/cloudflare/authentication.md +19 -2
  39. package/docs/handbook/cloudflare/bindings.md +4 -11
  40. package/docs/handbook/cloudflare/conventional-worker.md +7 -3
  41. package/docs/handbook/cloudflare/deploy-and-operate.md +1 -1
  42. package/docs/handbook/concepts/runtime-and-adapters.md +7 -0
  43. package/docs/handbook/guides/agent-setup.md +3 -3
  44. package/docs/handbook/navigation.json +3 -2
  45. package/docs/handbook/reference/diagnostics.md +5 -0
  46. package/docs/handbook/reference/procedure.md +52 -1
  47. package/docs/handbook/reference/schema.md +27 -1
  48. package/docs/handbook/reference/surface.md +3 -3
  49. package/docs/handbook/reference/trigger.md +21 -1
  50. package/docs/handbook/releases/index.md +55 -0
  51. package/docs/handbook/start/overview.md +5 -2
  52. package/docs/handbook/start/project-and-cli.md +103 -119
  53. package/docs/handbook/start/quickstart-admin.md +6 -4
  54. package/docs/handbook/start/quickstart-worker.md +17 -8
  55. package/docs/skills/develop/SKILL.md +14 -6
  56. package/docs/skills/integrate/SKILL.md +60 -0
  57. package/package.json +18 -17
  58. package/skills/README.md +3 -1
  59. package/skills/develop/SKILL.md +14 -6
  60. package/skills/install/SKILL.md +100 -137
  61. package/skills/integrate/SKILL.md +60 -0
  62. package/templates/chatgpt-sites/auth.ts +66 -0
  63. package/templates/chatgpt-sites/build.mjs +14 -0
  64. package/templates/chatgpt-sites/env.ts +1 -0
  65. package/templates/chatgpt-sites/mcp.ts +48 -0
  66. package/templates/chatgpt-sites/public-mcp.ts +29 -0
  67. package/templates/chatgpt-sites/smoke-local.mjs +20 -0
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: mantle
3
- description: Author a new Mantle application directly from version-matched SDK docs, or continue an existing project. Use when asked to install Mantle, build a Mantle application, or open a Mantle repository.
3
+ description: Set up a new Mantle application or continue an existing one using the installed SDK's matching instructions.
4
4
  metadata:
5
5
  source: "@aotter/mantle"
6
6
  sourcePath: skills/install/SKILL.md
@@ -11,139 +11,102 @@ metadata:
11
11
 
12
12
  # Mantle
13
13
 
14
- Mantle is an embeddable manifest engine. The application owns its source and
15
- provider configuration. There is no Starter/type picker or `mantle create`.
16
- Do not use the SDK checkout as the application, copy an old Starter tree, or
17
- turn `generate` into implicit scaffolding.
18
- A same-version `mantle-starters` tag is not required; do not wait for one.
19
-
20
- ## Locate the version-matched instructions
21
-
22
- `npx skills add aotter/mantle` installs this brief, not the SDK,
23
- a project, or the handbook. Read the path printed by the installer (Codex's
24
- project-local path is `.agents/skills/mantle/SKILL.md`); `metadata.sourcePath`
25
- is repository provenance, not a consumer path. A repository install follows
26
- the selected Git ref and does not pin an npm release.
27
-
28
- After selecting the host and exact version, install `@aotter/mantle` locally
29
- with the selected optional packages. All `docs/...` paths below then mean
30
- `node_modules/@aotter/mantle/docs/...`; package skills are under
31
- `node_modules/@aotter/mantle/skills/`. Read that package's
32
- `skills/install/SKILL.md` before authoring: it supersedes this bootstrap copy.
33
- Use the new application's installation, never another project's `node_modules`
34
- or an SDK checkout packing harness, to resolve these paths.
35
- Before reading a handbook page, verify the version in that application's
36
- `node_modules/@aotter/mantle/package.json` against its lockfile and confirm
37
- the page exists under the same package directory. If the package is missing or
38
- the versions differ, fix the installation first; do not substitute a nearby
39
- checkout's docs or another project's `node_modules`.
40
- If the user requested a prerelease, resolve that channel explicitly; `latest`
41
- need not contain prerelease features. Never mix versioned npm code with branch
42
- docs. No local CLI exists until the package is installed.
43
-
44
- ## New application
45
-
46
- 1. Determine the actual host and required surfaces from the request. Reuse an
47
- existing application when available; otherwise work in its own directory.
48
- Do not assume Cloudflare, public HTML or Admin is required. Check Node 22+
49
- and pnpm 9+ for these SDK examples. A ChatGPT Site is not a conventional
50
- Cloudflare Worker deployment; use the installed
51
- `docs/handbook/chatgpt-sites/index.md` integration guide and
52
- `docs/examples/host-chatgpt-sites/` runnable reference when selected.
53
- 2. Choose the requested exact SDK version, or resolve the intended release
54
- channel once. Pin all selected `@aotter/mantle*` dependencies to that same
55
- version. Install only the adapter/optional packages the application needs.
56
- If a global scope registry overrides public npmjs, use a project-owned
57
- `.npmrc` with `@aotter:registry=https://registry.npmjs.org/`.
58
- 3. Use the host and surfaces already established in step 1; ask only for
59
- information still missing. Read `docs/handbook/start/overview.md` and
60
- `docs/handbook/reference/features.md`, then select the matching path:
61
- - Spec + generate / embed Runtime — `docs/handbook/start/project-and-cli.md`.
62
- - Adapter without Admin — `docs/examples/host-minimal-worker/`.
63
- - Opt-in Admin / Dev UI — only when a human needs a console: interview
64
- the bootstrap owner email, then `docs/examples/host-local-admin-otp/`
65
- (`pnpm install && pnpm generate && pnpm dev`, `/admin/sign-in`, OTP
66
- in wrangler logs). Admin needs `@aotter/mantle-admin`,
67
- `@aotter/mantle-admin-ui`, wrangler `ASSETS` on `./public`, and
68
- `createAuth` email-otp + `ConsoleEmailSender`. Do not Vite-build Admin.
69
- - ChatGPT Sites with Admin/D1/R2 — follow
70
- `docs/examples/host-chatgpt-sites/`, not the email-OTP Worker example.
71
- Copy it outside the SDK checkout, pin its `@aotter/mantle*` dependencies
72
- to the selected exact version, then `npm install`,
73
- `npx mantle validate --phase deploy`, `npm run generate`, `npm run check`,
74
- `npx wrangler d1 migrations apply DB --local`,
75
- `npm run dev -- --port 4174`, and `npm test` in a second terminal.
76
- Preserve its Sites-owned identity ingress and hosting manifest; author the
77
- user's Schema/View/Procedure/Trigger, then review migrations, media policy
78
- and the local/production smoke gates. Sites provisions and deploys; never
79
- `wrangler deploy` a Site. Request both D1 and R2 when uploads are in scope.
80
- Browser Admin WebMCP and Sites-session `/api/mcp/staff` do not enable remote staff OAuth MCP.
81
- At handoff, show the owner where Admin WebMCP and the public/staff MCP
82
- endpoints appear, and state which are usable from the browser versus a
83
- remote connector.
84
- - ChatGPT Sites with custom business rules or an external callback — the
85
- runnable reference covers builtin content only. For application-owned
86
- operational state, `handler: { kind: ref }` Procedures, staff-only SQL
87
- Views, staff MCP Triggers with `requires.auth`, and outbound webhooks
88
- called from handler code, follow
89
- `docs/handbook/chatgpt-sites/equipment-checkout.md`. It is an implementation
90
- guide, not a shipped app: keep Mantle-owned Schema tables and
91
- application-owned tables separate, and give every application table a
92
- reviewed migration.
93
- - Grammar — `docs/examples/README.md`. Copy `builtin-*` Manifests directly.
94
- Read `cf-primitives-*` when the request needs Durable Objects, Queues,
95
- cron, payment-provider callbacks, or API-key and entitlement guards;
96
- those carry `ref` handlers and are not Builder-ingestible.
97
- None of these is a template to install wholesale. Other hosts use the
98
- embedded adapter guides. Author package scripts, manifests, entry and
99
- configuration for the user's requirements. No default notes model, home
100
- page, icon, launch metadata or visitor frontend is required.
101
- 4. Compile and verify using the application's commands. The fundamental CLI
102
- sequence is:
103
-
104
- ```sh
105
- pnpm exec mantle generate
106
- pnpm exec mantle generate --check
107
- pnpm exec mantle validate
108
- pnpm exec mantle skills
109
- pnpm exec mantle skills --check
110
- ```
111
-
112
- Run the project's TypeScript check and start its actual local server. Probe
113
- a route the application declares. An API-only or adapter-only project may
114
- correctly return 404 at `/` and have no Admin. When Admin was requested,
115
- probe `/admin/sign-in` and a `/_mantle/admin/assets/*` URL — both must be
116
- 200. A white screen is an assets 404, not a missing frontend build.
117
- Conventional Auth routes may return `503 setup_incomplete` until that mode
118
- is configured; the local OTP path replaces construction instead. Do not
119
- introduce an auth bypass to make smoke pass.
120
-
121
- Commit the resolved lockfile in the application's normal workflow; subsequent
122
- installs use `pnpm install --frozen-lockfile`. Do not initialize/push a remote,
123
- provision resources, deploy or commit secrets as part of local verification.
124
-
125
- ## Existing application
126
-
127
- Read package.json, lockfile, actual entry, manifest files, provider config and
128
- project instructions. `.mantle/launch-state.json`, features or handoff files
129
- are optional legacy context, never prerequisites. Preserve them and user code.
130
- Install the frozen dependency graph, project installed Core skills, then read
131
- those skills and embedded docs. Never apply develop docs to an older package.
132
- Use the installed `mantle --help` and the project's scripts as authority.
133
-
134
- For a legacy pre-stable project, retain its pinned behavior until an explicit
135
- upgrade is requested. Do not rewrite provider identities, delete metadata or
136
- fetch a nonexistent new Starter tag. SDK upgrades follow the update skill, not
137
- a bundle comparison command.
138
-
139
- ## Ship and report
140
-
141
- When deployment is requested, read
142
- `node_modules/@aotter/mantle/skills/provision/SKILL.md` (not projected by
143
- `mantle skills`) and follow that version-matched skill and the
144
- observed host configuration. Legacy Landing remains a pre-stable product; it
145
- is not a launch dependency for new Core projects.
146
-
147
- Report the project path, exact SDK version, local URL/HTTP result and checks,
148
- plus any genuinely missing auth/provider setup. Do not claim a working homepage
149
- or authenticated MCP based only on successful generation.
14
+ `npx skills add aotter/mantle` installs this small bootstrap skill. It does not
15
+ install the SDK or create a project. The skill name is `mantle`; the source
16
+ folder remains `skills/install/` so installation does not copy the repository.
17
+
18
+ ## Find the application and its version
19
+
20
+ First locate the user's target project. If it already has `@aotter/mantle`,
21
+ read that project's `package.json`, lockfile, and
22
+ `node_modules/@aotter/mantle/package.json`. Follow the CLI and docs shipped in
23
+ that exact installed package. Never use another project's dependencies, the
24
+ SDK checkout, a floating GitHub handbook, or an old alpha/Starter fallback.
25
+ An older installed SDK may only support direct authoring; its own
26
+ `mantle generate --help` decides which flags exist. Upgrade only when requested.
27
+
28
+ For a new project, choose `cf` or `chatgpt-sites` from the request; ask only if
29
+ it remains ambiguous. A host-free Spec-only project needs no host. Resolve the
30
+ latest **published stable** `@aotter/mantle` version from npm once, confirm it
31
+ is not a prerelease, and install it with an exact version in the new project.
32
+ For example, run `npm view @aotter/mantle@latest version`, inspect the returned
33
+ plain `X.Y.Z`, then `npm install --save-exact @aotter/mantle@X.Y.Z` with that
34
+ actual version substituted. Set `type=module` in `package.json` first; npm's
35
+ default project type may be CommonJS.
36
+ When testing a release candidate, use the explicitly supplied exact version
37
+ or local tarball instead of npm latest. Pin every selected `@aotter/mantle*`
38
+ package to the running CLI's exact version; `generate` writes the required
39
+ package declarations. Do not let npm's default `^` range remain. If the
40
+ project registry overrides public npmjs, set `@aotter:registry=https://registry.npmjs.org/`
41
+ in a project-owned `.npmrc`.
42
+
43
+ ## New project sequence
44
+
45
+ 1. Create a separate application directory with Node 22+ and a project
46
+ `package.json` containing `"type":"module"`. Install the exact chosen
47
+ `@aotter/mantle` version locally. Read its
48
+ `node_modules/@aotter/mantle/skills/install/SKILL.md` and version-matched
49
+ `docs/handbook/start/overview.md` before continuing. These installed
50
+ instructions take precedence over this GitHub bootstrap copy. Check
51
+ `npx mantle generate --help`: if it does not list `--host`, follow the
52
+ installed version's direct-authoring path instead of the steps below.
53
+ 2. On a CLI that supports it, run with an explicit host, for example
54
+ `npx mantle generate --host cf` or `npx mantle generate --host chatgpt-sites`.
55
+ No `--features` means Spec, Runtime, API, MCP, Admin and a blank editable
56
+ home. For a smaller app, positively list the modules with `--features`
57
+ (for example `spec,api`); use `--features spec` for a host-free compiler.
58
+ Do not remove Admin merely because its package is not installed yet.
59
+ 3. The first run may declare selected dependencies and exit asking for an
60
+ install. Install them using the project's package manager, then rerun
61
+ `mantle generate` to finish. Read `docs/handbook/start/project-and-cli.md`
62
+ from the same installed package for saved selections, owned files,
63
+ `--check`.
64
+ 4. Before adding a Schema on ChatGPT Sites, review and apply its initial local
65
+ D1 migration as the installed Sites guide says. Then add only the user's
66
+ Schema, View, Procedure and Trigger manifests. Keep the blank home editable;
67
+ do not invent business data or media bindings. Run `mantle generate` to
68
+ append any new migration, review and apply it, then run `generate --check`,
69
+ `validate`, `skills`, build/typecheck and local smoke.
70
+ 5. For Cloudflare, follow installed `docs/handbook/start/project-and-cli.md`
71
+ and `docs/handbook/cloudflare/authentication.md`. Copy `.dev.vars.example`
72
+ to `.dev.vars`, set `ADMIN_EMAIL` and `BETTER_AUTH_SECRET`, and match
73
+ `PUBLIC_ORIGIN` to the loopback URL printed by `wrangler dev`. Open
74
+ `/admin/sign-in` with that email; read the OTP from wrangler logs. Check
75
+ Admin assets and an owner-only Admin API before claiming Admin works. For
76
+ ChatGPT Sites, follow installed
77
+ `docs/handbook/chatgpt-sites/generated-app.md`: review and apply local D1
78
+ migrations, set `OWNER_EMAIL` and `PUBLIC_ORIGIN`, then test the blank home,
79
+ unauthorized Admin, owner Admin, public MCP and staff MCP. Local owner
80
+ identity is simulated by the loopback-only smoke; it does not create a
81
+ browser ChatGPT session. Sites owns
82
+ production identity and deployment; do not deploy its Worker directly.
83
+ Browser Admin WebMCP and Sites-session `/api/mcp/staff` do not provide
84
+ remote staff OAuth MCP. The owner can maintain content through Admin and
85
+ the applicable authenticated MCP surface.
86
+
87
+ Use installed `docs/handbook/reference/features.md` for Manifest choices and
88
+ `docs/examples/` for optional examples. The generated project is the starting
89
+ point; a domain example is not a template to copy wholesale. Do not use a
90
+ `mantle-starters` tag or `mantle create`.
91
+
92
+ ## Existing project and handoff
93
+
94
+ For an application that already uses Mantle, preserve selected features,
95
+ user-authored files, migrations, package versions, identity settings and
96
+ provider resources. Use its installed CLI's `--help` and package-local docs.
97
+ Run `generate --check` without writing, then the project's normal tests.
98
+
99
+ When the request is to add Mantle to an independently authored application,
100
+ inspect its host, code and data first. Use the exact installed package's
101
+ `docs/skills/integrate/SKILL.md` if available. If Mantle is not installed,
102
+ choose between selective embedding, incremental replacement and creating a
103
+ separate Mantle app followed by frontend/data migration before installing an
104
+ exact SDK version. The existing application does not need to adopt the
105
+ generated project's entry or directory layout. An older SDK's own docs and
106
+ CLI govern its behavior; upgrade deliberately if the selected strategy needs
107
+ newer capabilities.
108
+
109
+ When deployment is requested, read the installed version's
110
+ `skills/provision/SKILL.md`. Report the target path, exact SDK version, local
111
+ HTTP results, Admin/MCP identity evidence, and any missing provider setup.
112
+ Local simulated Sites headers do not prove deployed ChatGPT sign-in.
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: integrate
3
+ description: Integrate Mantle into an existing application. Use when the application has its own code, routes, storage or deployment and the owner wants to add Mantle, replace part of it, or rebuild with Mantle while migrating frontend and data.
4
+ metadata:
5
+ source: "@aotter/mantle"
6
+ sourcePath: docs/skills/integrate/SKILL.md
7
+ applies_to: mantle grammar v0.1
8
+ projection: package
9
+ projectionReason: Existing-project migration is opt-in; a fresh generated app does not need this brief.
10
+ ---
11
+
12
+ # Integrate Mantle into an existing application
13
+
14
+ Work in the target application. Read its installed `@aotter/mantle` version,
15
+ CLI help and package-local docs before editing. If Mantle is absent, inspect
16
+ the application first, then install an exact SDK version only after choosing an
17
+ integration strategy. Use the installed version's contract; do not copy
18
+ `develop` branch examples into an older consumer.
19
+
20
+ ## Inspect and decide
21
+
22
+ Map the actual entry points, router, frontend, auth, storage, data volume and
23
+ relationships, deployment pipeline, URLs, background jobs and existing tests.
24
+ Identify which behavior the owner wants Mantle to own and what must remain
25
+ stable. Compare three approaches using the application's evidence:
26
+
27
+ 1. **Embed selectively:** keep the host and call Mantle's parser, Runtime or
28
+ selected surfaces through its documented package APIs. The host owns wiring.
29
+ 2. **Replace incrementally:** move one bounded capability at a time. Define
30
+ where reads and writes go during coexistence and how each step is checked.
31
+ 3. **Rebuild and migrate:** create a separate new Mantle application, port the
32
+ frontend and business behavior, and transform/import existing data. Prefer
33
+ this when adapting the old structure costs more than rebuilding. Preserve
34
+ relevant IDs, relations, URLs and identity semantics; rehearse the data
35
+ transfer before a production cutover.
36
+
37
+ Explain the chosen path and its concrete tradeoffs to the owner. A small
38
+ prototype or read-only inventory may resolve uncertainty. Do not run a generic
39
+ project converter or assume the current application's shape matches a
40
+ generated Cloudflare or ChatGPT Sites skeleton.
41
+
42
+ ## Implement and verify
43
+
44
+ - For embedding, retain user-owned entry/config/build files. Use the installed
45
+ direct-authoring docs and CLI to validate manifests and emit bindings; wire
46
+ the selected Mantle capabilities into the existing host explicitly.
47
+ - For incremental replacement, establish a clear owner for each data write
48
+ and request route at every stage. Verify old and new behavior together.
49
+ - For rebuilding, generate in a **separate directory** with the installed
50
+ CLI, move frontend and handlers deliberately, and plan data mapping and
51
+ import separately. Review database migrations before application or cutover.
52
+ - Run the application's build and representative behavior checks for the
53
+ changed routes, auth and data. `mantle generate --check` checks generated
54
+ output against its declared selection; it cannot certify host integration.
55
+ For live data, record migration counts and a rollback or recovery path
56
+ appropriate to the project.
57
+
58
+ Leave existing data and deployment untouched until the requested migration
59
+ and release steps are ready. Report which capabilities Mantle now owns,
60
+ what was verified, and what remains for application deployment or cutover.
@@ -0,0 +1,66 @@
1
+ import type { AdminAuth, AdminStaffUser, StaffRole } from '@aotter/mantle/admin';
2
+
3
+ export interface Env { DB: D1Database; ASSETS?: Fetcher; PUBLIC_ORIGIN: string; OWNER_EMAIL?: string }
4
+ type UserRow = { id: string; email: string; name: string; role: StaffRole | null; created_at: string; signed_in: number };
5
+ const project = (row: UserRow): AdminStaffUser => ({ id:row.id,email:row.email,name:row.name,role:row.role,githubLogin:null,emailVerified:!!row.signed_in,createdAt:new Date(row.created_at) });
6
+
7
+ // This adapter must only receive requests through Sites' identity-stripping dispatcher.
8
+ // Never expose the Worker independently with caller-controlled oai-* headers.
9
+ export function readIdentity(request: Request) {
10
+ const sub=request.headers.get('oai-authenticated-user-id');
11
+ const email=request.headers.get('oai-authenticated-user-email')?.trim().toLowerCase();
12
+ if(!sub || !email || sub.length>512 || email.length>320) return null;
13
+ let name=email;
14
+ if(request.headers.get('oai-authenticated-user-full-name-encoding')==='percent-encoded-utf-8') {
15
+ try { name=decodeURIComponent(request.headers.get('oai-authenticated-user-full-name')||'')||email; } catch {}
16
+ }
17
+ return {id:`chatgpt:${sub}`,email,name};
18
+ }
19
+
20
+ export function createChatGPTAuth(env: Env): AdminAuth {
21
+ const db=env.DB;
22
+ const roleFor=async(id:string)=>(await db.prepare('SELECT role FROM sites_users WHERE id = ?').bind(id).first<{role:StaffRole|null}>())?.role??null;
23
+ const auth: AdminAuth = {
24
+ basePath:'/api/auth', methods:[],
25
+ async getSession(request) {
26
+ const who=readIdentity(request); if(!who)return null;
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
+ }
35
+ if(!row)return null;
36
+ return {session:{id:`sites:${who.id}`},user:{id:row.id,email:row.email,name:row.name,role:row.role,roleCurrent:true}};
37
+ },
38
+ getUserRole:roleFor,
39
+ async handler(request) {
40
+ const path=new URL(request.url).pathname;
41
+ if(path==='/api/auth/get-session' && request.method==='GET') return Response.json(await auth.getSession(request));
42
+ if(path==='/api/auth/sign-out' && request.method==='POST') {
43
+ if(request.headers.get('origin')!==env.PUBLIC_ORIGIN)return new Response('Forbidden',{status:403});
44
+ // The shipped Admin UI navigates here after its POST; the next top-level request signs out at Sites.
45
+ return Response.json({success:true},{headers:{'set-cookie':'mantle-sites-signout=1; Path=/admin/sign-in; HttpOnly; SameSite=Strict; Max-Age=60'+(env.PUBLIC_ORIGIN.startsWith('https:')?'; Secure':'')}});
46
+ }
47
+ return Response.json({error:'unsupported_auth_route'},{status:404});
48
+ },
49
+ async listUsers(){const r=await db.prepare('SELECT * FROM sites_users WHERE role IS NOT NULL ORDER BY created_at,id').all<UserRow>();return r.results.map(project);},
50
+ async listMembers(args){
51
+ // ponytail: one bounded page for this integration lab; add cursor pagination before a large member rollout.
52
+ const r=await db.prepare('SELECT * FROM sites_users WHERE role IS NULL AND (email LIKE ? OR name LIKE ?) ORDER BY created_at,id LIMIT ?').bind(`%${args.search||''}%`,`%${args.search||''}%`,Math.min(args.limit,100)).all<UserRow>();
53
+ return {items:r.results.map(project),previousCursor:null,nextCursor:null};
54
+ },
55
+ async setUserRole(id,role){if(role!==null&&!['owner','editor','contributor'].includes(role))throw new Error('Invalid role');return !!(await db.prepare('UPDATE sites_users SET role=? WHERE id=?').bind(role,id).run()).meta.changes;},
56
+ async inviteUser(email,role){
57
+ const normalized=email.trim().toLowerCase();const id=`invite:${crypto.randomUUID()}`;
58
+ const r=await db.prepare('INSERT OR IGNORE INTO sites_users (id,email,name,role,signed_in) VALUES (?,?,?,?,0)').bind(id,normalized,normalized,role).run();
59
+ if(r.meta.changes)return {kind:'created',id};
60
+ const existing=await db.prepare('SELECT id FROM sites_users WHERE email=?').bind(normalized).first<{id:string}>();
61
+ if(!existing)throw new Error('Invitation could not be saved');return {kind:'exists',id:existing.id};
62
+ },
63
+ async revokeInvite(id){return !!(await db.prepare('DELETE FROM sites_users WHERE id=? AND signed_in=0').bind(id).run()).meta.changes;},
64
+ };
65
+ return auth;
66
+ }
@@ -0,0 +1,14 @@
1
+ import { build } from 'esbuild';
2
+ import { cp, mkdir, rm } from 'node:fs/promises';
3
+ import { execFileSync } from 'node:child_process';
4
+
5
+ execFileSync('node', ['node_modules/@aotter/mantle/dist/cli/main.js', 'generate', '--check'], { stdio: 'inherit' });
6
+ execFileSync('node', ['node_modules/typescript/bin/tsc', '--noEmit'], { stdio: 'inherit' });
7
+ await rm('dist', { recursive: true, force: true });
8
+ await mkdir('dist/server', { recursive: true });
9
+ await build({ entryPoints: ['src/index.ts'], outfile: 'dist/server/index.js', bundle: true, format: 'esm', platform: 'browser', conditions: ['workerd'], target: 'es2022', external: ['cloudflare:*', 'node:*'] });
10
+ await mkdir('dist/client', { recursive: true });
11
+ await cp('public', 'dist/client', { recursive: true, force: true }).catch(error => { if (error.code !== 'ENOENT') throw error; });
12
+ await mkdir('dist/.openai', { recursive: true });
13
+ await cp('.openai/hosting.json', 'dist/.openai/hosting.json');
14
+ await cp('drizzle', 'dist/.openai/drizzle', { recursive: true });
@@ -0,0 +1 @@
1
+ export interface Env { DB: D1Database; PUBLIC_ORIGIN: string; ASSETS?: Fetcher }
@@ -0,0 +1,48 @@
1
+ import type { Hono } from 'hono';
2
+ import type { AdminAuth, MantleAdminRuntime, StaffRole } from '@aotter/mantle/admin';
3
+ import { McpJsonRpcDispatcher, projectCallableCapabilities, type RuntimePlan } from '@aotter/mantle/runtime';
4
+ import type { Env } from './sites-auth';
5
+
6
+ const staffRoles=new Set<string>(['owner','editor','contributor']);
7
+
8
+ /** Sites-session MCP surfaces. Remote clients still need a separate OAuth bearer flow. */
9
+ export function mountMcp(app:Hono<{Bindings:Env}>, get:()=>Promise<MantleAdminRuntime>, auth:AdminAuth, plan:RuntimePlan) {
10
+ const cache=new WeakMap<object,Map<'public'|'staff',McpJsonRpcDispatcher>>();
11
+ const dispatcher=async(surface:'public'|'staff',origin:string)=>{
12
+ const runtime=await get();
13
+ let dispatchers=cache.get(runtime);
14
+ if(!dispatchers)cache.set(runtime,dispatchers=new Map());
15
+ let value=dispatchers.get(surface);
16
+ if(!value) {
17
+ value=new McpJsonRpcDispatcher({
18
+ getEntry:runtime.getEntry,
19
+ createDraft:runtime.createDraft,
20
+ updateDraft:runtime.updateDraft,
21
+ requestPublish:runtime.requestPublish,
22
+ unpublish:runtime.unpublish,
23
+ archive:runtime.archive,
24
+ deleteEntry:runtime.deleteEntry,
25
+ executeView:{execute:request=>runtime.executeView({...request,view:request.view.metadata.name})},
26
+ invokeTrigger:{execute:request=>runtime.invokeTrigger(request)},
27
+ },[...runtime.schemas.values()],{
28
+ surface,
29
+ capabilities:projectCallableCapabilities(plan,{surface}),
30
+ serverInfo:{name:`aotter.mantle.${surface}`,title:`Mantle ${surface}`,websiteUrl:origin},
31
+ });
32
+ dispatchers.set(surface,value);
33
+ }
34
+ return value;
35
+ };
36
+ app.all('/api/mcp',async c=>(await dispatcher('public',c.env.PUBLIC_ORIGIN)).dispatch(c.req.raw,{user:null,staff:null,env:c.env,waitUntil:p=>c.executionCtx.waitUntil(p)}));
37
+ app.all('/api/mcp/staff',async c=>{
38
+ const session=await auth.getSession(c.req.raw);
39
+ if(!session)return Response.json({error:'unauthorized'},{status:401});
40
+ const role=session.user.roleCurrent?session.user.role:await auth.getUserRole(session.user.id);
41
+ if(!role||!staffRoles.has(role))return Response.json({error:'forbidden'},{status:403});
42
+ return (await dispatcher('staff',c.env.PUBLIC_ORIGIN)).dispatch(c.req.raw,{
43
+ user:{id:session.user.id},staff:{id:session.user.id,role:role as StaffRole},
44
+ auth:{credential:'session',credentialId:session.session.id,clientId:null,scopes:[]},
45
+ env:c.env,waitUntil:p=>c.executionCtx.waitUntil(p),
46
+ });
47
+ });
48
+ }
@@ -0,0 +1,29 @@
1
+ import type { Hono } from 'hono';
2
+ import { McpJsonRpcDispatcher, projectCallableCapabilities, type MantleRuntime, type RuntimePlan } from '@aotter/mantle/runtime';
3
+ import type { Env } from './sites-env';
4
+
5
+ export function mountPublicMcp(app:Hono<{Bindings:Env}>,get:()=>Promise<MantleRuntime>,plan:RuntimePlan) {
6
+ let current:MantleRuntime|undefined;
7
+ let dispatcher:McpJsonRpcDispatcher|undefined;
8
+ app.all('/api/mcp',async c=>{
9
+ const runtime=await get();
10
+ if(current!==runtime){
11
+ current=runtime;
12
+ dispatcher=new McpJsonRpcDispatcher({
13
+ getEntry:runtime.getEntry,
14
+ createDraft:runtime.createDraft,
15
+ updateDraft:runtime.updateDraft,
16
+ requestPublish:runtime.requestPublish,
17
+ unpublish:runtime.unpublish,
18
+ archive:runtime.archive,
19
+ deleteEntry:runtime.deleteEntry,
20
+ executeView:{execute:request=>runtime.executeView({...request,view:request.view.metadata.name})},
21
+ invokeTrigger:{execute:request=>runtime.invokeTrigger(request)},
22
+ },[...runtime.schemas.values()],{
23
+ surface:'public',capabilities:projectCallableCapabilities(plan,{surface:'public'}),
24
+ serverInfo:{name:'aotter.mantle.public',title:'Mantle public',websiteUrl:c.env.PUBLIC_ORIGIN},
25
+ });
26
+ }
27
+ return dispatcher!.dispatch(c.req.raw,{user:null,staff:null,env:c.env,waitUntil:p=>c.executionCtx.waitUntil(p)});
28
+ });
29
+ }
@@ -0,0 +1,20 @@
1
+ import assert from 'node:assert/strict';
2
+
3
+ const origin=process.env.MANTLE_TEST_ORIGIN??'http://127.0.0.1:4174';
4
+ const url=new URL(origin);
5
+ assert.equal(url.protocol,'http:','Identity simulation requires loopback HTTP');
6
+ assert.ok(['localhost','127.0.0.1','[::1]'].includes(url.hostname),'Identity simulation is loopback-only');
7
+ const email=process.env.MANTLE_TEST_OWNER_EMAIL;
8
+ assert.ok(email,'Set MANTLE_TEST_OWNER_EMAIL to the locally configured owner');
9
+ const owner={'oai-authenticated-user-id':'mantle-local-owner','oai-authenticated-user-email':email};
10
+ const stranger={'oai-authenticated-user-id':'mantle-local-stranger','oai-authenticated-user-email':'stranger@example.test'};
11
+ const call=(path,headers={})=>fetch(new URL(path,url),{headers,redirect:'manual'});
12
+ assert.equal((await call('/')).status,200);
13
+ assert.equal((await call('/admin/api/me')).status,401);
14
+ assert.equal((await call('/admin/api/me',stranger)).status,403);
15
+ assert.equal((await call('/admin/api/me',owner)).status,200);
16
+ const mcp=(path,headers={})=>fetch(new URL(path,url),{method:'POST',headers:{...headers,'content-type':'application/json','mcp-protocol-version':'2025-11-25'},body:JSON.stringify({jsonrpc:'2.0',id:1,method:'initialize'})});
17
+ assert.equal((await mcp('/api/mcp')).status,200);
18
+ assert.equal((await mcp('/api/mcp/staff')).status,401);
19
+ assert.equal((await mcp('/api/mcp/staff',owner)).status,200);
20
+ console.log('Local Sites identity simulation passed; this does not verify deployed ChatGPT sign-in.');