emdash 0.31.0 → 0.31.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/api/schemas/index.d.mts +1 -1
  2. package/dist/{api-EibxGP-V.mjs → api-BkqFzIu4.mjs} +2 -2
  3. package/dist/{api-EibxGP-V.mjs.map → api-BkqFzIu4.mjs.map} +1 -1
  4. package/dist/{apply-fGHCAwWN.mjs → apply-DuXsqll7.mjs} +6 -6
  5. package/dist/{apply-fGHCAwWN.mjs.map → apply-DuXsqll7.mjs.map} +1 -1
  6. package/dist/astro/index.d.mts +2 -2
  7. package/dist/astro/index.mjs +1 -1
  8. package/dist/astro/middleware/auth.d.mts +2 -2
  9. package/dist/astro/middleware.d.mts +2 -2
  10. package/dist/astro/middleware.mjs +16 -11
  11. package/dist/astro/middleware.mjs.map +1 -1
  12. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +1 -1
  13. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +1 -1
  14. package/dist/astro/routes/api/admin/bylines/index.mjs +1 -1
  15. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +3 -3
  16. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +3 -3
  17. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +3 -3
  18. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +3 -3
  19. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +3 -3
  20. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +3 -3
  21. package/dist/astro/routes/api/admin/plugins/index.mjs +3 -3
  22. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +3 -3
  23. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +3 -3
  24. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +3 -3
  25. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +3 -3
  26. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +4 -4
  27. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +3 -3
  28. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +4 -4
  29. package/dist/astro/routes/api/admin/plugins/updates.mjs +3 -3
  30. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +3 -3
  31. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +3 -3
  32. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +1 -1
  33. package/dist/astro/routes/api/import/wordpress/execute.d.mts +2 -2
  34. package/dist/astro/routes/api/import/wordpress/execute.mjs +2 -2
  35. package/dist/astro/routes/api/import/wordpress/media.d.mts +2 -2
  36. package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +2 -2
  37. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +2 -2
  38. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +3 -3
  39. package/dist/astro/routes/api/manifest.mjs +1 -1
  40. package/dist/astro/routes/api/mcp.mjs +7 -7
  41. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +3 -3
  42. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +3 -3
  43. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +3 -3
  44. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +3 -3
  45. package/dist/astro/routes/api/schema/collections/index.mjs +3 -3
  46. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +3 -3
  47. package/dist/astro/routes/api/schema/orphans/index.mjs +3 -3
  48. package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +1 -1
  49. package/dist/astro/routes/api/settings/backups/archives/index.mjs +1 -1
  50. package/dist/astro/routes/api/settings/backups/export.mjs +1 -1
  51. package/dist/astro/routes/api/settings/backups/index.mjs +1 -1
  52. package/dist/astro/routes/api/setup/dev-bypass.mjs +1 -1
  53. package/dist/astro/routes/api/setup/index.mjs +1 -1
  54. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +2 -2
  55. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +2 -2
  56. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +2 -2
  57. package/dist/astro/routes/api/taxonomies/index.mjs +2 -2
  58. package/dist/astro/types.d.mts +2 -2
  59. package/dist/{backup-Qr9PaGUo.mjs → backup-DoW-H0r4.mjs} +2 -2
  60. package/dist/{backup-Qr9PaGUo.mjs.map → backup-DoW-H0r4.mjs.map} +1 -1
  61. package/dist/{bylines-FyZu6TbJ.mjs → bylines-Bc-3xtpz.mjs} +2 -2
  62. package/dist/{bylines-FyZu6TbJ.mjs.map → bylines-Bc-3xtpz.mjs.map} +1 -1
  63. package/dist/cli/index.mjs +1 -1
  64. package/dist/{index-BcEH_FfS.d.mts → index-BZJ_5FOX.d.mts} +18 -3
  65. package/dist/{index-BcEH_FfS.d.mts.map → index-BZJ_5FOX.d.mts.map} +1 -1
  66. package/dist/index.d.mts +2 -2
  67. package/dist/index.mjs +6 -6
  68. package/dist/media/local-runtime.d.mts +2 -2
  69. package/dist/plugin-utils.d.mts +2 -2
  70. package/dist/plugins/adapt-sandbox-entry.d.mts +2 -2
  71. package/dist/{query-BbJQ-dGg.mjs → query-Bz9bWZfm.mjs} +4 -4
  72. package/dist/{query-BbJQ-dGg.mjs.map → query-Bz9bWZfm.mjs.map} +1 -1
  73. package/dist/{relations-BKybBoOX.d.mts → relations-qKmSj9KM.d.mts} +16 -16
  74. package/dist/{relations-BKybBoOX.d.mts.map → relations-qKmSj9KM.d.mts.map} +1 -1
  75. package/dist/runtime.d.mts +2 -2
  76. package/dist/seed/index.mjs +1 -1
  77. package/dist/{taxonomies-rMeBzOjX.mjs → taxonomies-BN6M9Nrv.mjs} +2 -2
  78. package/dist/{taxonomies-rMeBzOjX.mjs.map → taxonomies-BN6M9Nrv.mjs.map} +1 -1
  79. package/dist/{taxonomies-HI9fnTdp.mjs → taxonomies-r7pqLBCn.mjs} +48 -19
  80. package/dist/taxonomies-r7pqLBCn.mjs.map +1 -0
  81. package/dist/version-BRMngh9I.mjs +7 -0
  82. package/dist/{version-Ci6bw6YK.mjs.map → version-BRMngh9I.mjs.map} +1 -1
  83. package/dist/{wxr-taxonomies-BmUnBNgz.mjs → wxr-taxonomies-DrihupaC.mjs} +2 -2
  84. package/dist/{wxr-taxonomies-BmUnBNgz.mjs.map → wxr-taxonomies-DrihupaC.mjs.map} +1 -1
  85. package/package.json +7 -7
  86. package/src/astro/prefetch.ts +7 -2
  87. package/src/components/widgets/Categories.astro +1 -1
  88. package/src/components/widgets/Tags.astro +1 -1
  89. package/src/taxonomies/index.ts +64 -23
  90. package/dist/taxonomies-HI9fnTdp.mjs.map +0 -1
  91. package/dist/version-Ci6bw6YK.mjs +0 -7
@@ -19,8 +19,8 @@ import "../../../../../../../seo-kJzVAmmu.mjs";
19
19
  import "../../../../../../../fts-manager-hyCU9OOp.mjs";
20
20
  import "../../../../../../../resolve-DFr4kq72.mjs";
21
21
  import "../../../../../../../loader-D4rigvjR.mjs";
22
- import "../../../../../../../taxonomies-HI9fnTdp.mjs";
23
- import { L as handleSchemaFieldUpdate, N as handleSchemaFieldDelete, P as handleSchemaFieldGet } from "../../../../../../../api-EibxGP-V.mjs";
22
+ import "../../../../../../../taxonomies-r7pqLBCn.mjs";
23
+ import { L as handleSchemaFieldUpdate, N as handleSchemaFieldDelete, P as handleSchemaFieldGet } from "../../../../../../../api-BkqFzIu4.mjs";
24
24
  import "../../../../../../../dashboard-CBm4zhc3.mjs";
25
25
  import "../../../../../../../content-refresh-CPMSauD3.mjs";
26
26
  import "../../../../../../../media-usage-DPQ3K4xB.mjs";
@@ -32,7 +32,7 @@ import "../../../../../../../seo-C3GDfT0V.mjs";
32
32
  import "../../../../../../../sections-CzVBAKjw.mjs";
33
33
  import "../../../../../../../settings-C2PaBS49.mjs";
34
34
  import "../../../../../../../settings-Dtc0DU31.mjs";
35
- import "../../../../../../../taxonomies-rMeBzOjX.mjs";
35
+ import "../../../../../../../taxonomies-BN6M9Nrv.mjs";
36
36
  import "../../../../../../../manifest-schema-bCq54i7F.mjs";
37
37
  import "../../../../../../../types-CK0LxPUQ.mjs";
38
38
  import "../../../../../../../ssrf-CviKqWmq.mjs";
@@ -19,8 +19,8 @@ import "../../../../../../../seo-kJzVAmmu.mjs";
19
19
  import "../../../../../../../fts-manager-hyCU9OOp.mjs";
20
20
  import "../../../../../../../resolve-DFr4kq72.mjs";
21
21
  import "../../../../../../../loader-D4rigvjR.mjs";
22
- import "../../../../../../../taxonomies-HI9fnTdp.mjs";
23
- import { F as handleSchemaFieldList, M as handleSchemaFieldCreate } from "../../../../../../../api-EibxGP-V.mjs";
22
+ import "../../../../../../../taxonomies-r7pqLBCn.mjs";
23
+ import { F as handleSchemaFieldList, M as handleSchemaFieldCreate } from "../../../../../../../api-BkqFzIu4.mjs";
24
24
  import "../../../../../../../dashboard-CBm4zhc3.mjs";
25
25
  import "../../../../../../../content-refresh-CPMSauD3.mjs";
26
26
  import "../../../../../../../media-usage-DPQ3K4xB.mjs";
@@ -32,7 +32,7 @@ import "../../../../../../../seo-C3GDfT0V.mjs";
32
32
  import "../../../../../../../sections-CzVBAKjw.mjs";
33
33
  import "../../../../../../../settings-C2PaBS49.mjs";
34
34
  import "../../../../../../../settings-Dtc0DU31.mjs";
35
- import "../../../../../../../taxonomies-rMeBzOjX.mjs";
35
+ import "../../../../../../../taxonomies-BN6M9Nrv.mjs";
36
36
  import "../../../../../../../manifest-schema-bCq54i7F.mjs";
37
37
  import "../../../../../../../types-CK0LxPUQ.mjs";
38
38
  import "../../../../../../../ssrf-CviKqWmq.mjs";
@@ -19,8 +19,8 @@ import "../../../../../../../seo-kJzVAmmu.mjs";
19
19
  import "../../../../../../../fts-manager-hyCU9OOp.mjs";
20
20
  import "../../../../../../../resolve-DFr4kq72.mjs";
21
21
  import "../../../../../../../loader-D4rigvjR.mjs";
22
- import "../../../../../../../taxonomies-HI9fnTdp.mjs";
23
- import { I as handleSchemaFieldReorder } from "../../../../../../../api-EibxGP-V.mjs";
22
+ import "../../../../../../../taxonomies-r7pqLBCn.mjs";
23
+ import { I as handleSchemaFieldReorder } from "../../../../../../../api-BkqFzIu4.mjs";
24
24
  import "../../../../../../../dashboard-CBm4zhc3.mjs";
25
25
  import "../../../../../../../content-refresh-CPMSauD3.mjs";
26
26
  import "../../../../../../../media-usage-DPQ3K4xB.mjs";
@@ -32,7 +32,7 @@ import "../../../../../../../seo-C3GDfT0V.mjs";
32
32
  import "../../../../../../../sections-CzVBAKjw.mjs";
33
33
  import "../../../../../../../settings-C2PaBS49.mjs";
34
34
  import "../../../../../../../settings-Dtc0DU31.mjs";
35
- import "../../../../../../../taxonomies-rMeBzOjX.mjs";
35
+ import "../../../../../../../taxonomies-BN6M9Nrv.mjs";
36
36
  import "../../../../../../../manifest-schema-bCq54i7F.mjs";
37
37
  import "../../../../../../../types-CK0LxPUQ.mjs";
38
38
  import "../../../../../../../ssrf-CviKqWmq.mjs";
@@ -19,8 +19,8 @@ import "../../../../../../seo-kJzVAmmu.mjs";
19
19
  import "../../../../../../fts-manager-hyCU9OOp.mjs";
20
20
  import "../../../../../../resolve-DFr4kq72.mjs";
21
21
  import "../../../../../../loader-D4rigvjR.mjs";
22
- import "../../../../../../taxonomies-HI9fnTdp.mjs";
23
- import { O as handleSchemaCollectionDelete, j as handleSchemaCollectionUpdate, k as handleSchemaCollectionGet } from "../../../../../../api-EibxGP-V.mjs";
22
+ import "../../../../../../taxonomies-r7pqLBCn.mjs";
23
+ import { O as handleSchemaCollectionDelete, j as handleSchemaCollectionUpdate, k as handleSchemaCollectionGet } from "../../../../../../api-BkqFzIu4.mjs";
24
24
  import "../../../../../../dashboard-CBm4zhc3.mjs";
25
25
  import "../../../../../../content-refresh-CPMSauD3.mjs";
26
26
  import "../../../../../../media-usage-DPQ3K4xB.mjs";
@@ -32,7 +32,7 @@ import "../../../../../../seo-C3GDfT0V.mjs";
32
32
  import "../../../../../../sections-CzVBAKjw.mjs";
