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
|
@@ -122,16 +122,27 @@ export async function handleTaxonomyList(
|
|
|
122
122
|
db: Kysely<Database>,
|
|
123
123
|
): Promise<ApiResult<TaxonomyListResponse>> {
|
|
124
124
|
try {
|
|
125
|
-
const rows = await
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
125
|
+
const [rows, collectionRows] = await Promise.all([
|
|
126
|
+
db.selectFrom("_emdash_taxonomy_defs").selectAll().execute(),
|
|
127
|
+
db.selectFrom("_emdash_collections").select("slug").execute(),
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
// Filter orphan collection references on read so the response stays
|
|
131
|
+
// consistent with `schema_list_collections`. Storage is untouched —
|
|
132
|
+
// re-creating the collection re-links automatically.
|
|
133
|
+
const realCollections = new Set(collectionRows.map((r) => r.slug));
|
|
134
|
+
|
|
135
|
+
const taxonomies: TaxonomyDef[] = rows.map((row) => {
|
|
136
|
+
const stored: string[] = row.collections ? JSON.parse(row.collections) : [];
|
|
137
|
+
return {
|
|
138
|
+
id: row.id,
|
|
139
|
+
name: row.name,
|
|
140
|
+
label: row.label,
|
|
141
|
+
labelSingular: row.label_singular ?? undefined,
|
|
142
|
+
hierarchical: row.hierarchical === 1,
|
|
143
|
+
collections: stored.filter((slug) => realCollections.has(slug)),
|
|
144
|
+
};
|
|
145
|
+
});
|
|
135
146
|
|
|
136
147
|
return { success: true, data: { taxonomies } };
|
|
137
148
|
} catch {
|
|
@@ -290,6 +301,84 @@ export async function handleTermList(
|
|
|
290
301
|
}
|
|
291
302
|
}
|
|
292
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Validate a parent term reference for create/update.
|
|
306
|
+
*
|
|
307
|
+
* Returns `null` on success or a structured error message that callers
|
|
308
|
+
* wrap in their own ApiResult.
|
|
309
|
+
*
|
|
310
|
+
* - `parentId === undefined` -> no-op (no parent change requested).
|
|
311
|
+
* - `parentId === null` -> caller intends to detach; no-op here.
|
|
312
|
+
* - parent must exist (FK exists -> term row not soft-deleted).
|
|
313
|
+
* - parent must live in the same taxonomy.
|
|
314
|
+
* - if `termId` is provided (update path), reject `parentId === termId`
|
|
315
|
+
* (self-parent) and walk up the parent chain to detect cycles.
|
|
316
|
+
*/
|
|
317
|
+
async function validateParentTerm(
|
|
318
|
+
repo: TaxonomyRepository,
|
|
319
|
+
taxonomyName: string,
|
|
320
|
+
termId: string | undefined,
|
|
321
|
+
parentId: string | null | undefined,
|
|
322
|
+
): Promise<{ code: "VALIDATION_ERROR"; message: string } | null> {
|
|
323
|
+
if (parentId === undefined || parentId === null) return null;
|
|
324
|
+
|
|
325
|
+
if (termId !== undefined && parentId === termId) {
|
|
326
|
+
return {
|
|
327
|
+
code: "VALIDATION_ERROR",
|
|
328
|
+
message: "A term cannot be its own parent",
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const parent = await repo.findById(parentId);
|
|
333
|
+
if (!parent) {
|
|
334
|
+
return {
|
|
335
|
+
code: "VALIDATION_ERROR",
|
|
336
|
+
message: `Parent term '${parentId}' not found`,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
if (parent.name !== taxonomyName) {
|
|
340
|
+
return {
|
|
341
|
+
code: "VALIDATION_ERROR",
|
|
342
|
+
message: `Parent term '${parentId}' belongs to taxonomy '${parent.name}', not '${taxonomyName}'`,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Walk up the parent chain. Two checks fold into one walk:
|
|
347
|
+
// - Cycle detection (only on update — a non-existent term-being-
|
|
348
|
+
// created can't be its own ancestor): if the walk revisits termId
|
|
349
|
+
// the proposed parent makes the term a descendant of itself.
|
|
350
|
+
// - Depth bound: refuse to extend a chain past MAX_DEPTH ancestors.
|
|
351
|
+
// Runs on both create and update so a malicious or buggy caller
|
|
352
|
+
// can't grow the tree without limit.
|
|
353
|
+
//
|
|
354
|
+
// The depth-exceeded error fires only when we hit the limit AND there
|
|
355
|
+
// was still chain to walk — a legitimate chain of exactly MAX_DEPTH
|
|
356
|
+
// ancestors exits with `cursor === null` and is accepted.
|
|
357
|
+
const MAX_DEPTH = 100;
|
|
358
|
+
let cursor: string | null = parent.parentId;
|
|
359
|
+
let steps = 0;
|
|
360
|
+
while (cursor !== null && steps < MAX_DEPTH) {
|
|
361
|
+
if (termId !== undefined && cursor === termId) {
|
|
362
|
+
return {
|
|
363
|
+
code: "VALIDATION_ERROR",
|
|
364
|
+
message: "Cycle detected: cannot make a descendant the parent",
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
const next = await repo.findById(cursor);
|
|
368
|
+
if (!next) break;
|
|
369
|
+
cursor = next.parentId;
|
|
370
|
+
steps++;
|
|
371
|
+
}
|
|
372
|
+
if (cursor !== null && steps >= MAX_DEPTH) {
|
|
373
|
+
return {
|
|
374
|
+
code: "VALIDATION_ERROR",
|
|
375
|
+
message: "Parent chain exceeds maximum depth",
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
293
382
|
/**
|
|
294
383
|
* Create a new term in a taxonomy
|
|
295
384
|
*/
|
|
@@ -304,6 +393,10 @@ export async function handleTermCreate(
|
|
|
304
393
|
|
|
305
394
|
const repo = new TaxonomyRepository(db);
|
|
306
395
|
|
|
396
|
+
// Coerce empty-string parentId to undefined (treat as "no parent").
|
|
397
|
+
const parentId =
|
|
398
|
+
input.parentId === "" || input.parentId === undefined ? undefined : input.parentId;
|
|
399
|
+
|
|
307
400
|
// Check for slug conflict
|
|
308
401
|
const existing = await repo.findBySlug(taxonomyName, input.slug);
|
|
309
402
|
if (existing) {
|
|
@@ -316,11 +409,18 @@ export async function handleTermCreate(
|
|
|
316
409
|
};
|
|
317
410
|
}
|
|
318
411
|
|
|
412
|
+
// Validate parentId: must exist AND belong to the same taxonomy.
|
|
413
|
+
// (Cycle check is N/A on create — the term doesn't exist yet.)
|
|
414
|
+
const parentError = await validateParentTerm(repo, taxonomyName, undefined, parentId);
|
|
415
|
+
if (parentError) {
|
|
416
|
+
return { success: false, error: parentError };
|
|
417
|
+
}
|
|
418
|
+
|
|
319
419
|
const term = await repo.create({
|
|
320
420
|
name: taxonomyName,
|
|
321
421
|
slug: input.slug,
|
|
322
422
|
label: input.label,
|
|
323
|
-
parentId:
|
|
423
|
+
parentId: parentId ?? undefined,
|
|
324
424
|
data: input.description ? { description: input.description } : undefined,
|
|
325
425
|
});
|
|
326
426
|
|
|
@@ -426,24 +526,36 @@ export async function handleTermUpdate(
|
|
|
426
526
|
};
|
|
427
527
|
}
|
|
428
528
|
|
|
529
|
+
// Coerce empty-string slug/parentId to undefined (treat as "no change").
|
|
530
|
+
// `null` parentId is a valid request meaning "detach from parent".
|
|
531
|
+
const newSlug = input.slug === "" || input.slug === undefined ? undefined : input.slug;
|
|
532
|
+
const newParentId =
|
|
533
|
+
input.parentId === "" || input.parentId === undefined ? undefined : input.parentId;
|
|
534
|
+
|
|
429
535
|
// Check if new slug conflicts
|
|
430
|
-
if (
|
|
431
|
-
const existing = await repo.findBySlug(taxonomyName,
|
|
536
|
+
if (newSlug !== undefined && newSlug !== termSlug) {
|
|
537
|
+
const existing = await repo.findBySlug(taxonomyName, newSlug);
|
|
432
538
|
if (existing && existing.id !== term.id) {
|
|
433
539
|
return {
|
|
434
540
|
success: false,
|
|
435
541
|
error: {
|
|
436
542
|
code: "CONFLICT",
|
|
437
|
-
message: `Term with slug '${
|
|
543
|
+
message: `Term with slug '${newSlug}' already exists in taxonomy '${taxonomyName}'`,
|
|
438
544
|
},
|
|
439
545
|
};
|
|
440
546
|
}
|
|
441
547
|
}
|
|
442
548
|
|
|
549
|
+
// Validate parentId: existence, same-taxonomy, no self-parent, no cycle.
|
|
550
|
+
const parentError = await validateParentTerm(repo, taxonomyName, term.id, newParentId);
|
|
551
|
+
if (parentError) {
|
|
552
|
+
return { success: false, error: parentError };
|
|
553
|
+
}
|
|
554
|
+
|
|
443
555
|
const updated = await repo.update(term.id, {
|
|
444
|
-
slug:
|
|
556
|
+
slug: newSlug,
|
|
445
557
|
label: input.label,
|
|
446
|
-
parentId:
|
|
558
|
+
parentId: newParentId,
|
|
447
559
|
data: input.description !== undefined ? { description: input.description } : undefined,
|
|
448
560
|
});
|
|
449
561
|
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field-level validation for content create / update.
|
|
3
|
+
*
|
|
4
|
+
* Wires the existing `generateZodSchema()` pipeline (`schema/zod-generator.ts`)
|
|
5
|
+
* into the handler boundary so REST and MCP both get the same enforcement:
|
|
6
|
+
*
|
|
7
|
+
* - required fields must be present and non-empty
|
|
8
|
+
* - select / multiSelect values must match the configured options
|
|
9
|
+
* - reference fields must resolve to a real, non-trashed target
|
|
10
|
+
*
|
|
11
|
+
* Errors surface as `{ code: "VALIDATION_ERROR", message }` with all
|
|
12
|
+
* offending fields listed in one message so callers can fix everything in
|
|
13
|
+
* a single round trip.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { sql, type Kysely } from "kysely";
|
|
17
|
+
|
|
18
|
+
import type { Database } from "../../database/types.js";
|
|
19
|
+
import { validateIdentifier } from "../../database/validate.js";
|
|
20
|
+
import { SchemaRegistry } from "../../schema/registry.js";
|
|
21
|
+
import type { Field } from "../../schema/types.js";
|
|
22
|
+
import { generateZodSchema } from "../../schema/zod-generator.js";
|
|
23
|
+
import { chunks, SQL_BATCH_SIZE } from "../../utils/chunks.js";
|
|
24
|
+
import { isMissingTableError } from "../../utils/db-errors.js";
|
|
25
|
+
|
|
26
|
+
type ValidationResult =
|
|
27
|
+
| { ok: true }
|
|
28
|
+
| { ok: false; error: { code: "VALIDATION_ERROR" | "COLLECTION_NOT_FOUND"; message: string } };
|
|
29
|
+
|
|
30
|
+
/** Treat `undefined`, `null`, and `""` as "not set". */
|
|
31
|
+
function isMissing(value: unknown): boolean {
|
|
32
|
+
return value === undefined || value === null || value === "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the target collection slug for a reference field.
|
|
37
|
+
*
|
|
38
|
+
* Schema-defined reference fields (the static `reference()` factory in
|
|
39
|
+
* `fields/reference.ts`) put the target in `options.collection`. The MCP
|
|
40
|
+
* `schema_create_field` tool also puts it there. Tests and some admin paths
|
|
41
|
+
* stash it inside `validation.collection` directly; we accept both.
|
|
42
|
+
*/
|
|
43
|
+
function getReferenceTargetCollection(field: Field): string | undefined {
|
|
44
|
+
const fromOptions = field.options?.collection;
|
|
45
|
+
if (typeof fromOptions === "string" && fromOptions.length > 0) return fromOptions;
|
|
46
|
+
const validation = field.validation;
|
|
47
|
+
if (validation && "collection" in validation) {
|
|
48
|
+
const fromValidation: unknown = (validation as { collection?: unknown }).collection;
|
|
49
|
+
if (typeof fromValidation === "string" && fromValidation.length > 0) return fromValidation;
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Format a Zod issue path into a human-readable field reference, e.g.
|
|
56
|
+
* `tags`, `tags.1`, `image.alt`.
|
|
57
|
+
*/
|
|
58
|
+
function formatIssuePath(path: ReadonlyArray<PropertyKey>): string {
|
|
59
|
+
if (path.length === 0) return "(root)";
|
|
60
|
+
return path.map((seg) => String(seg)).join(".");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validate `data` against the collection's field definitions.
|
|
65
|
+
*
|
|
66
|
+
* `partial: true` switches Zod into partial mode so updates can include
|
|
67
|
+
* only the fields being changed without tripping required-field errors on
|
|
68
|
+
* fields the caller didn't touch. Required fields that ARE present in
|
|
69
|
+
* partial-mode data still get the empty-string check below.
|
|
70
|
+
*/
|
|
71
|
+
export async function validateContentData(
|
|
72
|
+
db: Kysely<Database>,
|
|
73
|
+
collection: string,
|
|
74
|
+
data: Record<string, unknown>,
|
|
75
|
+
options: { partial?: boolean } = {},
|
|
76
|
+
): Promise<ValidationResult> {
|
|
77
|
+
const registry = new SchemaRegistry(db);
|
|
78
|
+
const collectionWithFields = await registry.getCollectionWithFields(collection);
|
|
79
|
+
if (!collectionWithFields) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
error: {
|
|
83
|
+
code: "COLLECTION_NOT_FOUND",
|
|
84
|
+
message: `Collection '${collection}' not found`,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const issues: string[] = [];
|
|
90
|
+
|
|
91
|
+
// Detect unknown keys explicitly so callers get a useful error rather
|
|
92
|
+
// than silently dropped data. Leading-underscore keys (e.g. `_slug`,
|
|
93
|
+
// `_rev`) are reserved for internal handler/runtime use and aren't real
|
|
94
|
+
// fields; skip them.
|
|
95
|
+
const knownFields = new Set(collectionWithFields.fields.map((f) => f.slug));
|
|
96
|
+
for (const key of Object.keys(data)) {
|
|
97
|
+
if (key.startsWith("_")) continue;
|
|
98
|
+
if (!knownFields.has(key)) {
|
|
99
|
+
issues.push(`${key}: unknown field on collection '${collection}'`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Zod handles type, enum, length and missing-required (in non-partial
|
|
104
|
+
// mode) checks. Empty-string handling for required string fields is
|
|
105
|
+
// done as a separate pass below since Zod's `z.string()` accepts "".
|
|
106
|
+
const baseSchema = generateZodSchema(collectionWithFields);
|
|
107
|
+
const schema = options.partial ? baseSchema.partial() : baseSchema;
|
|
108
|
+
const parsed = schema.safeParse(data);
|
|
109
|
+
if (!parsed.success) {
|
|
110
|
+
for (const issue of parsed.error.issues) {
|
|
111
|
+
issues.push(`${formatIssuePath(issue.path)}: ${issue.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Empty-string-on-required check. In create mode (partial=false) Zod
|
|
116
|
+
// already catches missing/null for required fields, but `z.string()`
|
|
117
|
+
// happily accepts "". In update mode (partial=true) the field is only
|
|
118
|
+
// checked if it's present in `data`.
|
|
119
|
+
for (const field of collectionWithFields.fields) {
|
|
120
|
+
if (!field.required) continue;
|
|
121
|
+
const present = Object.hasOwn(data, field.slug);
|
|
122
|
+
if (options.partial && !present) continue;
|
|
123
|
+
if (data[field.slug] === "") {
|
|
124
|
+
issues.push(`${field.slug}: required (empty value not allowed)`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Reference target existence. Only check fields that:
|
|
129
|
+
// - have a value (non-missing) in `data`
|
|
130
|
+
// - have a resolvable target collection
|
|
131
|
+
// - in partial mode: are present in `data`
|
|
132
|
+
// Batch one IN-query per target collection to keep round-trips low.
|
|
133
|
+
const refsByTarget = new Map<string, { field: string; id: string }[]>();
|
|
134
|
+
for (const field of collectionWithFields.fields) {
|
|
135
|
+
if (field.type !== "reference") continue;
|
|
136
|
+
if (options.partial && !Object.hasOwn(data, field.slug)) continue;
|
|
137
|
+
const value = data[field.slug];
|
|
138
|
+
if (isMissing(value)) continue;
|
|
139
|
+
if (typeof value !== "string") continue; // Zod will have flagged this already
|
|
140
|
+
const target = getReferenceTargetCollection(field);
|
|
141
|
+
if (!target) continue;
|
|
142
|
+
const list = refsByTarget.get(target) ?? [];
|
|
143
|
+
list.push({ field: field.slug, id: value });
|
|
144
|
+
refsByTarget.set(target, list);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (const [target, refs] of refsByTarget) {
|
|
148
|
+
// Validate the target collection slug before interpolating into raw
|
|
149
|
+
// SQL — defense-in-depth even though slugs are already validated at
|
|
150
|
+
// schema-create time.
|
|
151
|
+
try {
|
|
152
|
+
validateIdentifier(target, "reference target collection");
|
|
153
|
+
} catch {
|
|
154
|
+
for (const ref of refs) {
|
|
155
|
+
issues.push(`${ref.field}: invalid reference target collection '${target}'`);
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const ids = [...new Set(refs.map((r) => r.id))];
|
|
161
|
+
const tableName = `ec_${target}`;
|
|
162
|
+
|
|
163
|
+
// Chunk the IN clause to stay below D1's bind-parameter limit. One
|
|
164
|
+
// reference per request is the common case today; chunking makes the
|
|
165
|
+
// helper safe if a future multiSelect-of-references is added.
|
|
166
|
+
const found = new Set<string>();
|
|
167
|
+
let targetTableMissing = false;
|
|
168
|
+
for (const idChunk of chunks(ids, SQL_BATCH_SIZE)) {
|
|
169
|
+
try {
|
|
170
|
+
const rows = await sql<{ id: string }>`
|
|
171
|
+
SELECT id FROM ${sql.ref(tableName)}
|
|
172
|
+
WHERE id IN (${sql.join(idChunk)})
|
|
173
|
+
AND deleted_at IS NULL
|
|
174
|
+
`.execute(db);
|
|
175
|
+
for (const row of rows.rows) {
|
|
176
|
+
found.add(row.id);
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
// Missing table = the target collection table doesn't exist
|
|
180
|
+
// (orphan reference). Treat all those references as missing.
|
|
181
|
+
// Any other DB error (permissions, connection, syntax) must
|
|
182
|
+
// propagate — silently dropping data integrity errors as
|
|
183
|
+
// "not found" is exactly the bug F5 fixes.
|
|
184
|
+
if (isMissingTableError(error)) {
|
|
185
|
+
targetTableMissing = true;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (targetTableMissing) {
|
|
192
|
+
for (const ref of refs) {
|
|
193
|
+
issues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);
|
|
194
|
+
}
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
for (const ref of refs) {
|
|
198
|
+
if (!found.has(ref.id)) {
|
|
199
|
+
issues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (issues.length === 0) return { ok: true };
|
|
205
|
+
return {
|
|
206
|
+
ok: false,
|
|
207
|
+
error: {
|
|
208
|
+
code: "VALIDATION_ERROR",
|
|
209
|
+
message: issues.join("; "),
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
@@ -122,6 +122,7 @@ import {
|
|
|
122
122
|
reorderWidgetsBody,
|
|
123
123
|
updateWidgetBody,
|
|
124
124
|
widgetAreaSchema,
|
|
125
|
+
widgetAreaWithWidgetsAndCountSchema,
|
|
125
126
|
widgetAreaWithWidgetsSchema,
|
|
126
127
|
widgetSchema,
|
|
127
128
|
} from "../schemas/widgets.js";
|
|
@@ -1581,7 +1582,9 @@ const widgetPaths = {
|
|
|
1581
1582
|
description: "Widget area list",
|
|
1582
1583
|
content: {
|
|
1583
1584
|
[JSON_CONTENT]: {
|
|
1584
|
-
schema: successEnvelope(
|
|
1585
|
+
schema: successEnvelope(
|
|
1586
|
+
z.object({ items: z.array(widgetAreaWithWidgetsAndCountSchema) }),
|
|
1587
|
+
),
|
|
1585
1588
|
},
|
|
1586
1589
|
},
|
|
1587
1590
|
},
|
package/src/api/public-url.ts
CHANGED
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
* Workers-safe: no Node.js imports.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/** Minimal config shape — avoids importing the full EmDashConfig type tree. */
|
|
13
|
+
interface SiteUrlConfig {
|
|
14
|
+
siteUrl?: string;
|
|
15
|
+
}
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
18
|
* Resolve siteUrl from runtime environment variables.
|
|
@@ -67,7 +70,7 @@ function getEnvSiteUrl(): string | undefined {
|
|
|
67
70
|
* @param config The EmDash config (from `locals.emdash?.config`)
|
|
68
71
|
* @returns Origin string, e.g. `"https://mysite.example.com"`
|
|
69
72
|
*/
|
|
70
|
-
export function getPublicOrigin(url: URL, config?:
|
|
73
|
+
export function getPublicOrigin(url: URL, config?: SiteUrlConfig): string {
|
|
71
74
|
return config?.siteUrl || getEnvSiteUrl() || url.origin;
|
|
72
75
|
}
|
|
73
76
|
|
|
@@ -79,6 +82,6 @@ export function getPublicOrigin(url: URL, config?: EmDashConfig): string {
|
|
|
79
82
|
* @param path Path to append (must start with `/`)
|
|
80
83
|
* @returns Full URL string, e.g. `"https://mysite.example.com/_emdash/admin/login"`
|
|
81
84
|
*/
|
|
82
|
-
export function getPublicUrl(url: URL, config:
|
|
85
|
+
export function getPublicUrl(url: URL, config: SiteUrlConfig | undefined, path: string): string {
|
|
83
86
|
return `${getPublicOrigin(url, config)}${path}`;
|
|
84
87
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API route utilities for auth provider routes.
|
|
3
|
+
*
|
|
4
|
+
* This module re-exports the utilities that auth provider route handlers
|
|
5
|
+
* need from core. Auth providers (plugins) import these via `emdash/api/route-utils`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { apiError, apiSuccess, handleError } from "./error.js";
|
|
9
|
+
export { parseBody, parseQuery, isParseError } from "./parse.js";
|
|
10
|
+
export type { ParseResult } from "./parse.js";
|
|
11
|
+
export { finalizeSetup } from "./setup-complete.js";
|
|
12
|
+
export { OptionsRepository } from "../database/repositories/options.js";
|
|
13
|
+
export { getAuthProviderStorage } from "./auth-storage.js";
|
|
14
|
+
export { getPublicOrigin } from "./public-url.js";
|
|
@@ -22,7 +22,7 @@ export const roleLevel = z.coerce
|
|
|
22
22
|
/** Pagination query params — cursor-based */
|
|
23
23
|
export const cursorPaginationQuery = z
|
|
24
24
|
.object({
|
|
25
|
-
cursor: z.string().optional().meta({ description: "Opaque cursor for pagination" }),
|
|
25
|
+
cursor: z.string().max(2048).optional().meta({ description: "Opaque cursor for pagination" }),
|
|
26
26
|
limit: z.coerce.number().int().min(1).max(100).optional().default(50).meta({
|
|
27
27
|
description: "Maximum number of items to return (1-100, default 50)",
|
|
28
28
|
}),
|
|
@@ -27,6 +27,11 @@ export const contentListQuery = cursorPaginationQuery
|
|
|
27
27
|
})
|
|
28
28
|
.meta({ id: "ContentListQuery" });
|
|
29
29
|
|
|
30
|
+
/** ISO 8601 datetime for `publishedAt` / `createdAt`. Routes gate writes behind `content:publish_any`. */
|
|
31
|
+
const contentDateOverride = z.iso
|
|
32
|
+
.datetime({ offset: true, message: "must be an ISO 8601 datetime" })
|
|
33
|
+
.nullish();
|
|
34
|
+
|
|
30
35
|
export const contentCreateBody = z
|
|
31
36
|
.object({
|
|
32
37
|
data: z.record(z.string(), z.unknown()),
|
|
@@ -36,6 +41,8 @@ export const contentCreateBody = z
|
|
|
36
41
|
locale: localeCode.optional(),
|
|
37
42
|
translationOf: z.string().optional(),
|
|
38
43
|
seo: contentSeoInput.optional(),
|
|
44
|
+
publishedAt: contentDateOverride,
|
|
45
|
+
createdAt: contentDateOverride,
|
|
39
46
|
})
|
|
40
47
|
.meta({ id: "ContentCreateBody" });
|
|
41
48
|
|
|
@@ -52,6 +59,7 @@ export const contentUpdateBody = z
|
|
|
52
59
|
.meta({ description: "Opaque revision token for optimistic concurrency" }),
|
|
53
60
|
skipRevision: z.boolean().optional(),
|
|
54
61
|
seo: contentSeoInput.optional(),
|
|
62
|
+
publishedAt: contentDateOverride,
|
|
55
63
|
})
|
|
56
64
|
.meta({ id: "ContentUpdateBody" });
|
|
57
65
|
|
package/src/api/schemas/setup.ts
CHANGED
|
@@ -35,3 +35,11 @@ export const setupAdminBody = z.object({
|
|
|
35
35
|
export const setupAdminVerifyBody = z.object({
|
|
36
36
|
credential: registrationCredential,
|
|
37
37
|
});
|
|
38
|
+
|
|
39
|
+
export const atprotoLoginBody = z.object({
|
|
40
|
+
handle: z.string().trim().min(1),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const setupAtprotoAdminBody = z.object({
|
|
44
|
+
handle: z.string().trim().min(1),
|
|
45
|
+
});
|
|
@@ -60,16 +60,12 @@ export const widgetAreaSchema = z
|
|
|
60
60
|
export const widgetSchema = z
|
|
61
61
|
.object({
|
|
62
62
|
id: z.string(),
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
component_props: z.string().nullable(),
|
|
70
|
-
sort_order: z.number().int(),
|
|
71
|
-
created_at: z.string(),
|
|
72
|
-
updated_at: z.string(),
|
|
63
|
+
type: widgetType,
|
|
64
|
+
title: z.string().optional(),
|
|
65
|
+
content: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
66
|
+
menuName: z.string().optional(),
|
|
67
|
+
componentId: z.string().optional(),
|
|
68
|
+
componentProps: z.record(z.string(), z.unknown()).optional(),
|
|
73
69
|
})
|
|
74
70
|
.meta({ id: "Widget" });
|
|
75
71
|
|
|
@@ -78,3 +74,9 @@ export const widgetAreaWithWidgetsSchema = widgetAreaSchema
|
|
|
78
74
|
widgets: z.array(widgetSchema),
|
|
79
75
|
})
|
|
80
76
|
.meta({ id: "WidgetAreaWithWidgets" });
|
|
77
|
+
|
|
78
|
+
export const widgetAreaWithWidgetsAndCountSchema = widgetAreaWithWidgetsSchema
|
|
79
|
+
.extend({
|
|
80
|
+
widgetCount: z.number().int(),
|
|
81
|
+
})
|
|
82
|
+
.meta({ id: "WidgetAreaWithWidgetsAndCount" });
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared setup completion logic.
|
|
3
|
+
*
|
|
4
|
+
* Called by OAuth callbacks and the passkey verify step when the first user
|
|
5
|
+
* is created during setup. Persists site title/tagline from setup state
|
|
6
|
+
* and marks setup as complete.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Kysely } from "kysely";
|
|
10
|
+
|
|
11
|
+
import { OptionsRepository } from "../database/repositories/options.js";
|
|
12
|
+
import type { Database } from "../database/types.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Finalize setup after the first admin user is created.
|
|
16
|
+
*
|
|
17
|
+
* Reads the setup_state option (written by the setup wizard's step 1),
|
|
18
|
+
* persists site_title and site_tagline, then marks setup complete.
|
|
19
|
+
*
|
|
20
|
+
* Safe to call multiple times — checks setup_complete first and no-ops
|
|
21
|
+
* if already done.
|
|
22
|
+
*/
|
|
23
|
+
export async function finalizeSetup(db: Kysely<Database>): Promise<void> {
|
|
24
|
+
const options = new OptionsRepository(db);
|
|
25
|
+
|
|
26
|
+
const setupComplete = await options.get("emdash:setup_complete");
|
|
27
|
+
if (setupComplete === true || setupComplete === "true") return;
|
|
28
|
+
|
|
29
|
+
// Persist site title/tagline from setup state (stored in step 1)
|
|
30
|
+
const setupState = await options.get<Record<string, unknown>>("emdash:setup_state");
|
|
31
|
+
if (setupState?.title && typeof setupState.title === "string") {
|
|
32
|
+
await options.set("emdash:site_title", setupState.title);
|
|
33
|
+
}
|
|
34
|
+
if (setupState?.tagline && typeof setupState.tagline === "string") {
|
|
35
|
+
await options.set("emdash:site_tagline", setupState.tagline);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await options.set("emdash:setup_complete", true);
|
|
39
|
+
await options.delete("emdash:setup_state");
|
|
40
|
+
}
|
|
@@ -30,6 +30,7 @@ const ALL_GOOGLE_SUBSETS = [
|
|
|
30
30
|
"cyrillic-ext",
|
|
31
31
|
"devanagari",
|
|
32
32
|
"ethiopic",
|
|
33
|
+
"farsi",
|
|
33
34
|
"georgian",
|
|
34
35
|
"greek",
|
|
35
36
|
"greek-ext",
|
|
@@ -57,7 +58,7 @@ const ALL_GOOGLE_SUBSETS = [
|
|
|
57
58
|
];
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
|
-
* Known Noto Sans script families on Google Fonts.
|
|
61
|
+
* Known Noto Sans and Sans script families on Google Fonts.
|
|
61
62
|
* Maps user-friendly script names to Google Fonts family names.
|
|
62
63
|
*/
|
|
63
64
|
const NOTO_SCRIPT_FAMILIES: Record<string, string> = {
|
|
@@ -69,6 +70,7 @@ const NOTO_SCRIPT_FAMILIES: Record<string, string> = {
|
|
|
69
70
|
"chinese-hongkong": "Noto Sans HK",
|
|
70
71
|
devanagari: "Noto Sans Devanagari",
|
|
71
72
|
ethiopic: "Noto Sans Ethiopic",
|
|
73
|
+
farsi: "Vazirmatn",
|
|
72
74
|
georgian: "Noto Sans Georgian",
|
|
73
75
|
gujarati: "Noto Sans Gujarati",
|
|
74
76
|
gurmukhi: "Noto Sans Gurmukhi",
|
|
@@ -15,7 +15,12 @@ import type { AstroIntegration, AstroIntegrationLogger } from "astro";
|
|
|
15
15
|
import type { ResolvedPlugin } from "../../plugins/types.js";
|
|
16
16
|
import { local } from "../storage/adapters.js";
|
|
17
17
|
import { notoSans } from "./font-provider.js";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
injectCoreRoutes,
|
|
20
|
+
injectBuiltinAuthRoutes,
|
|
21
|
+
injectAuthProviderRoutes,
|
|
22
|
+
injectMcpRoute,
|
|
23
|
+
} from "./routes.js";
|
|
19
24
|
import type { EmDashConfig, PluginDescriptor } from "./runtime.js";
|
|
20
25
|
import { createViteConfig } from "./vite-config.js";
|
|
21
26
|
|
|
@@ -157,9 +162,12 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
|
|
|
157
162
|
database: resolvedConfig.database,
|
|
158
163
|
storage: resolvedConfig.storage,
|
|
159
164
|
auth: resolvedConfig.auth,
|
|
165
|
+
authProviders: resolvedConfig.authProviders,
|
|
160
166
|
marketplace: resolvedConfig.marketplace,
|
|
161
167
|
siteUrl: resolvedConfig.siteUrl,
|
|
168
|
+
trustedProxyHeaders: resolvedConfig.trustedProxyHeaders,
|
|
162
169
|
maxUploadSize: resolvedConfig.maxUploadSize,
|
|
170
|
+
admin: resolvedConfig.admin,
|
|
163
171
|
};
|
|
164
172
|
|
|
165
173
|
// Determine auth mode for route injection
|
|
@@ -265,7 +273,12 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
|
|
|
265
273
|
// Inject all core routes
|
|
266
274
|
injectCoreRoutes(injectRoute);
|
|
267
275
|
|
|
268
|
-
//
|
|
276
|
+
// Inject routes from pluggable auth providers (authProviders config)
|
|
277
|
+
if (resolvedConfig.authProviders?.length) {
|
|
278
|
+
injectAuthProviderRoutes(injectRoute, resolvedConfig.authProviders);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Inject passkey/oauth/magic-link routes unless transparent external auth is active
|
|
269
282
|
if (!useExternalAuth) {
|
|
270
283
|
injectBuiltinAuthRoutes(injectRoute);
|
|
271
284
|
}
|