@aotter/mantle 0.1.2-alpha.6 → 0.1.2
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 +15 -10
- package/dist/cli/generate.d.ts +9 -0
- package/dist/cli/generate.d.ts.map +1 -1
- package/dist/cli/generate.js +40 -1
- 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 +37 -9
- package/dist/cli/main.js.map +1 -1
- package/docs/adapter-guide.md +6 -1
- package/docs/adr/adr-lite-845-frontend-client.md +38 -0
- package/docs/agent-prompts.md +91 -0
- package/docs/api-mcp-authorization.md +1 -1
- package/docs/auth-hosting-model.md +1 -1
- package/docs/examples/README.md +22 -0
- package/docs/examples/builtin-commerce.md +269 -0
- package/docs/examples/builtin-intake.md +143 -0
- package/docs/examples/builtin-legal-documents.md +189 -0
- package/docs/examples/builtin-procurement.md +241 -0
- package/docs/examples/builtin-publication.md +241 -0
- package/docs/examples/builtin-reservation.md +149 -0
- package/docs/examples/cf-primitives-commerce-inventory.md +809 -0
- package/docs/examples/cf-primitives-guarded-api.md +429 -0
- package/docs/examples/cf-primitives-intake-hooks.md +319 -0
- package/docs/examples/host-chatgpt-sites/.openai/hosting.json +1 -0
- package/docs/examples/host-chatgpt-sites/README.md +53 -0
- package/docs/examples/host-chatgpt-sites/drizzle/0000_sites_users.sql +8 -0
- package/docs/examples/host-chatgpt-sites/drizzle/0001_mantle.sql +294 -0
- package/docs/examples/host-chatgpt-sites/drizzle/0002_article_cover.sql +5 -0
- package/docs/examples/host-chatgpt-sites/drizzle/meta/_journal.json +1 -0
- package/docs/examples/host-chatgpt-sites/manifests/site.yaml +27 -0
- package/docs/examples/host-chatgpt-sites/package.json +1 -0
- package/docs/examples/host-chatgpt-sites/public/site.css +1 -0
- package/docs/examples/host-chatgpt-sites/scripts/build.mjs +12 -0
- package/docs/examples/host-chatgpt-sites/scripts/check.mjs +99 -0
- package/docs/examples/host-chatgpt-sites/scripts/migration.mjs +9 -0
- package/docs/examples/host-chatgpt-sites/src/chatgpt-auth.ts +63 -0
- package/docs/examples/host-chatgpt-sites/src/index.ts +46 -0
- package/docs/examples/host-chatgpt-sites/src/mcp.ts +49 -0
- package/docs/examples/host-chatgpt-sites/src/media.ts +109 -0
- package/docs/examples/host-chatgpt-sites/src/r2-lab.ts +38 -0
- package/docs/examples/host-chatgpt-sites/src/storage-fingerprint.json +1 -0
- package/docs/examples/host-chatgpt-sites/src/web.ts +35 -0
- package/docs/examples/host-chatgpt-sites/tsconfig.json +1 -0
- package/docs/examples/host-chatgpt-sites/wrangler.jsonc +10 -0
- package/docs/examples/host-local-admin-otp/.dev.vars.example +3 -0
- package/docs/examples/host-local-admin-otp/README.md +70 -0
- package/docs/examples/host-local-admin-otp/ensure-dev-vars.mjs +5 -0
- package/docs/examples/host-local-admin-otp/package.json +29 -0
- package/docs/examples/host-local-admin-otp/public/.gitkeep +1 -0
- package/docs/examples/host-local-admin-otp/smoke.mjs +141 -0
- package/docs/examples/host-local-admin-otp/src/index.ts +54 -0
- package/docs/examples/host-local-admin-otp/wrangler.jsonc +23 -0
- package/docs/examples/{minimal-worker → host-minimal-worker}/README.md +9 -5
- package/docs/examples/host-minimal-worker/manifests/site.yaml +25 -0
- package/docs/examples/{minimal-worker → host-minimal-worker}/package.json +3 -3
- package/docs/examples/host-minimal-worker/tsconfig.json +17 -0
- package/docs/handbook/cloudflare/authentication.md +17 -2
- package/docs/handbook/cloudflare/bindings.md +9 -7
- package/docs/handbook/cloudflare/chatgpt-sites.md +29 -0
- package/docs/handbook/cloudflare/conventional-worker.md +3 -3
- package/docs/handbook/cloudflare/deferred-hooks-queues.md +0 -1
- package/docs/handbook/cloudflare/deploy-and-operate.md +14 -20
- package/docs/handbook/cloudflare/media-r2.md +2 -2
- package/docs/handbook/cloudflare/public-web.md +1 -1
- package/docs/handbook/cloudflare/site-chrome.md +75 -0
- package/docs/handbook/concepts/authorization.md +2 -2
- package/docs/handbook/concepts/four-atoms.md +2 -2
- package/docs/handbook/concepts/lifecycle-and-locales.md +1 -1
- package/docs/handbook/concepts/mcp-and-agents.md +1 -1
- package/docs/handbook/concepts/procedures-and-triggers.md +1 -1
- package/docs/handbook/concepts/runtime-and-adapters.md +2 -2
- package/docs/handbook/concepts/views.md +2 -2
- package/docs/handbook/examples/commerce-transaction.md +4 -806
- package/docs/handbook/examples/commerce.md +11 -0
- package/docs/handbook/examples/guarded-api.md +3 -420
- package/docs/handbook/examples/hub.md +10 -0
- package/docs/handbook/examples/intake-form.md +6 -313
- package/docs/handbook/examples/intake-hooks.md +11 -0
- package/docs/handbook/examples/legal-documents.md +3 -211
- package/docs/handbook/examples/procurement-approvals.md +3 -233
- package/docs/handbook/examples/publication.md +3 -233
- package/docs/handbook/examples/reservation.md +3 -213
- package/docs/handbook/navigation.json +17 -2
- package/docs/handbook/reference/authorization.md +1 -1
- package/docs/handbook/reference/procedure.md +2 -2
- package/docs/handbook/reference/schema.md +3 -3
- package/docs/handbook/reference/site-config.md +5 -16
- package/docs/handbook/reference/surface.md +3 -7
- package/docs/handbook/sites/equipment-checkout.md +231 -0
- package/docs/handbook/sites/host-reference.md +113 -0
- package/docs/handbook/sites/index.md +111 -0
- package/docs/handbook/start/project-and-cli.md +22 -14
- package/docs/handbook/start/quickstart-admin.md +239 -0
- package/docs/handbook/start/quickstart-worker.md +22 -23
- package/docs/migration-0.1.2.md +26 -0
- package/docs/release-process.md +92 -7
- package/docs/sealed-pipeline-ownership.md +2 -2
- package/docs/spec-only-host-adoption.md +3 -4
- package/docs/transaction-patterns.md +2 -2
- package/package.json +15 -15
- package/skills/README.md +18 -2
- package/skills/develop/SKILL.md +32 -23
- package/skills/install/SKILL.md +50 -11
- package/skills/provision/SKILL.md +21 -5
- /package/docs/examples/{minimal-worker → host-local-admin-otp}/manifests/site.yaml +0 -0
- /package/docs/examples/{minimal-worker → host-local-admin-otp}/tsconfig.json +0 -0
- /package/docs/examples/{minimal-worker → host-minimal-worker}/smoke.mjs +0 -0
- /package/docs/examples/{minimal-worker → host-minimal-worker}/src/index.ts +0 -0
- /package/docs/examples/{minimal-worker → host-minimal-worker}/wrangler.jsonc +0 -0
|
@@ -1,319 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: Redirects to the builtin public intake form in the Examples hub.
|
|
3
3
|
---
|
|
4
|
-
# Intake form
|
|
4
|
+
# Intake form
|
|
5
5
|
|
|
6
|
-
This example
|
|
7
|
-
|
|
8
|
-
## Problem
|
|
9
|
-
|
|
10
|
-
Visitors submit a name, an email address and a message. Staff read recent submissions in Admin, over the staff View REST route, or through Staff MCP. The public write must reject automated submissions before anything is stored, and a new row should trigger an email to the team without making the visitor wait for it or fail when email is not configured. Submissions are live records, not authored content, so the Schema is `operational`.
|
|
11
|
-
|
|
12
|
-
## Manifest
|
|
13
|
-
|
|
14
|
-
```yaml
|
|
15
|
-
apiVersion: cms.mantle.aotter.net/v1
|
|
16
|
-
kind: Schema
|
|
17
|
-
metadata:
|
|
18
|
-
name: requests
|
|
19
|
-
spec:
|
|
20
|
-
title: Requests
|
|
21
|
-
description: Requests submitted through the public intake flow.
|
|
22
|
-
lifecycle: operational
|
|
23
|
-
schema:
|
|
24
|
-
type: object
|
|
25
|
-
additionalProperties: false
|
|
26
|
-
required: [name, email, message]
|
|
27
|
-
properties:
|
|
28
|
-
name: { type: string, minLength: 1, maxLength: 120 }
|
|
29
|
-
email: { type: string, format: email }
|
|
30
|
-
message: { type: string, minLength: 1, maxLength: 2000 }
|
|
31
|
-
createdAt: { type: number, x-mcp-hint: timestamp-ms, x-mantle-bind: now }
|
|
32
|
-
---
|
|
33
|
-
apiVersion: cms.mantle.aotter.net/v1
|
|
34
|
-
kind: View
|
|
35
|
-
metadata:
|
|
36
|
-
name: recent-requests
|
|
37
|
-
spec:
|
|
38
|
-
title: Recent requests
|
|
39
|
-
surface: staff
|
|
40
|
-
from: requests
|
|
41
|
-
fields: [id, name, email, message, createdAt]
|
|
42
|
-
orderBy:
|
|
43
|
-
- { field: createdAt, direction: desc }
|
|
44
|
-
limit: 50
|
|
45
|
-
---
|
|
46
|
-
apiVersion: cms.mantle.aotter.net/v1
|
|
47
|
-
kind: Procedure
|
|
48
|
-
metadata:
|
|
49
|
-
name: submit-request
|
|
50
|
-
spec:
|
|
51
|
-
title: Submit request
|
|
52
|
-
description: Create a new public request.
|
|
53
|
-
input:
|
|
54
|
-
type: object
|
|
55
|
-
additionalProperties: false
|
|
56
|
-
required: [name, email, message]
|
|
57
|
-
properties:
|
|
58
|
-
name: { type: string, minLength: 1, maxLength: 120 }
|
|
59
|
-
email: { type: string, format: email }
|
|
60
|
-
message: { type: string, minLength: 1, maxLength: 2000 }
|
|
61
|
-
turnstileToken: { type: string }
|
|
62
|
-
output: { type: object }
|
|
63
|
-
handler: { kind: builtin, op: create, schema: requests }
|
|
64
|
-
---
|
|
65
|
-
apiVersion: cms.mantle.aotter.net/v1
|
|
66
|
-
kind: Trigger
|
|
67
|
-
metadata:
|
|
68
|
-
name: submit-request-http
|
|
69
|
-
spec:
|
|
70
|
-
source: { kind: http, method: POST, path: /api/requests }
|
|
71
|
-
target: { procedure: submit-request }
|
|
72
|
-
---
|
|
73
|
-
apiVersion: cms.mantle.aotter.net/v1
|
|
74
|
-
kind: Trigger
|
|
75
|
-
metadata:
|
|
76
|
-
name: submit-request-mcp
|
|
77
|
-
spec:
|
|
78
|
-
source: { kind: mcp, surface: public }
|
|
79
|
-
target: { procedure: submit-request }
|
|
80
|
-
---
|
|
81
|
-
apiVersion: cms.mantle.aotter.net/v1
|
|
82
|
-
kind: Procedure
|
|
83
|
-
metadata:
|
|
84
|
-
name: verify-turnstile
|
|
85
|
-
spec:
|
|
86
|
-
input:
|
|
87
|
-
type: object
|
|
88
|
-
properties:
|
|
89
|
-
turnstileToken: { type: string }
|
|
90
|
-
output: { type: object }
|
|
91
|
-
handler: { kind: ref, ref: verify-turnstile }
|
|
92
|
-
---
|
|
93
|
-
apiVersion: cms.mantle.aotter.net/v1
|
|
94
|
-
kind: Trigger
|
|
95
|
-
metadata:
|
|
96
|
-
name: 010-requests-verify-turnstile
|
|
97
|
-
spec:
|
|
98
|
-
source:
|
|
99
|
-
kind: lifecycle
|
|
100
|
-
schema: requests
|
|
101
|
-
on: [before_create]
|
|
102
|
-
errorPolicy: abort
|
|
103
|
-
target: { procedure: verify-turnstile }
|
|
104
|
-
---
|
|
105
|
-
apiVersion: cms.mantle.aotter.net/v1
|
|
106
|
-
kind: Procedure
|
|
107
|
-
metadata:
|
|
108
|
-
name: notify-request
|
|
109
|
-
spec:
|
|
110
|
-
input:
|
|
111
|
-
type: object
|
|
112
|
-
properties:
|
|
113
|
-
name: { type: string }
|
|
114
|
-
email: { type: string }
|
|
115
|
-
message: { type: string }
|
|
116
|
-
output: { type: object }
|
|
117
|
-
handler: { kind: ref, ref: notify-request }
|
|
118
|
-
---
|
|
119
|
-
apiVersion: cms.mantle.aotter.net/v1
|
|
120
|
-
kind: Trigger
|
|
121
|
-
metadata:
|
|
122
|
-
name: 020-requests-notify
|
|
123
|
-
spec:
|
|
124
|
-
source:
|
|
125
|
-
kind: lifecycle
|
|
126
|
-
schema: requests
|
|
127
|
-
on: [after_create]
|
|
128
|
-
errorPolicy: continue
|
|
129
|
-
target: { procedure: notify-request }
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
Three details carry the pattern:
|
|
133
|
-
|
|
134
|
-
- `submit-request.input` declares `turnstileToken` even though the `requests` Schema does not. The builtin `create` projects `input ∩ Schema.properties`, so the token is never stored. Because the input sets `additionalProperties: false`, the token must be declared or the request fails with `INPUT_VALIDATION_FAILED`.
|
|
135
|
-
- `before_create` hooks receive the original, pre-projection input, so `verify-turnstile` can read the token. Its own `input` schema must not set `additionalProperties: false`; it receives `name`, `email` and `message` too.
|
|
136
|
-
- `after_create` hooks receive the persisted `entry.data`. The token is gone by then, which is why verification cannot be an `after_*` hook.
|
|
137
|
-
|
|
138
|
-
Lifecycle Triggers on the same `(schema, hook)` run alphabetically by `metadata.name`; the `010-`/`020-` prefixes make the order explicit. See [Writes: Procedures, Triggers and hooks](../concepts/procedures-and-triggers.md).
|
|
139
|
-
|
|
140
|
-
## Worker and handlers
|
|
141
|
-
|
|
142
|
-
```ts
|
|
143
|
-
// src/handlers.ts
|
|
144
|
-
import { InvokeFailure, type HandlerContext } from "@aotter/mantle/runtime";
|
|
145
|
-
import { runtimeDiagnostic } from "@aotter/mantle/spec";
|
|
146
|
-
import type { Env } from "./index.js";
|
|
147
|
-
|
|
148
|
-
interface SiteverifyResult {
|
|
149
|
-
readonly success?: boolean;
|
|
150
|
-
readonly "error-codes"?: readonly string[];
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export async function verifyTurnstile(
|
|
154
|
-
input: { readonly turnstileToken?: string },
|
|
155
|
-
ctx: HandlerContext<Env>,
|
|
156
|
-
): Promise<{ ok: true }> {
|
|
157
|
-
const secret = ctx.env.TURNSTILE_SECRET_KEY?.trim();
|
|
158
|
-
if (!secret) return { ok: true }; // fail-open until the secret exists; see below
|
|
159
|
-
|
|
160
|
-
const token = input.turnstileToken?.trim();
|
|
161
|
-
if (!token) reject("Turnstile verification is required.");
|
|
162
|
-
|
|
163
|
-
const body = new FormData();
|
|
164
|
-
body.set("secret", secret);
|
|
165
|
-
body.set("response", token);
|
|
166
|
-
const response = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
|
|
167
|
-
method: "POST",
|
|
168
|
-
body,
|
|
169
|
-
});
|
|
170
|
-
const result = response.ok
|
|
171
|
-
? ((await response.json().catch(() => null)) as SiteverifyResult | null)
|
|
172
|
-
: null;
|
|
173
|
-
if (!result?.success) reject("Turnstile verification failed.", result?.["error-codes"]);
|
|
174
|
-
return { ok: true };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function reject(message: string, value?: unknown): never {
|
|
178
|
-
throw new InvokeFailure(
|
|
179
|
-
runtimeDiagnostic({
|
|
180
|
-
code: "LIFECYCLE_HOOK_REJECTED",
|
|
181
|
-
severity: "error",
|
|
182
|
-
path: "/turnstileToken",
|
|
183
|
-
value,
|
|
184
|
-
expected: "a valid Cloudflare Turnstile token",
|
|
185
|
-
message,
|
|
186
|
-
}),
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export async function notifyRequest(
|
|
191
|
-
input: { readonly name?: string; readonly email?: string; readonly message?: string },
|
|
192
|
-
ctx: HandlerContext<Env>,
|
|
193
|
-
): Promise<{ ok: true }> {
|
|
194
|
-
const { EMAIL, INTAKE_NOTIFY_TO, INTAKE_NOTIFY_FROM } = ctx.env;
|
|
195
|
-
if (!EMAIL || !INTAKE_NOTIFY_TO || !INTAKE_NOTIFY_FROM) {
|
|
196
|
-
console.info("[requests] notification not configured", { entry: ctx.event?.entry?.id });
|
|
197
|
-
return { ok: true }; // fail-soft: the row is already committed
|
|
198
|
-
}
|
|
199
|
-
await EMAIL.send({
|
|
200
|
-
to: INTAKE_NOTIFY_TO,
|
|
201
|
-
from: INTAKE_NOTIFY_FROM,
|
|
202
|
-
subject: `New request from ${input.name ?? "website"}`,
|
|
203
|
-
text: [`Name: ${input.name ?? ""}`, `Email: ${input.email ?? ""}`, "", input.message ?? ""].join("\n"),
|
|
204
|
-
...(input.email ? { replyTo: input.email } : {}),
|
|
205
|
-
});
|
|
206
|
-
return { ok: true };
|
|
207
|
-
}
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
```ts
|
|
211
|
-
// src/index.ts
|
|
212
|
-
import { createMantleWorker, type MantleCloudflareEnv } from "@aotter/mantle/cloudflare";
|
|
213
|
-
import { plan, type MantleHandlers } from "../.mantle/generated/mantle.js";
|
|
214
|
-
import { notifyRequest, verifyTurnstile } from "./handlers.js";
|
|
215
|
-
|
|
216
|
-
interface EmailBinding {
|
|
217
|
-
send(message: { to: string; from: string; subject: string; text?: string; replyTo?: string }): Promise<unknown>;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export interface Env extends MantleCloudflareEnv {
|
|
221
|
-
readonly TURNSTILE_SECRET_KEY?: string;
|
|
222
|
-
readonly EMAIL?: EmailBinding;
|
|
223
|
-
readonly INTAKE_NOTIFY_TO?: string;
|
|
224
|
-
readonly INTAKE_NOTIFY_FROM?: string;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const handlers = {
|
|
228
|
-
"verify-turnstile": verifyTurnstile,
|
|
229
|
-
"notify-request": notifyRequest,
|
|
230
|
-
} satisfies MantleHandlers<Env>;
|
|
231
|
-
|
|
232
|
-
export default createMantleWorker<Env>({ plan, extend: () => ({ handlers }) });
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
The keys of `handlers` are the opaque `handler.ref` strings from the Manifest. A missing key fails at boot with `HANDLER_NOT_REGISTERED`.
|
|
236
|
-
|
|
237
|
-
Bindings live in `wrangler.toml`: `[[send_email]] name = "EMAIL"` for Cloudflare Email Service, `INTAKE_NOTIFY_TO` and `INTAKE_NOTIFY_FROM` as vars, and `TURNSTILE_SECRET_KEY` as a secret (`wrangler secret put TURNSTILE_SECRET_KEY`). See [Bindings and primitives](../cloudflare/bindings.md).
|
|
238
|
-
|
|
239
|
-
Two policies are deliberate and reversible:
|
|
240
|
-
|
|
241
|
-
- **Bot check fails open when the secret is unset.** A first deployment without Turnstile configured still accepts submissions instead of rejecting every visitor with an opaque error. To fail closed, replace `if (!secret) return { ok: true }` with `if (!secret) reject("Turnstile is not configured.")`.
|
|
242
|
-
- **Notification fails soft.** The Trigger's `errorPolicy: continue` means a throwing `after_create` handler is logged and never rolls back the row; the handler additionally returns `ok` when the binding is absent so logs stay quiet. If you later route `after_*` hooks through a Queue, keep the handler idempotent as described in [Deferred hooks with Queues](../cloudflare/deferred-hooks-queues.md).
|
|
243
|
-
|
|
244
|
-
> **Warning**
|
|
245
|
-
> The runtime does not substitute an error code when a `before_*` hook aborts; the caller receives exactly the diagnostic the hook threw. This handler throws `LIFECYCLE_HOOK_REJECTED` (409). Throwing `INPUT_VALIDATION_FAILED` (400) is equally valid if you prefer to treat a missing token as a malformed request.
|
|
246
|
-
|
|
247
|
-
## Try it
|
|
248
|
-
|
|
249
|
-
Submit a request:
|
|
250
|
-
|
|
251
|
-
```sh
|
|
252
|
-
curl -sS -X POST http://localhost:8787/api/requests \
|
|
253
|
-
-H 'content-type: application/json' \
|
|
254
|
-
-d '{"name":"Ada","email":"ada@example.test","message":"Please call me back.","turnstileToken":"<token>"}'
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
```json
|
|
258
|
-
{
|
|
259
|
-
"ok": true,
|
|
260
|
-
"data": {
|
|
261
|
-
"id": "req_01j...",
|
|
262
|
-
"collection": "requests",
|
|
263
|
-
"status": "published",
|
|
264
|
-
"version": 1,
|
|
265
|
-
"data": { "name": "Ada", "email": "ada@example.test", "message": "Please call me back.", "createdAt": 1788879363492 },
|
|
266
|
-
"authorId": null,
|
|
267
|
-
"createdAt": 1788879363492,
|
|
268
|
-
"updatedAt": 1788879363492
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
The builtin `create` returns the `EntryRow`; `status` is `published` immediately because the Schema is operational. A rejected token, with the secret configured:
|
|
274
|
-
|
|
275
|
-
```json
|
|
276
|
-
{
|
|
277
|
-
"ok": false,
|
|
278
|
-
"diagnostic": {
|
|
279
|
-
"code": "LIFECYCLE_HOOK_REJECTED",
|
|
280
|
-
"phase": "runtime",
|
|
281
|
-
"severity": "error",
|
|
282
|
-
"path": "/turnstileToken",
|
|
283
|
-
"expected": "a valid Cloudflare Turnstile token",
|
|
284
|
-
"message": "Turnstile verification failed."
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
That response is HTTP 409 and no row exists. A missing `name` is HTTP 400 `INPUT_VALIDATION_FAILED` before any hook runs.
|
|
290
|
-
|
|
291
|
-
Staff read the queue at `GET /admin/api/views/recent-requests?page=1&show=50` with a staff session; the envelope is `{ ok, data: { rows, page, show, hasMore } }`.
|
|
292
|
-
|
|
293
|
-
MCP tools:
|
|
294
|
-
|
|
295
|
-
| Surface | Tool | Origin |
|
|
296
|
-
|---|---|---|
|
|
297
|
-
| `/mcp` | `submit_request` | `submit-request-mcp` Trigger |
|
|
298
|
-
| `/mcp/staff` | `query_view_recent_requests` | `recent-requests` View |
|
|
299
|
-
| `/mcp/staff` | `create_record_requests`, `update_record_requests` | operational Schema `requests` |
|
|
300
|
-
|
|
301
|
-
An agent calling `submit_request` on `/mcp` has no browser Turnstile widget. With the secret unset the call succeeds; with the secret set it is rejected unless the agent supplies a valid token. Keep or remove `submit-request-mcp` deliberately.
|
|
302
|
-
|
|
303
|
-
## What this deliberately leaves out
|
|
304
|
-
|
|
305
|
-
- **Deduplication.** Two identical submissions create two rows. Add a `uniqueIndexes` tuple or a `before_create` lookup if duplicates matter.
|
|
306
|
-
- **Rate limiting beyond Turnstile.** The adapter applies its own request limits to Auth and Admin routes, not a per-form quota.
|
|
307
|
-
- **CRM sync.** Forwarding rows to an external system belongs in another `after_create` handler, ideally deferred through a Queue with the `${ctx.event.id}:${ctx.event.trigger}` idempotency key.
|
|
308
|
-
|
|
309
|
-
Related: [Reservation requests](./reservation.md) uses the same builtin-create shape without hooks; [Procurement approvals](./procurement-approvals.md) adds member and staff roles.
|
|
6
|
+
This example lives in the Examples hub: [`docs/examples/builtin-intake.md`](../../examples/builtin-intake.md). Turnstile and email hooks are [`docs/examples/cf-primitives-intake-hooks.md`](../../examples/cf-primitives-intake-hooks.md).
|
|
310
7
|
|
|
311
8
|
## Source
|
|
312
9
|
|
|
313
|
-
- [
|
|
314
|
-
- [`
|
|
315
|
-
- [`
|
|
316
|
-
- [`packages/mantle-spec/src/kernel/diagnostic.ts`](../../../packages/mantle-spec/src/kernel/diagnostic.ts) — `LIFECYCLE_HOOK_REJECTED` → 409
|
|
317
|
-
- [`overlays/presence/manifests/site.yaml`](https://github.com/aotter/mantle-starters/blob/a66ec0ea3aaefc09a0229b7d8ca35af630f2b55d/overlays/presence/manifests/site.yaml) — retired contact-form pattern
|
|
318
|
-
- [`overlays/presence/src/worker/features/contact/notifyContact.ts`](https://github.com/aotter/mantle-starters/blob/a66ec0ea3aaefc09a0229b7d8ca35af630f2b55d/overlays/presence/src/worker/features/contact/notifyContact.ts)
|
|
319
|
-
- [`recipes/typed-web/src/worker/lib/turnstile.ts`](https://github.com/aotter/mantle-starters/blob/a66ec0ea3aaefc09a0229b7d8ca35af630f2b55d/recipes/typed-web/src/worker/lib/turnstile.ts)
|
|
10
|
+
- [`docs/examples/builtin-intake.md`](../../examples/builtin-intake.md)
|
|
11
|
+
- [`docs/examples/cf-primitives-intake-hooks.md`](../../examples/cf-primitives-intake-hooks.md)
|
|
12
|
+
- [`docs/examples/README.md`](../../examples/README.md)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Redirects to the Turnstile and email intake hooks in the Examples hub.
|
|
3
|
+
---
|
|
4
|
+
# Intake form with bot check and notification
|
|
5
|
+
|
|
6
|
+
This example lives in the Examples hub: [`docs/examples/cf-primitives-intake-hooks.md`](../../examples/cf-primitives-intake-hooks.md). The Builder-safe Manifest is [`docs/examples/builtin-intake.md`](../../examples/builtin-intake.md).
|
|
7
|
+
|
|
8
|
+
## Source
|
|
9
|
+
|
|
10
|
+
- [`docs/examples/cf-primitives-intake-hooks.md`](../../examples/cf-primitives-intake-hooks.md)
|
|
11
|
+
- [`docs/examples/builtin-intake.md`](../../examples/builtin-intake.md)
|
|
@@ -1,218 +1,10 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: Redirects to the builtin legal documents example in the Examples hub.
|
|
3
3
|
---
|
|
4
4
|
# Legal documents and consent
|
|
5
5
|
|
|
6
|
-
This example
|
|
7
|
-
|
|
8
|
-
Each `(kind, revision, locale)` row is one legal artifact. Immutability here is not a legal-specific Runtime: it is the generic-surface `schema.readOnly: true` pattern (Admin and Staff MCP suppress generic create, update, status and delete), the unique `(kind, revision, locale)` index, and a Procedure-only write path. Host code that calls `updateDraft` (or other mutation use cases) directly can still rewrite a row — do not expose those host mutation APIs for this collection. The English Procedure description is also the MCP and WebMCP authoring instruction.
|
|
9
|
-
|
|
10
|
-
```yaml
|
|
11
|
-
apiVersion: cms.mantle.aotter.net/v1
|
|
12
|
-
kind: Schema
|
|
13
|
-
metadata:
|
|
14
|
-
name: legal-documents
|
|
15
|
-
spec:
|
|
16
|
-
title:
|
|
17
|
-
en: Legal documents
|
|
18
|
-
zh-TW: 法律文件
|
|
19
|
-
description:
|
|
20
|
-
en: Supply the site's complete, reviewed legal text. Never create empty, placeholder, or agent-invented terms. Create a new revision instead of rewriting a published document accepted by users.
|
|
21
|
-
zh-TW: 請填入網站已審閱的完整法律正文。不得建立空白、佔位或由 agent 虛構的條款;已有使用者同意的已發佈文件應建立新修訂,不得覆寫。
|
|
22
|
-
localized: true
|
|
23
|
-
lifecycle: publishing
|
|
24
|
-
uniqueIndexes:
|
|
25
|
-
- [kind, revision, locale]
|
|
26
|
-
indexes:
|
|
27
|
-
- [kind, locale, effectiveAt]
|
|
28
|
-
searchableFields: [title, revision]
|
|
29
|
-
schema:
|
|
30
|
-
type: object
|
|
31
|
-
readOnly: true
|
|
32
|
-
additionalProperties: false
|
|
33
|
-
required: [kind, revision, locale, title, body, effectiveAt]
|
|
34
|
-
properties:
|
|
35
|
-
kind: { type: string, enum: [terms, privacy] }
|
|
36
|
-
revision: { type: string, minLength: 1, maxLength: 100 }
|
|
37
|
-
locale: { type: string }
|
|
38
|
-
title: { type: string, minLength: 1, maxLength: 200 }
|
|
39
|
-
body:
|
|
40
|
-
type: string
|
|
41
|
-
minLength: 1
|
|
42
|
-
x-mcp-hint: markdown
|
|
43
|
-
description:
|
|
44
|
-
en: Complete reviewed legal text in Markdown; placeholders are not acceptable.
|
|
45
|
-
zh-TW: 已審閱的完整 Markdown 法律正文,不得使用佔位文字。
|
|
46
|
-
effectiveAt: { type: integer, minimum: 0, x-mcp-hint: timestamp-ms }
|
|
47
|
-
---
|
|
48
|
-
apiVersion: cms.mantle.aotter.net/v1
|
|
49
|
-
kind: View
|
|
50
|
-
metadata:
|
|
51
|
-
name: current-legal-document
|
|
52
|
-
spec:
|
|
53
|
-
title:
|
|
54
|
-
en: Current legal document
|
|
55
|
-
zh-TW: 現行法律文件
|
|
56
|
-
surface: public
|
|
57
|
-
from: legal-documents
|
|
58
|
-
params:
|
|
59
|
-
type: object
|
|
60
|
-
additionalProperties: false
|
|
61
|
-
required: [kind, locale]
|
|
62
|
-
properties:
|
|
63
|
-
kind: { type: string, enum: [terms, privacy] }
|
|
64
|
-
locale: { type: string }
|
|
65
|
-
fields: [id, kind, revision, locale, title, body, effectiveAt, updatedAt]
|
|
66
|
-
filter:
|
|
67
|
-
and:
|
|
68
|
-
- eq: { field: status, value: published }
|
|
69
|
-
- eq: { field: kind, value: { $param: kind } }
|
|
70
|
-
- eq: { field: locale, value: { $param: locale } }
|
|
71
|
-
orderBy:
|
|
72
|
-
- { field: effectiveAt, direction: desc }
|
|
73
|
-
limit: 1
|
|
74
|
-
---
|
|
75
|
-
apiVersion: cms.mantle.aotter.net/v1
|
|
76
|
-
kind: Procedure
|
|
77
|
-
metadata:
|
|
78
|
-
name: create-legal-document
|
|
79
|
-
spec:
|
|
80
|
-
title:
|
|
81
|
-
en: Create legal document revision
|
|
82
|
-
zh-TW: 建立法律文件修訂
|
|
83
|
-
description:
|
|
84
|
-
en: Supply the site's complete, reviewed legal text. Never create empty, placeholder, or agent-invented terms. Create a new revision instead of rewriting a published document accepted by users.
|
|
85
|
-
zh-TW: 請填入網站已審閱的完整法律正文。不得建立空白、佔位或由 agent 虛構的條款;已有使用者同意的已發佈文件應建立新修訂,不得覆寫。
|
|
86
|
-
input:
|
|
87
|
-
type: object
|
|
88
|
-
additionalProperties: false
|
|
89
|
-
required: [kind, revision, locale, title, body, effectiveAt]
|
|
90
|
-
properties:
|
|
91
|
-
kind: { type: string, enum: [terms, privacy] }
|
|
92
|
-
revision: { type: string, minLength: 1, maxLength: 100 }
|
|
93
|
-
locale: { type: string }
|
|
94
|
-
title: { type: string, minLength: 1, maxLength: 200 }
|
|
95
|
-
body:
|
|
96
|
-
type: string
|
|
97
|
-
minLength: 1
|
|
98
|
-
x-mcp-hint: markdown
|
|
99
|
-
description:
|
|
100
|
-
en: Complete reviewed legal text in Markdown; placeholders are not acceptable.
|
|
101
|
-
zh-TW: 已審閱的完整 Markdown 法律正文,不得使用佔位文字。
|
|
102
|
-
effectiveAt: { type: integer, minimum: 0, x-mcp-hint: timestamp-ms }
|
|
103
|
-
output: { type: object }
|
|
104
|
-
handler: { kind: builtin, op: create, schema: legal-documents }
|
|
105
|
-
requires:
|
|
106
|
-
auth:
|
|
107
|
-
all:
|
|
108
|
-
- ctx.user
|
|
109
|
-
- { ctx.staff: [owner, editor] }
|
|
110
|
-
---
|
|
111
|
-
apiVersion: cms.mantle.aotter.net/v1
|
|
112
|
-
kind: Trigger
|
|
113
|
-
metadata:
|
|
114
|
-
name: create-legal-document-staff
|
|
115
|
-
spec:
|
|
116
|
-
source: { kind: mcp, surface: staff }
|
|
117
|
-
target: { procedure: create-legal-document }
|
|
118
|
-
---
|
|
119
|
-
apiVersion: cms.mantle.aotter.net/v1
|
|
120
|
-
kind: Schema
|
|
121
|
-
metadata:
|
|
122
|
-
name: legal-acceptances
|
|
123
|
-
spec:
|
|
124
|
-
title:
|
|
125
|
-
en: Legal acceptances
|
|
126
|
-
zh-TW: 法律文件同意紀錄
|
|
127
|
-
description:
|
|
128
|
-
en: Append-only receipts bound by the server to the signed-in user and acceptance time.
|
|
129
|
-
zh-TW: 由伺服器綁定登入使用者與同意時間的唯增紀錄。
|
|
130
|
-
lifecycle: operational
|
|
131
|
-
uniqueIndexes:
|
|
132
|
-
- [documentId, userId]
|
|
133
|
-
indexes:
|
|
134
|
-
- [userId, acceptedAt]
|
|
135
|
-
schema:
|
|
136
|
-
type: object
|
|
137
|
-
readOnly: true
|
|
138
|
-
additionalProperties: false
|
|
139
|
-
required: [documentId, userId, acceptedAt]
|
|
140
|
-
properties:
|
|
141
|
-
documentId: { type: string, format: uuid, x-mantle-ref: legal-documents }
|
|
142
|
-
userId: { type: string, x-mantle-bind: ctx.user }
|
|
143
|
-
acceptedAt: { type: integer, x-mantle-bind: now, x-mcp-hint: timestamp-ms }
|
|
144
|
-
---
|
|
145
|
-
apiVersion: cms.mantle.aotter.net/v1
|
|
146
|
-
kind: Procedure
|
|
147
|
-
metadata:
|
|
148
|
-
name: require-published-legal-document
|
|
149
|
-
spec:
|
|
150
|
-
title: Require a published legal document
|
|
151
|
-
input:
|
|
152
|
-
type: object
|
|
153
|
-
additionalProperties: false
|
|
154
|
-
required: [documentId]
|
|
155
|
-
properties:
|
|
156
|
-
documentId: { type: string, format: uuid, x-mantle-ref: legal-documents }
|
|
157
|
-
output: { type: object }
|
|
158
|
-
handler: { kind: ref, ref: require-published-legal-document }
|
|
159
|
-
requires: { auth: { all: [ctx.user] } }
|
|
160
|
-
---
|
|
161
|
-
apiVersion: cms.mantle.aotter.net/v1
|
|
162
|
-
kind: Procedure
|
|
163
|
-
metadata:
|
|
164
|
-
name: accept-legal-document
|
|
165
|
-
spec:
|
|
166
|
-
title:
|
|
167
|
-
en: Accept legal document
|
|
168
|
-
zh-TW: 同意法律文件
|
|
169
|
-
description:
|
|
170
|
-
en: Record the signed-in user's acceptance of one published legal document revision.
|
|
171
|
-
zh-TW: 記錄登入使用者對一份已發佈法律文件修訂的同意。
|
|
172
|
-
input:
|
|
173
|
-
type: object
|
|
174
|
-
additionalProperties: false
|
|
175
|
-
required: [documentId]
|
|
176
|
-
properties:
|
|
177
|
-
documentId: { type: string, format: uuid, x-mantle-ref: legal-documents }
|
|
178
|
-
output: { type: object }
|
|
179
|
-
handler: { kind: builtin, op: create, schema: legal-acceptances }
|
|
180
|
-
requires:
|
|
181
|
-
auth: { all: [ctx.user] }
|
|
182
|
-
guard: { procedure: require-published-legal-document }
|
|
183
|
-
---
|
|
184
|
-
apiVersion: cms.mantle.aotter.net/v1
|
|
185
|
-
kind: Trigger
|
|
186
|
-
metadata:
|
|
187
|
-
name: accept-legal-document-http
|
|
188
|
-
spec:
|
|
189
|
-
source: { kind: http, method: POST, path: /api/legal/acceptances }
|
|
190
|
-
target: { procedure: accept-legal-document }
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
The guard is application code because only the application owns its database and retention policy:
|
|
194
|
-
|
|
195
|
-
```ts
|
|
196
|
-
export async function requirePublishedLegalDocument(
|
|
197
|
-
{ documentId }: { documentId: string },
|
|
198
|
-
ctx: HandlerContext<{ DB: D1Database }>,
|
|
199
|
-
) {
|
|
200
|
-
const row = await ctx.env.DB.prepare(
|
|
201
|
-
'SELECT 1 FROM "legal-documents" WHERE "_mantle_id" = ? AND "_mantle_status" = \'published\'',
|
|
202
|
-
).bind(documentId).first();
|
|
203
|
-
if (!row) throw new Error("published_legal_document_required");
|
|
204
|
-
return {};
|
|
205
|
-
}
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
Register it under the manifest ref name `require-published-legal-document`. The HTTP endpoint then records only `documentId`; `userId` and `acceptedAt` are server-bound and cannot be supplied by the caller.
|
|
209
|
-
|
|
210
|
-
Serve `/terms` and `/privacy` in application code by querying `current-legal-document` with the requested locale, falling back to the site's default locale, and rendering Markdown as escaped/sanitized HTML. Return a clear unavailable page when no reviewed document is published. The application also owns the checkbox or other consent UI, authentication, retention and export policy.
|
|
211
|
-
|
|
212
|
-
Staff MCP and Admin WebMCP expose `create_legal_document` from the explicit staff Trigger. Both use the same Procedure description, so agents are told to collect real reviewed text rather than inventing it. Root `readOnly` deliberately emits no generic update tool on Admin or Staff MCP; that is a generic-surface gate, not a storage lock. Do not add an MCP Trigger for `accept-legal-document`: accepting legal terms is an explicit user-interface action.
|
|
6
|
+
This example lives in the Examples hub: [`docs/examples/builtin-legal-documents.md`](../../examples/builtin-legal-documents.md).
|
|
213
7
|
|
|
214
8
|
## Source
|
|
215
9
|
|
|
216
|
-
- [
|
|
217
|
-
- [Authorization](../reference/authorization.md) — server-bound identity and guard Procedures
|
|
218
|
-
- [MCP and agents](../concepts/mcp-and-agents.md) — manifest-derived tool contracts
|
|
10
|
+
- [`docs/examples/builtin-legal-documents.md`](../../examples/builtin-legal-documents.md)
|