@aotter/mantle 0.1.5-alpha.1 → 0.2.0-alpha.10

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 (495) hide show
  1. package/README.md +166 -298
  2. package/dist/auth/adapter.d.ts +32 -0
  3. package/dist/auth/adapter.js +288 -0
  4. package/dist/auth/admin.d.ts +3 -0
  5. package/dist/auth/admin.js +21 -0
  6. package/dist/auth/caller.d.ts +86 -0
  7. package/dist/auth/caller.js +155 -0
  8. package/dist/auth/hardening.d.ts +24 -0
  9. package/dist/auth/hardening.js +40 -0
  10. package/dist/auth/identity.d.ts +121 -0
  11. package/dist/auth/identity.js +203 -0
  12. package/dist/auth/oauth.d.ts +63 -0
  13. package/dist/auth/oauth.js +217 -0
  14. package/dist/auth/resource-seeds.js +10 -0
  15. package/dist/auth/routes.d.ts +32 -0
  16. package/dist/auth/routes.js +226 -0
  17. package/dist/build/compiler.d.ts +721 -0
  18. package/dist/build/compiler.js +7029 -0
  19. package/dist/build/spec/domain/model/ManifestGrammar.d.ts +439 -0
  20. package/dist/build/spec/domain/model/RuntimePlan.d.ts +168 -0
  21. package/dist/build/spec/domain/model/SqlIr.d.ts +129 -0
  22. package/dist/build/spec/domain/model/UserFields.d.ts +21 -0
  23. package/dist/build/spec/domain/service/PlanFingerprint.d.ts +7 -0
  24. package/dist/build/spec/kernel/diagnostic.d.ts +108 -0
  25. package/dist/bun/assets.d.ts +2 -0
  26. package/dist/bun/assets.js +24 -0
  27. package/dist/domain/adminOrder.js +24 -0
  28. package/dist/domain/columns.js +57 -0
  29. package/dist/domain/describe/chunk-ZVXBGP7Y.js +424 -0
  30. package/dist/domain/describe/index.d.ts +58 -0
  31. package/dist/domain/describe/index.js +115 -0
  32. package/dist/domain/describe/target-hash.d.ts +8 -0
  33. package/dist/domain/describe/target-hash.js +12 -0
  34. package/dist/domain/describe/targets.d.ts +40 -0
  35. package/dist/domain/entry.js +33 -0
  36. package/dist/domain/generated-tools.js +19 -0
  37. package/dist/domain/generation.d.ts +16 -0
  38. package/dist/domain/generation.js +1 -0
  39. package/dist/domain/input.js +17 -0
  40. package/dist/domain/model.js +1 -0
  41. package/dist/domain/paging.js +230 -0
  42. package/dist/domain/recipes.js +24 -0
  43. package/dist/domain/userReferences.js +5 -0
  44. package/dist/domain/wireScalars.js +47 -0
  45. package/dist/infrastructure/adminEntries.js +153 -0
  46. package/dist/infrastructure/adminPaging.js +72 -0
  47. package/dist/infrastructure/artifacts.js +71 -0
  48. package/dist/infrastructure/authSchema.js +83 -0
  49. package/dist/infrastructure/bundle.js +188 -0
  50. package/dist/infrastructure/cli.js +118 -0
  51. package/dist/infrastructure/generate.d.ts +12 -0
  52. package/dist/infrastructure/generate.js +189 -0
  53. package/dist/infrastructure/generateAuth.d.ts +8 -0
  54. package/dist/infrastructure/generateAuth.js +74 -0
  55. package/dist/infrastructure/help.js +60 -0
  56. package/dist/infrastructure/init.js +54 -0
  57. package/dist/infrastructure/inputs.js +41 -0
  58. package/dist/infrastructure/mounts.js +25 -0
  59. package/dist/infrastructure/wrangler.js +109 -0
  60. package/dist/kernel/authorization.d.ts +4 -0
  61. package/dist/kernel/authorization.js +16 -0
  62. package/dist/kernel/binds.d.ts +60 -0
  63. package/dist/kernel/binds.js +49 -0
  64. package/dist/kernel/caller-identity.d.ts +24 -0
  65. package/dist/kernel/caller-identity.js +22 -0
  66. package/dist/kernel/contract-error.d.ts +12 -0
  67. package/dist/kernel/contract-error.js +14 -0
  68. package/dist/kernel/contracts.d.ts +143 -0
  69. package/dist/kernel/contracts.js +97 -0
  70. package/dist/kernel/cron.d.ts +1 -0
  71. package/dist/kernel/cron.js +58 -0
  72. package/dist/kernel/dialect/common.d.ts +38 -0
  73. package/dist/kernel/dialect/common.js +6 -0
  74. package/dist/kernel/dialect/postgres.d.ts +16 -0
  75. package/dist/kernel/dialect/postgres.js +18 -0
  76. package/dist/kernel/dialect/sqlite.d.ts +20 -0
  77. package/dist/kernel/dialect/sqlite.js +34 -0
  78. package/dist/kernel/drain.d.ts +5 -0
  79. package/dist/kernel/drain.js +35 -0
  80. package/dist/kernel/driver/bun-sqlite.d.ts +19 -0
  81. package/dist/kernel/driver/bun-sqlite.js +77 -0
  82. package/dist/kernel/driver/d1.d.ts +24 -0
  83. package/dist/kernel/driver/d1.js +22 -0
  84. package/dist/kernel/driver/postgres.d.ts +25 -0
  85. package/dist/kernel/driver/postgres.js +99 -0
  86. package/dist/kernel/driver/transaction-context.d.ts +10 -0
  87. package/dist/kernel/driver/transaction-context.js +29 -0
  88. package/dist/kernel/email.d.ts +28 -0
  89. package/dist/kernel/email.js +38 -0
  90. package/dist/kernel/host-access.d.ts +10 -0
  91. package/dist/kernel/host-access.js +65 -0
  92. package/dist/kernel/host-caller.d.ts +39 -0
  93. package/dist/kernel/host-caller.js +56 -0
  94. package/dist/kernel/json-body.d.ts +4 -0
  95. package/dist/kernel/json-body.js +44 -0
  96. package/dist/kernel/mcp-apps.d.ts +69 -0
  97. package/dist/kernel/mcp-apps.js +40 -0
  98. package/dist/kernel/mcp-call.d.ts +5 -0
  99. package/dist/kernel/mcp-call.js +25 -0
  100. package/dist/kernel/mcp-handler.js +371 -0
  101. package/dist/kernel/mcp.d.ts +110 -0
  102. package/dist/kernel/mcp.js +50 -0
  103. package/dist/kernel/media.d.ts +27 -0
  104. package/dist/kernel/media.js +216 -0
  105. package/dist/kernel/paging.d.ts +55 -0
  106. package/dist/kernel/paging.js +95 -0
  107. package/dist/kernel/publishing.d.ts +4 -0
  108. package/dist/kernel/publishing.js +16 -0
  109. package/dist/kernel/r2-media.d.ts +42 -0
  110. package/dist/kernel/r2-media.js +78 -0
  111. package/dist/kernel/rest-dispatch.js +38 -0
  112. package/dist/kernel/rest-router.js +64 -0
  113. package/dist/kernel/rest.d.ts +58 -0
  114. package/dist/kernel/rest.js +41 -0
  115. package/dist/kernel/scalars.d.ts +6 -0
  116. package/dist/kernel/scalars.js +44 -0
  117. package/dist/kernel/search.js +5 -0
  118. package/dist/kernel/site-types.d.ts +146 -0
  119. package/dist/kernel/site-types.js +1 -0
  120. package/dist/kernel/site.d.ts +39 -0
  121. package/dist/kernel/site.js +32 -0
  122. package/dist/kernel/store.d.ts +175 -0
  123. package/dist/kernel/store.js +269 -0
  124. package/dist/kernel/surface.d.ts +10 -0
  125. package/dist/kernel/surface.js +19 -0
  126. package/dist/kernel/sweep.d.ts +17 -0
  127. package/dist/kernel/sweep.js +27 -0
  128. package/dist/kernel/translations.d.ts +1 -0
  129. package/dist/kernel/translations.js +13 -0
  130. package/dist/kernel/user-caller.d.ts +31 -0
  131. package/dist/kernel/user-caller.js +67 -0
  132. package/dist/kernel/user-field-name.js +3 -0
  133. package/dist/kernel/user-fields.d.ts +35 -0
  134. package/dist/kernel/user-fields.js +81 -0
  135. package/dist/kernel/validation.d.ts +15 -0
  136. package/dist/kernel/validation.js +54 -0
  137. package/dist/kernel/web-gateway.js +68 -0
  138. package/dist/kernel/web-head.js +15 -0
  139. package/dist/kernel/web-index.js +40 -0
  140. package/dist/kernel/web-response.js +8 -0
  141. package/dist/kernel/web.d.ts +77 -0
  142. package/dist/kernel/web.js +124 -0
  143. package/dist/kernel/where.js +27 -0
  144. package/dist/testing/index.d.ts +1 -0
  145. package/dist/testing/index.js +1 -0
  146. package/dist/usecase/adminBindings.js +143 -0
  147. package/dist/usecase/adminClient.js +87 -0
  148. package/dist/usecase/adminDescriptors.js +107 -0
  149. package/dist/usecase/generate.js +166 -0
  150. package/dist/usecase/handlers.js +21 -0
  151. package/dist/usecase/mcp-media.js +17 -0
  152. package/dist/usecase/mcp.js +100 -0
  153. package/dist/usecase/paging.js +71 -0
  154. package/dist/usecase/publishing.js +63 -0
  155. package/dist/usecase/refusals.js +27 -0
  156. package/dist/usecase/rest.js +54 -0
  157. package/dist/usecase/search.js +7 -0
  158. package/dist/usecase/site.js +39 -0
  159. package/dist/usecase/stores.js +64 -0
  160. package/dist/usecase/tool-version.js +9 -0
  161. package/dist/usecase/translations.js +78 -0
  162. package/dist/usecase/ttl.js +20 -0
  163. package/dist/usecase/validation.js +241 -0
  164. package/dist/usecase/web.js +23 -0
  165. package/docs/examples/README.md +39 -18
  166. package/docs/examples/app-shell/.dev.vars.example +4 -0
  167. package/docs/examples/app-shell/README.md +121 -0
  168. package/docs/examples/app-shell/accept.mjs +91 -0
  169. package/docs/examples/app-shell/gitignore +7 -0
  170. package/docs/examples/app-shell/package.json +43 -0
  171. package/docs/examples/app-shell/pnpm-workspace.yaml +8 -0
  172. package/docs/examples/app-shell/smoke-kit.mjs +168 -0
  173. package/docs/examples/app-shell/src/auth.ts +58 -0
  174. package/docs/examples/app-shell/src/handlers.ts +5 -0
  175. package/docs/examples/app-shell/src/index.ts +4 -0
  176. package/docs/examples/app-shell/src/routes.ts +14 -0
  177. package/docs/examples/app-shell/src/schedule.ts +8 -0
  178. package/docs/examples/app-shell/src/service.ts +70 -0
  179. package/docs/examples/app-shell/src/sign-in.ts +41 -0
  180. package/docs/examples/app-shell/src/signup.ts +8 -0
  181. package/docs/examples/app-shell/tsconfig.json +26 -0
  182. package/docs/examples/app-shell/worker-configuration.d.ts +22 -0
  183. package/docs/examples/app-shell/wrangler.jsonc +62 -0
  184. package/docs/examples/club-events/README.md +65 -0
  185. package/docs/examples/club-events/manifests/club-events.yaml +247 -0
  186. package/docs/examples/club-events/smoke.mjs +127 -0
  187. package/docs/examples/club-events/src/handlers.ts +25 -0
  188. package/docs/examples/club-events/src/signup.ts +9 -0
  189. package/docs/examples/generated-web/README.md +78 -0
  190. package/docs/examples/generated-web/posts.yaml +38 -0
  191. package/docs/examples/generated-web/seed.sql +6 -0
  192. package/docs/examples/generated-web/src/worker.ts +43 -0
  193. package/docs/examples/generated-web/tsconfig.json +8 -0
  194. package/docs/examples/generated-web/wrangler.jsonc +8 -0
  195. package/docs/examples/purchasing/.dev.vars.example +6 -0
  196. package/docs/examples/purchasing/README.md +99 -0
  197. package/docs/examples/purchasing/manifests/purchasing.yaml +374 -0
  198. package/docs/examples/purchasing/seed.sql +4 -0
  199. package/docs/examples/purchasing/smoke.mjs +191 -0
  200. package/docs/examples/purchasing/src/admin.tsx +13 -0
  201. package/docs/examples/purchasing/src/handlers.ts +33 -0
  202. package/docs/examples/purchasing/src/notify.ts +29 -0
  203. package/docs/examples/purchasing/src/requests-page.ts +57 -0
  204. package/docs/examples/purchasing/src/routes.ts +53 -0
  205. package/docs/examples/purchasing/src/schedule.ts +31 -0
  206. package/docs/examples/purchasing/src/signup.ts +5 -0
  207. package/docs/examples/purchasing/src/supplier.ts +32 -0
  208. package/docs/examples/purchasing/wrangler.jsonc +69 -0
  209. package/docs/examples/recipes/README.md +60 -0
  210. package/docs/examples/recipes/manifests/recipes.yaml +75 -0
  211. package/docs/examples/recipes/smoke.mjs +102 -0
  212. package/docs/examples/recipes/src/media.ts +13 -0
  213. package/docs/examples/recipes/src/pages.ts +99 -0
  214. package/docs/examples/recipes/src/routes.ts +71 -0
  215. package/docs/examples/recipes/src/site-config.ts +12 -0
  216. package/docs/examples/recipes/wrangler.jsonc +74 -0
  217. package/docs/handbook/cloudflare/authentication.md +180 -322
  218. package/docs/handbook/cloudflare/chatgpt-sites.md +5 -23
  219. package/docs/handbook/cloudflare/deploy-and-operate.md +55 -77
  220. package/docs/handbook/cloudflare/media-r2.md +61 -205
  221. package/docs/handbook/cloudflare/service-entry.md +239 -0
  222. package/docs/handbook/concepts/authorization.md +10 -107
  223. package/docs/handbook/concepts/four-atoms.md +9 -168
  224. package/docs/handbook/concepts/lifecycle-and-locales.md +9 -132
  225. package/docs/handbook/concepts/mcp-and-agents.md +24 -121
  226. package/docs/handbook/concepts/procedures-and-triggers.md +5 -217
  227. package/docs/handbook/concepts/runtime-and-adapters.md +15 -169
  228. package/docs/handbook/concepts/views.md +10 -185
  229. package/docs/handbook/examples/hub.md +3 -8
  230. package/docs/handbook/guides/adapter-guide.md +11 -0
  231. package/docs/handbook/guides/admin-ui.md +115 -133
  232. package/docs/handbook/guides/agent-setup.md +64 -74
  233. package/docs/handbook/guides/agent-workflow-acceptance.md +232 -0
  234. package/docs/handbook/guides/email.md +86 -0
  235. package/docs/handbook/guides/local-testing.md +40 -0
  236. package/docs/handbook/guides/mcp-apps.md +560 -0
  237. package/docs/handbook/guides/native-admin.md +566 -0
  238. package/docs/handbook/guides/native-execution.md +133 -0
  239. package/docs/handbook/guides/oauth.md +219 -0
  240. package/docs/handbook/guides/postgres-adapters.md +56 -0
  241. package/docs/handbook/guides/public-pages.md +87 -0
  242. package/docs/handbook/guides/size-and-startup.md +9 -0
  243. package/docs/handbook/guides/sql-patterns.md +266 -0
  244. package/docs/handbook/guides/typed-queries.md +211 -107
  245. package/docs/handbook/navigation.json +176 -55
  246. package/docs/handbook/reference/admin-descriptors.md +53 -0
  247. package/docs/handbook/reference/authorization.md +39 -266
  248. package/docs/handbook/reference/describe.md +56 -0
  249. package/docs/handbook/reference/diagnostics.md +19 -186
  250. package/docs/handbook/reference/features.md +45 -55
  251. package/docs/handbook/reference/manifest.md +6 -129
  252. package/docs/handbook/reference/procedure.md +11 -303
  253. package/docs/handbook/reference/schema.md +64 -240
  254. package/docs/handbook/reference/site-config.md +5 -218
  255. package/docs/handbook/reference/sql.md +64 -0
  256. package/docs/handbook/reference/surface.md +392 -201
  257. package/docs/handbook/reference/trigger.md +56 -192
  258. package/docs/handbook/reference/view.md +24 -243
  259. package/docs/handbook/releases/index.md +244 -77
  260. package/docs/handbook/start/agents.md +45 -0
  261. package/docs/handbook/start/overview.md +9 -48
  262. package/docs/handbook/start/project-and-cli.md +51 -104
  263. package/docs/handbook/start/quickstart-worker.md +43 -178
  264. package/docs/skills/develop/SKILL.md +79 -249
  265. package/docs/skills/integrate/SKILL.md +39 -50
  266. package/docs/skills/media-gc/SKILL.md +34 -10
  267. package/docs/skills/provision/SKILL.md +36 -186
  268. package/docs/skills/update/SKILL.md +23 -38
  269. package/docs/source/scripts/postgres-adapter.ts +101 -0
  270. package/docs/upgrade-0.1-to-0.2.md +227 -0
  271. package/package.json +120 -131
  272. package/skills/README.md +48 -108
  273. package/skills/mantle/SKILL.md +76 -0
  274. package/dist/admin-ui.d.ts +0 -2
  275. package/dist/admin-ui.d.ts.map +0 -1
  276. package/dist/admin-ui.js +0 -2
  277. package/dist/admin-ui.js.map +0 -1
  278. package/dist/admin.d.ts +0 -2
  279. package/dist/admin.d.ts.map +0 -1
  280. package/dist/admin.js +0 -2
  281. package/dist/admin.js.map +0 -1
  282. package/dist/auth.d.ts +0 -2
  283. package/dist/auth.d.ts.map +0 -1
  284. package/dist/auth.js +0 -2
  285. package/dist/auth.js.map +0 -1
  286. package/dist/bun.d.ts +0 -2
  287. package/dist/bun.d.ts.map +0 -1
  288. package/dist/bun.js +0 -2
  289. package/dist/bun.js.map +0 -1
  290. package/dist/cli/generate-cloudflare.d.ts +0 -7
  291. package/dist/cli/generate-cloudflare.d.ts.map +0 -1
  292. package/dist/cli/generate-cloudflare.js +0 -123
  293. package/dist/cli/generate-cloudflare.js.map +0 -1
  294. package/dist/cli/generate-project.d.ts +0 -25
  295. package/dist/cli/generate-project.d.ts.map +0 -1
  296. package/dist/cli/generate-project.js +0 -282
  297. package/dist/cli/generate-project.js.map +0 -1
  298. package/dist/cli/generate-sites.d.ts +0 -8
  299. package/dist/cli/generate-sites.d.ts.map +0 -1
  300. package/dist/cli/generate-sites.js +0 -273
  301. package/dist/cli/generate-sites.js.map +0 -1
  302. package/dist/cli/generate.d.ts +0 -20
  303. package/dist/cli/generate.d.ts.map +0 -1
  304. package/dist/cli/generate.js +0 -299
  305. package/dist/cli/generate.js.map +0 -1
  306. package/dist/cli/harness.d.ts +0 -3
  307. package/dist/cli/harness.d.ts.map +0 -1
  308. package/dist/cli/harness.js +0 -214
  309. package/dist/cli/harness.js.map +0 -1
  310. package/dist/cli/main.d.ts +0 -3
  311. package/dist/cli/main.d.ts.map +0 -1
  312. package/dist/cli/main.js +0 -77
  313. package/dist/cli/main.js.map +0 -1
  314. package/dist/cli/skills.d.ts +0 -3
  315. package/dist/cli/skills.d.ts.map +0 -1
  316. package/dist/cli/skills.js +0 -128
  317. package/dist/cli/skills.js.map +0 -1
  318. package/dist/cloudflare.d.ts +0 -2
  319. package/dist/cloudflare.d.ts.map +0 -1
  320. package/dist/cloudflare.js +0 -2
  321. package/dist/cloudflare.js.map +0 -1
  322. package/dist/codegen/emitMantleModule.d.ts +0 -22
  323. package/dist/codegen/emitMantleModule.d.ts.map +0 -1
  324. package/dist/codegen/emitMantleModule.js +0 -291
  325. package/dist/codegen/emitMantleModule.js.map +0 -1
  326. package/dist/codegen.d.ts +0 -2
  327. package/dist/codegen.d.ts.map +0 -1
  328. package/dist/codegen.js +0 -2
  329. package/dist/codegen.js.map +0 -1
  330. package/dist/runtime-testing.d.ts +0 -2
  331. package/dist/runtime-testing.d.ts.map +0 -1
  332. package/dist/runtime-testing.js +0 -2
  333. package/dist/runtime-testing.js.map +0 -1
  334. package/dist/runtime.d.ts +0 -2
  335. package/dist/runtime.d.ts.map +0 -1
  336. package/dist/runtime.js +0 -2
  337. package/dist/runtime.js.map +0 -1
  338. package/dist/spec.d.ts +0 -2
  339. package/dist/spec.d.ts.map +0 -1
  340. package/dist/spec.js +0 -2
  341. package/dist/spec.js.map +0 -1
  342. package/dist/vercel-libsql.d.ts +0 -2
  343. package/dist/vercel-libsql.d.ts.map +0 -1
  344. package/dist/vercel-libsql.js +0 -2
  345. package/dist/vercel-libsql.js.map +0 -1
  346. package/dist/vercel.d.ts +0 -2
  347. package/dist/vercel.d.ts.map +0 -1
  348. package/dist/vercel.js +0 -2
  349. package/dist/vercel.js.map +0 -1
  350. package/dist/web.d.ts +0 -2
  351. package/dist/web.d.ts.map +0 -1
  352. package/dist/web.js +0 -2
  353. package/dist/web.js.map +0 -1
  354. package/docs/adapter-guide.md +0 -222
  355. package/docs/adr/0001-four-atom-manifest-model.md +0 -440
  356. package/docs/adr/0002-closed-enums-for-bindings.md +0 -253
  357. package/docs/adr/0007-ai-as-primary-author.md +0 -473
  358. package/docs/adr/0008-structured-diagnostic-shape.md +0 -338
  359. package/docs/adr/0009-consumer-supplied-manifests.md +0 -149
  360. package/docs/adr/0010-locale-and-translates.md +0 -453
  361. package/docs/adr/0011-adapter-port-spec.md +0 -281
  362. package/docs/adr/0012-views-as-public-rest.md +0 -161
  363. package/docs/adr/0013-agent-provisioned-consumer-projects.md +0 -154
  364. package/docs/adr/0014-auth-better-auth-and-multi-tenant-mcp.md +0 -900
  365. package/docs/adr/0016-site-semantic-layer.md +0 -70
  366. package/docs/adr/0017-media-multi-variant-agent-side-optimization.md +0 -181
  367. package/docs/adr/0018-core-starters-repository-boundary.md +0 -179
  368. package/docs/adr/0019-sealed-manifest-runtime-pipeline.md +0 -240
  369. package/docs/adr/0020-builtin-handler-contracts-and-matched-upsert.md +0 -199
  370. package/docs/adr/0021-retire-starter-scaffolding.md +0 -30
  371. package/docs/adr/0022-caller-observed-version-occ.md +0 -212
  372. package/docs/adr/0023-port-failure-contract.md +0 -69
  373. package/docs/adr/0024-manifest-native-schema-tables.md +0 -264
  374. package/docs/adr/0025-reserved-native-columns-and-published-only-public-views.md +0 -105
  375. package/docs/adr/0026-progressive-generate.md +0 -36
  376. package/docs/adr/0027-scheduled-procedure-triggers.md +0 -31
  377. package/docs/adr/0028-schema-ttl-expiry.md +0 -33
  378. package/docs/adr/README.md +0 -81
  379. package/docs/adr/adr-lite-803-request-diagnostics.md +0 -38
  380. package/docs/adr/adr-lite-808-route-readiness.md +0 -47
  381. package/docs/adr/adr-lite-809-bounded-public-content.md +0 -71
  382. package/docs/adr/adr-lite-812-native-parity.md +0 -150
  383. package/docs/adr/adr-lite-823-home-statistics.md +0 -64
  384. package/docs/adr/adr-lite-842-mcp-authoring.md +0 -30
  385. package/docs/adr/adr-lite-845-frontend-client.md +0 -38
  386. package/docs/adr/adr-lite-861-admin-webmcp.md +0 -42
  387. package/docs/adr/adr-lite-909-admin-ui-kit.md +0 -37
  388. package/docs/agent-prompts.md +0 -64
  389. package/docs/api-mcp-authorization.md +0 -5
  390. package/docs/assets/mantle-admin-developer.png +0 -0
  391. package/docs/assets/mantle-admin-operations.png +0 -0
  392. package/docs/assets/mantle-hero.jpg +0 -0
  393. package/docs/auth-hosting-model.md +0 -166
  394. package/docs/cloudflare-low-level-composition.md +0 -5
  395. package/docs/consumer-onboarding-audit.md +0 -65
  396. package/docs/deferred-lifecycle-queues.md +0 -5
  397. package/docs/design-atoms.md +0 -16
  398. package/docs/design-references/editorial-blog-2026-05-05.md +0 -218
  399. package/docs/direct-authoring.md +0 -5
  400. package/docs/examples/builtin-commerce.md +0 -269
  401. package/docs/examples/builtin-intake.md +0 -143
  402. package/docs/examples/builtin-legal-documents.md +0 -189
  403. package/docs/examples/builtin-procurement.md +0 -241
  404. package/docs/examples/builtin-publication.md +0 -241
  405. package/docs/examples/builtin-reservation.md +0 -149
  406. package/docs/examples/cf-primitives-commerce-inventory.md +0 -809
  407. package/docs/examples/cf-primitives-guarded-api.md +0 -429
  408. package/docs/examples/cf-primitives-intake-hooks.md +0 -319
  409. package/docs/examples/host-chatgpt-sites/.openai/hosting.json +0 -1
  410. package/docs/examples/host-chatgpt-sites/README.md +0 -70
  411. package/docs/examples/host-chatgpt-sites/drizzle/0000_sites_users.sql +0 -8
  412. package/docs/examples/host-chatgpt-sites/drizzle/0001_mantle.sql +0 -294
  413. package/docs/examples/host-chatgpt-sites/drizzle/0002_article_cover.sql +0 -5
  414. package/docs/examples/host-chatgpt-sites/drizzle/0003_store_instance_id.sql +0 -1
  415. package/docs/examples/host-chatgpt-sites/drizzle/0004_mantle.sql +0 -10
  416. package/docs/examples/host-chatgpt-sites/drizzle/0005_mantle.sql +0 -24
  417. package/docs/examples/host-chatgpt-sites/drizzle/meta/_journal.json +0 -1
  418. package/docs/examples/host-chatgpt-sites/drizzle/meta/mantle-state.json +0 -60
  419. package/docs/examples/host-chatgpt-sites/manifests/site.yaml +0 -27
  420. package/docs/examples/host-chatgpt-sites/package.json +0 -1
  421. package/docs/examples/host-chatgpt-sites/public/site.css +0 -1
  422. package/docs/examples/host-chatgpt-sites/scripts/build.mjs +0 -13
  423. package/docs/examples/host-chatgpt-sites/scripts/check.mjs +0 -101
  424. package/docs/examples/host-chatgpt-sites/scripts/migration.mjs +0 -43
  425. package/docs/examples/host-chatgpt-sites/src/chatgpt-auth.ts +0 -66
  426. package/docs/examples/host-chatgpt-sites/src/index.ts +0 -46
  427. package/docs/examples/host-chatgpt-sites/src/mcp.ts +0 -49
  428. package/docs/examples/host-chatgpt-sites/src/media.ts +0 -109
  429. package/docs/examples/host-chatgpt-sites/src/r2-lab.ts +0 -38
  430. package/docs/examples/host-chatgpt-sites/src/storage-fingerprint.json +0 -1
  431. package/docs/examples/host-chatgpt-sites/src/web.ts +0 -35
  432. package/docs/examples/host-chatgpt-sites/tsconfig.json +0 -1
  433. package/docs/examples/host-chatgpt-sites/wrangler.jsonc +0 -10
  434. package/docs/examples/host-local-admin-otp/.dev.vars.example +0 -3
  435. package/docs/examples/host-local-admin-otp/README.md +0 -80
  436. package/docs/examples/host-local-admin-otp/ensure-dev-vars.mjs +0 -5
  437. package/docs/examples/host-local-admin-otp/manifests/site.yaml +0 -25
  438. package/docs/examples/host-local-admin-otp/package.json +0 -29
  439. package/docs/examples/host-local-admin-otp/public/.gitkeep +0 -1
  440. package/docs/examples/host-local-admin-otp/smoke.mjs +0 -141
  441. package/docs/examples/host-local-admin-otp/src/index.ts +0 -54
  442. package/docs/examples/host-local-admin-otp/tsconfig.json +0 -17
  443. package/docs/examples/host-local-admin-otp/wrangler.jsonc +0 -23
  444. package/docs/examples/host-minimal-worker/README.md +0 -41
  445. package/docs/examples/host-minimal-worker/manifests/site.yaml +0 -25
  446. package/docs/examples/host-minimal-worker/package.json +0 -26
  447. package/docs/examples/host-minimal-worker/smoke.mjs +0 -23
  448. package/docs/examples/host-minimal-worker/src/index.ts +0 -4
  449. package/docs/examples/host-minimal-worker/tsconfig.json +0 -17
  450. package/docs/examples/host-minimal-worker/wrangler.jsonc +0 -22
  451. package/docs/handbook/chatgpt-sites/equipment-checkout.md +0 -231
  452. package/docs/handbook/chatgpt-sites/generated-app.md +0 -84
  453. package/docs/handbook/chatgpt-sites/host-reference.md +0 -113
  454. package/docs/handbook/chatgpt-sites/index.md +0 -133
  455. package/docs/handbook/cloudflare/bindings.md +0 -291
  456. package/docs/handbook/cloudflare/conventional-worker.md +0 -166
  457. package/docs/handbook/cloudflare/deferred-hooks-queues.md +0 -170
  458. package/docs/handbook/cloudflare/low-level-composition.md +0 -169
  459. package/docs/handbook/cloudflare/public-web.md +0 -144
  460. package/docs/handbook/cloudflare/site-chrome.md +0 -75
  461. package/docs/handbook/examples/commerce-transaction.md +0 -11
  462. package/docs/handbook/examples/commerce.md +0 -11
  463. package/docs/handbook/examples/guarded-api.md +0 -10
  464. package/docs/handbook/examples/intake-form.md +0 -12
  465. package/docs/handbook/examples/intake-hooks.md +0 -11
  466. package/docs/handbook/examples/legal-documents.md +0 -10
  467. package/docs/handbook/examples/procurement-approvals.md +0 -10
  468. package/docs/handbook/examples/publication.md +0 -10
  469. package/docs/handbook/examples/reservation.md +0 -10
  470. package/docs/handbook/start/quickstart-admin.md +0 -52
  471. package/docs/labels.md +0 -71
  472. package/docs/media-uploads.md +0 -5
  473. package/docs/migration-0.1.2.md +0 -13
  474. package/docs/performance-harness.md +0 -228
  475. package/docs/release-process.md +0 -244
  476. package/docs/schema-indexes.md +0 -5
  477. package/docs/sealed-pipeline-ownership.md +0 -101
  478. package/docs/skills/plugin/SKILL.md +0 -132
  479. package/docs/skills/theme/SKILL.md +0 -77
  480. package/docs/spec-only-host-adoption.md +0 -157
  481. package/docs/transaction-patterns.md +0 -5
  482. package/skills/develop/SKILL.md +0 -258
  483. package/skills/install/SKILL.md +0 -112
  484. package/skills/integrate/SKILL.md +0 -60
  485. package/skills/media-gc/SKILL.md +0 -85
  486. package/skills/plugin/SKILL.md +0 -132
  487. package/skills/provision/SKILL.md +0 -194
  488. package/skills/theme/SKILL.md +0 -77
  489. package/skills/update/SKILL.md +0 -51
  490. package/templates/chatgpt-sites/auth.ts +0 -66
  491. package/templates/chatgpt-sites/build.mjs +0 -14
  492. package/templates/chatgpt-sites/env.ts +0 -1
  493. package/templates/chatgpt-sites/mcp.ts +0 -48
  494. package/templates/chatgpt-sites/public-mcp.ts +0 -29
  495. package/templates/chatgpt-sites/smoke-local.mjs +0 -20
