@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.
Files changed (110) hide show
  1. package/README.md +15 -10
  2. package/dist/cli/generate.d.ts +9 -0
  3. package/dist/cli/generate.d.ts.map +1 -1
  4. package/dist/cli/generate.js +40 -1
  5. package/dist/cli/generate.js.map +1 -1
  6. package/dist/cli/main.d.ts +1 -1
  7. package/dist/cli/main.d.ts.map +1 -1
  8. package/dist/cli/main.js +37 -9
  9. package/dist/cli/main.js.map +1 -1
  10. package/docs/adapter-guide.md +6 -1
  11. package/docs/adr/adr-lite-845-frontend-client.md +38 -0
  12. package/docs/agent-prompts.md +91 -0
  13. package/docs/api-mcp-authorization.md +1 -1
  14. package/docs/auth-hosting-model.md +1 -1
  15. package/docs/examples/README.md +22 -0
  16. package/docs/examples/builtin-commerce.md +269 -0
  17. package/docs/examples/builtin-intake.md +143 -0
  18. package/docs/examples/builtin-legal-documents.md +189 -0
  19. package/docs/examples/builtin-procurement.md +241 -0
  20. package/docs/examples/builtin-publication.md +241 -0
  21. package/docs/examples/builtin-reservation.md +149 -0
  22. package/docs/examples/cf-primitives-commerce-inventory.md +809 -0
  23. package/docs/examples/cf-primitives-guarded-api.md +429 -0
  24. package/docs/examples/cf-primitives-intake-hooks.md +319 -0
  25. package/docs/examples/host-chatgpt-sites/.openai/hosting.json +1 -0
  26. package/docs/examples/host-chatgpt-sites/README.md +53 -0
  27. package/docs/examples/host-chatgpt-sites/drizzle/0000_sites_users.sql +8 -0
  28. package/docs/examples/host-chatgpt-sites/drizzle/0001_mantle.sql +294 -0
  29. package/docs/examples/host-chatgpt-sites/drizzle/0002_article_cover.sql +5 -0
  30. package/docs/examples/host-chatgpt-sites/drizzle/meta/_journal.json +1 -0
  31. package/docs/examples/host-chatgpt-sites/manifests/site.yaml +27 -0
  32. package/docs/examples/host-chatgpt-sites/package.json +1 -0
  33. package/docs/examples/host-chatgpt-sites/public/site.css +1 -0
  34. package/docs/examples/host-chatgpt-sites/scripts/build.mjs +12 -0
  35. package/docs/examples/host-chatgpt-sites/scripts/check.mjs +99 -0
  36. package/docs/examples/host-chatgpt-sites/scripts/migration.mjs +9 -0
  37. package/docs/examples/host-chatgpt-sites/src/chatgpt-auth.ts +63 -0
  38. package/docs/examples/host-chatgpt-sites/src/index.ts +46 -0
  39. package/docs/examples/host-chatgpt-sites/src/mcp.ts +49 -0
  40. package/docs/examples/host-chatgpt-sites/src/media.ts +109 -0
  41. package/docs/examples/host-chatgpt-sites/src/r2-lab.ts +38 -0
  42. package/docs/examples/host-chatgpt-sites/src/storage-fingerprint.json +1 -0
  43. package/docs/examples/host-chatgpt-sites/src/web.ts +35 -0
  44. package/docs/examples/host-chatgpt-sites/tsconfig.json +1 -0
  45. package/docs/examples/host-chatgpt-sites/wrangler.jsonc +10 -0
  46. package/docs/examples/host-local-admin-otp/.dev.vars.example +3 -0
  47. package/docs/examples/host-local-admin-otp/README.md +70 -0
  48. package/docs/examples/host-local-admin-otp/ensure-dev-vars.mjs +5 -0
  49. package/docs/examples/host-local-admin-otp/package.json +29 -0
  50. package/docs/examples/host-local-admin-otp/public/.gitkeep +1 -0
  51. package/docs/examples/host-local-admin-otp/smoke.mjs +141 -0
  52. package/docs/examples/host-local-admin-otp/src/index.ts +54 -0
  53. package/docs/examples/host-local-admin-otp/wrangler.jsonc +23 -0
  54. package/docs/examples/{minimal-worker → host-minimal-worker}/README.md +9 -5
  55. package/docs/examples/host-minimal-worker/manifests/site.yaml +25 -0
  56. package/docs/examples/{minimal-worker → host-minimal-worker}/package.json +3 -3
  57. package/docs/examples/host-minimal-worker/tsconfig.json +17 -0
  58. package/docs/handbook/cloudflare/authentication.md +17 -2
  59. package/docs/handbook/cloudflare/bindings.md +9 -7
  60. package/docs/handbook/cloudflare/chatgpt-sites.md +29 -0
  61. package/docs/handbook/cloudflare/conventional-worker.md +3 -3
  62. package/docs/handbook/cloudflare/deferred-hooks-queues.md +0 -1
  63. package/docs/handbook/cloudflare/deploy-and-operate.md +14 -20
  64. package/docs/handbook/cloudflare/media-r2.md +2 -2
  65. package/docs/handbook/cloudflare/public-web.md +1 -1
  66. package/docs/handbook/cloudflare/site-chrome.md +75 -0
  67. package/docs/handbook/concepts/authorization.md +2 -2
  68. package/docs/handbook/concepts/four-atoms.md +2 -2
  69. package/docs/handbook/concepts/lifecycle-and-locales.md +1 -1
  70. package/docs/handbook/concepts/mcp-and-agents.md +1 -1
  71. package/docs/handbook/concepts/procedures-and-triggers.md +1 -1
  72. package/docs/handbook/concepts/runtime-and-adapters.md +2 -2
  73. package/docs/handbook/concepts/views.md +2 -2
  74. package/docs/handbook/examples/commerce-transaction.md +4 -806
  75. package/docs/handbook/examples/commerce.md +11 -0
  76. package/docs/handbook/examples/guarded-api.md +3 -420
  77. package/docs/handbook/examples/hub.md +10 -0
  78. package/docs/handbook/examples/intake-form.md +6 -313
  79. package/docs/handbook/examples/intake-hooks.md +11 -0
  80. package/docs/handbook/examples/legal-documents.md +3 -211
  81. package/docs/handbook/examples/procurement-approvals.md +3 -233
  82. package/docs/handbook/examples/publication.md +3 -233
  83. package/docs/handbook/examples/reservation.md +3 -213
  84. package/docs/handbook/navigation.json +17 -2
  85. package/docs/handbook/reference/authorization.md +1 -1
  86. package/docs/handbook/reference/procedure.md +2 -2
  87. package/docs/handbook/reference/schema.md +3 -3
  88. package/docs/handbook/reference/site-config.md +5 -16
  89. package/docs/handbook/reference/surface.md +3 -7
  90. package/docs/handbook/sites/equipment-checkout.md +231 -0
  91. package/docs/handbook/sites/host-reference.md +113 -0
  92. package/docs/handbook/sites/index.md +111 -0
  93. package/docs/handbook/start/project-and-cli.md +22 -14
  94. package/docs/handbook/start/quickstart-admin.md +239 -0
  95. package/docs/handbook/start/quickstart-worker.md +22 -23
  96. package/docs/migration-0.1.2.md +26 -0
  97. package/docs/release-process.md +92 -7
  98. package/docs/sealed-pipeline-ownership.md +2 -2
  99. package/docs/spec-only-host-adoption.md +3 -4
  100. package/docs/transaction-patterns.md +2 -2
  101. package/package.json +15 -15
  102. package/skills/README.md +18 -2
  103. package/skills/develop/SKILL.md +32 -23
  104. package/skills/install/SKILL.md +50 -11
  105. package/skills/provision/SKILL.md +21 -5
  106. /package/docs/examples/{minimal-worker → host-local-admin-otp}/manifests/site.yaml +0 -0
  107. /package/docs/examples/{minimal-worker → host-local-admin-otp}/tsconfig.json +0 -0
  108. /package/docs/examples/{minimal-worker → host-minimal-worker}/smoke.mjs +0 -0
  109. /package/docs/examples/{minimal-worker → host-minimal-worker}/src/index.ts +0 -0
  110. /package/docs/examples/{minimal-worker → host-minimal-worker}/wrangler.jsonc +0 -0
