emdash 0.35.0 → 0.36.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/activation-K0kGBWuJ.mjs +382 -0
- package/dist/activation-K0kGBWuJ.mjs.map +1 -0
- package/dist/{adapters-Bb1vG41D.d.mts → adapters-CzL3H752.d.mts} +3 -3
- package/dist/{adapters-Bb1vG41D.d.mts.map → adapters-CzL3H752.d.mts.map} +1 -1
- package/dist/{allowed-origins-DW7sYyqg.mjs → allowed-origins-maylGaHv.mjs} +2 -2
- package/dist/{allowed-origins-DW7sYyqg.mjs.map → allowed-origins-maylGaHv.mjs.map} +1 -1
- package/dist/api/route-utils.d.mts +3 -3
- package/dist/api/route-utils.mjs +22 -20
- package/dist/api/route-utils.mjs.map +1 -1
- package/dist/api/schemas/index.d.mts +2 -2
- package/dist/api/schemas/index.mjs +5 -5
- package/dist/api-B0w3uBiG.mjs +2269 -0
- package/dist/api-B0w3uBiG.mjs.map +1 -0
- package/dist/{api-tokens-KO24G1Fd.mjs → api-tokens-9YrrCoE6.mjs} +2 -2
- package/dist/{api-tokens-KO24G1Fd.mjs.map → api-tokens-9YrrCoE6.mjs.map} +1 -1
- package/dist/{apply-kUDt80-G.mjs → apply-B_VMpJ2P.mjs} +34 -35
- package/dist/{apply-kUDt80-G.mjs.map → apply-B_VMpJ2P.mjs.map} +1 -1
- package/dist/astro/index.d.mts +12 -12
- package/dist/astro/index.mjs +26 -15
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +10 -10
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +10 -7
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/media-usage-write-fence.mjs +2 -2
- package/dist/astro/middleware/redirect.mjs +4 -4
- package/dist/astro/middleware/request-context.mjs +3 -3
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts +11 -12
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +168 -1305
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +6 -6
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs +6 -6
- package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +3 -3
- package/dist/astro/routes/api/admin/api-tokens/index.mjs +4 -4
- package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +4 -4
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +9 -9
- package/dist/astro/routes/api/admin/byline-fields/index.mjs +9 -9
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +9 -9
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +15 -15
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +15 -15
- package/dist/astro/routes/api/admin/bylines/index.mjs +15 -15
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +12 -12
- package/dist/astro/routes/api/admin/comments/_id_.mjs +4 -4
- package/dist/astro/routes/api/admin/comments/bulk.mjs +9 -9
- package/dist/astro/routes/api/admin/comments/counts.mjs +4 -4
- package/dist/astro/routes/api/admin/comments/index.mjs +9 -9
- package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +4 -4
- package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +3 -3
- package/dist/astro/routes/api/admin/media-usage/activation.d.mts +9 -0
- package/dist/astro/routes/api/admin/media-usage/activation.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/activation.mjs +46 -0
- package/dist/astro/routes/api/admin/media-usage/activation.mjs.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs +14 -13
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs +14 -13
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs.map +1 -1
- package/dist/astro/routes/api/admin/media-usage/progress.d.mts +9 -0
- package/dist/astro/routes/api/admin/media-usage/progress.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/progress.mjs +44 -0
- package/dist/astro/routes/api/admin/media-usage/progress.mjs.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/repair.mjs +16 -11
- package/dist/astro/routes/api/admin/media-usage/repair.mjs.map +1 -1
- package/dist/astro/routes/api/admin/media-usage/work/index.mjs +14 -13
- package/dist/astro/routes/api/admin/media-usage/work/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/media-usage/work/retry.mjs +14 -13
- package/dist/astro/routes/api/admin/media-usage/work/retry.mjs.map +1 -1
- package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +3 -3
- package/dist/astro/routes/api/admin/oauth-clients/index.mjs +3 -3
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +54 -48
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +54 -48
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs +4 -4
- package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +53 -47
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +53 -47
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/index.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +2 -2
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +53 -47
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +53 -47
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +54 -48
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/artifact.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +172 -133
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +54 -48
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/updates.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +52 -46
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +2 -2
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +52 -46
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/users/_id_/disable.mjs +2 -2
- package/dist/astro/routes/api/admin/users/_id_/enable.mjs +1 -1
- package/dist/astro/routes/api/admin/users/_id_/index.mjs +7 -7
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +3 -3
- package/dist/astro/routes/api/admin/users/index.mjs +6 -6
- package/dist/astro/routes/api/auth/dev-bypass.mjs +3 -3
- package/dist/astro/routes/api/auth/invite/accept.mjs +1 -1
- package/dist/astro/routes/api/auth/invite/complete.mjs +11 -11
- package/dist/astro/routes/api/auth/invite/index.mjs +8 -8
- package/dist/astro/routes/api/auth/invite/register-options.mjs +10 -10
- package/dist/astro/routes/api/auth/logout.mjs +2 -2
- package/dist/astro/routes/api/auth/magic-link/send.mjs +10 -10
- package/dist/astro/routes/api/auth/magic-link/verify.mjs +2 -2
- package/dist/astro/routes/api/auth/me.d.mts.map +1 -1
- package/dist/astro/routes/api/auth/me.mjs +19 -9
- package/dist/astro/routes/api/auth/me.mjs.map +1 -1
- package/dist/astro/routes/api/auth/mode.mjs +2 -2
- package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +4 -4
- package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/_id_.mjs +6 -6
- package/dist/astro/routes/api/auth/passkey/index.mjs +1 -1
- package/dist/astro/routes/api/auth/passkey/options.mjs +12 -12
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +10 -10
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +11 -11
- package/dist/astro/routes/api/auth/passkey/verify.mjs +11 -11
- package/dist/astro/routes/api/auth/signup/complete.mjs +11 -11
- package/dist/astro/routes/api/auth/signup/request.mjs +10 -10
- package/dist/astro/routes/api/auth/signup/verify.mjs +1 -1
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +13 -13
- package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +12 -12
- package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +11 -11
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +8 -8
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs +16 -14
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs +16 -14
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +8 -8
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +13 -13
- package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_.mjs +8 -8
- package/dist/astro/routes/api/content/_collection_/authors.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/index.mjs +8 -8
- package/dist/astro/routes/api/content/_collection_/trash.mjs +7 -7
- package/dist/astro/routes/api/dashboard.mjs +7 -7
- package/dist/astro/routes/api/dev/emails.mjs +2 -2
- package/dist/astro/routes/api/import/probe.d.mts +3 -3
- package/dist/astro/routes/api/import/probe.mjs +16 -16
- package/dist/astro/routes/api/import/wordpress/analyze.mjs +2 -2
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +11 -11
- package/dist/astro/routes/api/import/wordpress/execute.mjs +12 -12
- package/dist/astro/routes/api/import/wordpress/media.d.mts +10 -10
- package/dist/astro/routes/api/import/wordpress/media.mjs +11 -11
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +9 -9
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +11 -11
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +12 -10
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +16 -16
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +11 -11
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +26 -26
- package/dist/astro/routes/api/manifest.mjs +4 -4
- package/dist/astro/routes/api/mcp.mjs +42 -42
- package/dist/astro/routes/api/media/_id_/confirm.mjs +11 -11
- package/dist/astro/routes/api/media/_id_/upload.mjs +6 -6
- package/dist/astro/routes/api/media/_id_/usage.mjs +18 -13
- package/dist/astro/routes/api/media/_id_/usage.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_.d.mts.map +1 -1
- package/dist/astro/routes/api/media/_id_.mjs +21 -13
- package/dist/astro/routes/api/media/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/media/file/_...key_.mjs +1 -1
- package/dist/astro/routes/api/media/folders/_id_.d.mts +10 -0
- package/dist/astro/routes/api/media/folders/_id_.d.mts.map +1 -0
- package/dist/astro/routes/api/media/folders/_id_.mjs +52 -0
- package/dist/astro/routes/api/media/folders/_id_.mjs.map +1 -0
- package/dist/astro/routes/api/media/folders/index.d.mts +9 -0
- package/dist/astro/routes/api/media/folders/index.d.mts.map +1 -0
- package/dist/astro/routes/api/media/folders/index.mjs +41 -0
- package/dist/astro/routes/api/media/folders/index.mjs.map +1 -0
- package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +2 -2
- package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +2 -2
- package/dist/astro/routes/api/media/providers/index.mjs +2 -2
- package/dist/astro/routes/api/media/upload-url.mjs +9 -9
- package/dist/astro/routes/api/media.d.mts.map +1 -1
- package/dist/astro/routes/api/media.mjs +28 -19
- package/dist/astro/routes/api/media.mjs.map +1 -1
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +8 -8
- package/dist/astro/routes/api/menus/_name_/items.mjs +8 -8
- package/dist/astro/routes/api/menus/_name_/reorder.mjs +8 -8
- package/dist/astro/routes/api/menus/_name_/translations.mjs +8 -8
- package/dist/astro/routes/api/menus/_name_.mjs +8 -8
- package/dist/astro/routes/api/menus/index.mjs +8 -8
- package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
- package/dist/astro/routes/api/oauth/device/authorize.mjs +5 -5
- package/dist/astro/routes/api/oauth/device/code.mjs +8 -8
- package/dist/astro/routes/api/oauth/device/token.mjs +7 -7
- package/dist/astro/routes/api/oauth/register.mjs +2 -2
- package/dist/astro/routes/api/oauth/token/refresh.mjs +5 -5
- package/dist/astro/routes/api/oauth/token/revoke.mjs +5 -5
- package/dist/astro/routes/api/oauth/token.mjs +5 -5
- package/dist/astro/routes/api/openapi.json.mjs +153 -6
- package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +4 -4
- package/dist/astro/routes/api/redirects/404s/index.mjs +10 -10
- package/dist/astro/routes/api/redirects/404s/summary.mjs +10 -10
- package/dist/astro/routes/api/redirects/_id_.mjs +11 -11
- package/dist/astro/routes/api/redirects/index.mjs +11 -11
- package/dist/astro/routes/api/relations/_id_/index.mjs +16 -14
- package/dist/astro/routes/api/relations/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/relations/_id_/translations.mjs +11 -9
- package/dist/astro/routes/api/relations/_id_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/relations/index.mjs +16 -14
- package/dist/astro/routes/api/relations/index.mjs.map +1 -1
- package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +2 -2
- package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +52 -46
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +52 -46
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +52 -46
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +52 -46
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/index.mjs +52 -46
- package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/reorder.mjs +52 -46
- package/dist/astro/routes/api/schema/collections/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/schema/index.mjs +10 -8
- package/dist/astro/routes/api/schema/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +52 -46
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/schema/orphans/index.mjs +52 -46
- package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
- package/dist/astro/routes/api/search/enable.mjs +10 -10
- package/dist/astro/routes/api/search/index.mjs +9 -9
- package/dist/astro/routes/api/search/rebuild.mjs +10 -10
- package/dist/astro/routes/api/search/stats.mjs +5 -5
- package/dist/astro/routes/api/search/suggest.mjs +9 -9
- package/dist/astro/routes/api/sections/_slug_.mjs +9 -9
- package/dist/astro/routes/api/sections/index.mjs +9 -9
- package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +6 -6
- package/dist/astro/routes/api/settings/backups/archives/index.mjs +5 -5
- package/dist/astro/routes/api/settings/backups/export.mjs +6 -6
- package/dist/astro/routes/api/settings/backups/index.mjs +7 -7
- package/dist/astro/routes/api/settings/email.mjs +4 -4
- package/dist/astro/routes/api/settings.mjs +13 -13
- package/dist/astro/routes/api/setup/admin-verify.mjs +12 -12
- package/dist/astro/routes/api/setup/admin.mjs +11 -11
- package/dist/astro/routes/api/setup/dev-bypass.d.mts.map +1 -1
- package/dist/astro/routes/api/setup/dev-bypass.mjs +34 -27
- package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
- package/dist/astro/routes/api/setup/dev-reset.mjs +2 -2
- package/dist/astro/routes/api/setup/index.mjs +31 -29
- package/dist/astro/routes/api/setup/index.mjs.map +1 -1
- package/dist/astro/routes/api/setup/status.mjs +3 -3
- package/dist/astro/routes/api/snapshot.mjs +7 -7
- package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs +13 -13
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +13 -13
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +13 -13
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +13 -13
- package/dist/astro/routes/api/taxonomies/_name_/translations.mjs +13 -13
- package/dist/astro/routes/api/taxonomies/_name_.mjs +13 -13
- package/dist/astro/routes/api/taxonomies/index.mjs +13 -13
- package/dist/astro/routes/api/themes/preview.mjs +5 -5
- package/dist/astro/routes/api/typegen.mjs +4 -4
- package/dist/astro/routes/api/well-known/auth.mjs +2 -2
- package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
- package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +7 -7
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +10 -10
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +10 -10
- package/dist/astro/routes/api/widget-areas/_name_.mjs +5 -5
- package/dist/astro/routes/api/widget-areas/index.mjs +10 -10
- package/dist/astro/routes/api/widget-components.mjs +2 -2
- package/dist/astro/routes/robots.txt.mjs +6 -6
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +9 -9
- package/dist/astro/routes/sitemap.xml.mjs +7 -7
- package/dist/astro/types.d.mts +19 -14
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/auth/providers/github.d.mts +1 -1
- package/dist/auth/providers/google.d.mts +1 -1
- package/dist/{authorize-DUElxgOQ.mjs → authorize-ByYs-Uxs.mjs} +2 -2
- package/dist/{authorize-DUElxgOQ.mjs.map → authorize-ByYs-Uxs.mjs.map} +1 -1
- package/dist/{backup-DWYpJlLj.mjs → backup-ChyHfIoX.mjs} +5 -5
- package/dist/{backup-DWYpJlLj.mjs.map → backup-ChyHfIoX.mjs.map} +1 -1
- package/dist/{bundle-vn56sLos.mjs → bundle-DZFhFurR.mjs} +4 -4
- package/dist/{bundle-vn56sLos.mjs.map → bundle-DZFhFurR.mjs.map} +1 -1
- package/dist/{byline-yjvWq4ip.mjs → byline-Cpnwrje1.mjs} +6 -6
- package/dist/{byline-yjvWq4ip.mjs.map → byline-Cpnwrje1.mjs.map} +1 -1
- package/dist/{byline-fields-txaLGoME.mjs → byline-fields-CAyA_Umd.mjs} +2 -2
- package/dist/{byline-fields-txaLGoME.mjs.map → byline-fields-CAyA_Umd.mjs.map} +1 -1
- package/dist/{byline-registry-DDun8sxh.mjs → byline-registry-Bpu9-MVs.mjs} +2 -2
- package/dist/{byline-registry-DDun8sxh.mjs.map → byline-registry-Bpu9-MVs.mjs.map} +1 -1
- package/dist/{bylines-DjXS5p0P.mjs → bylines-BSo9B9RX.mjs} +7 -7
- package/dist/{bylines-DjXS5p0P.mjs.map → bylines-BSo9B9RX.mjs.map} +1 -1
- package/dist/{bylines-Bu7pchQ4.mjs → bylines-CHjvehVX.mjs} +3 -3
- package/dist/{bylines-Bu7pchQ4.mjs.map → bylines-CHjvehVX.mjs.map} +1 -1
- package/dist/{cache-C46-3Irl.mjs → cache-5VirJ2Cn.mjs} +4 -4
- package/dist/{cache-C46-3Irl.mjs.map → cache-5VirJ2Cn.mjs.map} +1 -1
- package/dist/capture-triggers-CNW1q3HK.mjs +362 -0
- package/dist/capture-triggers-CNW1q3HK.mjs.map +1 -0
- package/dist/{challenge-store-SIL5DsF2.mjs → challenge-store-D5pSY2S1.mjs} +1 -1
- package/dist/{challenge-store-SIL5DsF2.mjs.map → challenge-store-D5pSY2S1.mjs.map} +1 -1
- package/dist/{chunks-fiZ8G9RJ.mjs → chunks-bNiX6_DR.mjs} +2 -2
- package/dist/{chunks-fiZ8G9RJ.mjs.map → chunks-bNiX6_DR.mjs.map} +1 -1
- package/dist/cli/index.mjs +36 -52
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +74 -4
- package/dist/client/index.d.mts.map +1 -1
- package/dist/client/index.mjs +54 -2
- package/dist/client/index.mjs.map +1 -1
- package/dist/{collection-deletion-DQPvMD8R.mjs → collection-deletion-m_oi2c0N.mjs} +13 -356
- package/dist/collection-deletion-m_oi2c0N.mjs.map +1 -0
- package/dist/{comment-Dxlep6cE.mjs → comment-DhvLEdhQ.mjs} +1 -1
- package/dist/{comment-Dxlep6cE.mjs.map → comment-DhvLEdhQ.mjs.map} +1 -1
- package/dist/{comment-reaction-D-2J84UE.mjs → comment-reaction-CRuItEjt.mjs} +2 -2
- package/dist/{comment-reaction-D-2J84UE.mjs.map → comment-reaction-CRuItEjt.mjs.map} +1 -1
- package/dist/{comments-CTfZtLmM.mjs → comments-CLj4bi8I.mjs} +2 -2
- package/dist/{comments-CTfZtLmM.mjs.map → comments-CLj4bi8I.mjs.map} +1 -1
- package/dist/{components-CQy3lh9x.mjs → components-v4U4DT2y.mjs} +1 -1
- package/dist/{components-CQy3lh9x.mjs.map → components-v4U4DT2y.mjs.map} +1 -1
- package/dist/{config-COQzTe5Q.d.mts → config-DrfghTPe.d.mts} +1 -1
- package/dist/{config-COQzTe5Q.d.mts.map → config-DrfghTPe.d.mts.map} +1 -1
- package/dist/{content-ZHCc1ngN.mjs → content-CP-SeXNa.mjs} +5 -4
- package/dist/content-CP-SeXNa.mjs.map +1 -0
- package/dist/{content-list-query-wJ2WN8Vp.d.mts → content-list-query-BhejKVqb.d.mts} +1 -1
- package/dist/{content-list-query-wJ2WN8Vp.d.mts.map → content-list-query-BhejKVqb.d.mts.map} +1 -1
- package/dist/{content-refresh-B_8SJzuU.mjs → content-refresh-DLOfIhYc.mjs} +1094 -93
- package/dist/content-refresh-DLOfIhYc.mjs.map +1 -0
- package/dist/{media-usage-BZCydfvu.mjs → content-repair-BCxzy0r4.mjs} +5 -196
- package/dist/content-repair-BCxzy0r4.mjs.map +1 -0
- package/dist/{context-ZldbZcle.mjs → context-BR5hRWiK.mjs} +13 -13
- package/dist/{context-ZldbZcle.mjs.map → context-BR5hRWiK.mjs.map} +1 -1
- package/dist/{cron-Y9aLtgrA.mjs → cron-CWfWrWKG.mjs} +1 -1
- package/dist/{cron-Y9aLtgrA.mjs.map → cron-CWfWrWKG.mjs.map} +1 -1
- package/dist/{dashboard-DvHbk_cv.mjs → dashboard-CkIV2rnQ.mjs} +5 -5
- package/dist/{dashboard-DvHbk_cv.mjs.map → dashboard-CkIV2rnQ.mjs.map} +1 -1
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +3 -3
- package/dist/db/index.mjs.map +1 -1
- package/dist/db/libsql-migrations.d.mts +1 -1
- package/dist/db/libsql-migrations.mjs +4 -4
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres-migrations.d.mts +1 -1
- package/dist/db/postgres-migrations.mjs +4 -4
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite-migrations.d.mts +1 -1
- package/dist/db/sqlite-migrations.mjs +4 -4
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/db/sqlite.mjs +3 -3
- package/dist/db/sqlite.mjs.map +1 -1
- package/dist/{default-DdDl3fxv.mjs → default-YMEfcQUx.mjs} +1 -1
- package/dist/{default-DdDl3fxv.mjs.map → default-YMEfcQUx.mjs.map} +1 -1
- package/dist/{deferred-tasks-SDWxrgzJ.d.mts → deferred-tasks-DfqxH5Xf.d.mts} +1 -1
- package/dist/{deferred-tasks-SDWxrgzJ.d.mts.map → deferred-tasks-DfqxH5Xf.d.mts.map} +1 -1
- package/dist/{device-flow-C0GdD_z8.mjs → device-flow-CojjSBwb.mjs} +5 -5
- package/dist/{device-flow-C0GdD_z8.mjs.map → device-flow-CojjSBwb.mjs.map} +1 -1
- package/dist/{direct-executor-CDXeJCSs.mjs → direct-executor-BWeWiRYB.mjs} +3 -3
- package/dist/{direct-executor-CDXeJCSs.mjs.map → direct-executor-BWeWiRYB.mjs.map} +1 -1
- package/dist/{dist-D_6CKR8z.mjs → dist-89xCrGqi.mjs} +1 -1
- package/dist/{dist-D_6CKR8z.mjs.map → dist-89xCrGqi.mjs.map} +1 -1
- package/dist/{email-console-BHNjIwiB.mjs → email-console-CEJgR_LB.mjs} +1 -1
- package/dist/{email-console-BHNjIwiB.mjs.map → email-console-CEJgR_LB.mjs.map} +1 -1
- package/dist/{enrich-_ySMEvEE.mjs → enrich-BX0IDp4z.mjs} +3 -3
- package/dist/{enrich-_ySMEvEE.mjs.map → enrich-BX0IDp4z.mjs.map} +1 -1
- package/dist/{error-DupyJZE2.mjs → error-D9w94fjV.mjs} +2 -2
- package/dist/{error-DupyJZE2.mjs.map → error-D9w94fjV.mjs.map} +1 -1
- package/dist/{errors-CLUPm7aD.mjs → errors-n2j49uNf.mjs} +22 -2
- package/dist/errors-n2j49uNf.mjs.map +1 -0
- package/dist/{escape-DUY2r2Zs.mjs → escape-DN4Qmjsl.mjs} +1 -1
- package/dist/{escape-DUY2r2Zs.mjs.map → escape-DN4Qmjsl.mjs.map} +1 -1
- package/dist/{field-defs-cache-BzwBlJrR.mjs → field-defs-cache-Bh4q6oar.mjs} +5 -5
- package/dist/{field-defs-cache-BzwBlJrR.mjs.map → field-defs-cache-Bh4q6oar.mjs.map} +1 -1
- package/dist/focal-point-BeWFo6Xx.mjs +23 -0
- package/dist/focal-point-BeWFo6Xx.mjs.map +1 -0
- package/dist/{fts-manager-DyZEohZC.mjs → fts-manager-BhOdW0bi.mjs} +3 -3
- package/dist/{fts-manager-DyZEohZC.mjs.map → fts-manager-BhOdW0bi.mjs.map} +1 -1
- package/dist/{hash-Cg0TXJPV.mjs → hash-CTcngWvj.mjs} +1 -1
- package/dist/{hash-Cg0TXJPV.mjs.map → hash-CTcngWvj.mjs.map} +1 -1
- package/dist/{hreflang-DwT2E0Sa.d.mts → hreflang-7pTHsLZJ.d.mts} +2 -2
- package/dist/{hreflang-DwT2E0Sa.d.mts.map → hreflang-7pTHsLZJ.d.mts.map} +1 -1
- package/dist/{hreflang-BBe83kKa.mjs → hreflang-Huc6O5wa.mjs} +7 -7
- package/dist/{hreflang-BBe83kKa.mjs.map → hreflang-Huc6O5wa.mjs.map} +1 -1
- package/dist/{identity-Ys6Dlc9C.mjs → identity-DXmb4aVp.mjs} +3 -3
- package/dist/{identity-Ys6Dlc9C.mjs.map → identity-DXmb4aVp.mjs.map} +1 -1
- package/dist/{import-DPkLmeXO.mjs → import-C4_O3KXU.mjs} +5 -5
- package/dist/{import-DPkLmeXO.mjs.map → import-C4_O3KXU.mjs.map} +1 -1
- package/dist/{index-C9KuQyO-.d.mts → index-6bRee8zb.d.mts} +3 -3
- package/dist/{index-C9KuQyO-.d.mts.map → index-6bRee8zb.d.mts.map} +1 -1
- package/dist/{index-BVBZj4-7.d.mts → index-DWbJrsAg.d.mts} +128 -52
- package/dist/index-DWbJrsAg.d.mts.map +1 -0
- package/dist/index.d.mts +21 -21
- package/dist/index.mjs +78 -72
- package/dist/{init-lock-6b309ZrF.mjs → init-lock-DJkX6Hto.mjs} +1 -1
- package/dist/{init-lock-6b309ZrF.mjs.map → init-lock-DJkX6Hto.mjs.map} +1 -1
- package/dist/{load-YJ2xl3ra.mjs → load-Bo7fZAbg.mjs} +2 -2
- package/dist/{load-YJ2xl3ra.mjs.map → load-Bo7fZAbg.mjs.map} +1 -1
- package/dist/{loader-CdG_Ci3H.mjs → loader-ZsjE4paL.mjs} +2 -2
- package/dist/{loader-CdG_Ci3H.mjs.map → loader-ZsjE4paL.mjs.map} +1 -1
- package/dist/{manifest-builder-B252yQUf.mjs → manifest-builder-QXuqvVMP.mjs} +2 -2
- package/dist/{manifest-builder-B252yQUf.mjs.map → manifest-builder-QXuqvVMP.mjs.map} +1 -1
- package/dist/{manifest-schema-BXbUAHod.mjs → manifest-schema-CUYPfCEH.mjs} +1 -1
- package/dist/{manifest-schema-BXbUAHod.mjs.map → manifest-schema-CUYPfCEH.mjs.map} +1 -1
- package/dist/media/image-endpoint.mjs +1 -1
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +13 -3
- package/dist/media/index.mjs.map +1 -1
- package/dist/media/local-runtime.d.mts +14 -12
- package/dist/media/local-runtime.d.mts.map +1 -1
- package/dist/media/local-runtime.mjs +11 -5
- package/dist/media/local-runtime.mjs.map +1 -1
- package/dist/{media-BWWWb8kg.mjs → media-BjwNuLEu.mjs} +100 -6
- package/dist/media-BjwNuLEu.mjs.map +1 -0
- package/dist/{media-CzhAn2--.mjs → media-D6SeAfBl.mjs} +41 -12
- package/dist/media-D6SeAfBl.mjs.map +1 -0
- package/dist/{media-allowlist-D6x-Us02.mjs → media-allowlist-D35v7bmL.mjs} +3 -2
- package/dist/media-allowlist-D35v7bmL.mjs.map +1 -0
- package/dist/media-folders-D-0CmRZj.mjs +210 -0
- package/dist/media-folders-D-0CmRZj.mjs.map +1 -0
- package/dist/{media-upload-CHBVn6m-.mjs → media-upload-C7bLXGcI.mjs} +9 -9
- package/dist/{media-upload-CHBVn6m-.mjs.map → media-upload-C7bLXGcI.mjs.map} +1 -1
- package/dist/{media-url-BvoX88FA.mjs → media-url-DH7giMx_.mjs} +1 -1
- package/dist/{media-url-BvoX88FA.mjs.map → media-url-DH7giMx_.mjs.map} +1 -1
- package/dist/media-usage-BTRJvS6V.mjs +1833 -0
- package/dist/media-usage-BTRJvS6V.mjs.map +1 -0
- package/dist/media-usage-activation-Dx0kPV6b.mjs +78 -0
- package/dist/media-usage-activation-Dx0kPV6b.mjs.map +1 -0
- package/dist/{media-usage-work-DrCbqBcl.mjs → media-usage-work-C-fhM30c.mjs} +221 -59
- package/dist/media-usage-work-C-fhM30c.mjs.map +1 -0
- package/dist/{media-usage-work-CLo_6tct.mjs → media-usage-work-DWb2XPMI.mjs} +4 -4
- package/dist/{media-usage-work-CLo_6tct.mjs.map → media-usage-work-DWb2XPMI.mjs.map} +1 -1
- package/dist/{media-usage-write-fence-aHEgN1zB.mjs → media-usage-write-fence-BwoJVRDC.mjs} +2 -2
- package/dist/{media-usage-write-fence-aHEgN1zB.mjs.map → media-usage-write-fence-BwoJVRDC.mjs.map} +1 -1
- package/dist/{menus-TCNSd2vp.mjs → menus-CD7EFLwz.mjs} +3 -3
- package/dist/{menus-TCNSd2vp.mjs.map → menus-CD7EFLwz.mjs.map} +1 -1
- package/dist/{menus-DSo5QeW0.mjs → menus-CNg0QlDt.mjs} +42 -26
- package/dist/menus-CNg0QlDt.mjs.map +1 -0
- package/dist/migrations/index.d.mts +1 -1
- package/dist/migrations/index.mjs +3 -3
- package/dist/{mime-DfIVjNkr.mjs → mime-CUIG6x84.mjs} +1 -1
- package/dist/mime-CUIG6x84.mjs.map +1 -0
- package/dist/{mode-BDoH_WcF.mjs → mode-DaUM_MB2.mjs} +1 -1
- package/dist/{mode-BDoH_WcF.mjs.map → mode-DaUM_MB2.mjs.map} +1 -1
- package/dist/node-sqlite-compat-Udb4Y-rs.mjs +46 -0
- package/dist/node-sqlite-compat-Udb4Y-rs.mjs.map +1 -0
- package/dist/{normalize-Bc-s_r-a.mjs → normalize-CSxdKBVa.mjs} +22 -1
- package/dist/normalize-CSxdKBVa.mjs.map +1 -0
- package/dist/{oauth-authorization-iOikmrJd.mjs → oauth-authorization-Dpsb-fMJ.mjs} +5 -5
- package/dist/{oauth-authorization-iOikmrJd.mjs.map → oauth-authorization-Dpsb-fMJ.mjs.map} +1 -1
- package/dist/{oauth-clients-CvI0nHMJ.mjs → oauth-clients-Bxnoe_67.mjs} +1 -1
- package/dist/{oauth-clients-CvI0nHMJ.mjs.map → oauth-clients-Bxnoe_67.mjs.map} +1 -1
- package/dist/{oauth-state-store-Ba8KZlwW.mjs → oauth-state-store-0C4s0K6w.mjs} +1 -1
- package/dist/{oauth-state-store-Ba8KZlwW.mjs.map → oauth-state-store-0C4s0K6w.mjs.map} +1 -1
- package/dist/{oauth-user-lookup-C5SQwGYC.mjs → oauth-user-lookup-wlGYSeqX.mjs} +1 -1
- package/dist/{oauth-user-lookup-C5SQwGYC.mjs.map → oauth-user-lookup-wlGYSeqX.mjs.map} +1 -1
- package/dist/object-cache/memory.d.mts +1 -1
- package/dist/{options-CBXIHUoT.mjs → options-BHPeIbBd.mjs} +1 -1
- package/dist/{options-CBXIHUoT.mjs.map → options-BHPeIbBd.mjs.map} +1 -1
- package/dist/{options--hhz-jZ1.d.mts → options-BhZIeFY1.d.mts} +3 -3
- package/dist/{options--hhz-jZ1.d.mts.map → options-BhZIeFY1.d.mts.map} +1 -1
- package/dist/page/index.d.mts +2 -2
- package/dist/{parse-DFbuc-Fh.mjs → parse-BRJg1kz8.mjs} +2 -2
- package/dist/{parse-DFbuc-Fh.mjs.map → parse-BRJg1kz8.mjs.map} +1 -1
- package/dist/{passkey-config-DFAmUyuE.mjs → passkey-config-B3P0nHRR.mjs} +1 -1
- package/dist/{passkey-config-DFAmUyuE.mjs.map → passkey-config-B3P0nHRR.mjs.map} +1 -1
- package/dist/{patterns-CiyXeDgr.mjs → patterns-D9iDO3j8.mjs} +1 -1
- package/dist/{patterns-CiyXeDgr.mjs.map → patterns-D9iDO3j8.mjs.map} +1 -1
- package/dist/{placeholder-Dp0K3RkD.d.mts → placeholder-BR4ZhVJl.d.mts} +7 -1
- package/dist/placeholder-BR4ZhVJl.d.mts.map +1 -0
- package/dist/plugin-types.d.mts +1 -1
- package/dist/plugin-utils.d.mts +10 -10
- package/dist/plugins/adapt-sandbox-entry.d.mts +10 -10
- package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
- package/dist/{policy-TluUIxuu.mjs → policy-BVRganJQ.mjs} +2 -2
- package/dist/{policy-TluUIxuu.mjs.map → policy-BVRganJQ.mjs.map} +1 -1
- package/dist/{portable-text-imBW5cHK.mjs → portable-text-C8Tvp4_g.mjs} +1 -1
- package/dist/{portable-text-imBW5cHK.mjs.map → portable-text-C8Tvp4_g.mjs.map} +1 -1
- package/dist/{preview-PW-nOnq4.mjs → preview-dn2dUyM0.mjs} +2 -2
- package/dist/{preview-PW-nOnq4.mjs.map → preview-dn2dUyM0.mjs.map} +1 -1
- package/dist/{protocol-DjsJHTlK.d.mts → protocol-CuBEsqgE.d.mts} +2 -2
- package/dist/{protocol-DjsJHTlK.d.mts.map → protocol-CuBEsqgE.d.mts.map} +1 -1
- package/dist/{public-url-CWoFhQT8.mjs → public-url-DRz_AinV.mjs} +1 -1
- package/dist/{public-url-CWoFhQT8.mjs.map → public-url-DRz_AinV.mjs.map} +1 -1
- package/dist/{query-zTVAR1Sl.mjs → query-D18vTdha.mjs} +14 -14
- package/dist/{query-zTVAR1Sl.mjs.map → query-D18vTdha.mjs.map} +1 -1
- package/dist/{query-BWs7O5Il.mjs → query-tZDEdtCN.mjs} +4 -4
- package/dist/{query-BWs7O5Il.mjs.map → query-tZDEdtCN.mjs.map} +1 -1
- package/dist/{rate-limit-Bx4g9m3p.mjs → rate-limit-on7KPo-v.mjs} +2 -2
- package/dist/{rate-limit-Bx4g9m3p.mjs.map → rate-limit-on7KPo-v.mjs.map} +1 -1
- package/dist/{redirect-COsS6SWJ.mjs → redirect-CZkbozu9.mjs} +2 -2
- package/dist/{redirect-COsS6SWJ.mjs.map → redirect-CZkbozu9.mjs.map} +1 -1
- package/dist/{redirect-BgAOGHDq.mjs → redirect-DrbEmU9a.mjs} +1 -1
- package/dist/{redirect-BgAOGHDq.mjs.map → redirect-DrbEmU9a.mjs.map} +1 -1
- package/dist/{redirects-DyChwDkG.mjs → redirects-BahWiu8Z.mjs} +5 -5
- package/dist/{redirects-DyChwDkG.mjs.map → redirects-BahWiu8Z.mjs.map} +1 -1
- package/dist/{redirects-DTDHWH31.mjs → redirects-CRIM55KH.mjs} +6 -6
- package/dist/{redirects-DTDHWH31.mjs.map → redirects-CRIM55KH.mjs.map} +1 -1
- package/dist/{api-BeBglVxU.mjs → registry-BiO5WwPq.mjs} +483 -2462
- package/dist/registry-BiO5WwPq.mjs.map +1 -0
- package/dist/{registry-Cqa2I749.mjs → registry-Cx-jEJOS.mjs} +9 -123
- package/dist/registry-Cx-jEJOS.mjs.map +1 -0
- package/dist/{relation-BnHB0T4J.mjs → relation-A6wLF_DS.mjs} +2 -2
- package/dist/{relation-BnHB0T4J.mjs.map → relation-A6wLF_DS.mjs.map} +1 -1
- package/dist/{relations-BBApI4e2.mjs → relations-BTPM2saV.mjs} +2 -2
- package/dist/{relations-BBApI4e2.mjs.map → relations-BTPM2saV.mjs.map} +1 -1
- package/dist/{relations-BHo5A8Zf.d.mts → relations-C7B46bcU.d.mts} +192 -33
- package/dist/relations-C7B46bcU.d.mts.map +1 -0
- package/dist/{relations-D5sM5fW0.mjs → relations-Cj5hZ6Zg.mjs} +4 -4
- package/dist/{relations-D5sM5fW0.mjs.map → relations-Cj5hZ6Zg.mjs.map} +1 -1
- package/dist/{request-cache-BSUptuJR.mjs → request-cache-DH9zdCOD.mjs} +1 -1
- package/dist/{request-cache-BSUptuJR.mjs.map → request-cache-DH9zdCOD.mjs.map} +1 -1
- package/dist/request-context.d.mts +1 -1
- package/dist/{request-meta-DgZJh3zG.mjs → request-meta-DRp5ENd_.mjs} +2 -2
- package/dist/{request-meta-DgZJh3zG.mjs.map → request-meta-DRp5ENd_.mjs.map} +1 -1
- package/dist/{resolve-DOhVEvty.mjs → resolve-DxUpTMmE.mjs} +1 -1
- package/dist/{resolve-DOhVEvty.mjs.map → resolve-DxUpTMmE.mjs.map} +1 -1
- package/dist/{runner-C1T11CfC.mjs → runner-77GOZCnH.mjs} +410 -286
- package/dist/runner-77GOZCnH.mjs.map +1 -0
- package/dist/{runner-BYHZn3Jy.d.mts → runner-zgqA_CqP.d.mts} +2 -2
- package/dist/{runner-BYHZn3Jy.d.mts.map → runner-zgqA_CqP.d.mts.map} +1 -1
- package/dist/runtime.d.mts +11 -11
- package/dist/runtime.mjs +1 -1
- package/dist/{schema-DZ5YVVZj.mjs → schema-BY-oVntF.mjs} +3 -3
- package/dist/{schema-DZ5YVVZj.mjs.map → schema-BY-oVntF.mjs.map} +1 -1
- package/dist/{schema-2AOsSn2v.mjs → schema-Cw9kVK0I.mjs} +35 -5
- package/dist/{schema-2AOsSn2v.mjs.map → schema-Cw9kVK0I.mjs.map} +1 -1
- package/dist/{scopes-8ClPF2bC.mjs → scopes-vMR1uIbt.mjs} +3 -3
- package/dist/{scopes-8ClPF2bC.mjs.map → scopes-vMR1uIbt.mjs.map} +1 -1
- package/dist/{search-u1QYdOCG.mjs → search-CfT3KxO5.mjs} +4 -4
- package/dist/{search-u1QYdOCG.mjs.map → search-CfT3KxO5.mjs.map} +1 -1
- package/dist/{secrets-BEgsAQEU.mjs → secrets-BH2Qow1-.mjs} +3 -3
- package/dist/{secrets-BEgsAQEU.mjs.map → secrets-BH2Qow1-.mjs.map} +1 -1
- package/dist/{sections-DmHviGyM.mjs → sections-DTfjyf1z.mjs} +2 -2
- package/dist/{sections-DmHviGyM.mjs.map → sections-DTfjyf1z.mjs.map} +1 -1
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +24 -22
- package/dist/seo/index.d.mts +3 -3
- package/dist/seo/index.mjs +16 -14
- package/dist/seo/index.mjs.map +1 -1
- package/dist/{seo-AFpVvTqr.mjs → seo-BSiTIXzd.mjs} +1 -1
- package/dist/{seo-AFpVvTqr.mjs.map → seo-BSiTIXzd.mjs.map} +1 -1
- package/dist/{seo-Dq4G-RrO.mjs → seo-Cm-dtvgk.mjs} +2 -2
- package/dist/{seo-Dq4G-RrO.mjs.map → seo-Cm-dtvgk.mjs.map} +1 -1
- package/dist/{service-BpArayPr.mjs → service-DYsDHEKe.mjs} +3 -3
- package/dist/{service-BpArayPr.mjs.map → service-DYsDHEKe.mjs.map} +1 -1
- package/dist/{session-user-BEorKVT-.mjs → session-user-CNAfvsdw.mjs} +1 -1
- package/dist/{session-user-BEorKVT-.mjs.map → session-user-CNAfvsdw.mjs.map} +1 -1
- package/dist/{settings-xRdoUZr0.mjs → settings-BgN-0DT-.mjs} +6 -6
- package/dist/{settings-xRdoUZr0.mjs.map → settings-BgN-0DT-.mjs.map} +1 -1
- package/dist/{settings-DE9xtzqd.mjs → settings-DjtkUo__.mjs} +3 -3
- package/dist/{settings-DE9xtzqd.mjs.map → settings-DjtkUo__.mjs.map} +1 -1
- package/dist/{setup-complete-BnT8pq_C.mjs → setup-complete-CcpXaPU7.mjs} +2 -2
- package/dist/{setup-complete-BnT8pq_C.mjs.map → setup-complete-CcpXaPU7.mjs.map} +1 -1
- package/dist/{setup-nonce-CBitf_oQ.mjs → setup-nonce-Reyw30jg.mjs} +1 -1
- package/dist/{setup-nonce-CBitf_oQ.mjs.map → setup-nonce-Reyw30jg.mjs.map} +1 -1
- package/dist/{single-flight-cache-B0eLKS6K.mjs → single-flight-cache-BPuGPdcN.mjs} +2 -2
- package/dist/{single-flight-cache-B0eLKS6K.mjs.map → single-flight-cache-BPuGPdcN.mjs.map} +1 -1
- package/dist/{site-url-DjpIXMXJ.mjs → site-url-Cs_V9n3V.mjs} +3 -3
- package/dist/{site-url-DjpIXMXJ.mjs.map → site-url-Cs_V9n3V.mjs.map} +1 -1
- package/dist/{snapshot-BBlGkb2q.mjs → snapshot-DSB8P7Lm.mjs} +1 -1
- package/dist/{snapshot-BBlGkb2q.mjs.map → snapshot-DSB8P7Lm.mjs.map} +1 -1
- package/dist/{ssrf-Cpvg_n3A.mjs → ssrf-DqebVE34.mjs} +21 -5
- package/dist/{ssrf-Cpvg_n3A.mjs.map → ssrf-DqebVE34.mjs.map} +1 -1
- package/dist/{state-DYwPue40.mjs → state-BJfU9Uzw.mjs} +12 -4
- package/dist/state-BJfU9Uzw.mjs.map +1 -0
- package/dist/{status-CWVYpbD8.mjs → status-DDu8HIQl.mjs} +1 -1
- package/dist/{status-CWVYpbD8.mjs.map → status-DDu8HIQl.mjs.map} +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/{target-DixqqV5i.mjs → target-D0o26Azo.mjs} +1 -1
- package/dist/{target-DixqqV5i.mjs.map → target-D0o26Azo.mjs.map} +1 -1
- package/dist/{taxonomies-OHUIiIpI.mjs → taxonomies-CPGxo-bn.mjs} +5 -5
- package/dist/{taxonomies-OHUIiIpI.mjs.map → taxonomies-CPGxo-bn.mjs.map} +1 -1
- package/dist/{taxonomies-BlrncHi9.mjs → taxonomies-kmyudNGP.mjs} +7 -7
- package/dist/{taxonomies-BlrncHi9.mjs.map → taxonomies-kmyudNGP.mjs.map} +1 -1
- package/dist/{taxonomy-BqCZ1TdU.mjs → taxonomy-8X09dLN5.mjs} +4 -4
- package/dist/{taxonomy-BqCZ1TdU.mjs.map → taxonomy-8X09dLN5.mjs.map} +1 -1
- package/dist/{tokens-C39klX8R.mjs → tokens-gTS-IVUp.mjs} +1 -1
- package/dist/{tokens-C39klX8R.mjs.map → tokens-gTS-IVUp.mjs.map} +1 -1
- package/dist/{transaction-NQj4VJ7Z.mjs → transaction-0KOpOyMu.mjs} +2 -1
- package/dist/{transaction-NQj4VJ7Z.mjs.map → transaction-0KOpOyMu.mjs.map} +1 -1
- package/dist/{transport-RQE36APG.d.mts → transport-_6R1ub7t.d.mts} +1 -1
- package/dist/{transport-RQE36APG.d.mts.map → transport-_6R1ub7t.d.mts.map} +1 -1
- package/dist/{transport-DBJwa-Si.mjs → transport-fX59xaJz.mjs} +4 -3
- package/dist/{transport-DBJwa-Si.mjs.map → transport-fX59xaJz.mjs.map} +1 -1
- package/dist/{trusted-proxy-CEVekpby.mjs → trusted-proxy-fe6nEgR4.mjs} +1 -1
- package/dist/{trusted-proxy-CEVekpby.mjs.map → trusted-proxy-fe6nEgR4.mjs.map} +1 -1
- package/dist/types-75KIqzL9.mjs.map +1 -1
- package/dist/{types-BW1syrlw.mjs → types-BXUmsINL.mjs} +1 -1
- package/dist/{types-BW1syrlw.mjs.map → types-BXUmsINL.mjs.map} +1 -1
- package/dist/{types-BntycPTF.d.mts → types-Blb-MRC3.d.mts} +1 -1
- package/dist/{types-BntycPTF.d.mts.map → types-Blb-MRC3.d.mts.map} +1 -1
- package/dist/{types-C2pMvO62.d.mts → types-Br-ERaxh.d.mts} +2 -1
- package/dist/types-Br-ERaxh.d.mts.map +1 -0
- package/dist/{types-DWRhk9xZ.d.mts → types-BzBd3PeX.d.mts} +1 -1
- package/dist/{types-DWRhk9xZ.d.mts.map → types-BzBd3PeX.d.mts.map} +1 -1
- package/dist/{types-CXnOzDe1.d.mts → types-CLqvnd05.d.mts} +1 -1
- package/dist/{types-CXnOzDe1.d.mts.map → types-CLqvnd05.d.mts.map} +1 -1
- package/dist/{types-Dfk8KI5R.d.mts → types-D5VVLr7J.d.mts} +1 -1
- package/dist/{types-Dfk8KI5R.d.mts.map → types-D5VVLr7J.d.mts.map} +1 -1
- package/dist/{types-DQIDRKF2.d.mts → types-DHxzBjbD.d.mts} +10 -1
- package/dist/types-DHxzBjbD.d.mts.map +1 -0
- package/dist/{types-DN64_zzZ.d.mts → types-DJQxln7q.d.mts} +3 -3
- package/dist/{types-DN64_zzZ.d.mts.map → types-DJQxln7q.d.mts.map} +1 -1
- package/dist/{types-CtzytVXn.mjs → types-DM3vioBY.mjs} +1 -1
- package/dist/{types-CtzytVXn.mjs.map → types-DM3vioBY.mjs.map} +1 -1
- package/dist/{types-BcqdcT-r.d.mts → types-D_DWe7k9.d.mts} +1 -1
- package/dist/{types-BcqdcT-r.d.mts.map → types-D_DWe7k9.d.mts.map} +1 -1
- package/dist/{types-DrJrVCT1.d.mts → types-Vg6f5i3s.d.mts} +3 -3
- package/dist/{types-DrJrVCT1.d.mts.map → types-Vg6f5i3s.d.mts.map} +1 -1
- package/dist/{upload-attempts-D4FS8LT6.mjs → upload-attempts-CNyGSepg.mjs} +1 -1
- package/dist/{upload-attempts-D4FS8LT6.mjs.map → upload-attempts-CNyGSepg.mjs.map} +1 -1
- package/dist/{url-pattern-BHS0V5Ng.mjs → url-pattern-K-KdXcSe.mjs} +1 -1
- package/dist/{url-pattern-BHS0V5Ng.mjs.map → url-pattern-K-KdXcSe.mjs.map} +1 -1
- package/dist/{user-DJZfUJVE.mjs → user-CtYY0tp2.mjs} +2 -2
- package/dist/{user-DJZfUJVE.mjs.map → user-CtYY0tp2.mjs.map} +1 -1
- package/dist/{utils-D4vD_Y98.mjs → utils-HztC73hq.mjs} +1 -1
- package/dist/{utils-D4vD_Y98.mjs.map → utils-HztC73hq.mjs.map} +1 -1
- package/dist/{validate-BMLBvxOC.mjs → validate-Dd-IUjnZ.mjs} +2 -2
- package/dist/{validate-BMLBvxOC.mjs.map → validate-Dd-IUjnZ.mjs.map} +1 -1
- package/dist/{validate-CTqt5nuE.d.mts → validate-DlqdOZXB.d.mts} +5 -5
- package/dist/{validate-CTqt5nuE.d.mts.map → validate-DlqdOZXB.d.mts.map} +1 -1
- package/dist/{validation-npvENUCB.mjs → validation-D83n7Npi.mjs} +10 -8
- package/dist/{validation-npvENUCB.mjs.map → validation-D83n7Npi.mjs.map} +1 -1
- package/dist/version-ri-sCyAp.mjs +7 -0
- package/dist/{version-DB_fqCSI.mjs.map → version-ri-sCyAp.mjs.map} +1 -1
- package/dist/{widgets-BD3ho7Rq.mjs → widgets-CpmZd9Q-.mjs} +4 -4
- package/dist/{widgets-BD3ho7Rq.mjs.map → widgets-CpmZd9Q-.mjs.map} +1 -1
- package/dist/{wxr-taxonomies-Ca68766D.d.mts → wxr-taxonomies-BFd0y72_.d.mts} +1 -1
- package/dist/{wxr-taxonomies-Ca68766D.d.mts.map → wxr-taxonomies-BFd0y72_.d.mts.map} +1 -1
- package/dist/{wxr-taxonomies-B1R3vBoD.mjs → wxr-taxonomies-Df4vxWcU.mjs} +4 -4
- package/dist/{wxr-taxonomies-B1R3vBoD.mjs.map → wxr-taxonomies-Df4vxWcU.mjs.map} +1 -1
- package/dist/{zod-generator-LC-on_Vb.mjs → zod-generator-DylzusHh.mjs} +24 -18
- package/dist/zod-generator-DylzusHh.mjs.map +1 -0
- package/package.json +16 -8
- package/src/api/errors.ts +20 -0
- package/src/api/handlers/index.ts +17 -0
- package/src/api/handlers/media-allowlist.ts +1 -0
- package/src/api/handlers/media-folders.ts +130 -0
- package/src/api/handlers/media-usage-activation.ts +102 -0
- package/src/api/handlers/media-usage.ts +108 -0
- package/src/api/handlers/media.ts +62 -2
- package/src/api/handlers/registry.ts +134 -316
- package/src/api/openapi/document.ts +192 -2
- package/src/api/schemas/media-usage.ts +77 -0
- package/src/api/schemas/media.ts +69 -0
- package/src/astro/integration/routes.ts +18 -0
- package/src/astro/integration/vite-config.ts +2 -7
- package/src/astro/middleware/auth.ts +4 -0
- package/src/astro/middleware.ts +0 -7
- package/src/astro/routes/api/admin/media-usage/activation.ts +42 -0
- package/src/astro/routes/api/admin/media-usage/progress.ts +37 -0
- package/src/astro/routes/api/admin/plugins/registry/artifact.ts +246 -96
- package/src/astro/routes/api/auth/me.ts +14 -2
- package/src/astro/routes/api/media/[id].ts +3 -0
- package/src/astro/routes/api/media/folders/[id].ts +55 -0
- package/src/astro/routes/api/media/folders/index.ts +37 -0
- package/src/astro/routes/api/media.ts +12 -2
- package/src/astro/routes/api/setup/dev-bypass.ts +12 -0
- package/src/astro/types.ts +11 -1
- package/src/client/index.ts +140 -2
- package/src/client/transport.ts +6 -2
- package/src/components/EmDashImage.astro +154 -50
- package/src/components/Gallery.astro +7 -0
- package/src/components/InlinePortableTextEditor.tsx +130 -1
- package/src/components/LiveSearch.astro +23 -0
- package/src/components/inline-code-block.tsx +180 -49
- package/src/content/converters/gallery.ts +2 -0
- package/src/content/converters/types.ts +2 -0
- package/src/database/connection.ts +2 -31
- package/src/database/index.ts +1 -1
- package/src/database/json-recordset.ts +16 -0
- package/src/database/migrations/019_i18n.ts +1 -1
- package/src/database/migrations/071_restore_content_bylines_table.ts +43 -0
- package/src/database/migrations/072_media_folders.ts +72 -0
- package/src/database/migrations/073_media_focal_point.ts +57 -0
- package/src/database/migrations/runner.ts +8 -2
- package/src/database/repositories/content.ts +5 -4
- package/src/database/repositories/media-folders.ts +115 -0
- package/src/database/repositories/media-usage-work.ts +395 -111
- package/src/database/repositories/media-usage.ts +1038 -10
- package/src/database/repositories/media.ts +97 -29
- package/src/database/transaction.ts +1 -0
- package/src/database/types.ts +13 -0
- package/src/db/adapters.ts +2 -2
- package/src/db/node-sqlite-compat.ts +78 -0
- package/src/db/sqlite.ts +3 -3
- package/src/emdash-runtime.ts +110 -105
- package/src/fields/image.ts +7 -1
- package/src/fields/types.ts +6 -2
- package/src/index.ts +5 -0
- package/src/media/focal-point.ts +37 -0
- package/src/media/local-runtime.ts +8 -0
- package/src/media/mime.ts +1 -0
- package/src/media/normalize.ts +10 -0
- package/src/media/types.ts +10 -0
- package/src/media/usage/activation.ts +162 -35
- package/src/media/usage/capture-triggers.ts +19 -3
- package/src/media/usage/collection-deletion-processor.ts +7 -3
- package/src/media/usage/collection-deletion.ts +10 -0
- package/src/media/usage/content-refresh.ts +258 -39
- package/src/media/usage/content-snapshots.ts +128 -31
- package/src/media/usage/extractor.ts +32 -13
- package/src/media/usage/maintenance-engine.ts +166 -0
- package/src/media/usage/reconciliation-processor.ts +106 -29
- package/src/media/usage/reconciliation.ts +90 -7
- package/src/media/usage/work-processor.ts +342 -101
- package/src/plugins/index.ts +1 -0
- package/src/plugins/save-rejection.ts +20 -0
- package/src/plugins/scheduler/node.ts +1 -13
- package/src/plugins/scheduler/types.ts +0 -2
- package/src/plugins/state.ts +13 -3
- package/src/registry/artifact-fetch.ts +530 -0
- package/src/registry/config.ts +24 -6
- package/src/registry/types.ts +12 -15
- package/src/schema/types.ts +1 -0
- package/src/schema/zod-generator.ts +12 -4
- package/src/security/ssrf.ts +21 -2
- package/src/seed/apply.ts +4 -4
- package/src/ui.ts +2 -0
- package/dist/api-BeBglVxU.mjs.map +0 -1
- package/dist/collection-deletion-DQPvMD8R.mjs.map +0 -1
- package/dist/content-ZHCc1ngN.mjs.map +0 -1
- package/dist/content-refresh-B_8SJzuU.mjs.map +0 -1
- package/dist/errors-CLUPm7aD.mjs.map +0 -1
- package/dist/index-BVBZj4-7.d.mts.map +0 -1
- package/dist/media-BWWWb8kg.mjs.map +0 -1
- package/dist/media-CzhAn2--.mjs.map +0 -1
- package/dist/media-allowlist-D6x-Us02.mjs.map +0 -1
- package/dist/media-usage-BZCydfvu.mjs.map +0 -1
- package/dist/media-usage-work-DrCbqBcl.mjs.map +0 -1
- package/dist/menus-DSo5QeW0.mjs.map +0 -1
- package/dist/mime-DfIVjNkr.mjs.map +0 -1
- package/dist/normalize-Bc-s_r-a.mjs.map +0 -1
- package/dist/placeholder-Dp0K3RkD.d.mts.map +0 -1
- package/dist/registry-Cqa2I749.mjs.map +0 -1
- package/dist/relations-BHo5A8Zf.d.mts.map +0 -1
- package/dist/runner-C1T11CfC.mjs.map +0 -1
- package/dist/state-DYwPue40.mjs.map +0 -1
- package/dist/types-C2pMvO62.d.mts.map +0 -1
- package/dist/types-DQIDRKF2.d.mts.map +0 -1
- package/dist/version-DB_fqCSI.mjs +0 -7
- package/dist/zod-generator-LC-on_Vb.mjs.map +0 -1
- /package/dist/{api-tokens-rIXC6LdE.mjs → api-tokens-B5NHLNUN.mjs} +0 -0
- /package/dist/{ssrf-DpXgKsna.mjs → ssrf-BPKzkm6O.mjs} +0 -0
- /package/dist/{types-CP1CflWf.mjs → types-D6OcIp2P.mjs} +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { o as decodeCursor, s as encodeCursor } from "./types-CQPWhPxE.mjs";
|
|
2
|
+
import { n as normalizeFocalPoint } from "./focal-point-BeWFo6Xx.mjs";
|
|
2
3
|
import { sql } from "kysely";
|
|
3
4
|
import { ulid } from "ulidx";
|
|
4
5
|
|
|
@@ -45,6 +46,8 @@ var MediaRepository = class {
|
|
|
45
46
|
size: input.size ?? null,
|
|
46
47
|
width: input.width ?? null,
|
|
47
48
|
height: input.height ?? null,
|
|
49
|
+
focal_x: null,
|
|
50
|
+
focal_y: null,
|
|
48
51
|
alt: input.alt ?? null,
|
|
49
52
|
caption: input.caption ?? null,
|
|
50
53
|
storage_key: input.storageKey,
|
|
@@ -53,7 +56,8 @@ var MediaRepository = class {
|
|
|
53
56
|
dominant_color: input.dominantColor ?? null,
|
|
54
57
|
status: input.status ?? "ready",
|
|
55
58
|
created_at: now,
|
|
56
|
-
author_id: input.authorId ?? null
|
|
59
|
+
author_id: input.authorId ?? null,
|
|
60
|
+
folder_id: null
|
|
57
61
|
};
|
|
58
62
|
await this.db.insertInto("media").values(row).execute();
|
|
59
63
|
return this.rowToItem(row);
|
|
@@ -161,19 +165,11 @@ var MediaRepository = class {
|
|
|
161
165
|
*/
|
|
162
166
|
async findMany(options = {}) {
|
|
163
167
|
const limit = Math.min(options.limit || 50, 100);
|
|
164
|
-
let query = this.db.selectFrom("media").selectAll().orderBy("created_at", "desc").orderBy("id", "desc").limit(limit + 1);
|
|
168
|
+
let query = this.applyListFilters(this.db.selectFrom("media"), options).selectAll().orderBy("created_at", "desc").orderBy("id", "desc").limit(limit + 1);
|
|
165
169
|
if (options.cursor) {
|
|
166
170
|
const { orderValue: createdAt, id: cursorId } = decodeCursor(options.cursor);
|
|
167
171
|
query = query.where((eb) => eb.or([eb("created_at", "<", createdAt), eb.and([eb("created_at", "=", createdAt), eb("id", "<", cursorId)])]));
|
|
168
172
|
}
|
|
169
|
-
const mimeFilters = normalizeMimeFilter(options.mimeType);
|
|
170
|
-
if (mimeFilters.length > 0) query = query.where((eb) => mimeMatchExpr(eb, mimeFilters));
|
|
171
|
-
const term = options.q?.trim();
|
|
172
|
-
if (term) {
|
|
173
|
-
const pattern = `%${escapeLike(term)}%`;
|
|
174
|
-
query = query.where(sql`lower(filename)`, "like", sql`lower(${pattern}) escape '\\'`);
|
|
175
|
-
}
|
|
176
|
-
if (options.status !== "all") query = query.where("status", "=", options.status ?? "ready");
|
|
177
173
|
const rows = await query.execute();
|
|
178
174
|
const hasMore = rows.length > limit;
|
|
179
175
|
const items = rows.slice(0, limit).map((row) => this.rowToItem(row));
|
|
@@ -187,6 +183,17 @@ var MediaRepository = class {
|
|
|
187
183
|
nextCursor
|
|
188
184
|
};
|
|
189
185
|
}
|
|
186
|
+
async findPage(options) {
|
|
187
|
+
const limit = Math.min(options.limit || 50, 100);
|
|
188
|
+
const offset = (options.page - 1) * limit;
|
|
189
|
+
const filtered = this.applyListFilters(this.db.selectFrom("media"), options);
|
|
190
|
+
const rows = await filtered.selectAll().orderBy("created_at", "desc").orderBy("id", "desc").limit(limit).offset(offset).execute();
|
|
191
|
+
const count = await filtered.select((eb) => eb.fn.count("id").as("count")).executeTakeFirst();
|
|
192
|
+
return {
|
|
193
|
+
items: rows.map((row) => this.rowToItem(row)),
|
|
194
|
+
totalCount: Number(count?.count ?? 0)
|
|
195
|
+
};
|
|
196
|
+
}
|
|
190
197
|
/**
|
|
191
198
|
* Update media metadata
|
|
192
199
|
*/
|
|
@@ -197,6 +204,11 @@ var MediaRepository = class {
|
|
|
197
204
|
if (input.caption !== void 0) updates.caption = input.caption;
|
|
198
205
|
if (input.width !== void 0) updates.width = input.width;
|
|
199
206
|
if (input.height !== void 0) updates.height = input.height;
|
|
207
|
+
if (input.folderId !== void 0) updates.folder_id = input.folderId;
|
|
208
|
+
if (input.focalX !== void 0 && input.focalY !== void 0) {
|
|
209
|
+
updates.focal_x = input.focalX;
|
|
210
|
+
updates.focal_y = input.focalY;
|
|
211
|
+
}
|
|
200
212
|
if (Object.keys(updates).length > 0) await this.db.updateTable("media").set(updates).where("id", "=", id).execute();
|
|
201
213
|
return this.findById(id);
|
|
202
214
|
}
|
|
@@ -221,6 +233,19 @@ var MediaRepository = class {
|
|
|
221
233
|
const result = await query.executeTakeFirst();
|
|
222
234
|
return Number(result?.count || 0);
|
|
223
235
|
}
|
|
236
|
+
applyListFilters(query, options) {
|
|
237
|
+
const mimeFilters = normalizeMimeFilter(options.mimeType);
|
|
238
|
+
if (mimeFilters.length > 0) query = query.where((eb) => mimeMatchExpr(eb, mimeFilters));
|
|
239
|
+
const term = options.q?.trim();
|
|
240
|
+
if (term) {
|
|
241
|
+
const pattern = `%${escapeLike(term)}%`;
|
|
242
|
+
query = query.where(sql`lower(filename)`, "like", sql`lower(${pattern}) escape '\\'`);
|
|
243
|
+
}
|
|
244
|
+
if (options.status !== "all") query = query.where("status", "=", options.status ?? "ready");
|
|
245
|
+
if (options.folderId === null) query = query.where("folder_id", "is", null);
|
|
246
|
+
else if (options.folderId !== void 0) query = query.where("folder_id", "=", options.folderId);
|
|
247
|
+
return query;
|
|
248
|
+
}
|
|
224
249
|
/**
|
|
225
250
|
* Delete pending uploads older than the given age.
|
|
226
251
|
* Pending uploads that were never confirmed indicate abandoned upload flows.
|
|
@@ -236,6 +261,7 @@ var MediaRepository = class {
|
|
|
236
261
|
* Convert database row to MediaItem
|
|
237
262
|
*/
|
|
238
263
|
rowToItem(row) {
|
|
264
|
+
const focalPoint = normalizeFocalPoint(row.focal_x, row.focal_y);
|
|
239
265
|
return {
|
|
240
266
|
id: row.id,
|
|
241
267
|
filename: row.filename,
|
|
@@ -243,6 +269,8 @@ var MediaRepository = class {
|
|
|
243
269
|
size: row.size,
|
|
244
270
|
width: row.width,
|
|
245
271
|
height: row.height,
|
|
272
|
+
focalX: focalPoint?.focalX ?? null,
|
|
273
|
+
focalY: focalPoint?.focalY ?? null,
|
|
246
274
|
alt: row.alt,
|
|
247
275
|
caption: row.caption,
|
|
248
276
|
storageKey: row.storage_key,
|
|
@@ -251,11 +279,12 @@ var MediaRepository = class {
|
|
|
251
279
|
dominantColor: row.dominant_color,
|
|
252
280
|
status: row.status,
|
|
253
281
|
createdAt: row.created_at,
|
|
254
|
-
authorId: row.author_id
|
|
282
|
+
authorId: row.author_id,
|
|
283
|
+
folderId: row.folder_id
|
|
255
284
|
};
|
|
256
285
|
}
|
|
257
286
|
};
|
|
258
287
|
|
|
259
288
|
//#endregion
|
|
260
289
|
export { MediaRepository as t };
|
|
261
|
-
//# sourceMappingURL=media-
|
|
290
|
+
//# sourceMappingURL=media-D6SeAfBl.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-D6SeAfBl.mjs","names":[],"sources":["../src/database/repositories/media.ts"],"sourcesContent":["import {\n\tsql,\n\ttype ExpressionBuilder,\n\ttype Kysely,\n\ttype SelectQueryBuilder,\n\ttype SqlBool,\n} from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { normalizeFocalPoint, type FocalPointUpdate } from \"../../media/focal-point.js\";\nimport type { Database, MediaRow } from \"../types.js\";\nimport type { FindManyResult } from \"./types.js\";\nimport { encodeCursor, decodeCursor } from \"./types.js\";\n\n/** Escape LIKE wildcard characters and the escape char itself in user-supplied values */\nfunction escapeLike(value: string): string {\n\treturn value.replaceAll(\"\\\\\", \"\\\\\\\\\").replaceAll(\"%\", \"\\\\%\").replaceAll(\"_\", \"\\\\_\");\n}\n\n/**\n * Normalize a mimeType filter (string or array) into a clean string[].\n * Entries that are empty strings are dropped.\n */\nfunction normalizeMimeFilter(input?: string | readonly string[]): string[] {\n\tif (!input) return [];\n\tconst arr = Array.isArray(input) ? input : [input];\n\treturn arr\n\t\t.filter((entry): entry is string => typeof entry === \"string\" && entry.length > 0)\n\t\t.map((entry) =>\n\t\t\tentry.endsWith(\"/\") ? entry.toLowerCase() : entry.split(\";\")[0].trim().toLowerCase(),\n\t\t);\n}\n\n/**\n * Build a WHERE clause that matches `mime_type` against any of the given\n * filter entries — exact equality for full MIMEs, LIKE prefix for entries\n * ending in \"/\".\n */\nfunction mimeMatchExpr(eb: ExpressionBuilder<Database, \"media\">, filters: string[]) {\n\treturn eb.or(\n\t\tfilters.map((entry) =>\n\t\t\tentry.endsWith(\"/\")\n\t\t\t\t? sql<SqlBool>`mime_type LIKE ${`${escapeLike(entry)}%`} ESCAPE '\\\\'`\n\t\t\t\t: eb(\"mime_type\", \"=\", entry),\n\t\t),\n\t);\n}\n\nexport type MediaStatus = \"pending\" | \"ready\" | \"failed\";\n\nexport interface MediaItem {\n\tid: string;\n\tfilename: string;\n\tmimeType: string;\n\tsize: number | null;\n\twidth: number | null;\n\theight: number | null;\n\tfocalX: number | null;\n\tfocalY: number | null;\n\talt: string | null;\n\tcaption: string | null;\n\tstorageKey: string;\n\tstatus: MediaStatus;\n\tcontentHash: string | null;\n\tblurhash: string | null;\n\tdominantColor: string | null;\n\tcreatedAt: string;\n\tauthorId: string | null;\n\tfolderId?: string | null;\n}\n\nexport interface CreateMediaInput {\n\tfilename: string;\n\tmimeType: string;\n\tsize?: number;\n\twidth?: number;\n\theight?: number;\n\talt?: string;\n\tcaption?: string;\n\tstorageKey: string;\n\tcontentHash?: string;\n\tblurhash?: string;\n\tdominantColor?: string;\n\tstatus?: MediaStatus;\n\tauthorId?: string;\n}\n\nexport interface FindManyMediaOptions {\n\tlimit?: number;\n\tcursor?: string;\n\t/** Filter by MIME type. Pass a string for a single prefix/exact, or an array to match any. Strings ending with \"/\" are treated as LIKE prefix matches; others are exact equality. */\n\tmimeType?: string | readonly string[];\n\tstatus?: MediaStatus | \"all\"; // Filter by status, defaults to \"ready\"\n\t/** Case-insensitive substring matched against the filename (covers filename and extension). */\n\tq?: string;\n\t/** Omit for all media, pass null for the Main library, or pass a folder ID. */\n\tfolderId?: string | null;\n}\n\nexport interface UpdateMediaInput extends FocalPointUpdate {\n\talt?: string;\n\tcaption?: string;\n\twidth?: number;\n\theight?: number;\n\tfolderId?: string | null;\n}\n\nexport interface FindMediaPageOptions extends Omit<FindManyMediaOptions, \"cursor\"> {\n\tpage: number;\n}\n\nexport interface MediaPageResult {\n\titems: MediaItem[];\n\ttotalCount: number;\n}\n\nconst UPLOAD_ATTEMPT_CLEANUP_AGE_MS = 60 * 60 * 1000;\nconst UPLOAD_ATTEMPT_CLEANUP_BATCH_SIZE = 100;\n\n/**\n * Media repository for database operations\n */\nexport class MediaRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Create a new media item\n\t */\n\tasync create(input: CreateMediaInput): Promise<MediaItem> {\n\t\tconst id = ulid();\n\t\tconst now = new Date().toISOString();\n\n\t\tconst row: Omit<MediaRow, \"rowid\"> = {\n\t\t\tid,\n\t\t\tfilename: input.filename,\n\t\t\tmime_type: input.mimeType,\n\t\t\tsize: input.size ?? null,\n\t\t\twidth: input.width ?? null,\n\t\t\theight: input.height ?? null,\n\t\t\tfocal_x: null,\n\t\t\tfocal_y: null,\n\t\t\talt: input.alt ?? null,\n\t\t\tcaption: input.caption ?? null,\n\t\t\tstorage_key: input.storageKey,\n\t\t\tcontent_hash: input.contentHash ?? null,\n\t\t\tblurhash: input.blurhash ?? null,\n\t\t\tdominant_color: input.dominantColor ?? null,\n\t\t\tstatus: input.status ?? \"ready\",\n\t\t\tcreated_at: now,\n\t\t\tauthor_id: input.authorId ?? null,\n\t\t\tfolder_id: null,\n\t\t};\n\n\t\tawait this.db.insertInto(\"media\").values(row).execute();\n\n\t\treturn this.rowToItem(row);\n\t}\n\n\t/**\n\t * Create a pending media item (for signed URL upload flow)\n\t */\n\tasync createPending(input: {\n\t\tfilename: string;\n\t\tmimeType: string;\n\t\tsize?: number;\n\t\tstorageKey: string;\n\t\tcontentHash?: string;\n\t\tauthorId?: string;\n\t}): Promise<MediaItem> {\n\t\treturn this.create({\n\t\t\t...input,\n\t\t\tstatus: \"pending\",\n\t\t});\n\t}\n\n\tasync createUploadAttempt(mediaId: string, storageKey: string): Promise<void> {\n\t\tconst now = new Date().toISOString();\n\t\tawait this.db\n\t\t\t.insertInto(\"_emdash_media_upload_attempts\")\n\t\t\t.values({\n\t\t\t\tmedia_id: mediaId,\n\t\t\t\tstorage_key: storageKey,\n\t\t\t\tstatus: \"active\",\n\t\t\t\tcreated_at: now,\n\t\t\t\tupdated_at: now,\n\t\t\t})\n\t\t\t.execute();\n\t}\n\n\tasync hasUploadAttempt(storageKey: string): Promise<boolean> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_media_upload_attempts\")\n\t\t\t.select(\"storage_key\")\n\t\t\t.where(\"storage_key\", \"=\", storageKey)\n\t\t\t.executeTakeFirst();\n\t\treturn row !== undefined;\n\t}\n\n\tasync claimUploadAttemptForCleanup(storageKey: string): Promise<boolean> {\n\t\tconst result = await this.db\n\t\t\t.updateTable(\"_emdash_media_upload_attempts\")\n\t\t\t.set({ status: \"cleanup\", updated_at: new Date().toISOString() })\n\t\t\t.where(\"storage_key\", \"=\", storageKey)\n\t\t\t.where((eb) =>\n\t\t\t\teb.not(\n\t\t\t\t\teb.exists(\n\t\t\t\t\t\teb\n\t\t\t\t\t\t\t.selectFrom(\"media\")\n\t\t\t\t\t\t\t.select(\"media.id\")\n\t\t\t\t\t\t\t.whereRef(\"media.storage_key\", \"=\", \"_emdash_media_upload_attempts.storage_key\"),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t)\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result.numUpdatedRows ?? 0) > 0;\n\t}\n\n\tasync deleteUploadAttempt(storageKey: string): Promise<void> {\n\t\tawait this.db\n\t\t\t.deleteFrom(\"_emdash_media_upload_attempts\")\n\t\t\t.where(\"storage_key\", \"=\", storageKey)\n\t\t\t.execute();\n\t}\n\n\tasync deleteCompletedUploadAttempts(): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_media_upload_attempts\")\n\t\t\t.where((eb) =>\n\t\t\t\teb.exists(\n\t\t\t\t\teb\n\t\t\t\t\t\t.selectFrom(\"media\")\n\t\t\t\t\t\t.select(\"media.id\")\n\t\t\t\t\t\t.whereRef(\"media.id\", \"=\", \"_emdash_media_upload_attempts.media_id\")\n\t\t\t\t\t\t.whereRef(\"media.storage_key\", \"=\", \"_emdash_media_upload_attempts.storage_key\")\n\t\t\t\t\t\t.where(\"media.status\", \"=\", \"ready\"),\n\t\t\t\t),\n\t\t\t)\n\t\t\t.executeTakeFirst();\n\t\treturn Number(result.numDeletedRows ?? 0);\n\t}\n\n\tasync findUploadAttemptsForCleanup(\n\t\tmaxAgeMs: number = UPLOAD_ATTEMPT_CLEANUP_AGE_MS,\n\t\tlimit: number = UPLOAD_ATTEMPT_CLEANUP_BATCH_SIZE,\n\t): Promise<string[]> {\n\t\tconst cutoff = new Date(Date.now() - maxAgeMs).toISOString();\n\t\tconst rows = await this.db\n\t\t\t.selectFrom(\"_emdash_media_upload_attempts\")\n\t\t\t.select(\"storage_key\")\n\t\t\t.where((eb) => eb.or([eb(\"status\", \"=\", \"cleanup\"), eb(\"created_at\", \"<\", cutoff)]))\n\t\t\t.where((eb) =>\n\t\t\t\teb.not(\n\t\t\t\t\teb.exists(\n\t\t\t\t\t\teb\n\t\t\t\t\t\t\t.selectFrom(\"media\")\n\t\t\t\t\t\t\t.select(\"media.id\")\n\t\t\t\t\t\t\t.whereRef(\"media.storage_key\", \"=\", \"_emdash_media_upload_attempts.storage_key\"),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t)\n\t\t\t.orderBy(\"created_at\", \"asc\")\n\t\t\t.limit(limit)\n\t\t\t.execute();\n\n\t\treturn rows.map((row) => row.storage_key);\n\t}\n\n\tasync publishPendingStorageKey(\n\t\tid: string,\n\t\texpectedStorageKey: string,\n\t\tstorageKey: string,\n\t\tcontentHash?: string,\n\t): Promise<boolean> {\n\t\tconst result = await this.db\n\t\t\t.updateTable(\"media\")\n\t\t\t.set({\n\t\t\t\tstorage_key: storageKey,\n\t\t\t\t...(contentHash !== undefined ? { content_hash: contentHash } : {}),\n\t\t\t})\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.where(\"status\", \"=\", \"pending\")\n\t\t\t.where(\"storage_key\", \"=\", expectedStorageKey)\n\t\t\t.where((eb) =>\n\t\t\t\teb.exists(\n\t\t\t\t\teb\n\t\t\t\t\t\t.selectFrom(\"_emdash_media_upload_attempts\")\n\t\t\t\t\t\t.select(\"storage_key\")\n\t\t\t\t\t\t.where(\"media_id\", \"=\", id)\n\t\t\t\t\t\t.where(\"storage_key\", \"=\", storageKey)\n\t\t\t\t\t\t.where(\"status\", \"=\", \"active\"),\n\t\t\t\t),\n\t\t\t)\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result.numUpdatedRows ?? 0) > 0;\n\t}\n\n\t/**\n\t * Confirm upload (mark as ready)\n\t */\n\tasync confirmUpload(\n\t\tid: string,\n\t\tmetadata?: {\n\t\t\twidth?: number;\n\t\t\theight?: number;\n\t\t\tsize?: number;\n\t\t\tblurhash?: string;\n\t\t\tdominantColor?: string;\n\t\t\tcontentHash?: string | null;\n\t\t},\n\t\texpectedStorageKey?: string,\n\t): Promise<MediaItem | null> {\n\t\tconst updates: Partial<MediaRow> = {\n\t\t\tstatus: \"ready\",\n\t\t};\n\t\tif (metadata?.width !== undefined) updates.width = metadata.width;\n\t\tif (metadata?.height !== undefined) updates.height = metadata.height;\n\t\tif (metadata?.size !== undefined) updates.size = metadata.size;\n\t\tif (metadata?.blurhash !== undefined) updates.blurhash = metadata.blurhash;\n\t\tif (metadata?.dominantColor !== undefined) updates.dominant_color = metadata.dominantColor;\n\t\tif (metadata?.contentHash !== undefined) updates.content_hash = metadata.contentHash;\n\n\t\tlet query = this.db\n\t\t\t.updateTable(\"media\")\n\t\t\t.set(updates)\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.where(\"status\", \"=\", \"pending\");\n\t\tif (expectedStorageKey !== undefined) {\n\t\t\tquery = query.where(\"storage_key\", \"=\", expectedStorageKey);\n\t\t}\n\n\t\tconst row = await query.returningAll().executeTakeFirst();\n\n\t\treturn row ? this.rowToItem(row) : null;\n\t}\n\n\t/**\n\t * Mark upload as failed\n\t */\n\tasync markFailed(id: string, expectedStorageKey?: string): Promise<MediaItem | null> {\n\t\tlet query = this.db.updateTable(\"media\").set({ status: \"failed\" }).where(\"id\", \"=\", id);\n\t\tif (expectedStorageKey !== undefined) {\n\t\t\tquery = query.where(\"status\", \"=\", \"pending\").where(\"storage_key\", \"=\", expectedStorageKey);\n\t\t}\n\n\t\tconst row = await query.returningAll().executeTakeFirst();\n\t\treturn row ? this.rowToItem(row) : null;\n\t}\n\n\t/**\n\t * Find media by ID\n\t */\n\tasync findById(id: string): Promise<MediaItem | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"media\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToItem(row) : null;\n\t}\n\n\t/**\n\t * Find media by filename\n\t * Useful for idempotent imports\n\t */\n\tasync findByFilename(filename: string): Promise<MediaItem | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"media\")\n\t\t\t.selectAll()\n\t\t\t.where(\"filename\", \"=\", filename)\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToItem(row) : null;\n\t}\n\n\t/**\n\t * Find media by content hash\n\t * Used for deduplication - same content = same hash\n\t */\n\tasync findByContentHash(contentHash: string): Promise<MediaItem | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"media\")\n\t\t\t.selectAll()\n\t\t\t.where(\"content_hash\", \"=\", contentHash)\n\t\t\t.where(\"status\", \"=\", \"ready\")\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToItem(row) : null;\n\t}\n\n\t/**\n\t * Find many media items with cursor pagination\n\t *\n\t * Uses keyset pagination (cursor-based) for consistent results.\n\t * The cursor encodes the created_at and id of the last item.\n\t */\n\tasync findMany(options: FindManyMediaOptions = {}): Promise<FindManyResult<MediaItem>> {\n\t\tconst limit = Math.min(options.limit || 50, 100);\n\n\t\tlet query = this.applyListFilters(this.db.selectFrom(\"media\"), options)\n\t\t\t.selectAll()\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.orderBy(\"id\", \"desc\")\n\t\t\t.limit(limit + 1);\n\n\t\t// Handle cursor-based pagination — throws on invalid cursor.\n\t\tif (options.cursor) {\n\t\t\tconst { orderValue: createdAt, id: cursorId } = decodeCursor(options.cursor);\n\n\t\t\t// Keyset pagination: get items where (created_at, id) < cursor\n\t\t\tquery = query.where((eb) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb(\"created_at\", \"<\", createdAt),\n\t\t\t\t\teb.and([eb(\"created_at\", \"=\", createdAt), eb(\"id\", \"<\", cursorId)]),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tconst rows = await query.execute();\n\n\t\tconst hasMore = rows.length > limit;\n\t\tconst items = rows.slice(0, limit).map((row) => this.rowToItem(row));\n\n\t\tlet nextCursor: string | undefined;\n\t\tif (hasMore && items.length > 0) {\n\t\t\tconst lastItem = items.at(-1)!;\n\t\t\tnextCursor = encodeCursor(lastItem.createdAt, lastItem.id);\n\t\t}\n\n\t\treturn { items, nextCursor };\n\t}\n\n\tasync findPage(options: FindMediaPageOptions): Promise<MediaPageResult> {\n\t\tconst limit = Math.min(options.limit || 50, 100);\n\t\tconst offset = (options.page - 1) * limit;\n\t\tconst filtered = this.applyListFilters(this.db.selectFrom(\"media\"), options);\n\t\tconst rows = await filtered\n\t\t\t.selectAll()\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.orderBy(\"id\", \"desc\")\n\t\t\t.limit(limit)\n\t\t\t.offset(offset)\n\t\t\t.execute();\n\t\tconst count = await filtered\n\t\t\t.select((eb) => eb.fn.count<number>(\"id\").as(\"count\"))\n\t\t\t.executeTakeFirst();\n\n\t\treturn {\n\t\t\titems: rows.map((row) => this.rowToItem(row)),\n\t\t\ttotalCount: Number(count?.count ?? 0),\n\t\t};\n\t}\n\n\t/**\n\t * Update media metadata\n\t */\n\tasync update(id: string, input: UpdateMediaInput): Promise<MediaItem | null> {\n\t\tconst existing = await this.findById(id);\n\t\tif (!existing) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst updates: Partial<MediaRow> = {};\n\t\tif (input.alt !== undefined) updates.alt = input.alt;\n\t\tif (input.caption !== undefined) updates.caption = input.caption;\n\t\tif (input.width !== undefined) updates.width = input.width;\n\t\tif (input.height !== undefined) updates.height = input.height;\n\t\tif (input.folderId !== undefined) updates.folder_id = input.folderId;\n\t\tif (input.focalX !== undefined && input.focalY !== undefined) {\n\t\t\tupdates.focal_x = input.focalX;\n\t\t\tupdates.focal_y = input.focalY;\n\t\t}\n\n\t\tif (Object.keys(updates).length > 0) {\n\t\t\tawait this.db.updateTable(\"media\").set(updates).where(\"id\", \"=\", id).execute();\n\t\t}\n\n\t\treturn this.findById(id);\n\t}\n\n\t/**\n\t * Delete media item\n\t */\n\tasync deleteWithStorageKey(id: string): Promise<string | null> {\n\t\tconst deleted = await this.db\n\t\t\t.deleteFrom(\"media\")\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.returning(\"storage_key\")\n\t\t\t.executeTakeFirst();\n\t\tif (deleted) return deleted.storage_key;\n\t\treturn null;\n\t}\n\n\tasync delete(id: string): Promise<boolean> {\n\t\treturn (await this.deleteWithStorageKey(id)) !== null;\n\t}\n\n\t/**\n\t * Count media items\n\t */\n\tasync count(mimeType?: string | readonly string[]): Promise<number> {\n\t\tconst filters = normalizeMimeFilter(mimeType);\n\t\tlet query = this.db.selectFrom(\"media\").select((eb) => eb.fn.count<number>(\"id\").as(\"count\"));\n\n\t\tif (filters.length > 0) {\n\t\t\tquery = query.where((eb) => mimeMatchExpr(eb, filters));\n\t\t}\n\n\t\tconst result = await query.executeTakeFirst();\n\t\treturn Number(result?.count || 0);\n\t}\n\n\tprivate applyListFilters<O>(\n\t\tquery: SelectQueryBuilder<Database, \"media\", O>,\n\t\toptions: Omit<FindManyMediaOptions, \"cursor\" | \"limit\">,\n\t): SelectQueryBuilder<Database, \"media\", O> {\n\t\tconst mimeFilters = normalizeMimeFilter(options.mimeType);\n\t\tif (mimeFilters.length > 0) {\n\t\t\tquery = query.where((eb) => mimeMatchExpr(eb, mimeFilters));\n\t\t}\n\n\t\tconst term = options.q?.trim();\n\t\tif (term) {\n\t\t\tconst pattern = `%${escapeLike(term)}%`;\n\t\t\tquery = query.where(\n\t\t\t\tsql<string>`lower(filename)`,\n\t\t\t\t\"like\",\n\t\t\t\tsql<string>`lower(${pattern}) escape '\\\\'`,\n\t\t\t);\n\t\t}\n\n\t\tif (options.status !== \"all\") {\n\t\t\tquery = query.where(\"status\", \"=\", options.status ?? \"ready\");\n\t\t}\n\n\t\tif (options.folderId === null) {\n\t\t\tquery = query.where(\"folder_id\", \"is\", null);\n\t\t} else if (options.folderId !== undefined) {\n\t\t\tquery = query.where(\"folder_id\", \"=\", options.folderId);\n\t\t}\n\n\t\treturn query;\n\t}\n\n\t/**\n\t * Delete pending uploads older than the given age.\n\t * Pending uploads that were never confirmed indicate abandoned upload flows.\n\t *\n\t * Returns the storage keys of deleted rows so callers can remove the\n\t * corresponding files from object storage.\n\t */\n\tasync cleanupPendingUploads(maxAgeMs: number = 60 * 60 * 1000): Promise<string[]> {\n\t\tconst cutoff = new Date(Date.now() - maxAgeMs).toISOString();\n\n\t\tconst rows = await this.db\n\t\t\t.deleteFrom(\"media\")\n\t\t\t.where(\"status\", \"=\", \"pending\")\n\t\t\t.where(\"created_at\", \"<\", cutoff)\n\t\t\t.returning(\"storage_key\")\n\t\t\t.execute();\n\n\t\treturn rows.map((r) => r.storage_key);\n\t}\n\n\t/**\n\t * Convert database row to MediaItem\n\t */\n\tprivate rowToItem(row: MediaRow): MediaItem {\n\t\tconst focalPoint = normalizeFocalPoint(row.focal_x, row.focal_y);\n\t\treturn {\n\t\t\tid: row.id,\n\t\t\tfilename: row.filename,\n\t\t\tmimeType: row.mime_type,\n\t\t\tsize: row.size,\n\t\t\twidth: row.width,\n\t\t\theight: row.height,\n\t\t\tfocalX: focalPoint?.focalX ?? null,\n\t\t\tfocalY: focalPoint?.focalY ?? null,\n\t\t\talt: row.alt,\n\t\t\tcaption: row.caption,\n\t\t\tstorageKey: row.storage_key,\n\t\t\tcontentHash: row.content_hash,\n\t\t\tblurhash: row.blurhash,\n\t\t\tdominantColor: row.dominant_color,\n\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- DB stores string; validated at insert but linter can't follow\n\t\t\tstatus: row.status as MediaStatus,\n\t\t\tcreatedAt: row.created_at,\n\t\t\tauthorId: row.author_id,\n\t\t\tfolderId: row.folder_id,\n\t\t};\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as parseAllowedMimeTypes } from "./mime-
|
|
1
|
+
import { r as parseAllowedMimeTypes } from "./mime-CUIG6x84.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/api/handlers/media-allowlist.ts
|
|
4
4
|
/**
|
|
@@ -19,6 +19,7 @@ const GLOBAL_UPLOAD_ALLOWLIST = [
|
|
|
19
19
|
"image/jpeg",
|
|
20
20
|
"image/gif",
|
|
21
21
|
"image/webp",
|
|
22
|
+
"image/avif",
|
|
22
23
|
"video/",
|
|
23
24
|
"audio/",
|
|
24
25
|
"application/pdf"
|
|
@@ -40,4 +41,4 @@ async function resolveFieldAllowlist(db, fieldId) {
|
|
|
40
41
|
|
|
41
42
|
//#endregion
|
|
42
43
|
export { resolveFieldAllowlist as n, GLOBAL_UPLOAD_ALLOWLIST as t };
|
|
43
|
-
//# sourceMappingURL=media-allowlist-
|
|
44
|
+
//# sourceMappingURL=media-allowlist-D35v7bmL.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-allowlist-D35v7bmL.mjs","names":[],"sources":["../src/api/handlers/media-allowlist.ts"],"sourcesContent":["import type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport { parseAllowedMimeTypes } from \"../../media/mime.js\";\n\n/**\n * MIME types allowed for upload by default (when no field-specific list\n * overrides this). Entries ending with \"/\" are prefix-matched (e.g.\n * \"video/\" matches \"video/mp4\", \"video/webm\", etc.).\n *\n * Image types are enumerated explicitly rather than using a bare \"image/\"\n * prefix so that \"image/svg+xml\" is excluded by default: there is no\n * upload-time content validation for SVG, so an unvalidated bare-prefix\n * allowlist would let a Contributor+ upload an SVG with an embedded\n * `<script>`. Fields that genuinely need SVG uploads can still opt in via\n * their own `allowedMimeTypes` (see `resolveFieldAllowlist` below), which is\n * unaffected by this default.\n */\nexport const GLOBAL_UPLOAD_ALLOWLIST: readonly string[] = [\n\t\"image/png\",\n\t\"image/jpeg\",\n\t\"image/gif\",\n\t\"image/webp\",\n\t\"image/avif\",\n\t\"video/\",\n\t\"audio/\",\n\t\"application/pdf\",\n];\n\n/**\n * Resolve the MIME allowlist for a specific field.\n *\n * Returns the field's `allowedMimeTypes` list when the field exists, is of\n * type \"file\" or \"image\", and has a non-empty list configured. Returns null\n * in all other cases — callers should fall back to GLOBAL_UPLOAD_ALLOWLIST.\n *\n * Authentication is the caller's responsibility (the upload routes already\n * gate on `media:upload`).\n */\nexport async function resolveFieldAllowlist(\n\tdb: Kysely<Database>,\n\tfieldId: string,\n): Promise<string[] | null> {\n\tconst row = await db\n\t\t.selectFrom(\"_emdash_fields\")\n\t\t.select([\"type\", \"validation\"])\n\t\t.where(\"id\", \"=\", fieldId)\n\t\t.where(\"type\", \"in\", [\"file\", \"image\"])\n\t\t.executeTakeFirst();\n\n\treturn row ? parseAllowedMimeTypes(row.validation) : null;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAkBA,MAAa,0BAA6C;CACzD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;;;;;;;;;;;AAYD,eAAsB,sBACrB,IACA,SAC2B;CAC3B,MAAM,MAAM,MAAM,GAChB,WAAW,iBAAiB,CAC5B,OAAO,CAAC,QAAQ,aAAa,CAAC,CAC9B,MAAM,MAAM,KAAK,QAAQ,CACzB,MAAM,QAAQ,MAAM,CAAC,QAAQ,QAAQ,CAAC,CACtC,kBAAkB;AAEpB,QAAO,MAAM,sBAAsB,IAAI,WAAW,GAAG"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { i as InvalidCursorError, o as decodeCursor, r as EmDashValidationError, s as encodeCursor } from "./types-CQPWhPxE.mjs";
|
|
2
|
+
import { sql } from "kysely";
|
|
3
|
+
import { ulid } from "ulidx";
|
|
4
|
+
|
|
5
|
+
//#region src/database/repositories/media-folders.ts
|
|
6
|
+
function escapeLike(value) {
|
|
7
|
+
return value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
|
8
|
+
}
|
|
9
|
+
function normalizeFolderSearch(value) {
|
|
10
|
+
return value.trim().normalize("NFKC").toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
function normalizeFolderName(name) {
|
|
13
|
+
if (typeof name !== "string") throw new EmDashValidationError("Folder name must be a string");
|
|
14
|
+
const trimmed = name.trim();
|
|
15
|
+
if (trimmed.length < 1 || trimmed.length > 200) throw new EmDashValidationError("Folder name must be between 1 and 200 characters");
|
|
16
|
+
return {
|
|
17
|
+
name: trimmed,
|
|
18
|
+
nameKey: trimmed.normalize("NFKC").toLowerCase()
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
var MediaFolderRepository = class {
|
|
22
|
+
constructor(db) {
|
|
23
|
+
this.db = db;
|
|
24
|
+
}
|
|
25
|
+
async findMany(options = {}) {
|
|
26
|
+
const limit = Math.max(1, Math.min(options.limit ?? 50, 100));
|
|
27
|
+
let query = this.db.selectFrom("media_folders").select([
|
|
28
|
+
"id",
|
|
29
|
+
"name",
|
|
30
|
+
"name_key"
|
|
31
|
+
]).orderBy("name_key", "asc").orderBy("id", "asc").limit(limit + 1);
|
|
32
|
+
const term = normalizeFolderSearch(options.q ?? "");
|
|
33
|
+
if (term) {
|
|
34
|
+
const pattern = `%${escapeLike(term)}%`;
|
|
35
|
+
query = query.where("name_key", "like", sql`${pattern} escape '\\'`);
|
|
36
|
+
}
|
|
37
|
+
if (options.cursor !== void 0) {
|
|
38
|
+
const { orderValue: nameKey, id } = decodeCursor(options.cursor);
|
|
39
|
+
query = query.where((eb) => eb.or([eb("name_key", ">", nameKey), eb.and([eb("name_key", "=", nameKey), eb("id", ">", id)])]));
|
|
40
|
+
}
|
|
41
|
+
const rows = await query.execute();
|
|
42
|
+
const hasMore = rows.length > limit;
|
|
43
|
+
const visible = rows.slice(0, limit);
|
|
44
|
+
return {
|
|
45
|
+
items: visible.map(({ id, name }) => ({
|
|
46
|
+
id,
|
|
47
|
+
name
|
|
48
|
+
})),
|
|
49
|
+
nextCursor: hasMore && visible.length > 0 ? encodeCursor(visible.at(-1).name_key, visible.at(-1).id) : void 0
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async findById(id) {
|
|
53
|
+
return await this.db.selectFrom("media_folders").select(["id", "name"]).where("id", "=", id).executeTakeFirst() ?? null;
|
|
54
|
+
}
|
|
55
|
+
async create(inputName) {
|
|
56
|
+
const { name, nameKey } = normalizeFolderName(inputName);
|
|
57
|
+
return await this.db.insertInto("media_folders").values({
|
|
58
|
+
id: ulid(),
|
|
59
|
+
name,
|
|
60
|
+
name_key: nameKey
|
|
61
|
+
}).returning(["id", "name"]).executeTakeFirstOrThrow();
|
|
62
|
+
}
|
|
63
|
+
async update(id, inputName) {
|
|
64
|
+
const { name, nameKey } = normalizeFolderName(inputName);
|
|
65
|
+
return await this.db.updateTable("media_folders").set({
|
|
66
|
+
name,
|
|
67
|
+
name_key: nameKey
|
|
68
|
+
}).where("id", "=", id).returning(["id", "name"]).executeTakeFirst() ?? null;
|
|
69
|
+
}
|
|
70
|
+
async delete(id) {
|
|
71
|
+
return await this.db.deleteFrom("media_folders").where("id", "=", id).returning("id").executeTakeFirst() !== void 0;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/api/handlers/media-folders.ts
|
|
77
|
+
const UNIQUE_VIOLATION_RE = /unique constraint failed|duplicate key value violates unique constraint/i;
|
|
78
|
+
async function handleMediaFolderList(db, options = {}) {
|
|
79
|
+
try {
|
|
80
|
+
return {
|
|
81
|
+
success: true,
|
|
82
|
+
data: await new MediaFolderRepository(db).findMany(options)
|
|
83
|
+
};
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error instanceof InvalidCursorError) return {
|
|
86
|
+
success: false,
|
|
87
|
+
error: {
|
|
88
|
+
code: "INVALID_CURSOR",
|
|
89
|
+
message: error.message
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
error: {
|
|
95
|
+
code: "MEDIA_FOLDER_LIST_ERROR",
|
|
96
|
+
message: "Failed to list media folders"
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function handleMediaFolderGet(db, id) {
|
|
102
|
+
try {
|
|
103
|
+
const item = await new MediaFolderRepository(db).findById(id);
|
|
104
|
+
if (!item) return {
|
|
105
|
+
success: false,
|
|
106
|
+
error: {
|
|
107
|
+
code: "NOT_FOUND",
|
|
108
|
+
message: "Media folder not found"
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
success: true,
|
|
113
|
+
data: { item }
|
|
114
|
+
};
|
|
115
|
+
} catch {
|
|
116
|
+
return {
|
|
117
|
+
success: false,
|
|
118
|
+
error: {
|
|
119
|
+
code: "MEDIA_FOLDER_GET_ERROR",
|
|
120
|
+
message: "Failed to get media folder"
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function handleMediaFolderCreate(db, input) {
|
|
126
|
+
try {
|
|
127
|
+
return {
|
|
128
|
+
success: true,
|
|
129
|
+
data: { item: await new MediaFolderRepository(db).create(input.name) }
|
|
130
|
+
};
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return mediaFolderWriteError(error, "MEDIA_FOLDER_CREATE_ERROR", "Failed to create media folder");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function handleMediaFolderUpdate(db, id, input) {
|
|
136
|
+
try {
|
|
137
|
+
const item = await new MediaFolderRepository(db).update(id, input.name);
|
|
138
|
+
if (!item) return {
|
|
139
|
+
success: false,
|
|
140
|
+
error: {
|
|
141
|
+
code: "NOT_FOUND",
|
|
142
|
+
message: "Media folder not found"
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
return {
|
|
146
|
+
success: true,
|
|
147
|
+
data: { item }
|
|
148
|
+
};
|
|
149
|
+
} catch (error) {
|
|
150
|
+
return mediaFolderWriteError(error, "MEDIA_FOLDER_UPDATE_ERROR", "Failed to update media folder");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function handleMediaFolderDelete(db, id) {
|
|
154
|
+
try {
|
|
155
|
+
if (!await new MediaFolderRepository(db).delete(id)) return {
|
|
156
|
+
success: false,
|
|
157
|
+
error: {
|
|
158
|
+
code: "NOT_FOUND",
|
|
159
|
+
message: "Media folder not found"
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
return {
|
|
163
|
+
success: true,
|
|
164
|
+
data: { deleted: true }
|
|
165
|
+
};
|
|
166
|
+
} catch {
|
|
167
|
+
return {
|
|
168
|
+
success: false,
|
|
169
|
+
error: {
|
|
170
|
+
code: "MEDIA_FOLDER_DELETE_ERROR",
|
|
171
|
+
message: "Failed to delete media folder"
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function mediaFolderWriteError(error, code, message) {
|
|
177
|
+
if (error instanceof EmDashValidationError) return {
|
|
178
|
+
success: false,
|
|
179
|
+
error: {
|
|
180
|
+
code: "VALIDATION_ERROR",
|
|
181
|
+
message: error.message
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
if (isUniqueViolation(error)) return {
|
|
185
|
+
success: false,
|
|
186
|
+
error: {
|
|
187
|
+
code: "CONFLICT",
|
|
188
|
+
message: "A media folder with this name already exists"
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
return {
|
|
192
|
+
success: false,
|
|
193
|
+
error: {
|
|
194
|
+
code,
|
|
195
|
+
message
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function isUniqueViolation(error) {
|
|
200
|
+
if (error && typeof error === "object") {
|
|
201
|
+
if ("code" in error && error.code === "23505") return true;
|
|
202
|
+
}
|
|
203
|
+
const message = error instanceof Error ? error.message : "";
|
|
204
|
+
if (UNIQUE_VIOLATION_RE.test(message)) return true;
|
|
205
|
+
return Boolean(error && typeof error === "object" && "cause" in error && isUniqueViolation(error.cause));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
//#endregion
|
|
209
|
+
export { handleMediaFolderUpdate as a, handleMediaFolderList as i, handleMediaFolderDelete as n, handleMediaFolderGet as r, handleMediaFolderCreate as t };
|
|
210
|
+
//# sourceMappingURL=media-folders-D-0CmRZj.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-folders-D-0CmRZj.mjs","names":[],"sources":["../src/database/repositories/media-folders.ts","../src/api/handlers/media-folders.ts"],"sourcesContent":["import { sql, type Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport type { Database } from \"../types.js\";\nimport { decodeCursor, EmDashValidationError, encodeCursor, type FindManyResult } from \"./types.js\";\n\nexport interface MediaFolder {\n\tid: string;\n\tname: string;\n}\n\nexport interface FindManyMediaFoldersOptions {\n\tlimit?: number;\n\tcursor?: string;\n\tq?: string;\n}\n\nfunction escapeLike(value: string): string {\n\treturn value.replaceAll(\"\\\\\", \"\\\\\\\\\").replaceAll(\"%\", \"\\\\%\").replaceAll(\"_\", \"\\\\_\");\n}\n\nfunction normalizeFolderSearch(value: string): string {\n\treturn value.trim().normalize(\"NFKC\").toLowerCase();\n}\n\nfunction normalizeFolderName(name: string): { name: string; nameKey: string } {\n\tif (typeof name !== \"string\") {\n\t\tthrow new EmDashValidationError(\"Folder name must be a string\");\n\t}\n\tconst trimmed = name.trim();\n\tif (trimmed.length < 1 || trimmed.length > 200) {\n\t\tthrow new EmDashValidationError(\"Folder name must be between 1 and 200 characters\");\n\t}\n\treturn { name: trimmed, nameKey: trimmed.normalize(\"NFKC\").toLowerCase() };\n}\n\nexport class MediaFolderRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\tasync findMany(options: FindManyMediaFoldersOptions = {}): Promise<FindManyResult<MediaFolder>> {\n\t\tconst limit = Math.max(1, Math.min(options.limit ?? 50, 100));\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"media_folders\")\n\t\t\t.select([\"id\", \"name\", \"name_key\"])\n\t\t\t.orderBy(\"name_key\", \"asc\")\n\t\t\t.orderBy(\"id\", \"asc\")\n\t\t\t.limit(limit + 1);\n\n\t\tconst term = normalizeFolderSearch(options.q ?? \"\");\n\t\tif (term) {\n\t\t\tconst pattern = `%${escapeLike(term)}%`;\n\t\t\tquery = query.where(\"name_key\", \"like\", sql<string>`${pattern} escape '\\\\'`);\n\t\t}\n\n\t\tif (options.cursor !== undefined) {\n\t\t\tconst { orderValue: nameKey, id } = decodeCursor(options.cursor);\n\t\t\tquery = query.where((eb) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb(\"name_key\", \">\", nameKey),\n\t\t\t\t\teb.and([eb(\"name_key\", \"=\", nameKey), eb(\"id\", \">\", id)]),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tconst rows = await query.execute();\n\t\tconst hasMore = rows.length > limit;\n\t\tconst visible = rows.slice(0, limit);\n\t\treturn {\n\t\t\titems: visible.map(({ id, name }) => ({ id, name })),\n\t\t\tnextCursor:\n\t\t\t\thasMore && visible.length > 0\n\t\t\t\t\t? encodeCursor(visible.at(-1)!.name_key, visible.at(-1)!.id)\n\t\t\t\t\t: undefined,\n\t\t};\n\t}\n\n\tasync findById(id: string): Promise<MediaFolder | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"media_folders\")\n\t\t\t.select([\"id\", \"name\"])\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\t\treturn row ?? null;\n\t}\n\n\tasync create(inputName: string): Promise<MediaFolder> {\n\t\tconst { name, nameKey } = normalizeFolderName(inputName);\n\t\tconst row = await this.db\n\t\t\t.insertInto(\"media_folders\")\n\t\t\t.values({ id: ulid(), name, name_key: nameKey })\n\t\t\t.returning([\"id\", \"name\"])\n\t\t\t.executeTakeFirstOrThrow();\n\t\treturn row;\n\t}\n\n\tasync update(id: string, inputName: string): Promise<MediaFolder | null> {\n\t\tconst { name, nameKey } = normalizeFolderName(inputName);\n\t\tconst row = await this.db\n\t\t\t.updateTable(\"media_folders\")\n\t\t\t.set({ name, name_key: nameKey })\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.returning([\"id\", \"name\"])\n\t\t\t.executeTakeFirst();\n\t\treturn row ?? null;\n\t}\n\n\tasync delete(id: string): Promise<boolean> {\n\t\tconst row = await this.db\n\t\t\t.deleteFrom(\"media_folders\")\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.returning(\"id\")\n\t\t\t.executeTakeFirst();\n\t\treturn row !== undefined;\n\t}\n}\n","import type { Kysely } from \"kysely\";\n\nimport {\n\tMediaFolderRepository,\n\ttype MediaFolder,\n} from \"../../database/repositories/media-folders.js\";\nimport { EmDashValidationError, InvalidCursorError } from \"../../database/repositories/types.js\";\nimport type { Database } from \"../../database/types.js\";\nimport type { ApiResult } from \"../types.js\";\n\nconst UNIQUE_VIOLATION_RE =\n\t/unique constraint failed|duplicate key value violates unique constraint/i;\n\nexport async function handleMediaFolderList(\n\tdb: Kysely<Database>,\n\toptions: { limit?: number; cursor?: string; q?: string } = {},\n): Promise<ApiResult<{ items: MediaFolder[]; nextCursor?: string }>> {\n\ttry {\n\t\tconst result = await new MediaFolderRepository(db).findMany(options);\n\t\treturn { success: true, data: result };\n\t} catch (error) {\n\t\tif (error instanceof InvalidCursorError) {\n\t\t\treturn { success: false, error: { code: \"INVALID_CURSOR\", message: error.message } };\n\t\t}\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MEDIA_FOLDER_LIST_ERROR\", message: \"Failed to list media folders\" },\n\t\t};\n\t}\n}\n\nexport async function handleMediaFolderGet(\n\tdb: Kysely<Database>,\n\tid: string,\n): Promise<ApiResult<{ item: MediaFolder }>> {\n\ttry {\n\t\tconst item = await new MediaFolderRepository(db).findById(id);\n\t\tif (!item) {\n\t\t\treturn { success: false, error: { code: \"NOT_FOUND\", message: \"Media folder not found\" } };\n\t\t}\n\t\treturn { success: true, data: { item } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MEDIA_FOLDER_GET_ERROR\", message: \"Failed to get media folder\" },\n\t\t};\n\t}\n}\n\nexport async function handleMediaFolderCreate(\n\tdb: Kysely<Database>,\n\tinput: { name: string },\n): Promise<ApiResult<{ item: MediaFolder }>> {\n\ttry {\n\t\tconst item = await new MediaFolderRepository(db).create(input.name);\n\t\treturn { success: true, data: { item } };\n\t} catch (error) {\n\t\treturn mediaFolderWriteError(\n\t\t\terror,\n\t\t\t\"MEDIA_FOLDER_CREATE_ERROR\",\n\t\t\t\"Failed to create media folder\",\n\t\t);\n\t}\n}\n\nexport async function handleMediaFolderUpdate(\n\tdb: Kysely<Database>,\n\tid: string,\n\tinput: { name: string },\n): Promise<ApiResult<{ item: MediaFolder }>> {\n\ttry {\n\t\tconst item = await new MediaFolderRepository(db).update(id, input.name);\n\t\tif (!item) {\n\t\t\treturn { success: false, error: { code: \"NOT_FOUND\", message: \"Media folder not found\" } };\n\t\t}\n\t\treturn { success: true, data: { item } };\n\t} catch (error) {\n\t\treturn mediaFolderWriteError(\n\t\t\terror,\n\t\t\t\"MEDIA_FOLDER_UPDATE_ERROR\",\n\t\t\t\"Failed to update media folder\",\n\t\t);\n\t}\n}\n\nexport async function handleMediaFolderDelete(\n\tdb: Kysely<Database>,\n\tid: string,\n): Promise<ApiResult<{ deleted: true }>> {\n\ttry {\n\t\tconst deleted = await new MediaFolderRepository(db).delete(id);\n\t\tif (!deleted) {\n\t\t\treturn { success: false, error: { code: \"NOT_FOUND\", message: \"Media folder not found\" } };\n\t\t}\n\t\treturn { success: true, data: { deleted: true } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MEDIA_FOLDER_DELETE_ERROR\", message: \"Failed to delete media folder\" },\n\t\t};\n\t}\n}\n\nfunction mediaFolderWriteError(\n\terror: unknown,\n\tcode: \"MEDIA_FOLDER_CREATE_ERROR\" | \"MEDIA_FOLDER_UPDATE_ERROR\",\n\tmessage: string,\n): ApiResult<never> {\n\tif (error instanceof EmDashValidationError) {\n\t\treturn { success: false, error: { code: \"VALIDATION_ERROR\", message: error.message } };\n\t}\n\tif (isUniqueViolation(error)) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"CONFLICT\", message: \"A media folder with this name already exists\" },\n\t\t};\n\t}\n\treturn { success: false, error: { code, message } };\n}\n\nfunction isUniqueViolation(error: unknown): boolean {\n\tif (error && typeof error === \"object\") {\n\t\tif (\"code\" in error && error.code === \"23505\") return true;\n\t}\n\tconst message = error instanceof Error ? error.message : \"\";\n\tif (UNIQUE_VIOLATION_RE.test(message)) return true;\n\treturn Boolean(\n\t\terror && typeof error === \"object\" && \"cause\" in error && isUniqueViolation(error.cause),\n\t);\n}\n"],"mappings":"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"}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import "./config-C3EBstHi.mjs";
|
|
2
2
|
import { n as decodeBase64Bytes } from "./base64-CvLhUKQv.mjs";
|
|
3
3
|
import "./types-CQPWhPxE.mjs";
|
|
4
|
-
import { t as MediaRepository } from "./media-
|
|
5
|
-
import "./dist-
|
|
6
|
-
import { n as normalizeMime, t as matchesMimeAllowlist } from "./mime-
|
|
7
|
-
import { t as enrichImageMetadata } from "./enrich-
|
|
8
|
-
import { n as computeContentHash } from "./hash-
|
|
9
|
-
import {
|
|
10
|
-
import { n as DEFAULT_MAX_UPLOAD_SIZE, r as formatFileSize, t as CONTENT_TYPE_RE } from "./media-
|
|
11
|
-
import { t as GLOBAL_UPLOAD_ALLOWLIST } from "./media-allowlist-
|
|
4
|
+
import { t as MediaRepository } from "./media-D6SeAfBl.mjs";
|
|
5
|
+
import "./dist-89xCrGqi.mjs";
|
|
6
|
+
import { n as normalizeMime, t as matchesMimeAllowlist } from "./mime-CUIG6x84.mjs";
|
|
7
|
+
import { t as enrichImageMetadata } from "./enrich-BX0IDp4z.mjs";
|
|
8
|
+
import { n as computeContentHash } from "./hash-CTcngWvj.mjs";
|
|
9
|
+
import { i as ssrfSafeFetch, t as SsrfError } from "./ssrf-DqebVE34.mjs";
|
|
10
|
+
import { n as DEFAULT_MAX_UPLOAD_SIZE, r as formatFileSize, t as CONTENT_TYPE_RE } from "./media-BjwNuLEu.mjs";
|
|
11
|
+
import { t as GLOBAL_UPLOAD_ALLOWLIST } from "./media-allowlist-D35v7bmL.mjs";
|
|
12
12
|
import { ulid } from "ulidx";
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
|
|
@@ -144,4 +144,4 @@ async function handleMediaUpload(db, storage, input) {
|
|
|
144
144
|
|
|
145
145
|
//#endregion
|
|
146
146
|
export { handleMediaUpload };
|
|
147
|
-
//# sourceMappingURL=media-upload-
|
|
147
|
+
//# sourceMappingURL=media-upload-C7bLXGcI.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media-upload-CHBVn6m-.mjs","names":[],"sources":["../src/api/handlers/media-upload.ts"],"sourcesContent":["/**\n * Programmatic media upload handler (MCP `media_upload` tool).\n *\n * Accepts file bytes as base64 or fetches them from an external URL\n * (SSRF-guarded), then runs the same pipeline as the multipart REST\n * upload route: allowlist + size validation, content-hash deduplication,\n * storage upload, image metadata enrichment, and record creation.\n */\n\nimport * as path from \"node:path\";\n\nimport type { Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { MediaRepository, type MediaItem } from \"../../database/repositories/media.js\";\nimport type { Database } from \"../../database/types.js\";\nimport { enrichImageMetadata } from \"../../media/enrich.js\";\nimport { matchesMimeAllowlist, normalizeMime } from \"../../media/mime.js\";\nimport { SsrfError, ssrfSafeFetch } from \"../../security/ssrf.js\";\nimport type { Storage } from \"../../storage/types.js\";\nimport { decodeBase64Bytes } from \"../../utils/base64.js\";\nimport { computeContentHash } from \"../../utils/hash.js\";\nimport { CONTENT_TYPE_RE, DEFAULT_MAX_UPLOAD_SIZE, formatFileSize } from \"../schemas/media.js\";\nimport type { ApiResult } from \"../types.js\";\nimport { GLOBAL_UPLOAD_ALLOWLIST } from \"./media-allowlist.js\";\n\nexport interface MediaUploadInput {\n\t/** Original filename (e.g. 'logo.png'); the extension is kept on the storage key. */\n\tfilename: string;\n\t/** Base64-encoded file contents. Exactly one of `base64` / `url` must be set. */\n\tbase64?: string;\n\t/** External http(s) URL to fetch the file from. Exactly one of `base64` / `url` must be set. */\n\turl?: string;\n\t/**\n\t * MIME type. Required with `base64`; optional with `url` (falls back to\n\t * the response's Content-Type header).\n\t */\n\tcontentType?: string;\n\t/** Alt text stored on the media record. */\n\talt?: string;\n\tauthorId?: string;\n\t/** Upload size limit in bytes (defaults to DEFAULT_MAX_UPLOAD_SIZE). */\n\tmaxUploadSize?: number;\n}\n\nexport type MediaUploadResult = ApiResult<{\n\titem: MediaItem & { url: string };\n\tdeduplicated?: boolean;\n}>;\n\nfunction fail(code: string, message: string): MediaUploadResult {\n\treturn { success: false, error: { code, message } };\n}\n\n/** Same relative-URL shape the REST media routes return. */\nfunction withUrl(item: MediaItem): MediaItem & { url: string } {\n\treturn { ...item, url: `/_emdash/api/media/file/${item.storageKey}` };\n}\n\n/** Strip parameters from a Content-Type header value (e.g. '; charset=...'). */\nfunction bareMime(headerValue: string): string {\n\treturn (headerValue.split(\";\")[0] ?? \"\").trim();\n}\n\n/**\n * Acquire the file bytes and MIME type from either the base64 payload or\n * the external URL. Returns an error result on any validation failure.\n */\nasync function acquireBytes(\n\tinput: MediaUploadInput,\n\tmaxUploadSize: number,\n): Promise<{ bytes: Uint8Array; mimeType: string } | MediaUploadResult> {\n\tif (input.base64) {\n\t\tif (!input.contentType) {\n\t\t\treturn fail(\"VALIDATION_ERROR\", \"contentType is required when uploading base64 data\");\n\t\t}\n\t\t// Cheap size precheck on the encoded string (decoded size is ~3/4 of\n\t\t// the base64 length) before allocating the decoded buffer.\n\t\tif ((input.base64.length * 3) / 4 > maxUploadSize) {\n\t\t\treturn fail(\n\t\t\t\t\"PAYLOAD_TOO_LARGE\",\n\t\t\t\t`File exceeds maximum size of ${formatFileSize(maxUploadSize)}`,\n\t\t\t);\n\t\t}\n\t\ttry {\n\t\t\treturn { bytes: decodeBase64Bytes(input.base64), mimeType: input.contentType };\n\t\t} catch {\n\t\t\treturn fail(\"VALIDATION_ERROR\", \"Invalid base64 data\");\n\t\t}\n\t}\n\n\t// url mode — the caller guarantees exactly one source, so url is set here\n\tconst url = input.url;\n\tif (!url) {\n\t\treturn fail(\"VALIDATION_ERROR\", \"Provide exactly one of 'base64' or 'url'\");\n\t}\n\tlet response: Response;\n\ttry {\n\t\tresponse = await ssrfSafeFetch(url, { headers: { accept: \"*/*\" } });\n\t} catch (error) {\n\t\tif (error instanceof SsrfError) {\n\t\t\treturn fail(\"VALIDATION_ERROR\", `URL not allowed: ${error.message}`);\n\t\t}\n\t\treturn fail(\"FETCH_ERROR\", \"Failed to fetch file from URL\");\n\t}\n\tif (!response.ok) {\n\t\treturn fail(\"FETCH_ERROR\", `Failed to fetch file from URL (HTTP ${response.status})`);\n\t}\n\n\tconst contentLength = response.headers.get(\"Content-Length\");\n\tif (contentLength && parseInt(contentLength, 10) > maxUploadSize) {\n\t\treturn fail(\n\t\t\t\"PAYLOAD_TOO_LARGE\",\n\t\t\t`File exceeds maximum size of ${formatFileSize(maxUploadSize)}`,\n\t\t);\n\t}\n\n\tconst mimeType = input.contentType ?? bareMime(response.headers.get(\"Content-Type\") ?? \"\");\n\tif (!mimeType) {\n\t\treturn fail(\"VALIDATION_ERROR\", \"Could not determine MIME type — pass contentType explicitly\");\n\t}\n\n\tconst bytes = new Uint8Array(await response.arrayBuffer());\n\treturn { bytes, mimeType };\n}\n\n/**\n * Upload a media file from base64 data or an external URL.\n *\n * Mirrors the REST `POST /_emdash/api/media` route: global MIME allowlist,\n * size limit, content-hash dedupe (returns the existing item with\n * `deduplicated: true`), storage upload with cleanup on failure, and\n * image metadata enrichment (dimensions, blurhash, dominant color).\n */\nexport async function handleMediaUpload(\n\tdb: Kysely<Database>,\n\tstorage: Storage,\n\tinput: MediaUploadInput,\n): Promise<MediaUploadResult> {\n\tif (!input.base64 === !input.url) {\n\t\treturn fail(\"VALIDATION_ERROR\", \"Provide exactly one of 'base64' or 'url'\");\n\t}\n\n\tconst rawMax = input.maxUploadSize ?? DEFAULT_MAX_UPLOAD_SIZE;\n\tif (!Number.isFinite(rawMax) || rawMax <= 0) {\n\t\treturn fail(\"CONFIGURATION_ERROR\", \"Invalid maxUploadSize configuration\");\n\t}\n\n\tconst acquired = await acquireBytes(input, rawMax);\n\tif (\"success\" in acquired) return acquired;\n\tconst { bytes } = acquired;\n\n\t// Validate the raw MIME string before normalize/allowlist: normalizeMime\n\t// only strips parameters and matchesMimeAllowlist only checks startsWith,\n\t// so without this a crafted value like \"image/png\\r\\nX-Evil: 1\" would\n\t// reach the storage backend's ContentType header and be echoed by the\n\t// media file serving route.\n\tif (!CONTENT_TYPE_RE.test(acquired.mimeType)) {\n\t\treturn fail(\"VALIDATION_ERROR\", \"Invalid content type\");\n\t}\n\tconst mimeType = normalizeMime(acquired.mimeType);\n\n\tif (!matchesMimeAllowlist(mimeType, GLOBAL_UPLOAD_ALLOWLIST)) {\n\t\treturn fail(\"INVALID_TYPE\", \"File type not allowed\");\n\t}\n\tif (bytes.byteLength > rawMax) {\n\t\treturn fail(\"PAYLOAD_TOO_LARGE\", `File exceeds maximum size of ${formatFileSize(rawMax)}`);\n\t}\n\n\ttry {\n\t\tconst contentHash = await computeContentHash(bytes);\n\t\tconst repo = new MediaRepository(db);\n\n\t\tconst existing = await repo.findByContentHash(contentHash);\n\t\tif (existing) {\n\t\t\treturn { success: true, data: { item: withUrl(existing), deduplicated: true } };\n\t\t}\n\n\t\tconst storageKey = `${ulid()}${path.extname(input.filename)}`;\n\t\tawait storage.upload({ key: storageKey, body: bytes, contentType: mimeType });\n\n\t\ttry {\n\t\t\tconst enriched = await enrichImageMetadata(bytes, mimeType);\n\t\t\tconst item = await repo.create({\n\t\t\t\tfilename: input.filename,\n\t\t\t\tmimeType,\n\t\t\t\tsize: bytes.byteLength,\n\t\t\t\twidth: enriched.width,\n\t\t\t\theight: enriched.height,\n\t\t\t\talt: input.alt,\n\t\t\t\tstorageKey,\n\t\t\t\tcontentHash,\n\t\t\t\tblurhash: enriched.blurhash,\n\t\t\t\tdominantColor: enriched.dominantColor,\n\t\t\t\tauthorId: input.authorId,\n\t\t\t});\n\t\t\treturn { success: true, data: { item: withUrl(item) } };\n\t\t} catch (error) {\n\t\t\t// Don't leave an orphaned object in storage when record creation fails\n\t\t\ttry {\n\t\t\t\tawait storage.delete(storageKey);\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t} catch {\n\t\treturn fail(\"UPLOAD_ERROR\", \"Upload failed\");\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"media-upload-C7bLXGcI.mjs","names":[],"sources":["../src/api/handlers/media-upload.ts"],"sourcesContent":["/**\n * Programmatic media upload handler (MCP `media_upload` tool).\n *\n * Accepts file bytes as base64 or fetches them from an external URL\n * (SSRF-guarded), then runs the same pipeline as the multipart REST\n * upload route: allowlist + size validation, content-hash deduplication,\n * storage upload, image metadata enrichment, and record creation.\n */\n\nimport * as path from \"node:path\";\n\nimport type { Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { MediaRepository, type MediaItem } from \"../../database/repositories/media.js\";\nimport type { Database } from \"../../database/types.js\";\nimport { enrichImageMetadata } from \"../../media/enrich.js\";\nimport { matchesMimeAllowlist, normalizeMime } from \"../../media/mime.js\";\nimport { SsrfError, ssrfSafeFetch } from \"../../security/ssrf.js\";\nimport type { Storage } from \"../../storage/types.js\";\nimport { decodeBase64Bytes } from \"../../utils/base64.js\";\nimport { computeContentHash } from \"../../utils/hash.js\";\nimport { CONTENT_TYPE_RE, DEFAULT_MAX_UPLOAD_SIZE, formatFileSize } from \"../schemas/media.js\";\nimport type { ApiResult } from \"../types.js\";\nimport { GLOBAL_UPLOAD_ALLOWLIST } from \"./media-allowlist.js\";\n\nexport interface MediaUploadInput {\n\t/** Original filename (e.g. 'logo.png'); the extension is kept on the storage key. */\n\tfilename: string;\n\t/** Base64-encoded file contents. Exactly one of `base64` / `url` must be set. */\n\tbase64?: string;\n\t/** External http(s) URL to fetch the file from. Exactly one of `base64` / `url` must be set. */\n\turl?: string;\n\t/**\n\t * MIME type. Required with `base64`; optional with `url` (falls back to\n\t * the response's Content-Type header).\n\t */\n\tcontentType?: string;\n\t/** Alt text stored on the media record. */\n\talt?: string;\n\tauthorId?: string;\n\t/** Upload size limit in bytes (defaults to DEFAULT_MAX_UPLOAD_SIZE). */\n\tmaxUploadSize?: number;\n}\n\nexport type MediaUploadResult = ApiResult<{\n\titem: MediaItem & { url: string };\n\tdeduplicated?: boolean;\n}>;\n\nfunction fail(code: string, message: string): MediaUploadResult {\n\treturn { success: false, error: { code, message } };\n}\n\n/** Same relative-URL shape the REST media routes return. */\nfunction withUrl(item: MediaItem): MediaItem & { url: string } {\n\treturn { ...item, url: `/_emdash/api/media/file/${item.storageKey}` };\n}\n\n/** Strip parameters from a Content-Type header value (e.g. '; charset=...'). */\nfunction bareMime(headerValue: string): string {\n\treturn (headerValue.split(\";\")[0] ?? \"\").trim();\n}\n\n/**\n * Acquire the file bytes and MIME type from either the base64 payload or\n * the external URL. Returns an error result on any validation failure.\n */\nasync function acquireBytes(\n\tinput: MediaUploadInput,\n\tmaxUploadSize: number,\n): Promise<{ bytes: Uint8Array; mimeType: string } | MediaUploadResult> {\n\tif (input.base64) {\n\t\tif (!input.contentType) {\n\t\t\treturn fail(\"VALIDATION_ERROR\", \"contentType is required when uploading base64 data\");\n\t\t}\n\t\t// Cheap size precheck on the encoded string (decoded size is ~3/4 of\n\t\t// the base64 length) before allocating the decoded buffer.\n\t\tif ((input.base64.length * 3) / 4 > maxUploadSize) {\n\t\t\treturn fail(\n\t\t\t\t\"PAYLOAD_TOO_LARGE\",\n\t\t\t\t`File exceeds maximum size of ${formatFileSize(maxUploadSize)}`,\n\t\t\t);\n\t\t}\n\t\ttry {\n\t\t\treturn { bytes: decodeBase64Bytes(input.base64), mimeType: input.contentType };\n\t\t} catch {\n\t\t\treturn fail(\"VALIDATION_ERROR\", \"Invalid base64 data\");\n\t\t}\n\t}\n\n\t// url mode — the caller guarantees exactly one source, so url is set here\n\tconst url = input.url;\n\tif (!url) {\n\t\treturn fail(\"VALIDATION_ERROR\", \"Provide exactly one of 'base64' or 'url'\");\n\t}\n\tlet response: Response;\n\ttry {\n\t\tresponse = await ssrfSafeFetch(url, { headers: { accept: \"*/*\" } });\n\t} catch (error) {\n\t\tif (error instanceof SsrfError) {\n\t\t\treturn fail(\"VALIDATION_ERROR\", `URL not allowed: ${error.message}`);\n\t\t}\n\t\treturn fail(\"FETCH_ERROR\", \"Failed to fetch file from URL\");\n\t}\n\tif (!response.ok) {\n\t\treturn fail(\"FETCH_ERROR\", `Failed to fetch file from URL (HTTP ${response.status})`);\n\t}\n\n\tconst contentLength = response.headers.get(\"Content-Length\");\n\tif (contentLength && parseInt(contentLength, 10) > maxUploadSize) {\n\t\treturn fail(\n\t\t\t\"PAYLOAD_TOO_LARGE\",\n\t\t\t`File exceeds maximum size of ${formatFileSize(maxUploadSize)}`,\n\t\t);\n\t}\n\n\tconst mimeType = input.contentType ?? bareMime(response.headers.get(\"Content-Type\") ?? \"\");\n\tif (!mimeType) {\n\t\treturn fail(\"VALIDATION_ERROR\", \"Could not determine MIME type — pass contentType explicitly\");\n\t}\n\n\tconst bytes = new Uint8Array(await response.arrayBuffer());\n\treturn { bytes, mimeType };\n}\n\n/**\n * Upload a media file from base64 data or an external URL.\n *\n * Mirrors the REST `POST /_emdash/api/media` route: global MIME allowlist,\n * size limit, content-hash dedupe (returns the existing item with\n * `deduplicated: true`), storage upload with cleanup on failure, and\n * image metadata enrichment (dimensions, blurhash, dominant color).\n */\nexport async function handleMediaUpload(\n\tdb: Kysely<Database>,\n\tstorage: Storage,\n\tinput: MediaUploadInput,\n): Promise<MediaUploadResult> {\n\tif (!input.base64 === !input.url) {\n\t\treturn fail(\"VALIDATION_ERROR\", \"Provide exactly one of 'base64' or 'url'\");\n\t}\n\n\tconst rawMax = input.maxUploadSize ?? DEFAULT_MAX_UPLOAD_SIZE;\n\tif (!Number.isFinite(rawMax) || rawMax <= 0) {\n\t\treturn fail(\"CONFIGURATION_ERROR\", \"Invalid maxUploadSize configuration\");\n\t}\n\n\tconst acquired = await acquireBytes(input, rawMax);\n\tif (\"success\" in acquired) return acquired;\n\tconst { bytes } = acquired;\n\n\t// Validate the raw MIME string before normalize/allowlist: normalizeMime\n\t// only strips parameters and matchesMimeAllowlist only checks startsWith,\n\t// so without this a crafted value like \"image/png\\r\\nX-Evil: 1\" would\n\t// reach the storage backend's ContentType header and be echoed by the\n\t// media file serving route.\n\tif (!CONTENT_TYPE_RE.test(acquired.mimeType)) {\n\t\treturn fail(\"VALIDATION_ERROR\", \"Invalid content type\");\n\t}\n\tconst mimeType = normalizeMime(acquired.mimeType);\n\n\tif (!matchesMimeAllowlist(mimeType, GLOBAL_UPLOAD_ALLOWLIST)) {\n\t\treturn fail(\"INVALID_TYPE\", \"File type not allowed\");\n\t}\n\tif (bytes.byteLength > rawMax) {\n\t\treturn fail(\"PAYLOAD_TOO_LARGE\", `File exceeds maximum size of ${formatFileSize(rawMax)}`);\n\t}\n\n\ttry {\n\t\tconst contentHash = await computeContentHash(bytes);\n\t\tconst repo = new MediaRepository(db);\n\n\t\tconst existing = await repo.findByContentHash(contentHash);\n\t\tif (existing) {\n\t\t\treturn { success: true, data: { item: withUrl(existing), deduplicated: true } };\n\t\t}\n\n\t\tconst storageKey = `${ulid()}${path.extname(input.filename)}`;\n\t\tawait storage.upload({ key: storageKey, body: bytes, contentType: mimeType });\n\n\t\ttry {\n\t\t\tconst enriched = await enrichImageMetadata(bytes, mimeType);\n\t\t\tconst item = await repo.create({\n\t\t\t\tfilename: input.filename,\n\t\t\t\tmimeType,\n\t\t\t\tsize: bytes.byteLength,\n\t\t\t\twidth: enriched.width,\n\t\t\t\theight: enriched.height,\n\t\t\t\talt: input.alt,\n\t\t\t\tstorageKey,\n\t\t\t\tcontentHash,\n\t\t\t\tblurhash: enriched.blurhash,\n\t\t\t\tdominantColor: enriched.dominantColor,\n\t\t\t\tauthorId: input.authorId,\n\t\t\t});\n\t\t\treturn { success: true, data: { item: withUrl(item) } };\n\t\t} catch (error) {\n\t\t\t// Don't leave an orphaned object in storage when record creation fails\n\t\t\ttry {\n\t\t\t\tawait storage.delete(storageKey);\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t} catch {\n\t\treturn fail(\"UPLOAD_ERROR\", \"Upload failed\");\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media-url-
|
|
1
|
+
{"version":3,"file":"media-url-DH7giMx_.mjs","names":[],"sources":["../src/seo/media-url.ts"],"sourcesContent":["/**\n * Resolve a stored SEO image reference to a URL.\n *\n * The CMS SEO panel stores `seo_image` in one of these shapes:\n * - an absolute URL (`https://...`) — returned as-is;\n * - a root-relative path that already includes the media API prefix\n * (`/_emdash/api/media/file/01KS....webp`) — prefixed with `siteUrl`;\n * - a bare media id (`01KS...`) — expanded to the media API path, then\n * prefixed with `siteUrl`.\n *\n * Shared by the SEO meta builder (`og:image`) and the sitemap route\n * (`<image:image>`) so both resolve image references identically.\n */\nconst TRAILING_SLASH_RE = /\\/$/;\nconst ABSOLUTE_URL_RE = /^https?:\\/\\//i;\n\nexport function buildSeoImageUrl(imageRef: string, siteUrl?: string): string {\n\t// Already absolute — use as-is.\n\tif (ABSOLUTE_URL_RE.test(imageRef)) {\n\t\treturn imageRef;\n\t}\n\n\t// Root-relative path (already includes the media API prefix). Without\n\t// this branch we'd re-prefix and produce a doubled path that 404s.\n\tif (imageRef.startsWith(\"/\")) {\n\t\treturn siteUrl ? `${siteUrl.replace(TRAILING_SLASH_RE, \"\")}${imageRef}` : imageRef;\n\t}\n\n\t// Bare media id — build the full media API path.\n\tconst mediaPath = `/_emdash/api/media/file/${imageRef}`;\n\treturn siteUrl ? `${siteUrl.replace(TRAILING_SLASH_RE, \"\")}${mediaPath}` : mediaPath;\n}\n"],"mappings":";;;;;;;;;;;;;;AAaA,MAAM,oBAAoB;AAC1B,MAAM,kBAAkB;AAExB,SAAgB,iBAAiB,UAAkB,SAA0B;AAE5E,KAAI,gBAAgB,KAAK,SAAS,CACjC,QAAO;AAKR,KAAI,SAAS,WAAW,IAAI,CAC3B,QAAO,UAAU,GAAG,QAAQ,QAAQ,mBAAmB,GAAG,GAAG,aAAa;CAI3E,MAAM,YAAY,2BAA2B;AAC7C,QAAO,UAAU,GAAG,QAAQ,QAAQ,mBAAmB,GAAG,GAAG,cAAc"}
|