33
33
  import "../../../../../../settings-C2PaBS49.mjs";
34
34
  import "../../../../../../settings-Dtc0DU31.mjs";
35
- import "../../../../../../taxonomies-rMeBzOjX.mjs";
35
+ import "../../../../../../taxonomies-BN6M9Nrv.mjs";
36
36
  import "../../../../../../manifest-schema-bCq54i7F.mjs";
37
37
  import "../../../../../../types-CK0LxPUQ.mjs";
38
38
  import "../../../../../../ssrf-CviKqWmq.mjs";
@@ -19,8 +19,8 @@ import "../../../../../seo-kJzVAmmu.mjs";
19
19
  import "../../../../../fts-manager-hyCU9OOp.mjs";
20
20
  import "../../../../../resolve-DFr4kq72.mjs";
21
21
  import "../../../../../loader-D4rigvjR.mjs";
22
- import "../../../../../taxonomies-HI9fnTdp.mjs";
23
- import { A as handleSchemaCollectionList, D as handleSchemaCollectionCreate } from "../../../../../api-EibxGP-V.mjs";
22
+ import "../../../../../taxonomies-r7pqLBCn.mjs";
23
+ import { A as handleSchemaCollectionList, D as handleSchemaCollectionCreate } from "../../../../../api-BkqFzIu4.mjs";
24
24
  import "../../../../../dashboard-CBm4zhc3.mjs";
25
25
  import "../../../../../content-refresh-CPMSauD3.mjs";
26
26
  import "../../../../../media-usage-DPQ3K4xB.mjs";
@@ -32,7 +32,7 @@ import "../../../../../seo-C3GDfT0V.mjs";
32
32
  import "../../../../../sections-CzVBAKjw.mjs";
33
33
  import "../../../../../settings-C2PaBS49.mjs";
34
34
  import "../../../../../settings-Dtc0DU31.mjs";
35
- import "../../../../../taxonomies-rMeBzOjX.mjs";
35
+ import "../../../../../taxonomies-BN6M9Nrv.mjs";
36
36
  import "../../../../../manifest-schema-bCq54i7F.mjs";
37
37
  import "../../../../../types-CK0LxPUQ.mjs";
38
38
  import "../../../../../ssrf-CviKqWmq.mjs";
@@ -19,8 +19,8 @@ import "../../../../../seo-kJzVAmmu.mjs";
19
19
  import "../../../../../fts-manager-hyCU9OOp.mjs";
20
20
  import "../../../../../resolve-DFr4kq72.mjs";
21
21
  import "../../../../../loader-D4rigvjR.mjs";
22
- import "../../../../../taxonomies-HI9fnTdp.mjs";
23
- import { E as handleOrphanedTableRegister } from "../../../../../api-EibxGP-V.mjs";
22
+ import "../../../../../taxonomies-r7pqLBCn.mjs";
23
+ import { E as handleOrphanedTableRegister } from "../../../../../api-BkqFzIu4.mjs";
24
24
  import "../../../../../dashboard-CBm4zhc3.mjs";
25
25
  import "../../../../../content-refresh-CPMSauD3.mjs";
26
26
  import "../../../../../media-usage-DPQ3K4xB.mjs";
@@ -32,7 +32,7 @@ import "../../../../../seo-C3GDfT0V.mjs";
32
32
  import "../../../../../sections-CzVBAKjw.mjs";
33
33
  import "../../../../../settings-C2PaBS49.mjs";
34
34
  import "../../../../../settings-Dtc0DU31.mjs";
35
- import "../../../../../taxonomies-rMeBzOjX.mjs";
35
+ import "../../../../../taxonomies-BN6M9Nrv.mjs";
36
36
  import "../../../../../manifest-schema-bCq54i7F.mjs";
37
37
  import "../../../../../types-CK0LxPUQ.mjs";
38
38
  import "../../../../../ssrf-CviKqWmq.mjs";
@@ -19,8 +19,8 @@ import "../../../../../seo-kJzVAmmu.mjs";
19
19
  import "../../../../../fts-manager-hyCU9OOp.mjs";
20
20
  import "../../../../../resolve-DFr4kq72.mjs";
21
21
  import "../../../../../loader-D4rigvjR.mjs";
22
- import "../../../../../taxonomies-HI9fnTdp.mjs";
23
- import { T as handleOrphanedTableList } from "../../../../../api-EibxGP-V.mjs";
22
+ import "../../../../../taxonomies-r7pqLBCn.mjs";
23
+ import { T as handleOrphanedTableList } from "../../../../../api-BkqFzIu4.mjs";
24
24
  import "../../../../../dashboard-CBm4zhc3.mjs";
25
25
  import "../../../../../content-refresh-CPMSauD3.mjs";
26
26
  import "../../../../../media-usage-DPQ3K4xB.mjs";
@@ -32,7 +32,7 @@ import "../../../../../seo-C3GDfT0V.mjs";
32
32
  import "../../../../../sections-CzVBAKjw.mjs";
33
33
  import "../../../../../settings-C2PaBS49.mjs";
34
34
  import "../../../../../settings-Dtc0DU31.mjs";
35
- import "../../../../../taxonomies-rMeBzOjX.mjs";
35
+ import "../../../../../taxonomies-BN6M9Nrv.mjs";
36
36
  import "../../../../../manifest-schema-bCq54i7F.mjs";
37
37
  import "../../../../../types-CK0LxPUQ.mjs";
38
38
  import "../../../../../ssrf-CviKqWmq.mjs";
@@ -4,7 +4,7 @@ import "../../../../../../options-BlmBHTvX.mjs";
4
4
  import { t as ErrorCode } from "../../../../../../errors-DtEXIQQV.mjs";
5
5
  import { a as unwrapResult, r as handleError, t as apiError } from "../../../../../../error-DJ98TRbQ.mjs";
6
6
  import "../../../../../../snapshot-BdpUJKD-.mjs";
7
- import { a as deleteBackupArchive, c as isValidArchiveName, r as BACKUP_STORAGE_PREFIX } from "../../../../../../backup-Qr9PaGUo.mjs";
7
+ import { a as deleteBackupArchive, c as isValidArchiveName, r as BACKUP_STORAGE_PREFIX } from "../../../../../../backup-DoW-H0r4.mjs";
8
8
  import { r as requirePerm } from "../../../../../../authorize-6WPZa1xZ.mjs";
9
9
 
10
10
  //#region src/astro/routes/api/settings/backups/archives/[name].ts
@@ -3,7 +3,7 @@ import "../../../../../../types-CrRUFdej.mjs";
3
3
  import "../../../../../../options-BlmBHTvX.mjs";
4
4
  import { a as unwrapResult, t as apiError } from "../../../../../../error-DJ98TRbQ.mjs";
5
5
  import "../../../../../../snapshot-BdpUJKD-.mjs";
6
- import { d as runBackupToStorage, s as getBackupSettings } from "../../../../../../backup-Qr9PaGUo.mjs";
6
+ import { d as runBackupToStorage, s as getBackupSettings } from "../../../../../../backup-DoW-H0r4.mjs";
7
7
  import { r as requirePerm } from "../../../../../../authorize-6WPZa1xZ.mjs";
8
8
 
9
9
  //#region src/astro/routes/api/settings/backups/archives/index.ts
@@ -4,7 +4,7 @@ import "../../../../../options-BlmBHTvX.mjs";
4
4
  import { t as ErrorCode } from "../../../../../errors-DtEXIQQV.mjs";
5
5
  import { r as handleError, t as apiError } from "../../../../../error-DJ98TRbQ.mjs";
6
6
  import "../../../../../snapshot-BdpUJKD-.mjs";
7
- import { i as archiveNameForDate, o as generateBackupJson } from "../../../../../backup-Qr9PaGUo.mjs";
7
+ import { i as archiveNameForDate, o as generateBackupJson } from "../../../../../backup-DoW-H0r4.mjs";
8
8
  import { r as requirePerm } from "../../../../../authorize-6WPZa1xZ.mjs";
9
9
 
10
10
  //#region src/astro/routes/api/settings/backups/export.ts
@@ -5,7 +5,7 @@ import { t as ErrorCode } from "../../../../../errors-DtEXIQQV.mjs";
5
5
  import { a as unwrapResult, n as apiSuccess, r as handleError, t as apiError } from "../../../../../error-DJ98TRbQ.mjs";
6
6
  import { n as parseBody, t as isParseError } from "../../../../../parse-53zyxlpv.mjs";
7
7
  import "../../../../../snapshot-BdpUJKD-.mjs";
8
- import { f as updateBackupSettings, l as listBackupArchives, n as BACKUP_RETENTION_MIN, s as getBackupSettings, t as BACKUP_RETENTION_MAX } from "../../../../../backup-Qr9PaGUo.mjs";
8
+ import { f as updateBackupSettings, l as listBackupArchives, n as BACKUP_RETENTION_MIN, s as getBackupSettings, t as BACKUP_RETENTION_MAX } from "../../../../../backup-DoW-H0r4.mjs";
9
9
  import { r as requirePerm } from "../../../../../authorize-6WPZa1xZ.mjs";
10
10
  import { z } from "zod";
11
11
 
@@ -22,7 +22,7 @@ import "../../../../ssrf-CviKqWmq.mjs";
22
22
  import { n as apiSuccess, r as handleError, t as apiError } from "../../../../error-DJ98TRbQ.mjs";
23
23
  import "../../../../ssrf-BB__xfNM.mjs";
24
24
  import { t as validateSeed } from "../../../../validate-CDT1jl6_.mjs";
25
- import { t as applySeed } from "../../../../apply-fGHCAwWN.mjs";
25
+ import { t as applySeed } from "../../../../apply-DuXsqll7.mjs";
26
26
  import { t as loadSeed } from "../../../../load-Dg5gtWma.mjs";
27
27
  import { n as getPublicOrigin } from "../../../../public-url-DSGTnJFw.mjs";
28
28
  import "../../../../api-tokens-Cvmixds7.mjs";
@@ -28,7 +28,7 @@ import "../../../../status-COfycGIU.mjs";
28
28
  import "../../../../api/schemas/index.mjs";
29
29
  import "../../../../ssrf-BB__xfNM.mjs";
30
30
  import { t as validateSeed } from "../../../../validate-CDT1jl6_.mjs";
31
- import { t as applySeed } from "../../../../apply-fGHCAwWN.mjs";
31
+ import { t as applySeed } from "../../../../apply-DuXsqll7.mjs";
32
32
  import { t as loadSeed } from "../../../../load-Dg5gtWma.mjs";
33
33
  import { n as getPublicOrigin } from "../../../../public-url-DSGTnJFw.mjs";
34
34
  import { t as getAuthMode } from "../../../../mode-fiXRMfeA.mjs";
@@ -7,8 +7,8 @@ import "../../../../../../../taxonomy-0mCI25HN.mjs";
7
7
  import "../../../../../../../request-cache-BSUptuJR.mjs";
8
8
  import "../../../../../../../resolve-DFr4kq72.mjs";
9
9
  import "../../../../../../../loader-D4rigvjR.mjs";
10
- import "../../../../../../../taxonomies-HI9fnTdp.mjs";
11
- import { a as handleTermGet, r as handleTermCreate, s as handleTermTranslations } from "../../../../../../../taxonomies-rMeBzOjX.mjs";
10
+ import "../../../../../../../taxonomies-r7pqLBCn.mjs";
11
+ import { a as handleTermGet, r as handleTermCreate, s as handleTermTranslations } from "../../../../../../../taxonomies-BN6M9Nrv.mjs";
12
12
  import { a as unwrapResult, i as requireDb, r as handleError, t as apiError } from "../../../../../../../error-DJ98TRbQ.mjs";
13
13
  import { i as parseQuery, n as parseBody, t as isParseError } from "../../../../../../../parse-53zyxlpv.mjs";
14
14
  import { z as localeFilterQuery } from "../../../../../../../media-BJ6SRY3g.mjs";
@@ -7,8 +7,8 @@ import "../../../../../../taxonomy-0mCI25HN.mjs";
7
7
  import "../../../../../../request-cache-BSUptuJR.mjs";
8
8
  import "../../../../../../resolve-DFr4kq72.mjs";
9
9
  import "../../../../../../loader-D4rigvjR.mjs";
10
- import "../../../../../../taxonomies-HI9fnTdp.mjs";
11
- import { a as handleTermGet, c as handleTermUpdate, i as handleTermDelete } from "../../../../../../taxonomies-rMeBzOjX.mjs";
10
+ import "../../../../../../taxonomies-r7pqLBCn.mjs";
11
+ import { a as handleTermGet, c as handleTermUpdate, i as handleTermDelete } from "../../../../../../taxonomies-BN6M9Nrv.mjs";
12
12
  import { a as unwrapResult, i as requireDb, r as handleError, t as apiError } from "../../../../../../error-DJ98TRbQ.mjs";
