@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
package/skills/update/SKILL.md
CHANGED
|
@@ -3,9 +3,9 @@ name: update
|
|
|
3
3
|
description: Review and upgrade Mantle SDK dependencies and project-local skills while preserving application source, provider identities and plugin lockfiles.
|
|
4
4
|
metadata:
|
|
5
5
|
source: "@aotter/mantle"
|
|
6
|
-
sourcePath: skills/update/SKILL.md
|
|
6
|
+
sourcePath: docs/skills/update/SKILL.md
|
|
7
7
|
applies_to: mantle grammar v0.1
|
|
8
|
-
projection: project
|
|
8
|
+
projection: project
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# Mantle Update
|
|
@@ -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.');
|
package/skills/.keep
DELETED
|
File without changes
|
|
File without changes
|