@@ -1,319 +1,12 @@
1
1
  ---
2
- description: A public intake form with a Turnstile bot check before the write and an email notification after it.
2
+ description: Redirects to the builtin public intake form in the Examples hub.
3
3
  ---
4
- # Intake form with bot check and notification
4
+ # Intake form
5
5
 
6
- This example collects a public request, verifies a Cloudflare Turnstile token before the row is written, and notifies staff after it is written. It extends the Builder `intake` preset with two lifecycle Triggers and two small handlers. Read it if you need any form that anonymous visitors submit.
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
- - [Procedure reference](../reference/procedure.md) — builtin table, side-channel input fields, lifecycle hooks
314
- - [`packages/mantle-runtime/src/usecase/lifecycle/RunLifecycleHooksUseCase.ts`](../../../packages/mantle-runtime/src/usecase/lifecycle/RunLifecycleHooksUseCase.ts) — abort propagates the hook's diagnostic
315
- - [`packages/mantle-runtime/src/usecase/procedure/InvokeProcedureUseCase.ts`](../../../packages/mantle-runtime/src/usecase/procedure/InvokeProcedureUseCase.ts) — `InvokeFailure`
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: Localized Terms and Privacy documents with immutable revisions, public lookup and signed-in acceptance records.
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 gives an application a portable shape for Terms of Use, Privacy Policy and consent receipts. It uses existing Mantle atoms. Core does not install it or own the public pages.
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
- - [Publication](./publication.md) — localized publishing pattern
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)