emdash 0.1.0 → 0.1.1
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/LICENSE +9 -0
- package/dist/{apply-Bjfq_b4-.mjs → apply-kC39ev1Z.mjs} +4 -4
- package/dist/{apply-Bjfq_b4-.mjs.map → apply-kC39ev1Z.mjs.map} +1 -1
- package/dist/astro/index.d.mts +3 -3
- package/dist/astro/index.mjs +16 -1
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +3 -3
- package/dist/astro/middleware/request-context.mjs +84 -22
- package/dist/astro/middleware/request-context.mjs.map +1 -1
- package/dist/astro/middleware.mjs +41 -12
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +5 -4
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/cli/index.mjs +65 -6
- package/dist/cli/index.mjs.map +1 -1
- package/dist/db/index.mjs +1 -1
- package/dist/{index-C1xF3OGh.d.mts → index-CLBc4gw-.d.mts} +42 -11
- package/dist/{index-C1xF3OGh.d.mts.map → index-CLBc4gw-.d.mts.map} +1 -1
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +9 -9
- package/dist/{manifest-schema-Dcl0R6nM.mjs → manifest-schema-CL8DWO9b.mjs} +5 -2
- package/dist/manifest-schema-CL8DWO9b.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 +4 -4
- package/dist/page/index.d.mts +1 -1
- package/dist/{placeholder-CmGAmqeO.d.mts → placeholder-SvFCKbz_.d.mts} +10 -2
- package/dist/{placeholder-CmGAmqeO.d.mts.map → placeholder-SvFCKbz_.d.mts.map} +1 -1
- package/dist/{placeholder-SmpOx-_v.mjs → placeholder-aiCD8aSZ.mjs} +27 -2
- package/dist/placeholder-aiCD8aSZ.mjs.map +1 -0
- package/dist/plugins/adapt-sandbox-entry.d.mts +3 -3
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-CS_iSj34.mjs → query-BVYN0PJ6.mjs} +2 -2
- package/dist/{query-CS_iSj34.mjs.map → query-BVYN0PJ6.mjs.map} +1 -1
- package/dist/{registry-D_w5HW4G.mjs → registry-BNYQKX_d.mjs} +23 -38
- package/dist/registry-BNYQKX_d.mjs.map +1 -0
- package/dist/{runner-C0hCbYnD.mjs → runner-BraqvGYk.mjs} +251 -158
- package/dist/runner-BraqvGYk.mjs.map +1 -0
- package/dist/runner-EAtf0ZIe.d.mts.map +1 -1
- package/dist/runtime.d.mts +4 -4
- package/dist/{search-DG603UrT.mjs → search-C1gg67nN.mjs} +125 -18
- package/dist/search-C1gg67nN.mjs.map +1 -0
- package/dist/seed/index.d.mts +1 -1
- package/dist/seed/index.mjs +3 -3
- package/dist/{types-DvhsUmSJ.d.mts → types-BQo5JS0J.d.mts} +15 -2
- package/dist/{types-DvhsUmSJ.d.mts.map → types-BQo5JS0J.d.mts.map} +1 -1
- package/dist/{types-DY5zk5HN.mjs → types-CiA5Gac0.mjs} +5 -3
- package/dist/types-CiA5Gac0.mjs.map +1 -0
- package/dist/{types-C4-fAxN3.d.mts → types-DPfzHnjW.d.mts} +13 -2
- package/dist/types-DPfzHnjW.d.mts.map +1 -0
- package/dist/{validate-CpBtVMsD.d.mts → validate-HtxZeaBi.d.mts} +2 -2
- package/dist/{validate-CpBtVMsD.d.mts.map → validate-HtxZeaBi.d.mts.map} +1 -1
- package/dist/{validate-O7PWmlnq.mjs → validate-_rsF-Dx_.mjs} +2 -2
- package/dist/{validate-O7PWmlnq.mjs.map → validate-_rsF-Dx_.mjs.map} +1 -1
- package/package.json +6 -4
- package/src/api/handlers/marketplace.ts +7 -4
- package/src/api/schemas/schema.ts +12 -0
- package/src/astro/integration/index.ts +17 -0
- package/src/astro/integration/runtime.ts +13 -0
- package/src/astro/integration/virtual-modules.ts +13 -1
- package/src/astro/routes/admin.astro +1 -1
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +3 -1
- package/src/astro/routes/api/auth/invite/complete.ts +2 -1
- package/src/astro/routes/api/auth/passkey/options.ts +2 -1
- package/src/astro/routes/api/auth/passkey/register/options.ts +2 -1
- package/src/astro/routes/api/auth/passkey/register/verify.ts +2 -1
- package/src/astro/routes/api/auth/passkey/verify.ts +2 -1
- package/src/astro/routes/api/auth/signup/complete.ts +2 -1
- package/src/astro/routes/api/import/wordpress/analyze.ts +24 -3
- package/src/astro/routes/api/import/wordpress/execute.ts +5 -1
- package/src/astro/routes/api/import/wordpress/prepare.ts +2 -2
- package/src/astro/routes/api/media.ts +16 -4
- package/src/astro/routes/api/search/index.ts +1 -5
- package/src/astro/routes/api/search/suggest.ts +1 -5
- package/src/astro/routes/api/setup/admin-verify.ts +2 -1
- package/src/astro/routes/api/setup/admin.ts +2 -1
- package/src/astro/types.ts +1 -0
- package/src/auth/passkey-config.ts +24 -3
- package/src/cli/commands/bundle-utils.ts +26 -0
- package/src/cli/commands/bundle.ts +15 -0
- package/src/cli/commands/content.ts +11 -1
- package/src/cli/commands/login.ts +2 -0
- package/src/cli/commands/media.ts +5 -1
- package/src/cli/commands/menu.ts +3 -1
- package/src/cli/commands/schema.ts +7 -1
- package/src/cli/commands/search-cmd.ts +2 -1
- package/src/cli/commands/taxonomy.ts +4 -1
- package/src/cli/output.ts +14 -0
- package/src/components/InlinePortableTextEditor.tsx +33 -3
- package/src/database/migrations/033_optimize_content_indexes.ts +113 -0
- package/src/database/migrations/runner.ts +40 -33
- package/src/database/repositories/comment.ts +32 -20
- package/src/emdash-runtime.ts +64 -2
- package/src/media/placeholder.ts +31 -0
- package/src/media/thumbnail.ts +32 -0
- package/src/plugins/hooks.ts +91 -0
- package/src/plugins/manager.ts +22 -0
- package/src/plugins/manifest-schema.ts +3 -0
- package/src/plugins/marketplace.ts +25 -12
- package/src/plugins/types.ts +24 -0
- package/src/schema/registry.ts +23 -27
- package/src/schema/types.ts +27 -1
- package/src/search/fts-manager.ts +1 -18
- package/src/visual-editing/toolbar.ts +84 -22
- package/dist/manifest-schema-Dcl0R6nM.mjs.map +0 -1
- package/dist/placeholder-SmpOx-_v.mjs.map +0 -1
- package/dist/registry-D_w5HW4G.mjs.map +0 -1
- package/dist/runner-C0hCbYnD.mjs.map +0 -1
- package/dist/search-DG603UrT.mjs.map +0 -1
- package/dist/types-C4-fAxN3.d.mts.map +0 -1
- package/dist/types-DY5zk5HN.mjs.map +0 -1
|
@@ -14,6 +14,8 @@ import type { MediaProviderDescriptor } from "../../media/types.js";
|
|
|
14
14
|
import { defaultSeed } from "../../seed/default.js";
|
|
15
15
|
import type { PluginDescriptor } from "./runtime.js";
|
|
16
16
|
|
|
17
|
+
const TS_SOURCE_EXT_RE = /^\.(ts|tsx|mts|cts|jsx)$/;
|
|
18
|
+
|
|
17
19
|
/** Pattern to remove scoped package prefix from plugin ID */
|
|
18
20
|
const SCOPED_PREFIX_PATTERN = /^@[^/]+\/plugin-/;
|
|
19
21
|
|
|
@@ -435,7 +437,17 @@ export const sandboxedPlugins = [];
|
|
|
435
437
|
|
|
436
438
|
// Resolve the bundle to a file path using project's require context
|
|
437
439
|
const filePath = resolveModulePathFromProject(bundleSpecifier, projectRoot);
|
|
438
|
-
|
|
440
|
+
|
|
441
|
+
const ext = filePath.slice(filePath.lastIndexOf("."));
|
|
442
|
+
if (TS_SOURCE_EXT_RE.test(ext)) {
|
|
443
|
+
throw new Error(
|
|
444
|
+
`Sandboxed plugin "${descriptor.id}" entrypoint "${bundleSpecifier}" resolves to ` +
|
|
445
|
+
`unbuilt source (${filePath}). Sandbox entries must be pre-built JavaScript. ` +
|
|
446
|
+
`Ensure the plugin's package.json exports point to built files (e.g. dist/*.mjs) ` +
|
|
447
|
+
`and run the plugin's build step before building the site.`,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
439
451
|
const code = readFileSync(filePath, "utf-8");
|
|
440
452
|
|
|
441
453
|
// Create the plugin entry with embedded code and sandbox config
|
|
@@ -20,7 +20,7 @@ export const prerender = false;
|
|
|
20
20
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
21
21
|
<link
|
|
22
22
|
rel="icon"
|
|
23
|
-
href="data:image/svg+xml
|
|
23
|
+
href="data:image/svg+xml,<svg width='75' height='75' viewBox='0 0 75 75' fill='none' xmlns='http://www.w3.org/2000/svg'> <g clip-path='url(%23clip0_50_99)'> <rect x='3' y='3' width='69' height='69' rx='10.518' stroke='url(%23paint0_linear_50_99)' stroke-width='6'/> <rect x='18' y='34' width='39.3661' height='6.56101' fill='url(%23paint1_linear_50_99)'/> </g> <defs> <linearGradient id='paint0_linear_50_99' x1='-42.9996' y1='124' x2='92.4233' y2='-41.7456' gradientUnits='userSpaceOnUse'> <stop stop-color='%230F006B'/> <stop offset='0.0833333' stop-color='%23281A81'/> <stop offset='0.166667' stop-color='%235D0C83'/> <stop offset='0.25' stop-color='%23911475'/> <stop offset='0.333333' stop-color='%23CE2F55'/> <stop offset='0.416667' stop-color='%23FF6633'/> <stop offset='0.5' stop-color='%23F6821F'/> <stop offset='0.583333' stop-color='%23FBAD41'/> <stop offset='0.666667' stop-color='%23FFCD89'/> <stop offset='0.75' stop-color='%23FFE9CB'/> <stop offset='0.833333' stop-color='%23FFF7EC'/> <stop offset='0.916667' stop-color='%23FFF8EE'/> <stop offset='1' stop-color='white'/> </linearGradient> <linearGradient id='paint1_linear_50_99' x1='91.4992' y1='27.4982' x2='28.1217' y2='54.1775' gradientUnits='userSpaceOnUse'> <stop stop-color='white'/> <stop offset='0.129253' stop-color='%23FFF8EE'/> <stop offset='0.617058' stop-color='%23FBAD41'/> <stop offset='0.848019' stop-color='%23F6821F'/> <stop offset='1' stop-color='%23FF6633'/> </linearGradient> <clipPath id='clip0_50_99'> <rect width='75' height='75' fill='white'/> </clipPath> </defs> </svg>"
|
|
24
24
|
/>
|
|
25
25
|
<title>EmDash Admin</title>
|
|
26
26
|
</head>
|
|
@@ -41,13 +41,15 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
|
41
41
|
emdash.configuredPlugins.map((p: { id: string }) => p.id),
|
|
42
42
|
);
|
|
43
43
|
|
|
44
|
+
const siteOrigin = new URL(request.url).origin;
|
|
45
|
+
|
|
44
46
|
const result = await handleMarketplaceInstall(
|
|
45
47
|
emdash.db,
|
|
46
48
|
emdash.storage,
|
|
47
49
|
emdash.getSandboxRunner(),
|
|
48
50
|
emdash.config.marketplace,
|
|
49
51
|
id,
|
|
50
|
-
{ version: body.version, configuredPluginIds },
|
|
52
|
+
{ version: body.version, configuredPluginIds, siteOrigin },
|
|
51
53
|
);
|
|
52
54
|
|
|
53
55
|
if (!result.success) return unwrapResult(result);
|
|
@@ -22,6 +22,7 @@ import { OptionsRepository } from "#db/repositories/options.js";
|
|
|
22
22
|
|
|
23
23
|
export const POST: APIRoute = async ({ request, locals, session }) => {
|
|
24
24
|
const { emdash } = locals;
|
|
25
|
+
const passkeyPublicOrigin = emdash?.config.passkeyPublicOrigin;
|
|
25
26
|
|
|
26
27
|
if (!emdash?.db) {
|
|
27
28
|
return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
|
|
@@ -37,7 +38,7 @@ export const POST: APIRoute = async ({ request, locals, session }) => {
|
|
|
37
38
|
const url = new URL(request.url);
|
|
38
39
|
const options = new OptionsRepository(emdash.db);
|
|
39
40
|
const siteName = (await options.get<string>("emdash:site_title")) ?? undefined;
|
|
40
|
-
const passkeyConfig = getPasskeyConfig(url, siteName);
|
|
41
|
+
const passkeyConfig = getPasskeyConfig(url, siteName, passkeyPublicOrigin);
|
|
41
42
|
|
|
42
43
|
// Verify the passkey registration response
|
|
43
44
|
const challengeStore = createChallengeStore(emdash.db);
|
|
@@ -23,6 +23,7 @@ import { OptionsRepository } from "#db/repositories/options.js";
|
|
|
23
23
|
|
|
24
24
|
export const POST: APIRoute = async ({ request, locals }) => {
|
|
25
25
|
const { emdash } = locals;
|
|
26
|
+
const passkeyPublicOrigin = emdash?.config.passkeyPublicOrigin;
|
|
26
27
|
|
|
27
28
|
if (!emdash?.db) {
|
|
28
29
|
return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
|
|
@@ -62,7 +63,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
62
63
|
const url = new URL(request.url);
|
|
63
64
|
const options = new OptionsRepository(emdash.db);
|
|
64
65
|
const siteName = (await options.get<string>("emdash:site_title")) ?? undefined;
|
|
65
|
-
const passkeyConfig = getPasskeyConfig(url, siteName);
|
|
66
|
+
const passkeyConfig = getPasskeyConfig(url, siteName, passkeyPublicOrigin);
|
|
66
67
|
|
|
67
68
|
// Generate authentication options
|
|
68
69
|
const challengeStore = createChallengeStore(emdash.db);
|
|
@@ -22,6 +22,7 @@ const MAX_PASSKEYS = 10;
|
|
|
22
22
|
|
|
23
23
|
export const POST: APIRoute = async ({ request, locals }) => {
|
|
24
24
|
const { emdash, user } = locals;
|
|
25
|
+
const passkeyPublicOrigin = emdash?.config.passkeyPublicOrigin;
|
|
25
26
|
|
|
26
27
|
if (!emdash?.db) {
|
|
27
28
|
return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
|
|
@@ -52,7 +53,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
52
53
|
const url = new URL(request.url);
|
|
53
54
|
const optionsRepo = new OptionsRepository(emdash.db);
|
|
54
55
|
const siteName = (await optionsRepo.get<string>("emdash:site_title")) ?? undefined;
|
|
55
|
-
const passkeyConfig = getPasskeyConfig(url, siteName);
|
|
56
|
+
const passkeyConfig = getPasskeyConfig(url, siteName, passkeyPublicOrigin);
|
|
56
57
|
|
|
57
58
|
// Generate registration options
|
|
58
59
|
const challengeStore = createChallengeStore(emdash.db);
|
|
@@ -31,6 +31,7 @@ interface PasskeyResponse {
|
|
|
31
31
|
|
|
32
32
|
export const POST: APIRoute = async ({ request, locals }) => {
|
|
33
33
|
const { emdash, user } = locals;
|
|
34
|
+
const passkeyPublicOrigin = emdash?.config.passkeyPublicOrigin;
|
|
34
35
|
|
|
35
36
|
if (!emdash?.db) {
|
|
36
37
|
return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
|
|
@@ -58,7 +59,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
58
59
|
const url = new URL(request.url);
|
|
59
60
|
const optionsRepo = new OptionsRepository(emdash.db);
|
|
60
61
|
const siteName = (await optionsRepo.get<string>("emdash:site_title")) ?? undefined;
|
|
61
|
-
const passkeyConfig = getPasskeyConfig(url, siteName);
|
|
62
|
+
const passkeyConfig = getPasskeyConfig(url, siteName, passkeyPublicOrigin);
|
|
62
63
|
|
|
63
64
|
// Verify the registration response
|
|
64
65
|
const challengeStore = createChallengeStore(emdash.db);
|
|
@@ -20,6 +20,7 @@ import { OptionsRepository } from "#db/repositories/options.js";
|
|
|
20
20
|
|
|
21
21
|
export const POST: APIRoute = async ({ request, locals, session }) => {
|
|
22
22
|
const { emdash } = locals;
|
|
23
|
+
const passkeyPublicOrigin = emdash?.config.passkeyPublicOrigin;
|
|
23
24
|
|
|
24
25
|
if (!emdash?.db) {
|
|
25
26
|
return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
|
|
@@ -33,7 +34,7 @@ export const POST: APIRoute = async ({ request, locals, session }) => {
|
|
|
33
34
|
const url = new URL(request.url);
|
|
34
35
|
const options = new OptionsRepository(emdash.db);
|
|
35
36
|
const siteName = (await options.get<string>("emdash:site_title")) ?? undefined;
|
|
36
|
-
const passkeyConfig = getPasskeyConfig(url, siteName);
|
|
37
|
+
const passkeyConfig = getPasskeyConfig(url, siteName, passkeyPublicOrigin);
|
|
37
38
|
|
|
38
39
|
// Authenticate with passkey
|
|
39
40
|
const adapter = createKyselyAdapter(emdash.db);
|
|
@@ -22,6 +22,7 @@ import { OptionsRepository } from "#db/repositories/options.js";
|
|
|
22
22
|
|
|
23
23
|
export const POST: APIRoute = async ({ request, locals, session }) => {
|
|
24
24
|
const { emdash } = locals;
|
|
25
|
+
const passkeyPublicOrigin = emdash?.config.passkeyPublicOrigin;
|
|
25
26
|
|
|
26
27
|
if (!emdash?.db) {
|
|
27
28
|
return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
|
|
@@ -37,7 +38,7 @@ export const POST: APIRoute = async ({ request, locals, session }) => {
|
|
|
37
38
|
const url = new URL(request.url);
|
|
38
39
|
const options = new OptionsRepository(emdash.db);
|
|
39
40
|
const siteName = (await options.get<string>("emdash:site_title")) ?? undefined;
|
|
40
|
-
const passkeyConfig = getPasskeyConfig(url, siteName);
|
|
41
|
+
const passkeyConfig = getPasskeyConfig(url, siteName, passkeyPublicOrigin);
|
|
41
42
|
|
|
42
43
|
// Verify the passkey registration response
|
|
43
44
|
const challengeStore = createChallengeStore(emdash.db);
|
|
@@ -13,11 +13,14 @@ import mime from "mime/lite";
|
|
|
13
13
|
|
|
14
14
|
import { requirePerm } from "#api/authorize.js";
|
|
15
15
|
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
16
|
+
import { RESERVED_COLLECTION_SLUGS } from "#schema/types.js";
|
|
16
17
|
import type { EmDashHandlers } from "#types";
|
|
17
18
|
|
|
18
19
|
export const prerender = false;
|
|
19
20
|
|
|
20
21
|
const NUMERIC_PATTERN = /^-?\d+(\.\d+)?$/;
|
|
22
|
+
const INVALID_SLUG_CHARS = /[^a-z0-9_]/g;
|
|
23
|
+
const LEADING_NON_ALPHA = /^[^a-z]+/;
|
|
21
24
|
|
|
22
25
|
/** Field compatibility status */
|
|
23
26
|
export type FieldCompatibility =
|
|
@@ -252,10 +255,18 @@ function analyzeWxr(
|
|
|
252
255
|
.toSorted((a, b) => b.count - a.count);
|
|
253
256
|
|
|
254
257
|
// Build post type analysis with schema compatibility
|
|
258
|
+
const seenSlugs = new Map<string, number>();
|
|
255
259
|
const postTypes: PostTypeAnalysis[] = [...postTypeCounts.entries()]
|
|
256
260
|
.filter(([type]) => !isInternalPostType(type))
|
|
257
261
|
.map(([name, count]) => {
|
|
258
|
-
|
|
262
|
+
let suggestedCollection = mapPostTypeToCollection(name);
|
|
263
|
+
|
|
264
|
+
// Deduplicate: if multiple post types produce the same slug, append a suffix
|
|
265
|
+
const seen = seenSlugs.get(suggestedCollection) ?? 0;
|
|
266
|
+
seenSlugs.set(suggestedCollection, seen + 1);
|
|
267
|
+
if (seen > 0) {
|
|
268
|
+
suggestedCollection = `${suggestedCollection}_${seen}`;
|
|
269
|
+
}
|
|
259
270
|
const existingCollection = existingCollections.get(suggestedCollection);
|
|
260
271
|
|
|
261
272
|
// Build required fields - add featured_image only if posts have thumbnails
|
|
@@ -445,6 +456,16 @@ function isInternalMetaKey(key: string): boolean {
|
|
|
445
456
|
return false;
|
|
446
457
|
}
|
|
447
458
|
|
|
459
|
+
function sanitizeSlug(slug: string): string {
|
|
460
|
+
const sanitized = slug
|
|
461
|
+
.toLowerCase()
|
|
462
|
+
.replace(INVALID_SLUG_CHARS, "_")
|
|
463
|
+
.replace(LEADING_NON_ALPHA, "");
|
|
464
|
+
if (!sanitized) return "imported";
|
|
465
|
+
if (RESERVED_COLLECTION_SLUGS.includes(sanitized)) return `wp_${sanitized}`;
|
|
466
|
+
return sanitized;
|
|
467
|
+
}
|
|
468
|
+
|
|
448
469
|
function mapPostTypeToCollection(postType: string): string {
|
|
449
470
|
const mapping: Record<string, string> = {
|
|
450
471
|
post: "posts",
|
|
@@ -457,7 +478,7 @@ function mapPostTypeToCollection(postType: string): string {
|
|
|
457
478
|
event: "events",
|
|
458
479
|
faq: "faqs",
|
|
459
480
|
};
|
|
460
|
-
return mapping[postType] || postType;
|
|
481
|
+
return mapping[postType] || sanitizeSlug(postType);
|
|
461
482
|
}
|
|
462
483
|
|
|
463
484
|
function mapMetaKeyToField(key: string): string {
|
|
@@ -507,4 +528,4 @@ function singularize(str: string): string {
|
|
|
507
528
|
}
|
|
508
529
|
|
|
509
530
|
// Export helpers for use in prepare endpoint
|
|
510
|
-
export { capitalize, singularize, mapPostTypeToCollection };
|
|
531
|
+
export { capitalize, sanitizeSlug, singularize, mapPostTypeToCollection };
|
|
@@ -22,6 +22,8 @@ import { resolveImportByline } from "#import/utils.js";
|
|
|
22
22
|
import type { EmDashHandlers, EmDashManifest } from "#types";
|
|
23
23
|
import { slugify } from "#utils/slugify.js";
|
|
24
24
|
|
|
25
|
+
import { sanitizeSlug } from "./analyze.js";
|
|
26
|
+
|
|
25
27
|
export const prerender = false;
|
|
26
28
|
|
|
27
29
|
export interface ImportConfig {
|
|
@@ -165,7 +167,9 @@ async function importContent(
|
|
|
165
167
|
continue;
|
|
166
168
|
}
|
|
167
169
|
|
|
168
|
-
|
|
170
|
+
// Defensive: mapping.collection is already sanitized by prepare, but the user
|
|
171
|
+
// could manually edit the import config between prepare and execute.
|
|
172
|
+
const collection = sanitizeSlug(mapping.collection);
|
|
169
173
|
|
|
170
174
|
// Check if collection exists in manifest
|
|
171
175
|
if (!manifest?.collections[collection]) {
|
|
@@ -16,7 +16,7 @@ import { wpPrepareBody } from "#api/schemas.js";
|
|
|
16
16
|
import { FIELD_TYPES, type FieldType } from "#schema/types.js";
|
|
17
17
|
import type { EmDashHandlers } from "#types";
|
|
18
18
|
|
|
19
|
-
import { capitalize, singularize, type ImportFieldDef } from "./analyze.js";
|
|
19
|
+
import { capitalize, sanitizeSlug, singularize, type ImportFieldDef } from "./analyze.js";
|
|
20
20
|
|
|
21
21
|
/** Validate that a string is a known FieldType, returning undefined if not */
|
|
22
22
|
function asFieldType(value: string): FieldType | undefined {
|
|
@@ -79,7 +79,7 @@ async function prepareImport(
|
|
|
79
79
|
};
|
|
80
80
|
|
|
81
81
|
for (const postType of request.postTypes) {
|
|
82
|
-
const collectionSlug = postType.collection;
|
|
82
|
+
const collectionSlug = sanitizeSlug(postType.collection);
|
|
83
83
|
|
|
84
84
|
try {
|
|
85
85
|
// Check if collection exists
|
|
@@ -151,10 +151,22 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
151
151
|
const width = widthStr ? parseInt(widthStr, 10) : undefined;
|
|
152
152
|
const height = heightStr ? parseInt(heightStr, 10) : undefined;
|
|
153
153
|
|
|
154
|
-
// Generate placeholder data for images
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
// Generate placeholder data for images.
|
|
155
|
+
// If the client sent a thumbnail (small pre-resized image), use that
|
|
156
|
+
// instead of the full buffer to avoid OOM on memory-constrained runtimes.
|
|
157
|
+
const thumbnailEntry = formData.get("thumbnail");
|
|
158
|
+
const thumbnail = thumbnailEntry instanceof File ? thumbnailEntry : null;
|
|
159
|
+
|
|
160
|
+
let placeholder: Awaited<ReturnType<typeof generatePlaceholder>> = null;
|
|
161
|
+
if (file.type.startsWith("image/")) {
|
|
162
|
+
if (thumbnail) {
|
|
163
|
+
const thumbBuffer = new Uint8Array(await thumbnail.arrayBuffer());
|
|
164
|
+
placeholder = await generatePlaceholder(thumbBuffer, thumbnail.type);
|
|
165
|
+
} else {
|
|
166
|
+
const clientDims = width && height ? { width, height } : undefined;
|
|
167
|
+
placeholder = await generatePlaceholder(buffer, file.type, clientDims);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
158
170
|
|
|
159
171
|
// Create media record
|
|
160
172
|
const result = await emdash.handleMediaCreate({
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
import type { APIRoute } from "astro";
|
|
8
8
|
|
|
9
|
-
import { requirePerm } from "#api/authorize.js";
|
|
10
9
|
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
11
10
|
import { isParseError, parseQuery } from "#api/parse.js";
|
|
12
11
|
import { searchQuery } from "#api/schemas.js";
|
|
@@ -24,10 +23,7 @@ export const prerender = false;
|
|
|
24
23
|
* - limit: Maximum results (optional, defaults to 20)
|
|
25
24
|
*/
|
|
26
25
|
export const GET: APIRoute = async ({ url, locals }) => {
|
|
27
|
-
const { emdash
|
|
28
|
-
|
|
29
|
-
const denied = requirePerm(user, "search:read");
|
|
30
|
-
if (denied) return denied;
|
|
26
|
+
const { emdash } = locals;
|
|
31
27
|
|
|
32
28
|
if (!emdash?.db) {
|
|
33
29
|
return apiError("NOT_CONFIGURED", "EmDash not configured", 500);
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
import type { APIRoute } from "astro";
|
|
8
8
|
|
|
9
|
-
import { requirePerm } from "#api/authorize.js";
|
|
10
9
|
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
11
10
|
import { isParseError, parseQuery } from "#api/parse.js";
|
|
12
11
|
import { searchSuggestQuery } from "#api/schemas.js";
|
|
@@ -23,10 +22,7 @@ export const prerender = false;
|
|
|
23
22
|
* - limit: Maximum suggestions (optional, defaults to 5)
|
|
24
23
|
*/
|
|
25
24
|
export const GET: APIRoute = async ({ url, locals }) => {
|
|
26
|
-
const { emdash
|
|
27
|
-
|
|
28
|
-
const denied = requirePerm(user, "search:read");
|
|
29
|
-
if (denied) return denied;
|
|
25
|
+
const { emdash } = locals;
|
|
30
26
|
|
|
31
27
|
if (!emdash?.db) {
|
|
32
28
|
return apiError("NOT_CONFIGURED", "EmDash not configured", 500);
|
|
@@ -21,6 +21,7 @@ import { OptionsRepository } from "#db/repositories/options.js";
|
|
|
21
21
|
|
|
22
22
|
export const POST: APIRoute = async ({ request, locals }) => {
|
|
23
23
|
const { emdash } = locals;
|
|
24
|
+
const passkeyPublicOrigin = emdash?.config.passkeyPublicOrigin;
|
|
24
25
|
|
|
25
26
|
if (!emdash?.db) {
|
|
26
27
|
return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
|
|
@@ -57,7 +58,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
57
58
|
// Get passkey config
|
|
58
59
|
const url = new URL(request.url);
|
|
59
60
|
const siteName = (await options.get<string>("emdash:site_title")) ?? undefined;
|
|
60
|
-
const passkeyConfig = getPasskeyConfig(url, siteName);
|
|
61
|
+
const passkeyConfig = getPasskeyConfig(url, siteName, passkeyPublicOrigin);
|
|
61
62
|
|
|
62
63
|
// Verify the registration response
|
|
63
64
|
const challengeStore = createChallengeStore(emdash.db);
|
|
@@ -20,6 +20,7 @@ import { OptionsRepository } from "#db/repositories/options.js";
|
|
|
20
20
|
|
|
21
21
|
export const POST: APIRoute = async ({ request, locals }) => {
|
|
22
22
|
const { emdash } = locals;
|
|
23
|
+
const passkeyPublicOrigin = emdash?.config.passkeyPublicOrigin;
|
|
23
24
|
|
|
24
25
|
if (!emdash?.db) {
|
|
25
26
|
return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
|
|
@@ -56,7 +57,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
56
57
|
// Get passkey config
|
|
57
58
|
const url = new URL(request.url);
|
|
58
59
|
const siteName = (await options.get<string>("emdash:site_title")) ?? undefined;
|
|
59
|
-
const passkeyConfig = getPasskeyConfig(url, siteName);
|
|
60
|
+
const passkeyConfig = getPasskeyConfig(url, siteName, passkeyPublicOrigin);
|
|
60
61
|
|
|
61
62
|
// Generate registration options
|
|
62
63
|
const challengeStore = createChallengeStore(emdash.db);
|
package/src/astro/types.ts
CHANGED
|
@@ -15,10 +15,31 @@ export interface PasskeyConfig {
|
|
|
15
15
|
/**
|
|
16
16
|
* Get passkey configuration from request URL
|
|
17
17
|
*
|
|
18
|
-
* @param url The request URL
|
|
19
|
-
* @param siteName Optional site name for rpName (defaults to hostname)
|
|
18
|
+
* @param url The request URL (typically `new URL(Astro.request.url)` or `new URL(request.url)`)
|
|
19
|
+
* @param siteName Optional site name for rpName (defaults to hostname from `url` or public origin)
|
|
20
|
+
* @param passkeyPublicOrigin Optional browser-facing origin (see `EmDashConfig.passkeyPublicOrigin`).
|
|
21
|
+
* When set, **origin** and **rpId** are taken from this URL so they match WebAuthn `clientData.origin`.
|
|
22
|
+
* @throws If `passkeyPublicOrigin` is non-empty but not parseable by `new URL()`.
|
|
20
23
|
*/
|
|
21
|
-
export function getPasskeyConfig(
|
|
24
|
+
export function getPasskeyConfig(
|
|
25
|
+
url: URL,
|
|
26
|
+
siteName?: string,
|
|
27
|
+
passkeyPublicOrigin?: string,
|
|
28
|
+
): PasskeyConfig {
|
|
29
|
+
if (passkeyPublicOrigin) {
|
|
30
|
+
let publicUrl: URL;
|
|
31
|
+
try {
|
|
32
|
+
publicUrl = new URL(passkeyPublicOrigin);
|
|
33
|
+
} catch (e) {
|
|
34
|
+
throw new Error(`Invalid passkeyPublicOrigin: "${passkeyPublicOrigin}"`, { cause: e });
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
rpName: siteName || publicUrl.hostname,
|
|
38
|
+
rpId: publicUrl.hostname,
|
|
39
|
+
origin: publicUrl.origin,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
22
43
|
return {
|
|
23
44
|
rpName: siteName || url.hostname,
|
|
24
45
|
rpId: url.hostname,
|
|
@@ -213,6 +213,32 @@ export async function resolveSourceEntry(
|
|
|
213
213
|
return undefined;
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
// ── Export validation ───────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
const TS_SOURCE_EXPORT_RE = /\.(?:ts|tsx|mts|cts|jsx)$/;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Find package.json exports that point to source files instead of built output.
|
|
222
|
+
* Returns an array of `{ exportPath, resolvedPath }` for each offending export.
|
|
223
|
+
*/
|
|
224
|
+
export function findSourceExports(
|
|
225
|
+
exports: Record<string, unknown>,
|
|
226
|
+
): Array<{ exportPath: string; resolvedPath: string }> {
|
|
227
|
+
const issues: Array<{ exportPath: string; resolvedPath: string }> = [];
|
|
228
|
+
for (const [exportPath, exportValue] of Object.entries(exports)) {
|
|
229
|
+
const resolved =
|
|
230
|
+
typeof exportValue === "string"
|
|
231
|
+
? exportValue
|
|
232
|
+
: exportValue && typeof exportValue === "object" && "import" in exportValue
|
|
233
|
+
? (exportValue as { import: string }).import
|
|
234
|
+
: null;
|
|
235
|
+
if (resolved && TS_SOURCE_EXPORT_RE.test(resolved)) {
|
|
236
|
+
issues.push({ exportPath, resolvedPath: resolved });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return issues;
|
|
240
|
+
}
|
|
241
|
+
|
|
216
242
|
// ── Directory helpers ────────────────────────────────────────────────────────
|
|
217
243
|
|
|
218
244
|
/**
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
extractManifest,
|
|
28
28
|
findNodeBuiltinImports,
|
|
29
29
|
findBuildOutput,
|
|
30
|
+
findSourceExports,
|
|
30
31
|
resolveSourceEntry,
|
|
31
32
|
calculateDirectorySize,
|
|
32
33
|
createTarball,
|
|
@@ -495,6 +496,20 @@ export const bundleCommand = defineCommand({
|
|
|
495
496
|
consola.start("Validating bundle...");
|
|
496
497
|
let hasErrors = false;
|
|
497
498
|
|
|
499
|
+
// Check that package.json exports point to built files, not source.
|
|
500
|
+
// Plugins published to npm with source exports will break site builds
|
|
501
|
+
// because the sandbox module generator embeds the resolved file as-is.
|
|
502
|
+
if (pkg.exports) {
|
|
503
|
+
for (const issue of findSourceExports(pkg.exports)) {
|
|
504
|
+
consola.error(
|
|
505
|
+
`Export "${issue.exportPath}" points to source (${issue.resolvedPath}). ` +
|
|
506
|
+
`Package exports must point to built files (e.g. dist/*.mjs). ` +
|
|
507
|
+
`Add a build step and update the exports map.`,
|
|
508
|
+
);
|
|
509
|
+
hasErrors = true;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
498
513
|
// Check for Node.js builtins in backend.js
|
|
499
514
|
const backendPath = join(bundleDir, "backend.js");
|
|
500
515
|
if (await fileExists(backendPath)) {
|
|
@@ -10,7 +10,7 @@ import { defineCommand } from "citty";
|
|
|
10
10
|
import { consola } from "consola";
|
|
11
11
|
|
|
12
12
|
import { connectionArgs, createClientFromArgs } from "../client-factory.js";
|
|
13
|
-
import { output } from "../output.js";
|
|
13
|
+
import { configureOutputMode, output } from "../output.js";
|
|
14
14
|
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
// Helpers
|
|
@@ -77,6 +77,7 @@ const listCommand = defineCommand({
|
|
|
77
77
|
...connectionArgs,
|
|
78
78
|
},
|
|
79
79
|
async run({ args }) {
|
|
80
|
+
configureOutputMode(args);
|
|
80
81
|
try {
|
|
81
82
|
const client = createClientFromArgs(args);
|
|
82
83
|
const result = await client.list(args.collection, {
|
|
@@ -130,6 +131,7 @@ const getCommand = defineCommand({
|
|
|
130
131
|
...connectionArgs,
|
|
131
132
|
},
|
|
132
133
|
async run({ args }) {
|
|
134
|
+
configureOutputMode(args);
|
|
133
135
|
try {
|
|
134
136
|
const client = createClientFromArgs(args);
|
|
135
137
|
const item = await client.get(args.collection, args.id, {
|
|
@@ -177,6 +179,7 @@ const createCommand = defineCommand({
|
|
|
177
179
|
...connectionArgs,
|
|
178
180
|
},
|
|
179
181
|
async run({ args }) {
|
|
182
|
+
configureOutputMode(args);
|
|
180
183
|
try {
|
|
181
184
|
const data = await readInputData(args);
|
|
182
185
|
const client = createClientFromArgs(args);
|
|
@@ -229,6 +232,7 @@ const updateCommand = defineCommand({
|
|
|
229
232
|
...connectionArgs,
|
|
230
233
|
},
|
|
231
234
|
async run({ args }) {
|
|
235
|
+
configureOutputMode(args);
|
|
232
236
|
try {
|
|
233
237
|
const data = await readInputData(args);
|
|
234
238
|
const client = createClientFromArgs(args);
|
|
@@ -270,6 +274,7 @@ const deleteCommand = defineCommand({
|
|
|
270
274
|
...connectionArgs,
|
|
271
275
|
},
|
|
272
276
|
async run({ args }) {
|
|
277
|
+
configureOutputMode(args);
|
|
273
278
|
try {
|
|
274
279
|
const client = createClientFromArgs(args);
|
|
275
280
|
await client.delete(args.collection, args.id);
|
|
@@ -297,6 +302,7 @@ const publishCommand = defineCommand({
|
|
|
297
302
|
...connectionArgs,
|
|
298
303
|
},
|
|
299
304
|
async run({ args }) {
|
|
305
|
+
configureOutputMode(args);
|
|
300
306
|
try {
|
|
301
307
|
const client = createClientFromArgs(args);
|
|
302
308
|
await client.publish(args.collection, args.id);
|
|
@@ -324,6 +330,7 @@ const unpublishCommand = defineCommand({
|
|
|
324
330
|
...connectionArgs,
|
|
325
331
|
},
|
|
326
332
|
async run({ args }) {
|
|
333
|
+
configureOutputMode(args);
|
|
327
334
|
try {
|
|
328
335
|
const client = createClientFromArgs(args);
|
|
329
336
|
await client.unpublish(args.collection, args.id);
|
|
@@ -356,6 +363,7 @@ const scheduleCommand = defineCommand({
|
|
|
356
363
|
...connectionArgs,
|
|
357
364
|
},
|
|
358
365
|
async run({ args }) {
|
|
366
|
+
configureOutputMode(args);
|
|
359
367
|
try {
|
|
360
368
|
const client = createClientFromArgs(args);
|
|
361
369
|
await client.schedule(args.collection, args.id, { at: args.at });
|
|
@@ -383,6 +391,7 @@ const restoreCommand = defineCommand({
|
|
|
383
391
|
...connectionArgs,
|
|
384
392
|
},
|
|
385
393
|
async run({ args }) {
|
|
394
|
+
configureOutputMode(args);
|
|
386
395
|
try {
|
|
387
396
|
const client = createClientFromArgs(args);
|
|
388
397
|
await client.restore(args.collection, args.id);
|
|
@@ -410,6 +419,7 @@ const translationsCommand = defineCommand({
|
|
|
410
419
|
...connectionArgs,
|
|
411
420
|
},
|
|
412
421
|
async run({ args }) {
|
|
422
|
+
configureOutputMode(args);
|
|
413
423
|
try {
|
|
414
424
|
const client = createClientFromArgs(args);
|
|
415
425
|
const translations = await client.translations(args.collection, args.id);
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
resolveCredentialKey,
|
|
30
30
|
saveCredentials,
|
|
31
31
|
} from "../credentials.js";
|
|
32
|
+
import { configureOutputMode } from "../output.js";
|
|
32
33
|
|
|
33
34
|
// ---------------------------------------------------------------------------
|
|
34
35
|
// Types for discovery + device flow responses
|
|
@@ -423,6 +424,7 @@ export const whoamiCommand = defineCommand({
|
|
|
423
424
|
},
|
|
424
425
|
},
|
|
425
426
|
async run({ args }) {
|
|
427
|
+
configureOutputMode(args);
|
|
426
428
|
const baseUrl = args.url || "http://localhost:4321";
|
|
427
429
|
|
|
428
430
|
// Resolve token: --token flag > EMDASH_TOKEN env > stored credentials
|
|
@@ -11,7 +11,7 @@ import { defineCommand } from "citty";
|
|
|
11
11
|
import { consola } from "consola";
|
|
12
12
|
|
|
13
13
|
import { connectionArgs, createClientFromArgs } from "../client-factory.js";
|
|
14
|
-
import { output } from "../output.js";
|
|
14
|
+
import { configureOutputMode, output } from "../output.js";
|
|
15
15
|
|
|
16
16
|
const listCommand = defineCommand({
|
|
17
17
|
meta: {
|
|
@@ -34,6 +34,7 @@ const listCommand = defineCommand({
|
|
|
34
34
|
},
|
|
35
35
|
},
|
|
36
36
|
async run({ args }) {
|
|
37
|
+
configureOutputMode(args);
|
|
37
38
|
const client = createClientFromArgs(args);
|
|
38
39
|
|
|
39
40
|
try {
|
|
@@ -73,6 +74,7 @@ const uploadCommand = defineCommand({
|
|
|
73
74
|
},
|
|
74
75
|
},
|
|
75
76
|
async run({ args }) {
|
|
77
|
+
configureOutputMode(args);
|
|
76
78
|
const client = createClientFromArgs(args);
|
|
77
79
|
const filename = basename(args.file);
|
|
78
80
|
|
|
@@ -108,6 +110,7 @@ const getCommand = defineCommand({
|
|
|
108
110
|
...connectionArgs,
|
|
109
111
|
},
|
|
110
112
|
async run({ args }) {
|
|
113
|
+
configureOutputMode(args);
|
|
111
114
|
const client = createClientFromArgs(args);
|
|
112
115
|
|
|
113
116
|
try {
|
|
@@ -134,6 +137,7 @@ const deleteCommand = defineCommand({
|
|
|
134
137
|
...connectionArgs,
|
|
135
138
|
},
|
|
136
139
|
async run({ args }) {
|
|
140
|
+
configureOutputMode(args);
|
|
137
141
|
const client = createClientFromArgs(args);
|
|
138
142
|
|
|
139
143
|
try {
|
package/src/cli/commands/menu.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { defineCommand } from "citty";
|
|
|
8
8
|
import { consola } from "consola";
|
|
9
9
|
|
|
10
10
|
import { connectionArgs, createClientFromArgs } from "../client-factory.js";
|
|
11
|
-
import { output } from "../output.js";
|
|
11
|
+
import { configureOutputMode, output } from "../output.js";
|
|
12
12
|
|
|
13
13
|
const listCommand = defineCommand({
|
|
14
14
|
meta: {
|
|
@@ -19,6 +19,7 @@ const listCommand = defineCommand({
|
|
|
19
19
|
...connectionArgs,
|
|
20
20
|
},
|
|
21
21
|
async run({ args }) {
|
|
22
|
+
configureOutputMode(args);
|
|
22
23
|
try {
|
|
23
24
|
const client = createClientFromArgs(args);
|
|
24
25
|
const menus = await client.menus();
|
|
@@ -44,6 +45,7 @@ const getCommand = defineCommand({
|
|
|
44
45
|
...connectionArgs,
|
|
45
46
|
},
|
|
46
47
|
async run({ args }) {
|
|
48
|
+
configureOutputMode(args);
|
|
47
49
|
try {
|
|
48
50
|
const client = createClientFromArgs(args);
|
|
49
51
|
const menu = await client.menu(args.name);
|