emdash 0.8.0 → 0.9.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-BKSf3T9R.d.mts → adapters-DoNJiveC.d.mts} +1 -1
- package/dist/{adapters-BKSf3T9R.d.mts.map → adapters-DoNJiveC.d.mts.map} +1 -1
- package/dist/{apply-x0eMK1lX.mjs → apply-BzltprvY.mjs} +85 -135
- package/dist/apply-BzltprvY.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 +110 -4
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +6 -7
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +16 -59
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.d.mts.map +1 -1
- package/dist/astro/middleware/redirect.mjs +17 -12
- package/dist/astro/middleware/redirect.mjs.map +1 -1
- package/dist/astro/middleware/request-context.d.mts.map +1 -1
- package/dist/astro/middleware/request-context.mjs +9 -6
- 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 +72 -124
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +26 -10
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{base64-MBPo9ozB.mjs → base64-BRICGH2l.mjs} +1 -1
- package/dist/{base64-MBPo9ozB.mjs.map → base64-BRICGH2l.mjs.map} +1 -1
- package/dist/{byline-Chbr2GoP.mjs → byline-BSaNL1w7.mjs} +4 -4
- package/dist/{byline-Chbr2GoP.mjs.map → byline-BSaNL1w7.mjs.map} +1 -1
- package/dist/bylines-CvJ3PYz2.mjs +113 -0
- package/dist/bylines-CvJ3PYz2.mjs.map +1 -0
- package/dist/cache-C6N_hhN7.mjs +65 -0
- package/dist/cache-C6N_hhN7.mjs.map +1 -0
- package/dist/{chunks-HGz06Soa.mjs → chunks-NBQVDOci.mjs} +8 -2
- package/dist/{chunks-HGz06Soa.mjs.map → chunks-NBQVDOci.mjs.map} +1 -1
- package/dist/cli/index.mjs +224 -30
- 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 +3 -3
- package/dist/client/index.mjs.map +1 -1
- package/dist/{config-BXwuX8Bx.mjs → config-BI0V3ICQ.mjs} +1 -1
- package/dist/{config-BXwuX8Bx.mjs.map → config-BI0V3ICQ.mjs.map} +1 -1
- package/dist/{content-BcQPYxdV.mjs → content-8lOYF0pr.mjs} +32 -15
- package/dist/{content-BcQPYxdV.mjs.map → content-8lOYF0pr.mjs.map} +1 -1
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +2 -2
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/libsql.d.mts.map +1 -1
- package/dist/db/libsql.mjs +7 -2
- package/dist/db/libsql.mjs.map +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/db/sqlite.d.mts.map +1 -1
- package/dist/db/sqlite.mjs +8 -3
- package/dist/db/sqlite.mjs.map +1 -1
- package/dist/{db-errors-l1Qh2RPR.mjs → db-errors-WRezodiz.mjs} +1 -1
- package/dist/{db-errors-l1Qh2RPR.mjs.map → db-errors-WRezodiz.mjs.map} +1 -1
- package/dist/{default-DCVqE5ib.mjs → default-D8ksjWhO.mjs} +1 -1
- package/dist/{default-DCVqE5ib.mjs.map → default-D8ksjWhO.mjs.map} +1 -1
- package/dist/{dialect-helpers-DhTzaUxP.mjs → dialect-helpers-BKCvISIQ.mjs} +19 -2
- package/dist/dialect-helpers-BKCvISIQ.mjs.map +1 -0
- package/dist/{error-zG5T1UGA.mjs → error-D_-tqP-I.mjs} +1 -1
- package/dist/{error-zG5T1UGA.mjs.map → error-D_-tqP-I.mjs.map} +1 -1
- package/dist/{index-DIb-CzNx.d.mts → index-BFRaVcD6.d.mts} +94 -34
- package/dist/index-BFRaVcD6.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +29 -27
- package/dist/{load-CyEoextb.mjs → load-DDqMMvZL.mjs} +2 -2
- package/dist/{load-CyEoextb.mjs.map → load-DDqMMvZL.mjs.map} +1 -1
- package/dist/{loader-CndGj8kM.mjs → loader-CKLbBnhK.mjs} +27 -7
- package/dist/loader-CKLbBnhK.mjs.map +1 -0
- package/dist/{manifest-schema-DH9xhc6t.mjs → manifest-schema-DqWNC3lM.mjs} +33 -3
- package/dist/manifest-schema-DqWNC3lM.mjs.map +1 -0
- 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/media/local-runtime.mjs +3 -3
- package/dist/{media-D8FbNsl0.mjs → media-BW32b4gi.mjs} +2 -2
- package/dist/{media-D8FbNsl0.mjs.map → media-BW32b4gi.mjs.map} +1 -1
- package/dist/{mode-BnAOqItE.mjs → mode-ier8jbBk.mjs} +1 -1
- package/dist/{mode-BnAOqItE.mjs.map → mode-ier8jbBk.mjs.map} +1 -1
- package/dist/options-BVp3UsTS.mjs +117 -0
- package/dist/options-BVp3UsTS.mjs.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/{placeholder-D29tWZ7o.d.mts → placeholder-BE4o_2dc.d.mts} +1 -1
- package/dist/{placeholder-D29tWZ7o.d.mts.map → placeholder-BE4o_2dc.d.mts.map} +1 -1
- package/dist/{placeholder-C-fk5hYI.mjs → placeholder-CIJejMlK.mjs} +1 -1
- package/dist/{placeholder-C-fk5hYI.mjs.map → placeholder-CIJejMlK.mjs.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
- package/dist/plugins/adapt-sandbox-entry.mjs +6 -5
- package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
- package/dist/public-url-DByxYjUw.mjs +51 -0
- package/dist/public-url-DByxYjUw.mjs.map +1 -0
- package/dist/{query-fqEdLFms.mjs → query-Cg9ZKRQ0.mjs} +114 -16
- package/dist/query-Cg9ZKRQ0.mjs.map +1 -0
- package/dist/{redirect-D_pshWdf.mjs → redirect-BhUBKRc1.mjs} +11 -6
- package/dist/redirect-BhUBKRc1.mjs.map +1 -0
- package/dist/{registry-C3Mr0ODu.mjs → registry-Dw70ChxB.mjs} +38 -4
- package/dist/registry-Dw70ChxB.mjs.map +1 -0
- package/dist/{request-cache-Ci7f5pBb.mjs → request-cache-B-bmkipQ.mjs} +1 -1
- package/dist/{request-cache-Ci7f5pBb.mjs.map → request-cache-B-bmkipQ.mjs.map} +1 -1
- package/dist/runner-Bnoj7vjK.d.mts +44 -0
- package/dist/runner-Bnoj7vjK.d.mts.map +1 -0
- package/dist/{runner-tQ7BJ4T7.mjs → runner-C7ADox5q.mjs} +185 -55
- package/dist/{runner-tQ7BJ4T7.mjs.map → runner-C7ADox5q.mjs.map} +1 -1
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +4 -4
- package/dist/{search-BoZYFuUk.mjs → search-dOGEccMa.mjs} +129 -83
- package/dist/search-dOGEccMa.mjs.map +1 -0
- package/dist/secrets-CW3reAnU.mjs +314 -0
- package/dist/secrets-CW3reAnU.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +15 -14
- 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-B4IAshV8.mjs → taxonomies-ZlRtD6AG.mjs} +14 -7
- package/dist/taxonomies-ZlRtD6AG.mjs.map +1 -0
- package/dist/{tokens-D9vnZqYS.mjs → tokens-D7zMmWi2.mjs} +2 -2
- package/dist/{tokens-D9vnZqYS.mjs.map → tokens-D7zMmWi2.mjs.map} +1 -1
- package/dist/{transport-C9ugt2Nr.mjs → transport-BeMCmin1.mjs} +6 -5
- package/dist/{transport-C9ugt2Nr.mjs.map → transport-BeMCmin1.mjs.map} +1 -1
- package/dist/{transport-CUnEL3Vs.d.mts → transport-DNEfeMaU.d.mts} +1 -1
- package/dist/{transport-CUnEL3Vs.d.mts.map → transport-DNEfeMaU.d.mts.map} +1 -1
- package/dist/types-4fVtCIm0.mjs +68 -0
- package/dist/types-4fVtCIm0.mjs.map +1 -0
- package/dist/{types-BmPPSUEx.d.mts → types-BSyXeCFW.d.mts} +24 -2
- package/dist/{types-BmPPSUEx.d.mts.map → types-BSyXeCFW.d.mts.map} +1 -1
- package/dist/{types-i36XcA_X.d.mts → types-BuBIptGk.d.mts} +65 -134
- package/dist/types-BuBIptGk.d.mts.map +1 -0
- package/dist/{types-CgqmmMJB.mjs → types-CDbKp7ND.mjs} +1 -1
- package/dist/{types-CgqmmMJB.mjs.map → types-CDbKp7ND.mjs.map} +1 -1
- package/dist/{types-Bm1dn-q3.mjs → types-CIOg5AR8.mjs} +1 -1
- package/dist/{types-Bm1dn-q3.mjs.map → types-CIOg5AR8.mjs.map} +1 -1
- package/dist/{types-BrA0xf5I.d.mts → types-CJsYGpco.d.mts} +1 -1
- package/dist/{types-BrA0xf5I.d.mts.map → types-CJsYGpco.d.mts.map} +1 -1
- package/dist/{types-BIgulNsW.mjs → types-CRxNbK-Z.mjs} +2 -2
- package/dist/{types-BIgulNsW.mjs.map → types-CRxNbK-Z.mjs.map} +1 -1
- package/dist/{types-CS8FIX7L.d.mts → types-CrtWgIvl.d.mts} +1 -1
- package/dist/{types-CS8FIX7L.d.mts.map → types-CrtWgIvl.d.mts.map} +1 -1
- package/dist/{types-DIMwPFub.d.mts → types-M78DQ1lx.d.mts} +1 -1
- package/dist/{types-DIMwPFub.d.mts.map → types-M78DQ1lx.d.mts.map} +1 -1
- package/dist/{validate-CxVsLehf.mjs → validate-Baqf0slj.mjs} +3 -3
- package/dist/{validate-CxVsLehf.mjs.map → validate-Baqf0slj.mjs.map} +1 -1
- package/dist/{validate-DHxmpFJt.d.mts → validate-BfQh_C_y.d.mts} +4 -4
- package/dist/{validate-DHxmpFJt.d.mts.map → validate-BfQh_C_y.d.mts.map} +1 -1
- package/dist/{validation-C-ZpN2GI.mjs → validation-BfEI7tNe.mjs} +6 -6
- package/dist/{validation-C-ZpN2GI.mjs.map → validation-BfEI7tNe.mjs.map} +1 -1
- package/dist/version-DoxrVdYf.mjs +7 -0
- package/dist/{version-Bbq8TCrz.mjs.map → version-DoxrVdYf.mjs.map} +1 -1
- package/dist/{zod-generator-CpwccCIv.mjs → zod-generator-CC0xNe_K.mjs} +4 -4
- package/dist/zod-generator-CC0xNe_K.mjs.map +1 -0
- package/locals.d.ts +1 -6
- package/package.json +9 -8
- package/src/api/handlers/comments.ts +6 -4
- package/src/api/handlers/content.ts +29 -1
- package/src/api/handlers/device-flow.ts +5 -0
- package/src/api/handlers/marketplace.ts +11 -4
- package/src/api/handlers/oauth-authorization.ts +72 -33
- package/src/api/handlers/revision.ts +23 -14
- package/src/api/handlers/taxonomies.ts +3 -6
- package/src/api/public-url.ts +48 -2
- package/src/api/schemas/comments.ts +2 -2
- package/src/api/schemas/content.ts +17 -0
- package/src/api/schemas/sections.ts +3 -3
- package/src/api/schemas/users.ts +1 -1
- package/src/api/types.ts +5 -1
- package/src/astro/integration/index.ts +17 -0
- package/src/astro/integration/runtime.ts +30 -0
- package/src/astro/integration/virtual-modules.ts +32 -2
- package/src/astro/integration/vite-config.ts +6 -1
- package/src/astro/middleware/auth.ts +13 -6
- package/src/astro/middleware/redirect.ts +29 -16
- package/src/astro/middleware/request-context.ts +15 -5
- package/src/astro/middleware.ts +23 -9
- package/src/astro/routes/api/auth/invite/complete.ts +6 -1
- package/src/astro/routes/api/auth/passkey/register/verify.ts +6 -1
- package/src/astro/routes/api/auth/passkey/verify.ts +6 -1
- package/src/astro/routes/api/auth/signup/complete.ts +6 -1
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +2 -2
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +34 -12
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +32 -2
- package/src/astro/routes/api/content/[collection]/[id]/restore.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +3 -2
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +8 -4
- package/src/astro/routes/api/content/[collection]/[id].ts +12 -0
- package/src/astro/routes/api/import/wordpress/execute.ts +3 -1
- package/src/astro/routes/api/import/wordpress/prepare.ts +7 -8
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +3 -1
- package/src/astro/routes/api/manifest.ts +62 -45
- package/src/astro/routes/api/media/[id]/confirm.ts +10 -1
- package/src/astro/routes/api/media/providers/[providerId]/index.ts +12 -3
- package/src/astro/routes/api/openapi.json.ts +27 -10
- package/src/astro/routes/api/redirects/404s/index.ts +10 -4
- package/src/astro/routes/api/redirects/404s/summary.ts +4 -2
- package/src/astro/routes/api/redirects/[id].ts +10 -4
- package/src/astro/routes/api/redirects/index.ts +7 -3
- package/src/astro/routes/api/revisions/[revisionId]/index.ts +1 -1
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +0 -2
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +0 -1
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +0 -1
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +2 -2
- package/src/astro/routes/api/schema/collections/index.ts +1 -1
- package/src/astro/routes/api/search/index.ts +10 -2
- package/src/astro/routes/api/sections/[slug].ts +10 -4
- package/src/astro/routes/api/sections/index.ts +7 -3
- package/src/astro/routes/api/setup/admin-verify.ts +6 -1
- package/src/astro/routes/api/snapshot.ts +44 -18
- package/src/astro/routes/api/taxonomies/index.ts +0 -1
- package/src/astro/routes/api/themes/preview.ts +11 -5
- package/src/astro/types.ts +23 -3
- package/src/auth/allowed-origins.ts +168 -0
- package/src/auth/passkey-config.ts +35 -13
- package/src/bylines/index.ts +37 -88
- package/src/cli/commands/auth.ts +28 -6
- package/src/cli/commands/bundle-utils.ts +11 -2
- package/src/cli/commands/bundle.ts +28 -8
- package/src/cli/commands/content.ts +13 -0
- package/src/cli/commands/login.ts +8 -1
- package/src/cli/commands/publish.ts +24 -0
- package/src/cli/commands/secrets.ts +183 -0
- package/src/cli/credentials.ts +1 -1
- package/src/cli/index.ts +5 -1
- package/src/client/index.ts +4 -4
- package/src/client/transport.ts +17 -7
- package/src/components/Break.astro +2 -2
- package/src/components/EmDashHead.astro +18 -13
- package/src/components/Embed.astro +1 -1
- package/src/components/Gallery.astro +1 -1
- package/src/components/Image.astro +1 -1
- package/src/components/InlinePortableTextEditor.tsx +104 -18
- package/src/config/secrets.ts +528 -0
- package/src/database/dialect-helpers.ts +50 -0
- package/src/database/migrations/034_published_at_index.ts +1 -1
- package/src/database/migrations/035_bounded_404_log.ts +56 -39
- package/src/database/migrations/runner.ts +156 -23
- package/src/database/repositories/content.ts +36 -12
- package/src/database/repositories/redirect.ts +14 -3
- package/src/database/repositories/taxonomy.ts +26 -0
- package/src/db/libsql.ts +1 -3
- package/src/db/sqlite.ts +2 -5
- package/src/emdash-runtime.ts +84 -159
- package/src/index.ts +9 -0
- package/src/loader.ts +24 -1
- package/src/mcp/server.ts +103 -36
- package/src/page/site-identity.ts +58 -0
- package/src/plugins/adapt-sandbox-entry.ts +22 -10
- package/src/plugins/context.ts +13 -10
- package/src/plugins/define-plugin.ts +40 -12
- package/src/plugins/hooks.ts +23 -19
- package/src/plugins/index.ts +9 -0
- package/src/plugins/manifest-schema.ts +37 -2
- package/src/plugins/types.ts +151 -11
- package/src/preview/urls.ts +23 -3
- package/src/query.ts +148 -5
- package/src/redirects/cache.ts +38 -18
- package/src/schema/registry.ts +56 -0
- package/src/schema/zod-generator.ts +27 -5
- package/src/seed/apply.ts +2 -0
- package/src/settings/index.ts +80 -6
- package/src/settings/types.ts +23 -1
- package/src/taxonomies/index.ts +11 -1
- package/dist/apply-x0eMK1lX.mjs.map +0 -1
- package/dist/bylines-CRNsVG88.mjs +0 -157
- package/dist/bylines-CRNsVG88.mjs.map +0 -1
- package/dist/cache-BkKBuIvS.mjs +0 -56
- package/dist/cache-BkKBuIvS.mjs.map +0 -1
- package/dist/chunk-ClPoSABd.mjs +0 -21
- package/dist/dialect-helpers-DhTzaUxP.mjs.map +0 -1
- package/dist/index-DIb-CzNx.d.mts.map +0 -1
- package/dist/loader-CndGj8kM.mjs.map +0 -1
- package/dist/manifest-schema-DH9xhc6t.mjs.map +0 -1
- package/dist/query-fqEdLFms.mjs.map +0 -1
- package/dist/redirect-D_pshWdf.mjs.map +0 -1
- package/dist/registry-C3Mr0ODu.mjs.map +0 -1
- package/dist/runner-OURCaApa.d.mts +0 -34
- package/dist/runner-OURCaApa.d.mts.map +0 -1
- package/dist/search-BoZYFuUk.mjs.map +0 -1
- package/dist/taxonomies-B4IAshV8.mjs.map +0 -1
- package/dist/types-i36XcA_X.d.mts.map +0 -1
- package/dist/version-Bbq8TCrz.mjs +0 -7
- package/dist/zod-generator-CpwccCIv.mjs.map +0 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolution and validation of multi-origin passkey verification.
|
|
3
|
+
*
|
|
4
|
+
* `allowedOrigins` lets one EmDash deployment accept passkey assertions from
|
|
5
|
+
* several hostnames sharing the same `rpId` (e.g. apex + preview/staging
|
|
6
|
+
* subdomains under one registrable parent). Origins come from two sources:
|
|
7
|
+
*
|
|
8
|
+
* - `EmDashConfig.allowedOrigins` (declared in `astro.config.mjs`)
|
|
9
|
+
* - `EMDASH_ALLOWED_ORIGINS` (comma-separated runtime env var)
|
|
10
|
+
*
|
|
11
|
+
* Sources are merged (union of permissions, deduplicated). Each entry is
|
|
12
|
+
* validated against `siteUrl` to fail loud on dead config the browser would
|
|
13
|
+
* never honor.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { getEnvAllowedOrigins } from "../api/public-url.js";
|
|
17
|
+
import type { EmDashConfig } from "../astro/integration/runtime.js";
|
|
18
|
+
|
|
19
|
+
export type AllowedOriginSource = "config.allowedOrigins" | "EMDASH_ALLOWED_ORIGINS";
|
|
20
|
+
|
|
21
|
+
export interface TaggedOrigin {
|
|
22
|
+
/** Raw entry as declared by the operator. */
|
|
23
|
+
origin: string;
|
|
24
|
+
/** Where the entry came from (used for source-attributed errors). */
|
|
25
|
+
source: AllowedOriginSource;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Collect raw allowedOrigins from config and env, source-tagged.
|
|
30
|
+
*
|
|
31
|
+
* Returns raw values — the caller is expected to pass the result through
|
|
32
|
+
* `validateAllowedOrigins()` before use in passkey verification.
|
|
33
|
+
*/
|
|
34
|
+
export function getConfiguredAllowedOrigins(config?: EmDashConfig): TaggedOrigin[] {
|
|
35
|
+
const tagged: TaggedOrigin[] = [];
|
|
36
|
+
if (config?.allowedOrigins) {
|
|
37
|
+
for (const origin of config.allowedOrigins) {
|
|
38
|
+
if (origin) tagged.push({ origin, source: "config.allowedOrigins" });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const origin of getEnvAllowedOrigins()) {
|
|
42
|
+
tagged.push({ origin, source: "EMDASH_ALLOWED_ORIGINS" });
|
|
43
|
+
}
|
|
44
|
+
return tagged;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Validate per-entry shape rules (no `siteUrl` needed):
|
|
49
|
+
* - parses as `URL`
|
|
50
|
+
* - protocol is `http:` or `https:`
|
|
51
|
+
* - hostname has no trailing dot (`example.com.` rejected)
|
|
52
|
+
* - hostname has no empty labels (`foo..example.com` rejected)
|
|
53
|
+
*
|
|
54
|
+
* Returns the deduplicated, normalized origin form (`URL.origin`) of every
|
|
55
|
+
* input, in input order. Throws on the first violation with a source-tagged
|
|
56
|
+
* error message.
|
|
57
|
+
*/
|
|
58
|
+
export function validateOriginShape(tagged: TaggedOrigin[]): string[] {
|
|
59
|
+
const normalized: string[] = [];
|
|
60
|
+
const seen = new Set<string>();
|
|
61
|
+
for (const { origin, source } of tagged) {
|
|
62
|
+
let parsed: URL;
|
|
63
|
+
try {
|
|
64
|
+
parsed = new URL(origin);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
throw configError(source, `invalid URL: "${origin}"`, e);
|
|
67
|
+
}
|
|
68
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
69
|
+
throw configError(
|
|
70
|
+
source,
|
|
71
|
+
`origin must be http or https: "${origin}" (got ${parsed.protocol})`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (parsed.hostname.endsWith(".")) {
|
|
75
|
+
throw configError(
|
|
76
|
+
source,
|
|
77
|
+
`hostname has a trailing dot: "${origin}". Remove the trailing dot — assertion origins from the browser do not include it.`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (parsed.hostname.split(".").includes("")) {
|
|
81
|
+
throw configError(source, `hostname has empty labels: "${origin}"`);
|
|
82
|
+
}
|
|
83
|
+
if (!seen.has(parsed.origin)) {
|
|
84
|
+
seen.add(parsed.origin);
|
|
85
|
+
normalized.push(parsed.origin);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return normalized;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Validate the effective merged allowedOrigins set against `siteUrl`.
|
|
93
|
+
*
|
|
94
|
+
* Performs `validateOriginShape()` plus the siteUrl-dependent rules:
|
|
95
|
+
* - Rule A: non-empty origins ⇒ `siteUrl` is set
|
|
96
|
+
* - `siteUrl` hostname is not an IP literal (multi-origin requires a domain)
|
|
97
|
+
* - `siteUrl` hostname has no trailing dot (cannot match assertion origins)
|
|
98
|
+
* - Rule B: each origin's hostname is `siteHost` exactly or a subdomain
|
|
99
|
+
*
|
|
100
|
+
* Throws on first violation. Returns the deduplicated normalized origins.
|
|
101
|
+
*
|
|
102
|
+
* Use this at the runtime chokepoint (where config + env are merged into the
|
|
103
|
+
* effective set). At Astro integration init, prefer `validateOriginShape()`
|
|
104
|
+
* for shape-only checks on `config.allowedOrigins`, since `siteUrl` may be
|
|
105
|
+
* supplied at runtime via `EMDASH_SITE_URL`.
|
|
106
|
+
*/
|
|
107
|
+
export function validateAllowedOrigins(
|
|
108
|
+
siteUrl: string | undefined,
|
|
109
|
+
tagged: TaggedOrigin[],
|
|
110
|
+
): string[] {
|
|
111
|
+
const normalized = validateOriginShape(tagged);
|
|
112
|
+
if (normalized.length === 0) return normalized;
|
|
113
|
+
|
|
114
|
+
if (!siteUrl) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`EmDash config error: allowedOrigins is set (${normalized.length} ${
|
|
117
|
+
normalized.length === 1 ? "entry" : "entries"
|
|
118
|
+
}) but siteUrl is not. Without a canonical siteUrl, rpId is derived from the request hostname, defeating multi-origin passkeys. Set siteUrl in astro.config.mjs or via EMDASH_SITE_URL.`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let siteHost: string;
|
|
123
|
+
try {
|
|
124
|
+
siteHost = new URL(siteUrl).hostname;
|
|
125
|
+
} catch (e) {
|
|
126
|
+
throw new Error(`EmDash config error: siteUrl is not a valid URL: "${siteUrl}"`, {
|
|
127
|
+
cause: e,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (siteHost.endsWith(".")) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`EmDash config error: siteUrl "${siteUrl}" has a trailing-dot hostname, which cannot match assertion origins. Remove the trailing dot when using allowedOrigins.`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (isIPLiteralHostname(siteHost)) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`EmDash config error: siteUrl "${siteUrl}" uses an IP-literal hostname. Multi-origin passkeys require a domain-based siteUrl — IP addresses cannot have valid subdomains for WebAuthn rpId.`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const { origin, source } of tagged) {
|
|
143
|
+
const h = new URL(origin).hostname;
|
|
144
|
+
if (h !== siteHost && !h.endsWith("." + siteHost)) {
|
|
145
|
+
throw configError(
|
|
146
|
+
source,
|
|
147
|
+
`"${origin}" is not a subdomain of siteUrl "${siteUrl}". Allowed origins must be the same hostname as siteUrl or a subdomain of it.`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return normalized;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function configError(source: AllowedOriginSource, detail: string, cause?: unknown): Error {
|
|
156
|
+
const err = new Error(`EmDash config error in ${source}: ${detail}`);
|
|
157
|
+
if (cause !== undefined) (err as Error & { cause?: unknown }).cause = cause;
|
|
158
|
+
return err;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const IPV4_DOTTED_DECIMAL_RE = /^\d+(\.\d+){3}$/;
|
|
162
|
+
|
|
163
|
+
function isIPLiteralHostname(h: string): boolean {
|
|
164
|
+
// IPv6 hostnames are bracketed by URL.hostname, e.g. "[::1]"
|
|
165
|
+
if (h.startsWith("[")) return true;
|
|
166
|
+
// IPv4 dotted-decimal
|
|
167
|
+
return IPV4_DOTTED_DECIMAL_RE.test(h);
|
|
168
|
+
}
|
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
export interface PasskeyConfig {
|
|
10
10
|
rpName: string;
|
|
11
11
|
rpId: string;
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Accepted client-data origins. First entry is the canonical/preferred origin;
|
|
14
|
+
* additional entries support multi-origin deployments (e.g. apex + preview
|
|
15
|
+
* subdomain sharing the same `rpId`). See `allowedOrigins` parameter.
|
|
16
|
+
*/
|
|
17
|
+
origins: string[];
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
/**
|
|
@@ -18,10 +23,22 @@ export interface PasskeyConfig {
|
|
|
18
23
|
* @param url The request URL (typically `new URL(Astro.request.url)` or `new URL(request.url)`)
|
|
19
24
|
* @param siteName Optional site name for rpName (defaults to hostname from `url` or public origin)
|
|
20
25
|
* @param siteUrl Optional browser-facing origin (see `EmDashConfig.siteUrl`).
|
|
21
|
-
* When set, **origin** and **rpId** are taken from this URL
|
|
26
|
+
* When set, the canonical **origin** and **rpId** are taken from this URL.
|
|
27
|
+
* @param allowedOrigins Optional list of additional accepted origins for verification.
|
|
28
|
+
* Each must share `rpId` with the canonical origin (WebAuthn requirement).
|
|
29
|
+
* Typical use: apex + preview subdomain on the same registrable domain.
|
|
22
30
|
* @throws If `siteUrl` is non-empty but not parseable by `new URL()`.
|
|
23
31
|
*/
|
|
24
|
-
export function getPasskeyConfig(
|
|
32
|
+
export function getPasskeyConfig(
|
|
33
|
+
url: URL,
|
|
34
|
+
siteName?: string,
|
|
35
|
+
siteUrl?: string,
|
|
36
|
+
allowedOrigins?: string[],
|
|
37
|
+
): PasskeyConfig {
|
|
38
|
+
let rpName: string;
|
|
39
|
+
let rpId: string;
|
|
40
|
+
let canonicalOrigin: string;
|
|
41
|
+
|
|
25
42
|
if (siteUrl) {
|
|
26
43
|
let publicUrl: URL;
|
|
27
44
|
try {
|
|
@@ -29,16 +46,21 @@ export function getPasskeyConfig(url: URL, siteName?: string, siteUrl?: string):
|
|
|
29
46
|
} catch (e) {
|
|
30
47
|
throw new Error(`Invalid siteUrl: "${siteUrl}"`, { cause: e });
|
|
31
48
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
rpName = siteName || publicUrl.hostname;
|
|
50
|
+
rpId = publicUrl.hostname;
|
|
51
|
+
canonicalOrigin = publicUrl.origin;
|
|
52
|
+
} else {
|
|
53
|
+
rpName = siteName || url.hostname;
|
|
54
|
+
rpId = url.hostname;
|
|
55
|
+
canonicalOrigin = url.origin;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const origins = [canonicalOrigin];
|
|
59
|
+
if (allowedOrigins) {
|
|
60
|
+
for (const extra of allowedOrigins) {
|
|
61
|
+
if (extra && !origins.includes(extra)) origins.push(extra);
|
|
62
|
+
}
|
|
37
63
|
}
|
|
38
64
|
|
|
39
|
-
return {
|
|
40
|
-
rpName: siteName || url.hostname,
|
|
41
|
-
rpId: url.hostname,
|
|
42
|
-
origin: url.origin,
|
|
43
|
-
};
|
|
65
|
+
return { rpName, rpId, origins };
|
|
44
66
|
}
|
package/src/bylines/index.ts
CHANGED
|
@@ -12,7 +12,6 @@ import { BylineRepository } from "../database/repositories/byline.js";
|
|
|
12
12
|
import type { BylineSummary, ContentBylineCredit } from "../database/repositories/types.js";
|
|
13
13
|
import { validateIdentifier } from "../database/validate.js";
|
|
14
14
|
import { getDb } from "../loader.js";
|
|
15
|
-
import { chunks, SQL_BATCH_SIZE } from "../utils/chunks.js";
|
|
16
15
|
import { isMissingTableError } from "../utils/db-errors.js";
|
|
17
16
|
|
|
18
17
|
/**
|
|
@@ -73,15 +72,11 @@ export async function getBylineBySlug(slug: string): Promise<BylineSummary | nul
|
|
|
73
72
|
* but the entry has an `authorId`, falls back to the user-linked byline
|
|
74
73
|
* (marked as source: "inferred").
|
|
75
74
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* for (const credit of bylines) {
|
|
82
|
-
* console.log(credit.byline.displayName, credit.roleLabel);
|
|
83
|
-
* }
|
|
84
|
-
* ```
|
|
75
|
+
* Internal: not re-exported from the `emdash` package entry point. Every
|
|
76
|
+
* entry returned by `getEmDashCollection` / `getEmDashEntry` already has
|
|
77
|
+
* `data.bylines` populated by `hydrateEntryBylines` (which uses the batch
|
|
78
|
+
* helper `getBylinesForEntries` directly). Site code should read those
|
|
79
|
+
* fields rather than calling this function.
|
|
85
80
|
*/
|
|
86
81
|
export async function getEntryBylines(
|
|
87
82
|
collection: string,
|
|
@@ -108,55 +103,55 @@ export async function getEntryBylines(
|
|
|
108
103
|
return [];
|
|
109
104
|
}
|
|
110
105
|
|
|
106
|
+
/**
|
|
107
|
+
* An entry reference for batch byline lookups.
|
|
108
|
+
*
|
|
109
|
+
* `authorId` is read directly from the row when computing the inferred-byline
|
|
110
|
+
* fallback — passing it in avoids a redundant `SELECT id, author_id` against
|
|
111
|
+
* the content table after every list/entry fetch.
|
|
112
|
+
*/
|
|
113
|
+
export interface BylineEntry {
|
|
114
|
+
id: string;
|
|
115
|
+
authorId: string | null;
|
|
116
|
+
}
|
|
117
|
+
|
|
111
118
|
/**
|
|
112
119
|
* Batch-fetch byline credits for multiple content entries in a single query.
|
|
113
120
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
121
|
+
* Internal: consumed by `hydrateEntryBylines` in `query.ts` so that every
|
|
122
|
+
* entry returned from `getEmDashCollection` / `getEmDashEntry` already has
|
|
123
|
+
* `data.bylines` populated. Site code should rely on that eager hydration
|
|
124
|
+
* rather than calling this directly -- this function is not re-exported
|
|
125
|
+
* from the `emdash` package entry point.
|
|
116
126
|
*
|
|
117
127
|
* @param collection - The collection slug (e.g., "posts")
|
|
118
|
-
* @param
|
|
128
|
+
* @param entries - Entry id + authorId pairs (authorId is already on the row)
|
|
119
129
|
* @returns Map from entry ID to array of byline credits
|
|
120
|
-
*
|
|
121
|
-
* @example
|
|
122
|
-
* ```ts
|
|
123
|
-
* import { getBylinesForEntries, getEmDashCollection } from "emdash";
|
|
124
|
-
*
|
|
125
|
-
* const { entries } = await getEmDashCollection("posts");
|
|
126
|
-
* const ids = entries.map(e => e.data.id);
|
|
127
|
-
* const bylinesMap = await getBylinesForEntries("posts", ids);
|
|
128
|
-
*
|
|
129
|
-
* for (const entry of entries) {
|
|
130
|
-
* const bylines = bylinesMap.get(entry.data.id) ?? [];
|
|
131
|
-
* // render bylines
|
|
132
|
-
* }
|
|
133
|
-
* ```
|
|
134
130
|
*/
|
|
135
131
|
export async function getBylinesForEntries(
|
|
136
132
|
collection: string,
|
|
137
|
-
|
|
133
|
+
entries: BylineEntry[],
|
|
138
134
|
): Promise<Map<string, ContentBylineCredit[]>> {
|
|
139
135
|
validateIdentifier(collection, "collection");
|
|
140
136
|
const result = new Map<string, ContentBylineCredit[]>();
|
|
141
137
|
|
|
142
|
-
|
|
143
|
-
for (const id of entryIds) {
|
|
138
|
+
for (const { id } of entries) {
|
|
144
139
|
result.set(id, []);
|
|
145
140
|
}
|
|
146
141
|
|
|
147
|
-
if (
|
|
142
|
+
if (entries.length === 0) {
|
|
148
143
|
return result;
|
|
149
144
|
}
|
|
150
145
|
|
|
151
146
|
const db = await getDb();
|
|
152
147
|
const repo = new BylineRepository(db);
|
|
148
|
+
const entryIds = entries.map((e) => e.id);
|
|
153
149
|
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
// `isMissingTableError` catch below and return empty results.
|
|
150
|
+
// Sites with no bylines get an empty map back for one query — the previous
|
|
151
|
+
// "has any bylines" probe traded an extra round-trip on every request to
|
|
152
|
+
// save that one query on empty sites, which is exactly backwards for the
|
|
153
|
+
// common case. Pre-migration databases (bylines table missing) fall
|
|
154
|
+
// through to the `isMissingTableError` catch below and return empty.
|
|
160
155
|
let bylinesMap;
|
|
161
156
|
try {
|
|
162
157
|
bylinesMap = await repo.getContentBylinesMany(collection, entryIds);
|
|
@@ -165,32 +160,17 @@ export async function getBylinesForEntries(
|
|
|
165
160
|
throw error;
|
|
166
161
|
}
|
|
167
162
|
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
for (const id of entryIds) {
|
|
173
|
-
if (!bylinesMap.has(id)) {
|
|
174
|
-
// Need to check author_id for this entry — but we only have the IDs,
|
|
175
|
-
// so batch-fetch them from the content table
|
|
176
|
-
fallbackEntryIds.push(id);
|
|
163
|
+
const needsFallback = new Map<string, string>();
|
|
164
|
+
for (const { id, authorId } of entries) {
|
|
165
|
+
if (!bylinesMap.has(id) && authorId) {
|
|
166
|
+
needsFallback.set(id, authorId);
|
|
177
167
|
}
|
|
178
168
|
}
|
|
179
169
|
|
|
180
|
-
// Batch-fetch author_ids for entries that need fallback
|
|
181
|
-
if (fallbackEntryIds.length > 0) {
|
|
182
|
-
const authorMap = await getAuthorIds(db, collection, fallbackEntryIds);
|
|
183
|
-
for (const [entryId, authorId] of authorMap) {
|
|
184
|
-
needsFallback.set(entryId, authorId);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// 3. Batch fetch user-linked bylines for fallback
|
|
189
170
|
const uniqueAuthorIds = [...new Set(needsFallback.values())];
|
|
190
171
|
const authorBylineMap = await repo.findByUserIds(uniqueAuthorIds);
|
|
191
172
|
|
|
192
|
-
|
|
193
|
-
for (const id of entryIds) {
|
|
173
|
+
for (const { id } of entries) {
|
|
194
174
|
const explicit = bylinesMap.get(id);
|
|
195
175
|
if (explicit && explicit.length > 0) {
|
|
196
176
|
result.set(
|
|
@@ -205,11 +185,8 @@ export async function getBylinesForEntries(
|
|
|
205
185
|
const fallback = authorBylineMap.get(authorId);
|
|
206
186
|
if (fallback) {
|
|
207
187
|
result.set(id, [{ byline: fallback, sortOrder: 0, roleLabel: null, source: "inferred" }]);
|
|
208
|
-
continue;
|
|
209
188
|
}
|
|
210
189
|
}
|
|
211
|
-
|
|
212
|
-
// Already initialized with empty array
|
|
213
190
|
}
|
|
214
191
|
|
|
215
192
|
return result;
|
|
@@ -235,31 +212,3 @@ async function getAuthorId(
|
|
|
235
212
|
|
|
236
213
|
return result.rows[0]?.author_id ?? null;
|
|
237
214
|
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Batch-fetch author_ids for multiple content entries.
|
|
241
|
-
* Returns Map<entryId, authorId> (only entries with non-null author_id).
|
|
242
|
-
*/
|
|
243
|
-
async function getAuthorIds(
|
|
244
|
-
db: Awaited<ReturnType<typeof getDb>>,
|
|
245
|
-
collection: string,
|
|
246
|
-
entryIds: string[],
|
|
247
|
-
): Promise<Map<string, string>> {
|
|
248
|
-
validateIdentifier(collection, "collection");
|
|
249
|
-
const tableName = `ec_${collection}`;
|
|
250
|
-
|
|
251
|
-
const map = new Map<string, string>();
|
|
252
|
-
for (const chunk of chunks(entryIds, SQL_BATCH_SIZE)) {
|
|
253
|
-
const result = await sql<{ id: string; author_id: string | null }>`
|
|
254
|
-
SELECT id, author_id FROM ${sql.ref(tableName)}
|
|
255
|
-
WHERE id IN (${sql.join(chunk.map((id) => sql`${id}`))})
|
|
256
|
-
`.execute(db);
|
|
257
|
-
|
|
258
|
-
for (const row of result.rows) {
|
|
259
|
-
if (row.author_id) {
|
|
260
|
-
map.set(row.id, row.author_id);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
return map;
|
|
265
|
-
}
|
package/src/cli/commands/auth.ts
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Auth CLI commands
|
|
2
|
+
* Auth CLI commands (deprecated)
|
|
3
|
+
*
|
|
4
|
+
* Kept as a deprecated alias for backwards compatibility. The original
|
|
5
|
+
* `emdash auth secret` was documented in published docs and is plausibly
|
|
6
|
+
* scripted in user CI (e.g. `npx emdash auth secret >> .env`). Removing
|
|
7
|
+
* it outright would break those scripts on minor-version upgrade.
|
|
8
|
+
*
|
|
9
|
+
* The command still emits an `EMDASH_AUTH_SECRET=<32-byte-base64url>`
|
|
10
|
+
* line, unchanged. `EMDASH_AUTH_SECRET` itself is now legacy: it's only
|
|
11
|
+
* read as a fallback source for the commenter-IP hash salt so installs
|
|
12
|
+
* upgrading from a prior version keep stable IP hashes (and therefore
|
|
13
|
+
* stable rate-limit buckets). New installs don't need to set it.
|
|
14
|
+
*
|
|
15
|
+
* The deprecation note steers users toward `emdash secrets generate`
|
|
16
|
+
* (which emits a different, versioned `emdash_enc_v1_*` value for
|
|
17
|
+
* `EMDASH_ENCRYPTION_KEY` — used to encrypt plugin secrets at rest).
|
|
18
|
+
*
|
|
19
|
+
* Will be removed in a future minor.
|
|
3
20
|
*/
|
|
4
21
|
|
|
5
22
|
import { defineCommand } from "citty";
|
|
@@ -8,9 +25,6 @@ import pc from "picocolors";
|
|
|
8
25
|
|
|
9
26
|
import { encodeBase64url } from "../../utils/base64.js";
|
|
10
27
|
|
|
11
|
-
/**
|
|
12
|
-
* Generate a cryptographically secure auth secret
|
|
13
|
-
*/
|
|
14
28
|
function generateAuthSecret(): string {
|
|
15
29
|
const bytes = new Uint8Array(32);
|
|
16
30
|
crypto.getRandomValues(bytes);
|
|
@@ -20,11 +34,13 @@ function generateAuthSecret(): string {
|
|
|
20
34
|
const secretCommand = defineCommand({
|
|
21
35
|
meta: {
|
|
22
36
|
name: "secret",
|
|
23
|
-
description: "Generate a
|
|
37
|
+
description: "[DEPRECATED] Generate a value for legacy EMDASH_AUTH_SECRET",
|
|
24
38
|
},
|
|
25
39
|
run() {
|
|
26
40
|
const secret = generateAuthSecret();
|
|
27
41
|
|
|
42
|
+
// Match the original behavior verbatim: pretty-printed name=value
|
|
43
|
+
// on stdout (most scripts piped this to a file expecting that shape).
|
|
28
44
|
consola.log("");
|
|
29
45
|
consola.log(pc.bold("Generated auth secret:"));
|
|
30
46
|
consola.log("");
|
|
@@ -32,13 +48,19 @@ const secretCommand = defineCommand({
|
|
|
32
48
|
consola.log("");
|
|
33
49
|
consola.log(pc.dim("Add this to your environment variables."));
|
|
34
50
|
consola.log("");
|
|
51
|
+
// Deprecation note on stderr so it doesn't break stdout consumers.
|
|
52
|
+
process.stderr.write(
|
|
53
|
+
`${pc.yellow("Note:")} ${pc.bold("emdash auth secret")} is deprecated and will be removed in a future minor. ` +
|
|
54
|
+
`${pc.cyan("EMDASH_AUTH_SECRET")} itself is now optional — it's only used as a legacy fallback for the commenter-IP hash salt. ` +
|
|
55
|
+
`For encrypting plugin secrets at rest, use ${pc.bold("emdash secrets generate")} (a different secret: ${pc.cyan("EMDASH_ENCRYPTION_KEY")}).\n`,
|
|
56
|
+
);
|
|
35
57
|
},
|
|
36
58
|
});
|
|
37
59
|
|
|
38
60
|
export const authCommand = defineCommand({
|
|
39
61
|
meta: {
|
|
40
62
|
name: "auth",
|
|
41
|
-
description: "Authentication utilities",
|
|
63
|
+
description: "[DEPRECATED] Authentication utilities (use `emdash secrets` for new flows)",
|
|
42
64
|
},
|
|
43
65
|
subCommands: {
|
|
44
66
|
secret: secretCommand,
|
|
@@ -30,8 +30,17 @@ export const ICON_SIZE = 256;
|
|
|
30
30
|
|
|
31
31
|
// ── Regex patterns (module-scope to avoid re-compilation) ────────────────────
|
|
32
32
|
|
|
33
|
-
/**
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Matches Node.js built-in imports in bundled output:
|
|
35
|
+
* - require("node:xxx") / require("xxx")
|
|
36
|
+
* - import("node:xxx") / import("xxx")
|
|
37
|
+
* - import X from "node:xxx" / import { X } from "node:xxx"
|
|
38
|
+
* - import * as X from "node:xxx"
|
|
39
|
+
* - export { X } from "node:xxx"
|
|
40
|
+
* Captures the base module name (e.g. "fs" from "node:fs/promises").
|
|
41
|
+
*/
|
|
42
|
+
const NODE_BUILTIN_IMPORT_RE =
|
|
43
|
+
/(?:import|export|require)\s*(?:\(|[^(]*?\bfrom\s+)["'](?:node:)?([a-z_]+)(?:\/[^"']*)?\s*["']\)?/g;
|
|
35
44
|
const LEADING_DOT_SLASH_RE = /^\.\//;
|
|
36
45
|
const DIST_PREFIX_RE = /^dist\//;
|
|
37
46
|
const MJS_EXT_RE = /\.m?js$/;
|
|
@@ -20,6 +20,7 @@ import { resolve, join, extname, basename } from "node:path";
|
|
|
20
20
|
import { defineCommand } from "citty";
|
|
21
21
|
import consola from "consola";
|
|
22
22
|
|
|
23
|
+
import { CAPABILITY_RENAMES, isDeprecatedCapability } from "../../plugins/types.js";
|
|
23
24
|
import type { ResolvedPlugin } from "../../plugins/types.js";
|
|
24
25
|
import {
|
|
25
26
|
fileExists,
|
|
@@ -524,20 +525,39 @@ export const bundleCommand = defineCommand({
|
|
|
524
525
|
}
|
|
525
526
|
}
|
|
526
527
|
|
|
527
|
-
// Check capabilities warnings
|
|
528
|
-
|
|
528
|
+
// Check capabilities warnings — use canonical names. Deprecated
|
|
529
|
+
// names are accepted (and warned about separately below) so we
|
|
530
|
+
// also check the legacy aliases here for the duration of the
|
|
531
|
+
// deprecation window.
|
|
532
|
+
const declaresUnrestricted =
|
|
533
|
+
manifest.capabilities.includes("network:request:unrestricted") ||
|
|
534
|
+
manifest.capabilities.includes("network:fetch:any");
|
|
535
|
+
const declaresHostRestricted =
|
|
536
|
+
manifest.capabilities.includes("network:request") ||
|
|
537
|
+
manifest.capabilities.includes("network:fetch");
|
|
538
|
+
if (declaresUnrestricted) {
|
|
529
539
|
consola.warn(
|
|
530
|
-
"Plugin declares unrestricted network access (network:
|
|
540
|
+
"Plugin declares unrestricted network access (network:request:unrestricted) — it can make requests to any host",
|
|
531
541
|
);
|
|
532
|
-
} else if (
|
|
533
|
-
manifest.capabilities.includes("network:fetch") &&
|
|
534
|
-
manifest.allowedHosts.length === 0
|
|
535
|
-
) {
|
|
542
|
+
} else if (declaresHostRestricted && manifest.allowedHosts.length === 0) {
|
|
536
543
|
consola.warn(
|
|
537
|
-
"Plugin declares network:
|
|
544
|
+
"Plugin declares network:request capability but no allowedHosts — all requests will be blocked",
|
|
538
545
|
);
|
|
539
546
|
}
|
|
540
547
|
|
|
548
|
+
// Warn for each deprecated capability used. The warning points
|
|
549
|
+
// to the new name so the fix is mechanical. We continue (not
|
|
550
|
+
// error) here — the hard fail lives in `publish` so authors
|
|
551
|
+
// can still build and test locally.
|
|
552
|
+
const deprecatedCaps = manifest.capabilities.filter(isDeprecatedCapability);
|
|
553
|
+
if (deprecatedCaps.length > 0) {
|
|
554
|
+
consola.warn("Plugin uses deprecated capability names. Rename them before publishing:");
|
|
555
|
+
for (const cap of deprecatedCaps) {
|
|
556
|
+
const replacement = CAPABILITY_RENAMES[cap];
|
|
557
|
+
consola.warn(` ${cap} → ${replacement}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
541
561
|
// Check for features that won't work in sandboxed mode
|
|
542
562
|
if (
|
|
543
563
|
resolvedPlugin.admin?.portableTextBlocks &&
|
|
@@ -9,6 +9,7 @@ import { readFile } from "node:fs/promises";
|
|
|
9
9
|
import { defineCommand } from "citty";
|
|
10
10
|
import { consola } from "consola";
|
|
11
11
|
|
|
12
|
+
import { convertDataForRead } from "../../client/portable-text.js";
|
|
12
13
|
import { connectionArgs, createClientFromArgs } from "../client-factory.js";
|
|
13
14
|
import { configureOutputMode, output } from "../output.js";
|
|
14
15
|
|
|
@@ -144,6 +145,13 @@ const getCommand = defineCommand({
|
|
|
144
145
|
const comparison = await client.compare(args.collection, args.id);
|
|
145
146
|
if (comparison.hasChanges && comparison.draft) {
|
|
146
147
|
item.data = comparison.draft;
|
|
148
|
+
// The comparison endpoint returns raw PT data. Apply the same
|
|
149
|
+
// PT-to-markdown conversion that `client.get` does, unless --raw.
|
|
150
|
+
if (!args.raw && item.data) {
|
|
151
|
+
const col = await client.collection(args.collection);
|
|
152
|
+
const fields = col.fields.map((f) => ({ slug: f.slug, type: f.type }));
|
|
153
|
+
item.data = convertDataForRead(item.data, fields, false);
|
|
154
|
+
}
|
|
147
155
|
}
|
|
148
156
|
}
|
|
149
157
|
|
|
@@ -278,6 +286,7 @@ const deleteCommand = defineCommand({
|
|
|
278
286
|
try {
|
|
279
287
|
const client = createClientFromArgs(args);
|
|
280
288
|
await client.delete(args.collection, args.id);
|
|
289
|
+
output({ success: true }, args);
|
|
281
290
|
consola.success(`Deleted ${args.collection}/${args.id}`);
|
|
282
291
|
} catch (error) {
|
|
283
292
|
consola.error(error instanceof Error ? error.message : "Unknown error");
|
|
@@ -306,6 +315,7 @@ const publishCommand = defineCommand({
|
|
|
306
315
|
try {
|
|
307
316
|
const client = createClientFromArgs(args);
|
|
308
317
|
await client.publish(args.collection, args.id);
|
|
318
|
+
output({ success: true }, args);
|
|
309
319
|
consola.success(`Published ${args.collection}/${args.id}`);
|
|
310
320
|
} catch (error) {
|
|
311
321
|
consola.error(error instanceof Error ? error.message : "Unknown error");
|
|
@@ -334,6 +344,7 @@ const unpublishCommand = defineCommand({
|
|
|
334
344
|
try {
|
|
335
345
|
const client = createClientFromArgs(args);
|
|
336
346
|
await client.unpublish(args.collection, args.id);
|
|
347
|
+
output({ success: true }, args);
|
|
337
348
|
consola.success(`Unpublished ${args.collection}/${args.id}`);
|
|
338
349
|
} catch (error) {
|
|
339
350
|
consola.error(error instanceof Error ? error.message : "Unknown error");
|
|
@@ -367,6 +378,7 @@ const scheduleCommand = defineCommand({
|
|
|
367
378
|
try {
|
|
368
379
|
const client = createClientFromArgs(args);
|
|
369
380
|
await client.schedule(args.collection, args.id, { at: args.at });
|
|
381
|
+
output({ success: true }, args);
|
|
370
382
|
consola.success(`Scheduled ${args.collection}/${args.id} for ${args.at}`);
|
|
371
383
|
} catch (error) {
|
|
372
384
|
consola.error(error instanceof Error ? error.message : "Unknown error");
|
|
@@ -395,6 +407,7 @@ const restoreCommand = defineCommand({
|
|
|
395
407
|
try {
|
|
396
408
|
const client = createClientFromArgs(args);
|
|
397
409
|
await client.restore(args.collection, args.id);
|
|
410
|
+
output({ success: true }, args);
|
|
398
411
|
consola.success(`Restored ${args.collection}/${args.id}`);
|
|
399
412
|
} catch (error) {
|
|
400
413
|
consola.error(error instanceof Error ? error.message : "Unknown error");
|
|
@@ -459,7 +459,14 @@ export const whoamiCommand = defineCommand({
|
|
|
459
459
|
},
|
|
460
460
|
);
|
|
461
461
|
if (refreshRes.ok) {
|
|
462
|
-
const
|
|
462
|
+
const json = (await refreshRes.json()) as Record<string, unknown>;
|
|
463
|
+
// Token endpoint wraps response in { data: ... } via apiSuccess.
|
|
464
|
+
// Handle both wrapped and bare shapes for robustness.
|
|
465
|
+
const refreshed = (
|
|
466
|
+
json.data && typeof json.data === "object" && "access_token" in json.data
|
|
467
|
+
? json.data
|
|
468
|
+
: json
|
|
469
|
+
) as TokenResponse;
|
|
463
470
|
token = refreshed.access_token;
|
|
464
471
|
saveCredentials(baseUrl, {
|
|
465
472
|
...cred,
|