13
13
  import { i as parseQuery, n as parseBody, t as isParseError } from "../../../../../../parse-53zyxlpv.mjs";
14
14
  import { z as localeFilterQuery } from "../../../../../../media-BJ6SRY3g.mjs";
@@ -7,8 +7,8 @@ import "../../../../../../taxonomy-0mCI25HN.mjs";
7
7
  import "../../../../../../request-cache-BSUptuJR.mjs";
8
8
  import "../../../../../../resolve-DFr4kq72.mjs";
9
9
  import "../../../../../../loader-D4rigvjR.mjs";
10
- import "../../../../../../taxonomies-HI9fnTdp.mjs";
11
- import { o as handleTermList, r as handleTermCreate } from "../../../../../../taxonomies-rMeBzOjX.mjs";
10
+ import "../../../../../../taxonomies-r7pqLBCn.mjs";
11
+ import { o as handleTermList, r as handleTermCreate } from "../../../../../../taxonomies-BN6M9Nrv.mjs";
12
12
  import { a as unwrapResult, i as requireDb, r as handleError, t as apiError } from "../../../../../../error-DJ98TRbQ.mjs";
13
13
  import { i as parseQuery, n as parseBody, t as isParseError } from "../../../../../../parse-53zyxlpv.mjs";
14
14
  import { z as localeFilterQuery } from "../../../../../../media-BJ6SRY3g.mjs";
@@ -7,8 +7,8 @@ import "../../../../taxonomy-0mCI25HN.mjs";
7
7
  import "../../../../request-cache-BSUptuJR.mjs";
8
8
  import "../../../../resolve-DFr4kq72.mjs";
9
9
  import "../../../../loader-D4rigvjR.mjs";
10
- import "../../../../taxonomies-HI9fnTdp.mjs";
11
- import { n as handleTaxonomyList, t as handleTaxonomyCreate } from "../../../../taxonomies-rMeBzOjX.mjs";
10
+ import "../../../../taxonomies-r7pqLBCn.mjs";
11
+ import { n as handleTaxonomyList, t as handleTaxonomyCreate } from "../../../../taxonomies-BN6M9Nrv.mjs";
12
12
  import { a as unwrapResult, i as requireDb, r as handleError } from "../../../../error-DJ98TRbQ.mjs";
13
13
  import { i as parseQuery, n as parseBody, t as isParseError } from "../../../../parse-53zyxlpv.mjs";
14
14
  import { z as localeFilterQuery } from "../../../../media-BJ6SRY3g.mjs";
@@ -2,9 +2,9 @@ import { i as ContentItem } from "../types-BfYUPJdP.mjs";
2
2
  import { d as ContentListResponse, f as ContentResponse } from "../options-BqMkWfYa.mjs";
3
3
  import { t as Database } from "../types-DPKhI5ib.mjs";
4
4
  import { Et as ResolvedPlugin, St as PublicPageContext, Ut as Element, et as PageFragmentContribution, rt as PageMetadataContribution } from "../types-BvB7gDOD.mjs";
5
- import "../relations-BKybBoOX.mjs";
5
+ import "../relations-qKmSj9KM.mjs";
6
6
  import { f as MediaProvider, p as MediaProviderCapabilities } from "../placeholder-Dop47JMM.mjs";
7
- import { Er as MediaResponse, Tr as MediaListResponse, Yn as SandboxedPluginEntry, er as RouteMeta, gi as MediaItem, hr as EmDashConfig, nr as HookPipeline, tr as EmailPipeline, ur as SandboxRunner } from "../index-BcEH_FfS.mjs";
7
+ import { Er as MediaResponse, Tr as MediaListResponse, Yn as SandboxedPluginEntry, er as RouteMeta, gi as MediaItem, hr as EmDashConfig, nr as HookPipeline, tr as EmailPipeline, ur as SandboxRunner } from "../index-BZJ_5FOX.mjs";
8
8
  import "../runner-Dd8m8k9F.mjs";
9
9
  import "../index-BEui-cRd.mjs";
10
10
  import { d as Storage } from "../types-B97jg3aW.mjs";
@@ -1,6 +1,6 @@
1
1
  import { t as OptionsRepository } from "./options-BlmBHTvX.mjs";
2
2
  import { t as ErrorCode } from "./errors-DtEXIQQV.mjs";
3
- import { n as VERSION } from "./version-Ci6bw6YK.mjs";
3
+ import { n as VERSION } from "./version-BRMngh9I.mjs";
4
4
  import { t as generateSnapshot } from "./snapshot-BdpUJKD-.mjs";
5
5
 
6
6
  //#region src/api/handlers/backup.ts
