@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,288 @@
1
+ /**
2
+ * Better Auth `database` adapters on the same native handle as the data layer: a Cloudflare D1 binding, or PostgreSQL through Bun.SQL or postgres.js.
3
+ * No Kysely. Rules (where semantics, joins, field/model mapping, value transforms) come from Better Auth's own
4
+ * `createAdapterFactory`; this file only turns its already-normalised calls into parameterised SQL.
5
+ * `consumeOne` and `incrementOne` are single statements: D1 has no transactions, and they guard OTP consumption and
6
+ * attempt counting.
7
+ */
8
+ import { createAdapterFactory } from "@better-auth/core/db/adapter";
9
+ import { APIError } from "@better-auth/core/error";
10
+ import { getAuthTables } from "@better-auth/core/db";
11
+ import { resourceSeeds } from "./resource-seeds.js";
12
+ import { ContractError } from "../kernel/contracts.js";
13
+ import { adminOptionsOf } from "./caller.js";
14
+ import * as postgres from "../kernel/dialect/postgres.js";
15
+ import * as sqlite from "../kernel/dialect/sqlite.js";
16
+ import * as bunSql from "../kernel/driver/postgres.js";
17
+ import { condition as spell } from "../kernel/where.js";
18
+ const nativeReaders = new WeakMap();
19
+ /** Selected auth queries share this adapter's handle, dialect and Better Auth's physical model/field mapping. */
20
+ /** Better Auth's table and column names for these options (modelName/fieldName mapping); needs no database. */
21
+ export function authNames(options) {
22
+ const schema = getAuthTables(options);
23
+ return {
24
+ table: (model) => postgres.ident(schema[model].modelName),
25
+ column: (model, field) => postgres.ident(schema[model]?.fields[field]?.fieldName ?? field),
26
+ has: (model, field) => field === "id" || !!schema[model]?.fields[field],
27
+ };
28
+ }
29
+ export function nativeAuthRead(options) {
30
+ const native = options.database && nativeReaders.get(options.database);
31
+ if (!native)
32
+ throw new ContractError("AUTH_CONFIGURATION_ERROR", "OAuth requires Mantle's native auth database adapter.", "auth:database");
33
+ return { ...native, ...authNames(options) };
34
+ }
35
+ function customAdapter(d, exec, seed, insertOnly = false, options) {
36
+ const { ident } = d;
37
+ return ({ getFieldName, schema, getDefaultModelName }) => {
38
+ const condition = (bind, modelKey, w) => {
39
+ const col = ident(getFieldName({ model: modelKey, field: w.field }));
40
+ const { value, operator } = w;
41
+ const fold = w.mode === "insensitive" && (typeof value === "string" || (Array.isArray(value) && value.every(v => typeof v === "string")));
42
+ if (operator === "contains" || operator === "starts_with" || operator === "ends_with") {
43
+ const text = d.escapeLike(String(value));
44
+ return spell(d, col, "like", operator === "contains" ? `%${text}%` : operator === "starts_with" ? `${text}%` : `%${text}`, bind, fold);
45
+ }
46
+ return spell(d, col, operator === "not_in" ? "notIn" : operator, value, bind, fold);
47
+ };
48
+ /** Builds one statement (its own parameters) and runs it. `where` is `WHERE (and...) AND (or...)`, the Kysely adapter's grouping; empty without conditions. */
49
+ const run = (build, protectAdmin = false) => {
50
+ const values = [];
51
+ const bind = (value) => (values.push(value === undefined ? null : value), d.param(values.length));
52
+ const where = (modelKey, conditions, extra) => {
53
+ const group = (connector) => {
54
+ const parts = (conditions ?? []).filter(w => (w.connector === "OR" ? "OR" : "AND") === connector).map(w => condition(bind, modelKey, w));
55
+ return parts.length ? `(${parts.join(` ${connector} `)})` : null;
56
+ };
57
+ const parts = [group("AND"), group("OR"), extra ?? null].filter(Boolean);
58
+ return parts.length ? ` WHERE ${parts.join(" AND ")}` : "";
59
+ };
60
+ return exec(build({ bind, where }), values, protectAdmin);
61
+ };
62
+ const idOf = (modelKey) => ident(getFieldName({ model: modelKey, field: "id" }));
63
+ /** The WHERE plus `<id> IN (SELECT <id> FROM t WHERE ... LIMIT 1)`: the statement touches at most one row. */
64
+ const oneRow = (s, table, modelKey, where) => s.where(modelKey, where, `${idOf(modelKey)} IN (SELECT ${idOf(modelKey)} FROM ${ident(table)}${s.where(modelKey, where)} LIMIT 1)`);
65
+ /** The one row a `RETURNING *` statement changed, else null. */
66
+ const one = async (build) => ((await run(s => `${build(s)} RETURNING *`)).rows[0] ?? null);
67
+ /** The columns a write gives (an `undefined` is no value). */
68
+ const cols = (values) => Object.entries(values).filter(([, v]) => v !== undefined);
69
+ const assign = (s, values, increment = {}) => [...cols(values).map(([c, v]) => `${ident(c)} = ${s.bind(v)}`), ...Object.entries(increment).map(([c, n]) => `${ident(c)} = ${ident(c)} + ${s.bind(n)}`)].join(", ");
70
+ const { adminRoles: roles, adminUserIds } = options ? adminOptionsOf({ options }) : { adminRoles: [], adminUserIds: [] };
71
+ const holdsAdmin = (role) => typeof role === "string" && role.split(",").some(role => roles.includes(role));
72
+ const field = (name) => getFieldName({ model: "user", field: name });
73
+ const protects = (modelKey, values) => modelKey === "user" && (roles.length > 0 || adminUserIds.length > 0) &&
74
+ (!values || values[field("banned")] === true || values[field("banned")] === 1 || Object.hasOwn(values, field("role")) && !holdsAdmin(values[field("role")]));
75
+ const membership = (s, alias) => '(' + [...roles.map(role => `${d === sqlite ? "instr" : "strpos"}(',' || COALESCE(${alias}.${ident(field("role"))}, '') || ',', ${s.bind(',' + role + ',')}) > 0`), d === sqlite ? "0" : "FALSE"].join(' OR ') + ')';
76
+ const allowlisted = (s, alias) => `(${[...adminUserIds.map(id => `${alias}.${idOf("user")} = ${s.bind(id)}`), d === sqlite ? "0" : "FALSE"].join(" OR ")})`;
77
+ const administrator = (s, alias) => `(${membership(s, alias)} OR ${allowlisted(s, alias)})`;
78
+ const lastAdminGuard = (s, model, keepsAllowlisted = false) => {
79
+ const table = ident(model), other = '"_mantle_other_admin"', no = d === sqlite ? '0' : 'FALSE';
80
+ return `(${keepsAllowlisted ? `${allowlisted(s, table)} OR ` : ""}NOT ${administrator(s, table)} OR EXISTS (SELECT 1 FROM ${table} ${other} WHERE ${administrator(s, other)} AND ${other}.${idOf("user")} <> ${table}.${idOf("user")} AND COALESCE(${other}.${ident(field("banned"))}, ${no}) = ${no}))`;
81
+ };
82
+ const mutation = async (model, modelKey, where, values) => {
83
+ const protect = protects(modelKey, values);
84
+ const keepsAllowlisted = protect && !!values && values[field("banned")] !== true && values[field("banned")] !== 1;
85
+ if (protect && (where.length !== 1 || where[0].field !== field("id") || where[0].operator && where[0].operator !== "eq"))
86
+ throw new APIError("FORBIDDEN", { code: "AUTH_DENIED", message: "Destructive admin identity writes require one user id." });
87
+ const result = await run(s => `${values ? `UPDATE ${ident(model)} SET ${assign(s, values)}` : `DELETE FROM ${ident(model)}`}${s.where(modelKey, where, protect ? lastAdminGuard(s, model, keepsAllowlisted) : undefined)} RETURNING *`, protect);
88
+ if (protect && !result.changes && (await run(s => `SELECT ${idOf(modelKey)} FROM ${ident(model)}${s.where(modelKey, where, `NOT ${lastAdminGuard(s, model, keepsAllowlisted)}`)} LIMIT 1`)).rows.length)
89
+ throw new APIError("CONFLICT", { code: "CONFLICT", message: "This is the last admin; promote another admin before removing or banning this one." });
90
+ return result;
91
+ };
92
+ const deleteMany = async ({ model, modelKey = model, where }) => (await mutation(model, modelKey, where)).changes;
93
+ /** Joined rows come back as flat `__j<i>__<column>` aliases; fold them into `row[joinModel]` (object or array). */
94
+ const joinColumns = (join) => Object.entries(join).map(([model, config]) => {
95
+ const fields = schema[config.modelKey ?? getDefaultModelName(model)]?.fields ?? {};
96
+ return [...new Set(["id", ...Object.entries(fields).map(([field, attr]) => attr.fieldName || field)])];
97
+ });
98
+ const foldJoins = (rows, join, idColumn) => {
99
+ const entries = Object.entries(join);
100
+ const grouped = new Map();
101
+ rows.forEach((raw, index) => {
102
+ const main = {};
103
+ const joined = entries.map(() => ({}));
104
+ for (const [key, value] of Object.entries(raw)) {
105
+ const m = /^__j(\d+)__(.*)$/s.exec(key);
106
+ if (m)
107
+ joined[Number(m[1])][m[2]] = value;
108
+ else
109
+ main[key] = value;
110
+ }
111
+ const key = main[idColumn] ?? index;
112
+ let entry = grouped.get(key);
113
+ if (!entry) {
114
+ entry = { ...main };
115
+ entries.forEach(([model, config]) => (entry[model] = config.relation === "one-to-one" ? null : []));
116
+ grouped.set(key, entry);
117
+ }
118
+ entries.forEach(([model, config], i) => {
119
+ const obj = joined[i];
120
+ if (!Object.values(obj).some(v => v !== null && v !== undefined))
121
+ return;
122
+ if (config.relation === "one-to-one")
123
+ entry[model] = obj;
124
+ else {
125
+ const list = entry[model];
126
+ if (!list.some(item => item.id !== undefined && item.id === obj.id))
127
+ list.push(obj);
128
+ }
129
+ });
130
+ });
131
+ return [...grouped.values()];
132
+ };
133
+ const select = async (model, modelKey, o) => {
134
+ const column = (field) => getFieldName({ model: modelKey, field });
135
+ const sortColumn = o.sortBy?.field ? ident(column(o.sortBy.field)) : null;
136
+ const order = (prefix = "") => (sortColumn ? ` ORDER BY ${prefix}${sortColumn} ${o.sortBy.direction === "desc" ? "DESC" : "ASC"}` : "");
137
+ // With a join the primary subquery also needs the id (rows fold by it), every join key and the sort column; they are dropped again below.
138
+ const wanted = o.select?.length ? o.select.map(column) : null;
139
+ const needed = o.join ? [column("id"), ...Object.values(o.join).map(c => c.on.from), ...(o.sortBy ? [column(o.sortBy.field)] : [])] : [];
140
+ const { rows } = await run(s => {
141
+ const list = wanted ? [...new Set([...wanted, ...needed])].map(ident).join(", ") : "*";
142
+ const paging = `${o.limit !== undefined ? ` LIMIT ${Math.trunc(o.limit)}` : ""}${o.offset ? ` OFFSET ${Math.trunc(o.offset)}` : ""}`;
143
+ const primary = `SELECT ${list} FROM ${ident(model)}${s.where(modelKey, o.where)}${order()}${paging}`;
144
+ if (!o.join)
145
+ return primary;
146
+ const entries = Object.entries(o.join);
147
+ const names = joinColumns(o.join);
148
+ // The CTE is MATERIALIZED so an unordered LIMIT picks the same parents in both of its uses. One-to-many: number each parent's joined rows in SQL and keep the first `limit`, so only that many rows ever leave the database.
149
+ const joinSql = entries.map(([joined, c], i) => {
150
+ const source = c.relation === "one-to-one" ? ident(joined)
151
+ : `(SELECT *, ROW_NUMBER() OVER (PARTITION BY ${ident(c.on.to)} ORDER BY ${idOf(c.modelKey ?? getDefaultModelName(joined))}) AS "__rn" FROM ${ident(joined)} WHERE ${ident(c.on.to)} IN (SELECT ${ident(c.on.from)} FROM "primary"))`;
152
+ return ` LEFT JOIN ${source} AS "j${i}" ON "j${i}".${ident(c.on.to)} = "primary".${ident(c.on.from)}${c.relation === "one-to-one" ? "" : ` AND "j${i}"."__rn" <= ${Math.trunc(c.limit ?? 100)}`}`;
153
+ }).join("");
154
+ const joinSelect = entries.flatMap((_, i) => names[i].map(c => `"j${i}".${ident(c)} AS ${ident(`__j${i}__${c}`)}`)).join(", ");
155
+ return `WITH "primary" AS MATERIALIZED (${primary}) SELECT "primary".*, ${joinSelect} FROM "primary"${joinSql}${order('"primary".')}`;
156
+ });
157
+ if (!o.join)
158
+ return rows;
159
+ const folded = foldJoins(rows, o.join, column("id"));
160
+ if (wanted)
161
+ for (const row of folded)
162
+ for (const extra of needed)
163
+ if (!wanted.includes(extra))
164
+ delete row[extra];
165
+ return folded;
166
+ };
167
+ return {
168
+ async create({ data, model }) {
169
+ const given = cols(data);
170
+ const { rows } = await run(s => `INSERT INTO ${ident(model)}${given.length ? ` (${given.map(([c]) => ident(c)).join(", ")}) VALUES (${given.map(([, v]) => s.bind(v)).join(", ")})` : " DEFAULT VALUES"} ${insertOnly ? "ON CONFLICT DO NOTHING " : ""}RETURNING *`);
171
+ return rows[0];
172
+ },
173
+ async findOne({ model, modelKey = model, where, select: fields, join }) {
174
+ let row = (await select(model, modelKey, { where, select: fields, limit: 1, join }))[0] ?? null;
175
+ if (!row && modelKey === "oauthResource" && where?.length === 1 && where[0].field === getFieldName({ model: modelKey, field: "identifier" }) && (!where[0].operator || where[0].operator === "eq") && await seed(where[0].value)) {
176
+ row = (await select(model, modelKey, { where, select: fields, limit: 1, join }))[0] ?? null;
177
+ }
178
+ return row;
179
+ },
180
+ async findMany({ model, modelKey = model, where, limit, select: fields, sortBy, offset, join }) {
181
+ return (await select(model, modelKey, { where, select: fields, limit, offset, sortBy, join }));
182
+ },
183
+ async update({ model, modelKey = model, where, update }) {
184
+ if (!where.length)
185
+ return null;
186
+ return ((await mutation(model, modelKey, where, update)).rows[0] ?? null);
187
+ },
188
+ async updateMany({ model, modelKey = model, where, update }) {
189
+ return (await mutation(model, modelKey, where, update)).changes;
190
+ },
191
+ async delete(options) { await deleteMany(options); },
192
+ deleteMany,
193
+ async consumeOne({ model, modelKey = model, where }) {
194
+ return one(s => `DELETE FROM ${ident(model)}${oneRow(s, model, modelKey, where)}`);
195
+ },
196
+ async incrementOne({ model, modelKey = model, where, increment, set }) {
197
+ return one(s => `UPDATE ${ident(model)} SET ${assign(s, set ?? {}, increment)}${oneRow(s, model, modelKey, where)}`);
198
+ },
199
+ async count({ model, modelKey = model, where }) {
200
+ const { rows } = await run(s => `SELECT COUNT(*) AS "count" FROM ${ident(model)}${s.where(modelKey, where)}`);
201
+ return Number(rows[0].count);
202
+ },
203
+ };
204
+ };
205
+ }
206
+ function build(d, native, adapterId, adapterName, exec, transaction, options) {
207
+ const resources = new Map((options.plugins ?? []).flatMap(plugin => plugin.id === "oauth-provider" ? resourceSeeds(plugin.options).map(input => {
208
+ const resource = typeof input === "string" ? { identifier: input } : input;
209
+ return [resource.identifier, { resource, options: plugin.options }];
210
+ }) : []).reverse()); // first configured entry wins, as upstream insertOnly does
211
+ // ponytail: one seed per identifier per isolate (upstream seeds once per init); disable a configured resource rather than deleting it.
212
+ const seeded = new Set();
213
+ let writer;
214
+ const pending = new Map();
215
+ const seed = (identifier) => {
216
+ const configured = resources.get(identifier);
217
+ if (!configured || seeded.has(identifier))
218
+ return Promise.resolve(false);
219
+ let work = pending.get(identifier);
220
+ if (!work) {
221
+ work = (async () => {
222
+ const { resource, options: oauth } = configured;
223
+ // Match upstream identifier validation and seed row defaults.
224
+ let valid;
225
+ try {
226
+ valid = oauth.identifierValidator ? !!(await oauth.identifierValidator(identifier)) : !new URL(identifier).hash;
227
+ }
228
+ catch {
229
+ valid = false;
230
+ }
231
+ if (!valid)
232
+ throw new APIError("INTERNAL_SERVER_ERROR", { code: "AUTH_CONFIGURATION_ERROR", message: "Configured OAuth resource identifier failed validation." });
233
+ const now = new Date();
234
+ const data = {
235
+ identifier, name: resource.name ?? identifier,
236
+ ...Object.fromEntries(["accessTokenTtl", "refreshTokenTtl", "signingKeyId", "allowedScopes", "customClaims", "metadata"].map(field => [field, resource[field] ?? null])),
237
+ signingAlgorithm: resource.signingAlgorithm && ["EdDSA", "ES256", "ES512", "PS256", "RS256"].includes(resource.signingAlgorithm) ? resource.signingAlgorithm : null,
238
+ dpopBoundAccessTokensRequired: resource.dpopBoundAccessTokensRequired ?? false, disabled: resource.disabled ?? false,
239
+ policyVersion: 1, createdAt: now, updatedAt: now,
240
+ };
241
+ // The factory owns ID generation, field mapping and native date/JSON/boolean encoding, including for seeds.
242
+ writer ??= createAdapterFactory({ config, adapter: customAdapter(d, exec, async () => false, true) })(options);
243
+ await writer.create({ model: "oauthResource", data });
244
+ seeded.add(identifier);
245
+ return true;
246
+ })().finally(() => pending.delete(identifier));
247
+ pending.set(identifier, work);
248
+ }
249
+ return work;
250
+ };
251
+ const config = {
252
+ adapterId, adapterName, supportsBooleans: native, supportsDates: native, supportsJSON: native, supportsUUIDs: native, transaction,
253
+ // Bun.SQL returns int8/numeric as strings; a `number` field must reach Better Auth as a number (the rate limiter adds to `lastRequest`). Ids stay strings.
254
+ customTransformOutput: ({ data, fieldAttributes, field }) => (fieldAttributes.type === "number" && typeof data === "string" && field !== "id" && !fieldAttributes.references ? Number(data) : data),
255
+ };
256
+ const factory = createAdapterFactory({ config, adapter: customAdapter(d, exec, seed, false, options) });
257
+ // Both native adapters implement joins; Better Auth otherwise emulates each joined read with another statement.
258
+ return (options) => factory({ ...options, advanced: { ...options.advanced, database: { ...options.advanced?.database, joins: options.advanced?.database?.joins ?? true } } });
259
+ }
260
+ /** Cloudflare D1 (SQLite). D1 has no interactive transactions, so `transaction` is off and the atomic operations are single statements. */
261
+ export function d1Adapter(db) {
262
+ const exec = async (text, values) => {
263
+ const { results, meta } = await db.prepare(text).bind(...values).all();
264
+ return { rows: results, changes: meta.changes };
265
+ };
266
+ const factory = (options) => build(sqlite, false, "d1", "Mantle D1 Adapter", exec, false, options)(options);
267
+ nativeReaders.set(factory, { exec, param: sqlite.param, now: ms => new Date(ms).toISOString() });
268
+ return factory;
269
+ }
270
+ /**
271
+ * PostgreSQL through Bun.SQL or postgres.js (the `Postgres` contract the data layer uses). Transactions use `begin`,
272
+ * which pins every statement to one connection. On Workers, create the postgres.js client per request (or per isolate
273
+ * with `max: 1`), as for the data layer.
274
+ */
275
+ export function postgresAdapter(sql) {
276
+ const executor = (handle) => async (text, values, protectAdmin) => {
277
+ const run = async (tx) => {
278
+ if (protectAdmin)
279
+ await tx.unsafe("SELECT pg_advisory_xact_lock(1296125516,1)", []);
280
+ return bunSql.run(tx, text, values);
281
+ };
282
+ return protectAdmin && handle === sql ? sql.begin("read write", tx => run(tx)) : run(handle);
283
+ };
284
+ const make = (handle, transaction) => (options) => build(postgres, true, "postgres", "Mantle PostgreSQL Adapter", executor(handle), transaction, options)(options);
285
+ const factory = (options) => make(sql, async (cb) => sql.begin("read write", tx => cb(make(tx, false)(options))))(options);
286
+ nativeReaders.set(factory, { exec: executor(sql), param: postgres.param, now: postgres.now });
287
+ return factory;
288
+ }
@@ -0,0 +1,3 @@
1
+ import { type Caller } from "../kernel/contracts.js";
2
+ /** Shared selected-boundary guard. Resolve fresh database authority only for Better Auth's raw admin endpoints. */
3
+ export declare function guardAuthAdminRequest(request: Request, resolve: () => Caller | Promise<Caller>, basePath?: string): Promise<void>;
@@ -0,0 +1,21 @@
1
+ import { checkOrigin, isAdmin, deny, ContractError } from "../kernel/contracts.js";
2
+ /** Shared selected-boundary guard. Resolve fresh database authority only for Better Auth's raw admin endpoints. */
3
+ export async function guardAuthAdminRequest(request, resolve, basePath = "/api/auth") {
4
+ let path;
5
+ try {
6
+ path = decodeURIComponent(new URL(request.url).pathname).replace(/\/{2,}/g, "/");
7
+ }
8
+ catch { }
9
+ if (path === undefined || path.split("/").some(part => part === "." || part === ".."))
10
+ throw new ContractError("INPUT_VALIDATION_FAILED", "Malformed auth path.", "request:path");
11
+ const admin = `${basePath.replace(/\/+$/, "")}/admin`;
12
+ if (path !== admin && !path.startsWith(`${admin}/`))
13
+ return;
14
+ const caller = await resolve();
15
+ // Mantle policy: only the admin layer reaches Better Auth admin endpoints (oauth-provider's /admin/oauth2/* admits any
16
+ // session by default); Better Auth access control then decides each action.
17
+ // Any signed-in user may ask about their own permissions or end an impersonation.
18
+ const own = path === `${admin}/has-permission` || path === `${admin}/stop-impersonating`;
19
+ deny(caller, caller.kind === "user" && caller.credential === "session" && (own || isAdmin(caller)), "auth:admin");
20
+ checkOrigin(request, caller);
21
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Request -> Caller with Better Auth's own API only: no cookie signing, expiry, refresh, revocation or token rule is
3
+ * re-implemented here. Nothing in this module imports Better Auth statically, so a public route that never calls
4
+ * `resolveCaller` never loads it, and a request without credentials never loads it either.
5
+ */
6
+ import type { BetterAuthOptions } from "better-auth/types";
7
+ import { type Caller } from "../kernel/contracts.js";
8
+ import type { UserFields } from "../kernel/user-fields.js";
9
+ type User = {
10
+ name?: string | null;
11
+ email?: string | null;
12
+ image?: string | null;
13
+ githubLogin?: string | null;
14
+ id: string;
15
+ role?: string | null;
16
+ banned?: boolean | null;
17
+ banExpires?: Date | string | number | null;
18
+ };
19
+ /** Display identity from the session this Caller already resolved; never an authority source. */
20
+ /** The Better Auth user record the caller was resolved from (session, OAuth or service credential): name, email, image. */
21
+ export declare const userOf: (caller: Caller) => Readonly<User> | undefined;
22
+ /** @deprecated Use `userOf`; it covers every credential, not only sessions. */
23
+ export declare const sessionUserOf: (caller: Caller) => Readonly<User> | undefined;
24
+ /** The admin plugin's ban, lapsed once `banExpires` has passed (as the plugin reads it at sign-in). */
25
+ export declare const isBanned: (user: User) => boolean;
26
+ /** The application's `betterAuth()` instance, as far as Caller resolution reads it. */
27
+ export interface AuthInstance {
28
+ readonly options: BetterAuthOptions;
29
+ readonly $context: Promise<unknown>;
30
+ readonly api: {
31
+ getSession(input: {
32
+ headers: Headers;
33
+ query?: {
34
+ disableCookieCache?: boolean;
35
+ disableRefresh?: boolean;
36
+ };
37
+ returnHeaders: true;
38
+ }): Promise<{
39
+ headers: Headers | null;
40
+ response: {
41
+ session: {
42
+ id: string;
43
+ impersonatedBy?: string | null;
44
+ };
45
+ user: User;
46
+ } | null;
47
+ }>;
48
+ };
49
+ }
50
+ /** Better Auth owns the role model; absence of its admin plugin grants no admin layer. */
51
+ export declare function adminOptionsOf(auth: Pick<AuthInstance, "options">): {
52
+ adminRoles: string[];
53
+ adminUserIds: readonly string[];
54
+ defaultRole: string;
55
+ };
56
+ export interface AuthLoader<A extends AuthInstance = AuthInstance> {
57
+ /** `advanced.cookiePrefix` of the same options; read before Better Auth loads. */
58
+ readonly cookiePrefix: string;
59
+ /** The instance, imported on first use and checked against `cookiePrefix`. */
60
+ readonly get: () => Promise<A>;
61
+ }
62
+ /**
63
+ * `load` should be a dynamic import of the module that builds `betterAuth()`, so the bundler keeps it (and Better Auth)
64
+ * out of the entry module: `authLoader(() => import("./auth.js").then(m => m.auth))`.
65
+ */
66
+ export declare function authLoader<A extends AuthInstance>(load: () => Promise<A>, cookiePrefix?: string): AuthLoader<A>;
67
+ /**
68
+ * The Caller for a request that is not public. Public routes must not call it.
69
+ * - `Authorization: Bearer`: an OAuth access token verified by Better Auth's resource client for `audience`; an invalid
70
+ * token is UNAUTHENTICATED, never anonymous. The user (role, ban) is read from the database on every request.
71
+ * - Otherwise a session cookie, validated by `auth.api.getSession` without the cookie cache, so revocation and role
72
+ * changes apply on the next request. `readOnly: true` explicitly permits a bounded official cookie snapshot
73
+ * only with no secondaryStorage and cookieCache {enabled:true,maxAge:1..60,refreshCache:false}.
74
+ * The application classifies reads; HTTP method alone is not sufficient. No cookie means anonymous without loading Better Auth.
75
+ * When Better Auth extends the session (`updateAge`) it sets a fresh cookie: pass the outgoing response's headers as
76
+ * `responseHeaders` and those Set-Cookie lines are appended, so the browser cookie slides with the session.
77
+ */
78
+ export declare function resolveCaller(request: Request, auth: AuthLoader, options?: {
79
+ readonly fields?: UserFields;
80
+ readonly audience?: string | readonly string[];
81
+ readonly readOnly?: boolean;
82
+ readonly responseHeaders?: Headers;
83
+ readonly scopes?: readonly string[];
84
+ readonly resolveCredential?: (request: Request) => Caller | null | Promise<Caller | null>;
85
+ }): Promise<Caller>;
86
+ export {};
@@ -0,0 +1,155 @@
1
+ import { ContractError } from "../kernel/contracts.js";
2
+ import { userCaller } from "../kernel/user-caller.js";
3
+ const users = new WeakMap();
4
+ const withUser = (caller, user) => (users.set(caller, user), caller);
5
+ /** Display identity from the session this Caller already resolved; never an authority source. */
6
+ /** The Better Auth user record the caller was resolved from (session, OAuth or service credential): name, email, image. */
7
+ export const userOf = (caller) => users.get(caller);
8
+ /** @deprecated Use `userOf`; it covers every credential, not only sessions. */
9
+ export const sessionUserOf = userOf;
10
+ /** The admin plugin's ban, lapsed once `banExpires` has passed (as the plugin reads it at sign-in). */
11
+ export const isBanned = (user) => !!user.banned && (user.banExpires == null || new Date(user.banExpires).getTime() > Date.now());
12
+ /** Better Auth owns the role model; absence of its admin plugin grants no admin layer. */
13
+ export function adminOptionsOf(auth) {
14
+ const plugin = auth.options.plugins?.find(plugin => plugin.id === "admin");
15
+ const configured = plugin?.options?.adminRoles ?? ["admin"];
16
+ return { adminRoles: plugin ? (typeof configured === "string" ? configured.split(",") : [...configured]).map(role => role.trim()) : [], adminUserIds: plugin?.options?.adminUserIds ?? [], defaultRole: plugin?.options?.defaultRole ?? "user" };
17
+ }
18
+ /**
19
+ * `load` should be a dynamic import of the module that builds `betterAuth()`, so the bundler keeps it (and Better Auth)
20
+ * out of the entry module: `authLoader(() => import("./auth.js").then(m => m.auth))`.
21
+ */
22
+ export function authLoader(load, cookiePrefix = "better-auth") {
23
+ let instance;
24
+ return {
25
+ cookiePrefix,
26
+ get: () => instance ??= load().then((auth) => {
27
+ // Better Auth's getSessionCookie looks the cookie up by prefix only: any other spelling would make every signed-in user anonymous.
28
+ if ((auth.options.advanced?.cookiePrefix ?? "better-auth") !== cookiePrefix)
29
+ throw new Error(`authLoader cookiePrefix '${cookiePrefix}' differs from the auth options' advanced.cookiePrefix.`);
30
+ if (auth.options.advanced?.cookies?.session_token?.name)
31
+ throw new Error("advanced.cookies.session_token.name is not supported: Better Auth's getSessionCookie cannot find a renamed session cookie.");
32
+ if (auth.options.secondaryStorage && !auth.options.session?.storeSessionInDatabase)
33
+ throw new ContractError("AUTH_CONFIGURATION_ERROR", "Session secondaryStorage requires session.storeSessionInDatabase: true.", "auth:session");
34
+ return auth;
35
+ }).catch((error) => { instance = undefined; throw error; }),
36
+ };
37
+ }
38
+ function checkFieldSources(instance, fields, session = false) {
39
+ for (const field of Object.keys(fields ?? {})) {
40
+ const additional = instance.options.user?.additionalFields;
41
+ const stored = additional && Object.hasOwn(additional, field) ? additional[field] : undefined;
42
+ if (!(stored ? stored.input === false : session && instance.options.plugins?.some(plugin => plugin.id === "custom-session")))
43
+ throw new ContractError("AUTH_CONFIGURATION_ERROR", `User field '${field}' requires an input: false additional field; customSession-only fields are available only to session credentials.`, "auth:user");
44
+ }
45
+ }
46
+ /** All credentials read current authority from the same Better Auth user record. */
47
+ async function loadSubject(instance, id, fields, sessionUser) {
48
+ checkFieldSources(instance, fields, !!sessionUser);
49
+ const context = await instance.$context;
50
+ const user = await context.internalAdapter.findUserById(id);
51
+ if (!user || isBanned(user))
52
+ return null;
53
+ if (sessionUser)
54
+ for (const field of Object.keys(fields ?? {})) {
55
+ if (!Object.hasOwn(instance.options.user?.additionalFields ?? {}, field) && Object.hasOwn(sessionUser, field))
56
+ user[field] = sessionUser[field];
57
+ }
58
+ return user;
59
+ }
60
+ /**
61
+ * The Caller for a request that is not public. Public routes must not call it.
62
+ * - `Authorization: Bearer`: an OAuth access token verified by Better Auth's resource client for `audience`; an invalid
63
+ * token is UNAUTHENTICATED, never anonymous. The user (role, ban) is read from the database on every request.
64
+ * - Otherwise a session cookie, validated by `auth.api.getSession` without the cookie cache, so revocation and role
65
+ * changes apply on the next request. `readOnly: true` explicitly permits a bounded official cookie snapshot
66
+ * only with no secondaryStorage and cookieCache {enabled:true,maxAge:1..60,refreshCache:false}.
67
+ * The application classifies reads; HTTP method alone is not sufficient. No cookie means anonymous without loading Better Auth.
68
+ * When Better Auth extends the session (`updateAge`) it sets a fresh cookie: pass the outgoing response's headers as
69
+ * `responseHeaders` and those Set-Cookie lines are appended, so the browser cookie slides with the session.
70
+ */
71
+ export async function resolveCaller(request, auth, options = {}) {
72
+ const lacks = (have) => options.scopes?.some(scope => !have.includes(scope));
73
+ const supplied = await options.resolveCredential?.(request);
74
+ if (supplied) {
75
+ if (supplied.kind !== "user" || supplied.credential === "session")
76
+ throw new ContractError("UNAUTHENTICATED", "Invalid service credential.");
77
+ if (lacks(supplied.scopes))
78
+ throw new ContractError("AUTH_DENIED", "The service credential lacks a required scope.");
79
+ const instance = await auth.get();
80
+ const user = await loadSubject(instance, supplied.subject, options.fields);
81
+ if (!user)
82
+ throw new ContractError("UNAUTHENTICATED", "The service credential's user is not active.");
83
+ return withUser(userCaller(user, { fields: options.fields, ...adminOptionsOf(instance), credential: supplied.credential, credentialId: supplied.credentialId, clientId: supplied.clientId, scopes: supplied.scopes, issuer: supplied.issuer }), user);
84
+ }
85
+ const authorization = request.headers.get("authorization");
86
+ if (authorization && /^(bearer|dpop) /i.test(authorization)) {
87
+ if (!options.audience)
88
+ throw new ContractError("UNAUTHENTICATED", "This route does not accept access tokens.");
89
+ const instance = await auth.get();
90
+ checkFieldSources(instance, options.fields);
91
+ const { oauthProviderResourceClient } = await import("@better-auth/oauth-provider/resource-client");
92
+ const { createDpopReplayStore } = await import("better-auth/oauth2");
93
+ const context = await instance.$context;
94
+ // Issuance uses the normalized auth context URL, not the raw origin-only baseURL option.
95
+ const verifier = oauthProviderResourceClient({ options: { ...instance.options, baseURL: context.baseURL, basePath: "" }, $context: instance.$context }).getActions();
96
+ let claims;
97
+ try {
98
+ claims = await verifier.verifyAccessTokenRequest(request, { verifyOptions: { audience: options.audience }, dpop: { replayStore: createDpopReplayStore(context.internalAdapter) } });
99
+ }
100
+ catch (cause) {
101
+ throw new ContractError("UNAUTHENTICATED", "The access token is not valid for this resource.", "", { cause });
102
+ }
103
+ const { assertActiveUserGrant } = await import("./oauth.js");
104
+ const granted = await assertActiveUserGrant(instance, claims, options.audience);
105
+ const user = await loadSubject(instance, granted.id, options.fields);
106
+ const scopes = typeof claims.scope === "string" ? claims.scope.split(/\s+/).filter(Boolean) : [];
107
+ if (lacks(scopes))
108
+ throw new ContractError("AUTH_DENIED", "The access token lacks a required scope.");
109
+ if (!user)
110
+ throw new ContractError("UNAUTHENTICATED", "The access token's user is not active.");
111
+ const text = (v) => (typeof v === "string" ? v : null);
112
+ return withUser(userCaller(user, { fields: options.fields, ...adminOptionsOf(instance), issuer: claims.iss, scopes, credential: "oauth", credentialId: text(claims.jti), clientId: text(claims.azp) ?? text(claims.client_id) }), user);
113
+ }
114
+ if (authorization !== null)
115
+ throw new ContractError("UNAUTHENTICATED", "The authorization header is not a supported credential.");
116
+ // With no Cookie header at all getSessionCookie returns null: skip even loading the helper.
117
+ if (!request.headers.has("cookie"))
118
+ return { kind: "anonymous" };
119
+ const { getSessionCookie, splitSetCookieHeader } = await import("better-auth/cookies");
120
+ if (!getSessionCookie(request, { cookiePrefix: auth.cookiePrefix }))
121
+ return { kind: "anonymous" };
122
+ const instance = await auth.get();
123
+ checkFieldSources(instance, options.fields, true);
124
+ const mirror = !!instance.options.secondaryStorage;
125
+ const cache = instance.options.session?.cookieCache;
126
+ // Only an application-classified read may use Better Auth's bounded signed snapshot. KV remains primary-guarded.
127
+ const cachedRead = options.readOnly === true && !mirror && cache?.enabled === true && cache.refreshCache === false &&
128
+ typeof cache.maxAge === "number" && cache.maxAge >= 1 && cache.maxAge <= 60;
129
+ const initial = await instance.api.getSession({ headers: request.headers, query: { disableCookieCache: !cachedRead, disableRefresh: mirror || !options.responseHeaders }, returnHeaders: true });
130
+ const found = initial.response;
131
+ if (!found)
132
+ return { kind: "anonymous" };
133
+ if (mirror) {
134
+ // disableCookieCache bypasses browser cookies, not KV. Only mirrors need a primary authority re-read.
135
+ if (!instance.options.session?.storeSessionInDatabase)
136
+ throw new ContractError("AUTH_CONFIGURATION_ERROR", "Caller resolution with secondaryStorage requires session.storeSessionInDatabase: true.", "auth:session");
137
+ const context = await instance.$context;
138
+ const live = await context.adapter.findOne({ model: "session", where: [{ field: "id", value: found.session.id }] });
139
+ if (!live || live.userId !== found.user.id || !(new Date(live.expiresAt).getTime() > Date.now()))
140
+ return { kind: "anonymous" };
141
+ }
142
+ const user = await loadSubject(instance, found.user.id, options.fields, found.user);
143
+ if (!user)
144
+ return { kind: "anonymous" };
145
+ // Never let a stale secondary snapshot extend an already-expired primary session before the check above.
146
+ if (options.responseHeaders) {
147
+ const refreshed = mirror ? await instance.api.getSession({ headers: request.headers, query: { disableCookieCache: true }, returnHeaders: true }) : initial;
148
+ if (!refreshed.response || refreshed.response.session.id !== found.session.id || refreshed.response.user.id !== user.id)
149
+ return { kind: "anonymous" };
150
+ for (const cookie of refreshed.headers?.getSetCookie().flatMap(splitSetCookieHeader) ?? [])
151
+ options.responseHeaders.append("set-cookie", cookie);
152
+ }
153
+ const caller = userCaller(user, { fields: options.fields, ...adminOptionsOf(instance), credentialId: found.session.id, impersonatedBy: found.session.impersonatedBy ?? undefined });
154
+ return withUser(caller, user);
155
+ }
@@ -0,0 +1,24 @@
1
+ import type { BetterAuthOptions } from "better-auth/types";
2
+ /** Wrap the app's auth handler call with the current Worker's ctx.waitUntil.bind(ctx). Concurrent requests stay isolated. */
3
+ export declare function withAuthTasks<T>(waitUntil: (task: Promise<unknown>) => void, run: () => T): T;
4
+ export declare function retainAuthTask(task: Promise<unknown>): void;
5
+ /** Keyed HMAC prevents enumeration of the six-digit OTP space after a database leak. */
6
+ export declare function hashEmailOtp(secret: string, otp: string): Promise<string>;
7
+ /** Never default to a client-controlled X-Forwarded-For. The host must overwrite the configured headers. */
8
+ export declare function authHardening({ ipAddressHeaders, baseURL, errorURL }: {
9
+ ipAddressHeaders: readonly string[];
10
+ baseURL: string;
11
+ errorURL?: string;
12
+ }): Pick<BetterAuthOptions, "advanced" | "rateLimit" | "onAPIError">;
13
+ export type AuthHookContext = {
14
+ path: string;
15
+ params?: Readonly<Record<string, unknown>>;
16
+ } | null;
17
+ /** Accept the provider-owned profile only on explicitly trusted callbacks, never sign-up/update JSON. */
18
+ export declare function guardGithubLogin(user: Readonly<Record<string, unknown>>, ctx: AuthHookContext, trustedProviders?: readonly string[]): {
19
+ data: {
20
+ githubLogin: null;
21
+ };
22
+ } | undefined;
23
+ /** First-deploy facade: failed setup never falls back to an anonymous/successful auth response. */
24
+ export declare const authSetupIncomplete: () => Response;
@@ -0,0 +1,40 @@
1
+ /** Optional app-owned Better Auth configuration; no Better Auth runtime import. */
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ const retention = new AsyncLocalStorage();
4
+ /** Wrap the app's auth handler call with the current Worker's ctx.waitUntil.bind(ctx). Concurrent requests stay isolated. */
5
+ export function withAuthTasks(waitUntil, run) { return retention.run(waitUntil, run); }
6
+ export function retainAuthTask(task) {
7
+ const settled = task.catch(() => { console.error("[mantle auth] background task failed"); });
8
+ retention.getStore()?.(settled);
9
+ }
10
+ /** Keyed HMAC prevents enumeration of the six-digit OTP space after a database leak. */
11
+ export async function hashEmailOtp(secret, otp) {
12
+ if (!secret)
13
+ throw new Error("AUTH_SECRET is required for OTP hashing.");
14
+ const bytes = new TextEncoder(), key = await crypto.subtle.importKey("raw", bytes.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
15
+ const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key, bytes.encode(otp)));
16
+ return btoa(String.fromCharCode(...mac)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
17
+ }
18
+ /** Never default to a client-controlled X-Forwarded-For. The host must overwrite the configured headers. */
19
+ export function authHardening({ ipAddressHeaders, baseURL, errorURL = "/" }) {
20
+ const headers = ipAddressHeaders.map(h => h.trim()).filter(Boolean);
21
+ if (!headers.length || headers.some(h => !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(h)))
22
+ throw new Error("ipAddressHeaders must name host-trusted ingress headers.");
23
+ const target = new URL(errorURL, baseURL);
24
+ if (target.origin !== new URL(baseURL).origin)
25
+ throw new Error("Auth errorURL must be same-origin.");
26
+ return { advanced: { ipAddress: { ipAddressHeaders: headers }, backgroundTasks: { handler: retainAuthTask } },
27
+ // Shared across isolates; Better Auth's defaults and plugin rules (e.g. 3/min per OTP path) set the limits.
28
+ rateLimit: { enabled: true, storage: "database" }, onAPIError: { errorURL: `${target.pathname}${target.search}` } };
29
+ }
30
+ /** Accept the provider-owned profile only on explicitly trusted callbacks, never sign-up/update JSON. */
31
+ export function guardGithubLogin(user, ctx, trustedProviders = ["github"]) {
32
+ if (!("githubLogin" in user))
33
+ return;
34
+ const provider = ctx?.path === "/callback/:id" ? ctx.params?.id : null;
35
+ if (typeof provider === "string" && trustedProviders.includes(provider))
36
+ return;
37
+ return { data: { githubLogin: null } };
38
+ }
39
+ /** First-deploy facade: failed setup never falls back to an anonymous/successful auth response. */
40
+ export const authSetupIncomplete = () => Response.json({ error: { code: "SERVICE_UNAVAILABLE", message: "Authentication setup is incomplete.", path: "auth" } }, { status: 503, headers: { "cache-control": "no-store", "retry-after": "60" } });