@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.
- package/README.md +166 -298
- package/dist/auth/adapter.d.ts +32 -0
- package/dist/auth/adapter.js +288 -0
- package/dist/auth/admin.d.ts +3 -0
- package/dist/auth/admin.js +21 -0
- package/dist/auth/caller.d.ts +86 -0
- package/dist/auth/caller.js +155 -0
- package/dist/auth/hardening.d.ts +24 -0
- package/dist/auth/hardening.js +40 -0
- package/dist/auth/identity.d.ts +121 -0
- package/dist/auth/identity.js +203 -0
- package/dist/auth/oauth.d.ts +63 -0
- package/dist/auth/oauth.js +217 -0
- package/dist/auth/resource-seeds.js +10 -0
- package/dist/auth/routes.d.ts +32 -0
- package/dist/auth/routes.js +226 -0
- package/dist/build/compiler.d.ts +721 -0
- package/dist/build/compiler.js +7029 -0
- package/dist/build/spec/domain/model/ManifestGrammar.d.ts +439 -0
- package/dist/build/spec/domain/model/RuntimePlan.d.ts +168 -0
- package/dist/build/spec/domain/model/SqlIr.d.ts +129 -0
- package/dist/build/spec/domain/model/UserFields.d.ts +21 -0
- package/dist/build/spec/domain/service/PlanFingerprint.d.ts +7 -0
- package/dist/build/spec/kernel/diagnostic.d.ts +108 -0
- package/dist/bun/assets.d.ts +2 -0
- package/dist/bun/assets.js +24 -0
- package/dist/domain/adminOrder.js +24 -0
- package/dist/domain/columns.js +57 -0
- package/dist/domain/describe/chunk-ZVXBGP7Y.js +424 -0
- package/dist/domain/describe/index.d.ts +58 -0
- package/dist/domain/describe/index.js +115 -0
- package/dist/domain/describe/target-hash.d.ts +8 -0
- package/dist/domain/describe/target-hash.js +12 -0
- package/dist/domain/describe/targets.d.ts +40 -0
- package/dist/domain/entry.js +33 -0
- package/dist/domain/generated-tools.js +19 -0
- package/dist/domain/generation.d.ts +16 -0
- package/dist/domain/generation.js +1 -0
- package/dist/domain/input.js +17 -0
- package/dist/domain/model.js +1 -0
- package/dist/domain/paging.js +230 -0
- package/dist/domain/recipes.js +24 -0
- package/dist/domain/userReferences.js +5 -0
- package/dist/domain/wireScalars.js +47 -0
- package/dist/infrastructure/adminEntries.js +153 -0
- package/dist/infrastructure/adminPaging.js +72 -0
- package/dist/infrastructure/artifacts.js +71 -0
- package/dist/infrastructure/authSchema.js +83 -0
- package/dist/infrastructure/bundle.js +188 -0
- package/dist/infrastructure/cli.js +118 -0
- package/dist/infrastructure/generate.d.ts +12 -0
- package/dist/infrastructure/generate.js +189 -0
- package/dist/infrastructure/generateAuth.d.ts +8 -0
- package/dist/infrastructure/generateAuth.js +74 -0
- package/dist/infrastructure/help.js +60 -0
- package/dist/infrastructure/init.js +54 -0
- package/dist/infrastructure/inputs.js +41 -0
- package/dist/infrastructure/mounts.js +25 -0
- package/dist/infrastructure/wrangler.js +109 -0
- package/dist/kernel/authorization.d.ts +4 -0
- package/dist/kernel/authorization.js +16 -0
- package/dist/kernel/binds.d.ts +60 -0
- package/dist/kernel/binds.js +49 -0
- package/dist/kernel/caller-identity.d.ts +24 -0
- package/dist/kernel/caller-identity.js +22 -0
- package/dist/kernel/contract-error.d.ts +12 -0
- package/dist/kernel/contract-error.js +14 -0
- package/dist/kernel/contracts.d.ts +143 -0
- package/dist/kernel/contracts.js +97 -0
- package/dist/kernel/cron.d.ts +1 -0
- package/dist/kernel/cron.js +58 -0
- package/dist/kernel/dialect/common.d.ts +38 -0
- package/dist/kernel/dialect/common.js +6 -0
- package/dist/kernel/dialect/postgres.d.ts +16 -0
- package/dist/kernel/dialect/postgres.js +18 -0
- package/dist/kernel/dialect/sqlite.d.ts +20 -0
- package/dist/kernel/dialect/sqlite.js +34 -0
- package/dist/kernel/drain.d.ts +5 -0
- package/dist/kernel/drain.js +35 -0
- package/dist/kernel/driver/bun-sqlite.d.ts +19 -0
- package/dist/kernel/driver/bun-sqlite.js +77 -0
- package/dist/kernel/driver/d1.d.ts +24 -0
- package/dist/kernel/driver/d1.js +22 -0
- package/dist/kernel/driver/postgres.d.ts +25 -0
- package/dist/kernel/driver/postgres.js +99 -0
- package/dist/kernel/driver/transaction-context.d.ts +10 -0
- package/dist/kernel/driver/transaction-context.js +29 -0
- package/dist/kernel/email.d.ts +28 -0
- package/dist/kernel/email.js +38 -0
- package/dist/kernel/host-access.d.ts +10 -0
- package/dist/kernel/host-access.js +65 -0
- package/dist/kernel/host-caller.d.ts +39 -0
- package/dist/kernel/host-caller.js +56 -0
- package/dist/kernel/json-body.d.ts +4 -0
- package/dist/kernel/json-body.js +44 -0
- package/dist/kernel/mcp-apps.d.ts +69 -0
- package/dist/kernel/mcp-apps.js +40 -0
- package/dist/kernel/mcp-call.d.ts +5 -0
- package/dist/kernel/mcp-call.js +25 -0
- package/dist/kernel/mcp-handler.js +371 -0
- package/dist/kernel/mcp.d.ts +110 -0
- package/dist/kernel/mcp.js +50 -0
- package/dist/kernel/media.d.ts +27 -0
- package/dist/kernel/media.js +216 -0
- package/dist/kernel/paging.d.ts +55 -0
- package/dist/kernel/paging.js +95 -0
- package/dist/kernel/publishing.d.ts +4 -0
- package/dist/kernel/publishing.js +16 -0
- package/dist/kernel/r2-media.d.ts +42 -0
- package/dist/kernel/r2-media.js +78 -0
- package/dist/kernel/rest-dispatch.js +38 -0
- package/dist/kernel/rest-router.js +64 -0
- package/dist/kernel/rest.d.ts +58 -0
- package/dist/kernel/rest.js +41 -0
- package/dist/kernel/scalars.d.ts +6 -0
- package/dist/kernel/scalars.js +44 -0
- package/dist/kernel/search.js +5 -0
- package/dist/kernel/site-types.d.ts +146 -0
- package/dist/kernel/site-types.js +1 -0
- package/dist/kernel/site.d.ts +39 -0
- package/dist/kernel/site.js +32 -0
- package/dist/kernel/store.d.ts +175 -0
- package/dist/kernel/store.js +269 -0
- package/dist/kernel/surface.d.ts +10 -0
- package/dist/kernel/surface.js +19 -0
- package/dist/kernel/sweep.d.ts +17 -0
- package/dist/kernel/sweep.js +27 -0
- package/dist/kernel/translations.d.ts +1 -0
- package/dist/kernel/translations.js +13 -0
- package/dist/kernel/user-caller.d.ts +31 -0
- package/dist/kernel/user-caller.js +67 -0
- package/dist/kernel/user-field-name.js +3 -0
- package/dist/kernel/user-fields.d.ts +35 -0
- package/dist/kernel/user-fields.js +81 -0
- package/dist/kernel/validation.d.ts +15 -0
- package/dist/kernel/validation.js +54 -0
- package/dist/kernel/web-gateway.js +68 -0
- package/dist/kernel/web-head.js +15 -0
- package/dist/kernel/web-index.js +40 -0
- package/dist/kernel/web-response.js +8 -0
- package/dist/kernel/web.d.ts +77 -0
- package/dist/kernel/web.js +124 -0
- package/dist/kernel/where.js +27 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/dist/usecase/adminBindings.js +143 -0
- package/dist/usecase/adminClient.js +87 -0
- package/dist/usecase/adminDescriptors.js +107 -0
- package/dist/usecase/generate.js +166 -0
- package/dist/usecase/handlers.js +21 -0
- package/dist/usecase/mcp-media.js +17 -0
- package/dist/usecase/mcp.js +100 -0
- package/dist/usecase/paging.js +71 -0
- package/dist/usecase/publishing.js +63 -0
- package/dist/usecase/refusals.js +27 -0
- package/dist/usecase/rest.js +54 -0
- package/dist/usecase/search.js +7 -0
- package/dist/usecase/site.js +39 -0
- package/dist/usecase/stores.js +64 -0
- package/dist/usecase/tool-version.js +9 -0
- package/dist/usecase/translations.js +78 -0
- package/dist/usecase/ttl.js +20 -0
- package/dist/usecase/validation.js +241 -0
- package/dist/usecase/web.js +23 -0
- package/docs/examples/README.md +39 -18
- package/docs/examples/app-shell/.dev.vars.example +4 -0
- package/docs/examples/app-shell/README.md +121 -0
- package/docs/examples/app-shell/accept.mjs +91 -0
- package/docs/examples/app-shell/gitignore +7 -0
- package/docs/examples/app-shell/package.json +43 -0
- package/docs/examples/app-shell/pnpm-workspace.yaml +8 -0
- package/docs/examples/app-shell/smoke-kit.mjs +168 -0
- package/docs/examples/app-shell/src/auth.ts +58 -0
- package/docs/examples/app-shell/src/handlers.ts +5 -0
- package/docs/examples/app-shell/src/index.ts +4 -0
- package/docs/examples/app-shell/src/routes.ts +14 -0
- package/docs/examples/app-shell/src/schedule.ts +8 -0
- package/docs/examples/app-shell/src/service.ts +70 -0
- package/docs/examples/app-shell/src/sign-in.ts +41 -0
- package/docs/examples/app-shell/src/signup.ts +8 -0
- package/docs/examples/app-shell/tsconfig.json +26 -0
- package/docs/examples/app-shell/worker-configuration.d.ts +22 -0
- package/docs/examples/app-shell/wrangler.jsonc +62 -0
- package/docs/examples/club-events/README.md +65 -0
- package/docs/examples/club-events/manifests/club-events.yaml +247 -0
- package/docs/examples/club-events/smoke.mjs +127 -0
- package/docs/examples/club-events/src/handlers.ts +25 -0
- package/docs/examples/club-events/src/signup.ts +9 -0
- package/docs/examples/generated-web/README.md +78 -0
- package/docs/examples/generated-web/posts.yaml +38 -0
- package/docs/examples/generated-web/seed.sql +6 -0
- package/docs/examples/generated-web/src/worker.ts +43 -0
- package/docs/examples/generated-web/tsconfig.json +8 -0
- package/docs/examples/generated-web/wrangler.jsonc +8 -0
- package/docs/examples/purchasing/.dev.vars.example +6 -0
- package/docs/examples/purchasing/README.md +99 -0
- package/docs/examples/purchasing/manifests/purchasing.yaml +374 -0
- package/docs/examples/purchasing/seed.sql +4 -0
- package/docs/examples/purchasing/smoke.mjs +191 -0
- package/docs/examples/purchasing/src/admin.tsx +13 -0
- package/docs/examples/purchasing/src/handlers.ts +33 -0
- package/docs/examples/purchasing/src/notify.ts +29 -0
- package/docs/examples/purchasing/src/requests-page.ts +57 -0
- package/docs/examples/purchasing/src/routes.ts +53 -0
- package/docs/examples/purchasing/src/schedule.ts +31 -0
- package/docs/examples/purchasing/src/signup.ts +5 -0
- package/docs/examples/purchasing/src/supplier.ts +32 -0
- package/docs/examples/purchasing/wrangler.jsonc +69 -0
- package/docs/examples/recipes/README.md +60 -0
- package/docs/examples/recipes/manifests/recipes.yaml +75 -0
- package/docs/examples/recipes/smoke.mjs +102 -0
- package/docs/examples/recipes/src/media.ts +13 -0
- package/docs/examples/recipes/src/pages.ts +99 -0
- package/docs/examples/recipes/src/routes.ts +71 -0
- package/docs/examples/recipes/src/site-config.ts +12 -0
- package/docs/examples/recipes/wrangler.jsonc +74 -0
- package/docs/handbook/cloudflare/authentication.md +180 -322
- package/docs/handbook/cloudflare/chatgpt-sites.md +5 -23
- package/docs/handbook/cloudflare/deploy-and-operate.md +55 -77
- package/docs/handbook/cloudflare/media-r2.md +61 -205
- package/docs/handbook/cloudflare/service-entry.md +239 -0
- package/docs/handbook/concepts/authorization.md +10 -107
- package/docs/handbook/concepts/four-atoms.md +9 -168
- package/docs/handbook/concepts/lifecycle-and-locales.md +9 -132
- package/docs/handbook/concepts/mcp-and-agents.md +24 -121
- package/docs/handbook/concepts/procedures-and-triggers.md +5 -217
- package/docs/handbook/concepts/runtime-and-adapters.md +15 -169
- package/docs/handbook/concepts/views.md +10 -185
- package/docs/handbook/examples/hub.md +3 -8
- package/docs/handbook/guides/adapter-guide.md +11 -0
- package/docs/handbook/guides/admin-ui.md +115 -133
- package/docs/handbook/guides/agent-setup.md +64 -74
- package/docs/handbook/guides/agent-workflow-acceptance.md +232 -0
- package/docs/handbook/guides/email.md +86 -0
- package/docs/handbook/guides/local-testing.md +40 -0
- package/docs/handbook/guides/mcp-apps.md +560 -0
- package/docs/handbook/guides/native-admin.md +566 -0
- package/docs/handbook/guides/native-execution.md +133 -0
- package/docs/handbook/guides/oauth.md +219 -0
- package/docs/handbook/guides/postgres-adapters.md +56 -0
- package/docs/handbook/guides/public-pages.md +87 -0
- package/docs/handbook/guides/size-and-startup.md +9 -0
- package/docs/handbook/guides/sql-patterns.md +266 -0
- package/docs/handbook/guides/typed-queries.md +211 -107
- package/docs/handbook/navigation.json +176 -55
- package/docs/handbook/reference/admin-descriptors.md +53 -0
- package/docs/handbook/reference/authorization.md +39 -266
- package/docs/handbook/reference/describe.md +56 -0
- package/docs/handbook/reference/diagnostics.md +19 -186
- package/docs/handbook/reference/features.md +45 -55
- package/docs/handbook/reference/manifest.md +6 -129
- package/docs/handbook/reference/procedure.md +11 -303
- package/docs/handbook/reference/schema.md +64 -240
- package/docs/handbook/reference/site-config.md +5 -218
- package/docs/handbook/reference/sql.md +64 -0
- package/docs/handbook/reference/surface.md +392 -201
- package/docs/handbook/reference/trigger.md +56 -192
- package/docs/handbook/reference/view.md +24 -243
- package/docs/handbook/releases/index.md +244 -77
- package/docs/handbook/start/agents.md +45 -0
- package/docs/handbook/start/overview.md +9 -48
- package/docs/handbook/start/project-and-cli.md +51 -104
- package/docs/handbook/start/quickstart-worker.md +43 -178
- package/docs/skills/develop/SKILL.md +79 -249
- package/docs/skills/integrate/SKILL.md +39 -50
- package/docs/skills/media-gc/SKILL.md +34 -10
- package/docs/skills/provision/SKILL.md +36 -186
- package/docs/skills/update/SKILL.md +23 -38
- package/docs/source/scripts/postgres-adapter.ts +101 -0
- package/docs/upgrade-0.1-to-0.2.md +227 -0
- package/package.json +120 -131
- package/skills/README.md +48 -108
- package/skills/mantle/SKILL.md +76 -0
- package/dist/admin-ui.d.ts +0 -2
- package/dist/admin-ui.d.ts.map +0 -1
- package/dist/admin-ui.js +0 -2
- package/dist/admin-ui.js.map +0 -1
- package/dist/admin.d.ts +0 -2
- package/dist/admin.d.ts.map +0 -1
- package/dist/admin.js +0 -2
- package/dist/admin.js.map +0 -1
- package/dist/auth.d.ts +0 -2
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js +0 -2
- package/dist/auth.js.map +0 -1
- package/dist/bun.d.ts +0 -2
- package/dist/bun.d.ts.map +0 -1
- package/dist/bun.js +0 -2
- package/dist/bun.js.map +0 -1
- package/dist/cli/generate-cloudflare.d.ts +0 -7
- package/dist/cli/generate-cloudflare.d.ts.map +0 -1
- package/dist/cli/generate-cloudflare.js +0 -123
- package/dist/cli/generate-cloudflare.js.map +0 -1
- package/dist/cli/generate-project.d.ts +0 -25
- package/dist/cli/generate-project.d.ts.map +0 -1
- package/dist/cli/generate-project.js +0 -282
- package/dist/cli/generate-project.js.map +0 -1
- package/dist/cli/generate-sites.d.ts +0 -8
- package/dist/cli/generate-sites.d.ts.map +0 -1
- package/dist/cli/generate-sites.js +0 -273
- package/dist/cli/generate-sites.js.map +0 -1
- package/dist/cli/generate.d.ts +0 -20
- package/dist/cli/generate.d.ts.map +0 -1
- package/dist/cli/generate.js +0 -299
- package/dist/cli/generate.js.map +0 -1
- package/dist/cli/harness.d.ts +0 -3
- package/dist/cli/harness.d.ts.map +0 -1
- package/dist/cli/harness.js +0 -214
- package/dist/cli/harness.js.map +0 -1
- package/dist/cli/main.d.ts +0 -3
- package/dist/cli/main.d.ts.map +0 -1
- package/dist/cli/main.js +0 -77
- package/dist/cli/main.js.map +0 -1
- package/dist/cli/skills.d.ts +0 -3
- package/dist/cli/skills.d.ts.map +0 -1
- package/dist/cli/skills.js +0 -128
- package/dist/cli/skills.js.map +0 -1
- package/dist/cloudflare.d.ts +0 -2
- package/dist/cloudflare.d.ts.map +0 -1
- package/dist/cloudflare.js +0 -2
- package/dist/cloudflare.js.map +0 -1
- package/dist/codegen/emitMantleModule.d.ts +0 -22
- package/dist/codegen/emitMantleModule.d.ts.map +0 -1
- package/dist/codegen/emitMantleModule.js +0 -291
- package/dist/codegen/emitMantleModule.js.map +0 -1
- package/dist/codegen.d.ts +0 -2
- package/dist/codegen.d.ts.map +0 -1
- package/dist/codegen.js +0 -2
- package/dist/codegen.js.map +0 -1
- package/dist/runtime-testing.d.ts +0 -2
- package/dist/runtime-testing.d.ts.map +0 -1
- package/dist/runtime-testing.js +0 -2
- package/dist/runtime-testing.js.map +0 -1
- package/dist/runtime.d.ts +0 -2
- package/dist/runtime.d.ts.map +0 -1
- package/dist/runtime.js +0 -2
- package/dist/runtime.js.map +0 -1
- package/dist/spec.d.ts +0 -2
- package/dist/spec.d.ts.map +0 -1
- package/dist/spec.js +0 -2
- package/dist/spec.js.map +0 -1
- package/dist/vercel-libsql.d.ts +0 -2
- package/dist/vercel-libsql.d.ts.map +0 -1
- package/dist/vercel-libsql.js +0 -2
- package/dist/vercel-libsql.js.map +0 -1
- package/dist/vercel.d.ts +0 -2
- package/dist/vercel.d.ts.map +0 -1
- package/dist/vercel.js +0 -2
- package/dist/vercel.js.map +0 -1
- package/dist/web.d.ts +0 -2
- package/dist/web.d.ts.map +0 -1
- package/dist/web.js +0 -2
- package/dist/web.js.map +0 -1
- package/docs/adapter-guide.md +0 -222
- package/docs/adr/0001-four-atom-manifest-model.md +0 -440
- package/docs/adr/0002-closed-enums-for-bindings.md +0 -253
- package/docs/adr/0007-ai-as-primary-author.md +0 -473
- package/docs/adr/0008-structured-diagnostic-shape.md +0 -338
- package/docs/adr/0009-consumer-supplied-manifests.md +0 -149
- package/docs/adr/0010-locale-and-translates.md +0 -453
- package/docs/adr/0011-adapter-port-spec.md +0 -281
- package/docs/adr/0012-views-as-public-rest.md +0 -161
- package/docs/adr/0013-agent-provisioned-consumer-projects.md +0 -154
- package/docs/adr/0014-auth-better-auth-and-multi-tenant-mcp.md +0 -900
- package/docs/adr/0016-site-semantic-layer.md +0 -70
- package/docs/adr/0017-media-multi-variant-agent-side-optimization.md +0 -181
- package/docs/adr/0018-core-starters-repository-boundary.md +0 -179
- package/docs/adr/0019-sealed-manifest-runtime-pipeline.md +0 -240
- package/docs/adr/0020-builtin-handler-contracts-and-matched-upsert.md +0 -199
- package/docs/adr/0021-retire-starter-scaffolding.md +0 -30
- package/docs/adr/0022-caller-observed-version-occ.md +0 -212
- package/docs/adr/0023-port-failure-contract.md +0 -69
- package/docs/adr/0024-manifest-native-schema-tables.md +0 -264
- package/docs/adr/0025-reserved-native-columns-and-published-only-public-views.md +0 -105
- package/docs/adr/0026-progressive-generate.md +0 -36
- package/docs/adr/0027-scheduled-procedure-triggers.md +0 -31
- package/docs/adr/0028-schema-ttl-expiry.md +0 -33
- package/docs/adr/README.md +0 -81
- package/docs/adr/adr-lite-803-request-diagnostics.md +0 -38
- package/docs/adr/adr-lite-808-route-readiness.md +0 -47
- package/docs/adr/adr-lite-809-bounded-public-content.md +0 -71
- package/docs/adr/adr-lite-812-native-parity.md +0 -150
- package/docs/adr/adr-lite-823-home-statistics.md +0 -64
- package/docs/adr/adr-lite-842-mcp-authoring.md +0 -30
- package/docs/adr/adr-lite-845-frontend-client.md +0 -38
- package/docs/adr/adr-lite-861-admin-webmcp.md +0 -42
- package/docs/adr/adr-lite-909-admin-ui-kit.md +0 -37
- package/docs/agent-prompts.md +0 -64
- package/docs/api-mcp-authorization.md +0 -5
- package/docs/assets/mantle-admin-developer.png +0 -0
- package/docs/assets/mantle-admin-operations.png +0 -0
- package/docs/assets/mantle-hero.jpg +0 -0
- package/docs/auth-hosting-model.md +0 -166
- package/docs/cloudflare-low-level-composition.md +0 -5
- package/docs/consumer-onboarding-audit.md +0 -65
- package/docs/deferred-lifecycle-queues.md +0 -5
- package/docs/design-atoms.md +0 -16
- package/docs/design-references/editorial-blog-2026-05-05.md +0 -218
- package/docs/direct-authoring.md +0 -5
- package/docs/examples/builtin-commerce.md +0 -269
- package/docs/examples/builtin-intake.md +0 -143
- package/docs/examples/builtin-legal-documents.md +0 -189
- package/docs/examples/builtin-procurement.md +0 -241
- package/docs/examples/builtin-publication.md +0 -241
- package/docs/examples/builtin-reservation.md +0 -149
- package/docs/examples/cf-primitives-commerce-inventory.md +0 -809
- package/docs/examples/cf-primitives-guarded-api.md +0 -429
- package/docs/examples/cf-primitives-intake-hooks.md +0 -319
- package/docs/examples/host-chatgpt-sites/.openai/hosting.json +0 -1
- package/docs/examples/host-chatgpt-sites/README.md +0 -70
- package/docs/examples/host-chatgpt-sites/drizzle/0000_sites_users.sql +0 -8
- package/docs/examples/host-chatgpt-sites/drizzle/0001_mantle.sql +0 -294
- package/docs/examples/host-chatgpt-sites/drizzle/0002_article_cover.sql +0 -5
- package/docs/examples/host-chatgpt-sites/drizzle/0003_store_instance_id.sql +0 -1
- package/docs/examples/host-chatgpt-sites/drizzle/0004_mantle.sql +0 -10
- package/docs/examples/host-chatgpt-sites/drizzle/0005_mantle.sql +0 -24
- package/docs/examples/host-chatgpt-sites/drizzle/meta/_journal.json +0 -1
- package/docs/examples/host-chatgpt-sites/drizzle/meta/mantle-state.json +0 -60
- package/docs/examples/host-chatgpt-sites/manifests/site.yaml +0 -27
- package/docs/examples/host-chatgpt-sites/package.json +0 -1
- package/docs/examples/host-chatgpt-sites/public/site.css +0 -1
- package/docs/examples/host-chatgpt-sites/scripts/build.mjs +0 -13
- package/docs/examples/host-chatgpt-sites/scripts/check.mjs +0 -101
- package/docs/examples/host-chatgpt-sites/scripts/migration.mjs +0 -43
- package/docs/examples/host-chatgpt-sites/src/chatgpt-auth.ts +0 -66
- package/docs/examples/host-chatgpt-sites/src/index.ts +0 -46
- package/docs/examples/host-chatgpt-sites/src/mcp.ts +0 -49
- package/docs/examples/host-chatgpt-sites/src/media.ts +0 -109
- package/docs/examples/host-chatgpt-sites/src/r2-lab.ts +0 -38
- package/docs/examples/host-chatgpt-sites/src/storage-fingerprint.json +0 -1
- package/docs/examples/host-chatgpt-sites/src/web.ts +0 -35
- package/docs/examples/host-chatgpt-sites/tsconfig.json +0 -1
- package/docs/examples/host-chatgpt-sites/wrangler.jsonc +0 -10
- package/docs/examples/host-local-admin-otp/.dev.vars.example +0 -3
- package/docs/examples/host-local-admin-otp/README.md +0 -80
- package/docs/examples/host-local-admin-otp/ensure-dev-vars.mjs +0 -5
- package/docs/examples/host-local-admin-otp/manifests/site.yaml +0 -25
- package/docs/examples/host-local-admin-otp/package.json +0 -29
- package/docs/examples/host-local-admin-otp/public/.gitkeep +0 -1
- package/docs/examples/host-local-admin-otp/smoke.mjs +0 -141
- package/docs/examples/host-local-admin-otp/src/index.ts +0 -54
- package/docs/examples/host-local-admin-otp/tsconfig.json +0 -17
- package/docs/examples/host-local-admin-otp/wrangler.jsonc +0 -23
- package/docs/examples/host-minimal-worker/README.md +0 -41
- package/docs/examples/host-minimal-worker/manifests/site.yaml +0 -25
- package/docs/examples/host-minimal-worker/package.json +0 -26
- package/docs/examples/host-minimal-worker/smoke.mjs +0 -23
- package/docs/examples/host-minimal-worker/src/index.ts +0 -4
- package/docs/examples/host-minimal-worker/tsconfig.json +0 -17
- package/docs/examples/host-minimal-worker/wrangler.jsonc +0 -22
- package/docs/handbook/chatgpt-sites/equipment-checkout.md +0 -231
- package/docs/handbook/chatgpt-sites/generated-app.md +0 -84
- package/docs/handbook/chatgpt-sites/host-reference.md +0 -113
- package/docs/handbook/chatgpt-sites/index.md +0 -133
- package/docs/handbook/cloudflare/bindings.md +0 -291
- package/docs/handbook/cloudflare/conventional-worker.md +0 -166
- package/docs/handbook/cloudflare/deferred-hooks-queues.md +0 -170
- package/docs/handbook/cloudflare/low-level-composition.md +0 -169
- package/docs/handbook/cloudflare/public-web.md +0 -144
- package/docs/handbook/cloudflare/site-chrome.md +0 -75
- package/docs/handbook/examples/commerce-transaction.md +0 -11
- package/docs/handbook/examples/commerce.md +0 -11
- package/docs/handbook/examples/guarded-api.md +0 -10
- package/docs/handbook/examples/intake-form.md +0 -12
- package/docs/handbook/examples/intake-hooks.md +0 -11
- package/docs/handbook/examples/legal-documents.md +0 -10
- package/docs/handbook/examples/procurement-approvals.md +0 -10
- package/docs/handbook/examples/publication.md +0 -10
- package/docs/handbook/examples/reservation.md +0 -10
- package/docs/handbook/start/quickstart-admin.md +0 -52
- package/docs/labels.md +0 -71
- package/docs/media-uploads.md +0 -5
- package/docs/migration-0.1.2.md +0 -13
- package/docs/performance-harness.md +0 -228
- package/docs/release-process.md +0 -244
- package/docs/schema-indexes.md +0 -5
- package/docs/sealed-pipeline-ownership.md +0 -101
- package/docs/skills/plugin/SKILL.md +0 -132
- package/docs/skills/theme/SKILL.md +0 -77
- package/docs/spec-only-host-adoption.md +0 -157
- package/docs/transaction-patterns.md +0 -5
- package/skills/develop/SKILL.md +0 -258
- package/skills/install/SKILL.md +0 -112
- package/skills/integrate/SKILL.md +0 -60
- package/skills/media-gc/SKILL.md +0 -85
- package/skills/plugin/SKILL.md +0 -132
- package/skills/provision/SKILL.md +0 -194
- package/skills/theme/SKILL.md +0 -77
- package/skills/update/SKILL.md +0 -51
- package/templates/chatgpt-sites/auth.ts +0 -66
- package/templates/chatgpt-sites/build.mjs +0 -14
- package/templates/chatgpt-sites/env.ts +0 -1
- package/templates/chatgpt-sites/mcp.ts +0 -48
- package/templates/chatgpt-sites/public-mcp.ts +0 -29
- package/templates/chatgpt-sites/smoke-local.mjs +0 -20
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Helpers of generated input validation: the checks themselves are compiled per operation, no schema is read at request time. */
|
|
2
|
+
import { invalid } from "./surface.js";
|
|
3
|
+
/** INPUT_VALIDATION_FAILED at the JSON pointer `path`. */
|
|
4
|
+
export function fail(path, message) { return invalid(message, path); }
|
|
5
|
+
/** JSON equality for `enum`. */
|
|
6
|
+
export function equal(a, b) {
|
|
7
|
+
if (a === b)
|
|
8
|
+
return true;
|
|
9
|
+
if (!a || !b || typeof a !== "object" || typeof b !== "object" || Array.isArray(a) !== Array.isArray(b))
|
|
10
|
+
return false;
|
|
11
|
+
const left = a, right = b, keys = Object.keys(left);
|
|
12
|
+
return keys.length === Object.keys(right).length && keys.every(key => Object.hasOwn(right, key) && equal(left[key], right[key]));
|
|
13
|
+
}
|
|
14
|
+
const pointer = (key) => key.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
15
|
+
const tests = { string: (value) => typeof value === "string", number: Number.isFinite, integer: Number.isSafeInteger, boolean: (value) => typeof value === "boolean", object: (value) => typeof value === "object" && value !== null && !Array.isArray(value) };
|
|
16
|
+
const is = (value, path, type, nullable) => { if (!(tests[type](value) || nullable && value === null))
|
|
17
|
+
fail(path, "Expected " + type + (nullable ? " or null." : ".")); };
|
|
18
|
+
/** The check of the publishing transitions' input `{id, expectedVersion}` at `path`. */
|
|
19
|
+
export function transition(path, raw) {
|
|
20
|
+
const keys = ["id", "expectedVersion"];
|
|
21
|
+
is(raw, path, "object");
|
|
22
|
+
const object = raw;
|
|
23
|
+
for (const key of keys)
|
|
24
|
+
if (!Object.hasOwn(object, key))
|
|
25
|
+
fail(path + "/" + key, "Required field is missing.");
|
|
26
|
+
is(object.id, path + "/id", "string");
|
|
27
|
+
if (object.id === "")
|
|
28
|
+
fail(path + "/id", "Value violates minLength.");
|
|
29
|
+
is(object.expectedVersion, path + "/expectedVersion", "integer");
|
|
30
|
+
if (object.expectedVersion < 1)
|
|
31
|
+
fail(path + "/expectedVersion", "Value violates minimum.");
|
|
32
|
+
for (const key of Object.keys(object))
|
|
33
|
+
if (!keys.includes(key))
|
|
34
|
+
fail(path + "/" + pointer(key), "Unknown field.");
|
|
35
|
+
return object;
|
|
36
|
+
}
|
|
37
|
+
function number(raw, path, integer) { const value = Number(raw); if (raw.trim() === "" || !Number.isFinite(value) || integer && !Number.isSafeInteger(value))
|
|
38
|
+
fail(path, "Expected a valid number."); return value; }
|
|
39
|
+
function boolean(raw, path) { if (raw !== "true" && raw !== "false")
|
|
40
|
+
fail(path, "Expected true or false."); return raw === "true"; }
|
|
41
|
+
/**
|
|
42
|
+
* A View's query string: each declared field (typed `boolean`, `integer` or `number` when the input says so) is read
|
|
43
|
+
* from `params`, then validated; `limit` and `cursor` are the paging options. `operation` is `views/<name>`.
|
|
44
|
+
*/
|
|
45
|
+
export function query(params, validate, operation, fields) {
|
|
46
|
+
const input = {};
|
|
47
|
+
for (const [key, type] of fields) {
|
|
48
|
+
const raw = params.get(key), path = operation + "/input/" + pointer(key);
|
|
49
|
+
if (raw !== null)
|
|
50
|
+
Object.defineProperty(input, key, { value: type === "boolean" ? boolean(raw, path) : type ? number(raw, path, type === "integer") : raw, enumerable: true });
|
|
51
|
+
}
|
|
52
|
+
const limit = params.get("limit"), cursor = params.get("cursor");
|
|
53
|
+
return { input: validate(input), ...(limit === null ? {} : { limit: number(limit, operation + "/limit", true) }), ...(cursor ? { cursor } : {}) };
|
|
54
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Selected web authorization loads on a request, outside the ordinary web startup graph. */
|
|
2
|
+
import { web } from "./web.js";
|
|
3
|
+
import { gatewayCaller, hostFailure } from "./host-caller.js";
|
|
4
|
+
import { accessRequest } from "./host-access.js";
|
|
5
|
+
import { authorizeTarget } from "./authorization.js";
|
|
6
|
+
import { forwarded, ContractError } from "./contracts.js";
|
|
7
|
+
export function gatewayWeb(options, table) {
|
|
8
|
+
const run = async (method, request, env, ctx) => {
|
|
9
|
+
let caller, failure, ran = false;
|
|
10
|
+
const access = options.gateway ? accessRequest(ctx, options) : undefined;
|
|
11
|
+
try {
|
|
12
|
+
const pathname = new URL(request.url).pathname, path = method === "fetch" ? pathname.replace(/\.md$/, "") : pathname;
|
|
13
|
+
const page = Object.entries(options.pages).sort(([a], [b]) => (a.match(/\{[^{}]+\}/g)?.length ?? 0) - (b.match(/\{[^{}]+\}/g)?.length ?? 0)).find(([template]) => new RegExp("^" + template.split(/(\{[^{}]+\})/).map(part => part.startsWith("{") ? "[^/]+" : part.replace(/[.*+?^$()|[\]\\]/g, "\\$&")).join("") + "$").test(path))?.[1];
|
|
14
|
+
const index = pathname === "/llms.txt" || pathname === "/sitemap.xml";
|
|
15
|
+
if (method === "fetch" ? !["GET", "HEAD"].includes(request.method) || !index && (!pathname.endsWith(".md") || !page?.markdown) : !page)
|
|
16
|
+
return method === "head" ? "" : undefined;
|
|
17
|
+
const name = page?.view ?? Object.values(options.pages).find(p => p.list)?.list;
|
|
18
|
+
await access?.select(name ? `view:${name}` : "web");
|
|
19
|
+
caller = forwarded(ctx, options.gateway) ? gatewayCaller(ctx, options.gateway, options.fields) : typeof options.caller === "function" ? await options.caller(request, env, { readOnly: true }) : options.caller ?? { kind: "anonymous" };
|
|
20
|
+
const site = web({ ...options, gateway: undefined, authorize: undefined, onAccess: undefined, caller }, table.map(([name, view]) => [name, { ...view, page: async (...args) => {
|
|
21
|
+
try {
|
|
22
|
+
ran = true;
|
|
23
|
+
await access?.select(`view:${name}`, caller);
|
|
24
|
+
await access?.check(true);
|
|
25
|
+
view.check?.(caller);
|
|
26
|
+
if (options.authorize)
|
|
27
|
+
await authorizeTarget(options.authorize, { kind: "view", name }, caller);
|
|
28
|
+
if (access?.dryRun)
|
|
29
|
+
return { rows: [] };
|
|
30
|
+
return view.page(...args);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
failure = error;
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
} }]));
|
|
37
|
+
// The ordinary renderer validates query/path fields; wrapped pages decide before any SQL.
|
|
38
|
+
const result = await site[method](request, env, options.gateway ? undefined : ctx);
|
|
39
|
+
if (failure)
|
|
40
|
+
throw failure;
|
|
41
|
+
if (result === undefined)
|
|
42
|
+
return;
|
|
43
|
+
if (access && result instanceof Response && result.status >= 400) {
|
|
44
|
+
const payload = await result.clone().json().catch(() => null);
|
|
45
|
+
if (payload?.error)
|
|
46
|
+
throw new ContractError(payload.error.code, "", payload.error.path, { status: result.status, fields: payload.error.field ? { field: payload.error.field } : undefined });
|
|
47
|
+
}
|
|
48
|
+
if (!ran)
|
|
49
|
+
return result;
|
|
50
|
+
if (access?.dryRun) {
|
|
51
|
+
const response = access.response();
|
|
52
|
+
return method === "fetch" ? response : "";
|
|
53
|
+
}
|
|
54
|
+
access?.finish();
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
access?.finish(error);
|
|
59
|
+
if (method === "head")
|
|
60
|
+
throw error;
|
|
61
|
+
return hostFailure(error, caller, "web");
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
fetch: (request, env, ctx) => run("fetch", request, env, ctx),
|
|
66
|
+
head: (request, env, ctx) => run("head", request, env, ctx),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** HTML head formatting loads only when the application asks for head tags. */
|
|
2
|
+
const escape = (v) => String(v).replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
3
|
+
export function headTags(canonical, row, markdown, locales) {
|
|
4
|
+
let tags = '<link rel="canonical" href="' + escape(canonical) + '">';
|
|
5
|
+
if (markdown)
|
|
6
|
+
tags += '<link rel="alternate" type="text/markdown" href="' + escape(canonical + ".md") + '">';
|
|
7
|
+
if (locales?.[0])
|
|
8
|
+
tags += '<link rel="alternate" hreflang="' + escape(locales[0]) + '" href="' + escape(canonical) + '">';
|
|
9
|
+
for (const key of Object.keys(row))
|
|
10
|
+
if (key.endsWith("_lat") && typeof row[key] === "number" && typeof row[key.slice(0, -4) + "_lng"] === "number") {
|
|
11
|
+
tags += '<meta name="geo.position" content="' + escape(row[key]) + ";" + escape(row[key.slice(0, -4) + "_lng"]) + '">';
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
return tags;
|
|
15
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** /llms.txt and /sitemap.xml bodies; loaded only when an index is requested, so page Workers do not pay for it at startup. */
|
|
2
|
+
const escape = (v) => String(v).replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
3
|
+
const label = (v) => String(v).replace(/[\r\n]/g, " ").replace(/[\\[\]]/g, "\\$&");
|
|
4
|
+
const size = (s) => new TextEncoder().encode(s).length;
|
|
5
|
+
export async function webIndex(sitemap, options, pages, list, validUrl) {
|
|
6
|
+
const cap = sitemap ? 50000 : 5000, limit = sitemap ? 50 * 1024 * 1024 : 1024 * 1024;
|
|
7
|
+
const start = sitemap ? '<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' : "# " + label(options.title) + "\n\n> " + label(options.description) + "\n\n## Pages\n\n", end = sitemap ? "</urlset>" : "\n";
|
|
8
|
+
const links = [];
|
|
9
|
+
let visited = 0, requests = 0, bytes = size(start + end);
|
|
10
|
+
if (bytes > limit)
|
|
11
|
+
throw new TypeError("Web index header exceeds its byte limit.");
|
|
12
|
+
pagesLoop: for (const entry of pages) {
|
|
13
|
+
if (!entry.page.list)
|
|
14
|
+
continue;
|
|
15
|
+
let cursor;
|
|
16
|
+
do {
|
|
17
|
+
if (requests >= (sitemap ? 100 : 10))
|
|
18
|
+
break pagesLoop;
|
|
19
|
+
requests++;
|
|
20
|
+
const page = await list(entry.page.list, cursor);
|
|
21
|
+
for (const row of page.rows) {
|
|
22
|
+
if (visited++ >= cap)
|
|
23
|
+
break pagesLoop;
|
|
24
|
+
const url = validUrl(entry.path, row, entry.page.view);
|
|
25
|
+
if (!url)
|
|
26
|
+
continue;
|
|
27
|
+
const line = sitemap ? "<url><loc>" + escape(url) + "</loc></url>" : "- [" + label(row.title ?? url) + "](" + url + "): " + label(row.description ?? "") + "\n";
|
|
28
|
+
const length = size(line);
|
|
29
|
+
if (bytes + length > limit)
|
|
30
|
+
break pagesLoop;
|
|
31
|
+
bytes += length;
|
|
32
|
+
links.push(line);
|
|
33
|
+
}
|
|
34
|
+
if (visited >= cap)
|
|
35
|
+
break pagesLoop;
|
|
36
|
+
cursor = page.nextCursor;
|
|
37
|
+
} while (cursor);
|
|
38
|
+
}
|
|
39
|
+
return start + links.join("") + end;
|
|
40
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Response hashing is needed only when a Web route actually renders. */
|
|
2
|
+
export async function webResponse(request, body, type, cacheControl) {
|
|
3
|
+
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(body));
|
|
4
|
+
const etag = '"' + Array.from(new Uint8Array(hash), b => b.toString(16).padStart(2, '0')).join('') + '"';
|
|
5
|
+
const headers = { 'content-type': type + '; charset=utf-8', 'cache-control': cacheControl, etag };
|
|
6
|
+
const cached = request.headers.get('if-none-match')?.split(',').some(tag => tag.trim() === '*' || tag.trim().replace(/^W\//, '') === etag);
|
|
7
|
+
return new Response(cached || request.method === 'HEAD' ? null : body, { status: cached ? 304 : 200, headers });
|
|
8
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** Web output for a generated public View table: /llms.txt, /sitemap.xml, Markdown pages and head tags. SQL owns visibility. */
|
|
2
|
+
import { type Caller, type Gateway, type RequestContext } from "./contracts.js";
|
|
3
|
+
import type { UserFields } from "./user-fields.js";
|
|
4
|
+
import { type Conversions } from "./surface.js";
|
|
5
|
+
/** An app's public Views: row type and input keys per View, and the Views a list may name (input that accepts no fields). */
|
|
6
|
+
export interface App {
|
|
7
|
+
Rows: Record<string, any>;
|
|
8
|
+
Inputs: Record<string, string>;
|
|
9
|
+
List: string;
|
|
10
|
+
}
|
|
11
|
+
type At<T, K> = K extends keyof T ? T[K] : never;
|
|
12
|
+
type Params<S extends string> = S extends `${string}{${infer P}}${infer Rest}` ? P | Params<Rest> : never;
|
|
13
|
+
/** A page: the View that reads one row, an optional list View enumerating its URLs, and an optional Markdown text field. */
|
|
14
|
+
export type Page<A extends App> = {
|
|
15
|
+
[V in keyof A["Rows"] & string]: {
|
|
16
|
+
view: V;
|
|
17
|
+
list?: A["List"];
|
|
18
|
+
markdown?: {
|
|
19
|
+
[K in keyof A["Rows"][V]]: A["Rows"][V][K] extends string | null ? K : never;
|
|
20
|
+
}[keyof A["Rows"][V]] & string;
|
|
21
|
+
};
|
|
22
|
+
}[keyof A["Rows"] & string];
|
|
23
|
+
type ListKeys<A extends App, T> = T extends {
|
|
24
|
+
list: infer L extends string;
|
|
25
|
+
} ? keyof At<A["Rows"], L> : string;
|
|
26
|
+
/** `{name}` parameters must equal the View input, and be columns of the list and of the View. */
|
|
27
|
+
export type Pages<A extends App, P extends Record<string, Page<A>>> = {
|
|
28
|
+
[K in keyof P]: K extends string ? [Params<K>] extends [At<A["Inputs"], P[K]["view"]>] ? [At<A["Inputs"], P[K]["view"]>] extends [Params<K>] ? [Params<K>] extends [ListKeys<A, P[K]>] ? [Params<K>] extends [keyof At<A["Rows"], P[K]["view"]>] ? P[K] : never : never : never : never : never;
|
|
29
|
+
};
|
|
30
|
+
export type Options<A extends App, Database, P extends Record<string, Page<A>>, E> = Omit<WebOptions<Database, E>, "pages"> & {
|
|
31
|
+
pages: P & Pages<A, P>;
|
|
32
|
+
};
|
|
33
|
+
/** One public View: its query-string reader, paged boundary, bind conversions and boolean input keys. */
|
|
34
|
+
export interface View<Database> {
|
|
35
|
+
readonly check?: (caller: Caller) => void;
|
|
36
|
+
/** The input rejects an empty query, so a list cannot name this View. */
|
|
37
|
+
readonly unlisted?: true;
|
|
38
|
+
readonly query: (params: URLSearchParams) => {
|
|
39
|
+
input: Record<string, unknown>;
|
|
40
|
+
};
|
|
41
|
+
readonly page: (db: Database, caller: Caller, input: never, paging: {
|
|
42
|
+
limit: number;
|
|
43
|
+
cursor?: string;
|
|
44
|
+
}) => Promise<{
|
|
45
|
+
rows: Record<string, unknown>[];
|
|
46
|
+
nextCursor?: string;
|
|
47
|
+
}>;
|
|
48
|
+
readonly convert?: Conversions;
|
|
49
|
+
readonly booleans?: readonly string[];
|
|
50
|
+
}
|
|
51
|
+
export interface WebHandler<E> {
|
|
52
|
+
fetch(request: Request, env?: E, ctx?: RequestContext): Promise<Response | undefined>;
|
|
53
|
+
head(request: Request, env?: E, ctx?: RequestContext): Promise<string>;
|
|
54
|
+
}
|
|
55
|
+
export type WebOptions<Database, E> = {
|
|
56
|
+
db: Database;
|
|
57
|
+
caller?: Caller | ((request: Request, env: E, authority: {
|
|
58
|
+
readonly readOnly: boolean;
|
|
59
|
+
}) => Caller | Promise<Caller>);
|
|
60
|
+
origin: string;
|
|
61
|
+
title: string;
|
|
62
|
+
description: string;
|
|
63
|
+
locales?: readonly string[];
|
|
64
|
+
pages: Record<string, {
|
|
65
|
+
view: string;
|
|
66
|
+
list?: string;
|
|
67
|
+
markdown?: string;
|
|
68
|
+
}>;
|
|
69
|
+
cacheControl?: string;
|
|
70
|
+
gateway?: Gateway;
|
|
71
|
+
fields?: UserFields;
|
|
72
|
+
authorize?: import("./contracts.js").Authorize;
|
|
73
|
+
onAccess?: import("./contracts.js").GatewayOptions["onAccess"];
|
|
74
|
+
accessTargets?: import("./contracts.js").GatewayOptions["accessTargets"];
|
|
75
|
+
};
|
|
76
|
+
export declare function web<Database, E>(options: WebOptions<Database, E>, table: readonly (readonly [string, View<Database>])[]): WebHandler<E>;
|
|
77
|
+
export {};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/** Web output for a generated public View table: /llms.txt, /sitemap.xml, Markdown pages and head tags. SQL owns visibility. */
|
|
2
|
+
import { forwarded, wire, ContractError } from "./contracts.js";
|
|
3
|
+
import { convert, decode } from "./surface.js";
|
|
4
|
+
const NO_STORE = { "cache-control": "no-store" };
|
|
5
|
+
const notFound = () => new Response(null, { status: 404, headers: NO_STORE });
|
|
6
|
+
export function web(options, table) {
|
|
7
|
+
const views = new Map(table);
|
|
8
|
+
const query = (name, db, caller, params, cursor) => {
|
|
9
|
+
const view = views.get(name), input = { ...view.query(params).input };
|
|
10
|
+
convert(input, view.convert);
|
|
11
|
+
return view.page(db, caller, input, { limit: 500, cursor });
|
|
12
|
+
};
|
|
13
|
+
for (const [path, page] of Object.entries(options.pages))
|
|
14
|
+
if (page.list && views.get(page.list)?.unlisted)
|
|
15
|
+
throw new TypeError(`Page ${path} lists View '${page.list}', whose input rejects an empty query; a list View must accept no input.`);
|
|
16
|
+
const origin = new URL(options.origin);
|
|
17
|
+
if (!/^https?:$/.test(origin.protocol) || origin.username || origin.password || origin.pathname !== "/" || origin.search || origin.hash)
|
|
18
|
+
throw new TypeError("origin must be an HTTP(S) origin.");
|
|
19
|
+
const pages = Object.entries(options.pages).map(([path, page]) => {
|
|
20
|
+
if (!path.startsWith("/") || /[?#]/.test(path))
|
|
21
|
+
throw new TypeError("Page templates must be absolute paths without queries or fragments.");
|
|
22
|
+
const keys = [];
|
|
23
|
+
const pattern = path.split(/(\{[^{}]+\})/).map(part => part.startsWith("{") ? (keys.push(part.slice(1, -1)), "([^/]+)") : part.replace(/[.*+?^$()|[\]\\]/g, "\\$&")).join("");
|
|
24
|
+
return { path, page, keys, pattern: new RegExp("^" + pattern + "$") };
|
|
25
|
+
}).sort((a, b) => a.keys.length - b.keys.length);
|
|
26
|
+
const context = async (request, env, ctx) => ({ db: options.db, caller: forwarded(ctx, options.gateway)
|
|
27
|
+
? (await import("./host-caller.js")).gatewayCaller(ctx, options.gateway, options.fields)
|
|
28
|
+
: typeof options.caller === "function" ? await options.caller(request, env, { readOnly: ["GET", "HEAD"].includes(request.method) }) : options.caller ?? { kind: "anonymous" } });
|
|
29
|
+
const urlFor = (path, row, view) => origin.origin + path.replace(/\{([^{}]+)\}/g, (_, key) => {
|
|
30
|
+
let value = row[key];
|
|
31
|
+
if (views.get(view).booleans?.includes(key) && (value === 0 || value === 1))
|
|
32
|
+
value = Boolean(value);
|
|
33
|
+
if (value === null || value === undefined)
|
|
34
|
+
throw new TypeError("List View must return every page parameter: " + key);
|
|
35
|
+
const segment = String(value);
|
|
36
|
+
if (segment === "." || segment === "..")
|
|
37
|
+
return "";
|
|
38
|
+
return encodeURIComponent(segment).replace(/[()]/g, c => "%" + c.charCodeAt(0).toString(16).toUpperCase());
|
|
39
|
+
});
|
|
40
|
+
const validUrl = (path, row, view) => { if ([...path.matchAll(/\{([^{}]+)\}/g)].some(([, key]) => row[key] === "." || row[key] === ".."))
|
|
41
|
+
return; return urlFor(path, row, view); };
|
|
42
|
+
const match = (path) => {
|
|
43
|
+
for (const entry of pages) {
|
|
44
|
+
const hit = entry.pattern.exec(path);
|
|
45
|
+
if (!hit)
|
|
46
|
+
continue;
|
|
47
|
+
return { ...entry, params: new URLSearchParams(decode(entry.keys, hit, "Malformed URL parameter.", "web/path")) };
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const read = async (entry, ctx) => (await query(entry.page.view, ctx.db, ctx.caller, entry.params)).rows[0];
|
|
51
|
+
const response = async (request, body, type, caller) => {
|
|
52
|
+
const { webResponse } = await import('./web-response.js');
|
|
53
|
+
return webResponse(request, body, type, caller.kind === "anonymous" ? (options.cacheControl ?? "public, max-age=60") : "private, no-store");
|
|
54
|
+
};
|
|
55
|
+
if (options.gateway || options.authorize) {
|
|
56
|
+
let handle;
|
|
57
|
+
const load = async () => handle ??= (await import("./web-gateway.js")).gatewayWeb(options, table);
|
|
58
|
+
return {
|
|
59
|
+
fetch: async (request, env, ctx) => (await load()).fetch(request, env, ctx),
|
|
60
|
+
head: async (request, env, ctx) => (await load()).head(request, env, ctx),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
async fetch(request, env, execution) {
|
|
65
|
+
let caller;
|
|
66
|
+
try {
|
|
67
|
+
if (request.method !== "GET" && request.method !== "HEAD")
|
|
68
|
+
return;
|
|
69
|
+
const path = new URL(request.url).pathname, index = path === "/llms.txt" || path === "/sitemap.xml";
|
|
70
|
+
const entry = !index && path.endsWith(".md") ? match(path.slice(0, -3)) : undefined, markdown = entry?.page.markdown;
|
|
71
|
+
if (!index && !markdown)
|
|
72
|
+
return;
|
|
73
|
+
const ctx = await context(request, env, execution);
|
|
74
|
+
caller = ctx.caller;
|
|
75
|
+
if (index) {
|
|
76
|
+
const sitemap = path === "/sitemap.xml";
|
|
77
|
+
const body = await (await import("./web-index.js")).webIndex(sitemap, options, pages, (list, cursor) => query(list, ctx.db, ctx.caller, new URLSearchParams(), cursor), validUrl);
|
|
78
|
+
return response(request, body, sitemap ? "application/xml" : "text/plain", ctx.caller);
|
|
79
|
+
}
|
|
80
|
+
const exact = match(path);
|
|
81
|
+
if (exact)
|
|
82
|
+
try {
|
|
83
|
+
if (await read(exact, ctx))
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (wire(error).status !== 400)
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
const row = await read(entry, ctx);
|
|
91
|
+
if (!row)
|
|
92
|
+
return notFound();
|
|
93
|
+
const body = row[markdown];
|
|
94
|
+
if (body == null)
|
|
95
|
+
return notFound();
|
|
96
|
+
if (typeof body !== "string")
|
|
97
|
+
throw new TypeError("Markdown field must return text.");
|
|
98
|
+
return response(request, body, "text/markdown", ctx.caller);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
return (await import("./host-caller.js")).hostFailure(error, caller, "web");
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
async head(request, env, execution) {
|
|
105
|
+
try {
|
|
106
|
+
const path = new URL(request.url).pathname, entry = match(path);
|
|
107
|
+
if (!entry)
|
|
108
|
+
return "";
|
|
109
|
+
const ctx = await context(request, env, execution), row = await read(entry, ctx);
|
|
110
|
+
if (!row)
|
|
111
|
+
return "";
|
|
112
|
+
const canonical = validUrl(entry.path, row, entry.page.view);
|
|
113
|
+
if (!canonical)
|
|
114
|
+
return "";
|
|
115
|
+
return (await import("./web-head.js")).headTags(canonical, row, !!entry.page.markdown, options.locales);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error instanceof ContractError && error.code === "HOST_CONTRACT")
|
|
119
|
+
throw error;
|
|
120
|
+
return "";
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const COMPARE = { gt: ">", gte: ">=", lt: "<", lte: "<=" };
|
|
2
|
+
/**
|
|
3
|
+
* `column` is already quoted. `bind` registers one parameter and returns its placeholder. `insensitive` folds both sides of
|
|
4
|
+
* eq/ne/in/notIn with LOWER (text only) and picks the dialect's case-insensitive LIKE. A null eq/ne is IS [NOT] NULL.
|
|
5
|
+
* `like` takes a finished pattern (`escapeLike` the literal parts).
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export function condition(d, column, operator, value, bind, insensitive = false) {
|
|
9
|
+
const lhs = insensitive ? `LOWER(${column})` : column;
|
|
10
|
+
const rhs = (v) => insensitive ? `LOWER(${bind(v)})` : bind(v);
|
|
11
|
+
switch (operator) {
|
|
12
|
+
case "in":
|
|
13
|
+
case "notIn": {
|
|
14
|
+
const list = Array.isArray(value) ? value : [value];
|
|
15
|
+
if (!list.length)
|
|
16
|
+
return operator === "in" ? "1 = 0" : "1 = 1";
|
|
17
|
+
return `${lhs} ${operator === "in" ? "IN" : "NOT IN"} (${list.map(rhs).join(", ")})`;
|
|
18
|
+
}
|
|
19
|
+
case "like": return `${column} ${d.like(insensitive)} ${bind(value)}${d.likeEscape}`;
|
|
20
|
+
case "eq":
|
|
21
|
+
case "ne":
|
|
22
|
+
if (value === null)
|
|
23
|
+
return `${column} IS ${operator === "eq" ? "" : "NOT "}NULL`;
|
|
24
|
+
return `${lhs} ${operator === "eq" ? "=" : "<>"} ${rhs(value)}`;
|
|
25
|
+
default: return `${column} ${COMPARE[operator]} ${bind(value)}`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { userCaller as authenticatedAs } from "../kernel/user-caller.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { userCaller as authenticatedAs } from "../kernel/user-caller.js";
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { userReferences } from "../domain/userReferences.js";
|
|
2
|
+
/** Selected application bindings: compiled checks/native SQL; only the app depends on Admin adapters. */
|
|
3
|
+
import { isPrimaryCollection } from "../build/compiler.js";
|
|
4
|
+
import { enumOptions } from "../build/compiler.js";
|
|
5
|
+
import { databaseImport, isSqlite, literal as q } from "../domain/entry.js";
|
|
6
|
+
import { conversions, authLines } from "../domain/input.js";
|
|
7
|
+
import { recipesOf } from "../domain/recipes.js";
|
|
8
|
+
import { boundaryType, conversion, serverOwned } from "../domain/columns.js";
|
|
9
|
+
import { adminOrder } from "../domain/adminOrder.js";
|
|
10
|
+
import { emitValidation } from "./validation.js";
|
|
11
|
+
const scalar = (type) => ["text", "integer", "int4", "int8", "real", "float8", "bool", "date", "timestamptz"].includes(type ?? "");
|
|
12
|
+
const wire = (fields, sqlite) => {
|
|
13
|
+
const columns = fields.flatMap(([name, type]) => conversion(name, type, sqlite));
|
|
14
|
+
const geo = fields.filter(([, type]) => type === "geo").map(([field]) => `row[${q(field)}]=row[${q(field + "_lat")}] == null || row[${q(field + "_lng")}] == null ? null : {lat:row[${q(field + "_lat")}],lng:row[${q(field + "_lng")}]};delete row[${q(field + "_lat")}];delete row[${q(field + "_lng")}];`);
|
|
15
|
+
return `(raw:Row):Row=>{const row=${columns.length ? `wireRows([raw],${q(columns)})[0]!` : geo.length ? "{...raw}" : "raw"};${geo.join("")}return row;}`;
|
|
16
|
+
};
|
|
17
|
+
/** Core's titled string choices are enum checks; constrained or general oneOf remains refused. */
|
|
18
|
+
function adminProperty(property) {
|
|
19
|
+
const choices = enumOptions(property);
|
|
20
|
+
if (!property.oneOf || property.enum || !choices || !property.oneOf.every(branch => Object.entries(branch).every(([key, value]) => key === "const" || key === "title" || key === "type" && value === "string")) || new Set(choices.map(c => c.value)).size !== choices.length)
|
|
21
|
+
return property;
|
|
22
|
+
const { oneOf, ...base } = property;
|
|
23
|
+
return { ...base, enum: choices.map(c => c.value) };
|
|
24
|
+
}
|
|
25
|
+
function checks(s, full) {
|
|
26
|
+
const properties = Object.fromEntries(Object.entries(s.schema.properties ?? {}).filter(([name]) => name.toLowerCase() !== s.scope && s.fields[name.toLowerCase()] !== "geo").map(([name, property]) => [name, adminProperty(property)]));
|
|
27
|
+
return { ...s.schema, properties, required: full ? (s.schema.required ?? []).filter(name => Object.hasOwn(properties, name) && !(s.userScopes ?? []).some(u => u.column === name.toLowerCase())) : [], additionalProperties: false };
|
|
28
|
+
}
|
|
29
|
+
const allowed = (requires, path) => {
|
|
30
|
+
const lines = authLines(requires, path);
|
|
31
|
+
return lines.length ? `(caller:Caller)=>{try{${lines.join("")}return true;}catch{return false;}}` : "(_caller:Caller)=>true";
|
|
32
|
+
};
|
|
33
|
+
/** Share repeated compiler SQL constants; generated modules evaluate no decoder or kernel at import. */
|
|
34
|
+
function sqlConstants(value) {
|
|
35
|
+
const counts = new Map();
|
|
36
|
+
const visit = (v) => { if (typeof v === "string" && v.length > 80)
|
|
37
|
+
counts.set(v, (counts.get(v) ?? 0) + 1);
|
|
38
|
+
else if (Array.isArray(v))
|
|
39
|
+
v.forEach(visit);
|
|
40
|
+
else if (v && typeof v === "object")
|
|
41
|
+
Object.values(v).forEach(visit); };
|
|
42
|
+
visit(value);
|
|
43
|
+
const pool = new Map([...counts].filter(([, count]) => count > 1).map(([text], n) => [text, `sql${n}`]));
|
|
44
|
+
const literal = (v) => typeof v === "string" ? pool.get(v) ?? q(v) : Array.isArray(v) ? "[" + v.map(literal).join(",") + "]" : v && typeof v === "object" ? "{" + Object.entries(v).map(([name, child]) => `${name === "__proto__" ? `[${q(name)}]` : q(name)}:${literal(child)}`).join(",") + "}" : q(v);
|
|
45
|
+
return { code: literal(value), constants: [...pool].map(([text, name]) => `const ${name}=${q(text)};`) };
|
|
46
|
+
}
|
|
47
|
+
export function emitAdminBindings(input, refuse) {
|
|
48
|
+
if (!input.admin)
|
|
49
|
+
return {};
|
|
50
|
+
const { plan, lowered, target } = input, sqlite = isSqlite(target), refs = Object.values(plan.procedures).some(p => "ref" in p.handler), files = {};
|
|
51
|
+
const user = userReferences(plan, sqlite);
|
|
52
|
+
const imports = [...(user.used ? [user.code, 'import {userBinds} from "@aotter/mantle/user-fields";'] : []), databaseImport(target), 'import {wireRows,type Caller,type Row} from "@aotter/mantle/contracts";', 'import {deny,isAdmin,isUser} from "@aotter/mantle/contracts";', 'import {invalid} from "@aotter/mantle/surface";', 'import type {Handlers} from "./generated.js";', 'import {bootstrap,allCollections} from "./admin-descriptors.js";', 'import type {View,Operation} from "@aotter/mantle-admin/server";', 'import type {EntrySchema} from "@aotter/mantle-admin/server/entries";'];
|
|
53
|
+
imports.push('import {nativePage,nativeStatistics,nativeText} from "@aotter/mantle-admin/server/native";', `import * as D from "@aotter/mantle/dialect/${sqlite ? "sqlite" : "postgres"}";`, `import {read,procedure} from "@aotter/mantle/driver/${target === "bun-sqlite" ? "bun-sqlite" : target === "cloudflare-d1" ? "d1" : "postgres"}";`, `const adapter={read,procedure,values:${sqlite ? (user.used ? "{...D,...USER_VALUES}" : "D") : `{...D,${user.used ? "...USER_VALUES," : ""}timestamp:(v:Parameters<typeof D.timestamp>[0])=>{if(typeof v==="string"&&/^0000-/.test(v.trim()))invalid("PostgreSQL timestamps require a nonzero year.","admin/input");return D.timestamp(v);},date:(v:string)=>{if(/^0000-/.test(v.trim()))invalid("PostgreSQL dates require a nonzero year.","admin/input");return D.date(v);}}`},param:D.param,escapeLike:D.escapeLike,sqlite:${sqlite}} as const;`);
|
|
54
|
+
const schemas = [], views = [], adminViews = [], operations = [], pageSpecs = [];
|
|
55
|
+
for (const [n, s] of Object.values(plan.schemas).entries()) {
|
|
56
|
+
const schemaPath = `schemas/${s.name}/admin`, declared = Object.keys(s.fields).filter(f => f !== s.scope), writable = declared.filter(f => scalar(s.fields[f]) || s.fields[f] === "json");
|
|
57
|
+
// readOnly properties are server-maintained: values() pins them to their default, and they have no Admin UPDATE.
|
|
58
|
+
const server = Object.fromEntries(writable.filter(f => serverOwned(s.schema, f)).map(f => [f, Object.entries(s.schema.properties ?? {}).find(([k]) => k.toLowerCase() === f)?.[1]?.default ?? null]));
|
|
59
|
+
// Experimental geo collections remain readable; edits require a declared Procedure.
|
|
60
|
+
const validators = emitValidation({ views: {}, procedures: { full: { input: checks(s, true) }, partial: { input: checks(s, false) } } }, (path, message) => refuse(schemaPath + "/" + path, message));
|
|
61
|
+
for (const mode of ["full", "partial"]) {
|
|
62
|
+
const name = `validation/admin/${encodeURIComponent(s.name)}-${mode}.ts`;
|
|
63
|
+
files[name] = validators[`validation/procedures/${mode}.ts`].replaceAll(`procedures/${mode}/input`, schemaPath + "/input");
|
|
64
|
+
imports.push(`import {validate as ${mode}${n}} from "./${name.replace(/\.ts$/, ".js")}";`);
|
|
65
|
+
}
|
|
66
|
+
const names = Object.fromEntries(writable.map(f => [s.names[f] ?? f, f])), allNames = { ...names, id: "id", version: "version", createdAt: "created_at", updatedAt: "updated_at", authorId: "author_id", ...(s.publishing ? { status: "status", publishedAt: "published_at" } : {}) };
|
|
67
|
+
const temporal = writable.filter(f => ["date", "timestamptz"].includes(s.fields[f])).map(f => s.names[f] ?? f);
|
|
68
|
+
const yearZero = sqlite ? "" : `for(const key of ${q(temporal)})if(typeof data[key]==="string"&&/^0000-/.test(data[key].trim()))invalid("PostgreSQL dates require a nonzero year.",${q(schemaPath)});`;
|
|
69
|
+
const coercers = Object.entries(allNames).filter(([, f]) => scalar(s.fields[f] ?? (f === "version" ? "integer" : "text"))).map(([name, f]) => {
|
|
70
|
+
const type = s.fields[f] ?? (f === "version" ? "integer" : "text");
|
|
71
|
+
const value = type === "bool" ? `raw === "true" ? true : raw === "false" ? false : fail("Expected true or false.")` : ["integer", "int4", "int8", "real", "float8"].includes(type) ? `number(raw,${["integer", "int4", "int8"].includes(type)})` : `raw`;
|
|
72
|
+
return `case ${q(name)}:return {column:${q(f)},value:${value}};`;
|
|
73
|
+
});
|
|
74
|
+
const order = adminOrder(s), orderNames = Object.fromEntries(Object.keys(order).map(f => [f === "updated_at" ? "updatedAt" : s.names[f] ?? f, f]));
|
|
75
|
+
const parent = s.translates && plan.schemas[s.translates.parent.toLowerCase()];
|
|
76
|
+
const inherited = parent ? Object.keys(parent.fields).filter(f => f !== parent.scope && !Object.hasOwn(s.fields, f)) : [];
|
|
77
|
+
const rowNames = { ...(parent ? parent.names : {}), ...s.names };
|
|
78
|
+
const rowFields = [...declared.map(f => [f, s.fields[f]]), ...inherited.map(f => [f, parent && parent.fields[f]]), ...["version"].map(f => [f, "integer"])];
|
|
79
|
+
const named = `(raw:Row):Row=>{const row=row${n}(raw);return Object.fromEntries(Object.entries(row).map(([key,value])=>[Object.hasOwn(fieldNames${n},key)?fieldNames${n}[key as keyof typeof fieldNames${n}]:key,value]));}`;
|
|
80
|
+
const entrySpec = `entrySpecs[${q(s.name)}]`;
|
|
81
|
+
if (!input.adminEntries?.[s.name]) {
|
|
82
|
+
refuse(schemaPath, "Missing compiler-authorized entry queries and Procedures.");
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const defaults = Object.fromEntries(Object.entries(checks(s, true).properties ?? {}).flatMap(([key, p]) => p.default !== undefined ? [[key, p.default]] : []));
|
|
86
|
+
const primary = isPrimaryCollection(s, Object.values(plan.schemas));
|
|
87
|
+
const schemaCode = `{
|
|
88
|
+
name:${q(s.name)},primary:${primary},descriptor:allCollections[${q(s.name)}],publishing:${!!s.publishing},readOnly:${s.schema.readOnly === true || declared.some(f => s.fields[f] === "geo")},fields:names${n},defaults:${q(defaults)},
|
|
89
|
+
binding:${entrySpec},
|
|
90
|
+
query:query${n},validate:(data,full)=>{const checked=Object.fromEntries(Object.entries(data).filter(([key,v])=>v!==null||${q(s.schema.required ?? [])}.includes(key)));(full?full${n}:partial${n})(checked);${sqlite ? "" : "nativeText(data);"}${yearZero}return data;},
|
|
91
|
+
values:values${n},row:${named},
|
|
92
|
+
${sqlite ? "" : "native:nativeText,"}
|
|
93
|
+
${s.publishing ? `transition:async(db,caller,verb,id,expectedVersion)=>{const run=await ({publish:()=>import("./procedures/${encodeURIComponent(s.name + ".publish")}.js"),unpublish:()=>import("./procedures/${encodeURIComponent(s.name + ".unpublish")}.js"),archive:()=>import("./procedures/${encodeURIComponent(s.name + ".archive")}.js")})[verb]();const version=wireRows([{version:expectedVersion}],[["version","int"]])[0]!.version;return run.default(db,caller,{id,expectedVersion:version} as Parameters<typeof run.default>[2]);},` : ""}
|
|
94
|
+
}`;
|
|
95
|
+
schemas.push(schemaCode);
|
|
96
|
+
files[`admin/entries-${encodeURIComponent(s.name)}.ts`] = `import {invalid} from "@aotter/mantle/surface";\nimport {wireRows,type Row} from "@aotter/mantle/contracts";\nimport type {EntryPageQuery} from "@aotter/mantle-admin/server/entries";\nimport {entriesQuery,scalarNumber,nativeText} from "@aotter/mantle-admin/server/native";\nconst fail=(message:string):never=>invalid(message,${q(schemaPath)});\nconst number=(raw:string,integer:boolean)=>scalarNumber(raw,integer,${q(schemaPath)});\nimport * as D from "@aotter/mantle/dialect/${sqlite ? "sqlite" : "postgres"}";\nexport const names=${q(names)} as const;\nfunction encode(column:string,value:unknown):unknown{if(value==null)return value;switch(column){${[...writable, ...["created_at", "updated_at", ...(s.publishing ? ["published_at"] : [])]].map(f => { const type = s.fields[f] ?? "timestamptz"; return type === "date" || type === "timestamptz" ? `case ${q(f)}:return D.${type === "date" ? "date" : "timestamp"}(value as string);` : type === "json" ? `case ${q(f)}:return JSON.stringify(value);` : sqlite && type === "bool" ? `case ${q(f)}:return Number(value);` : ""; }).join("")}default:return value;}}\nconst server:Row=${q(server)};\n// Server-maintained (readOnly) fields: create stores their default, an edit drops them (the console echoes whole records).\nexport const values=(data:Row,patch=false):Row=>Object.fromEntries(Object.entries(data).flatMap(([key,value])=>{const column=names[key as keyof typeof names];return Object.hasOwn(server,column)?patch?[]:[[column,encode(column,server[column])]]:[[column,encode(column,value)]];}));\nexport const row=${wire(rowFields, sqlite)};\nfunction coerce(name:string,raw:string):{column:string;value:string|number|boolean}{${sqlite ? "" : "nativeText(raw);" + (temporal.length ? "if(" + q(temporal) + ".includes(name)&&/^0000-/.test(raw.trim()))fail(\"PostgreSQL dates require a nonzero year.\");" : "")}switch(name){${coercers.join("")}default:return fail("Unknown scalar filter field.");}}\nimport {validate as checkFilter} from "../validation/admin/${encodeURIComponent(s.name)}-partial.js";\nfunction field(name:string,raw:string){const f=coerce(name,raw);if(Object.hasOwn(names,name))checkFilter({[name]:f.value});return {...f,value:encode(f.column,f.value) as string|number|boolean};}\nexport const query=(params:URLSearchParams):EntryPageQuery|null=>entriesQuery(params,{path:${q(schemaPath)},orderFields:${q(orderNames)},publishing:${!!s.publishing},sqlite:${sqlite},field});\n`;
|
|
97
|
+
imports.push(`const fieldNames${n}=${q(rowNames)} as const;`);
|
|
98
|
+
imports.push(`import {names as names${n},row as row${n},query as query${n},values as values${n}} from "./admin/entries-${encodeURIComponent(s.name)}.js";`);
|
|
99
|
+
}
|
|
100
|
+
for (const [name, v] of Object.entries(plan.views).filter(([, v]) => v.surface !== "internal")) {
|
|
101
|
+
const n = views.length, encoded = encodeURIComponent(name), converted = conversions(v.input?.properties, (lowered.views[name]?.public ?? lowered.views[name]?.caller)?.binds ?? [], target);
|
|
102
|
+
imports.push(`import {query as queryView${n}} from "./validation/views/${encoded}.js";`);
|
|
103
|
+
const rename = Object.fromEntries(Object.entries(v.columns ?? {}).map(([key, col]) => [key, key === col.field ? plan.schemas[col.schema].names[col.field] ?? key : key]));
|
|
104
|
+
imports.push(`const viewNames${n}=${q(rename)} as const;`);
|
|
105
|
+
const pageData = input.adminPages?.[name];
|
|
106
|
+
// A supplied page map omits only Views its producer already refused (at `views/<name>` or `views/<name>/admin`).
|
|
107
|
+
if (!pageData) {
|
|
108
|
+
if (!input.adminPages)
|
|
109
|
+
refuse(`views/${name}/admin`, "Admin list was not generated: no compiler-authorized matching pages were supplied for this View.");
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const kinds = (v.uiSchema?.list ?? {});
|
|
113
|
+
const filters = Object.fromEntries((kinds.filterFields ?? []).map(field => {
|
|
114
|
+
const lineage = v.columns?.[field] ?? v.columns?.[field.toLowerCase()], schema = lineage && plan.schemas[lineage.schema];
|
|
115
|
+
return [field, adminProperty(schema?.schema.properties?.[schema.names[lineage.field] ?? lineage.field] ?? { type: "string" })];
|
|
116
|
+
}));
|
|
117
|
+
const filterChecks = emitValidation({ procedures: { filters: { input: { type: "object", properties: filters, additionalProperties: false } } }, views: {} }, (path, msg) => refuse(`views/${name}/admin/` + path, msg));
|
|
118
|
+
files[`validation/admin/view-${encoded}.ts`] = filterChecks["validation/procedures/filters.ts"];
|
|
119
|
+
imports.push(`import {validate as filters${n}} from "./validation/admin/view-${encoded}.js";`);
|
|
120
|
+
const filterTransforms = Object.keys(filters).flatMap(field => { const col = v.columns?.[field] ?? v.columns?.[field.toLowerCase()], type = col && plan.schemas[col.schema]?.fields[col.field]; const convert = type === "date" && sqlite ? "encodeDate" : type === "timestamptz" && sqlite ? "encodeTimestamptz" : type === "bool" && sqlite ? "Number" : undefined; return convert ? [`if(Object.hasOwn(filters,${q(field)}))filters[${q(field)}]=${convert}(filters[${q(field)}] as ${type === "bool" ? "boolean" : "string"});`] : []; });
|
|
121
|
+
if (filterTransforms.some(t => t.includes("encode")))
|
|
122
|
+
imports.push('import {encodeDate,encodeTimestamptz} from "@aotter/mantle/scalars";');
|
|
123
|
+
const filterReads = Object.entries(filters).map(([field, s]) => { const type = [s.type].flat().find(t => t && t !== "null"); return `if(params.has(${q("filter." + field)})&¶ms.get(${q("filter." + field)})!=="")filters[${q(field)}]=${type === "boolean" ? `params.get(${q("filter." + field)})==="true"?true:params.get(${q("filter." + field)})==="false"?false:invalid("Expected true or false.",${q("views/" + name)})` : ["number", "integer"].includes(type ?? "") ? `Number(params.get(${q("filter." + field)}))` : `params.get(${q("filter." + field)})!`};`; });
|
|
124
|
+
const cursorKinds = (input.paging?.[name]?.types ?? []).map(type => type === "bool" ? "bool" : type === "integer" || type === "int8" ? "int" : type === "int4" ? "int4" : type.startsWith("numeric(") ? "numeric" : type === "real" || type === "float8" ? "float" : type === "date" ? "date" : type === "timestamptz" ? "timestamp" : "text");
|
|
125
|
+
const modes = Object.fromEntries(Object.entries({ caller: pageData.caller, trusted: pageData.trusted }).map(([mode, p]) => [mode, { ...p, binds: recipesOf(p, Object.keys(v.input?.properties ?? {}), message => refuse(`views/${name}/admin`, message)) }]));
|
|
126
|
+
pageSpecs.push(`const viewPage${n}=${q({ path: "views/" + name, cap: v.stmts[0]?.SelectStmt.limitCount?.A_Const?.ival?.ival ?? 500, cursorKinds, ...modes })} as const;`);
|
|
127
|
+
imports.push(`const allowsView${n}=${allowed(v.requires, `views/${name}`)};`);
|
|
128
|
+
if (v.surface === "admin")
|
|
129
|
+
adminViews.push(`view${n}`);
|
|
130
|
+
views.push(`{${user.used ? `check:(caller)=>{${authLines(undefined, `views/${name}`, pageData.caller.binds, sqlite).join("")}},` : ""}name:${q(name)},allows:allowsView${n},query:(params)=>{const result=queryView${n}(params),filters:Record<string,unknown>=Object.create(null);${filterReads.join("")}filters${n}(filters);${filterTransforms.join("")}${sqlite ? "" : "nativeText(result.input);nativeText(filters);"}${kinds.searchFields?.length ? `const search=params.get("search")?.trim();` : ""}return {...result,match:{filters${kinds.searchFields?.length ? ",...(search?{search}:{})" : ""}}};},page:(db,caller,input,paging)=>{deny(caller,allowsView${n}(caller),${q("views/" + name)});return nativePage(viewPage${n},adapter,db,caller,input,paging);},${converted ? `convert:${converted},` : ""}row:(raw)=>{const row=(${wire((input.viewKeys[name] ?? []).map(key => [key, boundaryType(plan, name, key)]), sqlite)})(raw);return Object.fromEntries(Object.entries(row).map(([key,value])=>[Object.hasOwn(viewNames${n},key)?viewNames${n}[key as keyof typeof viewNames${n}]:key,value]));}}`);
|
|
131
|
+
}
|
|
132
|
+
for (const [name, p] of Object.entries(plan.procedures).filter(([name]) => Object.values(plan.triggers).some(t => t.procedure === name && t.source.kind === "mcp" && t.source.surface === "admin"))) {
|
|
133
|
+
const n = operations.length, ref = "ref" in p.handler, encoded = encodeURIComponent(name), converted = conversions(p.input?.properties, lowered.procedures[name]?.caller?.flatMap(s => s.binds) ?? [], target);
|
|
134
|
+
imports.push(`import {validate as operation${n}} from "./validation/procedures/${encoded}.js";`);
|
|
135
|
+
operations.push(`{${input.description.targets.find(t => t._meta.id === `procedure:${name}`)?._meta.readOnly ? "readOnly:true," : ""}${user.used || p.trustedOnly ? `check:(caller)=>{${authLines(undefined, `procedures/${name}`, lowered.procedures[name]?.caller?.flatMap(s => s.binds), sqlite, p.trustedOnly).join("")}},` : ""}name:${q(name)},allows:${allowed(p.requires, `procedures/${name}`)},validate:operation${n},${converted ? `convert:${converted},` : ""}run:${ref ? `async(db,caller,input)=>(await import("./generated.js")).store(db,caller,{handlers:options.handlers}).procedures[${q(name)}](input)` : `async(db,caller,input)=>(await import("./procedures/${encoded}.js")).default(db,caller,input)`}}`);
|
|
136
|
+
}
|
|
137
|
+
const entryData = sqlConstants(input.adminEntries);
|
|
138
|
+
imports.push(...views.map((view, n) => `const view${n}=${view} satisfies View<Database>;`), ...pageSpecs, ...entryData.constants, `const entrySpecs=${entryData.code} as const;`);
|
|
139
|
+
imports.push(`export const statisticsView=(db:Database,caller:Caller,collection:keyof typeof entrySpecs,args:{from:number;to:number;bucketMs:number})=>nativeStatistics(entrySpecs,adapter,db,caller,collection,args);`);
|
|
140
|
+
const metadataGuards = [...Object.entries(plan.views).map(([name, v]) => `case ${q("view:" + name)}: return (${allowed(v.requires, `views/${name}`)})(caller);`), ...Object.entries(plan.procedures).map(([name, p]) => `case ${q("procedure:" + name)}: return (${allowed(p.requires, `procedures/${name}`)})(caller);`), ...Object.values(plan.schemas).filter(s => s.publishing).flatMap(s => ["publish", "unpublish", "archive"].map(verb => `case ${q("procedure:" + s.name + "." + verb)}: return isAdmin(caller);`))];
|
|
141
|
+
files["admin-bindings.ts"] = `${imports.filter(Boolean).join("\n")}\nexport function adminBindings${refs ? "<const H extends Handlers>(options:{handlers:H & Record<Exclude<keyof H,keyof Handlers>,never>})" : "(_options?:{handlers?:Handlers})"}{\nreturn {metadata:{...bootstrap,accessTargets:()=>import("./access-targets.js").then(m=>m.targets),${plan.access ? `userFields:${q(plan.access.userFields)} as const,` : ""}allows:(target:import("@aotter/mantle/contracts").Target,caller:Caller)=>{switch(target.kind+":"+target.name){${metadataGuards.join("")}default:return target.kind==="schema";}}},adapter,schemas:[${schemas.join(",\n")}] satisfies readonly EntrySchema<Database>[],views:[${adminViews.join(",")}],developerViews:[${views.map((_, n) => `view${n}`).join(",")}],operations:[${operations.join(",\n")}] satisfies readonly Operation<Database,${refs ? "Handlers" : "never"}>[]};\n}\n`;
|
|
142
|
+
return files;
|
|
143
|
+
}
|