@@ -238,4 +238,4 @@ async function maybeRunScheduledBackup(db, storage) {
238
238
 
239
239
  //#endregion
240
240
  export { deleteBackupArchive as a, isValidArchiveName as c, runBackupToStorage as d, updateBackupSettings as f, archiveNameForDate as i, listBackupArchives as l, BACKUP_RETENTION_MIN as n, generateBackupJson as o, BACKUP_STORAGE_PREFIX as r, getBackupSettings as s, BACKUP_RETENTION_MAX as t, maybeRunScheduledBackup as u };
241
- //# sourceMappingURL=backup-Qr9PaGUo.mjs.map
241
+ //# sourceMappingURL=backup-DoW-H0r4.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"backup-Qr9PaGUo.mjs","names":[],"sources":["../src/api/handlers/backup.ts"],"sourcesContent":["/**\n * Backup handlers — portable content backups, on demand and scheduled.\n *\n * A backup is the snapshot format (see `snapshot.ts`) wrapped in a small\n * envelope: all content (including drafts, scheduled, and trashed entries),\n * schema definitions, taxonomies, menus, widgets, revisions, media metadata,\n * and site settings.\n *\n * Deliberately NOT included:\n * - Users, sessions, credentials, API/OAuth tokens (auth data is neither\n * portable nor safe in a user-downloadable file)\n * - Secrets (`emdash:preview_secret`, plugin config, passkey challenges)\n * - Media binaries (metadata only — the files live in the same bucket the\n * scheduled archives are written to)\n *\n * For full point-in-time database recovery on Cloudflare, D1 Time Travel\n * covers the last 30 days out of the box; these backups complement it with\n * user-holdable, longer-lived archives.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { OptionsRepository } from \"../../database/repositories/options.js\";\nimport type { Database } from \"../../database/types.js\";\nimport type { Storage } from \"../../storage/types.js\";\nimport { VERSION } from \"../../version.js\";\nimport { ErrorCode } from \"../errors.js\";\nimport type { ApiResult } from \"../types.js\";\nimport { generateSnapshot } from \"./snapshot.js\";\n\n// ── Constants ───────────────────────────────────────────────────\n\n/** Storage key prefix for scheduled/manual archives. */\nexport const BACKUP_STORAGE_PREFIX = \"backups/\";\n\n/**\n * Filename prefix within the backups/ folder. Included in the list() prefix\n * so LocalStorage (which matches directory + filename prefix, not flat keys\n * like S3/R2) finds the archives too.\n */\nconst BACKUP_FILE_PREFIX = \"emdash-backup-\";\n\n/** Options key holding the scheduled-backup settings. */\nexport const BACKUP_SETTINGS_KEY = \"emdash:backups\";\n\n/** Options key holding the ISO timestamp of the last scheduled run. */\nconst BACKUP_LAST_RUN_KEY = \"emdash:backups_last_run\";\n\n/** Minimum interval between scheduled backups (23h — daily with cron jitter). */\nconst SCHEDULED_BACKUP_INTERVAL_MS = 23 * 60 * 60 * 1000;\n\n/** Retention bounds for stored archives. */\nexport const BACKUP_RETENTION_MIN = 1;\nexport const BACKUP_RETENTION_MAX = 30;\nconst BACKUP_RETENTION_DEFAULT = 7;\n\n/**\n * Options-table key prefixes included in backups. Site settings plus the\n * site-identity keys (`emdash:site_title`, `emdash:site_tagline`,\n * `emdash:site_url`). Never widen this to a prefix that can match secrets\n * (`emdash:preview_secret`, `plugin:`, `emdash:passkey_pending:`).\n */\nconst BACKUP_OPTION_PREFIXES = [\"site:\", \"emdash:site_\", \"emdash:locale\"];\n\n/**\n * Archive filename shape. Strict allowlist — the download/delete routes\n * interpolate this into a storage key, so it must never contain `/` or `..`.\n * The random suffix makes names unguessable (defense in depth on top of the\n * media route's backups/ deny) and avoids same-second collisions.\n */\nconst ARCHIVE_NAME_PATTERN =\n\t/^emdash-backup-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-[0-9a-f]{8}\\.json$/;\n\nexport function isValidArchiveName(name: string): boolean {\n\treturn ARCHIVE_NAME_PATTERN.test(name);\n}\n\n// ── Types ───────────────────────────────────────────────────────\n\nexport interface BackupSettings {\n\t/** Whether daily scheduled backups to storage are enabled. */\n\tenabled: boolean;\n\t/** How many archives to keep in storage (oldest pruned first). */\n\tretention: number;\n}\n\nexport interface BackupArchive {\n\t/** Filename within the backups/ prefix (no path separators). */\n\tname: string;\n\t/** Size in bytes. */\n\tsize: number;\n\t/** Last-modified timestamp (ISO). */\n\tlastModified: string;\n}\n\nconst DEFAULT_SETTINGS: BackupSettings = {\n\tenabled: false,\n\tretention: BACKUP_RETENTION_DEFAULT,\n};\n\nfunction clampRetention(value: number): number {\n\tif (!Number.isFinite(value)) return BACKUP_RETENTION_DEFAULT;\n\treturn Math.min(BACKUP_RETENTION_MAX, Math.max(BACKUP_RETENTION_MIN, Math.trunc(value)));\n}\n\n// ── Export ──────────────────────────────────────────────────────\n\n/**\n * Generate a full content backup as a JSON string.\n *\n * ponytail: the whole backup is materialized in memory. Fine for the sites\n * EmDash targets today; truly huge databases should use `wrangler d1 export`\n * (documented on the backups docs page). Upgrade path: stream table-by-table.\n */\nexport async function generateBackupJson(db: Kysely<Database>): Promise<string> {\n\tconst snapshot = await generateSnapshot(db, {\n\t\tincludeDrafts: true,\n\t\tincludeTrashed: true,\n\t\toptionPrefixes: BACKUP_OPTION_PREFIXES,\n\t});\n\n\treturn JSON.stringify({\n\t\tformat: \"emdash-backup\",\n\t\tformatVersion: 1,\n\t\temdashVersion: VERSION,\n\t\tgeneratedAt: snapshot.generatedAt,\n\t\tschema: snapshot.schema,\n\t\ttables: snapshot.tables,\n\t});\n}\n\n/** Derive the archive filename for a given date (plus a random suffix). */\nexport function archiveNameForDate(date: Date): string {\n\t// 2026-07-09T08:45:12.345Z → emdash-backup-2026-07-09T08-45-12-1a2b3c4d.json\n\tconst stamp = date.toISOString().slice(0, 19).replaceAll(\":\", \"-\");\n\tconst suffix = crypto.randomUUID().replaceAll(\"-\", \"\").slice(0, 8);\n\treturn `emdash-backup-${stamp}-${suffix}.json`;\n}\n\n// ── Settings ────────────────────────────────────────────────────\n\nexport async function getBackupSettings(db: Kysely<Database>): Promise<BackupSettings> {\n\tconst options = new OptionsRepository(db);\n\tconst stored = await options.get<Partial<BackupSettings>>(BACKUP_SETTINGS_KEY);\n\tif (!stored) return { ...DEFAULT_SETTINGS };\n\treturn {\n\t\tenabled: stored.enabled === true,\n\t\tretention: clampRetention(stored.retention ?? BACKUP_RETENTION_DEFAULT),\n\t};\n}\n\nexport async function updateBackupSettings(\n\tdb: Kysely<Database>,\n\tinput: { enabled: boolean; retention: number },\n): Promise<ApiResult<BackupSettings>> {\n\ttry {\n\t\tconst settings: BackupSettings = {\n\t\t\tenabled: input.enabled,\n\t\t\tretention: clampRetention(input.retention),\n\t\t};\n\t\tconst options = new OptionsRepository(db);\n\t\tawait options.set(BACKUP_SETTINGS_KEY, settings);\n\t\treturn { success: true, data: settings };\n\t} catch (error) {\n\t\tconsole.error(\"[backup] Failed to update settings:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: ErrorCode.BACKUP_SETTINGS_ERROR, message: \"Failed to update backup settings\" },\n\t\t};\n\t}\n}\n\n// ── Archives in storage ─────────────────────────────────────────\n\n/**\n * List stored archives, newest first.\n *\n * ponytail: single unpaginated list. The retention cap (max 30) bounds the\n * archive count, so one page always suffices.\n */\nexport async function listBackupArchives(storage: Storage): Promise<ApiResult<BackupArchive[]>> {\n\ttry {\n\t\tconst result = await storage.list({\n\t\t\tprefix: `${BACKUP_STORAGE_PREFIX}${BACKUP_FILE_PREFIX}`,\n\t\t\tlimit: 100,\n\t\t});\n\t\tconst archives = result.files\n\t\t\t.map((file) => ({\n\t\t\t\tname: file.key.slice(BACKUP_STORAGE_PREFIX.length),\n\t\t\t\tsize: file.size,\n\t\t\t\tlastModified: file.lastModified.toISOString(),\n\t\t\t}))\n\t\t\t.filter((archive) => isValidArchiveName(archive.name))\n\t\t\t.toSorted((a, b) => (a.name < b.name ? 1 : -1));\n\t\treturn { success: true, data: archives };\n\t} catch (error) {\n\t\tconsole.error(\"[backup] Failed to list archives:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: ErrorCode.BACKUP_LIST_ERROR, message: \"Failed to list backup archives\" },\n\t\t};\n\t}\n}\n\n/**\n * Create a backup and store it as an archive, then prune old archives\n * beyond `retention`.\n */\nexport async function runBackupToStorage(\n\tdb: Kysely<Database>,\n\tstorage: Storage,\n\tretention: number,\n): Promise<ApiResult<BackupArchive>> {\n\ttry {\n\t\tconst json = await generateBackupJson(db);\n\t\tconst name = archiveNameForDate(new Date());\n\t\tconst body = new TextEncoder().encode(json);\n\n\t\tawait storage.upload({\n\t\t\tkey: `${BACKUP_STORAGE_PREFIX}${name}`,\n\t\t\tbody,\n\t\t\tcontentType: \"application/json\",\n\t\t});\n\n\t\tawait pruneArchives(storage, clampRetention(retention));\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { name, size: body.byteLength, lastModified: new Date().toISOString() },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"[backup] Failed to create archive:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: ErrorCode.BACKUP_CREATE_ERROR, message: \"Failed to create backup archive\" },\n\t\t};\n\t}\n}\n\n/** Delete archives beyond the newest `keep`. Failures are logged, not fatal. */\nasync function pruneArchives(storage: Storage, keep: number): Promise<void> {\n\tconst listed = await listBackupArchives(storage);\n\tif (!listed.success) return;\n\n\tfor (const archive of listed.data.slice(keep)) {\n\t\ttry {\n\t\t\tawait storage.delete(`${BACKUP_STORAGE_PREFIX}${archive.name}`);\n\t\t} catch (error) {\n\t\t\tconsole.error(`[backup] Failed to prune archive ${archive.name}:`, error);\n\t\t}\n\t}\n}\n\nexport async function deleteBackupArchive(\n\tstorage: Storage,\n\tname: string,\n): Promise<ApiResult<{ deleted: true }>> {\n\tif (!isValidArchiveName(name)) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: ErrorCode.VALIDATION_ERROR, message: \"Invalid archive name\" },\n\t\t};\n\t}\n\ttry {\n\t\tawait storage.delete(`${BACKUP_STORAGE_PREFIX}${name}`);\n\t\treturn { success: true, data: { deleted: true } };\n\t} catch (error) {\n\t\tconsole.error(`[backup] Failed to delete archive ${name}:`, error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: ErrorCode.BACKUP_DELETE_ERROR, message: \"Failed to delete backup archive\" },\n\t\t};\n\t}\n}\n\n// ── Scheduled runs ──────────────────────────────────────────────\n\n/**\n * Run a scheduled backup if enabled and due. Called from the maintenance\n * tick alongside scheduled publishing and system cleanup — never from a\n * request. Never throws.\n *\n * ponytail: last-run bookkeeping is a plain read-then-write, so two isolates\n * ticking simultaneously could both back up. Worst case is a duplicate\n * archive that retention prunes; not worth a lock.\n */\nexport async function maybeRunScheduledBackup(\n\tdb: Kysely<Database>,\n\tstorage: Storage | undefined,\n): Promise<void> {\n\ttry {\n\t\tif (!storage) return;\n\n\t\tconst settings = await getBackupSettings(db);\n\t\tif (!settings.enabled) return;\n\n\t\tconst options = new OptionsRepository(db);\n\t\tconst lastRun = await options.get<string>(BACKUP_LAST_RUN_KEY);\n\t\tif (lastRun) {\n\t\t\tconst elapsed = Date.now() - Date.parse(lastRun);\n\t\t\tif (Number.isFinite(elapsed) && elapsed < SCHEDULED_BACKUP_INTERVAL_MS) return;\n\t\t}\n\n\t\tconst result = await runBackupToStorage(db, storage, settings.retention);\n\t\tif (result.success) {\n\t\t\tawait options.set(BACKUP_LAST_RUN_KEY, new Date().toISOString());\n\t\t\tconsole.log(`[backup] Scheduled backup stored: ${result.data.name}`);\n\t\t}\n\t} catch (error) {\n\t\tconsole.error(\"[backup] Scheduled backup failed:\", error);\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"backup-DoW-H0r4.mjs","names":[],"sources":["../src/api/handlers/backup.ts"],"sourcesContent":["/**\n * Backup handlers — portable content backups, on demand and scheduled.\n *\n * A backup is the snapshot format (see `snapshot.ts`) wrapped in a small\n * envelope: all content (including drafts, scheduled, and trashed entries),\n * schema definitions, taxonomies, menus, widgets, revisions, media metadata,\n * and site settings.\n *\n * Deliberately NOT included:\n * - Users, sessions, credentials, API/OAuth tokens (auth data is neither\n * portable nor safe in a user-downloadable file)\n * - Secrets (`emdash:preview_secret`, plugin config, passkey challenges)\n * - Media binaries (metadata only — the files live in the same bucket the\n * scheduled archives are written to)\n *\n * For full point-in-time database recovery on Cloudflare, D1 Time Travel\n * covers the last 30 days out of the box; these backups complement it with\n * user-holdable, longer-lived archives.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { OptionsRepository } from \"../../database/repositories/options.js\";\nimport type { Database } from \"../../database/types.js\";\nimport type { Storage } from \"../../storage/types.js\";\nimport { VERSION } from \"../../version.js\";\nimport { ErrorCode } from \"../errors.js\";\nimport type { ApiResult } from \"../types.js\";\nimport { generateSnapshot } from \"./snapshot.js\";\n\n// ── Constants ───────────────────────────────────────────────────\n\n/** Storage key prefix for scheduled/manual archives. */\nexport const BACKUP_STORAGE_PREFIX = \"backups/\";\n\n/**\n * Filename prefix within the backups/ folder. Included in the list() prefix\n * so LocalStorage (which matches directory + filename prefix, not flat keys\n * like S3/R2) finds the archives too.\n */\nconst BACKUP_FILE_PREFIX = \"emdash-backup-\";\n\n/** Options key holding the scheduled-backup settings. */\nexport const BACKUP_SETTINGS_KEY = \"emdash:backups\";\n\n/** Options key holding the ISO timestamp of the last scheduled run. */\nconst BACKUP_LAST_RUN_KEY = \"emdash:backups_last_run\";\n\n/** Minimum interval between scheduled backups (23h — daily with cron jitter). */\nconst SCHEDULED_BACKUP_INTERVAL_MS = 23 * 60 * 60 * 1000;\n\n/** Retention bounds for stored archives. */\nexport const BACKUP_RETENTION_MIN = 1;\nexport const BACKUP_RETENTION_MAX = 30;\nconst BACKUP_RETENTION_DEFAULT = 7;\n\n/**\n * Options-table key prefixes included in backups. Site settings plus the\n * site-identity keys (`emdash:site_title`, `emdash:site_tagline`,\n * `emdash:site_url`). Never widen this to a prefix that can match secrets\n * (`emdash:preview_secret`, `plugin:`, `emdash:passkey_pending:`).\n */\nconst BACKUP_OPTION_PREFIXES = [\"site:\", \"emdash:site_\", \"emdash:locale\"];\n\n/**\n * Archive filename shape. Strict allowlist — the download/delete routes\n * interpolate this into a storage key, so it must never contain `/` or `..`.\n * The random suffix makes names unguessable (defense in depth on top of the\n * media route's backups/ deny) and avoids same-second collisions.\n */\nconst ARCHIVE_NAME_PATTERN =\n\t/^emdash-backup-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-[0-9a-f]{8}\\.json$/;\n\nexport function isValidArchiveName(name: string): boolean {\n\treturn ARCHIVE_NAME_PATTERN.test(name);\n}\n\n// ── Types ───────────────────────────────────────────────────────\n\nexport interface BackupSettings {\n\t/** Whether daily scheduled backups to storage are enabled. */\n\tenabled: boolean;\n\t/** How many archives to keep in storage (oldest pruned first). */\n\tretention: number;\n}\n\nexport interface BackupArchive {\n\t/** Filename within the backups/ prefix (no path separators). */\n\tname: string;\n\t/** Size in bytes. */\n\tsize: number;\n\t/** Last-modified timestamp (ISO). */\n\tlastModified: string;\n}\n\nconst DEFAULT_SETTINGS: BackupSettings = {\n\tenabled: false,\n\tretention: BACKUP_RETENTION_DEFAULT,\n};\n\nfunction clampRetention(value: number): number {\n\tif (!Number.isFinite(value)) return BACKUP_RETENTION_DEFAULT;\n\treturn Math.min(BACKUP_RETENTION_MAX, Math.max(BACKUP_RETENTION_MIN, Math.trunc(value)));\n}\n\n// ── Export ──────────────────────────────────────────────────────\n\n/**\n * Generate a full content backup as a JSON string.\n *\n * ponytail: the whole backup is materialized in memory. Fine for the sites\n * EmDash targets today; truly huge databases should use `wrangler d1 export`\n * (documented on the backups docs page). Upgrade path: stream table-by-table.\n */\nexport async function generateBackupJson(db: Kysely<Database>): Promise<string> {\n\tconst snapshot = await generateSnapshot(db, {\n\t\tincludeDrafts: true,\n\t\tincludeTrashed: true,\n\t\toptionPrefixes: BACKUP_OPTION_PREFIXES,\n\t});\n\n\treturn JSON.stringify({\n\t\tformat: \"emdash-backup\",\n\t\tformatVersion: 1,\n\t\temdashVersion: VERSION,\n\t\tgeneratedAt: snapshot.generatedAt,\n\t\tschema: snapshot.schema,\n\t\ttables: snapshot.tables,\n\t});\n}\n\n/** Derive the archive filename for a given date (plus a random suffix). */\nexport function archiveNameForDate(date: Date): string {\n\t// 2026-07-09T08:45:12.345Z → emdash-backup-2026-07-09T08-45-12-1a2b3c4d.json\n\tconst stamp = date.toISOString().slice(0, 19).replaceAll(\":\", \"-\");\n\tconst suffix = crypto.randomUUID().replaceAll(\"-\", \"\").slice(0, 8);\n\treturn `emdash-backup-${stamp}-${suffix}.json`;\n}\n\n// ── Settings ────────────────────────────────────────────────────\n\nexport async function getBackupSettings(db: Kysely<Database>): Promise<BackupSettings> {\n\tconst options = new OptionsRepository(db);\n\tconst stored = await options.get<Partial<BackupSettings>>(BACKUP_SETTINGS_KEY);\n\tif (!stored) return { ...DEFAULT_SETTINGS };\n\treturn {\n\t\tenabled: stored.enabled === true,\n\t\tretention: clampRetention(stored.retention ?? BACKUP_RETENTION_DEFAULT),\n\t};\n}\n\nexport async function updateBackupSettings(\n\tdb: Kysely<Database>,\n\tinput: { enabled: boolean; retention: number },\n): Promise<ApiResult<BackupSettings>> {\n\ttry {\n\t\tconst settings: BackupSettings = {\n\t\t\tenabled: input.enabled,\n\t\t\tretention: clampRetention(input.retention),\n\t\t};\n\t\tconst options = new OptionsRepository(db);\n\t\tawait options.set(BACKUP_SETTINGS_KEY, settings);\n\t\treturn { success: true, data: settings };\n\t} catch (error) {\n\t\tconsole.error(\"[backup] Failed to update settings:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: ErrorCode.BACKUP_SETTINGS_ERROR, message: \"Failed to update backup settings\" },\n\t\t};\n\t}\n}\n\n// ── Archives in storage ─────────────────────────────────────────\n\n/**\n * List stored archives, newest first.\n *\n * ponytail: single unpaginated list. The retention cap (max 30) bounds the\n * archive count, so one page always suffices.\n */\nexport async function listBackupArchives(storage: Storage): Promise<ApiResult<BackupArchive[]>> {\n\ttry {\n\t\tconst result = await storage.list({\n\t\t\tprefix: `${BACKUP_STORAGE_PREFIX}${BACKUP_FILE_PREFIX}`,\n\t\t\tlimit: 100,\n\t\t});\n\t\tconst archives = result.files\n\t\t\t.map((file) => ({\n\t\t\t\tname: file.key.slice(BACKUP_STORAGE_PREFIX.length),\n\t\t\t\tsize: file.size,\n\t\t\t\tlastModified: file.lastModified.toISOString(),\n\t\t\t}))\n\t\t\t.filter((archive) => isValidArchiveName(archive.name))\n\t\t\t.toSorted((a, b) => (a.name < b.name ? 1 : -1));\n\t\treturn { success: true, data: archives };\n\t} catch (error) {\n\t\tconsole.error(\"[backup] Failed to list archives:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: ErrorCode.BACKUP_LIST_ERROR, message: \"Failed to list backup archives\" },\n\t\t};\n\t}\n}\n\n/**\n * Create a backup and store it as an archive, then prune old archives\n * beyond `retention`.\n */\nexport async function runBackupToStorage(\n\tdb: Kysely<Database>,\n\tstorage: Storage,\n\tretention: number,\n): Promise<ApiResult<BackupArchive>> {\n\ttry {\n\t\tconst json = await generateBackupJson(db);\n\t\tconst name = archiveNameForDate(new Date());\n\t\tconst body = new TextEncoder().encode(json);\n\n\t\tawait storage.upload({\n\t\t\tkey: `${BACKUP_STORAGE_PREFIX}${name}`,\n\t\t\tbody,\n\t\t\tcontentType: \"application/json\",\n\t\t});\n\n\t\tawait pruneArchives(storage, clampRetention(retention));\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { name, size: body.byteLength, lastModified: new Date().toISOString() },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"[backup] Failed to create archive:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: ErrorCode.BACKUP_CREATE_ERROR, message: \"Failed to create backup archive\" },\n\t\t};\n\t}\n}\n\n/** Delete archives beyond the newest `keep`. Failures are logged, not fatal. */\nasync function pruneArchives(storage: Storage, keep: number): Promise<void> {\n\tconst listed = await listBackupArchives(storage);\n\tif (!listed.success) return;\n\n\tfor (const archive of listed.data.slice(keep)) {\n\t\ttry {\n\t\t\tawait storage.delete(`${BACKUP_STORAGE_PREFIX}${archive.name}`);\n\t\t} catch (error) {\n\t\t\tconsole.error(`[backup] Failed to prune archive ${archive.name}:`, error);\n\t\t}\n\t}\n}\n\nexport async function deleteBackupArchive(\n\tstorage: Storage,\n\tname: string,\n): Promise<ApiResult<{ deleted: true }>> {\n\tif (!isValidArchiveName(name)) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: ErrorCode.VALIDATION_ERROR, message: \"Invalid archive name\" },\n\t\t};\n\t}\n\ttry {\n\t\tawait storage.delete(`${BACKUP_STORAGE_PREFIX}${name}`);\n\t\treturn { success: true, data: { deleted: true } };\n\t} catch (error) {\n\t\tconsole.error(`[backup] Failed to delete archive ${name}:`, error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: ErrorCode.BACKUP_DELETE_ERROR, message: \"Failed to delete backup archive\" },\n\t\t};\n\t}\n}\n\n// ── Scheduled runs ──────────────────────────────────────────────\n\n/**\n * Run a scheduled backup if enabled and due. Called from the maintenance\n * tick alongside scheduled publishing and system cleanup — never from a\n * request. Never throws.\n *\n * ponytail: last-run bookkeeping is a plain read-then-write, so two isolates\n * ticking simultaneously could both back up. Worst case is a duplicate\n * archive that retention prunes; not worth a lock.\n */\nexport async function maybeRunScheduledBackup(\n\tdb: Kysely<Database>,\n\tstorage: Storage | undefined,\n): Promise<void> {\n\ttry {\n\t\tif (!storage) return;\n\n\t\tconst settings = await getBackupSettings(db);\n\t\tif (!settings.enabled) return;\n\n\t\tconst options = new OptionsRepository(db);\n\t\tconst lastRun = await options.get<string>(BACKUP_LAST_RUN_KEY);\n\t\tif (lastRun) {\n\t\t\tconst elapsed = Date.now() - Date.parse(lastRun);\n\t\t\tif (Number.isFinite(elapsed) && elapsed < SCHEDULED_BACKUP_INTERVAL_MS) return;\n\t\t}\n\n\t\tconst result = await runBackupToStorage(db, storage, settings.retention);\n\t\tif (result.success) {\n\t\t\tawait options.set(BACKUP_LAST_RUN_KEY, new Date().toISOString());\n\t\t\tconsole.log(`[backup] Scheduled backup stored: ${result.data.name}`);\n\t\t}\n\t} catch (error) {\n\t\tconsole.error(\"[backup] Scheduled backup failed:\", error);\n\t}\n}\n"],"mappings":"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"}
@@ -217,7 +217,7 @@ async function getBylinesForEntries(collection, entries) {
217
217
  * @param options - Optional locale, ordering, status, and limit
218
218
  */
219
219
  async function getEntriesByByline(collection, byline, options = {}) {
220
- const { getEmDashCollection } = await import("./query-BbJQ-dGg.mjs").then((n) => n.o);
220
+ const { getEmDashCollection } = await import("./query-Bz9bWZfm.mjs").then((n) => n.o);
221
221
  const queryOptions = { where: { byline } };
222
222
  if (options.locale !== void 0) queryOptions.locale = options.locale;
223
223
  if (options.orderBy !== void 0) queryOptions.orderBy = options.orderBy;
@@ -229,4 +229,4 @@ async function getEntriesByByline(collection, byline, options = {}) {
229
229
 
230
230
  //#endregion
231
231
  export { invalidateBylineCache as a, getEntriesByByline as i, getByline as n, getBylineBySlug as r, bylines_exports as t };
232
- //# sourceMappingURL=bylines-FyZu6TbJ.mjs.map
232
+ //# sourceMappingURL=bylines-Bc-3xtpz.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"bylines-FyZu6TbJ.mjs","names":[],"sources":["../src/bylines/index.ts"],"sourcesContent":["/**\n * Runtime API for bylines\n *\n * Provides functions to query byline profiles and byline credits\n * associated with content entries. Follows the same pattern as\n * the taxonomies runtime API.\n *\n * i18n model (migration 040): byline rows are per-locale and share a\n * `translation_group`. Credits on `_emdash_content_bylines.byline_id` store\n * the translation_group, so a single credit spans every locale of a byline.\n *\n * Hydration is strict per locale: a credit at locale X renders iff a byline\n * row exists at locale X within the credited translation_group. There is no\n * read-time fallback. Mirrors `getEntryTerms` and the convention in PR #916.\n * Locale is passed in by callers — `query.ts` resolves it from the entry's\n * own `data.locale` for the runtime path.\n */\n\nimport { sql } from \"kysely\";\n\nimport { BylineRepository } from \"../database/repositories/byline.js\";\nimport type { BylineSummary, ContentBylineCredit } from \"../database/repositories/types.js\";\nimport { validateIdentifier } from \"../database/validate.js\";\nimport { resolveLocaleChain } from \"../i18n/resolve.js\";\nimport { getDb } from \"../loader.js\";\nimport { requestCached } from \"../request-cache.js\";\nimport { isMissingTableError } from \"../utils/db-errors.js\";\n\n/**\n * No-op — kept for API compatibility.\n *\n * Used to invalidate a worker-lifetime \"has any byline?\" probe. That\n * probe added a query on every cold isolate to save one query on sites\n * with zero bylines (i.e. the wrong tradeoff), so we dropped it. The\n * batch byline join below returns an empty map for empty sites at the\n * same cost as the probe, without the pre-check.\n */\nexport function invalidateBylineCache(): void {\n\t// Intentionally empty.\n}\n\n/**\n * Get a byline by ID.\n *\n * @example\n * ```ts\n * import { getByline } from \"emdash\";\n *\n * const byline = await getByline(\"01HXYZ...\");\n * if (byline) {\n * console.log(byline.displayName);\n * }\n * ```\n */\nexport async function getByline(id: string): Promise<BylineSummary | null> {\n\tconst db = await getDb();\n\tconst repo = new BylineRepository(db);\n\treturn repo.findById(id);\n}\n\n/**\n * Get a byline by slug.\n *\n * Standalone identity lookup (e.g. rendering an author profile page). Walks\n * the configured locale fallback chain — same pattern as `getMenu` and\n * `getTerm`, see PR #916. Returns the first match found, walking\n * `[requestedLocale, ...fallbacks, defaultLocale]` in order.\n *\n * Note: this is intentionally different from credit hydration on a content\n * entry (`getEntryBylines`), which is strict per locale with no fallback.\n * The distinction: identity lookups answer \"give me this byline\", and\n * falling back to another locale's display name is acceptable. Credit\n * hydration answers \"what should render on this entry\", where falling back\n * silently surfaces a stale-locale name and contradicts editorial intent.\n *\n * @example\n * ```ts\n * import { getBylineBySlug } from \"emdash\";\n *\n * const byline = await getBylineBySlug(\"jane-doe\", { locale: \"de-de\" });\n * if (byline) {\n * console.log(byline.displayName);\n * }\n * ```\n */\nexport async function getBylineBySlug(\n\tslug: string,\n\toptions?: { locale?: string },\n): Promise<BylineSummary | null> {\n\tconst chain = resolveLocaleChain(options?.locale);\n\tconst cacheKey = `byline-by-slug:${slug}:${chain.length > 0 ? chain.join(\",\") : \"*\"}`;\n\treturn requestCached(cacheKey, async () => {\n\t\tconst db = await getDb();\n\t\tconst repo = new BylineRepository(db);\n\n\t\tif (chain.length === 0) {\n\t\t\t// No i18n or no resolved locale — fall back to the repo's\n\t\t\t// \"lowest-locale-code\" deterministic match.\n\t\t\treturn repo.findBySlug(slug);\n\t\t}\n\n\t\tfor (const locale of chain) {\n\t\t\tconst row = await repo.findBySlug(slug, { locale });\n\t\t\tif (row) return row;\n\t\t}\n\t\treturn null;\n\t});\n}\n\n/**\n * Get byline credits for a single content entry.\n *\n * Strict per locale (post-migration 040): a credit renders iff a byline row\n * exists at the requested locale within the credited translation_group.\n * Callers wanting fallback behaviour apply it themselves. When `locale` is\n * omitted, returns every locale variant of every credit on the entry —\n * useful for admin tooling, not for end-user rendering.\n *\n * Internal: not re-exported from the `emdash` package entry point. Every\n * entry returned by `getEmDashCollection` / `getEmDashEntry` already has\n * `data.bylines` populated by `hydrateEntryBylines` (which uses the batch\n * helper `getBylinesForEntries` directly). Site code should read those\n * fields rather than calling this function.\n */\nexport async function getEntryBylines(\n\tcollection: string,\n\tentryId: string,\n\toptions?: { locale?: string },\n): Promise<ContentBylineCredit[]> {\n\tvalidateIdentifier(collection, \"collection\");\n\tconst db = await getDb();\n\tconst repo = new BylineRepository(db);\n\n\tconst localeOpt = options?.locale !== undefined ? { locale: options.locale } : undefined;\n\tconst explicit = await repo.getContentBylines(collection, entryId, localeOpt);\n\tif (explicit.length > 0) {\n\t\treturn explicit.map((c) => ({ ...c, source: \"explicit\" as const }));\n\t}\n\n\t// `primary_byline_id` is the explicit-credit sentinel: non-null\n\t// suppresses author fallback even when the credit doesn't resolve\n\t// at this locale.\n\tconst ctx = await getEntryContext(db, collection, entryId);\n\tif (ctx.primaryBylineId) return [];\n\n\tif (ctx.authorId) {\n\t\tconst fallback = await repo.findByUserId(ctx.authorId, localeOpt);\n\t\tif (fallback) {\n\t\t\treturn [{ byline: fallback, sortOrder: 0, roleLabel: null, source: \"inferred\" }];\n\t\t}\n\t}\n\n\treturn [];\n}\n\n/**\n * Entry reference for batch byline lookups. Passing `authorId`,\n * `primaryBylineId`, and `locale` in directly avoids a per-entry\n * `SELECT` against the content table during hydration.\n *\n * `primaryBylineId` is the explicit-credit sentinel — non-null suppresses\n * author fallback. `locale` drives the strict per-locale join.\n */\nexport interface BylineEntry {\n\tid: string;\n\tauthorId: string | null;\n\tprimaryBylineId?: string | null;\n\tlocale?: string | null;\n}\n\n/**\n * Batch-fetch byline credits for multiple content entries.\n *\n * Per-entry strict-locale hydration: entries are bucketed by `entry.locale`\n * and each bucket gets a single batched call to the strict-locale repo\n * method. Items with no `locale` field (legacy / single-locale installs)\n * share an unscoped bucket.\n *\n * Internal: consumed by `hydrateEntryBylines` in `query.ts` so that every\n * entry returned from `getEmDashCollection` / `getEmDashEntry` already has\n * `data.bylines` populated. Site code should rely on that eager hydration\n * rather than calling this directly -- this function is not re-exported\n * from the `emdash` package entry point.\n *\n * @param collection - The collection slug (e.g., \"posts\")\n * @param entries - Entry id + authorId + locale (each entry resolves at its own locale)\n * @returns Map from entry ID to array of byline credits\n */\nexport async function getBylinesForEntries(\n\tcollection: string,\n\tentries: BylineEntry[],\n): Promise<Map<string, ContentBylineCredit[]>> {\n\tvalidateIdentifier(collection, \"collection\");\n\tconst result = new Map<string, ContentBylineCredit[]>();\n\n\tfor (const { id } of entries) {\n\t\tresult.set(id, []);\n\t}\n\n\tif (entries.length === 0) {\n\t\treturn result;\n\t}\n\n\tconst db = await getDb();\n\tconst repo = new BylineRepository(db);\n\n\t// Bucket entries by locale so each bucket fires a single strict-locale\n\t// `getContentBylinesMany` call. Items with no locale field share a\n\t// bucket keyed by null (no `WHERE locale = ?` applied — legacy\n\t// pre-i18n shape).\n\tconst buckets = new Map<string | null, BylineEntry[]>();\n\tfor (const entry of entries) {\n\t\tconst key = entry.locale ?? null;\n\t\tconst bucket = buckets.get(key);\n\t\tif (bucket) bucket.push(entry);\n\t\telse buckets.set(key, [entry]);\n\t}\n\n\t// Sites with no bylines get an empty map back at the same cost as the\n\t// previous \"has any bylines\" probe, without the extra round-trip.\n\t// Pre-migration databases (bylines table missing) fall through to the\n\t// `isMissingTableError` catch below and return empty.\n\t//\n\t// Each bucket's `getContentBylinesMany` call uses `skipHydration: true`\n\t// so the per-bucket fetches return bylines with `customFields = {}`.\n\t// We then hydrate the union of returned bylines in a SINGLE batched\n\t// pass via `hydrateBylineCustomFields`. This keeps mixed-locale list\n\t// hydration at one batched group-shared query (and one batched\n\t// translatable query) per request, even when locale buckets reference\n\t// disjoint translation_groups — the strict reading of the Phase 3\n\t// query-count envelope.\n\tconst explicitByEntry = new Map<string, ContentBylineCredit[]>();\n\tconst entriesNeedingAuthorCheck: BylineEntry[] = [];\n\tconst hydrationTargets: BylineSummary[] = [];\n\tfor (const [locale, bucket] of buckets) {\n\t\tconst localeOpt = locale ? { locale, skipHydration: true } : { skipHydration: true };\n\t\tconst bucketIds = bucket.map((e) => e.id);\n\t\tlet bylinesMap;\n\t\ttry {\n\t\t\tbylinesMap = await repo.getContentBylinesMany(collection, bucketIds, localeOpt);\n\t\t} catch (error) {\n\t\t\tif (isMissingTableError(error)) return result;\n\t\t\tthrow error;\n\t\t}\n\t\tfor (const [id, list] of bylinesMap) {\n\t\t\texplicitByEntry.set(id, list);\n\t\t\tfor (const credit of list) hydrationTargets.push(credit.byline);\n\t\t}\n\n\t\tfor (const entry of bucket) {\n\t\t\tconst hasResolved = bylinesMap.has(entry.id) && bylinesMap.get(entry.id)!.length > 0;\n\t\t\tif (hasResolved) continue;\n\t\t\tif (entry.authorId) entriesNeedingAuthorCheck.push(entry);\n\t\t}\n\t}\n\n\t// Only entries without an explicit credit (primaryBylineId null) are\n\t// eligible for author fallback.\n\tconst fallbackByEntry = new Map<string, BylineSummary>();\n\tif (entriesNeedingAuthorCheck.length > 0) {\n\t\tconst authorBuckets = new Map<string | null, BylineEntry[]>();\n\t\tfor (const entry of entriesNeedingAuthorCheck) {\n\t\t\tif (entry.primaryBylineId) continue;\n\t\t\tconst key = entry.locale ?? null;\n\t\t\tconst bucket = authorBuckets.get(key);\n\t\t\tif (bucket) bucket.push(entry);\n\t\t\telse authorBuckets.set(key, [entry]);\n\t\t}\n\n\t\tfor (const [locale, bucket] of authorBuckets) {\n\t\t\tconst localeOpt: { locale?: string; skipHydration: true } = locale\n\t\t\t\t? { locale, skipHydration: true }\n\t\t\t\t: { skipHydration: true };\n\t\t\tconst authorIds = bucket.map((e) => e.authorId).filter((id): id is string => id !== null);\n\t\t\tconst uniqueAuthorIds = [...new Set(authorIds)];\n\t\t\tif (uniqueAuthorIds.length === 0) continue;\n\t\t\t// `skipHydration: true` returns bylines with `customFields = {}`\n\t\t\t// so the fallback path participates in the single batched\n\t\t\t// `hydrateBylineCustomFields` call below — keeping the query\n\t\t\t// envelope at \"+1 group-shared query per hydration pass\" even\n\t\t\t// when author bylines across locale buckets reference disjoint\n\t\t\t// translation_groups.\n\t\t\tconst authorBylineMap = await repo.findByUserIds(uniqueAuthorIds, localeOpt);\n\t\t\tfor (const entry of bucket) {\n\t\t\t\tif (!entry.authorId) continue;\n\t\t\t\tconst f = authorBylineMap.get(entry.authorId);\n\t\t\t\tif (f) {\n\t\t\t\t\tfallbackByEntry.set(entry.id, f);\n\t\t\t\t\thydrationTargets.push(f);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Single batched hydration over every byline returned from both the\n\t// per-bucket explicit-credit fetches AND the per-bucket author-\n\t// fallback fetches. One translatable query + one group-shared query\n\t// for the whole pass, regardless of bucket count or whether\n\t// translation_groups overlap across locales.\n\tif (hydrationTargets.length > 0) {\n\t\tawait repo.hydrateBylineCustomFields(hydrationTargets);\n\t}\n\n\tfor (const { id } of entries) {\n\t\tconst explicit = explicitByEntry.get(id);\n\t\tif (explicit && explicit.length > 0) {\n\t\t\tresult.set(\n\t\t\t\tid,\n\t\t\t\texplicit.map((c) => ({ ...c, source: \"explicit\" as const })),\n\t\t\t);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst fallback = fallbackByEntry.get(id);\n\t\tif (fallback) {\n\t\t\tresult.set(id, [{ byline: fallback, sortOrder: 0, roleLabel: null, source: \"inferred\" }]);\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Get content entries credited to a byline, in any credit position.\n *\n * Unlike filtering on the content table's `primary_byline_id` column (which\n * only finds entries where the byline is the first/primary credit), this\n * matches every explicit credit recorded in `_emdash_content_bylines`, so\n * co-authored entries where the byline is a secondary credit are included.\n *\n * `byline` is matched against the byline's `translation_group` (the value\n * stored on credits since migration 040), so a single credit spans every\n * locale variant of the byline. Pass `byline.translationGroup ?? byline.id`\n * from `getByline` / `getBylineBySlug`. An array matches any of the given\n * bylines (OR).\n *\n * The result respects the active locale, status, ordering, and eager\n * hydration of `getEmDashCollection`.\n *\n * @example\n * ```ts\n * import { getBylineBySlug, getEntriesByByline } from \"emdash\";\n *\n * const byline = await getBylineBySlug(\"jane-doe\");\n * if (byline) {\n * const posts = await getEntriesByByline(\"posts\", byline.translationGroup ?? byline.id, {\n * orderBy: { published_at: \"desc\" },\n * });\n * }\n * ```\n *\n * @param collection - The collection slug (e.g. \"posts\")\n * @param byline - A byline translation group, or an array of them (OR)\n * @param options - Optional locale, ordering, status, and limit\n */\nexport async function getEntriesByByline(\n\tcollection: string,\n\tbyline: string | string[],\n\toptions: {\n\t\tlocale?: string;\n\t\torderBy?: Record<string, \"asc\" | \"desc\">;\n\t\tstatus?: \"draft\" | \"published\" | \"archived\";\n\t\tlimit?: number;\n\t} = {},\n): Promise<Array<{ id: string; data: Record<string, unknown> }>> {\n\tconst { getEmDashCollection } = await import(\"../query.js\");\n\n\tconst queryOptions: Record<string, unknown> = {\n\t\twhere: { byline },\n\t};\n\tif (options.locale !== undefined) queryOptions.locale = options.locale;\n\tif (options.orderBy !== undefined) queryOptions.orderBy = options.orderBy;\n\tif (options.status !== undefined) queryOptions.status = options.status;\n\tif (options.limit !== undefined) queryOptions.limit = options.limit;\n\n\tconst { entries } = await getEmDashCollection(collection, queryOptions);\n\treturn entries;\n}\n\n/** Reads `author_id` + `primary_byline_id` for one entry in a single query. */\nasync function getEntryContext(\n\tdb: Awaited<ReturnType<typeof getDb>>,\n\tcollection: string,\n\tentryId: string,\n): Promise<{ authorId: string | null; primaryBylineId: string | null }> {\n\tvalidateIdentifier(collection, \"collection\");\n\tconst tableName = `ec_${collection}`;\n\n\tconst result = await sql<{\n\t\tauthor_id: string | null;\n\t\tprimary_byline_id: string | null;\n\t}>`\n\t\tSELECT author_id, primary_byline_id FROM ${sql.ref(tableName)}\n\t\tWHERE id = ${entryId}\n\t\tLIMIT 1\n\t`.execute(db);\n\n\tconst row = result.rows[0];\n\treturn {\n\t\tauthorId: row?.author_id ?? null,\n\t\tprimaryBylineId: row?.primary_byline_id ?? null,\n\t};\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"bylines-Bc-3xtpz.mjs","names":[],"sources":["../src/bylines/index.ts"],"sourcesContent":["/**\n * Runtime API for bylines\n *\n * Provides functions to query byline profiles and byline credits\n * associated with content entries. Follows the same pattern as\n * the taxonomies runtime API.\n *\n * i18n model (migration 040): byline rows are per-locale and share a\n * `translation_group`. Credits on `_emdash_content_bylines.byline_id` store\n * the translation_group, so a single credit spans every locale of a byline.\n *\n * Hydration is strict per locale: a credit at locale X renders iff a byline\n * row exists at locale X within the credited translation_group. There is no\n * read-time fallback. Mirrors `getEntryTerms` and the convention in PR #916.\n * Locale is passed in by callers — `query.ts` resolves it from the entry's\n * own `data.locale` for the runtime path.\n */\n\nimport { sql } from \"kysely\";\n\nimport { BylineRepository } from \"../database/repositories/byline.js\";\nimport type { BylineSummary, ContentBylineCredit } from \"../database/repositories/types.js\";\nimport { validateIdentifier } from \"../database/validate.js\";\nimport { resolveLocaleChain } from \"../i18n/resolve.js\";\nimport { getDb } from \"../loader.js\";\nimport { requestCached } from \"../request-cache.js\";\nimport { isMissingTableError } from \"../utils/db-errors.js\";\n\n/**\n * No-op — kept for API compatibility.\n *\n * Used to invalidate a worker-lifetime \"has any byline?\" probe. That\n * probe added a query on every cold isolate to save one query on sites\n * with zero bylines (i.e. the wrong tradeoff), so we dropped it. The\n * batch byline join below returns an empty map for empty sites at the\n * same cost as the probe, without the pre-check.\n */\nexport function invalidateBylineCache(): void {\n\t// Intentionally empty.\n}\n\n/**\n * Get a byline by ID.\n *\n * @example\n * ```ts\n * import { getByline } from \"emdash\";\n *\n * const byline = await getByline(\"01HXYZ...\");\n * if (byline) {\n * console.log(byline.displayName);\n * }\n * ```\n */\nexport async function getByline(id: string): Promise<BylineSummary | null> {\n\tconst db = await getDb();\n\tconst repo = new BylineRepository(db);\n\treturn repo.findById(id);\n}\n\n/**\n * Get a byline by slug.\n *\n * Standalone identity lookup (e.g. rendering an author profile page). Walks\n * the configured locale fallback chain — same pattern as `getMenu` and\n * `getTerm`, see PR #916. Returns the first match found, walking\n * `[requestedLocale, ...fallbacks, defaultLocale]` in order.\n *\n * Note: this is intentionally different from credit hydration on a content\n * entry (`getEntryBylines`), which is strict per locale with no fallback.\n * The distinction: identity lookups answer \"give me this byline\", and\n * falling back to another locale's display name is acceptable. Credit\n * hydration answers \"what should render on this entry\", where falling back\n * silently surfaces a stale-locale name and contradicts editorial intent.\n *\n * @example\n * ```ts\n * import { getBylineBySlug } from \"emdash\";\n *\n * const byline = await getBylineBySlug(\"jane-doe\", { locale: \"de-de\" });\n * if (byline) {\n * console.log(byline.displayName);\n * }\n * ```\n */\nexport async function getBylineBySlug(\n\tslug: string,\n\toptions?: { locale?: string },\n): Promise<BylineSummary | null> {\n\tconst chain = resolveLocaleChain(options?.locale);\n\tconst cacheKey = `byline-by-slug:${slug}:${chain.length > 0 ? chain.join(\",\") : \"*\"}`;\n\treturn requestCached(cacheKey, async () => {\n\t\tconst db = await getDb();\n\t\tconst repo = new BylineRepository(db);\n\n\t\tif (chain.length === 0) {\n\t\t\t// No i18n or no resolved locale — fall back to the repo's\n\t\t\t// \"lowest-locale-code\" deterministic match.\n\t\t\treturn repo.findBySlug(slug);\n\t\t}\n\n\t\tfor (const locale of chain) {\n\t\t\tconst row = await repo.findBySlug(slug, { locale });\n\t\t\tif (row) return row;\n\t\t}\n\t\treturn null;\n\t});\n}\n\n/**\n * Get byline credits for a single content entry.\n *\n * Strict per locale (post-migration 040): a credit renders iff a byline row\n * exists at the requested locale within the credited translation_group.\n * Callers wanting fallback behaviour apply it themselves. When `locale` is\n * omitted, returns every locale variant of every credit on the entry —\n * useful for admin tooling, not for end-user rendering.\n *\n * Internal: not re-exported from the `emdash` package entry point. Every\n * entry returned by `getEmDashCollection` / `getEmDashEntry` already has\n * `data.bylines` populated by `hydrateEntryBylines` (which uses the batch\n * helper `getBylinesForEntries` directly). Site code should read those\n * fields rather than calling this function.\n */\nexport async function getEntryBylines(\n\tcollection: string,\n\tentryId: string,\n\toptions?: { locale?: string },\n): Promise<ContentBylineCredit[]> {\n\tvalidateIdentifier(collection, \"collection\");\n\tconst db = await getDb();\n\tconst repo = new BylineRepository(db);\n\n\tconst localeOpt = options?.locale !== undefined ? { locale: options.locale } : undefined;\n\tconst explicit = await repo.getContentBylines(collection, entryId, localeOpt);\n\tif (explicit.length > 0) {\n\t\treturn explicit.map((c) => ({ ...c, source: \"explicit\" as const }));\n\t}\n\n\t// `primary_byline_id` is the explicit-credit sentinel: non-null\n\t// suppresses author fallback even when the credit doesn't resolve\n\t// at this locale.\n\tconst ctx = await getEntryContext(db, collection, entryId);\n\tif (ctx.primaryBylineId) return [];\n\n\tif (ctx.authorId) {\n\t\tconst fallback = await repo.findByUserId(ctx.authorId, localeOpt);\n\t\tif (fallback) {\n\t\t\treturn [{ byline: fallback, sortOrder: 0, roleLabel: null, source: \"inferred\" }];\n\t\t}\n\t}\n\n\treturn [];\n}\n\n/**\n * Entry reference for batch byline lookups. Passing `authorId`,\n * `primaryBylineId`, and `locale` in directly avoids a per-entry\n * `SELECT` against the content table during hydration.\n *\n * `primaryBylineId` is the explicit-credit sentinel — non-null suppresses\n * author fallback. `locale` drives the strict per-locale join.\n */\nexport interface BylineEntry {\n\tid: string;\n\tauthorId: string | null;\n\tprimaryBylineId?: string | null;\n\tlocale?: string | null;\n}\n\n/**\n * Batch-fetch byline credits for multiple content entries.\n *\n * Per-entry strict-locale hydration: entries are bucketed by `entry.locale`\n * and each bucket gets a single batched call to the strict-locale repo\n * method. Items with no `locale` field (legacy / single-locale installs)\n * share an unscoped bucket.\n *\n * Internal: consumed by `hydrateEntryBylines` in `query.ts` so that every\n * entry returned from `getEmDashCollection` / `getEmDashEntry` already has\n * `data.bylines` populated. Site code should rely on that eager hydration\n * rather than calling this directly -- this function is not re-exported\n * from the `emdash` package entry point.\n *\n * @param collection - The collection slug (e.g., \"posts\")\n * @param entries - Entry id + authorId + locale (each entry resolves at its own locale)\n * @returns Map from entry ID to array of byline credits\n */\nexport async function getBylinesForEntries(\n\tcollection: string,\n\tentries: BylineEntry[],\n): Promise<Map<string, ContentBylineCredit[]>> {\n\tvalidateIdentifier(collection, \"collection\");\n\tconst result = new Map<string, ContentBylineCredit[]>();\n\n\tfor (const { id } of entries) {\n\t\tresult.set(id, []);\n\t}\n\n\tif (entries.length === 0) {\n\t\treturn result;\n\t}\n\n\tconst db = await getDb();\n\tconst repo = new BylineRepository(db);\n\n\t// Bucket entries by locale so each bucket fires a single strict-locale\n\t// `getContentBylinesMany` call. Items with no locale field share a\n\t// bucket keyed by null (no `WHERE locale = ?` applied — legacy\n\t// pre-i18n shape).\n\tconst buckets = new Map<string | null, BylineEntry[]>();\n\tfor (const entry of entries) {\n\t\tconst key = entry.locale ?? null;\n\t\tconst bucket = buckets.get(key);\n\t\tif (bucket) bucket.push(entry);\n\t\telse buckets.set(key, [entry]);\n\t}\n\n\t// Sites with no bylines get an empty map back at the same cost as the\n\t// previous \"has any bylines\" probe, without the extra round-trip.\n\t// Pre-migration databases (bylines table missing) fall through to the\n\t// `isMissingTableError` catch below and return empty.\n\t//\n\t// Each bucket's `getContentBylinesMany` call uses `skipHydration: true`\n\t// so the per-bucket fetches return bylines with `customFields = {}`.\n\t// We then hydrate the union of returned bylines in a SINGLE batched\n\t// pass via `hydrateBylineCustomFields`. This keeps mixed-locale list\n\t// hydration at one batched group-shared query (and one batched\n\t// translatable query) per request, even when locale buckets reference\n\t// disjoint translation_groups — the strict reading of the Phase 3\n\t// query-count envelope.\n\tconst explicitByEntry = new Map<string, ContentBylineCredit[]>();\n\tconst entriesNeedingAuthorCheck: BylineEntry[] = [];\n\tconst hydrationTargets: BylineSummary[] = [];\n\tfor (const [locale, bucket] of buckets) {\n\t\tconst localeOpt = locale ? { locale, skipHydration: true } : { skipHydration: true };\n\t\tconst bucketIds = bucket.map((e) => e.id);\n\t\tlet bylinesMap;\n\t\ttry {\n\t\t\tbylinesMap = await repo.getContentBylinesMany(collection, bucketIds, localeOpt);\n\t\t} catch (error) {\n\t\t\tif (isMissingTableError(error)) return result;\n\t\t\tthrow error;\n\t\t}\n\t\tfor (const [id, list] of bylinesMap) {\n\t\t\texplicitByEntry.set(id, list);\n\t\t\tfor (const credit of list) hydrationTargets.push(credit.byline);\n\t\t}\n\n\t\tfor (const entry of bucket) {\n\t\t\tconst hasResolved = bylinesMap.has(entry.id) && bylinesMap.get(entry.id)!.length > 0;\n\t\t\tif (hasResolved) continue;\n\t\t\tif (entry.authorId) entriesNeedingAuthorCheck.push(entry);\n\t\t}\n\t}\n\n\t// Only entries without an explicit credit (primaryBylineId null) are\n\t// eligible for author fallback.\n\tconst fallbackByEntry = new Map<string, BylineSummary>();\n\tif (entriesNeedingAuthorCheck.length > 0) {\n\t\tconst authorBuckets = new Map<string | null, BylineEntry[]>();\n\t\tfor (const entry of entriesNeedingAuthorCheck) {\n\t\t\tif (entry.primaryBylineId) continue;\n\t\t\tconst key = entry.locale ?? null;\n\t\t\tconst bucket = authorBuckets.get(key);\n\t\t\tif (bucket) bucket.push(entry);\n\t\t\telse authorBuckets.set(key, [entry]);\n\t\t}\n\n\t\tfor (const [locale, bucket] of authorBuckets) {\n\t\t\tconst localeOpt: { locale?: string; skipHydration: true } = locale\n\t\t\t\t? { locale, skipHydration: true }\n\t\t\t\t: { skipHydration: true };\n\t\t\tconst authorIds = bucket.map((e) => e.authorId).filter((id): id is string => id !== null);\n\t\t\tconst uniqueAuthorIds = [...new Set(authorIds)];\n\t\t\tif (uniqueAuthorIds.length === 0) continue;\n\t\t\t// `skipHydration: true` returns bylines with `customFields = {}`\n\t\t\t// so the fallback path participates in the single batched\n\t\t\t// `hydrateBylineCustomFields` call below — keeping the query\n\t\t\t// envelope at \"+1 group-shared query per hydration pass\" even\n\t\t\t// when author bylines across locale buckets reference disjoint\n\t\t\t// translation_groups.\n\t\t\tconst authorBylineMap = await repo.findByUserIds(uniqueAuthorIds, localeOpt);\n\t\t\tfor (const entry of bucket) {\n\t\t\t\tif (!entry.authorId) continue;\n\t\t\t\tconst f = authorBylineMap.get(entry.authorId);\n\t\t\t\tif (f) {\n\t\t\t\t\tfallbackByEntry.set(entry.id, f);\n\t\t\t\t\thydrationTargets.push(f);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Single batched hydration over every byline returned from both the\n\t// per-bucket explicit-credit fetches AND the per-bucket author-\n\t// fallback fetches. One translatable query + one group-shared query\n\t// for the whole pass, regardless of bucket count or whether\n\t// translation_groups overlap across locales.\n\tif (hydrationTargets.length > 0) {\n\t\tawait repo.hydrateBylineCustomFields(hydrationTargets);\n\t}\n\n\tfor (const { id } of entries) {\n\t\tconst explicit = explicitByEntry.get(id);\n\t\tif (explicit && explicit.length > 0) {\n\t\t\tresult.set(\n\t\t\t\tid,\n\t\t\t\texplicit.map((c) => ({ ...c, source: \"explicit\" as const })),\n\t\t\t);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst fallback = fallbackByEntry.get(id);\n\t\tif (fallback) {\n\t\t\tresult.set(id, [{ byline: fallback, sortOrder: 0, roleLabel: null, source: \"inferred\" }]);\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Get content entries credited to a byline, in any credit position.\n *\n * Unlike filtering on the content table's `primary_byline_id` column (which\n * only finds entries where the byline is the first/primary credit), this\n * matches every explicit credit recorded in `_emdash_content_bylines`, so\n * co-authored entries where the byline is a secondary credit are included.\n *\n * `byline` is matched against the byline's `translation_group` (the value\n * stored on credits since migration 040), so a single credit spans every\n * locale variant of the byline. Pass `byline.translationGroup ?? byline.id`\n * from `getByline` / `getBylineBySlug`. An array matches any of the given\n * bylines (OR).\n *\n * The result respects the active locale, status, ordering, and eager\n * hydration of `getEmDashCollection`.\n *\n * @example\n * ```ts\n * import { getBylineBySlug, getEntriesByByline } from \"emdash\";\n *\n * const byline = await getBylineBySlug(\"jane-doe\");\n * if (byline) {\n * const posts = await getEntriesByByline(\"posts\", byline.translationGroup ?? byline.id, {\n * orderBy: { published_at: \"desc\" },\n * });\n * }\n * ```\n *\n * @param collection - The collection slug (e.g. \"posts\")\n * @param byline - A byline translation group, or an array of them (OR)\n * @param options - Optional locale, ordering, status, and limit\n */\nexport async function getEntriesByByline(\n\tcollection: string,\n\tbyline: string | string[],\n\toptions: {\n\t\tlocale?: string;\n\t\torderBy?: Record<string, \"asc\" | \"desc\">;\n\t\tstatus?: \"draft\" | \"published\" | \"archived\";\n\t\tlimit?: number;\n\t} = {},\n): Promise<Array<{ id: string; data: Record<string, unknown> }>> {\n\tconst { getEmDashCollection } = await import(\"../query.js\");\n\n\tconst queryOptions: Record<string, unknown> = {\n\t\twhere: { byline },\n\t};\n\tif (options.locale !== undefined) queryOptions.locale = options.locale;\n\tif (options.orderBy !== undefined) queryOptions.orderBy = options.orderBy;\n\tif (options.status !== undefined) queryOptions.status = options.status;\n\tif (options.limit !== undefined) queryOptions.limit = options.limit;\n\n\tconst { entries } = await getEmDashCollection(collection, queryOptions);\n\treturn entries;\n}\n\n/** Reads `author_id` + `primary_byline_id` for one entry in a single query. */\nasync function getEntryContext(\n\tdb: Awaited<ReturnType<typeof getDb>>,\n\tcollection: string,\n\tentryId: string,\n): Promise<{ authorId: string | null; primaryBylineId: string | null }> {\n\tvalidateIdentifier(collection, \"collection\");\n\tconst tableName = `ec_${collection}`;\n\n\tconst result = await sql<{\n\t\tauthor_id: string | null;\n\t\tprimary_byline_id: string | null;\n\t}>`\n\t\tSELECT author_id, primary_byline_id FROM ${sql.ref(tableName)}\n\t\tWHERE id = ${entryId}\n\t\tLIMIT 1\n\t`.execute(db);\n\n\tconst row = result.rows[0];\n\treturn {\n\t\tauthorId: row?.author_id ?? null,\n\t\tprimaryBylineId: row?.primary_byline_id ?? null,\n\t};\n}\n"],"mappings":"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"}
@@ -30,7 +30,7 @@ import { i as isDeprecatedCapability, n as capabilitiesToDeclaredAccess, t as CA
30
30
  import "../ssrf-CviKqWmq.mjs";
31
31
  import "../ssrf-BB__xfNM.mjs";
32
32
  import { t as validateSeed } from "../validate-CDT1jl6_.mjs";
33
- import { t as applySeed } from "../apply-fGHCAwWN.mjs";
33
+ import { t as applySeed } from "../apply-DuXsqll7.mjs";
34
34
  import { n as fingerprintKey, r as generateEncryptionKey, t as EmDashSecretsError } from "../secrets-CQ-69kF9.mjs";
35
35
  import { LocalStorage } from "../storage/local.mjs";
36
36
  import { t as convertDataForRead } from "../portable-text-sDj92Kxv.mjs";
@@ -4744,8 +4744,8 @@ declare const pluginManifestSchema: z$1.ZodObject<{
4744
4744
  string: "string";
4745
4745
  number: "number";
4746
4746
  boolean: "boolean";
4747
- file: "file";
4748
4747
  slug: "slug";
4748
+ file: "file";
4749
4749
  image: "image";
4750
4750
  datetime: "datetime";
4751
4751
  text: "text";
@@ -5649,6 +5649,14 @@ interface UpdateTermInput {
5649
5649
  interface TaxonomyQueryOptions {
5650
5650
  locale?: string;
5651
5651
  }
5652
+ interface TaxonomyTermsOptions extends TaxonomyQueryOptions {
5653
+ /**
5654
+ * Populate each term's `count`. Counts aggregate the whole
5655
+ * `content_taxonomies` pivot against every declared collection, so callers
5656
+ * that don't render a count should opt out. Defaults to `true`.
5657
+ */
5658
+ includeCounts?: boolean;
5659
+ }
5652
5660
  /** Invalidate cached taxonomy term data and any content that hydrates terms. */
5653
5661
  declare function invalidateTermCache(): void;
5654
5662
  /**
@@ -5675,8 +5683,15 @@ declare function getTaxonomyDef(name: string, options?: TaxonomyQueryOptions): P
5675
5683
  /**
5676
5684
  * All terms of a taxonomy in a specific locale (flat for non-hierarchical,
5677
5685
  * tree for hierarchical).
5686
+ *
5687
+ * The term list and the visible-entry counts are loaded and cached separately:
5688
+ * the list depends only on the taxonomy epoch, while the counts additionally
5689
+ * depend on every counted collection's content epoch and cost an aggregate over
5690
+ * the whole assignment pivot. Callers that don't render counts pass
5691
+ * `includeCounts: false` and skip that aggregate entirely, while still sharing
5692
+ * the term list with callers that do.
5678
5693
  */
5679
- declare function getTaxonomyTerms(taxonomyName: string, options?: TaxonomyQueryOptions): Promise<TaxonomyTerm[]>;
5694
+ declare function getTaxonomyTerms(taxonomyName: string, options?: TaxonomyTermsOptions): Promise<TaxonomyTerm[]>;
5680
5695
  /**
5681
5696
  * Get a single term by (taxonomy, slug). Honours the fallback chain — if the
5682
5697
  * slug exists in a fallback locale, we return that row (useful for deep-linking
@@ -6152,4 +6167,4 @@ declare function extractPlainText(blocks: PortableTextBlock[] | string | null |
6152
6167
  declare function extractSearchableFields(entry: Record<string, unknown>, fields: string[]): Record<string, string>;
6153
6168
  //#endregion
6154
6169
  export { ReorderMenuItemsInput as $, PluginRouteError as $n, handleContentSchedule as $r, ContentEntry as $t, getEntriesByTerm as A, PortableTextCodeBlock as An, handleMediaList as Ar, pluginManifestSchema as At, TaxonomyTerm as B, ProseMirrorNode as Bn, handleContentCountScheduled as Br, invalidateMenuObjectCache as Bt, ReorderWidgetsInput as C, getDb as Cn, SchemaRegistry as Cr, NoopSandboxRunner as Ct, WidgetComponentDef as D, hashString as Dn, handleMediaCreate as Dr, PluginManager as Dt, WidgetArea as E, computeContentHash as En, MediaResponse as Er, NodeCronScheduler as Et, getTerm as F, PortableTextSpan as Fn, handleRevisionList as Fr, contentNamespace as Ft, getEntriesByByline as G, Section as Gn, handleContentDuplicate as Gr, getFallbackChain as Gt, UpdateTermInput as H, getSections as Hn, handleContentCreate as Hr, invalidateSchemaObjectCache as Ht, getTermsForEntries as I, PortableTextTextBlock as In, handleRevisionRestore as Ir, contentNamespaces as It, CreateMenuInput as J, EmDashRuntime as Jn, handleContentList as Jr, WaitUntilFn as Jt, getMenu as K, SectionSource as Kn, handleContentGet as Kr, getI18nConfig as Kt, invalidateTermCache as L, PortableTextUnknownBlock as Ln, generateManifest as Lr, invalidateBylineObjectCache as Lt, getTaxonomyDef as M, PortableTextImageBlock as Mn, RevisionListResponse as Mr, CacheNamespace as Mt, getTaxonomyDefs as N, PortableTextLinkMark as Nn, RevisionResponse as Nr, CachedQueryOptions as Nt, WidgetType as O, portableTextToProsemirror as On, handleMediaDelete as Or, createPluginManager as Ot, getTaxonomyTerms as P, PortableTextMarkDef as Pn, handleRevisionGet as Pr, cachedQuery as Pt, MenuItemType as Q, SystemCleanupFn as Qn, handleContentRestore as Qr, CollectionResult as Qt, CreateTermInput as R, ProseMirrorDocument as Rn, handleContentAuthors as Rr, invalidateCollectionCache as Rt, PropDef as S, emdashLoader as Sn, SchemaError as Sr, verifyPreviewToken as St, Widget as T, sanitizeHref as Tn, MediaListResponse as Tr, createNoopSandboxRunner as Tt, getByline as U, CreateSectionInput as Un, handleContentDelete as Ur, invalidateTaxonomyObjectCache as Ut, TaxonomyTermRow as V, getSection as Vn, handleContentCountTrashed as Vr, invalidateObjectCache as Vt, getBylineBySlug as W, GetSectionsOptions as Wn, handleContentDiscardDraft as Wr, I18nConfig as Wt, Menu as X, PublishedRef as Xn, handleContentPermanentDelete as Xr, CacheHint as Xt, CreateMenuItemInput as Y, SandboxedPluginEntry as Yn, handleContentListTrashed as Yr, after as Yt, MenuItem as Z, CronScheduler as Zn, handleContentPublish as Zr, CollectionFilter as Zt, getWidgetArea as _, MediaRepository as _i, CollectionFilter$1 as _n, getStoredConfig as _r, PreviewTokenPayload as _t, search as a, reference as ai, TranslationSummary as an, createHttpAccess as ar, getComments as at, CreateWidgetAreaInput as b, EmDashDatabaseError as bi, WhereRange as bn, StorageDescriptor as br, generatePreviewToken as bt, FTSManager as c, FieldDefinition as ci, getEmDashCollection as cn, SandboxEmailSendCallback as cr, getSiteSetting as ct, SearchOptions as d, ImageValue as di, resolveEmDashPath as dn, SandboxRunnerFactory as dr, getPreviewToken as dt, handleContentTranslations as ei, EditFieldMeta as en, RouteMeta as er, UpdateMenuInput as et, SearchResponse as f, PortableTextBlock$1 as fi, CMSAnnotation as fn, SandboxUnavailableError as fr, isPreviewRequest as ft, Suggestion as g, MediaItem as gi, createNoop as gn, PluginDescriptor as gr, GeneratePreviewTokenOptions as gt, SuggestOptions as h, CreateMediaInput as hi, createEditable as hn, EmDashConfig as hr, getPreviewUrl as ht, getSuggestions as i, portableText as ii, ResolvePathResult as in, createHookPipeline as ir, getCommentCount as it, getEntryTerms as j, PortableTextHtmlBlock as jn, handleMediaUpdate as jr, definePlugin as jt, getAllTermsForEntries as k, prosemirrorToPortableText as kn, handleMediaGet as kr, ValidatedPluginManifest as kt, CollectionSearchOptions as l, FieldUIHints as li, getEmDashEntry as ln, SandboxOptions as lr, getSiteSettings as lt, SearchStats as m, UserRepository as mi, FieldAnnotation as mn, SerializedRequest as mr, buildPreviewUrl as mt, extractSearchableFields as n, handleContentUnschedule as ni, EntryResult as nn, HookPipeline as nr, GetCommentsOptions as nt, searchCollection as o, file as oi, TranslationsResult as on, createUnrestrictedHttpAccess as or, getPluginSetting as ot, SearchResult as p, PluginStorageRepository as pi, EditProxy as pn, SandboxedPluginInstance as pr, GetPreviewUrlOptions as pt, getMenus as q, UpdateSectionInput as qn, handleContentGetIncludingTrashed as qr, isI18nEnabled as qt, getSearchStats as r, handleContentUpdate as ri, InferCollectionData as rn, HookResult as rr, GetCommentsResult as rt, searchWithDb as s, image as si, getEditMeta as sn, SandboxEmailMessage as sr, getPluginSettings as st, extractPlainText as t, handleContentUnpublish as ti, EmDashCollections as tn, EmailPipeline as tr, UpdateMenuItemInput as tt, SearchConfig as u, FileValue as ui, getTranslations as un, SandboxRunner as ur, setSiteSettings as ut, getWidgetAreas as v, ContentRepository as vi, EntryData as vn, LocalStorageConfig as vr, VerifyPreviewTokenOptions as vt, UpdateWidgetInput as w, isSafeHref as wn, markContentMediaUsageCollectionStaleSafely as wr, SandboxNotAvailableError as wt, CreateWidgetInput as x, WhereValue as xn, getCollectionInfo as xr, parseContentId as xt, getWidgetComponents as y, DatabaseConfig as yi, EntryFilter as yn, S3StorageConfig as yr, VerifyPreviewTokenResult as yt, TaxonomyDef as z, ProseMirrorMark as zn, handleContentCompare as zr, invalidateCommentObjectCache as zt };
6155
- //# sourceMappingURL=index-BcEH_FfS.d.mts.map
6170
+ //# sourceMappingURL=index-BZJ_5FOX.d.mts.map