emdash 0.6.0 → 1.0.0
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/dist/{adapters-Di31kZ28.d.mts → adapters-BKSf3T9R.d.mts} +1 -1
- package/dist/{adapters-Di31kZ28.d.mts.map → adapters-BKSf3T9R.d.mts.map} +1 -1
- package/dist/{apply-B4MsLM-w.mjs → apply-x0eMK1lX.mjs} +186 -28
- package/dist/apply-x0eMK1lX.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +92 -17
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +5 -5
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +22 -2
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.mjs +2 -2
- package/dist/astro/middleware/request-context.mjs +7 -2
- package/dist/astro/middleware/request-context.mjs.map +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +263 -74
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +25 -8
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-C4OVd8b3.mjs → byline-Chbr2GoP.mjs} +3 -3
- package/dist/byline-Chbr2GoP.mjs.map +1 -0
- package/dist/{bylines-hPTW79hw.mjs → bylines-CRNsVG88.mjs} +4 -4
- package/dist/{bylines-hPTW79hw.mjs.map → bylines-CRNsVG88.mjs.map} +1 -1
- package/dist/cli/index.mjs +17 -13
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +1 -1
- package/dist/{content-BsBoyj8G.mjs → content-BcQPYxdV.mjs} +39 -15
- package/dist/content-BcQPYxdV.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +1 -1
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/{db-errors-D0UT85nC.mjs → db-errors-l1Qh2RPR.mjs} +1 -1
- package/dist/{db-errors-D0UT85nC.mjs.map → db-errors-l1Qh2RPR.mjs.map} +1 -1
- package/dist/{default-CME5YdZ3.mjs → default-DCVqE5ib.mjs} +1 -1
- package/dist/{default-CME5YdZ3.mjs.map → default-DCVqE5ib.mjs.map} +1 -1
- package/dist/{error-CiYn9yDu.mjs → error-zG5T1UGA.mjs} +1 -1
- package/dist/error-zG5T1UGA.mjs.map +1 -0
- package/dist/{index-BYv0mB9g.d.mts → index-DIb-CzNx.d.mts} +232 -15
- package/dist/index-DIb-CzNx.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +23 -21
- package/dist/{load-CBcmDIot.mjs → load-CyEoextb.mjs} +1 -1
- package/dist/{load-CBcmDIot.mjs.map → load-CyEoextb.mjs.map} +1 -1
- package/dist/{loader-DeiBJEMe.mjs → loader-CndGj8kM.mjs} +8 -6
- package/dist/loader-CndGj8kM.mjs.map +1 -0
- package/dist/{manifest-schema-V30qsMft.mjs → manifest-schema-DH9xhc6t.mjs} +13 -1
- package/dist/manifest-schema-DH9xhc6t.mjs.map +1 -0
- package/dist/media/index.d.mts +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/media/local-runtime.mjs +2 -2
- package/dist/{media-DqHVh136.mjs → media-D8FbNsl0.mjs} +4 -7
- package/dist/media-D8FbNsl0.mjs.map +1 -0
- package/dist/{mode-CpNnGkPz.mjs → mode-BnAOqItE.mjs} +1 -1
- package/dist/mode-BnAOqItE.mjs.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/placeholder-C-fk5hYI.mjs.map +1 -1
- package/dist/{placeholder-tzpqGWII.d.mts → placeholder-D29tWZ7o.d.mts} +1 -1
- package/dist/{placeholder-tzpqGWII.d.mts.map → placeholder-D29tWZ7o.d.mts.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-Bk_3vKvU.mjs → query-fqEdLFms.mjs} +9 -9
- package/dist/{query-Bk_3vKvU.mjs.map → query-fqEdLFms.mjs.map} +1 -1
- package/dist/{redirect-7lGhLBNZ.mjs → redirect-D_pshWdf.mjs} +69 -13
- package/dist/redirect-D_pshWdf.mjs.map +1 -0
- package/dist/{registry-Ci3WxVAr.mjs → registry-C3Mr0ODu.mjs} +33 -9
- package/dist/registry-C3Mr0ODu.mjs.map +1 -0
- package/dist/{request-cache-DiR961CV.mjs → request-cache-Ci7f5pBb.mjs} +1 -1
- package/dist/request-cache-Ci7f5pBb.mjs.map +1 -0
- package/dist/{runner-Fl2NcUUz.d.mts → runner-OURCaApa.d.mts} +2 -2
- package/dist/{runner-Fl2NcUUz.d.mts.map → runner-OURCaApa.d.mts.map} +1 -1
- package/dist/{runner-Cd-_WyDo.mjs → runner-tQ7BJ4T7.mjs} +211 -134
- package/dist/runner-tQ7BJ4T7.mjs.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +2 -2
- package/dist/{search-DI4bM2w9.mjs → search-BoZYFuUk.mjs} +339 -102
- package/dist/search-BoZYFuUk.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +12 -12
- package/dist/seo/index.d.mts +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.d.mts.map +1 -1
- package/dist/storage/s3.mjs +4 -4
- package/dist/storage/s3.mjs.map +1 -1
- package/dist/{taxonomies-DbrKzDju.mjs → taxonomies-B4IAshV8.mjs} +5 -5
- package/dist/{taxonomies-DbrKzDju.mjs.map → taxonomies-B4IAshV8.mjs.map} +1 -1
- package/dist/{tokens-BFPFx3CA.mjs → tokens-D9vnZqYS.mjs} +1 -1
- package/dist/{tokens-BFPFx3CA.mjs.map → tokens-D9vnZqYS.mjs.map} +1 -1
- package/dist/{transport-BykRfpyy.mjs → transport-C9ugt2Nr.mjs} +1 -1
- package/dist/{transport-BykRfpyy.mjs.map → transport-C9ugt2Nr.mjs.map} +1 -1
- package/dist/{transport-H4Iwx7tC.d.mts → transport-CUnEL3Vs.d.mts} +1 -1
- package/dist/{transport-H4Iwx7tC.d.mts.map → transport-CUnEL3Vs.d.mts.map} +1 -1
- package/dist/types-BIgulNsW.mjs +68 -0
- package/dist/types-BIgulNsW.mjs.map +1 -0
- package/dist/{types-DDS4MxsT.mjs → types-Bm1dn-q3.mjs} +1 -1
- package/dist/{types-DDS4MxsT.mjs.map → types-Bm1dn-q3.mjs.map} +1 -1
- package/dist/{types-CnZYHyLW.d.mts → types-BmPPSUEx.d.mts} +1 -1
- package/dist/{types-CnZYHyLW.d.mts.map → types-BmPPSUEx.d.mts.map} +1 -1
- package/dist/{types-6CUZRrZP.d.mts → types-BrA0xf5I.d.mts} +24 -2
- package/dist/{types-6CUZRrZP.d.mts.map → types-BrA0xf5I.d.mts.map} +1 -1
- package/dist/{types-8xrvl_68.d.mts → types-CS8FIX7L.d.mts} +10 -1
- package/dist/{types-8xrvl_68.d.mts.map → types-CS8FIX7L.d.mts.map} +1 -1
- package/dist/{types-BH2L167P.mjs → types-CgqmmMJB.mjs} +1 -1
- package/dist/{types-BH2L167P.mjs.map → types-CgqmmMJB.mjs.map} +1 -1
- package/dist/{types-CFWjXmus.d.mts → types-DIMwPFub.d.mts} +1 -1
- package/dist/{types-CFWjXmus.d.mts.map → types-DIMwPFub.d.mts.map} +1 -1
- package/dist/{types-DgrIP0tF.d.mts → types-i36XcA_X.d.mts} +49 -6
- package/dist/types-i36XcA_X.d.mts.map +1 -0
- package/dist/{validate-CqsNItbt.mjs → validate-CxVsLehf.mjs} +2 -2
- package/dist/{validate-CqsNItbt.mjs.map → validate-CxVsLehf.mjs.map} +1 -1
- package/dist/{validate-CaLH1Ia2.d.mts → validate-DHxmpFJt.d.mts} +4 -4
- package/dist/{validate-CaLH1Ia2.d.mts.map → validate-DHxmpFJt.d.mts.map} +1 -1
- package/dist/validation-C-ZpN2GI.mjs +144 -0
- package/dist/validation-C-ZpN2GI.mjs.map +1 -0
- package/dist/version-DJrV1K0M.mjs +7 -0
- package/dist/{version-Uaf2ynPX.mjs.map → version-DJrV1K0M.mjs.map} +1 -1
- package/dist/zod-generator-CpwccCIv.mjs +132 -0
- package/dist/zod-generator-CpwccCIv.mjs.map +1 -0
- package/package.json +19 -6
- package/src/api/auth-storage.ts +37 -0
- package/src/api/error.ts +6 -0
- package/src/api/errors.ts +8 -0
- package/src/api/handlers/comments.ts +13 -0
- package/src/api/handlers/content.ts +124 -3
- package/src/api/handlers/index.ts +2 -0
- package/src/api/handlers/media.ts +8 -1
- package/src/api/handlers/menus.ts +160 -21
- package/src/api/handlers/redirects.ts +16 -3
- package/src/api/handlers/sections.ts +8 -1
- package/src/api/handlers/taxonomies.ts +128 -16
- package/src/api/handlers/validation.ts +212 -0
- package/src/api/openapi/document.ts +4 -1
- package/src/api/public-url.ts +6 -3
- package/src/api/route-utils.ts +14 -0
- package/src/api/schemas/common.ts +1 -1
- package/src/api/schemas/content.ts +8 -0
- package/src/api/schemas/setup.ts +8 -0
- package/src/api/schemas/widgets.ts +12 -10
- package/src/api/setup-complete.ts +40 -0
- package/src/astro/integration/font-provider.ts +3 -1
- package/src/astro/integration/index.ts +15 -2
- package/src/astro/integration/routes.ts +28 -0
- package/src/astro/integration/runtime.ts +74 -2
- package/src/astro/integration/virtual-modules.ts +41 -0
- package/src/astro/integration/vite-config.ts +43 -12
- package/src/astro/middleware/auth.ts +21 -0
- package/src/astro/middleware.ts +18 -1
- package/src/astro/routes/PluginRegistry.tsx +10 -1
- package/src/astro/routes/admin.astro +14 -7
- package/src/astro/routes/api/auth/magic-link/send.ts +2 -1
- package/src/astro/routes/api/auth/mode.ts +57 -0
- package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +23 -3
- package/src/astro/routes/api/auth/oauth/[provider].ts +10 -4
- package/src/astro/routes/api/auth/passkey/options.ts +2 -1
- package/src/astro/routes/api/auth/signup/request.ts +26 -8
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +10 -6
- package/src/astro/routes/api/content/[collection]/[id]/compare.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +26 -0
- package/src/astro/routes/api/content/[collection]/[id].ts +30 -2
- package/src/astro/routes/api/content/[collection]/index.ts +20 -10
- package/src/astro/routes/api/content/[collection]/trash.ts +1 -1
- package/src/astro/routes/api/import/wordpress/media.ts +2 -7
- package/src/astro/routes/api/import/wordpress/prepare.ts +10 -0
- package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +4 -3
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +4 -3
- package/src/astro/routes/api/manifest.ts +7 -0
- package/src/astro/routes/api/oauth/device/code.ts +2 -1
- package/src/astro/routes/api/oauth/device/token.ts +2 -1
- package/src/astro/routes/api/settings/email.ts +4 -9
- package/src/astro/routes/api/setup/admin-verify.ts +30 -5
- package/src/astro/routes/api/setup/admin.ts +38 -8
- package/src/astro/routes/api/setup/index.ts +7 -4
- package/src/astro/routes/api/setup/status.ts +3 -1
- package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +4 -1
- package/src/astro/routes/api/widget-areas/[name]/widgets.ts +4 -1
- package/src/astro/routes/api/widget-areas/[name].ts +4 -1
- package/src/astro/routes/api/widget-areas/index.ts +4 -1
- package/src/astro/types.ts +18 -0
- package/src/auth/mode.ts +15 -3
- package/src/auth/providers/github-admin.tsx +29 -0
- package/src/auth/providers/github.ts +31 -0
- package/src/auth/providers/google-admin.tsx +44 -0
- package/src/auth/providers/google.ts +31 -0
- package/src/auth/rate-limit.ts +50 -22
- package/src/auth/setup-nonce.ts +22 -0
- package/src/auth/trusted-proxy.ts +92 -0
- package/src/auth/types.ts +114 -4
- package/src/cli/commands/bundle.ts +3 -1
- package/src/components/EmDashImage.astro +7 -6
- package/src/components/Gallery.astro +5 -3
- package/src/components/Image.astro +8 -3
- package/src/components/InlinePortableTextEditor.tsx +2 -1
- package/src/components/LiveSearch.astro +5 -14
- package/src/database/migrations/035_bounded_404_log.ts +112 -0
- package/src/database/migrations/runner.ts +2 -0
- package/src/database/repositories/audit.ts +6 -8
- package/src/database/repositories/byline.ts +6 -8
- package/src/database/repositories/comment.ts +12 -16
- package/src/database/repositories/content.ts +79 -40
- package/src/database/repositories/index.ts +1 -1
- package/src/database/repositories/media.ts +10 -13
- package/src/database/repositories/options.ts +25 -0
- package/src/database/repositories/plugin-storage.ts +4 -6
- package/src/database/repositories/redirect.ts +123 -24
- package/src/database/repositories/taxonomy.ts +14 -3
- package/src/database/repositories/types.ts +57 -8
- package/src/database/repositories/user.ts +6 -8
- package/src/database/types.ts +9 -0
- package/src/emdash-runtime.ts +309 -91
- package/src/import/registry.ts +4 -3
- package/src/import/ssrf.ts +253 -12
- package/src/index.ts +5 -1
- package/src/loader.ts +6 -5
- package/src/mcp/server.ts +753 -107
- package/src/media/normalize.ts +1 -1
- package/src/media/url.ts +78 -0
- package/src/plugins/context.ts +15 -3
- package/src/plugins/email-console.ts +10 -3
- package/src/plugins/hooks.ts +11 -0
- package/src/plugins/manager.ts +6 -0
- package/src/plugins/manifest-schema.ts +12 -0
- package/src/plugins/request-meta.ts +66 -15
- package/src/plugins/routes.ts +3 -1
- package/src/plugins/types.ts +23 -2
- package/src/query.ts +1 -1
- package/src/request-cache.ts +3 -0
- package/src/schema/registry.ts +41 -5
- package/src/search/fts-manager.ts +0 -2
- package/src/search/query.ts +111 -26
- package/src/search/types.ts +8 -1
- package/src/sections/index.ts +7 -9
- package/src/seed/apply.ts +26 -0
- package/src/storage/s3.ts +12 -6
- package/src/virtual-modules.d.ts +21 -1
- package/src/visual-editing/toolbar.ts +6 -1
- package/src/widgets/index.ts +1 -1
- package/dist/apply-B4MsLM-w.mjs.map +0 -1
- package/dist/byline-C4OVd8b3.mjs.map +0 -1
- package/dist/content-BsBoyj8G.mjs.map +0 -1
- package/dist/error-CiYn9yDu.mjs.map +0 -1
- package/dist/index-BYv0mB9g.d.mts.map +0 -1
- package/dist/loader-DeiBJEMe.mjs.map +0 -1
- package/dist/manifest-schema-V30qsMft.mjs.map +0 -1
- package/dist/media-DqHVh136.mjs.map +0 -1
- package/dist/mode-CpNnGkPz.mjs.map +0 -1
- package/dist/redirect-7lGhLBNZ.mjs.map +0 -1
- package/dist/registry-Ci3WxVAr.mjs.map +0 -1
- package/dist/request-cache-DiR961CV.mjs.map +0 -1
- package/dist/runner-Cd-_WyDo.mjs.map +0 -1
- package/dist/search-DI4bM2w9.mjs.map +0 -1
- package/dist/types-CMMN0pNg.mjs +0 -31
- package/dist/types-CMMN0pNg.mjs.map +0 -1
- package/dist/types-DgrIP0tF.d.mts.map +0 -1
- package/dist/version-Uaf2ynPX.mjs +0 -7
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { t as validateIdentifier } from "./validate-VPnKoIzW.mjs";
|
|
2
|
+
import "./dialect-helpers-DhTzaUxP.mjs";
|
|
3
|
+
import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-HGz06Soa.mjs";
|
|
4
|
+
import { t as isMissingTableError } from "./db-errors-l1Qh2RPR.mjs";
|
|
5
|
+
import { t as generateZodSchema } from "./zod-generator-CpwccCIv.mjs";
|
|
6
|
+
import { n as SchemaRegistry } from "./registry-C3Mr0ODu.mjs";
|
|
7
|
+
import { sql } from "kysely";
|
|
8
|
+
|
|
9
|
+
//#region src/api/handlers/validation.ts
|
|
10
|
+
/**
|
|
11
|
+
* Field-level validation for content create / update.
|
|
12
|
+
*
|
|
13
|
+
* Wires the existing `generateZodSchema()` pipeline (`schema/zod-generator.ts`)
|
|
14
|
+
* into the handler boundary so REST and MCP both get the same enforcement:
|
|
15
|
+
*
|
|
16
|
+
* - required fields must be present and non-empty
|
|
17
|
+
* - select / multiSelect values must match the configured options
|
|
18
|
+
* - reference fields must resolve to a real, non-trashed target
|
|
19
|
+
*
|
|
20
|
+
* Errors surface as `{ code: "VALIDATION_ERROR", message }` with all
|
|
21
|
+
* offending fields listed in one message so callers can fix everything in
|
|
22
|
+
* a single round trip.
|
|
23
|
+
*/
|
|
24
|
+
/** Treat `undefined`, `null`, and `""` as "not set". */
|
|
25
|
+
function isMissing(value) {
|
|
26
|
+
return value === void 0 || value === null || value === "";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the target collection slug for a reference field.
|
|
30
|
+
*
|
|
31
|
+
* Schema-defined reference fields (the static `reference()` factory in
|
|
32
|
+
* `fields/reference.ts`) put the target in `options.collection`. The MCP
|
|
33
|
+
* `schema_create_field` tool also puts it there. Tests and some admin paths
|
|
34
|
+
* stash it inside `validation.collection` directly; we accept both.
|
|
35
|
+
*/
|
|
36
|
+
function getReferenceTargetCollection(field) {
|
|
37
|
+
const fromOptions = field.options?.collection;
|
|
38
|
+
if (typeof fromOptions === "string" && fromOptions.length > 0) return fromOptions;
|
|
39
|
+
const validation = field.validation;
|
|
40
|
+
if (validation && "collection" in validation) {
|
|
41
|
+
const fromValidation = validation.collection;
|
|
42
|
+
if (typeof fromValidation === "string" && fromValidation.length > 0) return fromValidation;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Format a Zod issue path into a human-readable field reference, e.g.
|
|
47
|
+
* `tags`, `tags.1`, `image.alt`.
|
|
48
|
+
*/
|
|
49
|
+
function formatIssuePath(path) {
|
|
50
|
+
if (path.length === 0) return "(root)";
|
|
51
|
+
return path.map((seg) => String(seg)).join(".");
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Validate `data` against the collection's field definitions.
|
|
55
|
+
*
|
|
56
|
+
* `partial: true` switches Zod into partial mode so updates can include
|
|
57
|
+
* only the fields being changed without tripping required-field errors on
|
|
58
|
+
* fields the caller didn't touch. Required fields that ARE present in
|
|
59
|
+
* partial-mode data still get the empty-string check below.
|
|
60
|
+
*/
|
|
61
|
+
async function validateContentData(db, collection, data, options = {}) {
|
|
62
|
+
const collectionWithFields = await new SchemaRegistry(db).getCollectionWithFields(collection);
|
|
63
|
+
if (!collectionWithFields) return {
|
|
64
|
+
ok: false,
|
|
65
|
+
error: {
|
|
66
|
+
code: "COLLECTION_NOT_FOUND",
|
|
67
|
+
message: `Collection '${collection}' not found`
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const issues = [];
|
|
71
|
+
const knownFields = new Set(collectionWithFields.fields.map((f) => f.slug));
|
|
72
|
+
for (const key of Object.keys(data)) {
|
|
73
|
+
if (key.startsWith("_")) continue;
|
|
74
|
+
if (!knownFields.has(key)) issues.push(`${key}: unknown field on collection '${collection}'`);
|
|
75
|
+
}
|
|
76
|
+
const baseSchema = generateZodSchema(collectionWithFields);
|
|
77
|
+
const parsed = (options.partial ? baseSchema.partial() : baseSchema).safeParse(data);
|
|
78
|
+
if (!parsed.success) for (const issue of parsed.error.issues) issues.push(`${formatIssuePath(issue.path)}: ${issue.message}`);
|
|
79
|
+
for (const field of collectionWithFields.fields) {
|
|
80
|
+
if (!field.required) continue;
|
|
81
|
+
const present = Object.hasOwn(data, field.slug);
|
|
82
|
+
if (options.partial && !present) continue;
|
|
83
|
+
if (data[field.slug] === "") issues.push(`${field.slug}: required (empty value not allowed)`);
|
|
84
|
+
}
|
|
85
|
+
const refsByTarget = /* @__PURE__ */ new Map();
|
|
86
|
+
for (const field of collectionWithFields.fields) {
|
|
87
|
+
if (field.type !== "reference") continue;
|
|
88
|
+
if (options.partial && !Object.hasOwn(data, field.slug)) continue;
|
|
89
|
+
const value = data[field.slug];
|
|
90
|
+
if (isMissing(value)) continue;
|
|
91
|
+
if (typeof value !== "string") continue;
|
|
92
|
+
const target = getReferenceTargetCollection(field);
|
|
93
|
+
if (!target) continue;
|
|
94
|
+
const list = refsByTarget.get(target) ?? [];
|
|
95
|
+
list.push({
|
|
96
|
+
field: field.slug,
|
|
97
|
+
id: value
|
|
98
|
+
});
|
|
99
|
+
refsByTarget.set(target, list);
|
|
100
|
+
}
|
|
101
|
+
for (const [target, refs] of refsByTarget) {
|
|
102
|
+
try {
|
|
103
|
+
validateIdentifier(target, "reference target collection");
|
|
104
|
+
} catch {
|
|
105
|
+
for (const ref of refs) issues.push(`${ref.field}: invalid reference target collection '${target}'`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const ids = [...new Set(refs.map((r) => r.id))];
|
|
109
|
+
const tableName = `ec_${target}`;
|
|
110
|
+
const found = /* @__PURE__ */ new Set();
|
|
111
|
+
let targetTableMissing = false;
|
|
112
|
+
for (const idChunk of chunks(ids, SQL_BATCH_SIZE)) try {
|
|
113
|
+
const rows = await sql`
|
|
114
|
+
SELECT id FROM ${sql.ref(tableName)}
|
|
115
|
+
WHERE id IN (${sql.join(idChunk)})
|
|
116
|
+
AND deleted_at IS NULL
|
|
117
|
+
`.execute(db);
|
|
118
|
+
for (const row of rows.rows) found.add(row.id);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (isMissingTableError(error)) {
|
|
121
|
+
targetTableMissing = true;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
if (targetTableMissing) {
|
|
127
|
+
for (const ref of refs) issues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
for (const ref of refs) if (!found.has(ref.id)) issues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);
|
|
131
|
+
}
|
|
132
|
+
if (issues.length === 0) return { ok: true };
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
error: {
|
|
136
|
+
code: "VALIDATION_ERROR",
|
|
137
|
+
message: issues.join("; ")
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
//#endregion
|
|
143
|
+
export { validateContentData };
|
|
144
|
+
//# sourceMappingURL=validation-C-ZpN2GI.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation-C-ZpN2GI.mjs","names":[],"sources":["../src/api/handlers/validation.ts"],"sourcesContent":["/**\n * Field-level validation for content create / update.\n *\n * Wires the existing `generateZodSchema()` pipeline (`schema/zod-generator.ts`)\n * into the handler boundary so REST and MCP both get the same enforcement:\n *\n * - required fields must be present and non-empty\n * - select / multiSelect values must match the configured options\n * - reference fields must resolve to a real, non-trashed target\n *\n * Errors surface as `{ code: \"VALIDATION_ERROR\", message }` with all\n * offending fields listed in one message so callers can fix everything in\n * a single round trip.\n */\n\nimport { sql, type Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport { validateIdentifier } from \"../../database/validate.js\";\nimport { SchemaRegistry } from \"../../schema/registry.js\";\nimport type { Field } from \"../../schema/types.js\";\nimport { generateZodSchema } from \"../../schema/zod-generator.js\";\nimport { chunks, SQL_BATCH_SIZE } from \"../../utils/chunks.js\";\nimport { isMissingTableError } from \"../../utils/db-errors.js\";\n\ntype ValidationResult =\n\t| { ok: true }\n\t| { ok: false; error: { code: \"VALIDATION_ERROR\" | \"COLLECTION_NOT_FOUND\"; message: string } };\n\n/** Treat `undefined`, `null`, and `\"\"` as \"not set\". */\nfunction isMissing(value: unknown): boolean {\n\treturn value === undefined || value === null || value === \"\";\n}\n\n/**\n * Resolve the target collection slug for a reference field.\n *\n * Schema-defined reference fields (the static `reference()` factory in\n * `fields/reference.ts`) put the target in `options.collection`. The MCP\n * `schema_create_field` tool also puts it there. Tests and some admin paths\n * stash it inside `validation.collection` directly; we accept both.\n */\nfunction getReferenceTargetCollection(field: Field): string | undefined {\n\tconst fromOptions = field.options?.collection;\n\tif (typeof fromOptions === \"string\" && fromOptions.length > 0) return fromOptions;\n\tconst validation = field.validation;\n\tif (validation && \"collection\" in validation) {\n\t\tconst fromValidation: unknown = (validation as { collection?: unknown }).collection;\n\t\tif (typeof fromValidation === \"string\" && fromValidation.length > 0) return fromValidation;\n\t}\n\treturn undefined;\n}\n\n/**\n * Format a Zod issue path into a human-readable field reference, e.g.\n * `tags`, `tags.1`, `image.alt`.\n */\nfunction formatIssuePath(path: ReadonlyArray<PropertyKey>): string {\n\tif (path.length === 0) return \"(root)\";\n\treturn path.map((seg) => String(seg)).join(\".\");\n}\n\n/**\n * Validate `data` against the collection's field definitions.\n *\n * `partial: true` switches Zod into partial mode so updates can include\n * only the fields being changed without tripping required-field errors on\n * fields the caller didn't touch. Required fields that ARE present in\n * partial-mode data still get the empty-string check below.\n */\nexport async function validateContentData(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tdata: Record<string, unknown>,\n\toptions: { partial?: boolean } = {},\n): Promise<ValidationResult> {\n\tconst registry = new SchemaRegistry(db);\n\tconst collectionWithFields = await registry.getCollectionWithFields(collection);\n\tif (!collectionWithFields) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COLLECTION_NOT_FOUND\",\n\t\t\t\tmessage: `Collection '${collection}' not found`,\n\t\t\t},\n\t\t};\n\t}\n\n\tconst issues: string[] = [];\n\n\t// Detect unknown keys explicitly so callers get a useful error rather\n\t// than silently dropped data. Leading-underscore keys (e.g. `_slug`,\n\t// `_rev`) are reserved for internal handler/runtime use and aren't real\n\t// fields; skip them.\n\tconst knownFields = new Set(collectionWithFields.fields.map((f) => f.slug));\n\tfor (const key of Object.keys(data)) {\n\t\tif (key.startsWith(\"_\")) continue;\n\t\tif (!knownFields.has(key)) {\n\t\t\tissues.push(`${key}: unknown field on collection '${collection}'`);\n\t\t}\n\t}\n\n\t// Zod handles type, enum, length and missing-required (in non-partial\n\t// mode) checks. Empty-string handling for required string fields is\n\t// done as a separate pass below since Zod's `z.string()` accepts \"\".\n\tconst baseSchema = generateZodSchema(collectionWithFields);\n\tconst schema = options.partial ? baseSchema.partial() : baseSchema;\n\tconst parsed = schema.safeParse(data);\n\tif (!parsed.success) {\n\t\tfor (const issue of parsed.error.issues) {\n\t\t\tissues.push(`${formatIssuePath(issue.path)}: ${issue.message}`);\n\t\t}\n\t}\n\n\t// Empty-string-on-required check. In create mode (partial=false) Zod\n\t// already catches missing/null for required fields, but `z.string()`\n\t// happily accepts \"\". In update mode (partial=true) the field is only\n\t// checked if it's present in `data`.\n\tfor (const field of collectionWithFields.fields) {\n\t\tif (!field.required) continue;\n\t\tconst present = Object.hasOwn(data, field.slug);\n\t\tif (options.partial && !present) continue;\n\t\tif (data[field.slug] === \"\") {\n\t\t\tissues.push(`${field.slug}: required (empty value not allowed)`);\n\t\t}\n\t}\n\n\t// Reference target existence. Only check fields that:\n\t// - have a value (non-missing) in `data`\n\t// - have a resolvable target collection\n\t// - in partial mode: are present in `data`\n\t// Batch one IN-query per target collection to keep round-trips low.\n\tconst refsByTarget = new Map<string, { field: string; id: string }[]>();\n\tfor (const field of collectionWithFields.fields) {\n\t\tif (field.type !== \"reference\") continue;\n\t\tif (options.partial && !Object.hasOwn(data, field.slug)) continue;\n\t\tconst value = data[field.slug];\n\t\tif (isMissing(value)) continue;\n\t\tif (typeof value !== \"string\") continue; // Zod will have flagged this already\n\t\tconst target = getReferenceTargetCollection(field);\n\t\tif (!target) continue;\n\t\tconst list = refsByTarget.get(target) ?? [];\n\t\tlist.push({ field: field.slug, id: value });\n\t\trefsByTarget.set(target, list);\n\t}\n\n\tfor (const [target, refs] of refsByTarget) {\n\t\t// Validate the target collection slug before interpolating into raw\n\t\t// SQL — defense-in-depth even though slugs are already validated at\n\t\t// schema-create time.\n\t\ttry {\n\t\t\tvalidateIdentifier(target, \"reference target collection\");\n\t\t} catch {\n\t\t\tfor (const ref of refs) {\n\t\t\t\tissues.push(`${ref.field}: invalid reference target collection '${target}'`);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst ids = [...new Set(refs.map((r) => r.id))];\n\t\tconst tableName = `ec_${target}`;\n\n\t\t// Chunk the IN clause to stay below D1's bind-parameter limit. One\n\t\t// reference per request is the common case today; chunking makes the\n\t\t// helper safe if a future multiSelect-of-references is added.\n\t\tconst found = new Set<string>();\n\t\tlet targetTableMissing = false;\n\t\tfor (const idChunk of chunks(ids, SQL_BATCH_SIZE)) {\n\t\t\ttry {\n\t\t\t\tconst rows = await sql<{ id: string }>`\n\t\t\t\t\tSELECT id FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE id IN (${sql.join(idChunk)})\n\t\t\t\t\tAND deleted_at IS NULL\n\t\t\t\t`.execute(db);\n\t\t\t\tfor (const row of rows.rows) {\n\t\t\t\t\tfound.add(row.id);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\t// Missing table = the target collection table doesn't exist\n\t\t\t\t// (orphan reference). Treat all those references as missing.\n\t\t\t\t// Any other DB error (permissions, connection, syntax) must\n\t\t\t\t// propagate — silently dropping data integrity errors as\n\t\t\t\t// \"not found\" is exactly the bug F5 fixes.\n\t\t\t\tif (isMissingTableError(error)) {\n\t\t\t\t\ttargetTableMissing = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\t\tif (targetTableMissing) {\n\t\t\tfor (const ref of refs) {\n\t\t\t\tissues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const ref of refs) {\n\t\t\tif (!found.has(ref.id)) {\n\t\t\t\tissues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (issues.length === 0) return { ok: true };\n\treturn {\n\t\tok: false,\n\t\terror: {\n\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\tmessage: issues.join(\"; \"),\n\t\t},\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version-
|
|
1
|
+
{"version":3,"file":"version-DJrV1K0M.mjs","names":[],"sources":["../src/version.ts"],"sourcesContent":["/**\n * Build-time version constants, replaced by tsdown/Vite `define`.\n * Falls back to \"dev\" when running uncompiled (tests, dev).\n */\n\ndeclare const __EMDASH_VERSION__: string;\ndeclare const __EMDASH_COMMIT__: string;\n\nexport const VERSION: string =\n\ttypeof __EMDASH_VERSION__ !== \"undefined\" ? __EMDASH_VERSION__ : \"dev\";\n\nexport const COMMIT: string = typeof __EMDASH_COMMIT__ !== \"undefined\" ? __EMDASH_COMMIT__ : \"dev\";\n"],"mappings":";AAQA,MAAa;AAGb,MAAa"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
//#region src/utils/hash.ts
|
|
4
|
+
/**
|
|
5
|
+
* SHA-256 hash of a string, truncated to 16 hex chars (64 bits).
|
|
6
|
+
* For cache invalidation / ETags — not for security.
|
|
7
|
+
*/
|
|
8
|
+
async function hashString(content) {
|
|
9
|
+
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(content));
|
|
10
|
+
return Array.from(new Uint8Array(buf).slice(0, 8), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Compute content hash using Web Crypto API
|
|
14
|
+
*
|
|
15
|
+
* Uses SHA-1 which is the fastest option in SubtleCrypto.
|
|
16
|
+
* SHA-1 is cryptographically weak but fine for content deduplication
|
|
17
|
+
* where we only need to detect identical files, not resist attacks.
|
|
18
|
+
*
|
|
19
|
+
* Returns hex string prefixed with "sha1:" for future-proofing
|
|
20
|
+
*/
|
|
21
|
+
async function computeContentHash(content) {
|
|
22
|
+
let buf;
|
|
23
|
+
if (content instanceof ArrayBuffer) buf = content;
|
|
24
|
+
else {
|
|
25
|
+
buf = new ArrayBuffer(content.byteLength);
|
|
26
|
+
new Uint8Array(buf).set(content);
|
|
27
|
+
}
|
|
28
|
+
const hashBuffer = await crypto.subtle.digest("SHA-1", buf);
|
|
29
|
+
const hashArray = new Uint8Array(hashBuffer);
|
|
30
|
+
return `sha1:${Array.from(hashArray, (b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/schema/zod-generator.ts
|
|
35
|
+
/**
|
|
36
|
+
* Generate a Zod schema from a collection's field definitions
|
|
37
|
+
*
|
|
38
|
+
* This allows runtime validation of content based on dynamically
|
|
39
|
+
* defined schemas stored in D1.
|
|
40
|
+
*/
|
|
41
|
+
function generateZodSchema(collection) {
|
|
42
|
+
const shape = {};
|
|
43
|
+
for (const field of collection.fields) shape[field.slug] = generateFieldSchema(field);
|
|
44
|
+
return z.object(shape);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Generate Zod schema for a single field
|
|
48
|
+
*/
|
|
49
|
+
function generateFieldSchema(field) {
|
|
50
|
+
let schema = getBaseSchema(field.type, field);
|
|
51
|
+
if (field.validation) schema = applyValidation(schema, field);
|
|
52
|
+
if (!field.required) schema = schema.optional();
|
|
53
|
+
if (field.defaultValue !== void 0) schema = schema.default(field.defaultValue);
|
|
54
|
+
return schema;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get base Zod schema for a field type
|
|
58
|
+
*/
|
|
59
|
+
function getBaseSchema(type, field) {
|
|
60
|
+
switch (type) {
|
|
61
|
+
case "url": return z.string().url();
|
|
62
|
+
case "string":
|
|
63
|
+
case "text":
|
|
64
|
+
case "slug": return z.string();
|
|
65
|
+
case "number": return z.number();
|
|
66
|
+
case "integer": return z.number().int();
|
|
67
|
+
case "boolean": return z.boolean();
|
|
68
|
+
case "datetime": return z.string().datetime().or(z.string().date());
|
|
69
|
+
case "select": {
|
|
70
|
+
const options = field.validation?.options;
|
|
71
|
+
if (options && options.length > 0) {
|
|
72
|
+
const [first, ...rest] = options;
|
|
73
|
+
return z.enum([first, ...rest]);
|
|
74
|
+
}
|
|
75
|
+
return z.string();
|
|
76
|
+
}
|
|
77
|
+
case "multiSelect": {
|
|
78
|
+
const multiOptions = field.validation?.options;
|
|
79
|
+
if (multiOptions && multiOptions.length > 0) {
|
|
80
|
+
const [first, ...rest] = multiOptions;
|
|
81
|
+
return z.array(z.enum([first, ...rest]));
|
|
82
|
+
}
|
|
83
|
+
return z.array(z.string());
|
|
84
|
+
}
|
|
85
|
+
case "portableText": return z.array(z.object({
|
|
86
|
+
_type: z.string(),
|
|
87
|
+
_key: z.string()
|
|
88
|
+
}).passthrough());
|
|
89
|
+
case "image": return z.object({
|
|
90
|
+
id: z.string(),
|
|
91
|
+
src: z.string().optional(),
|
|
92
|
+
alt: z.string().optional(),
|
|
93
|
+
width: z.number().optional(),
|
|
94
|
+
height: z.number().optional()
|
|
95
|
+
});
|
|
96
|
+
case "file": return z.object({
|
|
97
|
+
id: z.string(),
|
|
98
|
+
src: z.string().optional(),
|
|
99
|
+
filename: z.string().optional(),
|
|
100
|
+
mimeType: z.string().optional(),
|
|
101
|
+
size: z.number().optional()
|
|
102
|
+
});
|
|
103
|
+
case "reference": return z.string();
|
|
104
|
+
case "json": return z.unknown();
|
|
105
|
+
default: return z.unknown();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Apply validation rules to a schema
|
|
110
|
+
*/
|
|
111
|
+
function applyValidation(schema, field) {
|
|
112
|
+
const validation = field.validation;
|
|
113
|
+
if (!validation) return schema;
|
|
114
|
+
if (schema instanceof z.ZodString) {
|
|
115
|
+
let strSchema = schema;
|
|
116
|
+
if (validation.minLength !== void 0) strSchema = strSchema.min(validation.minLength);
|
|
117
|
+
if (validation.maxLength !== void 0) strSchema = strSchema.max(validation.maxLength);
|
|
118
|
+
if (validation.pattern) strSchema = strSchema.regex(new RegExp(validation.pattern));
|
|
119
|
+
return strSchema;
|
|
120
|
+
}
|
|
121
|
+
if (schema instanceof z.ZodNumber) {
|
|
122
|
+
let numSchema = schema;
|
|
123
|
+
if (validation.min !== void 0) numSchema = numSchema.min(validation.min);
|
|
124
|
+
if (validation.max !== void 0) numSchema = numSchema.max(validation.max);
|
|
125
|
+
return numSchema;
|
|
126
|
+
}
|
|
127
|
+
return schema;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
//#endregion
|
|
131
|
+
export { computeContentHash as n, hashString as r, generateZodSchema as t };
|
|
132
|
+
//# sourceMappingURL=zod-generator-CpwccCIv.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zod-generator-CpwccCIv.mjs","names":[],"sources":["../src/utils/hash.ts","../src/schema/zod-generator.ts"],"sourcesContent":["/**\n * SHA-256 hash of a string, truncated to 16 hex chars (64 bits).\n * For cache invalidation / ETags — not for security.\n */\nexport async function hashString(content: string): Promise<string> {\n\tconst buf = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(content));\n\treturn Array.from(new Uint8Array(buf).slice(0, 8), (b) => b.toString(16).padStart(2, \"0\")).join(\n\t\t\"\",\n\t);\n}\n\n/**\n * Compute content hash using Web Crypto API\n *\n * Uses SHA-1 which is the fastest option in SubtleCrypto.\n * SHA-1 is cryptographically weak but fine for content deduplication\n * where we only need to detect identical files, not resist attacks.\n *\n * Returns hex string prefixed with \"sha1:\" for future-proofing\n */\nexport async function computeContentHash(content: Uint8Array | ArrayBuffer): Promise<string> {\n\t// SubtleCrypto.digest() requires BufferSource (ArrayBuffer | ArrayBufferView<ArrayBuffer>).\n\t// Uint8Array.buffer is ArrayBufferLike which may include SharedArrayBuffer in the type system,\n\t// so we ensure we have a plain ArrayBuffer.\n\tlet buf: ArrayBuffer;\n\tif (content instanceof ArrayBuffer) {\n\t\tbuf = content;\n\t} else {\n\t\tbuf = new ArrayBuffer(content.byteLength);\n\t\tnew Uint8Array(buf).set(content);\n\t}\n\tconst hashBuffer = await crypto.subtle.digest(\"SHA-1\", buf);\n\tconst hashArray = new Uint8Array(hashBuffer);\n\tconst hashHex = Array.from(hashArray, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n\treturn `sha1:${hashHex}`;\n}\n","import { z, type ZodTypeAny } from \"zod\";\n\nimport { hashString } from \"../utils/hash.js\";\nimport type { Field, FieldType, CollectionWithFields } from \"./types.js\";\n\n/** Pattern to split on underscores, hyphens, and spaces for PascalCase conversion */\nconst PASCAL_CASE_SPLIT_PATTERN = /[_\\-\\s]+/;\n\n/**\n * Generate a Zod schema from a collection's field definitions\n *\n * This allows runtime validation of content based on dynamically\n * defined schemas stored in D1.\n */\nexport function generateZodSchema(\n\tcollection: CollectionWithFields,\n): z.ZodObject<Record<string, ZodTypeAny>> {\n\tconst shape: Record<string, ZodTypeAny> = {};\n\n\tfor (const field of collection.fields) {\n\t\tshape[field.slug] = generateFieldSchema(field);\n\t}\n\n\treturn z.object(shape);\n}\n\n/**\n * Generate Zod schema for a single field\n */\nexport function generateFieldSchema(field: Field): ZodTypeAny {\n\tlet schema = getBaseSchema(field.type, field);\n\n\t// Apply validation rules\n\tif (field.validation) {\n\t\tschema = applyValidation(schema, field);\n\t}\n\n\t// Apply required/optional\n\tif (!field.required) {\n\t\tschema = schema.optional();\n\t}\n\n\t// Apply default value\n\tif (field.defaultValue !== undefined) {\n\t\tschema = schema.default(field.defaultValue);\n\t}\n\n\treturn schema;\n}\n\n/**\n * Get base Zod schema for a field type\n */\nfunction getBaseSchema(type: FieldType, field: Field): ZodTypeAny {\n\tswitch (type) {\n\t\tcase \"url\":\n\t\t\treturn z.string().url();\n\n\t\tcase \"string\":\n\t\tcase \"text\":\n\t\tcase \"slug\":\n\t\t\treturn z.string();\n\n\t\tcase \"number\":\n\t\t\treturn z.number();\n\n\t\tcase \"integer\":\n\t\t\treturn z.number().int();\n\n\t\tcase \"boolean\":\n\t\t\treturn z.boolean();\n\n\t\tcase \"datetime\":\n\t\t\treturn z.string().datetime().or(z.string().date());\n\n\t\tcase \"select\": {\n\t\t\tconst options = field.validation?.options;\n\t\t\tif (options && options.length > 0) {\n\t\t\t\tconst [first, ...rest] = options;\n\t\t\t\treturn z.enum([first, ...rest]);\n\t\t\t}\n\t\t\treturn z.string();\n\t\t}\n\n\t\tcase \"multiSelect\": {\n\t\t\tconst multiOptions = field.validation?.options;\n\t\t\tif (multiOptions && multiOptions.length > 0) {\n\t\t\t\tconst [first, ...rest] = multiOptions;\n\t\t\t\treturn z.array(z.enum([first, ...rest]));\n\t\t\t}\n\t\t\treturn z.array(z.string());\n\t\t}\n\n\t\tcase \"portableText\":\n\t\t\t// Portable Text is an array of blocks\n\t\t\treturn z.array(\n\t\t\t\tz\n\t\t\t\t\t.object({\n\t\t\t\t\t\t_type: z.string(),\n\t\t\t\t\t\t_key: z.string(),\n\t\t\t\t\t})\n\t\t\t\t\t.passthrough(),\n\t\t\t);\n\n\t\tcase \"image\":\n\t\t\treturn z.object({\n\t\t\t\tid: z.string(),\n\t\t\t\tsrc: z.string().optional(),\n\t\t\t\talt: z.string().optional(),\n\t\t\t\twidth: z.number().optional(),\n\t\t\t\theight: z.number().optional(),\n\t\t\t});\n\n\t\tcase \"file\":\n\t\t\treturn z.object({\n\t\t\t\tid: z.string(),\n\t\t\t\tsrc: z.string().optional(),\n\t\t\t\tfilename: z.string().optional(),\n\t\t\t\tmimeType: z.string().optional(),\n\t\t\t\tsize: z.number().optional(),\n\t\t\t});\n\n\t\tcase \"reference\":\n\t\t\treturn z.string(); // Reference ID\n\n\t\tcase \"json\":\n\t\t\treturn z.unknown();\n\n\t\tdefault:\n\t\t\treturn z.unknown();\n\t}\n}\n\n/**\n * Apply validation rules to a schema\n */\nfunction applyValidation(schema: ZodTypeAny, field: Field): ZodTypeAny {\n\tconst validation = field.validation;\n\tif (!validation) return schema;\n\n\t// String validations\n\tif (schema instanceof z.ZodString) {\n\t\tlet strSchema = schema;\n\t\tif (validation.minLength !== undefined) {\n\t\t\tstrSchema = strSchema.min(validation.minLength);\n\t\t}\n\t\tif (validation.maxLength !== undefined) {\n\t\t\tstrSchema = strSchema.max(validation.maxLength);\n\t\t}\n\t\tif (validation.pattern) {\n\t\t\tstrSchema = strSchema.regex(new RegExp(validation.pattern));\n\t\t}\n\t\treturn strSchema;\n\t}\n\n\t// Number validations\n\tif (schema instanceof z.ZodNumber) {\n\t\tlet numSchema = schema;\n\t\tif (validation.min !== undefined) {\n\t\t\tnumSchema = numSchema.min(validation.min);\n\t\t}\n\t\tif (validation.max !== undefined) {\n\t\t\tnumSchema = numSchema.max(validation.max);\n\t\t}\n\t\treturn numSchema;\n\t}\n\n\treturn schema;\n}\n\n/**\n * Schema cache to avoid regenerating schemas on every request\n */\nconst schemaCache = new Map<string, { schema: z.ZodObject<any>; version: string }>();\n\n/**\n * Get or generate a cached schema for a collection\n */\nexport function getCachedSchema(\n\tcollection: CollectionWithFields,\n\tversion?: string,\n): z.ZodObject<any> {\n\tconst cacheKey = collection.slug;\n\tconst cached = schemaCache.get(cacheKey);\n\n\t// If version matches, return cached schema\n\tif (cached && (!version || cached.version === version)) {\n\t\treturn cached.schema;\n\t}\n\n\t// Generate new schema\n\tconst schema = generateZodSchema(collection);\n\n\t// Cache it\n\tschemaCache.set(cacheKey, {\n\t\tschema,\n\t\tversion: version || collection.updatedAt,\n\t});\n\n\treturn schema;\n}\n\n/**\n * Invalidate cached schema for a collection\n */\nexport function invalidateSchemaCache(slug: string): void {\n\tschemaCache.delete(slug);\n}\n\n/**\n * Clear all cached schemas\n */\nexport function clearSchemaCache(): void {\n\tschemaCache.clear();\n}\n\n/**\n * Validate data against a collection's schema\n */\nexport function validateContent(\n\tcollection: CollectionWithFields,\n\tdata: unknown,\n): { success: true; data: unknown } | { success: false; errors: z.ZodError } {\n\tconst schema = getCachedSchema(collection);\n\n\tconst result = schema.safeParse(data);\n\n\tif (result.success) {\n\t\treturn { success: true, data: result.data };\n\t}\n\n\treturn { success: false, errors: result.error };\n}\n\n/**\n * Generate TypeScript interface from field definitions\n * Used by CLI `emdash types` to generate types\n */\nexport function generateTypeScript(collection: CollectionWithFields): string {\n\tconst interfaceName = getInterfaceName(collection);\n\tconst lines: string[] = [];\n\n\tlines.push(`export interface ${interfaceName} {`);\n\tlines.push(` id: string;`);\n\tlines.push(` slug: string | null;`);\n\tlines.push(` status: string;`);\n\n\tfor (const field of collection.fields) {\n\t\tconst tsType = fieldTypeToTypeScript(field);\n\t\tconst optional = field.required ? \"\" : \"?\";\n\t\tlines.push(` ${field.slug}${optional}: ${tsType};`);\n\t}\n\n\tlines.push(` createdAt: Date;`);\n\tlines.push(` updatedAt: Date;`);\n\tlines.push(` publishedAt: Date | null;`);\n\t// Bylines are eagerly loaded by getEmDashCollection/getEmDashEntry\n\tlines.push(` bylines?: ContentBylineCredit[];`);\n\tlines.push(`}`);\n\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Generate a complete types file with module augmentation\n * This produces emdash-env.d.ts content that provides typed query functions\n */\nexport function generateTypesFile(collections: CollectionWithFields[]): string {\n\tconst lines: string[] = [];\n\n\t// Header\n\tlines.push(`// Generated by EmDash on dev server start`);\n\tlines.push(`// Do not edit manually`);\n\tlines.push(``);\n\tlines.push(`/// <reference types=\"emdash/locals\" />`);\n\tlines.push(``);\n\n\t// Check if we need PortableTextBlock import\n\tconst needsPortableText = collections.some((c) =>\n\t\tc.fields.some((f) => f.type === \"portableText\"),\n\t);\n\n\t// Build imports - ContentBylineCredit is always needed for bylines\n\tconst imports = [\"ContentBylineCredit\"];\n\tif (needsPortableText) {\n\t\timports.push(\"PortableTextBlock\");\n\t}\n\tlines.push(`import type { ${imports.join(\", \")} } from \"emdash\";`);\n\tlines.push(``);\n\n\t// Generate individual interfaces\n\tfor (const collection of collections) {\n\t\tlines.push(generateTypeScript(collection));\n\t\tlines.push(``);\n\t}\n\n\t// Generate the Collections interface for module augmentation\n\tlines.push(`declare module \"emdash\" {`);\n\tlines.push(` interface EmDashCollections {`);\n\tfor (const collection of collections) {\n\t\tconst interfaceName = getInterfaceName(collection);\n\t\tlines.push(` ${collection.slug}: ${interfaceName};`);\n\t}\n\tlines.push(` }`);\n\tlines.push(`}`);\n\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Generate schema hash for cache invalidation\n */\nexport async function generateSchemaHash(collections: CollectionWithFields[]): Promise<string> {\n\tconst str = JSON.stringify(\n\t\tcollections.map((c) => ({\n\t\t\tslug: c.slug,\n\t\t\tfields: c.fields.map((f) => ({\n\t\t\t\tslug: f.slug,\n\t\t\t\ttype: f.type,\n\t\t\t\trequired: f.required,\n\t\t\t\tvalidation: f.validation,\n\t\t\t})),\n\t\t})),\n\t);\n\treturn hashString(str);\n}\n\n/**\n * Map field type to TypeScript type\n */\nfunction fieldTypeToTypeScript(field: Field): string {\n\tswitch (field.type) {\n\t\tcase \"string\":\n\t\tcase \"text\":\n\t\tcase \"slug\":\n\t\tcase \"url\":\n\t\tcase \"datetime\":\n\t\t\treturn \"string\";\n\n\t\tcase \"number\":\n\t\tcase \"integer\":\n\t\t\treturn \"number\";\n\n\t\tcase \"boolean\":\n\t\t\treturn \"boolean\";\n\n\t\tcase \"select\":\n\t\t\tconst options = field.validation?.options;\n\t\t\tif (options && options.length > 0) {\n\t\t\t\treturn options.map((o) => `\"${o}\"`).join(\" | \");\n\t\t\t}\n\t\t\treturn \"string\";\n\n\t\tcase \"multiSelect\":\n\t\t\tconst multiOptions = field.validation?.options;\n\t\t\tif (multiOptions && multiOptions.length > 0) {\n\t\t\t\treturn `(${multiOptions.map((o) => `\"${o}\"`).join(\" | \")})[]`;\n\t\t\t}\n\t\t\treturn \"string[]\";\n\n\t\tcase \"portableText\":\n\t\t\treturn \"PortableTextBlock[]\";\n\n\t\tcase \"image\":\n\t\t\treturn \"{ id: string; src?: string; alt?: string; width?: number; height?: number }\";\n\n\t\tcase \"file\":\n\t\t\treturn \"{ id: string; src?: string; filename?: string; mimeType?: string; size?: number }\";\n\n\t\tcase \"reference\":\n\t\t\t// Could be enhanced to include the referenced collection type\n\t\t\treturn \"string\";\n\n\t\tcase \"json\":\n\t\t\treturn \"unknown\";\n\n\t\tdefault:\n\t\t\treturn \"unknown\";\n\t}\n}\n\n/**\n * Convert string to PascalCase (handles slugs, spaces, etc.)\n */\nfunction pascalCase(str: string): string {\n\treturn str\n\t\t.split(PASCAL_CASE_SPLIT_PATTERN)\n\t\t.filter(Boolean)\n\t\t.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n\t\t.join(\"\");\n}\n\n/**\n * Simple singularization - handles common cases\n */\nfunction singularize(str: string): string {\n\tif (str.endsWith(\"ies\")) {\n\t\treturn str.slice(0, -3) + \"y\";\n\t}\n\tif (\n\t\tstr.endsWith(\"es\") &&\n\t\t(str.endsWith(\"sses\") || str.endsWith(\"xes\") || str.endsWith(\"ches\") || str.endsWith(\"shes\"))\n\t) {\n\t\treturn str.slice(0, -2);\n\t}\n\tif (str.endsWith(\"s\") && !str.endsWith(\"ss\")) {\n\t\treturn str.slice(0, -1);\n\t}\n\treturn str;\n}\n\n/**\n * Get the interface name for a collection\n */\nfunction getInterfaceName(collection: CollectionWithFields): string {\n\treturn pascalCase(collection.labelSingular || singularize(collection.slug));\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "emdash",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Astro-native CMS with WordPress migration support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -45,6 +45,12 @@
|
|
|
45
45
|
"default": "./dist/cli/index.mjs"
|
|
46
46
|
},
|
|
47
47
|
"./routes/*": "./src/astro/routes/*",
|
|
48
|
+
"./api/route-utils": "./src/api/route-utils.ts",
|
|
49
|
+
"./api/schemas": "./src/api/schemas/index.ts",
|
|
50
|
+
"./auth/providers/github": "./src/auth/providers/github.ts",
|
|
51
|
+
"./auth/providers/github-admin": "./src/auth/providers/github-admin.tsx",
|
|
52
|
+
"./auth/providers/google": "./src/auth/providers/google.ts",
|
|
53
|
+
"./auth/providers/google-admin": "./src/auth/providers/google-admin.tsx",
|
|
48
54
|
"./db": {
|
|
49
55
|
"types": "./dist/db/index.d.mts",
|
|
50
56
|
"default": "./dist/db/index.mjs"
|
|
@@ -185,9 +191,9 @@
|
|
|
185
191
|
"ulidx": "^2.4.1",
|
|
186
192
|
"upng-js": "^2.1.0",
|
|
187
193
|
"zod": "^4.3.5",
|
|
188
|
-
"@emdash-cms/
|
|
189
|
-
"@emdash-cms/
|
|
190
|
-
"@emdash-cms/gutenberg-to-portable-text": "0.
|
|
194
|
+
"@emdash-cms/admin": "1.0.0",
|
|
195
|
+
"@emdash-cms/auth": "1.0.0",
|
|
196
|
+
"@emdash-cms/gutenberg-to-portable-text": "1.0.0"
|
|
191
197
|
},
|
|
192
198
|
"optionalDependencies": {
|
|
193
199
|
"@libsql/kysely-libsql": "^0.4.0",
|
|
@@ -199,12 +205,19 @@
|
|
|
199
205
|
"@tanstack/react-router": ">=1.100.0",
|
|
200
206
|
"astro": ">=6.0.0-beta.0",
|
|
201
207
|
"react": ">=18.0.0",
|
|
202
|
-
"react-dom": ">=18.0.0"
|
|
208
|
+
"react-dom": ">=18.0.0",
|
|
209
|
+
"@emdash-cms/auth-atproto": "1.0.0"
|
|
210
|
+
},
|
|
211
|
+
"peerDependenciesMeta": {
|
|
212
|
+
"@emdash-cms/auth-atproto": {
|
|
213
|
+
"optional": true
|
|
214
|
+
}
|
|
203
215
|
},
|
|
204
216
|
"devDependencies": {
|
|
205
217
|
"@apidevtools/swagger-parser": "^12.1.0",
|
|
206
218
|
"@arethetypeswrong/cli": "^0.18.2",
|
|
207
219
|
"@types/better-sqlite3": "^7.6.12",
|
|
220
|
+
"@types/react": "19.2.14",
|
|
208
221
|
"@types/pg": "^8.16.0",
|
|
209
222
|
"@types/sanitize-html": "^2.16.0",
|
|
210
223
|
"@types/sax": "^1.2.7",
|
|
@@ -215,7 +228,7 @@
|
|
|
215
228
|
"vite": "^6.0.0",
|
|
216
229
|
"vitest": "^4.0.18",
|
|
217
230
|
"zod-openapi": "^5.4.6",
|
|
218
|
-
"@emdash-cms/blocks": "0.
|
|
231
|
+
"@emdash-cms/blocks": "1.0.0"
|
|
219
232
|
},
|
|
220
233
|
"repository": {
|
|
221
234
|
"type": "git",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth provider storage helper.
|
|
3
|
+
*
|
|
4
|
+
* Gives auth provider routes access to plugin-style storage collections
|
|
5
|
+
* namespaced under `auth:<providerId>`. Reuses the existing `_plugin_storage`
|
|
6
|
+
* table and `PluginStorageRepository` infrastructure.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Kysely } from "kysely";
|
|
10
|
+
|
|
11
|
+
import type { Database } from "../database/types.js";
|
|
12
|
+
import { createStorageAccess } from "../plugins/context.js";
|
|
13
|
+
import type { StorageCollection, StorageCollectionConfig } from "../plugins/types.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get storage collections for an auth provider.
|
|
17
|
+
*
|
|
18
|
+
* Returns a record of `StorageCollection` instances, one per declared
|
|
19
|
+
* collection in the provider's `storage` config. Data is stored in the
|
|
20
|
+
* shared `_plugin_storage` table under the namespace `auth:<providerId>`.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const storage = getAuthProviderStorage(emdash.db, "atproto", {
|
|
25
|
+
* states: { indexes: [] },
|
|
26
|
+
* sessions: { indexes: [] },
|
|
27
|
+
* });
|
|
28
|
+
* const session = await storage.sessions.get(sessionId);
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function getAuthProviderStorage(
|
|
32
|
+
db: Kysely<Database>,
|
|
33
|
+
providerId: string,
|
|
34
|
+
storageConfig: Record<string, StorageCollectionConfig>,
|
|
35
|
+
): Record<string, StorageCollection> {
|
|
36
|
+
return createStorageAccess(db, `auth:${providerId}`, storageConfig);
|
|
37
|
+
}
|
package/src/api/error.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* `new Response(JSON.stringify({ error: ... }), ...)` patterns.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { InvalidCursorError } from "../database/repositories/types.js";
|
|
8
9
|
import { mapErrorStatus } from "./errors.js";
|
|
9
10
|
import type { ApiResult } from "./types.js";
|
|
10
11
|
|
|
@@ -54,6 +55,11 @@ export function handleError(
|
|
|
54
55
|
fallbackMessage: string,
|
|
55
56
|
fallbackCode: string,
|
|
56
57
|
): Response {
|
|
58
|
+
// Bubble malformed-cursor errors as a structured 400 instead of a
|
|
59
|
+
// generic 500.
|
|
60
|
+
if (error instanceof InvalidCursorError) {
|
|
61
|
+
return apiError("INVALID_CURSOR", error.message, 400);
|
|
62
|
+
}
|
|
57
63
|
console.error(`[${fallbackCode}]`, error);
|
|
58
64
|
return apiError(fallbackCode, fallbackMessage, 500);
|
|
59
65
|
}
|
package/src/api/errors.ts
CHANGED
|
@@ -12,7 +12,9 @@ export const ErrorCode = {
|
|
|
12
12
|
VALIDATION_ERROR: "VALIDATION_ERROR",
|
|
13
13
|
INVALID_INPUT: "INVALID_INPUT",
|
|
14
14
|
INVALID_JSON: "INVALID_JSON",
|
|
15
|
+
INVALID_CURSOR: "INVALID_CURSOR",
|
|
15
16
|
CONFLICT: "CONFLICT",
|
|
17
|
+
SLUG_CONFLICT: "SLUG_CONFLICT",
|
|
16
18
|
NOT_CONFIGURED: "NOT_CONFIGURED",
|
|
17
19
|
UNAUTHORIZED: "UNAUTHORIZED",
|
|
18
20
|
FORBIDDEN: "FORBIDDEN",
|
|
@@ -152,6 +154,8 @@ export const ErrorCode = {
|
|
|
152
154
|
INVALID_CODE: "INVALID_CODE",
|
|
153
155
|
EXPIRED_CODE: "EXPIRED_CODE",
|
|
154
156
|
INSUFFICIENT_ROLE: "INSUFFICIENT_ROLE",
|
|
157
|
+
INSUFFICIENT_SCOPE: "INSUFFICIENT_SCOPE",
|
|
158
|
+
INSUFFICIENT_PERMISSIONS: "INSUFFICIENT_PERMISSIONS",
|
|
155
159
|
TOKEN_EXCHANGE_ERROR: "TOKEN_EXCHANGE_ERROR",
|
|
156
160
|
TOKEN_REFRESH_ERROR: "TOKEN_REFRESH_ERROR",
|
|
157
161
|
TOKEN_REVOKE_ERROR: "TOKEN_REVOKE_ERROR",
|
|
@@ -335,6 +339,7 @@ export function mapErrorStatus(code: string | undefined): number {
|
|
|
335
339
|
case ErrorCode.VALIDATION_ERROR:
|
|
336
340
|
case ErrorCode.INVALID_INPUT:
|
|
337
341
|
case ErrorCode.INVALID_JSON:
|
|
342
|
+
case ErrorCode.INVALID_CURSOR:
|
|
338
343
|
case ErrorCode.MISSING_PARAM:
|
|
339
344
|
case ErrorCode.INVALID_REQUEST:
|
|
340
345
|
case ErrorCode.NOT_SUPPORTED:
|
|
@@ -373,6 +378,8 @@ export function mapErrorStatus(code: string | undefined): number {
|
|
|
373
378
|
case ErrorCode.COMMENT_REJECTED:
|
|
374
379
|
case ErrorCode.DOMAIN_NOT_ALLOWED:
|
|
375
380
|
case ErrorCode.INSUFFICIENT_ROLE:
|
|
381
|
+
case ErrorCode.INSUFFICIENT_SCOPE:
|
|
382
|
+
case ErrorCode.INSUFFICIENT_PERMISSIONS:
|
|
376
383
|
case ErrorCode.CAPABILITY_ESCALATION:
|
|
377
384
|
case ErrorCode.ROUTE_VISIBILITY_ESCALATION:
|
|
378
385
|
case ErrorCode.AUDIT_FAILED:
|
|
@@ -388,6 +395,7 @@ export function mapErrorStatus(code: string | undefined): number {
|
|
|
388
395
|
|
|
389
396
|
// 409 Conflict
|
|
390
397
|
case ErrorCode.CONFLICT:
|
|
398
|
+
case ErrorCode.SLUG_CONFLICT:
|
|
391
399
|
case ErrorCode.COLLECTION_EXISTS:
|
|
392
400
|
case ErrorCode.FIELD_EXISTS:
|
|
393
401
|
case ErrorCode.CREDENTIAL_EXISTS:
|
|
@@ -8,6 +8,7 @@ import type { Kysely } from "kysely";
|
|
|
8
8
|
|
|
9
9
|
import { CommentRepository } from "../../database/repositories/comment.js";
|
|
10
10
|
import type { Comment, CommentStatus, PublicComment } from "../../database/repositories/comment.js";
|
|
11
|
+
import { InvalidCursorError } from "../../database/repositories/types.js";
|
|
11
12
|
import type { Database } from "../../database/types.js";
|
|
12
13
|
import type { ApiResult } from "../types.js";
|
|
13
14
|
|
|
@@ -60,6 +61,12 @@ export async function handleCommentList(
|
|
|
60
61
|
},
|
|
61
62
|
};
|
|
62
63
|
} catch (error) {
|
|
64
|
+
if (error instanceof InvalidCursorError) {
|
|
65
|
+
return {
|
|
66
|
+
success: false,
|
|
67
|
+
error: { code: "INVALID_CURSOR", message: error.message },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
63
70
|
console.error("Comment list error:", error);
|
|
64
71
|
return {
|
|
65
72
|
success: false,
|
|
@@ -104,6 +111,12 @@ export async function handleCommentInbox(
|
|
|
104
111
|
},
|
|
105
112
|
};
|
|
106
113
|
} catch (error) {
|
|
114
|
+
if (error instanceof InvalidCursorError) {
|
|
115
|
+
return {
|
|
116
|
+
success: false,
|
|
117
|
+
error: { code: "INVALID_CURSOR", message: error.message },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
107
120
|
console.error("Comment inbox error:", error);
|
|
108
121
|
return {
|
|
109
122
|
success: false,
|