@@ -0,0 +1,121 @@
1
+ /** Optional identity operations. Better Auth owns access control, writes and user hooks. */
2
+ import { type AuthInstance } from "./caller.js";
3
+ import { type Caller, type Postgres, type Unsafe } from "../kernel/contracts.js";
4
+ import type { Dialect } from "../kernel/dialect/common.js";
5
+ import { type Reader } from "../kernel/driver/d1.js";
6
+ type User = {
7
+ id: string;
8
+ email: string;
9
+ name: string;
10
+ role?: string | null;
11
+ githubLogin?: string | null;
12
+ emailVerified: boolean | number;
13
+ createdAt: string | Date;
14
+ };
15
+ type Api = {
16
+ listUsers(input: {
17
+ headers: Headers;
18
+ query: Record<string, unknown>;
19
+ }): Promise<{
20
+ users: User[];
21
+ total: number;
22
+ }>;
23
+ setRole(input: {
24
+ headers: Headers;
25
+ body: {
26
+ userId: string;
27
+ role: never;
28
+ };
29
+ }): Promise<unknown>;
30
+ createUser(input: {
31
+ headers: Headers;
32
+ body: {
33
+ email: string;
34
+ name: string;
35
+ role: never;
36
+ };
37
+ }): Promise<{
38
+ user: User;
39
+ }>;
40
+ removeUser(input: {
41
+ headers: Headers;
42
+ body: {
43
+ userId: string;
44
+ };
45
+ }): Promise<unknown>;
46
+ userHasPermission(input: {
47
+ headers: Headers;
48
+ body: {
49
+ permissions: {
50
+ user: ("list" | "delete")[];
51
+ };
52
+ };
53
+ }): Promise<{
54
+ success: boolean;
55
+ }>;
56
+ };
57
+ /** Requires Better Auth's official admin plugin. Every API receives the request's headers. */
58
+ export interface IdentityAuth extends AuthInstance {
59
+ readonly api: AuthInstance["api"] & Api;
60
+ }
61
+ type Read = (sql: string, values: unknown[]) => PromiseLike<any[]>;
62
+ type Member = {
63
+ id: string;
64
+ email: string;
65
+ name: string;
66
+ role: string | null;
67
+ githubLogin: string | null;
68
+ emailVerified: boolean;
69
+ createdAt: Date;
70
+ };
71
+ declare function directory(auth: IdentityAuth, _caller: Caller, headers: Headers, d: Dialect, read: Read): {
72
+ adminRoles: string[];
73
+ listUsers(): Promise<Member[]>;
74
+ listMembers({ search, limit, cursor, cursorDirection }?: {
75
+ search?: string;
76
+ limit?: number;
77
+ cursor?: string;
78
+ cursorDirection?: "forward" | "backward";
79
+ }): Promise<{
80
+ items: Member[];
81
+ previousCursor: string | null;
82
+ nextCursor: string | null;
83
+ }>;
84
+ setUserRole(id: string, role: string | readonly string[] | null): Promise<boolean>;
85
+ inviteUser(email: string, role?: string): Promise<{
86
+ kind: "created";
87
+ id: string;
88
+ } | {
89
+ kind: "exists";
90
+ id: string;
91
+ }>;
92
+ revokeInvite(id: string): Promise<boolean>;
93
+ deleteUser: (id: string) => Promise<boolean>;
94
+ };
95
+ export type IdentityDirectory = ReturnType<typeof directory>;
96
+ export declare const d1Identity: (db: Reader, auth: IdentityAuth, caller: Caller, headers: Headers) => IdentityDirectory;
97
+ export declare const postgresIdentity: (db: Unsafe, auth: IdentityAuth, caller: Caller, headers: Headers) => IdentityDirectory;
98
+ /**
99
+ * Server-side reads of Better Auth users for code without a session (cron, system callers): no SQL against Better Auth's
100
+ * tables, each instance's own table mapping, banned users excluded. Read-only; writes stay with the caller-bound directory.
101
+ */
102
+ export declare function readIdentity(auth: AuthInstance): {
103
+ /** The active user with this id, or null. */
104
+ user: (id: string) => Promise<Member | null>;
105
+ /** Active users in the admin layer (`adminRoles` and `adminUserIds`). ponytail: reads up to 10,000 per role; page it if an app has more admins. */
106
+ admins: () => Promise<Member[]>;
107
+ };
108
+ export type BootstrapRule = {
109
+ match: "email" | "github-login";
110
+ value: string;
111
+ };
112
+ type BootstrapUser = {
113
+ id: string;
114
+ email?: string | null;
115
+ githubLogin?: string | null;
116
+ emailVerified?: boolean;
117
+ };
118
+ type BootstrapAuth = Pick<AuthInstance, "options"> | (() => Pick<AuthInstance, "options">);
119
+ export declare function d1BootstrapAdmin(db: Reader, rule: BootstrapRule, auth: BootstrapAuth): (user: BootstrapUser) => Promise<void>;
120
+ export declare function postgresBootstrapAdmin(db: Postgres, rule: BootstrapRule, source: BootstrapAuth): (user: BootstrapUser) => Promise<void>;
121
+ export {};
@@ -0,0 +1,203 @@
1
+ /** Optional identity operations. Better Auth owns access control, writes and user hooks. */
2
+ import { adminOptionsOf, isBanned } from "./caller.js";
3
+ import { authNames } from "./adapter.js";
4
+ import { APIError } from "@better-auth/core/error";
5
+ import { ContractError } from "../kernel/contracts.js";
6
+ import { adminLayerOf, rolesFrom } from "../kernel/user-caller.js";
7
+ import * as sqlite from "../kernel/dialect/sqlite.js";
8
+ import * as postgres from "../kernel/dialect/postgres.js";
9
+ import { read as d1Read } from "../kernel/driver/d1.js";
10
+ import { read as pgRead, transaction } from "../kernel/driver/postgres.js";
11
+ const invalid = (message) => new ContractError("INPUT_VALIDATION_FAILED", message, "identity");
12
+ const project = ({ id, email, name, role, githubLogin, emailVerified, createdAt }) => ({ id, email, name, role: role ?? null, githubLogin: githubLogin ?? null, emailVerified: Boolean(emailVerified), createdAt: new Date(createdAt) });
13
+ const literal = (value) => "'" + value.replaceAll("'", "''") + "'";
14
+ const adminSql = (d, auth) => {
15
+ const { column } = authNames(auth.options), { adminRoles, adminUserIds } = adminOptionsOf(auth);
16
+ const col = (field) => column("user", field);
17
+ return '(' + [...adminRoles.map(role => `${d === sqlite.dialect ? "instr" : "strpos"}(',' || COALESCE(${col("role")}, '') || ',', ${literal(',' + role + ',')}) > 0`), ...adminUserIds.map(id => `${col("id")} = ${literal(id)}`), 'FALSE'].join(' OR ') + ')';
18
+ };
19
+ /** Translate Better Auth errors at the selected transport boundary, retaining its status and code. */
20
+ async function api(run) {
21
+ try {
22
+ return await run();
23
+ }
24
+ catch (error) {
25
+ if (error instanceof APIError)
26
+ throw new ContractError(error.body?.code ?? "AUTH_DENIED", error.message, "identity", { status: error.statusCode, cause: error });
27
+ throw error;
28
+ }
29
+ }
30
+ function directory(auth, _caller, headers, d, read) {
31
+ const options = adminOptionsOf(auth), roles = options.adminRoles, privileged = adminSql(d, auth);
32
+ const { table, column, has } = authNames(auth.options), col = (field) => column("user", field);
33
+ const createdAt = d === sqlite.dialect ? col("createdAt") : postgres.wireTimestamp(col("createdAt")).join("");
34
+ const grantRole = (role = roles[0]) => { if (!role || !roles.includes(role))
35
+ throw invalid("Role must be configured in Better Auth adminRoles."); return role; };
36
+ const allowed = async (action) => {
37
+ if (!(await api(() => auth.api.userHasPermission({ headers, body: { permissions: { user: [action] } } }))).success)
38
+ throw new ContractError("AUTH_DENIED", `Requires Better Auth user:${action}.`, "identity");
39
+ };
40
+ const context = async () => await auth.$context;
41
+ const find = async (id) => (await context()).adapter.findOne({ model: "user", where: [{ field: "id", value: id }] });
42
+ const set = async (id, role) => {
43
+ try {
44
+ await api(() => auth.api.setRole({ headers, body: { userId: id, role: [...(role.length ? role : rolesFrom(options.defaultRole))] } }));
45
+ return true;
46
+ }
47
+ catch (error) {
48
+ if (error instanceof ContractError && error.code === "USER_NOT_FOUND")
49
+ return false;
50
+ throw error;
51
+ }
52
+ };
53
+ const remove = async (id) => {
54
+ try {
55
+ const found = await find(id);
56
+ // Better Auth deletes sessions and accounts before the adapter refuses the last admin row; refuse first.
57
+ if (found && adminLayerOf(found, options) && !(await read(`SELECT 1 FROM ${table("user")} WHERE ${privileged} AND ${col("id")} <> ${literal(id)} AND COALESCE(${col("banned")}, ${d === sqlite.dialect ? "0" : "FALSE"}) = ${d === sqlite.dialect ? "0" : "FALSE"} LIMIT 1`, [])).length)
58
+ throw new ContractError("CONFLICT", "This is the last admin; promote another admin before removing this one.", "identity", { status: 409 });
59
+ await api(() => auth.api.removeUser({ headers, body: { userId: id } }));
60
+ // The admin plugin removes accounts and sessions, but email-OTP proofs are keyed by email.
61
+ if (found)
62
+ await (await context()).adapter.deleteMany({ model: "verification", where: [{ field: "identifier", operator: "in", value: ["sign-in", "email-verification", "forget-password"].map(t => `${t}-otp-${found.email.toLowerCase()}`) }] });
63
+ return true;
64
+ }
65
+ catch (error) {
66
+ if (error instanceof ContractError && error.code === "USER_NOT_FOUND")
67
+ return false;
68
+ throw error;
69
+ }
70
+ };
71
+ return {
72
+ adminRoles: roles,
73
+ async listUsers() {
74
+ const query = { sortBy: "createdAt", sortDirection: "asc" };
75
+ let { users, total } = await api(() => auth.api.listUsers({ headers, query }));
76
+ if (users.length < total)
77
+ ({ users } = await api(() => auth.api.listUsers({ headers, query: { ...query, limit: total } })));
78
+ return users.filter(user => adminLayerOf(user, options)).map(project);
79
+ },
80
+ async listMembers({ search = "", limit = 50, cursor, cursorDirection = "forward" } = {}) {
81
+ await allowed("list");
82
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100 || search.length > 200 || !["forward", "backward"].includes(cursorDirection))
83
+ throw invalid("Invalid member page options.");
84
+ const term = search.trim().toLowerCase(), scope = JSON.stringify(["members", term]);
85
+ let key;
86
+ if (cursor) {
87
+ try {
88
+ const v = JSON.parse(decodeURIComponent(cursor.slice(2)));
89
+ if (!cursor.startsWith("m:") || !Array.isArray(v) || v.length !== 3 || v[0] !== scope || typeof v[1] !== "string" || !Number.isFinite(Date.parse(v[1])) || typeof v[2] !== "string" || !v[2])
90
+ throw 0;
91
+ key = [v[1], v[2]];
92
+ }
93
+ catch {
94
+ throw invalid("Invalid member cursor for this search.");
95
+ }
96
+ }
97
+ const values = [], bind = (v) => (values.push(v), d.param(values.length));
98
+ const conditions = [`NOT ${privileged}`];
99
+ if (term) {
100
+ const like = bind(`%${d.escapeLike(term)}%`);
101
+ conditions.push(`(${[`CAST(${col("id")} AS text)`, col("name"), col("email")].map(c => `${c} ${d.like(true)} ${like}${d.likeEscape}`).join(" OR ")})`);
102
+ }
103
+ const backward = cursorDirection === "backward", order = backward ? "DESC" : "ASC", op = backward ? "<" : ">";
104
+ if (key) {
105
+ const time = d.cast(bind(key[0]), "timestamptz"), id = bind(key[1]);
106
+ conditions.push(`(${col("createdAt")} ${op} ${time} OR (${col("createdAt")} = ${time} AND ${col("id")} ${op} ${id}))`);
107
+ }
108
+ const rows = await read(`SELECT ${["id", "email", "name", "role", "githubLogin", "emailVerified"].map(field => `${has("user", field) ? col(field) : "NULL"} AS "${field}"`).join(",")},${createdAt} AS "createdAt" FROM ${table("user")} WHERE ${conditions.join(" AND ")} ORDER BY ${col("createdAt")} ${order},${col("id")} ${order} LIMIT ${limit + 1}`, values);
109
+ const page = rows.slice(0, limit);
110
+ if (backward)
111
+ page.reverse();
112
+ const encode = (row) => `m:${encodeURIComponent(JSON.stringify([scope, typeof row.createdAt === "string" ? row.createdAt : row.createdAt.toISOString(), row.id]))}`;
113
+ return { items: page.map(project), previousCursor: (backward ? rows.length > limit : !!key) && page[0] ? encode(page[0]) : null, nextCursor: (backward ? !!key : rows.length > limit) && page.at(-1) ? encode(page.at(-1)) : null };
114
+ },
115
+ async setUserRole(id, role) {
116
+ const user = await find(id), current = rolesFrom(user?.role);
117
+ return set(id, role === null ? current.filter(name => !roles.includes(name)) : typeof role === "string" ? [...new Set([...current, grantRole(role)])] : role);
118
+ },
119
+ async inviteUser(email, role) {
120
+ const adminRole = grantRole(role), normalized = email.trim().toLowerCase();
121
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized))
122
+ throw invalid("Invalid invitation email.");
123
+ try {
124
+ const { user } = await api(() => auth.api.createUser({ headers, body: { email: normalized, name: normalized.split("@")[0], role: [adminRole] } }));
125
+ return { kind: "created", id: user.id };
126
+ }
127
+ catch (error) {
128
+ if (!(error instanceof ContractError) || error.code !== "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL")
129
+ throw error;
130
+ const { users: [found] } = await api(() => auth.api.listUsers({ headers, query: { filterField: "email", filterValue: normalized, limit: 1 } }));
131
+ if (!found)
132
+ throw error;
133
+ if (!found.emailVerified)
134
+ throw new ContractError("AUTH_DENIED", "The existing account must verify its email before receiving admin access.", "identity:invitation");
135
+ await set(found.id, [...new Set([...rolesFrom(found.role), adminRole])]);
136
+ return { kind: "exists", id: found.id };
137
+ }
138
+ },
139
+ async revokeInvite(id) {
140
+ await allowed("delete");
141
+ const user = await find(id);
142
+ if (!user || !adminLayerOf(user, options) || user.emailVerified || await (await context()).adapter.findOne({ model: "account", where: [{ field: "userId", value: id }] }))
143
+ return false;
144
+ return remove(id);
145
+ },
146
+ deleteUser: remove,
147
+ };
148
+ }
149
+ export const d1Identity = (db, auth, caller, headers) => directory(auth, caller, headers, sqlite.dialect, (sql, v) => d1Read(db, sql, v));
150
+ export const postgresIdentity = (db, auth, caller, headers) => directory(auth, caller, headers, postgres.dialect, (sql, v) => pgRead(db, sql, v));
151
+ /**
152
+ * Server-side reads of Better Auth users for code without a session (cron, system callers): no SQL against Better Auth's
153
+ * tables, each instance's own table mapping, banned users excluded. Read-only; writes stay with the caller-bound directory.
154
+ */
155
+ export function readIdentity(auth) {
156
+ const options = adminOptionsOf(auth), context = async () => await auth.$context;
157
+ const active = (user) => user && !isBanned(user) ? project(user) : null;
158
+ return {
159
+ /** The active user with this id, or null. */
160
+ user: async (id) => active(await (await context()).internalAdapter.findUserById(id)),
161
+ /** Active users in the admin layer (`adminRoles` and `adminUserIds`). ponytail: reads up to 10,000 per role; page it if an app has more admins. */
162
+ admins: async () => {
163
+ const { adapter } = await context();
164
+ const found = new Map();
165
+ for (const role of options.adminRoles)
166
+ for (const user of await adapter.findMany({ model: "user", where: [{ field: "role", operator: "contains", value: role }], limit: 10_000 }))
167
+ found.set(user.id, user);
168
+ if (options.adminUserIds.length)
169
+ for (const user of await adapter.findMany({ model: "user", where: [{ field: "id", operator: "in", value: [...options.adminUserIds] }], limit: 10_000 }))
170
+ found.set(user.id, user);
171
+ return [...found.values()].filter(user => adminLayerOf(user, options)).map(active).filter((user) => !!user);
172
+ },
173
+ };
174
+ }
175
+ const matches = (rule, user) => (rule.match === "email" ? user.emailVerified === true ? user.email : null : user.githubLogin)?.trim().toLowerCase() === rule.value.trim().toLowerCase();
176
+ const promote = (d, param, source) => {
177
+ const auth = typeof source === "function" ? source() : source, { adminRoles: roles } = adminOptionsOf(auth);
178
+ if (!roles[0])
179
+ throw new ContractError("AUTH_CONFIGURATION_ERROR", "Bootstrap requires Better Auth adminRoles.", "identity");
180
+ const { table, column } = authNames(auth.options), role = column("user", "role"), id = column("user", "id");
181
+ return `UPDATE ${table("user")} SET ${role}=${literal(roles[0])} || CASE WHEN COALESCE(${role}, '') = '' THEN '' ELSE ',' || ${role} END WHERE ${id}=${param} AND NOT EXISTS (SELECT 1 FROM ${table("user")} WHERE ${adminSql(d, auth)}) RETURNING ${id}`;
182
+ };
183
+ /** First-admin bootstrap has no authorized session for setRole; one conditional write elects a single admin. */
184
+ function bootstrap(rule, update) {
185
+ if (!rule.value.trim())
186
+ throw invalid("Bootstrap target cannot be empty.");
187
+ return async (user) => {
188
+ if (!matches(rule, user))
189
+ return;
190
+ if (!(await update(user.id)).length)
191
+ console.warn("[mantle bootstrap] promotion blocked: admin already exists.");
192
+ };
193
+ }
194
+ export function d1BootstrapAdmin(db, rule, auth) {
195
+ return bootstrap(rule, id => d1Read(db, promote(sqlite.dialect, "?", auth), [id]));
196
+ }
197
+ export function postgresBootstrapAdmin(db, rule, source) {
198
+ return bootstrap(rule, id => transaction(db, async (tx) => {
199
+ const auth = typeof source === "function" ? source() : source;
200
+ await tx.unsafe("SELECT pg_advisory_xact_lock(hashtext($1))", [authNames(auth.options).table("user")]);
201
+ return tx.unsafe(promote(postgres.dialect, "$1", auth), [id]);
202
+ }, { isolation: "read committed" }, "identity/bootstrap"));
203
+ }
@@ -0,0 +1,63 @@
1
+ import type { oauthProvider } from "@better-auth/oauth-provider";
2
+ import type { AuthInstance } from "./caller.js";
3
+ /**
4
+ * Fail closed for unbound tokens; reconsent cannot resurrect an issued JWT. As in Better Auth, an `offline_access` grant outlives
5
+ * the browser session: without a live session it stands on an unrevoked, unexpired offline refresh token for the same user and client.
6
+ */
7
+ export declare function assertActiveUserGrant(auth: AuthInstance, claims: Record<string, unknown>, audience: string | readonly string[]): Promise<{
8
+ id: string;
9
+ }>;
10
+ /** Add first to oauthProvider/mcp extensions; Better Auth retains ownership of referenceId. */
11
+ export declare const oauthGrantExtension: NonNullable<Parameters<typeof oauthProvider>[0]["extensions"]>[number];
12
+ export declare const OAUTH_GRANT_REVOCATION = "mantle-oauth-grant-revocation";
13
+ /**
14
+ * Add to the Better Auth plugins with the OAuth provider. An `offline_access` grant outlives sign-out and session expiry
15
+ * (as in Better Auth), but a successful password reset, a password change that signs out other devices, an admin ban
16
+ * (`/admin/ban-user`, or `/admin/update-user` setting `banned: true`), an admin session revocation or an admin password
17
+ * change ends every OAuth grant of that user: tokens, consents and unredeemed codes. So does an email OTP or magic-link
18
+ * sign-in attempt on an unverified user: Better Auth strips what was set up before that proof, possibly by someone else
19
+ * who registered the address first, and Mantle ends that party's grants before the attempt is checked.
20
+ */
21
+ export declare function oauthGrantRevocation(): {
22
+ id: "mantle-oauth-grant-revocation";
23
+ hooks: {
24
+ before: ({
25
+ matcher: (ctx: import("better-auth").HookEndpointContext) => boolean;
26
+ handler: import("better-auth").Middleware<import("better-auth").MiddlewareOptions, (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<{
27
+ context: {
28
+ mantleResetPasswordUser: string;
29
+ };
30
+ } | undefined>>;
31
+ } | {
32
+ matcher: (ctx: import("better-auth").HookEndpointContext) => boolean;
33
+ handler: import("better-auth").Middleware<import("better-auth").MiddlewareOptions, (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>>;
34
+ })[];
35
+ after: {
36
+ matcher: (ctx: import("better-auth").HookEndpointContext) => boolean;
37
+ handler: import("better-auth").Middleware<import("better-auth").MiddlewareOptions, (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>>;
38
+ }[];
39
+ };
40
+ };
41
+ export interface OAuthConsentRequest {
42
+ readonly clientName: string;
43
+ readonly redirectUri: string;
44
+ readonly scopes: readonly string[];
45
+ readonly oauthQuery: string;
46
+ }
47
+ export interface OAuthConsentInfo {
48
+ readonly id: string;
49
+ readonly clientId: string;
50
+ readonly clientName: string;
51
+ readonly scopes: readonly string[];
52
+ }
53
+ /** Signed-query and consent APIs over the same application-owned instance and adapter (including renamed tables). */
54
+ export declare function oauthConsents(auth: AuthInstance & {
55
+ handler(request: Request): Promise<Response>;
56
+ }): {
57
+ getOAuthConsentRequest: (request: Request) => Promise<OAuthConsentRequest | null>;
58
+ completeOAuthConsent: (request: Request, accept: boolean) => Promise<string>;
59
+ listOAuthConsents: (userId: string) => Promise<OAuthConsentInfo[]>;
60
+ revokeOAuthConsent: (userId: string, consentId: string) => Promise<boolean>;
61
+ };
62
+ /** Optional scheduled storage hygiene, not boot work or an authorization rule. Owned/CIMD clients survive. */
63
+ export declare function pruneLegacyOAuthClients(auth: AuthInstance): Promise<number>;
@@ -0,0 +1,217 @@
1
+ import { createAuthMiddleware } from "@better-auth/core/api";
2
+ import { nativeAuthRead } from "./adapter.js";
3
+ import { ContractError } from "../kernel/contracts.js";
4
+ const pair = (field, value) => ({ field, value });
5
+ const array = (value) => {
6
+ try {
7
+ const parsed = typeof value === "string" ? JSON.parse(value) : value;
8
+ return Array.isArray(parsed) && parsed.every(x => typeof x === "string") ? parsed : null;
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ };
14
+ /**
15
+ * Fail closed for unbound tokens; reconsent cannot resurrect an issued JWT. As in Better Auth, an `offline_access` grant outlives
16
+ * the browser session: without a live session it stands on an unrevoked, unexpired offline refresh token for the same user and client.
17
+ */
18
+ export async function assertActiveUserGrant(auth, claims, audience) {
19
+ const { sub, azp, sid, mantle_consent_id: id, mantle_consent_reference: reference, mantle_skip_consent: skip } = claims;
20
+ const tokenScopes = typeof claims.scope === "string" ? claims.scope.split(/\s+/).filter(Boolean) : array(claims.scope);
21
+ const offline = !!tokenScopes?.includes("offline_access");
22
+ if (![sub, azp].every(x => typeof x === "string" && x.length) || !(offline && sid == null || typeof sid === "string" && sid.length) || !(skip === true && id === null || typeof id === "string" && id.length))
23
+ throw new ContractError("UNAUTHENTICATED", "OAuth token is not bound to a user session.");
24
+ const { exec, param, now, table, column } = nativeAuthRead(auth.options);
25
+ const at = now(Date.now());
26
+ const c = (field) => `c.${column("oauthConsent", field)}`, s = (field) => `s.${column("session", field)}`, client = (field) => `client.${column("oauthClient", field)}`, r = (field) => column("oauthRefreshToken", field);
27
+ const { rows } = await exec(`SELECT ${s("id")} AS session, ${c("id")} AS "consentId", ${c("referenceId")} AS reference, ${c("resources")} AS resources, ${c("scopes")} AS scopes,
28
+ ${client("skipConsent")} AS "skipConsent", ${client("disabled")} AS disabled, ${client("scopes")} AS "clientScopes"
29
+ FROM ${table("oauthClient")} client LEFT JOIN ${table("session")} s ON ${s("id")} = ${param(1)} AND ${s("userId")} = ${param(2)} AND ${s("expiresAt")} > ${param(3)}
30
+ LEFT JOIN ${table("oauthConsent")} c ON ${c("id")} = ${param(4)} AND ${c("userId")} = ${param(2)} AND ${c("clientId")} = ${client("clientId")}
31
+ WHERE ${client("clientId")} = ${param(5)}`, [sid ?? null, sub, at, id, azp]);
32
+ const grant = rows[0];
33
+ const live = !!grant?.session || offline && !!grant && (await exec(`SELECT ${r("scopes")} AS scopes FROM ${table("oauthRefreshToken")}
34
+ WHERE ${r("userId")} = ${param(1)} AND ${r("clientId")} = ${param(2)} AND ${r("revoked")} IS NULL AND ${r("expiresAt")} > ${param(3)}`, [sub, azp, at])).rows.some(row => array(row.scopes)?.includes("offline_access"));
35
+ const provider = auth.options.plugins?.find(plugin => plugin.id === "oauth-provider");
36
+ const firstParty = skip === true && id === null && !!grant?.skipConsent;
37
+ const resources = firstParty ? (typeof claims.aud === "string" ? [claims.aud] : array(claims.aud)) : array(grant?.resources);
38
+ const scopes = firstParty ? array(grant?.clientScopes) ?? provider?.options?.scopes : array(grant?.scopes);
39
+ const requested = typeof audience === "string" ? [audience] : audience;
40
+ const tokenAudiences = typeof claims.aud === "string" ? [claims.aud] : array(claims.aud) ?? [];
41
+ if (!grant || !live || grant.disabled || !firstParty && (!grant.consentId || (grant.reference ?? null) !== reference) || !resources || !scopes || !tokenScopes || !requested.some(x => resources.includes(x) && tokenAudiences.includes(x)) || !tokenScopes.every(x => scopes.includes(x)))
42
+ throw new ContractError("UNAUTHENTICATED", "OAuth authorization grant is no longer active.");
43
+ return { id: sub };
44
+ }
45
+ /** Add first to oauthProvider/mcp extensions; Better Auth retains ownership of referenceId. */
46
+ export const oauthGrantExtension = {
47
+ claims: { accessToken: async ({ ctx, user, client, referenceId, grantType, scopes, sessionId }) => {
48
+ // Narrowing away offline_access on refresh would mint a session-bound token for a session that has already ended.
49
+ if (grantType === "refresh_token" && !scopes.includes("offline_access")) {
50
+ const session = sessionId ? await ctx.context.adapter.findOne({ model: "session", where: [pair("id", sessionId)] }) : null;
51
+ if (!session || new Date(session.expiresAt).getTime() <= Date.now())
52
+ throw ctx.error("BAD_REQUEST", { error: "invalid_grant", error_description: "The browser session for this grant has ended; keep offline_access or sign in again." });
53
+ }
54
+ if (client.skipConsent)
55
+ return { mantle_consent_id: null, mantle_skip_consent: true };
56
+ const consent = user && await ctx.context.adapter.findOne({ model: "oauthConsent", where: [pair("userId", user.id), pair("clientId", client.clientId), { field: "referenceId", value: referenceId ?? null }] });
57
+ return { mantle_consent_id: consent?.id ?? null, mantle_consent_reference: referenceId ?? null };
58
+ } },
59
+ };
60
+ export const OAUTH_GRANT_REVOCATION = "mantle-oauth-grant-revocation";
61
+ /** Password resets (all channels) and an admin password change: the old credential may be compromised, so every session ends too. */
62
+ const RESETS = ["/reset-password", "/email-otp/reset-password", "/phone-number/reset-password", "/admin/set-user-password"];
63
+ const ENDING = [...RESETS, "/change-password", "/admin/ban-user", "/admin/update-user", "/admin/revoke-user-sessions"];
64
+ const RESET_USER = "mantleResetPasswordUser";
65
+ /** Better Auth's email-proof sign-ins: on an unverified user they delete its accounts and sessions (revokeUnprovenAccountAccess). */
66
+ const PROVING = ["/sign-in/email-otp", "/magic-link/verify"];
67
+ /**
68
+ * Add to the Better Auth plugins with the OAuth provider. An `offline_access` grant outlives sign-out and session expiry
69
+ * (as in Better Auth), but a successful password reset, a password change that signs out other devices, an admin ban
70
+ * (`/admin/ban-user`, or `/admin/update-user` setting `banned: true`), an admin session revocation or an admin password
71
+ * change ends every OAuth grant of that user: tokens, consents and unredeemed codes. So does an email OTP or magic-link
72
+ * sign-in attempt on an unverified user: Better Auth strips what was set up before that proof, possibly by someone else
73
+ * who registered the address first, and Mantle ends that party's grants before the attempt is checked.
74
+ */
75
+ export function oauthGrantRevocation() {
76
+ return {
77
+ id: OAUTH_GRANT_REVOCATION,
78
+ hooks: {
79
+ // Better Auth consumes the reset token, so its user rides this request's own context to the after hook.
80
+ before: [{ matcher: ctx => ctx.path === "/reset-password", handler: createAuthMiddleware(async (ctx) => {
81
+ // Better Auth reads the token from the body or the query, in that order.
82
+ const token = ctx.body?.token || ctx.query?.token;
83
+ const value = typeof token === "string" && token ? await ctx.context.internalAdapter.findVerificationValue(`reset-password:${token}`) : null;
84
+ if (value)
85
+ return { context: { [RESET_USER]: value.value } };
86
+ }) }, { matcher: ctx => PROVING.includes(ctx.path ?? ""), handler: createAuthMiddleware(async (ctx) => {
87
+ // Before the endpoint, while the user is still unverified and the magic-link token is unconsumed: once Better Auth
88
+ // marks the address verified, no later request would tell the pre-registrant's grants apart. A failure here fails
89
+ // the request before verification, so a retry ends them. A failed attempt ends them too: an unverified user's grants
90
+ // are exactly the pre-hijack risk.
91
+ const email = ctx.path === "/sign-in/email-otp" ? ctx.body?.email : await magicLinkEmail(ctx.context, ctx.query?.token);
92
+ const found = typeof email === "string" && email ? await ctx.context.internalAdapter.findUserByEmail(email.toLowerCase()) : null;
93
+ if (found && !found.user.emailVerified)
94
+ await endGrants(ctx.context.adapter, found.user.id);
95
+ }) }],
96
+ // After the endpoint: its own validation and permission checks have passed and it succeeded.
97
+ after: [{ matcher: ctx => ENDING.includes(ctx.path ?? ""), handler: createAuthMiddleware(async (ctx) => {
98
+ const returned = ctx.context.returned, path = ctx.path ?? "";
99
+ if (returned instanceof Error || returned instanceof Response && !returned.ok)
100
+ return;
101
+ const body = ctx.body;
102
+ const userId = path === "/reset-password" ? ctx[RESET_USER]
103
+ : path === "/email-otp/reset-password" ? typeof body?.email === "string" && (await ctx.context.internalAdapter.findUserByEmail(body.email.toLowerCase()))?.user.id
104
+ : path === "/phone-number/reset-password" ? typeof body?.phoneNumber === "string" && (await ctx.context.adapter.findOne({ model: "user", where: [pair("phoneNumber", body.phoneNumber)] }))?.id
105
+ // Better Auth itself ends the other sessions and keeps the current one.
106
+ : path === "/change-password" ? body?.revokeOtherSessions === true && (ctx.context.session?.user.id ?? returned?.user?.id)
107
+ : path === "/admin/update-user" ? body?.data?.banned === true && body.userId
108
+ : body?.userId;
109
+ if (typeof userId !== "string" || !userId)
110
+ return;
111
+ // Sessions first: a live session (perhaps the attacker's) could otherwise authorize a new grant after endGrants.
112
+ if (RESETS.includes(path))
113
+ await ctx.context.internalAdapter.deleteUserSessions(userId);
114
+ await endGrants(ctx.context.adapter, userId);
115
+ }) }],
116
+ },
117
+ };
118
+ }
119
+ /** The email a magic-link token names, read without consuming it; the token is stored as the magic-link plugin's `storeToken` says. */
120
+ async function magicLinkEmail(context, token) {
121
+ if (typeof token !== "string" || !token)
122
+ return;
123
+ const store = context.options.plugins?.find(plugin => plugin.id === "magic-link")?.options?.storeToken ?? "plain";
124
+ const hash = typeof store === "object" && store && "hash" in store && typeof store.hash === "function" ? store.hash : undefined;
125
+ const stored = store === "plain" ? token : hash ? await hash(token)
126
+ : store === "hashed" ? btoa(String.fromCharCode(...new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token))))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "") : undefined;
127
+ const value = stored === undefined ? null : await context.internalAdapter.findVerificationValue(`magic-link:${stored}`);
128
+ try {
129
+ return value ? JSON.parse(value.value).email : undefined;
130
+ }
131
+ catch {
132
+ return;
133
+ }
134
+ }
135
+ /** Ends a user's grants (for one client, or all): consents, tokens and unredeemed authorization codes. */
136
+ async function endGrants(db, userId, clientId) {
137
+ const where = [pair("userId", userId), ...clientId ? [pair("clientId", clientId)] : []];
138
+ // Delete first: even if cleanup fails, every issued JWT fails the live-grant check immediately.
139
+ await db.deleteMany({ model: "oauthConsent", where });
140
+ for (const model of ["oauthRefreshToken", "oauthAccessToken"])
141
+ await db.updateMany({ model, where: [...where, { field: "revoked", value: null }], update: { revoked: new Date() } });
142
+ // An unredeemed code would mint a new token lineage after reconsent.
143
+ const code = (v) => { const x = authCode(v); return x?.userId === userId && (!clientId || x.query.client_id === clientId); };
144
+ const codes = [userId, clientId].filter(x => x !== undefined).map(x => ({ field: "value", operator: "contains", value: JSON.stringify(x) }));
145
+ for (let offset = 0;;) {
146
+ const page = await db.findMany({ model: "verification", where: codes, limit: 100, offset, sortBy: { field: "id", direction: "asc" } });
147
+ const ids = page.filter(v => code(v.value)).map(v => v.id);
148
+ if (ids.length)
149
+ await db.deleteMany({ model: "verification", where: [{ field: "id", operator: "in", value: ids }] });
150
+ if (page.length < 100)
151
+ break;
152
+ offset += page.length - ids.length;
153
+ }
154
+ }
155
+ /** A verification value that is an authorization code, with its user and client; else undefined. */
156
+ function authCode(text) {
157
+ let value;
158
+ try {
159
+ value = JSON.parse(text);
160
+ }
161
+ catch {
162
+ return;
163
+ }
164
+ return value?.type === "authorization_code" && typeof value.userId === "string" && typeof value.query?.client_id === "string" ? value : undefined;
165
+ }
166
+ /** Signed-query and consent APIs over the same application-owned instance and adapter (including renamed tables). */
167
+ export function oauthConsents(auth) {
168
+ const adapter = async () => (await auth.$context).adapter;
169
+ return {
170
+ getOAuthConsentRequest: async (request) => {
171
+ const url = new URL(request.url), clientId = url.searchParams.get("client_id");
172
+ if (!clientId || !url.search)
173
+ return null;
174
+ const api = auth.api;
175
+ const oauthQuery = url.search.slice(1);
176
+ const client = await api.getOAuthClientPublicPrelogin({ headers: request.headers, body: { client_id: clientId, oauth_query: oauthQuery } });
177
+ return { clientName: client.client_name ?? clientId, redirectUri: url.searchParams.get("redirect_uri") ?? client.redirect_uris?.[0] ?? "", scopes: (url.searchParams.get("scope") ?? "").split(/\s+/).filter(Boolean), oauthQuery };
178
+ },
179
+ completeOAuthConsent: async (request, accept) => {
180
+ const oauth_query = (await request.formData()).get("oauth_query");
181
+ if (typeof oauth_query !== "string" || !oauth_query)
182
+ throw new ContractError("INPUT_VALIDATION_FAILED", "Missing signed authorization request.");
183
+ const headers = new Headers(request.headers);
184
+ headers.set("content-type", "application/json");
185
+ headers.delete("content-length");
186
+ const response = await auth.handler(new Request(new URL(`${auth.options.basePath ?? "/api/auth"}/oauth2/consent`, request.url).href, { method: "POST", headers, body: JSON.stringify({ accept, oauth_query }) }));
187
+ const body = await response.json();
188
+ if (!response.ok || typeof body.url !== "string")
189
+ throw new ContractError("INPUT_VALIDATION_FAILED", "Invalid authorization request.");
190
+ return body.url;
191
+ },
192
+ listOAuthConsents: async (userId) => {
193
+ const { exec, param, table, column } = nativeAuthRead(auth.options);
194
+ const c = (field) => `c.${column("oauthConsent", field)}`, client = (field) => `client.${column("oauthClient", field)}`;
195
+ const { rows } = await exec(`SELECT ${c("id")} AS id, ${c("clientId")} AS "clientId", COALESCE(${client("name")}, ${c("clientId")}) AS "clientName", ${c("scopes")} AS scopes
196
+ FROM ${table("oauthConsent")} c LEFT JOIN ${table("oauthClient")} client ON ${client("clientId")} = ${c("clientId")}
197
+ WHERE ${c("userId")} = ${param(1)} ORDER BY ${c("updatedAt")} DESC, ${c("id")} ASC`, [userId]);
198
+ return rows.map(row => ({ id: row.id, clientId: row.clientId, clientName: row.clientName, scopes: array(row.scopes) ?? [] }));
199
+ },
200
+ revokeOAuthConsent: async (userId, consentId) => {
201
+ const db = await adapter();
202
+ const consent = await db.findOne({ model: "oauthConsent", where: [pair("id", consentId), pair("userId", userId)] });
203
+ if (!consent)
204
+ return false;
205
+ await endGrants(db, userId, consent.clientId);
206
+ return true;
207
+ },
208
+ };
209
+ }
210
+ /** Optional scheduled storage hygiene, not boot work or an authorization rule. Owned/CIMD clients survive. */
211
+ export async function pruneLegacyOAuthClients(auth) {
212
+ const { adapter } = await auth.$context;
213
+ return adapter.deleteMany({ model: "oauthClient", where: [
214
+ { field: "clientDiscoveryId", value: null }, { field: "userId", value: null }, { field: "referenceId", value: null },
215
+ { field: "createdAt", operator: "lt", value: new Date(Date.now() - 90 * 86400_000) },
216
+ ] });
217
+ }
@@ -0,0 +1,10 @@
1
+ /** Keep runtime inputs while disabling the provider's eager and bulk lazy seeding. No schema or database work. */
2
+ // OAuth provider 1.7.7 shares plugin.options with its eager/lazy seed closures; its seedResources/checkIdentifier helpers are not exported.
3
+ const seeds = new WeakMap();
4
+ export function resourceSeeds(options) {
5
+ const inputs = (seeds.get(options) ?? options.resources ?? []);
6
+ if (!seeds.has(options))
7
+ seeds.set(options, [...inputs]);
8
+ options.resources = [];
9
+ return inputs;
10
+ }