emdash 0.40.0 → 0.40.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/dist/api/route-utils.mjs +6 -6
- package/dist/api/schemas/index.d.mts +1 -1
- package/dist/{api-cFFZpyS0.mjs → api-CuyNEHZG.mjs} +3 -3
- package/dist/{api-cFFZpyS0.mjs.map → api-CuyNEHZG.mjs.map} +1 -1
- package/dist/{apply-CoT603H3.mjs → apply-npY1TAhG.mjs} +237 -34
- package/dist/apply-npY1TAhG.mjs.map +1 -0
- package/dist/astro/index.d.mts +3 -3
- package/dist/astro/index.mjs +9 -9
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +3 -3
- package/dist/astro/middleware/redirect.mjs +1 -1
- package/dist/astro/middleware.d.mts +3 -3
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +19 -18
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/media-usage/progress.mjs +2 -2
- package/dist/astro/routes/api/admin/media-usage/repair.mjs +2 -2
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +8 -8
- package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +8 -8
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +8 -8
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +11 -11
- package/dist/astro/routes/api/admin/plugins/index.mjs +8 -8
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +8 -8
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +11 -11
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +8 -8
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +8 -8
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +12 -12
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +9 -9
- package/dist/astro/routes/api/admin/plugins/registry/verify.mjs +9 -9
- package/dist/astro/routes/api/admin/plugins/updates.mjs +8 -8
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +8 -8
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +8 -8
- package/dist/astro/routes/api/admin/transfer/approvals/_id_/approve.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/approvals/_id_/deny.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/approvals/index.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/capabilities.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/exports/_id_/advance.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/exports/_id_/archive.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/exports/_id_/files/_...path_.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/exports/_id_/index.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/exports/_id_/manifest.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/exports/index.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/abandon.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/advance.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/analyze.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/cancel.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/execute.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/files/_...path_.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/index.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/missing.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/plan.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/receipt.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/index.mjs +3 -3
- package/dist/astro/routes/api/dashboard.mjs +2 -2
- package/dist/astro/routes/api/health.mjs +1 -1
- package/dist/astro/routes/api/import/probe.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +3 -3
- package/dist/astro/routes/api/import/wordpress/media.d.mts +3 -3
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +3 -3
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +3 -3
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +3 -3
- package/dist/astro/routes/api/manifest.mjs +1 -1
- package/dist/astro/routes/api/mcp.mjs +13 -33
- package/dist/astro/routes/api/mcp.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_/confirm.mjs +1 -1
- package/dist/astro/routes/api/media/_id_/upload.mjs +1 -1
- package/dist/astro/routes/api/media/_id_/usage.mjs +2 -2
- package/dist/astro/routes/api/media/_id_.mjs +2 -2
- package/dist/astro/routes/api/media/asset/_id_/_filename_.mjs +1 -1
- package/dist/astro/routes/api/media/upload-url.mjs +1 -1
- package/dist/astro/routes/api/media.mjs +2 -2
- package/dist/astro/routes/api/redirects/404s/index.mjs +2 -2
- package/dist/astro/routes/api/redirects/404s/summary.mjs +2 -2
- package/dist/astro/routes/api/redirects/_id_.mjs +2 -2
- package/dist/astro/routes/api/redirects/index.mjs +2 -2
- package/dist/astro/routes/api/schema/block-types/_slug_/index.mjs +8 -8
- package/dist/astro/routes/api/schema/block-types/_slug_/versions/_version_/activate.mjs +8 -8
- package/dist/astro/routes/api/schema/block-types/index.mjs +8 -8
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +8 -8
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +8 -8
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +8 -8
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +8 -8
- package/dist/astro/routes/api/schema/collections/index.mjs +8 -8
- package/dist/astro/routes/api/schema/collections/reorder.mjs +8 -8
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +8 -8
- package/dist/astro/routes/api/schema/orphans/index.mjs +8 -8
- package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +2 -2
- package/dist/astro/routes/api/settings/backups/archives/index.mjs +2 -2
- package/dist/astro/routes/api/settings/backups/export.mjs +2 -2
- package/dist/astro/routes/api/settings/backups/index.mjs +2 -2
- package/dist/astro/routes/api/settings.mjs +3 -3
- package/dist/astro/routes/api/setup/dev-bypass.mjs +4 -4
- package/dist/astro/routes/api/setup/index.d.mts.map +1 -1
- package/dist/astro/routes/api/setup/index.mjs +35 -10
- package/dist/astro/routes/api/setup/index.mjs.map +1 -1
- package/dist/astro/routes/api/snapshot.mjs +2 -2
- package/dist/astro/routes/api/snapshot.mjs.map +1 -1
- package/dist/astro/routes/robots.txt.mjs +2 -2
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +2 -2
- package/dist/astro/routes/sitemap.xml.mjs +2 -2
- package/dist/astro/types.d.mts +7 -3
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{backup-CBu0LrXf.mjs → backup-BQRj3afm.mjs} +3 -3
- package/dist/{backup-CBu0LrXf.mjs.map → backup-BQRj3afm.mjs.map} +1 -1
- package/dist/cli/index.mjs +6 -6
- package/dist/client/index.d.mts +14 -14
- package/dist/{config-5ii2haYu.d.mts → config-BjEm_3xL.d.mts} +25 -16
- package/dist/config-BjEm_3xL.d.mts.map +1 -0
- package/dist/{context-BeGccdUX.mjs → context-BV_7t-Ua.mjs} +6 -6
- package/dist/{context-BeGccdUX.mjs.map → context-BV_7t-Ua.mjs.map} +1 -1
- package/dist/{dashboard-DjrB-xxZ.mjs → dashboard-BP-iJqpd.mjs} +2 -2
- package/dist/{dashboard-DjrB-xxZ.mjs.map → dashboard-BP-iJqpd.mjs.map} +1 -1
- package/dist/db/libsql-migrations.mjs +2 -2
- package/dist/db/postgres-migrations.mjs +2 -2
- package/dist/db/sqlite-migrations.mjs +2 -2
- package/dist/{direct-executor-BmuFJJZV.mjs → direct-executor-D2lNadwo.mjs} +2 -2
- package/dist/{direct-executor-BmuFJJZV.mjs.map → direct-executor-D2lNadwo.mjs.map} +1 -1
- package/dist/{emdash-runtime-DAjghyEG.mjs → emdash-runtime-CtfnZmPi.mjs} +56 -56
- package/dist/emdash-runtime-CtfnZmPi.mjs.map +1 -0
- package/dist/{entry-lock-DVhCt49A.d.mts → entry-lock-Cf5OoIgN.d.mts} +18 -18
- package/dist/{entry-lock-DVhCt49A.d.mts.map → entry-lock-Cf5OoIgN.d.mts.map} +1 -1
- package/dist/{hreflang-DMFBL2eb.mjs → hreflang-CbVI4paR.mjs} +2 -2
- package/dist/{hreflang-DMFBL2eb.mjs.map → hreflang-CbVI4paR.mjs.map} +1 -1
- package/dist/{identity-IyNWwXr6.mjs → identity-Be5kzutJ.mjs} +2 -2
- package/dist/{identity-IyNWwXr6.mjs.map → identity-Be5kzutJ.mjs.map} +1 -1
- package/dist/{import-C3JZplWq.mjs → import-YzfKCaco.mjs} +2 -2
- package/dist/{import-C3JZplWq.mjs.map → import-YzfKCaco.mjs.map} +1 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +16 -16
- package/dist/{manifest-builder-B8zR-73J.mjs → manifest-builder-BmSAOOEF.mjs} +2 -2
- package/dist/{manifest-builder-B8zR-73J.mjs.map → manifest-builder-BmSAOOEF.mjs.map} +1 -1
- package/dist/media/local-runtime.d.mts +3 -3
- package/dist/media/local-runtime.mjs +3 -3
- package/dist/{media-Qp5S3eH3.mjs → media-CPXTbb1R.mjs} +20 -2
- package/dist/media-CPXTbb1R.mjs.map +1 -0
- package/dist/{media-h7tOiDLj.mjs → media-JXRWDhw3.mjs} +77 -4
- package/dist/media-JXRWDhw3.mjs.map +1 -0
- package/dist/{media-usage-My-Vi-ex.mjs → media-usage-De-zeR9E.mjs} +2 -2
- package/dist/{media-usage-My-Vi-ex.mjs.map → media-usage-De-zeR9E.mjs.map} +1 -1
- package/dist/{menus-TsSGmSR4.mjs → menus-Cm68rsAq.mjs} +4 -4
- package/dist/{menus-TsSGmSR4.mjs.map → menus-Cm68rsAq.mjs.map} +1 -1
- package/dist/migrations/index.mjs +2 -2
- package/dist/page/index.mjs +2 -2
- package/dist/plugin-test-runtime.d.mts +3 -3
- package/dist/plugin-test-runtime.mjs +18 -18
- package/dist/plugin-utils.d.mts +3 -3
- package/dist/plugins/adapt-sandbox-entry.d.mts +3 -3
- package/dist/plugins/host.d.mts +1 -1
- package/dist/plugins/host.mjs +6 -6
- package/dist/{redirect-iJf5fltD.mjs → redirect-B29UjdUn.mjs} +10 -14
- package/dist/redirect-B29UjdUn.mjs.map +1 -0
- package/dist/{redirects-CYu1u1aK.mjs → redirects-Ca2MPfEG.mjs} +2 -2
- package/dist/{redirects-CYu1u1aK.mjs.map → redirects-Ca2MPfEG.mjs.map} +1 -1
- package/dist/{routes-CUsM40Cc.mjs → routes-vrR95WR6.mjs} +2 -2
- package/dist/{routes-CUsM40Cc.mjs.map → routes-vrR95WR6.mjs.map} +1 -1
- package/dist/runtime.d.mts +3 -3
- package/dist/seed/index.mjs +4 -4
- package/dist/seo/index.mjs +3 -3
- package/dist/{settings-CgjRWGW9.mjs → settings-B4vDl1fK.mjs} +2 -2
- package/dist/{settings-CgjRWGW9.mjs.map → settings-B4vDl1fK.mjs.map} +1 -1
- package/dist/{settings-2xNYr90o.mjs → settings-Dyj6v8C4.mjs} +2 -2
- package/dist/{settings-2xNYr90o.mjs.map → settings-Dyj6v8C4.mjs.map} +1 -1
- package/dist/{snapshot-CugwUqKQ.mjs → snapshot-PawoX-WG.mjs} +180 -3
- package/dist/snapshot-PawoX-WG.mjs.map +1 -0
- package/dist/{storage-query-Bdv7A62k.d.mts → storage-query-FXJH5pI8.d.mts} +6 -1
- package/dist/storage-query-FXJH5pI8.d.mts.map +1 -0
- package/dist/{transfer-BhUCrgDc.mjs → transfer-DgeCvHnj.mjs} +4 -4
- package/dist/{transfer-BhUCrgDc.mjs.map → transfer-DgeCvHnj.mjs.map} +1 -1
- package/dist/validate-CM79QXYE.d.mts.map +1 -1
- package/dist/version-DXsxRrob.mjs +7 -0
- package/dist/{version-DUQDHgrf.mjs.map → version-DXsxRrob.mjs.map} +1 -1
- package/package.json +5 -5
- package/src/api/handlers/index.ts +1 -0
- package/src/api/handlers/media-upload.ts +17 -71
- package/src/api/handlers/media.ts +97 -3
- package/src/api/handlers/snapshot.ts +257 -3
- package/src/astro/integration/vite-config.ts +10 -4
- package/src/astro/middleware.ts +1 -0
- package/src/astro/routes/api/setup/index.ts +66 -23
- package/src/astro/routes/api/snapshot.ts +2 -1
- package/src/astro/types.ts +5 -0
- package/src/cleanup.ts +10 -0
- package/src/components/Columns.astro +3 -1
- package/src/components/Cover.astro +3 -1
- package/src/database/repositories/media.ts +40 -1
- package/src/database/repositories/redirect.ts +17 -34
- package/src/emdash-runtime.ts +28 -0
- package/src/index.ts +1 -0
- package/src/mcp/server.ts +13 -48
- package/src/seed/apply.ts +322 -41
- package/dist/apply-CoT603H3.mjs.map +0 -1
- package/dist/config-5ii2haYu.d.mts.map +0 -1
- package/dist/emdash-runtime-DAjghyEG.mjs.map +0 -1
- package/dist/media-Qp5S3eH3.mjs.map +0 -1
- package/dist/media-h7tOiDLj.mjs.map +0 -1
- package/dist/redirect-iJf5fltD.mjs.map +0 -1
- package/dist/snapshot-CugwUqKQ.mjs.map +0 -1
- package/dist/storage-query-Bdv7A62k.d.mts.map +0 -1
- package/dist/version-DUQDHgrf.mjs +0 -7
|
@@ -10,14 +10,25 @@ export const prerender = false;
|
|
|
10
10
|
|
|
11
11
|
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
12
12
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
13
|
-
import {
|
|
13
|
+
import { getConfiguredOrigin } from "#api/public-url.js";
|
|
14
14
|
import { setupBody } from "#api/schemas.js";
|
|
15
15
|
import { getAuthMode } from "#auth/mode.js";
|
|
16
16
|
import { OptionsRepository } from "#db/repositories/options.js";
|
|
17
|
-
import {
|
|
17
|
+
import { applySeedWithinBudget, type SeedApplyBudget } from "#seed/apply.js";
|
|
18
18
|
import { loadSeed } from "#seed/load.js";
|
|
19
19
|
import { validateSeed } from "#seed/validate.js";
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* What one setup request may spend on the seed before the rest continues in
|
|
23
|
+
* the next request. Cloudflare Workers Free allows 1,000 calls to D1, KV and R2
|
|
24
|
+
* and 50 external fetches per request. A `$media` download takes at least three
|
|
25
|
+
* fetches (two DNS-over-HTTPS lookups in `ssrfSafeFetch`, then the file) and
|
|
26
|
+
* three more per redirect. The margin covers a redirect per download, the entry
|
|
27
|
+
* that crosses the budget with its images, the phases after content and the
|
|
28
|
+
* object-cache writes at the end.
|
|
29
|
+
*/
|
|
30
|
+
const SEED_BUDGET_PER_REQUEST: SeedApplyBudget = { queries: 500, mediaDownloads: 5 };
|
|
31
|
+
|
|
21
32
|
export const POST: APIRoute = async ({ request, url, locals }) => {
|
|
22
33
|
const { emdash } = locals;
|
|
23
34
|
|
|
@@ -40,6 +51,19 @@ export const POST: APIRoute = async ({ request, url, locals }) => {
|
|
|
40
51
|
// Options table doesn't exist yet — first-ever setup, allow it
|
|
41
52
|
}
|
|
42
53
|
|
|
54
|
+
const configuredSiteUrl = getConfiguredOrigin(emdash.config);
|
|
55
|
+
const loopbackHost =
|
|
56
|
+
url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
57
|
+
const siteUrl =
|
|
58
|
+
configuredSiteUrl ?? (import.meta.env.DEV && loopbackHost ? url.origin : undefined);
|
|
59
|
+
if (!siteUrl) {
|
|
60
|
+
return apiError(
|
|
61
|
+
"SITE_URL_REQUIRED",
|
|
62
|
+
"Set siteUrl or EMDASH_SITE_URL before running production setup",
|
|
63
|
+
500,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
43
67
|
// Parse request body
|
|
44
68
|
const body = await parseBody(request, setupBody);
|
|
45
69
|
if (isParseError(body)) return body;
|
|
@@ -60,16 +84,22 @@ export const POST: APIRoute = async ({ request, url, locals }) => {
|
|
|
60
84
|
return apiError("INVALID_SEED", `Invalid seed file: ${validation.errors.join(", ")}`, 400);
|
|
61
85
|
}
|
|
62
86
|
|
|
63
|
-
let
|
|
87
|
+
let seeded;
|
|
64
88
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
89
|
+
seeded = await applySeedWithinBudget(
|
|
90
|
+
emdash.db,
|
|
91
|
+
seed,
|
|
92
|
+
{
|
|
93
|
+
includeContent: body.includeContent,
|
|
94
|
+
onConflict: "skip",
|
|
95
|
+
storage: emdash.storage ?? undefined,
|
|
96
|
+
},
|
|
97
|
+
SEED_BUDGET_PER_REQUEST,
|
|
98
|
+
);
|
|
70
99
|
} catch (error) {
|
|
71
100
|
return handleError(error, "Failed to apply seed", "SEED_ERROR");
|
|
72
101
|
}
|
|
102
|
+
const { result, complete: seedComplete, progress: seedProgress } = seeded;
|
|
73
103
|
|
|
74
104
|
// Store setup state
|
|
75
105
|
// In external auth mode, mark setup complete immediately (first user to login becomes admin)
|
|
@@ -85,35 +115,48 @@ export const POST: APIRoute = async ({ request, url, locals }) => {
|
|
|
85
115
|
// observe an empty value and race to write. A spoofed Host header
|
|
86
116
|
// on a later call during the wizard window must not be able to
|
|
87
117
|
// replace the first value.
|
|
88
|
-
const siteUrl = getPublicOrigin(url, emdash.config);
|
|
89
118
|
await options.setIfAbsent("emdash:site_url", siteUrl);
|
|
90
119
|
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
120
|
+
if (seedComplete) {
|
|
121
|
+
if (useExternalAuth) {
|
|
122
|
+
// External auth mode: mark setup complete now
|
|
123
|
+
// First user to log in via external provider will become admin
|
|
124
|
+
await options.set("emdash:setup_complete", true);
|
|
125
|
+
await options.set("emdash:site_title", body.title);
|
|
126
|
+
if (body.tagline) {
|
|
127
|
+
await options.set("emdash:site_tagline", body.tagline);
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
// Passkey/provider mode: store state for next step (admin creation)
|
|
131
|
+
await options.set("emdash:setup_state", {
|
|
132
|
+
step: "site_complete",
|
|
133
|
+
title: body.title,
|
|
134
|
+
tagline: body.tagline,
|
|
135
|
+
});
|
|
98
136
|
}
|
|
99
|
-
} else {
|
|
100
|
-
// Passkey/provider mode: store state for next step (admin creation)
|
|
101
|
-
await options.set("emdash:setup_state", {
|
|
102
|
-
step: "site_complete",
|
|
103
|
-
title: body.title,
|
|
104
|
-
tagline: body.tagline,
|
|
105
|
-
});
|
|
106
137
|
}
|
|
107
138
|
} catch (error) {
|
|
108
139
|
console.error("Failed to save setup state:", error);
|
|
109
140
|
// Non-fatal - continue anyway
|
|
110
141
|
}
|
|
111
142
|
|
|
143
|
+
if (!seedComplete) {
|
|
144
|
+
// The wizard posts again; items already created are skipped.
|
|
145
|
+
return apiSuccess({
|
|
146
|
+
success: true,
|
|
147
|
+
setupComplete: false,
|
|
148
|
+
seedComplete: false,
|
|
149
|
+
seedProgress,
|
|
150
|
+
result,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
112
154
|
// Return success with result
|
|
113
155
|
return apiSuccess({
|
|
114
156
|
success: true,
|
|
115
157
|
// In external auth mode, setup is complete - redirect to admin
|
|
116
158
|
setupComplete: useExternalAuth,
|
|
159
|
+
seedComplete: true,
|
|
117
160
|
result,
|
|
118
161
|
});
|
|
119
162
|
} catch (error) {
|
|
@@ -91,7 +91,8 @@ export const GET: APIRoute = async ({ request, locals, url, session }) => {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
try {
|
|
94
|
-
|
|
94
|
+
// A preview signature covers published content only.
|
|
95
|
+
const includeDrafts = !authorized && url.searchParams.get("drafts") === "true";
|
|
95
96
|
const snapshot = await generateSnapshot(emdash.db, {
|
|
96
97
|
includeDrafts,
|
|
97
98
|
origin: getPublicOrigin(url, emdash.config),
|
package/src/astro/types.ts
CHANGED
|
@@ -466,6 +466,11 @@ export interface EmDashHandlers {
|
|
|
466
466
|
folderId?: string | null;
|
|
467
467
|
}) => Promise<HandlerResponse>;
|
|
468
468
|
|
|
469
|
+
handleMediaRegisterUpload: (input: {
|
|
470
|
+
storageKey: string;
|
|
471
|
+
authorId?: string;
|
|
472
|
+
}) => Promise<HandlerResponse>;
|
|
473
|
+
|
|
469
474
|
handleMediaUpdate: (
|
|
470
475
|
id: string,
|
|
471
476
|
input: {
|
package/src/cleanup.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { Kysely } from "kysely";
|
|
|
14
14
|
|
|
15
15
|
import { cleanupExpiredChallenges } from "./auth/challenge-store.js";
|
|
16
16
|
import { MediaRepository } from "./database/repositories/media.js";
|
|
17
|
+
import { RedirectRepository } from "./database/repositories/redirect.js";
|
|
17
18
|
import { RevisionRepository } from "./database/repositories/revision.js";
|
|
18
19
|
import type { Database } from "./database/types.js";
|
|
19
20
|
import { removeUploadAttempt } from "./media/upload-attempts.js";
|
|
@@ -32,6 +33,8 @@ export interface CleanupResult {
|
|
|
32
33
|
uploadAttempts: number;
|
|
33
34
|
revisionsPruned: number;
|
|
34
35
|
mediaUsage: number;
|
|
36
|
+
/** Oldest 404-log rows deleted to restore the hard cap. */
|
|
37
|
+
notFoundLog: number;
|
|
35
38
|
/** Transfer operations whose staging area was collected. */
|
|
36
39
|
transferStaging: number;
|
|
37
40
|
}
|
|
@@ -62,6 +65,7 @@ export async function runSystemCleanup(
|
|
|
62
65
|
uploadAttempts: -1,
|
|
63
66
|
revisionsPruned: -1,
|
|
64
67
|
mediaUsage: -1,
|
|
68
|
+
notFoundLog: -1,
|
|
65
69
|
transferStaging: -1,
|
|
66
70
|
};
|
|
67
71
|
|
|
@@ -145,6 +149,12 @@ export async function runSystemCleanup(
|
|
|
145
149
|
console.error("[cleanup] Failed to clean media usage:", error);
|
|
146
150
|
}
|
|
147
151
|
|
|
152
|
+
try {
|
|
153
|
+
result.notFoundLog = await new RedirectRepository(db).cleanup404Log();
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error("[cleanup] Failed to cap the 404 log:", error);
|
|
156
|
+
}
|
|
157
|
+
|
|
148
158
|
if (storage) {
|
|
149
159
|
try {
|
|
150
160
|
const { collectTransferStaging } = await import("./transfer/gc.js");
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { PortableText } from "astro-portabletext";
|
|
8
8
|
|
|
9
|
+
import { emdashMarkComponents } from "./marks.js";
|
|
10
|
+
|
|
9
11
|
export interface Props {
|
|
10
12
|
node: {
|
|
11
13
|
_type: "columns";
|
|
@@ -34,7 +36,7 @@ if (!columns.length) {
|
|
|
34
36
|
class="emdash-column"
|
|
35
37
|
style={column.width ? `flex-basis: ${column.width}` : undefined}
|
|
36
38
|
>
|
|
37
|
-
<PortableText value={column.content} />
|
|
39
|
+
<PortableText value={column.content} components={{ mark: emdashMarkComponents }} />
|
|
38
40
|
</div>
|
|
39
41
|
))
|
|
40
42
|
}
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { PortableText } from "astro-portabletext";
|
|
8
8
|
|
|
9
|
+
import { emdashMarkComponents } from "./marks.js";
|
|
10
|
+
|
|
9
11
|
export interface Props {
|
|
10
12
|
node: {
|
|
11
13
|
_type: "cover";
|
|
@@ -61,7 +63,7 @@ const overlayStyle = overlayColor
|
|
|
61
63
|
}
|
|
62
64
|
{hasBackground && <div class="emdash-cover__overlay" style={overlayStyle} />}
|
|
63
65
|
<div class="emdash-cover__content">
|
|
64
|
-
<PortableText value={content} />
|
|
66
|
+
<PortableText value={content} components={{ mark: emdashMarkComponents }} />
|
|
65
67
|
</div>
|
|
66
68
|
</div>
|
|
67
69
|
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { ulid } from "ulidx";
|
|
9
9
|
|
|
10
10
|
import { normalizeFocalPoint, type FocalPointUpdate } from "../../media/focal-point.js";
|
|
11
|
+
import { chunks, SQL_BATCH_SIZE } from "../../utils/chunks.js";
|
|
11
12
|
import type { Database, MediaRow } from "../types.js";
|
|
12
13
|
import type { FindManyResult } from "./types.js";
|
|
13
14
|
import { encodeCursor, decodeCursor } from "./types.js";
|
|
@@ -393,6 +394,20 @@ export class MediaRepository {
|
|
|
393
394
|
return row ? this.rowToItem(row) : null;
|
|
394
395
|
}
|
|
395
396
|
|
|
397
|
+
/**
|
|
398
|
+
* Find the pending media row minted for a signed upload URL.
|
|
399
|
+
*/
|
|
400
|
+
async findPendingByStorageKey(storageKey: string): Promise<MediaItem | null> {
|
|
401
|
+
const row = await this.db
|
|
402
|
+
.selectFrom("media")
|
|
403
|
+
.selectAll()
|
|
404
|
+
.where("storage_key", "=", storageKey)
|
|
405
|
+
.where("status", "=", "pending")
|
|
406
|
+
.executeTakeFirst();
|
|
407
|
+
|
|
408
|
+
return row ? this.rowToItem(row) : null;
|
|
409
|
+
}
|
|
410
|
+
|
|
396
411
|
/**
|
|
397
412
|
* Find media by ID
|
|
398
413
|
*/
|
|
@@ -614,6 +629,15 @@ export class MediaRepository {
|
|
|
614
629
|
return null;
|
|
615
630
|
}
|
|
616
631
|
|
|
632
|
+
async isStorageKeyReferenced(storageKey: string): Promise<boolean> {
|
|
633
|
+
const row = await this.db
|
|
634
|
+
.selectFrom("media")
|
|
635
|
+
.select("id")
|
|
636
|
+
.where("storage_key", "=", storageKey)
|
|
637
|
+
.executeTakeFirst();
|
|
638
|
+
return row !== undefined;
|
|
639
|
+
}
|
|
640
|
+
|
|
617
641
|
async delete(id: string): Promise<boolean> {
|
|
618
642
|
return (await this.deleteWithStorageKey(id)) !== null;
|
|
619
643
|
}
|
|
@@ -682,7 +706,22 @@ export class MediaRepository {
|
|
|
682
706
|
.returning("storage_key")
|
|
683
707
|
.execute();
|
|
684
708
|
|
|
685
|
-
|
|
709
|
+
const keys = rows.map((r) => r.storage_key);
|
|
710
|
+
if (keys.length === 0) return keys;
|
|
711
|
+
|
|
712
|
+
// A stored object may still back another media row (for example after a
|
|
713
|
+
// legacy duplicate registration); never hand such a key to storage deletion.
|
|
714
|
+
const stillReferenced = new Set<string>();
|
|
715
|
+
for (const batch of chunks(keys, SQL_BATCH_SIZE)) {
|
|
716
|
+
const refs = await this.db
|
|
717
|
+
.selectFrom("media")
|
|
718
|
+
.select("storage_key")
|
|
719
|
+
.where("storage_key", "in", batch)
|
|
720
|
+
.execute();
|
|
721
|
+
for (const ref of refs) stillReferenced.add(ref.storage_key);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
return keys.filter((key) => !stillReferenced.has(key));
|
|
686
725
|
}
|
|
687
726
|
|
|
688
727
|
/**
|
|
@@ -17,9 +17,9 @@ import { encodeCursor, decodeCursor, type FindManyResult } from "./types.js";
|
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Hard cap on rows stored in `_emdash_404_log`.
|
|
21
|
-
* rows
|
|
22
|
-
*
|
|
20
|
+
* Hard cap on rows stored in `_emdash_404_log`. Scheduled maintenance evicts
|
|
21
|
+
* the oldest rows by `last_seen_at` without adding a read-amplifying count to
|
|
22
|
+
* the anonymous request path.
|
|
23
23
|
*/
|
|
24
24
|
export const MAX_404_LOG_ROWS = 10_000;
|
|
25
25
|
|
|
@@ -646,7 +646,7 @@ export class RedirectRepository {
|
|
|
646
646
|
// under concurrency: two requests for the same new path can't both
|
|
647
647
|
// insert — the second one hits the conflict branch and increments
|
|
648
648
|
// hits instead of failing with a uniqueness error.
|
|
649
|
-
|
|
649
|
+
await this.db
|
|
650
650
|
.insertInto("_emdash_404_log")
|
|
651
651
|
.values({
|
|
652
652
|
id,
|
|
@@ -667,14 +667,7 @@ export class RedirectRepository {
|
|
|
667
667
|
ip,
|
|
668
668
|
}),
|
|
669
669
|
)
|
|
670
|
-
.
|
|
671
|
-
.executeTakeFirst();
|
|
672
|
-
|
|
673
|
-
// The conflict branch only updates existing rows, so repeat hits
|
|
674
|
-
// cannot grow the table. Only enforce the row cap when we actually
|
|
675
|
-
// inserted a new path.
|
|
676
|
-
if (result?.id !== id) return;
|
|
677
|
-
await this.enforce404Cap();
|
|
670
|
+
.execute();
|
|
678
671
|
}
|
|
679
672
|
|
|
680
673
|
/**
|
|
@@ -682,36 +675,26 @@ export class RedirectRepository {
|
|
|
682
675
|
* MAX_404_LOG_ROWS. "Oldest" is by `last_seen_at`, so a path that keeps
|
|
683
676
|
* getting hit stays in the table even if it was first seen long ago.
|
|
684
677
|
*
|
|
685
|
-
*
|
|
678
|
+
* Called by scheduled system cleanup, never by the anonymous request path.
|
|
686
679
|
*/
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
const count = Number(countRow?.c ?? 0);
|
|
693
|
-
if (count <= MAX_404_LOG_ROWS) return;
|
|
694
|
-
|
|
695
|
-
const excess = count - MAX_404_LOG_ROWS;
|
|
696
|
-
|
|
697
|
-
// Evict the oldest rows in a single SQL statement. Using a subquery
|
|
698
|
-
// (rather than materialising the victim IDs in JS and passing them
|
|
699
|
-
// back as bind parameters) keeps the statement bounded regardless of
|
|
700
|
-
// how far over cap the table is — important for existing installs
|
|
701
|
-
// that crossed the threshold before this cap was introduced.
|
|
702
|
-
await this.db
|
|
680
|
+
async cleanup404Log(): Promise<number> {
|
|
681
|
+
// Keep the newest rows in one statement. Deriving the victims inside the
|
|
682
|
+
// DELETE makes overlapping cleanup runs idempotent: each statement
|
|
683
|
+
// evaluates the current newest set instead of acting on a stale count.
|
|
684
|
+
const result = await this.db
|
|
703
685
|
.deleteFrom("_emdash_404_log")
|
|
704
686
|
.where(
|
|
705
687
|
"id",
|
|
706
|
-
"in",
|
|
688
|
+
"not in",
|
|
707
689
|
this.db
|
|
708
690
|
.selectFrom("_emdash_404_log")
|
|
709
691
|
.select("id")
|
|
710
|
-
.orderBy("last_seen_at", "
|
|
711
|
-
.orderBy("id", "
|
|
712
|
-
.limit(
|
|
692
|
+
.orderBy("last_seen_at", "desc")
|
|
693
|
+
.orderBy("id", "desc")
|
|
694
|
+
.limit(MAX_404_LOG_ROWS),
|
|
713
695
|
)
|
|
714
|
-
.
|
|
696
|
+
.executeTakeFirst();
|
|
697
|
+
return Number(result.numDeletedRows);
|
|
715
698
|
}
|
|
716
699
|
|
|
717
700
|
async find404s(opts: {
|
package/src/emdash-runtime.ts
CHANGED
|
@@ -226,6 +226,7 @@ import {
|
|
|
226
226
|
handleMediaList,
|
|
227
227
|
handleMediaGet,
|
|
228
228
|
handleMediaCreate,
|
|
229
|
+
handleMediaRegisterUpload,
|
|
229
230
|
handleMediaUpdate,
|
|
230
231
|
handleMediaReplaceMetadata,
|
|
231
232
|
handleMediaDelete,
|
|
@@ -4572,6 +4573,33 @@ export class EmDashRuntime {
|
|
|
4572
4573
|
return result;
|
|
4573
4574
|
}
|
|
4574
4575
|
|
|
4576
|
+
async handleMediaRegisterUpload(input: { storageKey: string; authorId?: string }) {
|
|
4577
|
+
if (!this.storage) {
|
|
4578
|
+
return {
|
|
4579
|
+
success: false as const,
|
|
4580
|
+
error: { code: "NO_STORAGE", message: "Storage not configured" },
|
|
4581
|
+
};
|
|
4582
|
+
}
|
|
4583
|
+
const result = await handleMediaRegisterUpload(this.db, this.storage, input);
|
|
4584
|
+
|
|
4585
|
+
if (result.success && this.hooks.hasHooks("media:afterUpload")) {
|
|
4586
|
+
const item = result.data.item;
|
|
4587
|
+
const mediaItem: MediaItem = {
|
|
4588
|
+
id: item.id,
|
|
4589
|
+
filename: item.filename,
|
|
4590
|
+
mimeType: item.mimeType,
|
|
4591
|
+
size: item.size,
|
|
4592
|
+
url: `/media/${item.id}/${item.filename}`,
|
|
4593
|
+
createdAt: item.createdAt,
|
|
4594
|
+
};
|
|
4595
|
+
this.hooks
|
|
4596
|
+
.runMediaAfterUpload(mediaItem)
|
|
4597
|
+
.catch((err) => console.error("EmDash afterUpload hook error:", err));
|
|
4598
|
+
}
|
|
4599
|
+
|
|
4600
|
+
return result;
|
|
4601
|
+
}
|
|
4602
|
+
|
|
4575
4603
|
async handleMediaCreate(input: {
|
|
4576
4604
|
filename: string;
|
|
4577
4605
|
mimeType: string;
|
package/src/index.ts
CHANGED
package/src/mcp/server.ts
CHANGED
|
@@ -2547,27 +2547,15 @@ export function createMcpServer(
|
|
|
2547
2547
|
server.registerTool(
|
|
2548
2548
|
"media_create",
|
|
2549
2549
|
{
|
|
2550
|
-
title: "
|
|
2551
|
-
description:
|
|
2552
|
-
"
|
|
2553
|
-
"
|
|
2554
|
-
"
|
|
2555
|
-
"
|
|
2556
|
-
"media_list / media_get
|
|
2557
|
-
"file itself, use media_upload (base64 data or a public URL) instead.",
|
|
2550
|
+
title: "Confirm Signed Media Upload",
|
|
2551
|
+
description:
|
|
2552
|
+
"Confirm a file after uploading it with a signed URL obtained from " +
|
|
2553
|
+
"POST /_emdash/api/media/upload-url. `storageKey` must be the key returned " +
|
|
2554
|
+
"for the same user. Confirmation checks that the stored file exists and " +
|
|
2555
|
+
"matches the size fixed when the URL was issued, then makes it available " +
|
|
2556
|
+
"through media_list / media_get. To upload through MCP, use media_upload instead.",
|
|
2558
2557
|
inputSchema: z.object({
|
|
2559
|
-
|
|
2560
|
-
mimeType: z.string().describe("MIME type (e.g. 'image/png')"),
|
|
2561
|
-
storageKey: z.string().describe("Storage path/key the file was uploaded to"),
|
|
2562
|
-
size: z.number().int().nonnegative().optional().describe("File size in bytes"),
|
|
2563
|
-
width: z.number().int().positive().optional().describe("Image width in pixels"),
|
|
2564
|
-
height: z.number().int().positive().optional().describe("Image height in pixels"),
|
|
2565
|
-
contentHash: z.string().optional().describe("Hash of the file contents (for dedupe)"),
|
|
2566
|
-
blurhash: z.string().optional().describe("Blurhash for image placeholders"),
|
|
2567
|
-
dominantColor: z
|
|
2568
|
-
.string()
|
|
2569
|
-
.optional()
|
|
2570
|
-
.describe("Hex color string for the image's dominant color"),
|
|
2558
|
+
storageKey: z.string().describe("Storage key returned by the signed upload URL request"),
|
|
2571
2559
|
}),
|
|
2572
2560
|
},
|
|
2573
2561
|
async (args, extra) => {
|
|
@@ -2575,16 +2563,8 @@ export function createMcpServer(
|
|
|
2575
2563
|
requireRole(extra, Role.AUTHOR);
|
|
2576
2564
|
const { emdash, userId } = getExtra(extra);
|
|
2577
2565
|
return unwrap(
|
|
2578
|
-
await emdash.
|
|
2579
|
-
filename: args.filename,
|
|
2580
|
-
mimeType: args.mimeType,
|
|
2566
|
+
await emdash.handleMediaRegisterUpload({
|
|
2581
2567
|
storageKey: args.storageKey,
|
|
2582
|
-
size: args.size,
|
|
2583
|
-
width: args.width,
|
|
2584
|
-
height: args.height,
|
|
2585
|
-
contentHash: args.contentHash,
|
|
2586
|
-
blurhash: args.blurhash,
|
|
2587
|
-
dominantColor: args.dominantColor,
|
|
2588
2568
|
authorId: userId,
|
|
2589
2569
|
}),
|
|
2590
2570
|
);
|
|
@@ -2596,34 +2576,20 @@ export function createMcpServer(
|
|
|
2596
2576
|
{
|
|
2597
2577
|
title: "Upload Media",
|
|
2598
2578
|
description:
|
|
2599
|
-
"Upload a media file from base64-encoded data
|
|
2579
|
+
"Upload a media file from base64-encoded data and " +
|
|
2600
2580
|
"register it in the media library. Returns the media item with id, " +
|
|
2601
2581
|
"storageKey, and url — ready to reference from content fields (e.g. " +
|
|
2602
2582
|
"featured_image) via content_create / content_update. Uploads are " +
|
|
2603
2583
|
"deduplicated by content hash: re-uploading identical bytes returns " +
|
|
2604
|
-
"the existing item with deduplicated: true.
|
|
2605
|
-
"to a public http(s) host (SSRF-guarded). Subject to the global " +
|
|
2584
|
+
"the existing item with deduplicated: true. Subject to the global " +
|
|
2606
2585
|
"upload MIME allowlist and the configured maximum upload size.",
|
|
2607
2586
|
inputSchema: z.object({
|
|
2608
2587
|
filename: z.string().min(1).describe("Filename including extension (e.g. 'cover.png')"),
|
|
2609
|
-
base64: z
|
|
2610
|
-
.string()
|
|
2611
|
-
.optional()
|
|
2612
|
-
.describe("Base64-encoded file contents. Provide exactly one of base64 / url."),
|
|
2613
|
-
url: z
|
|
2614
|
-
.url()
|
|
2615
|
-
.optional()
|
|
2616
|
-
.describe(
|
|
2617
|
-
"Public http(s) URL to fetch the file from. Provide exactly one of base64 / url.",
|
|
2618
|
-
),
|
|
2588
|
+
base64: z.string().describe("Base64-encoded file contents."),
|
|
2619
2589
|
contentType: z
|
|
2620
2590
|
.string()
|
|
2621
2591
|
.regex(CONTENT_TYPE_RE, "Invalid content type")
|
|
2622
|
-
.
|
|
2623
|
-
.describe(
|
|
2624
|
-
"MIME type (e.g. 'image/png'). Required with base64; with url it " +
|
|
2625
|
-
"defaults to the response's Content-Type header.",
|
|
2626
|
-
),
|
|
2592
|
+
.describe("MIME type (e.g. 'image/png')."),
|
|
2627
2593
|
alt: z.string().optional().describe("Alt text for accessibility"),
|
|
2628
2594
|
}),
|
|
2629
2595
|
annotations: { destructiveHint: false },
|
|
@@ -2640,7 +2606,6 @@ export function createMcpServer(
|
|
|
2640
2606
|
await emdash.handleMediaUpload({
|
|
2641
2607
|
filename: args.filename,
|
|
2642
2608
|
base64: args.base64,
|
|
2643
|
-
url: args.url,
|
|
2644
2609
|
contentType: args.contentType,
|
|
2645
2610
|
alt: args.alt,
|
|
2646
2611
|
authorId: userId,
|