@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.
- package/README.md +16 -16
- 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 +10 -10
- 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 +5 -2
- package/docs/agent-prompts.md +40 -78
- package/docs/examples/host-chatgpt-sites/README.md +1 -1
- 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/scripts/migration.mjs +42 -8
- package/docs/examples/host-chatgpt-sites/src/chatgpt-auth.ts +9 -6
- package/docs/examples/host-local-admin-otp/package.json +2 -2
- package/docs/examples/host-minimal-worker/package.json +2 -2
- package/docs/handbook/chatgpt-sites/generated-app.md +84 -0
- package/docs/handbook/chatgpt-sites/index.md +21 -11
- package/docs/handbook/cloudflare/authentication.md +19 -2
- package/docs/handbook/cloudflare/bindings.md +4 -11
- package/docs/handbook/cloudflare/conventional-worker.md +7 -3
- package/docs/handbook/cloudflare/deploy-and-operate.md +1 -1
- package/docs/handbook/concepts/runtime-and-adapters.md +7 -0
- package/docs/handbook/guides/agent-setup.md +3 -3
- package/docs/handbook/navigation.json +3 -2
- 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 +55 -0
- package/docs/handbook/start/overview.md +5 -2
- 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 -8
- package/docs/skills/develop/SKILL.md +14 -6
- package/docs/skills/integrate/SKILL.md +60 -0
- package/package.json +18 -17
- package/skills/README.md +3 -1
- package/skills/develop/SKILL.md +14 -6
- package/skills/install/SKILL.md +100 -137
- package/skills/integrate/SKILL.md +60 -0
- 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/install/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mantle
|
|
3
|
-
description:
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
`
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
`
|
|
52
|
-
|
|
53
|
-
2.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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.');
|