emdash 0.5.0 → 0.7.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-C2BzVy0p.d.mts → adapters-Di31kZ28.d.mts} +16 -1
- package/dist/adapters-Di31kZ28.d.mts.map +1 -0
- package/dist/{apply-Cma_PiF6.mjs → apply-5uslYdUu.mjs} +197 -25
- package/dist/apply-5uslYdUu.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 +203 -33
- 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 +30 -4
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.mjs +2 -2
- package/dist/astro/middleware/request-context.d.mts.map +1 -1
- package/dist/astro/middleware/request-context.mjs +11 -4
- 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 +467 -186
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +17 -9
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-WuOq9MFJ.mjs → byline-C4OVd8b3.mjs} +3 -19
- package/dist/byline-C4OVd8b3.mjs.map +1 -0
- package/dist/{bylines-C_Wsnz4L.mjs → bylines-hPTW79hw.mjs} +20 -33
- package/dist/bylines-hPTW79hw.mjs.map +1 -0
- package/dist/{cache-E3Dts-yT.mjs → cache-BkKBuIvS.mjs} +1 -1
- package/dist/{cache-E3Dts-yT.mjs.map → cache-BkKBuIvS.mjs.map} +1 -1
- package/dist/chunks-HGz06Soa.mjs +19 -0
- package/dist/chunks-HGz06Soa.mjs.map +1 -0
- package/dist/cli/index.mjs +12 -11
- 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/{config-DkxPrM9l.mjs → config-BXwuX8Bx.mjs} +1 -1
- package/dist/{config-DkxPrM9l.mjs.map → config-BXwuX8Bx.mjs.map} +1 -1
- package/dist/{connection-B4zVnQIa.mjs → connection-2igzM-AT.mjs} +19 -2
- package/dist/connection-2igzM-AT.mjs.map +1 -0
- package/dist/{content-BsBoyj8G.mjs → content-D7J5y73J.mjs} +27 -1
- package/dist/{content-BsBoyj8G.mjs.map → content-D7J5y73J.mjs.map} +1 -1
- package/dist/database/instrumentation.d.mts +45 -0
- package/dist/database/instrumentation.d.mts.map +1 -0
- package/dist/database/instrumentation.mjs +61 -0
- package/dist/database/instrumentation.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +1 -1
- package/dist/db/index.mjs.map +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 +41 -0
- package/dist/db-errors-D0UT85nC.mjs.map +1 -0
- package/dist/{default-PUx9RK6u.mjs → default-CME5YdZ3.mjs} +1 -1
- package/dist/{default-PUx9RK6u.mjs.map → default-CME5YdZ3.mjs.map} +1 -1
- package/dist/{error-HBeQbVhV.mjs → error-CiYn9yDu.mjs} +1 -1
- package/dist/{error-HBeQbVhV.mjs.map → error-CiYn9yDu.mjs.map} +1 -1
- package/dist/{index-CCWzlriB.d.mts → index-De6_Xv3v.d.mts} +209 -19
- package/dist/index-De6_Xv3v.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +23 -21
- package/dist/{load-BhSSm-TS.mjs → load-CBcmDIot.mjs} +1 -1
- package/dist/{load-BhSSm-TS.mjs.map → load-CBcmDIot.mjs.map} +1 -1
- package/dist/{loader-BYzwzORf.mjs → loader-DeiBJEMe.mjs} +18 -12
- package/dist/loader-DeiBJEMe.mjs.map +1 -0
- package/dist/{manifest-schema-BsXINkQD.mjs → manifest-schema-V30qsMft.mjs} +1 -1
- package/dist/{manifest-schema-BsXINkQD.mjs.map → manifest-schema-V30qsMft.mjs.map} +1 -1
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/{mode-CyPLdO3C.mjs → mode-CpNnGkPz.mjs} +1 -1
- package/dist/{mode-CyPLdO3C.mjs.map → mode-CpNnGkPz.mjs.map} +1 -1
- package/dist/page/index.d.mts +11 -2
- package/dist/page/index.d.mts.map +1 -1
- package/dist/page/index.mjs +23 -1
- package/dist/page/index.mjs.map +1 -1
- package/dist/{placeholder-DntBEQo7.mjs → placeholder-C-fk5hYI.mjs} +1 -1
- package/dist/{placeholder-DntBEQo7.mjs.map → placeholder-C-fk5hYI.mjs.map} +1 -1
- package/dist/{placeholder-BBCtpTES.d.mts → placeholder-tzpqGWII.d.mts} +1 -1
- package/dist/{placeholder-BBCtpTES.d.mts.map → placeholder-tzpqGWII.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-B6Vu0d2i.mjs → query-g4Ug-9j9.mjs} +79 -12
- package/dist/query-g4Ug-9j9.mjs.map +1 -0
- package/dist/{redirect-7lGhLBNZ.mjs → redirect-CN0Rt9Ob.mjs} +66 -10
- package/dist/redirect-CN0Rt9Ob.mjs.map +1 -0
- package/dist/{registry-BgnP3ysR.mjs → registry-Ci3WxVAr.mjs} +133 -97
- package/dist/registry-Ci3WxVAr.mjs.map +1 -0
- package/dist/request-cache-DiR961CV.mjs +79 -0
- package/dist/request-cache-DiR961CV.mjs.map +1 -0
- package/dist/request-context.d.mts +19 -16
- package/dist/request-context.d.mts.map +1 -1
- package/dist/request-context.mjs.map +1 -1
- package/dist/{runner-DYv3rX8P.d.mts → runner-BR2xKwhn.d.mts} +2 -2
- package/dist/{runner-DYv3rX8P.d.mts.map → runner-BR2xKwhn.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 +1 -1
- package/dist/{search-Cn1SYvYF.mjs → search-B0effn3j.mjs} +210 -226
- package/dist/search-B0effn3j.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +10 -9
- 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.mjs +1 -1
- package/dist/taxonomies-K2z0Uhnj.mjs +308 -0
- package/dist/taxonomies-K2z0Uhnj.mjs.map +1 -0
- package/dist/{tokens-DKHiCYCB.mjs → tokens-BFPFx3CA.mjs} +1 -1
- package/dist/{tokens-DKHiCYCB.mjs.map → tokens-BFPFx3CA.mjs.map} +1 -1
- package/dist/{transport-BtcQ-Z7T.mjs → transport-BykRfpyy.mjs} +1 -1
- package/dist/{transport-BtcQ-Z7T.mjs.map → transport-BykRfpyy.mjs.map} +1 -1
- package/dist/{transport-CKQA_G44.d.mts → transport-H4Iwx7tC.d.mts} +1 -1
- package/dist/{transport-CKQA_G44.d.mts.map → transport-H4Iwx7tC.d.mts.map} +1 -1
- package/dist/{types-BmkQR1En.d.mts → types-6CUZRrZP.d.mts} +1 -1
- package/dist/{types-BmkQR1En.d.mts.map → types-6CUZRrZP.d.mts.map} +1 -1
- package/dist/{types-Dz9_WMS6.mjs → types-BH2L167P.mjs} +1 -1
- package/dist/{types-Dz9_WMS6.mjs.map → types-BH2L167P.mjs.map} +1 -1
- package/dist/{types-B6BzlZxx.d.mts → types-C2v0c34j.d.mts} +10 -1
- package/dist/{types-B6BzlZxx.d.mts.map → types-C2v0c34j.d.mts.map} +1 -1
- package/dist/{types-DNZpaCBk.d.mts → types-CFWjXmus.d.mts} +1 -1
- package/dist/{types-DNZpaCBk.d.mts.map → types-CFWjXmus.d.mts.map} +1 -1
- package/dist/{types-DeG21anB.d.mts → types-CnZYHyLW.d.mts} +55 -5
- package/dist/types-CnZYHyLW.d.mts.map +1 -0
- package/dist/{types-xxCWI3j0.mjs → types-DDS4MxsT.mjs} +11 -3
- package/dist/types-DDS4MxsT.mjs.map +1 -0
- package/dist/{types-C3ronwXb.d.mts → types-DgrIP0tF.d.mts} +102 -4
- package/dist/types-DgrIP0tF.d.mts.map +1 -0
- package/dist/{validate-DuZDIxfy.mjs → validate-CqsNItbt.mjs} +2 -2
- package/dist/{validate-DuZDIxfy.mjs.map → validate-CqsNItbt.mjs.map} +1 -1
- package/dist/{validate-Db1yNL3i.d.mts → validate-kM8Pjuf7.d.mts} +5 -52
- package/dist/validate-kM8Pjuf7.d.mts.map +1 -0
- package/dist/version-BnTKdfam.mjs +7 -0
- package/dist/{version-CMMjTuqu.mjs.map → version-BnTKdfam.mjs.map} +1 -1
- package/package.json +10 -5
- package/src/after.ts +62 -0
- package/src/api/handlers/content.ts +2 -0
- package/src/api/handlers/oauth-authorization.ts +2 -32
- package/src/api/handlers/oauth-clients.ts +40 -4
- package/src/api/handlers/taxonomies.ts +13 -0
- package/src/api/oauth/redirect-uri.ts +34 -0
- package/src/api/openapi/document.ts +126 -118
- package/src/api/schemas/content.ts +8 -0
- package/src/api/schemas/media.ts +26 -15
- package/src/api/schemas/schema.ts +1 -0
- package/src/astro/integration/font-provider.ts +178 -0
- package/src/astro/integration/index.ts +44 -0
- package/src/astro/integration/routes.ts +6 -0
- package/src/astro/integration/runtime.ts +117 -0
- package/src/astro/integration/virtual-modules.ts +41 -39
- package/src/astro/integration/vite-config.ts +16 -5
- package/src/astro/middleware/auth.ts +33 -1
- package/src/astro/middleware/request-context.ts +15 -3
- package/src/astro/middleware.ts +340 -263
- package/src/astro/routes/admin.astro +21 -10
- package/src/astro/routes/api/auth/magic-link/send.ts +2 -1
- package/src/astro/routes/api/auth/passkey/options.ts +2 -1
- package/src/astro/routes/api/auth/passkey/verify.ts +5 -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]/terms/[taxonomy].ts +5 -0
- 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 +19 -1
- package/src/astro/routes/api/content/[collection]/trash.ts +1 -1
- package/src/astro/routes/api/import/wordpress/execute.ts +1 -1
- package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +4 -3
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +5 -4
- package/src/astro/routes/api/manifest.ts +7 -0
- package/src/astro/routes/api/media/upload-url.ts +10 -2
- package/src/astro/routes/api/media.ts +10 -7
- 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/oauth/register.ts +178 -0
- package/src/astro/routes/api/oauth/token.ts +15 -0
- package/src/astro/routes/api/openapi.json.ts +15 -5
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +2 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +1 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +1 -0
- package/src/astro/routes/api/search/index.ts +5 -0
- package/src/astro/routes/api/search/suggest.ts +3 -0
- package/src/astro/routes/api/setup/admin-verify.ts +30 -5
- package/src/astro/routes/api/setup/admin.ts +32 -8
- package/src/astro/routes/api/setup/index.ts +5 -2
- package/src/astro/routes/api/taxonomies/index.ts +1 -0
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +1 -1
- package/src/astro/types.ts +9 -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/bylines/index.ts +22 -45
- package/src/components/EmDashHead.astro +23 -7
- package/src/database/connection.ts +23 -1
- package/src/database/instrumentation.ts +98 -0
- package/src/database/migrations/035_bounded_404_log.ts +112 -0
- package/src/database/migrations/runner.ts +2 -0
- package/src/database/repositories/content.ts +39 -0
- package/src/database/repositories/options.ts +25 -0
- package/src/database/repositories/redirect.ts +111 -8
- package/src/database/types.ts +9 -0
- package/src/db/adapters.ts +15 -0
- package/src/emdash-runtime.ts +312 -92
- package/src/import/registry.ts +4 -3
- package/src/import/ssrf.ts +253 -12
- package/src/index.ts +6 -0
- package/src/loader.ts +19 -24
- package/src/mcp/server.ts +76 -3
- package/src/menus/index.ts +6 -3
- package/src/page/index.ts +1 -1
- package/src/page/seo-contributions.ts +36 -0
- package/src/plugins/context.ts +15 -3
- package/src/plugins/manager.ts +6 -0
- package/src/plugins/request-meta.ts +66 -15
- package/src/plugins/routes.ts +3 -1
- package/src/query.ts +104 -7
- package/src/request-cache.ts +106 -0
- package/src/request-context.ts +19 -0
- package/src/schema/query.ts +5 -2
- package/src/schema/registry.ts +243 -166
- package/src/schema/types.ts +13 -2
- package/src/schema/zod-generator.ts +4 -0
- package/src/search/fts-manager.ts +19 -5
- package/src/search/query.ts +4 -3
- package/src/seed/apply.ts +41 -1
- package/src/settings/index.ts +24 -5
- package/src/taxonomies/index.ts +324 -124
- package/src/utils/db-errors.ts +46 -0
- package/src/virtual-modules.d.ts +31 -10
- package/src/visual-editing/toolbar.ts +6 -1
- package/src/widgets/index.ts +54 -25
- package/dist/adapters-C2BzVy0p.d.mts.map +0 -1
- package/dist/apply-Cma_PiF6.mjs.map +0 -1
- package/dist/byline-WuOq9MFJ.mjs.map +0 -1
- package/dist/bylines-C_Wsnz4L.mjs.map +0 -1
- package/dist/connection-B4zVnQIa.mjs.map +0 -1
- package/dist/index-CCWzlriB.d.mts.map +0 -1
- package/dist/loader-BYzwzORf.mjs.map +0 -1
- package/dist/query-B6Vu0d2i.mjs.map +0 -1
- package/dist/redirect-7lGhLBNZ.mjs.map +0 -1
- package/dist/registry-BgnP3ysR.mjs.map +0 -1
- package/dist/runner-Cd-_WyDo.mjs.map +0 -1
- package/dist/search-Cn1SYvYF.mjs.map +0 -1
- package/dist/types-C3ronwXb.d.mts.map +0 -1
- package/dist/types-DeG21anB.d.mts.map +0 -1
- package/dist/types-xxCWI3j0.mjs.map +0 -1
- package/dist/validate-Db1yNL3i.d.mts.map +0 -1
- package/dist/version-CMMjTuqu.mjs +0 -7
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
trashedContentListResponseSchema,
|
|
38
38
|
} from "../schemas/content.js";
|
|
39
39
|
import {
|
|
40
|
+
DEFAULT_MAX_UPLOAD_SIZE,
|
|
40
41
|
mediaConfirmBody,
|
|
41
42
|
mediaConfirmResponseSchema,
|
|
42
43
|
mediaExistingResponseSchema,
|
|
@@ -623,121 +624,123 @@ const contentPaths = {
|
|
|
623
624
|
// Media routes
|
|
624
625
|
// ---------------------------------------------------------------------------
|
|
625
626
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
627
|
+
function buildMediaPaths(maxUploadSize: number) {
|
|
628
|
+
return {
|
|
629
|
+
"/_emdash/api/media": {
|
|
630
|
+
get: {
|
|
631
|
+
operationId: "listMedia",
|
|
632
|
+
summary: "List media items",
|
|
633
|
+
tags: ["Media"],
|
|
634
|
+
requestParams: { query: mediaListQuery },
|
|
635
|
+
responses: {
|
|
636
|
+
"200": {
|
|
637
|
+
description: "Media list",
|
|
638
|
+
content: { [JSON_CONTENT]: { schema: successEnvelope(mediaListResponseSchema) } },
|
|
639
|
+
},
|
|
640
|
+
...authErrors,
|
|
641
|
+
...standardErrors(500),
|
|
637
642
|
},
|
|
638
|
-
...authErrors,
|
|
639
|
-
...standardErrors(500),
|
|
640
643
|
},
|
|
641
644
|
},
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
path: z.object({ id: z.string().meta({ description: "Media ID" }) }),
|
|
650
|
-
},
|
|
651
|
-
responses: {
|
|
652
|
-
"200": {
|
|
653
|
-
description: "Media item",
|
|
654
|
-
content: { [JSON_CONTENT]: { schema: successEnvelope(mediaResponseSchema) } },
|
|
645
|
+
"/_emdash/api/media/{id}": {
|
|
646
|
+
get: {
|
|
647
|
+
operationId: "getMedia",
|
|
648
|
+
summary: "Get a media item",
|
|
649
|
+
tags: ["Media"],
|
|
650
|
+
requestParams: {
|
|
651
|
+
path: z.object({ id: z.string().meta({ description: "Media ID" }) }),
|
|
655
652
|
},
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
description: "
|
|
671
|
-
|
|
653
|
+
responses: {
|
|
654
|
+
"200": {
|
|
655
|
+
description: "Media item",
|
|
656
|
+
content: { [JSON_CONTENT]: { schema: successEnvelope(mediaResponseSchema) } },
|
|
657
|
+
},
|
|
658
|
+
...authErrors,
|
|
659
|
+
...standardErrors(404, 500),
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
put: {
|
|
663
|
+
operationId: "updateMedia",
|
|
664
|
+
summary: "Update media metadata",
|
|
665
|
+
tags: ["Media"],
|
|
666
|
+
requestParams: {
|
|
667
|
+
path: z.object({ id: z.string().meta({ description: "Media ID" }) }),
|
|
668
|
+
},
|
|
669
|
+
requestBody: { content: { [JSON_CONTENT]: { schema: mediaUpdateBody } } },
|
|
670
|
+
responses: {
|
|
671
|
+
"200": {
|
|
672
|
+
description: "Updated media item",
|
|
673
|
+
content: { [JSON_CONTENT]: { schema: successEnvelope(mediaResponseSchema) } },
|
|
674
|
+
},
|
|
675
|
+
...authErrors,
|
|
676
|
+
...standardErrors(400, 404, 500),
|
|
672
677
|
},
|
|
673
|
-
...authErrors,
|
|
674
|
-
...standardErrors(400, 404, 500),
|
|
675
678
|
},
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
path: z.object({ id: z.string().meta({ description: "Media ID" }) }),
|
|
683
|
-
},
|
|
684
|
-
responses: {
|
|
685
|
-
"200": {
|
|
686
|
-
description: "Deleted",
|
|
687
|
-
content: { [JSON_CONTENT]: { schema: successEnvelope(deleteResponseSchema) } },
|
|
679
|
+
delete: {
|
|
680
|
+
operationId: "deleteMedia",
|
|
681
|
+
summary: "Delete a media item",
|
|
682
|
+
tags: ["Media"],
|
|
683
|
+
requestParams: {
|
|
684
|
+
path: z.object({ id: z.string().meta({ description: "Media ID" }) }),
|
|
688
685
|
},
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
686
|
+
responses: {
|
|
687
|
+
"200": {
|
|
688
|
+
description: "Deleted",
|
|
689
|
+
content: { [JSON_CONTENT]: { schema: successEnvelope(deleteResponseSchema) } },
|
|
690
|
+
},
|
|
691
|
+
...authErrors,
|
|
692
|
+
...standardErrors(404, 500),
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
"/_emdash/api/media/upload-url": {
|
|
697
|
+
post: {
|
|
698
|
+
operationId: "getMediaUploadUrl",
|
|
699
|
+
summary: "Get a signed URL for direct upload",
|
|
700
|
+
description:
|
|
701
|
+
"Returns a signed URL for direct-to-storage upload. Creates a pending media record.",
|
|
702
|
+
tags: ["Media"],
|
|
703
|
+
requestBody: { content: { [JSON_CONTENT]: { schema: mediaUploadUrlBody(maxUploadSize) } } },
|
|
704
|
+
responses: {
|
|
705
|
+
"200": {
|
|
706
|
+
description: "Upload URL or existing media (deduplication)",
|
|
707
|
+
content: {
|
|
708
|
+
[JSON_CONTENT]: {
|
|
709
|
+
schema: successEnvelope(
|
|
710
|
+
z.union([mediaUploadUrlResponseSchema, mediaExistingResponseSchema]),
|
|
711
|
+
),
|
|
712
|
+
},
|
|
710
713
|
},
|
|
711
714
|
},
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
},
|
|
716
|
-
},
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
715
|
+
...authErrors,
|
|
716
|
+
...standardErrors(400, 500),
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
"/_emdash/api/media/{id}/confirm": {
|
|
721
|
+
post: {
|
|
722
|
+
operationId: "confirmMediaUpload",
|
|
723
|
+
summary: "Confirm a media upload",
|
|
724
|
+
description: "Marks a pending media record as ready after the file has been uploaded.",
|
|
725
|
+
tags: ["Media"],
|
|
726
|
+
requestParams: {
|
|
727
|
+
path: z.object({ id: z.string().meta({ description: "Media ID" }) }),
|
|
728
|
+
},
|
|
729
|
+
requestBody: { content: { [JSON_CONTENT]: { schema: mediaConfirmBody } } },
|
|
730
|
+
responses: {
|
|
731
|
+
"200": {
|
|
732
|
+
description: "Confirmed media item with URL",
|
|
733
|
+
content: {
|
|
734
|
+
[JSON_CONTENT]: { schema: successEnvelope(mediaConfirmResponseSchema) },
|
|
735
|
+
},
|
|
733
736
|
},
|
|
737
|
+
...authErrors,
|
|
738
|
+
...standardErrors(400, 404, 500),
|
|
734
739
|
},
|
|
735
|
-
...authErrors,
|
|
736
|
-
...standardErrors(400, 404, 500),
|
|
737
740
|
},
|
|
738
741
|
},
|
|
739
|
-
}
|
|
740
|
-
}
|
|
742
|
+
} as const;
|
|
743
|
+
}
|
|
741
744
|
|
|
742
745
|
// ---------------------------------------------------------------------------
|
|
743
746
|
// Schema routes
|
|
@@ -2249,20 +2252,22 @@ const userPaths = {
|
|
|
2249
2252
|
// Merge all paths
|
|
2250
2253
|
// ---------------------------------------------------------------------------
|
|
2251
2254
|
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2255
|
+
function buildAllPaths(maxUploadSize: number) {
|
|
2256
|
+
return {
|
|
2257
|
+
...contentPaths,
|
|
2258
|
+
...buildMediaPaths(maxUploadSize),
|
|
2259
|
+
...schemaPaths,
|
|
2260
|
+
...commentsPaths,
|
|
2261
|
+
...taxonomyPaths,
|
|
2262
|
+
...menuPaths,
|
|
2263
|
+
...sectionPaths,
|
|
2264
|
+
...widgetPaths,
|
|
2265
|
+
...settingsPaths,
|
|
2266
|
+
...searchPaths,
|
|
2267
|
+
...redirectPaths,
|
|
2268
|
+
...userPaths,
|
|
2269
|
+
} as const;
|
|
2270
|
+
}
|
|
2266
2271
|
|
|
2267
2272
|
// ---------------------------------------------------------------------------
|
|
2268
2273
|
// Document
|
|
@@ -2274,7 +2279,10 @@ const allPaths = {
|
|
|
2274
2279
|
* Covers: Content, Media, Schema, Comments, Taxonomies, Menus,
|
|
2275
2280
|
* Sections, Widgets, Settings, Search, Redirects, Users.
|
|
2276
2281
|
*/
|
|
2277
|
-
export function generateOpenApiDocument(
|
|
2282
|
+
export function generateOpenApiDocument(
|
|
2283
|
+
options: { maxUploadSize?: number } = {},
|
|
2284
|
+
): oas31.OpenAPIObject {
|
|
2285
|
+
const maxUploadSize = options.maxUploadSize ?? DEFAULT_MAX_UPLOAD_SIZE;
|
|
2278
2286
|
return createDocument({
|
|
2279
2287
|
openapi: "3.1.0",
|
|
2280
2288
|
info: {
|
|
@@ -2363,6 +2371,6 @@ export function generateOpenApiDocument(): oas31.OpenAPIObject {
|
|
|
2363
2371
|
},
|
|
2364
2372
|
security: [{ session: [] }, { bearer: [] }],
|
|
2365
2373
|
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- readonly const paths are compatible at runtime
|
|
2366
|
-
paths:
|
|
2374
|
+
paths: buildAllPaths(maxUploadSize) as unknown as ZodOpenApiPathsObject,
|
|
2367
2375
|
});
|
|
2368
2376
|
}
|
|
@@ -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/media.ts
CHANGED
|
@@ -21,21 +21,32 @@ export const mediaUpdateBody = z
|
|
|
21
21
|
})
|
|
22
22
|
.meta({ id: "MediaUpdateBody" });
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
24
|
+
/** Default maximum allowed file upload size (50 MB). */
|
|
25
|
+
export const DEFAULT_MAX_UPLOAD_SIZE = 50 * 1024 * 1024;
|
|
26
|
+
|
|
27
|
+
export function formatFileSize(bytes: number): string {
|
|
28
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
29
|
+
if (bytes < 1024 * 1024) return `${Math.floor(bytes / 1024)}KB`;
|
|
30
|
+
return `${Math.floor(bytes / 1024 / 1024)}MB`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function mediaUploadUrlBody(maxSize: number) {
|
|
34
|
+
if (!Number.isFinite(maxSize) || maxSize <= 0) {
|
|
35
|
+
throw new Error(`EmDash: maxUploadSize must be a positive finite number, got ${maxSize}`);
|
|
36
|
+
}
|
|
37
|
+
return z
|
|
38
|
+
.object({
|
|
39
|
+
filename: z.string().min(1, "filename is required"),
|
|
40
|
+
contentType: z.string().min(1, "contentType is required"),
|
|
41
|
+
size: z
|
|
42
|
+
.number()
|
|
43
|
+
.int()
|
|
44
|
+
.positive()
|
|
45
|
+
.max(maxSize, `File size must not exceed ${formatFileSize(maxSize)}`),
|
|
46
|
+
contentHash: z.string().optional(),
|
|
47
|
+
})
|
|
48
|
+
.meta({ id: "MediaUploadUrlBody" });
|
|
49
|
+
}
|
|
39
50
|
|
|
40
51
|
export const mediaConfirmBody = z
|
|
41
52
|
.object({
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EmDash Noto Sans font provider
|
|
3
|
+
*
|
|
4
|
+
* A custom Astro font provider that wraps Google Fonts to resolve
|
|
5
|
+
* multiple Noto Sans families (Latin, Arabic, JP, etc.) under a
|
|
6
|
+
* single logical font entry. This lets all @font-face blocks share
|
|
7
|
+
* the same font-family name, so the browser picks the right file
|
|
8
|
+
* per character via unicode-range.
|
|
9
|
+
*
|
|
10
|
+
* Without this, registering "Noto Sans" and "Noto Sans Arabic" as
|
|
11
|
+
* separate font entries on the same cssVariable triggers an Astro
|
|
12
|
+
* warning and the last entry overwrites the first.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { fontProviders } from "astro/config";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* All subset names used by Google Fonts CSS responses.
|
|
19
|
+
* Passed when resolving extra script families so the unifont
|
|
20
|
+
* provider doesn't filter out any faces.
|
|
21
|
+
*/
|
|
22
|
+
const ALL_GOOGLE_SUBSETS = [
|
|
23
|
+
"arabic",
|
|
24
|
+
"armenian",
|
|
25
|
+
"bengali",
|
|
26
|
+
"chinese-simplified",
|
|
27
|
+
"chinese-traditional",
|
|
28
|
+
"chinese-hongkong",
|
|
29
|
+
"cyrillic",
|
|
30
|
+
"cyrillic-ext",
|
|
31
|
+
"devanagari",
|
|
32
|
+
"ethiopic",
|
|
33
|
+
"farsi",
|
|
34
|
+
"georgian",
|
|
35
|
+
"greek",
|
|
36
|
+
"greek-ext",
|
|
37
|
+
"gujarati",
|
|
38
|
+
"gurmukhi",
|
|
39
|
+
"hebrew",
|
|
40
|
+
"japanese",
|
|
41
|
+
"kannada",
|
|
42
|
+
"khmer",
|
|
43
|
+
"korean",
|
|
44
|
+
"lao",
|
|
45
|
+
"latin",
|
|
46
|
+
"latin-ext",
|
|
47
|
+
"malayalam",
|
|
48
|
+
"math",
|
|
49
|
+
"myanmar",
|
|
50
|
+
"oriya",
|
|
51
|
+
"sinhala",
|
|
52
|
+
"symbols",
|
|
53
|
+
"tamil",
|
|
54
|
+
"telugu",
|
|
55
|
+
"thai",
|
|
56
|
+
"tibetan",
|
|
57
|
+
"vietnamese",
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Known Noto Sans and Sans script families on Google Fonts.
|
|
62
|
+
* Maps user-friendly script names to Google Fonts family names.
|
|
63
|
+
*/
|
|
64
|
+
const NOTO_SCRIPT_FAMILIES: Record<string, string> = {
|
|
65
|
+
arabic: "Noto Sans Arabic",
|
|
66
|
+
armenian: "Noto Sans Armenian",
|
|
67
|
+
bengali: "Noto Sans Bengali",
|
|
68
|
+
"chinese-simplified": "Noto Sans SC",
|
|
69
|
+
"chinese-traditional": "Noto Sans TC",
|
|
70
|
+
"chinese-hongkong": "Noto Sans HK",
|
|
71
|
+
devanagari: "Noto Sans Devanagari",
|
|
72
|
+
ethiopic: "Noto Sans Ethiopic",
|
|
73
|
+
farsi: "Vazirmatn",
|
|
74
|
+
georgian: "Noto Sans Georgian",
|
|
75
|
+
gujarati: "Noto Sans Gujarati",
|
|
76
|
+
gurmukhi: "Noto Sans Gurmukhi",
|
|
77
|
+
hebrew: "Noto Sans Hebrew",
|
|
78
|
+
japanese: "Noto Sans JP",
|
|
79
|
+
kannada: "Noto Sans Kannada",
|
|
80
|
+
khmer: "Noto Sans Khmer",
|
|
81
|
+
korean: "Noto Sans KR",
|
|
82
|
+
lao: "Noto Sans Lao",
|
|
83
|
+
malayalam: "Noto Sans Malayalam",
|
|
84
|
+
myanmar: "Noto Sans Myanmar",
|
|
85
|
+
oriya: "Noto Sans Oriya",
|
|
86
|
+
sinhala: "Noto Sans Sinhala",
|
|
87
|
+
tamil: "Noto Sans Tamil",
|
|
88
|
+
telugu: "Noto Sans Telugu",
|
|
89
|
+
thai: "Noto Sans Thai",
|
|
90
|
+
tibetan: "Noto Sans Tibetan",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export interface NotoSansProviderOptions {
|
|
94
|
+
/**
|
|
95
|
+
* Additional Noto Sans script families to include.
|
|
96
|
+
* Use script names like "arabic", "japanese", "chinese-simplified".
|
|
97
|
+
*
|
|
98
|
+
* @see {@link NOTO_SCRIPT_FAMILIES} for the full list of supported scripts.
|
|
99
|
+
*/
|
|
100
|
+
scripts?: string[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Use ReturnType to get the provider type without importing it directly.
|
|
104
|
+
// The Astro FontProvider type is not part of the public API surface.
|
|
105
|
+
type GoogleProvider = ReturnType<typeof fontProviders.google>;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Create a font provider that resolves Noto Sans plus additional
|
|
109
|
+
* script-specific Noto families from Google Fonts, all under one
|
|
110
|
+
* font-family name.
|
|
111
|
+
*/
|
|
112
|
+
export function notoSans(options?: NotoSansProviderOptions): GoogleProvider {
|
|
113
|
+
// Create a single Google provider instance to share initialization
|
|
114
|
+
const googleProvider = fontProviders.google();
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
name: "emdash-noto",
|
|
118
|
+
async init(context) {
|
|
119
|
+
await googleProvider.init?.(context);
|
|
120
|
+
},
|
|
121
|
+
async resolveFont(resolveFontOptions) {
|
|
122
|
+
// Resolve the base Noto Sans (Latin, Cyrillic, Greek, etc.)
|
|
123
|
+
const base = await googleProvider.resolveFont(resolveFontOptions);
|
|
124
|
+
const baseFonts = base?.fonts ?? [];
|
|
125
|
+
|
|
126
|
+
if (!options?.scripts?.length) {
|
|
127
|
+
return base;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Collect subset names already covered by the base font so we
|
|
131
|
+
// can filter out duplicate faces from extra script families.
|
|
132
|
+
// e.g. Noto Sans Arabic includes latin/latin-ext faces that
|
|
133
|
+
// would otherwise override the base Noto Sans latin faces.
|
|
134
|
+
const baseSubsets = new Set(baseFonts.map((f) => f.meta?.subset).filter(Boolean));
|
|
135
|
+
|
|
136
|
+
// Resolve additional script families
|
|
137
|
+
const extraFonts = await Promise.all(
|
|
138
|
+
options.scripts.map(async (script) => {
|
|
139
|
+
const family = NOTO_SCRIPT_FAMILIES[script];
|
|
140
|
+
if (!family) {
|
|
141
|
+
// Silently skip subset names that are already covered
|
|
142
|
+
// by the base Noto Sans font (latin, cyrillic, etc.)
|
|
143
|
+
if (ALL_GOOGLE_SUBSETS.includes(script)) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
console.warn(
|
|
147
|
+
`[emdash] Unknown Noto Sans script "${script}". ` +
|
|
148
|
+
`Available: ${Object.keys(NOTO_SCRIPT_FAMILIES).join(", ")}`,
|
|
149
|
+
);
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
return googleProvider.resolveFont({
|
|
153
|
+
...resolveFontOptions,
|
|
154
|
+
familyName: family,
|
|
155
|
+
// Pass all known subset names so the unifont provider
|
|
156
|
+
// doesn't filter out any faces. Each script family
|
|
157
|
+
// only returns faces for its own subsets anyway.
|
|
158
|
+
subsets: ALL_GOOGLE_SUBSETS,
|
|
159
|
+
});
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Merge, dropping faces from extra fonts that duplicate base subsets
|
|
164
|
+
const extraFaces = extraFonts.flatMap((r) =>
|
|
165
|
+
(r?.fonts ?? []).filter((f) => !f.meta?.subset || !baseSubsets.has(f.meta.subset)),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
fonts: [...baseFonts, ...extraFaces],
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Get the list of available Noto Sans script names */
|
|
176
|
+
export function getAvailableNotoScripts(): string[] {
|
|
177
|
+
return Object.keys(NOTO_SCRIPT_FAMILIES);
|
|
178
|
+
}
|
|
@@ -14,6 +14,7 @@ import type { AstroIntegration, AstroIntegrationLogger } from "astro";
|
|
|
14
14
|
|
|
15
15
|
import type { ResolvedPlugin } from "../../plugins/types.js";
|
|
16
16
|
import { local } from "../storage/adapters.js";
|
|
17
|
+
import { notoSans } from "./font-provider.js";
|
|
17
18
|
import { injectCoreRoutes, injectBuiltinAuthRoutes, injectMcpRoute } from "./routes.js";
|
|
18
19
|
import type { EmDashConfig, PluginDescriptor } from "./runtime.js";
|
|
19
20
|
import { createViteConfig } from "./vite-config.js";
|
|
@@ -158,6 +159,9 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
|
|
|
158
159
|
auth: resolvedConfig.auth,
|
|
159
160
|
marketplace: resolvedConfig.marketplace,
|
|
160
161
|
siteUrl: resolvedConfig.siteUrl,
|
|
162
|
+
trustedProxyHeaders: resolvedConfig.trustedProxyHeaders,
|
|
163
|
+
maxUploadSize: resolvedConfig.maxUploadSize,
|
|
164
|
+
admin: resolvedConfig.admin,
|
|
161
165
|
};
|
|
162
166
|
|
|
163
167
|
// Determine auth mode for route injection
|
|
@@ -207,8 +211,48 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
|
|
|
207
211
|
? { allowedDomains: [{ hostname: new URL(resolvedConfig.siteUrl).hostname }] }
|
|
208
212
|
: {}),
|
|
209
213
|
};
|
|
214
|
+
|
|
215
|
+
// Inject default Noto Sans font for the admin UI.
|
|
216
|
+
// Uses the Astro Font API so fonts are downloaded at build time
|
|
217
|
+
// and self-hosted (no runtime CDN requests).
|
|
218
|
+
//
|
|
219
|
+
// The admin CSS references var(--font-emdash) with a system font
|
|
220
|
+
// fallback. Users can add extra script coverage (Arabic, CJK, etc.)
|
|
221
|
+
// by passing fonts.scripts in the emdash() config. The custom
|
|
222
|
+
// notoSans provider resolves all script families from Google Fonts
|
|
223
|
+
// under a single font-family name, so they stack via unicode-range.
|
|
224
|
+
const fontsConfig = resolvedConfig.fonts;
|
|
225
|
+
const emdashFonts =
|
|
226
|
+
fontsConfig === false
|
|
227
|
+
? []
|
|
228
|
+
: [
|
|
229
|
+
{
|
|
230
|
+
provider: notoSans({
|
|
231
|
+
scripts: fontsConfig?.scripts,
|
|
232
|
+
}),
|
|
233
|
+
name: "Noto Sans",
|
|
234
|
+
cssVariable: "--font-emdash",
|
|
235
|
+
weights: ["100 900" as const],
|
|
236
|
+
styles: ["normal" as const, "italic" as const],
|
|
237
|
+
subsets: [
|
|
238
|
+
"latin" as const,
|
|
239
|
+
"latin-ext" as const,
|
|
240
|
+
"cyrillic" as const,
|
|
241
|
+
"cyrillic-ext" as const,
|
|
242
|
+
"devanagari" as const,
|
|
243
|
+
"greek" as const,
|
|
244
|
+
"greek-ext" as const,
|
|
245
|
+
"vietnamese" as const,
|
|
246
|
+
],
|
|
247
|
+
fallbacks: ["ui-sans-serif", "system-ui", "sans-serif"],
|
|
248
|
+
},
|
|
249
|
+
];
|
|
250
|
+
|
|
210
251
|
updateConfig({
|
|
211
252
|
security: securityConfig,
|
|
253
|
+
// fonts is a valid AstroConfig key but may not be in the
|
|
254
|
+
// type definition for the minimum supported Astro version
|
|
255
|
+
...({ fonts: emdashFonts } as Record<string, unknown>),
|
|
212
256
|
vite: createViteConfig(
|
|
213
257
|
{
|
|
214
258
|
serializableConfig,
|
|
@@ -515,6 +515,12 @@ export function injectCoreRoutes(injectRoute: InjectRoute): void {
|
|
|
515
515
|
entrypoint: resolveRoute("api/well-known/oauth-authorization-server.ts"),
|
|
516
516
|
});
|
|
517
517
|
|
|
518
|
+
// RFC 7591 Dynamic Client Registration
|
|
519
|
+
injectRoute({
|
|
520
|
+
pattern: "/_emdash/api/oauth/register",
|
|
521
|
+
entrypoint: resolveRoute("api/oauth/register.ts"),
|
|
522
|
+
});
|
|
523
|
+
|
|
518
524
|
// Plugin-defined API routes
|
|
519
525
|
// All plugin routes are handled by a single catch-all handler
|
|
520
526
|
injectRoute({
|