@shapio/cms 0.4.1 → 0.5.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 (270) hide show
  1. package/dist/admin/assets/{AssetTile-CoNClUYS.js → AssetTile-B8ftUG4s.js} +2 -2
  2. package/dist/admin/assets/{AssetTile-CoNClUYS.js.map → AssetTile-B8ftUG4s.js.map} +1 -1
  3. package/dist/admin/assets/{AssistError-BoejyShH.js → AssistError-BidlzaBY.js} +2 -2
  4. package/dist/admin/assets/{AssistError-BoejyShH.js.map → AssistError-BidlzaBY.js.map} +1 -1
  5. package/dist/admin/assets/{BlockMenuItems-B2Jugisa.js → BlockMenuItems-DfNBHqVM.js} +2 -2
  6. package/dist/admin/assets/{BlockMenuItems-B2Jugisa.js.map → BlockMenuItems-DfNBHqVM.js.map} +1 -1
  7. package/dist/admin/assets/{Body-D3GwLGaB.js → Body-C0oQSX6L.js} +2 -2
  8. package/dist/admin/assets/{Body-D3GwLGaB.js.map → Body-C0oQSX6L.js.map} +1 -1
  9. package/dist/admin/assets/{Builder-CmT5-8hI.js → Builder-Z_hcyzNW.js} +2 -2
  10. package/dist/admin/assets/{Builder-CmT5-8hI.js.map → Builder-Z_hcyzNW.js.map} +1 -1
  11. package/dist/admin/assets/{ConfirmDialog-HS8Gc2t3.js → ConfirmDialog-DVN-n-tE.js} +2 -2
  12. package/dist/admin/assets/{ConfirmDialog-HS8Gc2t3.js.map → ConfirmDialog-DVN-n-tE.js.map} +1 -1
  13. package/dist/admin/assets/{CopyButton-DS-bAA23.js → CopyButton--tHegXQr.js} +2 -2
  14. package/dist/admin/assets/{CopyButton-DS-bAA23.js.map → CopyButton--tHegXQr.js.map} +1 -1
  15. package/dist/admin/assets/{CursorPager-C6PyD3wE.js → CursorPager-Cb94mITn.js} +2 -2
  16. package/dist/admin/assets/{CursorPager-C6PyD3wE.js.map → CursorPager-Cb94mITn.js.map} +1 -1
  17. package/dist/admin/assets/{ExternalLink-KdO72pTj.js → ExternalLink-M1cb1n2n.js} +2 -2
  18. package/dist/admin/assets/{ExternalLink-KdO72pTj.js.map → ExternalLink-M1cb1n2n.js.map} +1 -1
  19. package/dist/admin/assets/{FocalPoint-C62fRhmM.js → FocalPoint-pTq17ngi.js} +2 -2
  20. package/dist/admin/assets/{FocalPoint-C62fRhmM.js.map → FocalPoint-pTq17ngi.js.map} +1 -1
  21. package/dist/admin/assets/{Form-CLEr5Csd.js → Form-4zah109J.js} +2 -2
  22. package/dist/admin/assets/{Form-CLEr5Csd.js.map → Form-4zah109J.js.map} +1 -1
  23. package/dist/admin/assets/FormCheckboxGroup-DAeuGKAU.js +2 -0
  24. package/dist/admin/assets/FormCheckboxGroup-DAeuGKAU.js.map +1 -0
  25. package/dist/admin/assets/{FormRadioGroup-BjE1Jhar.js → FormRadioGroup--xx08rw6.js} +2 -2
  26. package/dist/admin/assets/{FormRadioGroup-BjE1Jhar.js.map → FormRadioGroup--xx08rw6.js.map} +1 -1
  27. package/dist/admin/assets/{FormSelectField-g4zrUlhv.js → FormSelectField-DNOCQKqc.js} +2 -2
  28. package/dist/admin/assets/{FormSelectField-g4zrUlhv.js.map → FormSelectField-DNOCQKqc.js.map} +1 -1
  29. package/dist/admin/assets/{FormSwitchField-gciThNGV.js → FormSwitchField-BECl-pzH.js} +2 -2
  30. package/dist/admin/assets/{FormSwitchField-gciThNGV.js.map → FormSwitchField-BECl-pzH.js.map} +1 -1
  31. package/dist/admin/assets/{FormTextareaField-tsS-UXym.js → FormTextareaField-DmRuKDJ_.js} +2 -2
  32. package/dist/admin/assets/{FormTextareaField-tsS-UXym.js.map → FormTextareaField-DmRuKDJ_.js.map} +1 -1
  33. package/dist/admin/assets/{HintedLabel-CVe-PIyv.js → HintedLabel-DIEBvftx.js} +2 -2
  34. package/dist/admin/assets/{HintedLabel-CVe-PIyv.js.map → HintedLabel-DIEBvftx.js.map} +1 -1
  35. package/dist/admin/assets/{InfoHint-DtaHU1Cg.js → InfoHint-BdAadb9U.js} +2 -2
  36. package/dist/admin/assets/{InfoHint-DtaHU1Cg.js.map → InfoHint-BdAadb9U.js.map} +1 -1
  37. package/dist/admin/assets/{InsertMenuItem-DlzaD9zr.js → InsertMenuItem-oh0NJrhB.js} +2 -2
  38. package/dist/admin/assets/{InsertMenuItem-DlzaD9zr.js.map → InsertMenuItem-oh0NJrhB.js.map} +1 -1
  39. package/dist/admin/assets/{LinkTabs-stM3j4aH.js → LinkTabs-EBgHX4Uh.js} +2 -2
  40. package/dist/admin/assets/{LinkTabs-stM3j4aH.js.map → LinkTabs-EBgHX4Uh.js.map} +1 -1
  41. package/dist/admin/assets/{Match-ErAxnNAY.js → Match-CvpSNEfi.js} +2 -2
  42. package/dist/admin/assets/{Match-ErAxnNAY.js.map → Match-CvpSNEfi.js.map} +1 -1
  43. package/dist/admin/assets/{Panel-ClY9b33D.js → Panel-BV1BpNCn.js} +2 -2
  44. package/dist/admin/assets/{Panel-ClY9b33D.js.map → Panel-BV1BpNCn.js.map} +1 -1
  45. package/dist/admin/assets/{QueryView-C630REML.js → QueryView-Bbw1eiV2.js} +2 -2
  46. package/dist/admin/assets/{QueryView-C630REML.js.map → QueryView-Bbw1eiV2.js.map} +1 -1
  47. package/dist/admin/assets/{RadioTile-CD4mel7U.js → RadioTile-CSYW-8gL.js} +2 -2
  48. package/dist/admin/assets/{RadioTile-CD4mel7U.js.map → RadioTile-CSYW-8gL.js.map} +1 -1
  49. package/dist/admin/assets/{RichTextField-CxeVX6C-.js → RichTextField-BDb67rCi.js} +2 -2
  50. package/dist/admin/assets/{RichTextField-CxeVX6C-.js.map → RichTextField-BDb67rCi.js.map} +1 -1
  51. package/dist/admin/assets/{RowTitle-B-7RptyN.js → RowTitle-Cted2GP8.js} +2 -2
  52. package/dist/admin/assets/{RowTitle-B-7RptyN.js.map → RowTitle-Cted2GP8.js.map} +1 -1
  53. package/dist/admin/assets/SecretReveal-Ccaul96d.js +2 -0
  54. package/dist/admin/assets/SecretReveal-Ccaul96d.js.map +1 -0
  55. package/dist/admin/assets/{SharedGlyph-97RaO6Bh.js → SharedGlyph-BrLi3Hug.js} +2 -2
  56. package/dist/admin/assets/{SharedGlyph-97RaO6Bh.js.map → SharedGlyph-BrLi3Hug.js.map} +1 -1
  57. package/dist/admin/assets/{StepList--0Fiqsq-.js → StepList-BLvElvPV.js} +2 -2
  58. package/dist/admin/assets/{StepList--0Fiqsq-.js.map → StepList-BLvElvPV.js.map} +1 -1
  59. package/dist/admin/assets/Structure-CRNAuVY7.js +2 -0
  60. package/dist/admin/assets/{Structure-CoO3iah3.js.map → Structure-CRNAuVY7.js.map} +1 -1
  61. package/dist/admin/assets/{SubNavLink-YjjaDN40.js → SubNavLink-biGV5ozd.js} +2 -2
  62. package/dist/admin/assets/{SubNavLink-YjjaDN40.js.map → SubNavLink-biGV5ozd.js.map} +1 -1
  63. package/dist/admin/assets/{SuggestAltButton-Dlyxoypu.js → SuggestAltButton-CrFiRWGr.js} +2 -2
  64. package/dist/admin/assets/{SuggestAltButton-Dlyxoypu.js.map → SuggestAltButton-CrFiRWGr.js.map} +1 -1
  65. package/dist/admin/assets/{TextLink-DIqr5zkJ.js → TextLink-BPLbXw2n.js} +2 -2
  66. package/dist/admin/assets/{TextLink-DIqr5zkJ.js.map → TextLink-BPLbXw2n.js.map} +1 -1
  67. package/dist/admin/assets/{Thumbnail-BUYOr4XJ.js → Thumbnail-B_TWlgEz.js} +2 -2
  68. package/dist/admin/assets/{Thumbnail-BUYOr4XJ.js.map → Thumbnail-B_TWlgEz.js.map} +1 -1
  69. package/dist/admin/assets/{UnsavedChangesGuard-BfzHgUkF.js → UnsavedChangesGuard-CrzR2boF.js} +2 -2
  70. package/dist/admin/assets/{UnsavedChangesGuard-BfzHgUkF.js.map → UnsavedChangesGuard-CrzR2boF.js.map} +1 -1
  71. package/dist/admin/assets/{UserAvatar-DNOWhSWS.js → UserAvatar-W-S3st7E.js} +2 -2
  72. package/dist/admin/assets/{UserAvatar-DNOWhSWS.js.map → UserAvatar-W-S3st7E.js.map} +1 -1
  73. package/dist/admin/assets/{auth-YVfK3SxF.js → auth-cXFFyABE.js} +2 -2
  74. package/dist/admin/assets/{auth-YVfK3SxF.js.map → auth-cXFFyABE.js.map} +1 -1
  75. package/dist/admin/assets/{badge-lJ4Ih_Zn.js → badge-DrPwivGk.js} +2 -2
  76. package/dist/admin/assets/{badge-lJ4Ih_Zn.js.map → badge-DrPwivGk.js.map} +1 -1
  77. package/dist/admin/assets/{boxes-DRvd25_u.js → boxes-CKJ6P9VA.js} +2 -2
  78. package/dist/admin/assets/{boxes-DRvd25_u.js.map → boxes-CKJ6P9VA.js.map} +1 -1
  79. package/dist/admin/assets/{calendar-clock-VENf4s1R.js → calendar-clock-DPyjti63.js} +2 -2
  80. package/dist/admin/assets/{calendar-clock-VENf4s1R.js.map → calendar-clock-DPyjti63.js.map} +1 -1
  81. package/dist/admin/assets/{changeSets-JmSyu3tu.js → changeSets-DYGm5j1A.js} +2 -2
  82. package/dist/admin/assets/{changeSets-JmSyu3tu.js.map → changeSets-DYGm5j1A.js.map} +1 -1
  83. package/dist/admin/assets/chevron-left-DIp5sJuv.js +2 -0
  84. package/dist/admin/assets/{chevron-left-D2DnQyeV.js.map → chevron-left-DIp5sJuv.js.map} +1 -1
  85. package/dist/admin/assets/{circle-check-jmRQz1Jq.js → circle-check-VXPKg4cE.js} +2 -2
  86. package/dist/admin/assets/{circle-check-jmRQz1Jq.js.map → circle-check-VXPKg4cE.js.map} +1 -1
  87. package/dist/admin/assets/{clock-DrSTMGjp.js → clock-DNhupIko.js} +2 -2
  88. package/dist/admin/assets/{clock-DrSTMGjp.js.map → clock-DNhupIko.js.map} +1 -1
  89. package/dist/admin/assets/cn-CEPUgFbZ.js +2 -0
  90. package/dist/admin/assets/cn-CEPUgFbZ.js.map +1 -0
  91. package/dist/admin/assets/coerce-BxuBjZdf.js +2 -0
  92. package/dist/admin/assets/{coerce-B4MJTGg1.js.map → coerce-BxuBjZdf.js.map} +1 -1
  93. package/dist/admin/assets/{constants-ahk0YSYl.js → constants-B9cNrCXV.js} +2 -2
  94. package/dist/admin/assets/{constants-ahk0YSYl.js.map → constants-B9cNrCXV.js.map} +1 -1
  95. package/dist/admin/assets/{content-DcvNzKGe.js → content-DjyCdi4c.js} +3 -3
  96. package/dist/admin/assets/{content-DcvNzKGe.js.map → content-DjyCdi4c.js.map} +1 -1
  97. package/dist/admin/assets/{contentCounts-DzIvdeXR.js → contentCounts-CX15iYcR.js} +2 -2
  98. package/dist/admin/assets/{contentCounts-DzIvdeXR.js.map → contentCounts-CX15iYcR.js.map} +1 -1
  99. package/dist/admin/assets/{contentTypes-YAd-AzOF.js → contentTypes-FgyxLhlk.js} +2 -2
  100. package/dist/admin/assets/{contentTypes-YAd-AzOF.js.map → contentTypes-FgyxLhlk.js.map} +1 -1
  101. package/dist/admin/assets/{context-4Jl20mYc.js → context-FqJXjS5k.js} +2 -2
  102. package/dist/admin/assets/{context-4Jl20mYc.js.map → context-FqJXjS5k.js.map} +1 -1
  103. package/dist/admin/assets/{copy-t6kAFw-O.js → copy-CoHbtS-Y.js} +2 -2
  104. package/dist/admin/assets/{copy-t6kAFw-O.js.map → copy-CoHbtS-Y.js.map} +1 -1
  105. package/dist/admin/assets/{dateTimeLocal-IDUOKulz.js → dateTimeLocal-BxmDLO_H.js} +2 -2
  106. package/dist/admin/assets/{dateTimeLocal-IDUOKulz.js.map → dateTimeLocal-BxmDLO_H.js.map} +1 -1
  107. package/dist/admin/assets/{develop-DYwiTd6R.js → develop-gx8WeWv4.js} +2 -2
  108. package/dist/admin/assets/{develop-DYwiTd6R.js.map → develop-gx8WeWv4.js.map} +1 -1
  109. package/dist/admin/assets/{dropdown-menu-DiYSm-g3.js → dropdown-menu-BG1rctRQ.js} +2 -2
  110. package/dist/admin/assets/{dropdown-menu-DiYSm-g3.js.map → dropdown-menu-BG1rctRQ.js.map} +1 -1
  111. package/dist/admin/assets/{ellipsis-DEHUaCkn.js → ellipsis-DHfP1Ah2.js} +2 -2
  112. package/dist/admin/assets/{ellipsis-DEHUaCkn.js.map → ellipsis-DHfP1Ah2.js.map} +1 -1
  113. package/dist/admin/assets/{external-link-B8UFREQ_.js → external-link-itxcNtaX.js} +2 -2
  114. package/dist/admin/assets/{external-link-B8UFREQ_.js.map → external-link-itxcNtaX.js.map} +1 -1
  115. package/dist/admin/assets/{fileRoute-CVQpW8cj.js → fileRoute-BZ0pGGsh.js} +2 -2
  116. package/dist/admin/assets/{fileRoute-CVQpW8cj.js.map → fileRoute-BZ0pGGsh.js.map} +1 -1
  117. package/dist/admin/assets/{folder-plus-iDg1hhew.js → folder-plus-BgsfY_6i.js} +2 -2
  118. package/dist/admin/assets/{folder-plus-iDg1hhew.js.map → folder-plus-BgsfY_6i.js.map} +1 -1
  119. package/dist/admin/assets/{followPlural-BdqfEe48.js → followPlural-BqjqhHmz.js} +2 -2
  120. package/dist/admin/assets/{followPlural-BdqfEe48.js.map → followPlural-BqjqhHmz.js.map} +1 -1
  121. package/dist/admin/assets/{globe-TooNSp9V.js → globe-RjXj4P-A.js} +2 -2
  122. package/dist/admin/assets/{globe-TooNSp9V.js.map → globe-RjXj4P-A.js.map} +1 -1
  123. package/dist/admin/assets/{image-CXZXXH58.js → image-4itB4TLl.js} +2 -2
  124. package/dist/admin/assets/{image-CXZXXH58.js.map → image-4itB4TLl.js.map} +1 -1
  125. package/dist/admin/assets/{image-off-cxva5G-Y.js → image-off-CokIymyg.js} +2 -2
  126. package/dist/admin/assets/{image-off-cxva5G-Y.js.map → image-off-CokIymyg.js.map} +1 -1
  127. package/dist/admin/assets/{inbox-CnzNfqt9.js → inbox-8AAyyM0T.js} +2 -2
  128. package/dist/admin/assets/{inbox-CnzNfqt9.js.map → inbox-8AAyyM0T.js.map} +1 -1
  129. package/dist/admin/assets/{inbox-B8c9Kj_z.js → inbox-97pzmuus.js} +2 -2
  130. package/dist/admin/assets/{inbox-B8c9Kj_z.js.map → inbox-97pzmuus.js.map} +1 -1
  131. package/dist/admin/assets/{index-D9KL7arJ.js → index-cpiZvLck.js} +4 -4
  132. package/dist/admin/assets/{index-D9KL7arJ.js.map → index-cpiZvLck.js.map} +1 -1
  133. package/dist/admin/assets/{key-round-CFJ61iCd.js → key-round-Dc4_SOi-.js} +2 -2
  134. package/dist/admin/assets/{key-round-CFJ61iCd.js.map → key-round-Dc4_SOi-.js.map} +1 -1
  135. package/dist/admin/assets/{languages-eh7U4anI.js → languages-DR2K97A5.js} +2 -2
  136. package/dist/admin/assets/{languages-eh7U4anI.js.map → languages-DR2K97A5.js.map} +1 -1
  137. package/dist/admin/assets/{layers-mGfmMsKE.js → layers-ZPmcbieN.js} +2 -2
  138. package/dist/admin/assets/{layers-mGfmMsKE.js.map → layers-ZPmcbieN.js.map} +1 -1
  139. package/dist/admin/assets/{link-2-off-DbZzRN_w.js → link-2-off-D_5_hkT7.js} +2 -2
  140. package/dist/admin/assets/{link-2-off-DbZzRN_w.js.map → link-2-off-D_5_hkT7.js.map} +1 -1
  141. package/dist/admin/assets/{link-b2pFaN-A.js → link-BnDFFyvi.js} +2 -2
  142. package/dist/admin/assets/{link-b2pFaN-A.js.map → link-BnDFFyvi.js.map} +1 -1
  143. package/dist/admin/assets/{locales-oBD0q1pN.js → locales-D-21t-l-.js} +2 -2
  144. package/dist/admin/assets/{locales-oBD0q1pN.js.map → locales-D-21t-l-.js.map} +1 -1
  145. package/dist/admin/assets/{media-5ks_0Rre.js → media-D7aimjyy.js} +2 -2
  146. package/dist/admin/assets/{media-5ks_0Rre.js.map → media-D7aimjyy.js.map} +1 -1
  147. package/dist/admin/assets/{media-Cs9LCQcz.js → media-Db0cHYNY.js} +2 -2
  148. package/dist/admin/assets/{media-Cs9LCQcz.js.map → media-Db0cHYNY.js.map} +1 -1
  149. package/dist/admin/assets/network-l_oLT5fa.js +2 -0
  150. package/dist/admin/assets/network-l_oLT5fa.js.map +1 -0
  151. package/dist/admin/assets/{pencil-C7eD5mlZ.js → pencil-Cn8c_gtv.js} +2 -2
  152. package/dist/admin/assets/{pencil-C7eD5mlZ.js.map → pencil-Cn8c_gtv.js.map} +1 -1
  153. package/dist/admin/assets/{plus-BDZHQBlU.js → plus-yxvf4aAT.js} +2 -2
  154. package/dist/admin/assets/{plus-BDZHQBlU.js.map → plus-yxvf4aAT.js.map} +1 -1
  155. package/dist/admin/assets/{preload-helper-CJNJ4S3k.js → preload-helper-pwSesV5P.js} +2 -2
  156. package/dist/admin/assets/{preload-helper-CJNJ4S3k.js.map → preload-helper-pwSesV5P.js.map} +1 -1
  157. package/dist/admin/assets/{publishing-C4YQ4XKt.js → publishing-PQLCOnvu.js} +2 -2
  158. package/dist/admin/assets/{publishing-C4YQ4XKt.js.map → publishing-PQLCOnvu.js.map} +1 -1
  159. package/dist/admin/assets/{radio-group-zOsGAYRP.js → radio-group-Fc2ENIMO.js} +2 -2
  160. package/dist/admin/assets/{radio-group-zOsGAYRP.js.map → radio-group-Fc2ENIMO.js.map} +1 -1
  161. package/dist/admin/assets/{rotate-ccw-DCWwb4Ga.js → rotate-ccw-BHa0BHug.js} +2 -2
  162. package/dist/admin/assets/{rotate-ccw-DCWwb4Ga.js.map → rotate-ccw-BHa0BHug.js.map} +1 -1
  163. package/dist/admin/assets/{schedules-DXrBSypK.js → schedules-B8y_GnqX.js} +2 -2
  164. package/dist/admin/assets/{schedules-DXrBSypK.js.map → schedules-B8y_GnqX.js.map} +1 -1
  165. package/dist/admin/assets/{schema-B7L9OJrX.js → schema-BJHcYshf.js} +2 -2
  166. package/dist/admin/assets/{schema-B7L9OJrX.js.map → schema-BJHcYshf.js.map} +1 -1
  167. package/dist/admin/assets/{scroll-text-BLThe4Ki.js → scroll-text-BHLmZpBB.js} +2 -2
  168. package/dist/admin/assets/{scroll-text-BLThe4Ki.js.map → scroll-text-BHLmZpBB.js.map} +1 -1
  169. package/dist/admin/assets/{search-CcJQqtRw.js → search-D3GGSaSL.js} +2 -2
  170. package/dist/admin/assets/{search-CcJQqtRw.js.map → search-D3GGSaSL.js.map} +1 -1
  171. package/dist/admin/assets/{searchSchemas-BCRZ1jyL.js → searchSchemas-C4dnYI9a.js} +2 -2
  172. package/dist/admin/assets/{searchSchemas-BCRZ1jyL.js.map → searchSchemas-C4dnYI9a.js.map} +1 -1
  173. package/dist/admin/assets/{sections-v5Flaor9.js → sections-BWoTaw2B.js} +2 -2
  174. package/dist/admin/assets/{sections-v5Flaor9.js.map → sections-BWoTaw2B.js.map} +1 -1
  175. package/dist/admin/assets/{sections-TIN_Nl1b.js → sections-eeTU636D.js} +2 -2
  176. package/dist/admin/assets/{sections-TIN_Nl1b.js.map → sections-eeTU636D.js.map} +1 -1
  177. package/dist/admin/assets/{select-CpHp3YOH.js → select-BMi4G1Ib.js} +2 -2
  178. package/dist/admin/assets/{select-CpHp3YOH.js.map → select-BMi4G1Ib.js.map} +1 -1
  179. package/dist/admin/assets/{send-CbP0Rc9_.js → send-Bqhmg9HP.js} +2 -2
  180. package/dist/admin/assets/{send-CbP0Rc9_.js.map → send-Bqhmg9HP.js.map} +1 -1
  181. package/dist/admin/assets/{seo-1n9s7YuX.js → seo-BL9C7Yt3.js} +2 -2
  182. package/dist/admin/assets/{seo-1n9s7YuX.js.map → seo-BL9C7Yt3.js.map} +1 -1
  183. package/dist/admin/assets/settings-D-nW3wQb.js +2 -0
  184. package/dist/admin/assets/settings-D-nW3wQb.js.map +1 -0
  185. package/dist/admin/assets/skeleton-BPLAzyDo.js +42 -0
  186. package/dist/admin/assets/skeleton-BPLAzyDo.js.map +1 -0
  187. package/dist/admin/assets/{snapshots-DkyyrWX3.js → snapshots-COSUeZBP.js} +2 -2
  188. package/dist/admin/assets/{snapshots-DkyyrWX3.js.map → snapshots-COSUeZBP.js.map} +1 -1
  189. package/dist/admin/assets/{sparkles-D88gf3vX.js → sparkles-CykwtKTd.js} +2 -2
  190. package/dist/admin/assets/{sparkles-D88gf3vX.js.map → sparkles-CykwtKTd.js.map} +1 -1
  191. package/dist/admin/assets/{stableId-DlNKh3HM.js → stableId-DfJkQUV3.js} +2 -2
  192. package/dist/admin/assets/{stableId-DlNKh3HM.js.map → stableId-DfJkQUV3.js.map} +1 -1
  193. package/dist/admin/assets/{store-jUWyOXDG.js → store-GOGeP63I.js} +2 -2
  194. package/dist/admin/assets/{store-jUWyOXDG.js.map → store-GOGeP63I.js.map} +1 -1
  195. package/dist/admin/assets/{switch-CijNGYkG.js → switch-XRU_EOoM.js} +2 -2
  196. package/dist/admin/assets/{switch-CijNGYkG.js.map → switch-XRU_EOoM.js.map} +1 -1
  197. package/dist/admin/assets/{table-DQ5ILlSh.js → table-CLVW6uJB.js} +2 -2
  198. package/dist/admin/assets/{table-DQ5ILlSh.js.map → table-CLVW6uJB.js.map} +1 -1
  199. package/dist/admin/assets/{tabs-CSqGW9Du.js → tabs-DB42Xljn.js} +3 -3
  200. package/dist/admin/assets/{tabs-CSqGW9Du.js.map → tabs-DB42Xljn.js.map} +1 -1
  201. package/dist/admin/assets/{textarea-FwSvwmyT.js → textarea-DcsLGCkq.js} +2 -2
  202. package/dist/admin/assets/{textarea-FwSvwmyT.js.map → textarea-DcsLGCkq.js.map} +1 -1
  203. package/dist/admin/assets/{theme-CHHVK8Ps.js → theme-CnSar6tt.js} +2 -2
  204. package/dist/admin/assets/{theme-CHHVK8Ps.js.map → theme-CnSar6tt.js.map} +1 -1
  205. package/dist/admin/assets/{titles-CQYc9nDQ.js → titles-DluirfBO.js} +2 -2
  206. package/dist/admin/assets/{titles-CQYc9nDQ.js.map → titles-DluirfBO.js.map} +1 -1
  207. package/dist/admin/assets/{tokens-C-20gLAT.js → tokens-CQooB8Sf.js} +2 -2
  208. package/dist/admin/assets/{tokens-C-20gLAT.js.map → tokens-CQooB8Sf.js.map} +1 -1
  209. package/dist/admin/assets/{tooltip-BXqFQv9N.js → tooltip-xfZn0Fla.js} +2 -2
  210. package/dist/admin/assets/{tooltip-BXqFQv9N.js.map → tooltip-xfZn0Fla.js.map} +1 -1
  211. package/dist/admin/assets/{undo-2-CLy2zklL.js → undo-2-B_QjuGmR.js} +2 -2
  212. package/dist/admin/assets/{undo-2-CLy2zklL.js.map → undo-2-B_QjuGmR.js.map} +1 -1
  213. package/dist/admin/assets/{useAuditLabels-BJF7M5RY.js → useAuditLabels-BZPAj3wG.js} +2 -2
  214. package/dist/admin/assets/{useAuditLabels-BJF7M5RY.js.map → useAuditLabels-BZPAj3wG.js.map} +1 -1
  215. package/dist/admin/assets/{useConfirmTarget-BUvlrGSW.js → useConfirmTarget-B7iLg66j.js} +2 -2
  216. package/dist/admin/assets/{useConfirmTarget-BUvlrGSW.js.map → useConfirmTarget-B7iLg66j.js.map} +1 -1
  217. package/dist/admin/assets/{useContentSchema-Cvtes34f.js → useContentSchema-BTyBYURX.js} +2 -2
  218. package/dist/admin/assets/{useContentSchema-Cvtes34f.js.map → useContentSchema-BTyBYURX.js.map} +1 -1
  219. package/dist/admin/assets/{useCursorPager-CzB6NDOB.js → useCursorPager-CTGOzl69.js} +2 -2
  220. package/dist/admin/assets/{useCursorPager-CzB6NDOB.js.map → useCursorPager-CTGOzl69.js.map} +1 -1
  221. package/dist/admin/assets/{useDiscardGuard-BAEGkGK9.js → useDiscardGuard-CSkPw8iq.js} +2 -2
  222. package/dist/admin/assets/{useDiscardGuard-BAEGkGK9.js.map → useDiscardGuard-CSkPw8iq.js.map} +1 -1
  223. package/dist/admin/assets/{useMediaPickerDialog-CiN_pias.js → useMediaPickerDialog-W0jhN-hi.js} +2 -2
  224. package/dist/admin/assets/{useMediaPickerDialog-CiN_pias.js.map → useMediaPickerDialog-W0jhN-hi.js.map} +1 -1
  225. package/dist/admin/assets/{useMediaUploader-CapEHBIt.js → useMediaUploader-BC5zCkQt.js} +2 -2
  226. package/dist/admin/assets/{useMediaUploader-CapEHBIt.js.map → useMediaUploader-BC5zCkQt.js.map} +1 -1
  227. package/dist/admin/assets/{usePaletteItems-wpn3ms6o.js → usePaletteItems-CqcKndUT.js} +2 -2
  228. package/dist/admin/assets/{usePaletteItems-wpn3ms6o.js.map → usePaletteItems-CqcKndUT.js.map} +1 -1
  229. package/dist/admin/assets/{usePlaces-6iTN15Xy.js → usePlaces-o4Os5Nbv.js} +2 -2
  230. package/dist/admin/assets/{usePlaces-6iTN15Xy.js.map → usePlaces-o4Os5Nbv.js.map} +1 -1
  231. package/dist/admin/assets/{useQueries-CYfcxI0Q.js → useQueries-BTnSD0k5.js} +2 -2
  232. package/dist/admin/assets/{useQueries-CYfcxI0Q.js.map → useQueries-BTnSD0k5.js.map} +1 -1
  233. package/dist/admin/assets/{useResolvedScheme-i1EwGHuF.js → useResolvedScheme-SoI-2Faf.js} +2 -2
  234. package/dist/admin/assets/{useResolvedScheme-i1EwGHuF.js.map → useResolvedScheme-SoI-2Faf.js.map} +1 -1
  235. package/dist/admin/assets/{useSchemaScopeAccess-WQKmj8_3.js → useSchemaScopeAccess-B4-31q7m.js} +2 -2
  236. package/dist/admin/assets/{useSchemaScopeAccess-WQKmj8_3.js.map → useSchemaScopeAccess-B4-31q7m.js.map} +1 -1
  237. package/dist/admin/assets/{users-BgTNDPXx.js → users-BVuapb3u.js} +2 -2
  238. package/dist/admin/assets/{users-BgTNDPXx.js.map → users-BVuapb3u.js.map} +1 -1
  239. package/dist/admin/assets/{users-B01EH47o.js → users-Oj5V5za_.js} +2 -2
  240. package/dist/admin/assets/{users-B01EH47o.js.map → users-Oj5V5za_.js.map} +1 -1
  241. package/dist/admin/assets/{zod-79xzBIrW.js → zod-cTJwCa4E.js} +2 -2
  242. package/dist/admin/assets/{zod-79xzBIrW.js.map → zod-cTJwCa4E.js.map} +1 -1
  243. package/dist/admin/index.html +55 -55
  244. package/dist/cli.js +128 -21
  245. package/dist/cli.js.map +1 -1
  246. package/dist/config.d.ts +1 -1
  247. package/dist/index.d.ts +1 -1
  248. package/dist/index.js +1 -1
  249. package/dist/{public-CTK82y3t.d.ts → public-C027OJX6.d.ts} +13 -2
  250. package/dist/s3Adapter-BipoQiwQ.js +125 -0
  251. package/dist/s3Adapter-BipoQiwQ.js.map +1 -0
  252. package/dist/{worker-BDYDbaea.js → worker-BU-V4qbH.js} +555 -411
  253. package/dist/worker-BU-V4qbH.js.map +1 -0
  254. package/package.json +8 -5
  255. package/dist/admin/assets/FormCheckboxGroup-BrXY6qBg.js +0 -2
  256. package/dist/admin/assets/FormCheckboxGroup-BrXY6qBg.js.map +0 -1
  257. package/dist/admin/assets/SecretReveal-Dhw_dYxe.js +0 -2
  258. package/dist/admin/assets/SecretReveal-Dhw_dYxe.js.map +0 -1
  259. package/dist/admin/assets/Structure-CoO3iah3.js +0 -2
  260. package/dist/admin/assets/chevron-left-D2DnQyeV.js +0 -2
  261. package/dist/admin/assets/cn-CL1YHRrS.js +0 -2
  262. package/dist/admin/assets/cn-CL1YHRrS.js.map +0 -1
  263. package/dist/admin/assets/coerce-B4MJTGg1.js +0 -2
  264. package/dist/admin/assets/network-DS8MW3AZ.js +0 -2
  265. package/dist/admin/assets/network-DS8MW3AZ.js.map +0 -1
  266. package/dist/admin/assets/settings-CuEO5l07.js +0 -2
  267. package/dist/admin/assets/settings-CuEO5l07.js.map +0 -1
  268. package/dist/admin/assets/skeleton-DElTumgk.js +0 -42
  269. package/dist/admin/assets/skeleton-DElTumgk.js.map +0 -1
  270. package/dist/worker-BDYDbaea.js.map +0 -1
@@ -7,7 +7,6 @@ import Fastify, { LogController } from "fastify";
7
7
  import { AliasNode, AndNode, BinaryOperationNode, ColumnNode, ColumnUpdateNode, IdentifierNode, InsertQueryNode, Kysely, ListNode, MysqlAdapter, MysqlIntrospector, MysqlQueryCompiler, OnDuplicateKeyNode, OperationNodeTransformer, OperatorNode, OrNode, OrderByItemNode, ParensNode, PostgresDialect, PrimitiveValueListNode, RawNode, ReferenceNode, SelectAllNode, SelectModifierNode, SelectQueryNode, SelectionNode, SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler, TableNode, UpdateQueryNode, ValueListNode, ValueNode, ValuesNode, WhereNode, sql } from "kysely";
8
8
  import pg from "pg";
9
9
  import { AsyncLocalStorage } from "node:async_hooks";
10
- import mysql from "mysql2";
11
10
  import { EventEmitter } from "node:events";
12
11
  import { DatabaseSync } from "node:sqlite";
13
12
  import { Type } from "typebox";
@@ -15,9 +14,6 @@ import { Value } from "typebox/value";
15
14
  import { isDeepStrictEqual } from "node:util";
16
15
  import { createJiti } from "jiti";
17
16
  import { pino } from "pino";
18
- import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
19
- import { createPresignedPost } from "@aws-sdk/s3-presigned-post";
20
- import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
21
17
  import fp from "fastify-plugin";
22
18
  import { hash, verify } from "@node-rs/argon2";
23
19
  import cookie from "@fastify/cookie";
@@ -177,6 +173,20 @@ const sqliteLocationOfUrl = (url) => {
177
173
  path
178
174
  };
179
175
  };
176
+ /** `host:port/database` (PostgreSQL, MySQL) or the file path (SQLite) from DATABASE_URL, never the credentials. */
177
+ const describeDatabaseTarget = (connectionString) => {
178
+ if (dialectOfUrl(connectionString) === "sqlite") {
179
+ const location = sqliteLocationOfUrl(connectionString);
180
+ return location.kind === "memory" ? "an in-memory database" : location.path;
181
+ }
182
+ try {
183
+ const url = new URL(connectionString);
184
+ const defaultPort = dialectOfUrl(connectionString) === "mysql" ? "3306" : "5432";
185
+ return `${url.hostname}:${url.port || defaultPort}${url.pathname}`;
186
+ } catch {
187
+ return "the configured database";
188
+ }
189
+ };
180
190
  let current;
181
191
  /** The dialect queries are built for. PostgreSQL until a database handle says otherwise. */
182
192
  const currentDialect = () => current ?? "postgres";
@@ -1899,9 +1909,10 @@ const MYSQL_TABLES = {
1899
1909
  "id",
1900
1910
  "schema_version",
1901
1911
  "permissions_version",
1902
- "updated_at"
1912
+ "updated_at",
1913
+ "release"
1903
1914
  ],
1904
- nullable: [],
1915
+ nullable: ["release"],
1905
1916
  timestamps: ["updated_at"],
1906
1917
  uniqueKeys: [{
1907
1918
  name: "system_versions_pkey",
@@ -2684,6 +2695,13 @@ const typeCast = (field, next) => {
2684
2695
  return next();
2685
2696
  };
2686
2697
  //#endregion
2698
+ //#region src/db/mysql/module.ts
2699
+ /**
2700
+ * mysql2, loaded on first use. A process on PostgreSQL never loads it, and a bundle of the read path (the
2701
+ * in-process delivery runtime) can leave it out as an optional dependency.
2702
+ */
2703
+ const loadMysql = async () => (await import("mysql2")).default;
2704
+ //#endregion
2687
2705
  //#region src/db/mysql/parameters.ts
2688
2706
  /**
2689
2707
  * Parameters the MySQL connection resolves itself just before a statement runs (ADR 0001, "MySQL").
@@ -2885,7 +2903,7 @@ var MysqlDriver = class {
2885
2903
  this.databaseKey = mysqlDatabaseKey(options.url);
2886
2904
  }
2887
2905
  async init() {
2888
- const pool = mysql.createPool(mysqlPoolOptions(this.#options.url, this.#options.poolMax, this.#options.applicationName));
2906
+ const pool = (await loadMysql()).createPool(mysqlPoolOptions(this.#options.url, this.#options.poolMax, this.#options.applicationName));
2889
2907
  pool.on("connection", (connection) => {
2890
2908
  connection.query(SESSION_SETUP, (error) => {
2891
2909
  if (error) {
@@ -9435,6 +9453,10 @@ const bumpSchemaVersion = async (trx, now) => {
9435
9453
  updated_at: now
9436
9454
  })).returning("schema_version").executeTakeFirstOrThrow()).schema_version;
9437
9455
  };
9456
+ /** Records the Shapio release this database is served by (in-process readers compare their own with it). */
9457
+ const setRelease = async (release, executor = db) => {
9458
+ await executor.updateTable("system_versions").set({ release }).execute();
9459
+ };
9438
9460
  //#endregion
9439
9461
  //#region src/schema/snapshot.ts
9440
9462
  /**
@@ -10412,6 +10434,14 @@ const CONTENT_ACTIONS = [
10412
10434
  "schemaManage"
10413
10435
  ];
10414
10436
  /**
10437
+ * What a delivery role may grant (plan drafts-mode §1): `read`, and `readDrafts`, which lets a delivery token
10438
+ * read draft heads (`?publicationState=draft`, GraphQL `publicationState: DRAFT`) on top of `read`. A
10439
+ * `readDrafts` grant always covers every model (no model, condition or field list), so one check per request
10440
+ * covers relation targets too. Admin and app roles never hold it; admin principals read drafts without it.
10441
+ */
10442
+ const DELIVERY_ACTIONS = ["read", "readDrafts"];
10443
+ const READ_DRAFTS_ACTION = "readDrafts";
10444
+ /**
10415
10445
  * Content actions about the schema rather than one site's entries: network roles only for a shared
10416
10446
  * definition; for a site's own definition, the roles that apply on that site (when the principal acts there).
10417
10447
  */
@@ -12131,14 +12161,25 @@ const groupBindings = (rows) => {
12131
12161
  }
12132
12162
  return bindings;
12133
12163
  };
12134
- const KNOWN_ACTIONS = /* @__PURE__ */ new Set([...CONTENT_ACTIONS, ...GLOBAL_ACTIONS]);
12164
+ const KNOWN_ACTIONS = /* @__PURE__ */ new Set([
12165
+ ...CONTENT_ACTIONS,
12166
+ ...DELIVERY_ACTIONS,
12167
+ ...GLOBAL_ACTIONS
12168
+ ]);
12169
+ const KNOWN_CONDITIONS = /* @__PURE__ */ new Set(["ownedByPrincipal"]);
12170
+ /**
12171
+ * A stored grant as this build understands it. Rows written by a newer Shapio may name an action or a row
12172
+ * condition this build does not know; they grant nothing here (fail closed). An unknown condition must never
12173
+ * read as "no condition", which would widen the grant to every row.
12174
+ */
12135
12175
  const toGrant$1 = (row) => {
12136
12176
  if (!KNOWN_ACTIONS.has(row.action)) return;
12177
+ if (row.condition !== null && !KNOWN_CONDITIONS.has(row.condition)) return;
12137
12178
  return {
12138
12179
  roleId: row.role_id,
12139
12180
  action: row.action,
12140
12181
  modelId: row.model_id,
12141
- condition: row.condition === "ownedByPrincipal" ? "ownedByPrincipal" : null,
12182
+ condition: row.condition,
12142
12183
  fieldIds: row.field_ids
12143
12184
  };
12144
12185
  };
@@ -12270,7 +12311,9 @@ const allowsGlobalAction = (grants, action) => grants.some((grant) => grant.acti
12270
12311
  //#endregion
12271
12312
  //#region src/permissions/evaluator.ts
12272
12313
  const NETWORK_ACTION_SET = new Set(NETWORK_ACTIONS);
12273
- const READ_ONLY$1 = /* @__PURE__ */ new Set(["read"]);
12314
+ /** Delivery tokens read, and read drafts where their role grants `readDrafts`. */
12315
+ const DELIVERY_TOKEN_ACTIONS = new Set(DELIVERY_ACTIONS);
12316
+ /** App users and anonymous callers: never `readDrafts`, whatever a role says. */
12274
12317
  const APP_ACTIONS = new Set(APP_CONTENT_ACTIONS);
12275
12318
  /**
12276
12319
  * Which roles a principal holds and how its masks are computed. App users hold the custom app roles assigned
@@ -12297,7 +12340,7 @@ const rolesOf = async (principal, grants, executor, versions) => {
12297
12340
  roleIds: [principal.roleId],
12298
12341
  networkRoleIds: [],
12299
12342
  audience: "delivery",
12300
- actions: READ_ONLY$1
12343
+ actions: DELIVERY_TOKEN_ACTIONS
12301
12344
  };
12302
12345
  case "appUser": return {
12303
12346
  roleIds: [...await grants.getSiteAppRoleIds(principal.siteId, "authenticated", executor, versions), ...principal.roleIds],
@@ -13400,6 +13443,22 @@ const SITE_QUERY_PARAMETER = "site";
13400
13443
  * and the header that selects the site. CDNs that key on the URL only need `?site=` instead of the header.
13401
13444
  */
13402
13445
  const DELIVERY_VARY = "Authorization, Cookie, Shapio-Site";
13446
+ /** `Cache-Control` of delivery responses carrying drafts (REST and GraphQL; plan drafts-mode): never stored. */
13447
+ const DRAFTS_CACHE_CONTROL = "private, no-store";
13448
+ //#endregion
13449
+ //#region src/services/contentContext.ts
13450
+ /**
13451
+ * The content service context for one call, the same however the call arrived: the site's view of the pinned
13452
+ * schema (plan site-schema: shared definitions and the site's own), scoped to the site.
13453
+ */
13454
+ const buildContentContext = ({ network, hooks, media, requestId, ip, ...rest }) => ({
13455
+ ...rest,
13456
+ snapshot: network.forSite(rest.site.id),
13457
+ hooks: hooks ?? createContentHooks(),
13458
+ ...media ? { media } : {},
13459
+ ...requestId !== void 0 ? { requestId } : {},
13460
+ ...ip !== void 0 ? { ip } : {}
13461
+ });
13403
13462
  const NUMERIC_TYPES = /* @__PURE__ */ new Set([
13404
13463
  "number",
13405
13464
  "integer",
@@ -13580,6 +13639,8 @@ const RICH_TEXT_MODES = [
13580
13639
  /** What a delivery read returns for rich text when the request does not say (`?richText=`). */
13581
13640
  const DEFAULT_RICH_TEXT_MODE = "json";
13582
13641
  /** `?seo=`: SEO fields as stored, or merged with the site's SEO defaults (plan seo-fields). */
13642
+ /** Which heads a delivery read serves (`?publicationState=`, GraphQL's `publicationState`). */
13643
+ const PUBLICATION_STATES = ["published", "draft"];
13583
13644
  const SEO_MODES = ["raw", "resolved"];
13584
13645
  const ENTRY_LIST_STATUSES = [
13585
13646
  "draft",
@@ -14047,6 +14108,7 @@ const TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
14047
14108
  "snapshot",
14048
14109
  "richText",
14049
14110
  "seo",
14111
+ "publicationState",
14050
14112
  "q",
14051
14113
  "status",
14052
14114
  "author"
@@ -14226,6 +14288,18 @@ const parseSeo = (tree, context) => {
14226
14288
  if (value === "resolved" && !context.model.fields.some((field) => !field.deprecated && isSeoField(field) && context.isReadable(field))) throw queryInvalid(`"${context.model.apiKey}" has no SEO field to resolve`);
14227
14289
  return { seo: value };
14228
14290
  };
14291
+ const PUBLICATION_STATE_SET = new Set(PUBLICATION_STATES);
14292
+ /**
14293
+ * `?publicationState=published|draft` on delivery reads; whether the caller may read drafts is the service's
14294
+ * check.
14295
+ */
14296
+ const parsePublicationState = (tree, context) => {
14297
+ const value = single(tree.publicationState, "publicationState");
14298
+ if (value === void 0) return {};
14299
+ if (!context.allowPublicationState) throw queryInvalid("publicationState is only available on the delivery API");
14300
+ if (!PUBLICATION_STATE_SET.has(value)) throw queryInvalid(`"publicationState" must be one of ${PUBLICATION_STATES.join(", ")}`);
14301
+ return { publicationState: value };
14302
+ };
14229
14303
  const parseContentQuery = (tree, context) => {
14230
14304
  for (const key of Object.keys(tree)) if (!TOP_LEVEL_KEYS.has(key)) throw queryInvalid(`Unknown query parameter "${key}"`);
14231
14305
  const filters = tree.filters;
@@ -14249,6 +14323,7 @@ const parseContentQuery = (tree, context) => {
14249
14323
  snapshot,
14250
14324
  ...parseRichText(tree, context),
14251
14325
  ...parseSeo(tree, context),
14326
+ ...parsePublicationState(tree, context),
14252
14327
  ...parseAdminFilters(tree, context)
14253
14328
  };
14254
14329
  };
@@ -14868,6 +14943,7 @@ const parseQueryFor = (context, model, policy, rawQuery, options) => parseConten
14868
14943
  allowAdminFilters: options.allowAdminFilters ?? false,
14869
14944
  allowRichText: options.allowRichText ?? false,
14870
14945
  allowSeo: options.allowSeo ?? false,
14946
+ allowPublicationState: options.allowPublicationState ?? false,
14871
14947
  resolveModel: (modelId) => resolveModelById(context.snapshot, modelId)?.definition
14872
14948
  });
14873
14949
  /** The admin content list: drafts, filtered/sorted/paginated by the same compiler as delivery. */
@@ -14967,13 +15043,12 @@ const SYSTEM_PRINCIPAL = {
14967
15043
  kind: "system",
14968
15044
  component: EXTENSION_SYSTEM_COMPONENT
14969
15045
  };
14970
- const contentContext = async (environment, site, principal) => ({
15046
+ const contentContext = async (environment, site, principal) => buildContentContext({
14971
15047
  db: environment.db,
14972
- snapshot: (await environment.registry.getSnapshot()).forSite(site.id),
15048
+ network: await environment.registry.getSnapshot(),
14973
15049
  permissions: environment.permissions,
14974
15050
  actor: principal ?? SYSTEM_PRINCIPAL,
14975
- site,
14976
- hooks: createContentHooks()
15051
+ site
14977
15052
  });
14978
15053
  const toReadEntry = (view) => ({
14979
15054
  id: view.id,
@@ -15258,157 +15333,6 @@ const createLoggerOptions = (config) => ({
15258
15333
  });
15259
15334
  const createLogger = (config) => pino(createLoggerOptions(config));
15260
15335
  //#endregion
15261
- //#region src/media/contentDisposition.ts
15262
- /**
15263
- * A Content-Disposition value that names the file safely: an ASCII fallback plus the RFC 5987 UTF-8 form,
15264
- * so quotes, semicolons or line breaks in a filename can never inject header parameters.
15265
- */
15266
- const contentDisposition = (filename, type = "inline") => {
15267
- return `${type}; filename="${filename.replace(/[^\x20-\x7e]/g, "_").replace(/["\\]/g, "_")}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
15268
- };
15269
- /** Types a browser only ever displays passively: opening one directly cannot run script. */
15270
- const PASSIVE_INLINE_TYPE = /^(image\/(png|jpeg|gif|webp|avif|bmp|tiff|x-icon|vnd\.microsoft\.icon)|video\/.+|audio\/.+)$/;
15271
- /**
15272
- * Content-Disposition to store on an object in a bucket. Public objects can be served straight from the
15273
- * bucket or a CDN, where Shapio's sandboxing headers are absent; anything that could run as a page there
15274
- * (SVG, HTML, XML, PDF...) is stored as an attachment, so opening it downloads it. `<img>` tags still show
15275
- * an SVG, since embedding ignores Content-Disposition.
15276
- */
15277
- const bucketContentDisposition = (contentType) => PASSIVE_INLINE_TYPE.test(contentType.toLowerCase()) ? void 0 : "attachment";
15278
- //#endregion
15279
- //#region src/media/errors.ts
15280
- /** Thrown by adapters when a key has no object, so callers can tell "missing" from "storage is down". */
15281
- var ObjectNotFoundError = class extends Error {
15282
- constructor(key, options) {
15283
- super(`No stored object at ${key}`, options);
15284
- this.name = "ObjectNotFoundError";
15285
- }
15286
- };
15287
- var InvalidStorageKeyError = class extends Error {
15288
- constructor(key) {
15289
- super(`Not a valid media storage key: ${JSON.stringify(key)}`);
15290
- this.name = "InvalidStorageKeyError";
15291
- }
15292
- };
15293
- //#endregion
15294
- //#region src/media/s3Adapter.ts
15295
- /** R2 accepts any region name ("auto" by convention); AWS needs the bucket's real region. */
15296
- const DEFAULT_REGION = "us-east-1";
15297
- const isNotFound$2 = (error) => error instanceof S3ServiceException && (error.$metadata.httpStatusCode === 404 || error.name === "NotFound" || error.name === "NoSuchKey");
15298
- const createS3Client = (config) => new S3Client({
15299
- region: config.region ?? DEFAULT_REGION,
15300
- ...config.endpoint ? { endpoint: config.endpoint } : {},
15301
- forcePathStyle: config.forcePathStyle,
15302
- ...config.accessKeyId && config.secretAccessKey ? { credentials: {
15303
- accessKeyId: config.accessKeyId,
15304
- secretAccessKey: config.secretAccessKey
15305
- } } : {},
15306
- requestChecksumCalculation: "WHEN_REQUIRED",
15307
- responseChecksumValidation: "WHEN_REQUIRED"
15308
- });
15309
- /**
15310
- * Any S3-compatible service (AWS S3, Cloudflare R2, B2, Spaces, Wasabi, versitygw). Private objects are read
15311
- * through presigned GET URLs; browsers upload straight to the bucket with a presigned POST whose policy
15312
- * limits size and content type, so Shapio never streams upload bytes.
15313
- */
15314
- const createS3Adapter = ({ config, publicBaseUrl, client }) => {
15315
- const s3 = client ?? createS3Client(config);
15316
- const Bucket = config.bucket;
15317
- const checkKey = (key) => {
15318
- if (!isValidStorageKey(key)) throw new InvalidStorageKeyError(key);
15319
- return key;
15320
- };
15321
- const head = async (key) => {
15322
- try {
15323
- const result = await s3.send(new HeadObjectCommand({
15324
- Bucket,
15325
- Key: checkKey(key)
15326
- }));
15327
- return {
15328
- size: result.ContentLength ?? 0,
15329
- contentType: result.ContentType
15330
- };
15331
- } catch (error) {
15332
- if (isNotFound$2(error)) return;
15333
- throw error;
15334
- }
15335
- };
15336
- return {
15337
- driver: "s3",
15338
- put: async (key, body, { contentType, contentLength }) => {
15339
- await s3.send(new PutObjectCommand({
15340
- Bucket,
15341
- Key: checkKey(key),
15342
- Body: body,
15343
- ContentType: contentType,
15344
- ContentLength: Buffer.isBuffer(body) ? body.length : contentLength,
15345
- ContentDisposition: bucketContentDisposition(contentType)
15346
- }));
15347
- },
15348
- getStream: async (key, range) => {
15349
- try {
15350
- const result = await s3.send(new GetObjectCommand({
15351
- Bucket,
15352
- Key: checkKey(key),
15353
- ...range ? { Range: `bytes=${range.start}-${range.end}` } : {}
15354
- }));
15355
- if (!result.Body) throw new ObjectNotFoundError(key);
15356
- return result.Body;
15357
- } catch (error) {
15358
- throw isNotFound$2(error) ? new ObjectNotFoundError(key, { cause: error }) : error;
15359
- }
15360
- },
15361
- head,
15362
- exists: async (key) => await head(key) !== void 0,
15363
- delete: async (key) => {
15364
- await s3.send(new DeleteObjectCommand({
15365
- Bucket,
15366
- Key: checkKey(key)
15367
- }));
15368
- },
15369
- signedGetUrl: async (key, { expiresInSeconds, filename, contentType }) => getSignedUrl(s3, new GetObjectCommand({
15370
- Bucket,
15371
- Key: checkKey(key),
15372
- ResponseContentType: contentType,
15373
- ResponseContentDisposition: contentDisposition(filename)
15374
- }), { expiresIn: expiresInSeconds }),
15375
- publicUrl: (key) => publicBaseUrl ? `${publicBaseUrl}/${encodeKeyPath(checkKey(key))}` : void 0,
15376
- presignedUpload: async ({ key, contentType, maxSizeBytes, expiresInSeconds }) => {
15377
- const disposition = bucketContentDisposition(contentType);
15378
- const { url, fields } = await createPresignedPost(s3, {
15379
- Bucket,
15380
- Key: checkKey(key),
15381
- Conditions: [
15382
- [
15383
- "content-length-range",
15384
- 1,
15385
- maxSizeBytes
15386
- ],
15387
- [
15388
- "eq",
15389
- "$Content-Type",
15390
- contentType
15391
- ],
15392
- ...disposition ? [[
15393
- "eq",
15394
- "$Content-Disposition",
15395
- disposition
15396
- ]] : []
15397
- ],
15398
- Fields: {
15399
- "Content-Type": contentType,
15400
- ...disposition ? { "Content-Disposition": disposition } : {}
15401
- },
15402
- Expires: expiresInSeconds
15403
- });
15404
- return {
15405
- url,
15406
- fields
15407
- };
15408
- }
15409
- };
15410
- };
15411
- //#endregion
15412
15336
  //#region src/media/origins.ts
15413
15337
  const SAMPLE_KEY = "public/00000000-0000-0000-0000-000000000000/sample/origin-probe";
15414
15338
  const originOf = (url) => {
@@ -15429,6 +15353,7 @@ const resolveMediaOrigins = async (storage) => {
15429
15353
  const base = originOf(storage.publicBaseUrl);
15430
15354
  if (base) origins.add(base);
15431
15355
  if (storage.s3) {
15356
+ const { createS3Adapter } = await import("./s3Adapter-BipoQiwQ.js");
15432
15357
  const adapter = createS3Adapter({
15433
15358
  config: storage.s3,
15434
15359
  publicBaseUrl: void 0
@@ -16086,7 +16011,42 @@ const memoizePermissions = (base, principal) => {
16086
16011
  };
16087
16012
  };
16088
16013
  //#endregion
16014
+ //#region src/permissions/scoped.ts
16015
+ const isLazy = (versions) => "read" in versions;
16016
+ /**
16017
+ * The evaluator at the scope's versions. Inside a transaction before a lazy scope read its versions, the
16018
+ * unbound evaluator checks the versions itself through the transaction, so no pooled connection is asked
16019
+ * for while the transaction holds one.
16020
+ */
16021
+ const boundEvaluator = async (base, versions, executor) => {
16022
+ if (!isLazy(versions)) return base.atVersions?.(versions) ?? base;
16023
+ if (executor && !versions.isRead()) return base;
16024
+ return base.atVersions?.(await versions.read()) ?? base;
16025
+ };
16026
+ /**
16027
+ * One scope's evaluator (an HTTP request, an in-process delivery call): checked against the scope's durable
16028
+ * versions (ADR 0005: a revoke applies from the next scope, not mid-scope) and memoized per (principal, action,
16029
+ * model) for the scope (`memo.ts`).
16030
+ */
16031
+ const scopePermissions = (base, principal, versions) => memoizePermissions({
16032
+ evaluate: async (caller, policyRequest, executor) => (await boundEvaluator(base, versions, executor)).evaluate(caller, policyRequest, executor),
16033
+ canPerform: async (caller, action, executor) => (await boundEvaluator(base, versions, executor)).canPerform(caller, action, executor),
16034
+ canPerformOnSite: async (caller, action, siteId, executor) => (await boundEvaluator(base, versions, executor)).canPerformOnSite(caller, action, siteId, executor)
16035
+ }, principal);
16036
+ //#endregion
16037
+ //#region src/constants/delivery.ts
16038
+ /** `system_settings` key of the delivery descriptor the server writes at start (services/deliveryDescriptor.ts). */
16039
+ const DELIVERY_DESCRIPTOR_SETTING = "delivery_descriptor";
16040
+ //#endregion
16089
16041
  //#region src/repositories/requestState.ts
16042
+ /** The site a lookup names, joined to the one `system_versions` row (at most one row matches). */
16043
+ const siteJoin = (join, lookup) => {
16044
+ switch (lookup.by) {
16045
+ case "id": return join.on("sites.id", "=", lookup.id);
16046
+ case "key": return join.on("sites.key", "=", lookup.key);
16047
+ case "primary": return join.on("sites.is_primary", "=", true);
16048
+ }
16049
+ };
16090
16050
  /**
16091
16051
  * Everything a request checks before its work, in one statement: the durable schema and permissions versions
16092
16052
  * (`system_versions`, one row) and, when asked, the request's site. Read once per request on the pool, before
@@ -16101,13 +16061,7 @@ const read = async (lookup, trx = db) => {
16101
16061
  site: void 0
16102
16062
  };
16103
16063
  }
16104
- const row = await trx.selectFrom("system_versions").leftJoin("sites", (join) => {
16105
- switch (lookup.by) {
16106
- case "id": return join.on("sites.id", "=", lookup.id);
16107
- case "key": return join.on("sites.key", "=", lookup.key);
16108
- case "primary": return join.on("sites.is_primary", "=", true);
16109
- }
16110
- }).select([
16064
+ const row = await trx.selectFrom("system_versions").leftJoin("sites", (join) => siteJoin(join, lookup)).select([
16111
16065
  "system_versions.schema_version",
16112
16066
  "system_versions.permissions_version",
16113
16067
  "sites.id as site_id",
@@ -16150,27 +16104,15 @@ const getRequestState = (request, lookup) => {
16150
16104
  /** Reads the request's site with its state, or on its own when the state was already read without it. */
16151
16105
  const readRequestSite = async (request, lookup) => request.requestState ? findSite(lookup) : (await getRequestState(request, lookup)).site;
16152
16106
  /**
16153
- * The server's evaluator at the request's versions. Inside a transaction before the state was read (no route
16154
- * does that today), the unbound evaluator checks the versions itself through the transaction, so no pooled
16155
- * connection is asked for while the transaction holds one.
16156
- */
16157
- const boundPermissions = async (request, executor) => {
16158
- const base = request.server.permissions;
16159
- if (executor && !request.requestState) return base;
16160
- const { versions } = await getRequestState(request);
16161
- return base.atVersions?.(versions) ?? base;
16162
- };
16163
- /**
16164
16107
  * The request's evaluator: checked against the request's versions, and memoized per (principal, action,
16165
16108
  * model) for the request (`permissions/memo.ts`), for REST as for GraphQL. Create it after site resolution,
16166
16109
  * which settles the principal.
16167
16110
  */
16168
16111
  const getRequestPermissions = (request) => {
16169
- request.requestPermissions ??= memoizePermissions({
16170
- evaluate: async (principal, policyRequest, executor) => (await boundPermissions(request, executor)).evaluate(principal, policyRequest, executor),
16171
- canPerform: async (principal, action, executor) => (await boundPermissions(request, executor)).canPerform(principal, action, executor),
16172
- canPerformOnSite: async (principal, action, siteId, executor) => (await boundPermissions(request, executor)).canPerformOnSite(principal, action, siteId, executor)
16173
- }, request.principal);
16112
+ request.requestPermissions ??= scopePermissions(request.server.permissions, request.principal, {
16113
+ read: async () => (await getRequestState(request)).versions,
16114
+ isRead: () => request.requestState !== null
16115
+ });
16174
16116
  return request.requestPermissions;
16175
16117
  };
16176
16118
  //#endregion
@@ -16413,12 +16355,14 @@ const revokeApiToken = async (context, permissions, id) => {
16413
16355
  });
16414
16356
  });
16415
16357
  };
16358
+ /** Whether a token with this expiry has expired at `now`. */
16359
+ const isTokenExpired = (expiresAt, now) => expiresAt !== null && expiresAt.getTime() <= now.getTime();
16416
16360
  /** The principal for a bearer API token, or undefined when unknown, revoked or expired. */
16417
- const resolveApiToken = async (value, now = /* @__PURE__ */ new Date()) => {
16418
- const row = await findLiveByHash$1(hashToken(value));
16419
- if (!row || row.expires_at !== null && row.expires_at.getTime() <= now.getTime()) return;
16361
+ const resolveApiToken = async (value, now = /* @__PURE__ */ new Date(), { recordUse = true, executor = db } = {}) => {
16362
+ const row = await findLiveByHash$1(hashToken(value), executor);
16363
+ if (!row || isTokenExpired(row.expires_at, now)) return;
16420
16364
  const staleBefore = /* @__PURE__ */ new Date(now.getTime() - LAST_SEEN_WRITE_INTERVAL_MS);
16421
- if (row.last_used_at === null || row.last_used_at.getTime() < staleBefore.getTime()) await touchLastUsed$1(row.id, now, staleBefore);
16365
+ if (recordUse && (row.last_used_at === null || row.last_used_at.getTime() < staleBefore.getTime())) await touchLastUsed$1(row.id, now, staleBefore, executor);
16422
16366
  return {
16423
16367
  kind: "token",
16424
16368
  tokenId: row.id,
@@ -17590,7 +17534,7 @@ const editorManifestPlugin = fp(async (app, { manifest }) => {
17590
17534
  app.decorate("editorManifest", manifest);
17591
17535
  }, { name: "shapio-editor-manifest" });
17592
17536
  //#endregion
17593
- //#region src/plugins/errorHandler.ts
17537
+ //#region src/helpers/errorResponse.ts
17594
17538
  const STATUS_CODES = {
17595
17539
  400: "BAD_REQUEST",
17596
17540
  401: "UNAUTHORIZED",
@@ -17607,6 +17551,10 @@ const STATUS_CODES = {
17607
17551
  };
17608
17552
  /** Errors raised by @fastify/csrf-protection (plugins/csrf.ts). */
17609
17553
  const CSRF_ERROR_CODES$1 = /* @__PURE__ */ new Set(["FST_CSRF_INVALID_TOKEN", "FST_CSRF_MISSING_SECRET"]);
17554
+ /**
17555
+ * The one error shape every endpoint returns, `{ error: { code, message, details? } }`, and its status.
17556
+ * Server errors (5xx) never carry their internal text; the caller logs the original error.
17557
+ */
17610
17558
  const toErrorResponse = (error) => {
17611
17559
  if (error instanceof AppError) {
17612
17560
  const body = { error: {
@@ -17619,7 +17567,7 @@ const toErrorResponse = (error) => {
17619
17567
  body
17620
17568
  };
17621
17569
  }
17622
- if (CSRF_ERROR_CODES$1.has(error.code)) return {
17570
+ if (error.code !== void 0 && CSRF_ERROR_CODES$1.has(error.code)) return {
17623
17571
  statusCode: 403,
17624
17572
  body: { error: {
17625
17573
  code: "CSRF_INVALID",
@@ -17654,6 +17602,8 @@ const toErrorResponse = (error) => {
17654
17602
  } }
17655
17603
  };
17656
17604
  };
17605
+ //#endregion
17606
+ //#region src/plugins/errorHandler.ts
17657
17607
  /** The single error handler: consistent `{ error: { code, message, details? } }` bodies and logging. */
17658
17608
  const errorHandlerPlugin = fp(async (app) => {
17659
17609
  app.decorateRequest("errorCode", void 0);
@@ -18097,10 +18047,11 @@ const csrfForGet = (app) => (request, reply, done) => {
18097
18047
  /**
18098
18048
  * Responses vary by credentials. GET queries get REST's validators (a strong ETag over the body, 304 on a
18099
18049
  * match) and its caching policy (public for anonymous callers, private otherwise); everything else is
18100
- * never stored.
18050
+ * never stored. An operation that read drafts (the controller set `DRAFTS_CACHE_CONTROL`) keeps it.
18101
18051
  */
18102
18052
  const cacheHeaders = async (request, reply, payload) => {
18103
18053
  appendVary(reply, DELIVERY_VARY);
18054
+ if (reply.getHeader("cache-control") === "private, no-store") return payload;
18104
18055
  if (request.method !== "GET" || reply.statusCode !== 200 || typeof payload !== "string") {
18105
18056
  reply.header("cache-control", "no-store");
18106
18057
  return payload;
@@ -18223,6 +18174,14 @@ const createMysqlNotificationListener = (options) => {
18223
18174
  });
18224
18175
  };
18225
18176
  const connect = async () => {
18177
+ if (closed) return;
18178
+ let mysql;
18179
+ try {
18180
+ mysql = await loadMysql();
18181
+ } catch (error) {
18182
+ log.error({ err: error }, "mysql2 could not be loaded; notifications are off");
18183
+ return;
18184
+ }
18226
18185
  if (closed) return;
18227
18186
  const current = mysql.createConnection(mysqlPoolOptions(connectionString, 1, options.applicationName ?? "shapio-listen"));
18228
18187
  connection = current;
@@ -18980,6 +18939,49 @@ const createLoaders = (siteId, content) => {
18980
18939
  };
18981
18940
  };
18982
18941
  //#endregion
18942
+ //#region src/schema/codegen/graphql/requestContext.ts
18943
+ /**
18944
+ * The context every resolver of one operation receives, with its own DataLoaders. Carries no HTTP objects, so
18945
+ * an operation can run outside a request.
18946
+ */
18947
+ const createGraphqlContext = ({ site, permissions, content }) => {
18948
+ let base;
18949
+ let drafts = false;
18950
+ const contentAt = async (snapshot) => {
18951
+ base ??= content(snapshot);
18952
+ return {
18953
+ ...await base,
18954
+ snapshot,
18955
+ permissions
18956
+ };
18957
+ };
18958
+ return {
18959
+ permissions,
18960
+ markDrafts: () => {
18961
+ drafts = true;
18962
+ },
18963
+ readDrafts: () => drafts,
18964
+ site,
18965
+ loaders: createLoaders(() => site.id, contentAt),
18966
+ content: contentAt
18967
+ };
18968
+ };
18969
+ //#endregion
18970
+ //#region src/helpers/siteQuery.ts
18971
+ const isSiteSegment = (segment) => {
18972
+ const key = segment.split("=", 1)[0] ?? "";
18973
+ try {
18974
+ return decodeURIComponent(key.replace(/\+/g, " ")) === SITE_QUERY_PARAMETER;
18975
+ } catch {
18976
+ return false;
18977
+ }
18978
+ };
18979
+ /**
18980
+ * A raw querystring (without `?`) without its `site` parameter: site resolution consumes it, and the content
18981
+ * compiler refuses unknown keys. The rest is left exactly as sent (bracket syntax is the compiler's to parse).
18982
+ */
18983
+ const withoutSiteParameter = (rawQuery) => rawQuery.split("&").filter((segment) => !isSiteSegment(segment)).join("&");
18984
+ //#endregion
18983
18985
  //#region src/controllers/contentContext.ts
18984
18986
  /**
18985
18987
  * The service context for a content request: the pinned snapshot, the principal and dependencies.
@@ -18987,9 +18989,9 @@ const createLoaders = (siteId, content) => {
18987
18989
  * memoized per (principal, action, model), so the read, relation visibility, populate and usage recording
18988
18990
  * evaluate each model's policy once.
18989
18991
  */
18990
- const contentContextFor = async (request) => ({
18992
+ const contentContextFor = async (request) => buildContentContext({
18991
18993
  db: request.server.db,
18992
- snapshot: await getRequestSchema(request),
18994
+ network: await getRequestNetworkSchema(request),
18993
18995
  permissions: getRequestPermissions(request),
18994
18996
  actor: request.principal,
18995
18997
  site: getRequestSite(request),
@@ -19001,14 +19003,6 @@ const contentContextFor = async (request) => ({
19001
19003
  requestId: request.id,
19002
19004
  ip: request.ip
19003
19005
  });
19004
- const isSiteSegment = (segment) => {
19005
- const key = segment.split("=", 1)[0] ?? "";
19006
- try {
19007
- return decodeURIComponent(key.replace(/\+/g, " ")) === SITE_QUERY_PARAMETER;
19008
- } catch {
19009
- return false;
19010
- }
19011
- };
19012
19006
  /**
19013
19007
  * The raw querystring (bracket syntax is parsed by the content compiler, not Fastify's flat parser), without
19014
19008
  * `?site=`: site resolution (plugins/siteResolution.ts) consumed it, and the compiler refuses unknown keys.
@@ -19016,34 +19010,16 @@ const isSiteSegment = (segment) => {
19016
19010
  const rawQueryOf = (request) => {
19017
19011
  const index = request.url.indexOf("?");
19018
19012
  if (index === -1) return "";
19019
- return request.url.slice(index + 1).split("&").filter((segment) => !isSiteSegment(segment)).join("&");
19013
+ return withoutSiteParameter(request.url.slice(index + 1));
19020
19014
  };
19021
19015
  //#endregion
19022
19016
  //#region src/controllers/graphqlContext.ts
19023
- /** Admin users and admin-scope API tokens: may read drafts. */
19024
- const isAdminPrincipal = (principal) => principal.kind === "admin" || principal.kind === "token" && principal.scope === "admin";
19025
19017
  /** The context every resolver of one `/api/graphql` request receives (built after site resolution). */
19026
- const createGraphqlRequestContext = (request) => {
19027
- const site = getRequestSite(request);
19028
- const permissions = getRequestPermissions(request);
19029
- let base;
19030
- const content = async (snapshot) => {
19031
- base ??= contentContextFor(request);
19032
- return {
19033
- ...await base,
19034
- snapshot,
19035
- permissions
19036
- };
19037
- };
19038
- return {
19039
- request,
19040
- permissions,
19041
- isAdmin: isAdminPrincipal(request.principal),
19042
- site,
19043
- loaders: createLoaders(() => site.id, content),
19044
- content
19045
- };
19046
- };
19018
+ const createGraphqlRequestContext = (request) => createGraphqlContext({
19019
+ site: getRequestSite(request),
19020
+ permissions: getRequestPermissions(request),
19021
+ content: () => contentContextFor(request)
19022
+ });
19047
19023
  //#endregion
19048
19024
  //#region src/controllers/graphql.ts
19049
19025
  const introspectionDisabled = () => new AppError(403, "INTROSPECTION_DISABLED", "Schema introspection needs an admin session or an API token (or GRAPHQL_PUBLIC_INTROSPECTION=true)");
@@ -19077,8 +19053,10 @@ const runGraphql = async (request, reply, input) => {
19077
19053
  }
19078
19054
  if (prepared.selectsIntrospection && !mayIntrospect) throw introspectionDisabled();
19079
19055
  const usage = collectUsage(request, schema, prepared.document, prepared.operation, input.variables ?? void 0);
19080
- const result = await executeOperation(schema, prepared, input, createGraphqlRequestContext(request));
19056
+ const context = createGraphqlRequestContext(request);
19057
+ const result = await executeOperation(schema, prepared, input, context);
19081
19058
  recordUsage$1(request, usage, result.data !== null && result.data !== void 0);
19059
+ if (context.readDrafts()) reply.header("cache-control", DRAFTS_CACHE_CONTROL);
19082
19060
  return send$1(reply, formatGraphqlResult(result, { log: request.log }));
19083
19061
  };
19084
19062
  const postGraphql = async (request, reply) => runGraphql(request, reply, request.body);
@@ -19492,7 +19470,7 @@ const createFixedTypes = () => {
19492
19470
  entry: createEntry$2(),
19493
19471
  publicationState: new GraphQLEnumType({
19494
19472
  name: "PublicationState",
19495
- description: "DRAFT is for admin principals only (previews).",
19473
+ description: "DRAFT is for admin principals and delivery tokens granted Read drafts (development servers).",
19496
19474
  values: {
19497
19475
  PUBLISHED: { value: "published" },
19498
19476
  DRAFT: { value: "draft" }
@@ -20473,6 +20451,26 @@ const restFieldReads = async ({ snapshot, model, policy, query, policyFor }) =>
20473
20451
  return reads;
20474
20452
  };
20475
20453
  //#endregion
20454
+ //#region src/services/draftAccess.ts
20455
+ /**
20456
+ * Who may read draft heads through the delivery API (plan drafts-mode §1). Admin users, admin-scope tokens
20457
+ * and Shapio itself always could (GraphQL previews). A delivery token may when its role grants `readDrafts`,
20458
+ * which always covers every model, so this one check per request also covers relation targets. App users and
20459
+ * anonymous callers never may, whatever a role says (the evaluator denies them too). The caller has already
20460
+ * checked `read` on the model.
20461
+ */
20462
+ const isAdminPrincipal = (principal) => principal.kind === "system" || principal.kind === "admin" || principal.kind === "token" && principal.scope === "admin";
20463
+ const draftsForbidden = (message) => new AppError(403, "DRAFTS_FORBIDDEN", message);
20464
+ const assertMayReadDrafts = async (context, modelId, modelKey) => {
20465
+ const { actor } = context;
20466
+ if (isAdminPrincipal(actor)) return;
20467
+ if (actor.kind !== "token") throw draftsForbidden("Drafts need an admin user or a delivery token granted Read drafts");
20468
+ if (!(await context.permissions.evaluate(actor, {
20469
+ action: "readDrafts",
20470
+ modelId
20471
+ })).allowed) throw draftsForbidden(`This token may not read drafts of "${modelKey}"; grant Read drafts on its delivery role`);
20472
+ };
20473
+ //#endregion
20476
20474
  //#region src/services/contentDelivery.ts
20477
20475
  const readableModel = async (context, modelKey) => {
20478
20476
  const model = resolveModel(context.snapshot, modelKey);
@@ -20486,10 +20484,19 @@ const readableModel = async (context, modelKey) => {
20486
20484
  policy
20487
20485
  };
20488
20486
  };
20489
- /** The heads a read serves: drafts (GraphQL previews), a pinned snapshot, or the published heads. */
20490
- const sourceOf = (query, options) => {
20491
- if (query.snapshot !== void 0 && options.drafts) throw queryInvalid("snapshot reads published content; it cannot be combined with drafts");
20492
- return options.drafts ? {
20487
+ /** Whether a read asks for drafts: GraphQL's option, or `?publicationState=draft`. */
20488
+ const readsDrafts = (query, options) => options.drafts === true || query.publicationState === "draft";
20489
+ /**
20490
+ * A draft read must be allowed: `read` was checked already; `readDrafts` (or an admin principal) is checked
20491
+ * here.
20492
+ */
20493
+ const authorizeDrafts$1 = async (context, model, modelKey, drafts) => {
20494
+ if (drafts) await assertMayReadDrafts(context, model.definition.id, modelKey);
20495
+ };
20496
+ /** The heads a read serves: drafts, a pinned snapshot, or the published heads. */
20497
+ const sourceOf = (query, drafts) => {
20498
+ if (query.snapshot !== void 0 && drafts) throw queryInvalid("snapshot reads published content; it cannot be combined with drafts");
20499
+ return drafts ? {
20493
20500
  kind: "heads",
20494
20501
  state: "draft"
20495
20502
  } : query.snapshot !== void 0 ? {
@@ -20510,7 +20517,7 @@ const snapshotCheck = (query) => (current) => {
20510
20517
  */
20511
20518
  const recordUsage = async (context, options, model, policy, query) => {
20512
20519
  const principalKey = usagePrincipalKey(context.actor);
20513
- if (!options.usage || options.drafts || principalKey === null) return;
20520
+ if (!options.usage || readsDrafts(query, options) || principalKey === null) return;
20514
20521
  const reads = await restFieldReads({
20515
20522
  snapshot: context.snapshot,
20516
20523
  model,
@@ -20538,8 +20545,24 @@ const ITEM_PARAMETERS$1 = /* @__PURE__ */ new Set([
20538
20545
  "locale",
20539
20546
  "snapshot",
20540
20547
  "richText",
20541
- "seo"
20548
+ "seo",
20549
+ "publicationState"
20542
20550
  ]);
20551
+ const DELIVERY_PARSE = {
20552
+ allowSnapshot: true,
20553
+ allowRichText: true,
20554
+ allowSeo: true,
20555
+ allowPublicationState: true
20556
+ };
20557
+ /** Meta every delivery response carries; draft reads add `publicationState` (published ones are unchanged). */
20558
+ const metaOf$1 = (locale, snapshot, drafts) => drafts ? {
20559
+ locale,
20560
+ snapshot,
20561
+ publicationState: "draft"
20562
+ } : {
20563
+ locale,
20564
+ snapshot
20565
+ };
20543
20566
  /** Reading one entry takes only these query parameters; anything else is refused, not ignored. */
20544
20567
  const assertItemParameters = (rawQuery, allowed) => {
20545
20568
  const unsupported = [...new URLSearchParams(rawQuery).keys()].find((key) => !allowed.has(key.split("[")[0] ?? key));
@@ -20548,16 +20571,15 @@ const assertItemParameters = (rawQuery, allowed) => {
20548
20571
  /** `GET /api/content/:modelKey`: a page of a collection, or a singleton's one entry. */
20549
20572
  const listDelivery$1 = async (context, modelKey, rawQuery, options = {}) => {
20550
20573
  const { model, policy } = await readableModel(context, modelKey);
20551
- const query = parseQueryFor(context, model, policy, rawQuery, {
20552
- allowSnapshot: true,
20553
- allowRichText: true,
20554
- allowSeo: true
20555
- });
20574
+ const query = parseQueryFor(context, model, policy, rawQuery, DELIVERY_PARSE);
20575
+ const drafts = readsDrafts(query, options);
20576
+ const source = sourceOf(query, drafts);
20577
+ await authorizeDrafts$1(context, model, modelKey, drafts);
20556
20578
  const locale = query.locale ?? context.snapshot.defaultLocale;
20557
20579
  const singleton = model.definition.kind === "singleton";
20558
20580
  const { entries, limit, total, seq } = await readEntryPage({
20559
20581
  context,
20560
- source: sourceOf(query, options),
20582
+ source,
20561
20583
  model,
20562
20584
  policy,
20563
20585
  query,
@@ -20573,34 +20595,32 @@ const listDelivery$1 = async (context, modelKey, rawQuery, options = {}) => {
20573
20595
  if (!entry) throw entryNotFound(modelKey);
20574
20596
  result = {
20575
20597
  data: entry,
20576
- meta: {
20577
- locale,
20578
- snapshot
20579
- }
20598
+ meta: metaOf$1(locale, snapshot, drafts)
20580
20599
  };
20581
20600
  } else result = {
20582
20601
  data: entries,
20583
20602
  meta: {
20584
- locale,
20585
- snapshot,
20603
+ ...metaOf$1(locale, snapshot, drafts),
20586
20604
  pagination: paginationMeta(query.page, limit, total)
20587
20605
  }
20588
20606
  };
20589
20607
  await afterRead(options, () => recordUsage(context, options, model, policy, query));
20590
20608
  return result;
20591
20609
  };
20592
- /** `GET /api/content/:modelKey/:id`: one published entry (404 for drafts, unknown or unreadable entries). */
20610
+ /**
20611
+ * `GET /api/content/:modelKey/:id`: one published entry (404 for unpublished, unknown or unreadable entries);
20612
+ * with drafts, the entry's draft.
20613
+ */
20593
20614
  const getDelivery$1 = async (context, modelKey, id, rawQuery, options = {}) => {
20594
20615
  assertItemParameters(rawQuery, ITEM_PARAMETERS$1);
20595
20616
  const { model, policy } = await readableModel(context, modelKey);
20596
- const query = parseQueryFor(context, model, policy, rawQuery, {
20597
- allowSnapshot: true,
20598
- allowRichText: true,
20599
- allowSeo: true
20600
- });
20617
+ const query = parseQueryFor(context, model, policy, rawQuery, DELIVERY_PARSE);
20618
+ const drafts = readsDrafts(query, options);
20619
+ const source = sourceOf(query, drafts);
20620
+ await authorizeDrafts$1(context, model, modelKey, drafts);
20601
20621
  const { entry, seq } = await readOneEntry({
20602
20622
  context,
20603
- source: sourceOf(query, options),
20623
+ source,
20604
20624
  model,
20605
20625
  policy,
20606
20626
  query,
@@ -20610,10 +20630,7 @@ const getDelivery$1 = async (context, modelKey, id, rawQuery, options = {}) => {
20610
20630
  await afterRead(options, () => recordUsage(context, options, model, policy, query));
20611
20631
  return {
20612
20632
  data: entry,
20613
- meta: {
20614
- locale: query.locale ?? context.snapshot.defaultLocale,
20615
- snapshot: query.snapshot ?? seq
20616
- }
20633
+ meta: metaOf$1(query.locale ?? context.snapshot.defaultLocale, query.snapshot ?? seq, drafts)
20617
20634
  };
20618
20635
  };
20619
20636
  //#endregion
@@ -20758,18 +20775,21 @@ const readArgs = (build) => ({
20758
20775
  publicationState: {
20759
20776
  type: build.fixed.publicationState,
20760
20777
  defaultValue: "published",
20761
- description: "DRAFT needs an admin user or an admin API token"
20778
+ description: "DRAFT needs an admin user, an admin API token or a delivery token granted Read drafts"
20762
20779
  },
20763
20780
  snapshot: {
20764
20781
  type: GraphQLInt,
20765
20782
  description: "Read published content as of this publication sequence"
20766
20783
  }
20767
20784
  });
20768
- const draftsForbidden = () => new AppError(403, "FORBIDDEN", "publicationState DRAFT needs an admin user or an admin API token");
20769
- /** The read scope of a root read; drafts only for admin principals (never delivery principals). */
20785
+ /**
20786
+ * The read scope of a root read. Drafts: the delivery service checks the caller may read them (admin
20787
+ * principals, delivery tokens granted Read drafts; plan drafts-mode) before reading any head, and the
20788
+ * response is marked never to be stored.
20789
+ */
20770
20790
  const scopeOf = (args, context) => {
20771
20791
  const drafts = args.publicationState === "draft";
20772
- if (drafts && !context.isAdmin) throw draftsForbidden();
20792
+ if (drafts) context.markDrafts();
20773
20793
  return {
20774
20794
  locale: args.locale ?? void 0,
20775
20795
  fallback: args.fallback ?? true,
@@ -21548,6 +21568,21 @@ const graphqlPlugin = fp(async (app, { config, urls }) => {
21548
21568
  ]
21549
21569
  });
21550
21570
  //#endregion
21571
+ //#region src/media/errors.ts
21572
+ /** Thrown by adapters when a key has no object, so callers can tell "missing" from "storage is down". */
21573
+ var ObjectNotFoundError = class extends Error {
21574
+ constructor(key, options) {
21575
+ super(`No stored object at ${key}`, options);
21576
+ this.name = "ObjectNotFoundError";
21577
+ }
21578
+ };
21579
+ var InvalidStorageKeyError = class extends Error {
21580
+ constructor(key) {
21581
+ super(`Not a valid media storage key: ${JSON.stringify(key)}`);
21582
+ this.name = "InvalidStorageKeyError";
21583
+ }
21584
+ };
21585
+ //#endregion
21551
21586
  //#region src/media/signing.ts
21552
21587
  const sign = (secret, purpose, subject, expiresAt) => createHmac("sha256", secret).update(`${purpose}\n${subject}\n${expiresAt}`).digest("base64url");
21553
21588
  const safeEqual = (a, b) => {
@@ -21665,9 +21700,10 @@ var StorageNotConfiguredError = class extends Error {
21665
21700
  };
21666
21701
  /**
21667
21702
  * Every adapter this instance can reach. Local disk is always available; S3 whenever its bucket is set,
21668
- * whatever STORAGE_DRIVER says, so assets keep being served while `shapio media migrate` moves them.
21703
+ * whatever STORAGE_DRIVER says, so assets keep being served while `shapio media migrate` moves them. The S3
21704
+ * adapter (and the AWS SDK) is loaded only then.
21669
21705
  */
21670
- const createMediaStorage = (config, deps) => {
21706
+ const createMediaStorage = async (config, deps) => {
21671
21707
  const adapters = /* @__PURE__ */ new Map();
21672
21708
  adapters.set("local", createLocalAdapter({
21673
21709
  root: config.mediaPath,
@@ -21675,10 +21711,13 @@ const createMediaStorage = (config, deps) => {
21675
21711
  signingSecret: deps.signingSecret,
21676
21712
  publicBaseUrl: config.publicBaseUrl
21677
21713
  }));
21678
- if (config.s3) adapters.set("s3", createS3Adapter({
21679
- config: config.s3,
21680
- publicBaseUrl: config.publicBaseUrl
21681
- }));
21714
+ if (config.s3) {
21715
+ const { createS3Adapter } = await import("./s3Adapter-BipoQiwQ.js");
21716
+ adapters.set("s3", createS3Adapter({
21717
+ config: config.s3,
21718
+ publicBaseUrl: config.publicBaseUrl
21719
+ }));
21720
+ }
21682
21721
  const get = (driver) => {
21683
21722
  const adapter = adapters.get(driver);
21684
21723
  if (!adapter) throw new StorageNotConfiguredError(driver);
@@ -21694,7 +21733,7 @@ const createMediaStorage = (config, deps) => {
21694
21733
  //#region src/plugins/mediaStorage.ts
21695
21734
  /** Exposes the media storage adapters to routes. Registered after the signing secret is resolved. */
21696
21735
  const mediaStoragePlugin = fp(async (app, { storage, urls, signingSecret }) => {
21697
- app.decorate("mediaStorage", createMediaStorage(storage, {
21736
+ app.decorate("mediaStorage", await createMediaStorage(storage, {
21698
21737
  urls,
21699
21738
  signingSecret
21700
21739
  }));
@@ -22492,7 +22531,8 @@ const outboundPolicy = (runtime, allowPrivateNetwork) => ({
22492
22531
  });
22493
22532
  //#endregion
22494
22533
  //#region src/services/roles.ts
22495
- const CONTENT_ACTION_SET = new Set(CONTENT_ACTIONS);
22534
+ const MODEL_ACTION_SET = /* @__PURE__ */ new Set([...CONTENT_ACTIONS, ...DELIVERY_ACTIONS]);
22535
+ const DELIVERY_ACTION_SET = new Set(DELIVERY_ACTIONS);
22496
22536
  const GLOBAL_ACTION_SET = new Set(GLOBAL_ACTIONS);
22497
22537
  const FIELD_SCOPED_ACTIONS = /* @__PURE__ */ new Set([
22498
22538
  "read",
@@ -22519,9 +22559,17 @@ const toRoleView = (role, grants) => ({
22519
22559
  });
22520
22560
  const invalidPermissions = (message, details) => new AppError(400, "INVALID_PERMISSIONS", message, details);
22521
22561
  /**
22562
+ * `readDrafts` (plan drafts-mode §1): delivery roles only, and always on every model with no condition or
22563
+ * field list, so one check per request covers every model a draft read reaches (relation targets too).
22564
+ */
22565
+ const validateReadDrafts = (kind, permission, at) => {
22566
+ if (kind !== "delivery") throw invalidPermissions("Read drafts is for delivery roles only", at);
22567
+ if (permission.modelId !== null || permission.condition !== null || permission.fieldIds !== null) throw invalidPermissions("Read drafts applies to every model, with no condition or field list", at);
22568
+ };
22569
+ /**
22522
22570
  * The server-side rules for grants (never only the UI): known actions, global actions only without a
22523
- * model, conditions and field lists only where they mean something, delivery roles read-only, no
22524
- * duplicate (action, model) pairs.
22571
+ * model, conditions and field lists only where they mean something, delivery roles read-only (`read`, and
22572
+ * `readDrafts` on every model), no duplicate (action, model) pairs.
22525
22573
  */
22526
22574
  const validatePermissions = (kind, permissions) => {
22527
22575
  const seen = /* @__PURE__ */ new Set();
@@ -22531,9 +22579,10 @@ const validatePermissions = (kind, permissions) => {
22531
22579
  action: permission.action
22532
22580
  };
22533
22581
  const isGlobal = GLOBAL_ACTION_SET.has(permission.action);
22534
- if (!isGlobal && !CONTENT_ACTION_SET.has(permission.action)) throw invalidPermissions("Unknown action", at);
22582
+ if (!isGlobal && !MODEL_ACTION_SET.has(permission.action)) throw invalidPermissions("Unknown action", at);
22535
22583
  if (isGlobal && (permission.modelId !== null || permission.condition !== null || permission.fieldIds !== null)) throw invalidPermissions("Global actions take no model, condition or field list", at);
22536
- if (kind === "delivery" && permission.action !== "read") throw invalidPermissions("Delivery roles can only read", at);
22584
+ if (kind === "delivery" && !DELIVERY_ACTION_SET.has(permission.action)) throw invalidPermissions("Delivery roles can only read", at);
22585
+ if (permission.action === "readDrafts") validateReadDrafts(kind, permission, at);
22537
22586
  if (permission.fieldIds !== null && !FIELD_SCOPED_ACTIONS.has(permission.action)) throw invalidPermissions("Field lists apply to read, create and update only", at);
22538
22587
  const key = `${permission.action}\u0000${permission.modelId ?? "*"}`;
22539
22588
  if (seen.has(key)) throw invalidPermissions("Each action may be granted once per model", at);
@@ -22885,7 +22934,11 @@ Type.Enum(SITE_ACTIONS);
22885
22934
  /** What `me.sitePermissions` lists: site actions, and network actions a role on the site grants there. */
22886
22935
  const SitePermissionSchema = Type.Enum([...SITE_ACTIONS, ...SITE_GRANTABLE_ACTION_LIST]);
22887
22936
  const PermissionSchema$1 = Type.Object({
22888
- action: Type.Enum([...CONTENT_ACTIONS, ...GLOBAL_ACTIONS]),
22937
+ action: Type.Enum([
22938
+ ...CONTENT_ACTIONS,
22939
+ READ_DRAFTS_ACTION,
22940
+ ...GLOBAL_ACTIONS
22941
+ ]),
22889
22942
  modelId: Type.Union([Type.String({
22890
22943
  minLength: 1,
22891
22944
  maxLength: 100
@@ -38999,7 +39052,7 @@ const openStoredBundle = (storage, stored) => storage.get(stored.driver).getStre
38999
39052
  const removeStoredBundle = (storage, stored) => storage.get(stored.driver).delete(stored.key);
39000
39053
  //#endregion
39001
39054
  //#region src/constants/version.ts
39002
- const SHAPIO_VERSION = "0.4.1";
39055
+ const SHAPIO_VERSION = "0.5.1";
39003
39056
  //#endregion
39004
39057
  //#region src/repositories/transferExport.ts
39005
39058
  /**
@@ -39791,21 +39844,21 @@ const importContent = async (run, initial) => {
39791
39844
  });
39792
39845
  };
39793
39846
  /** Deletes live target entries of the bundle's models that the bundle does not have. */
39794
- const prune = async (run, snapshot) => {
39847
+ const prune = async (run, network) => {
39795
39848
  const keep = /* @__PURE__ */ new Set();
39796
39849
  const models = /* @__PURE__ */ new Set();
39797
39850
  for await (const { record } of readBundle(await open$1(run))) if (record.type === "entry") {
39798
39851
  keep.add(record.id);
39799
39852
  models.add(record.modelId);
39800
39853
  }
39801
- const context = {
39854
+ const context = buildContentContext({
39802
39855
  db: run.deps.db,
39803
- snapshot,
39856
+ network,
39804
39857
  permissions: SYSTEM_PERMISSIONS,
39805
39858
  actor: SYSTEM_ACTOR,
39806
- site: await getSiteRef(run.payload.siteId, run.deps.db),
39807
- hooks: createContentHooks()
39808
- };
39859
+ site: await getSiteRef(run.payload.siteId, run.deps.db)
39860
+ });
39861
+ const { snapshot } = context;
39809
39862
  let failed = [];
39810
39863
  for (let round = 0; round < PRUNE_ROUNDS; round += 1) {
39811
39864
  failed = [];
@@ -39841,7 +39894,7 @@ const runPhase = async (run, phase) => {
39841
39894
  case "media": return importMedia(run);
39842
39895
  case "entries": return importEntryRows(run, await loadSiteSnapshot(run));
39843
39896
  case "content": return importContent(run, await loadSiteSnapshot(run));
39844
- case "prune": return run.payload.prune ? prune(run, await loadSiteSnapshot(run)) : void 0;
39897
+ case "prune": return run.payload.prune ? prune(run, await loadSnapshot(run.deps.db)) : void 0;
39845
39898
  case "cleanup": return removeStoredBundle(run.deps.storage, run.payload.bundle).catch((error) => run.job.log.warn({
39846
39899
  err: describeError(error),
39847
39900
  key: run.payload.bundle.key
@@ -41149,18 +41202,22 @@ const appAuthRoutes = async (app) => {
41149
41202
  };
41150
41203
  //#endregion
41151
41204
  //#region src/controllers/delivery.ts
41205
+ const cacheControlOf = (request, noStore) => {
41206
+ if (noStore) return DRAFTS_CACHE_CONTROL;
41207
+ return request.principal.kind === "anonymous" ? "public, max-age=0, must-revalidate" : "private, max-age=0, must-revalidate";
41208
+ };
41152
41209
  /**
41153
41210
  * Delivery responses are cacheable by validators: a strong ETag over the exact body (so it changes whenever
41154
41211
  * a head version, a populated target or the projection changes), `Vary` on the credentials that select the
41155
41212
  * principal and on the `Shapio-Site` header that selects the site (`?site=` is part of the URL), and `private`
41156
- * caching whenever a principal is authenticated (build plan §4.E6).
41213
+ * caching whenever a principal is authenticated (build plan §4.E6). `noStore`: draft reads, never stored.
41157
41214
  */
41158
- const sendCacheable = (request, reply, payload) => {
41215
+ const sendCacheable = (request, reply, payload, { noStore = false } = {}) => {
41159
41216
  const body = JSON.stringify(payload);
41160
41217
  const etag = `"${createHash("sha256").update(body).digest("base64url").slice(0, 32)}"`;
41161
41218
  reply.header("etag", etag);
41162
41219
  appendVary(reply, DELIVERY_VARY);
41163
- reply.header("cache-control", request.principal.kind === "anonymous" ? "public, max-age=0, must-revalidate" : "private, max-age=0, must-revalidate");
41220
+ reply.header("cache-control", cacheControlOf(request, noStore));
41164
41221
  const match = request.headers["if-none-match"];
41165
41222
  if (typeof match === "string" && match.split(",").some((candidate) => candidate.trim() === etag)) return reply.code(304).send();
41166
41223
  return reply.type("application/json; charset=utf-8").send(body);
@@ -41170,15 +41227,17 @@ const usageOptions = (request) => ({
41170
41227
  usage: request.server.usage,
41171
41228
  defer: (task) => request.server.recordUsageAfterResponse(request.log, task)
41172
41229
  });
41230
+ /** A delivery response, never stored by a cache when it carries drafts. */
41231
+ const sendDelivery = (request, reply, result) => sendCacheable(request, reply, result, { noStore: result.meta.publicationState === "draft" });
41173
41232
  const listDelivery = async (request, reply) => {
41174
41233
  const context = await contentContextFor(request);
41175
41234
  const apiKey = apiKeyOfRoute(context.snapshot, request.params.modelKey);
41176
- return sendCacheable(request, reply, await listDelivery$1(context, apiKey, rawQueryOf(request), usageOptions(request)));
41235
+ return sendDelivery(request, reply, await listDelivery$1(context, apiKey, rawQueryOf(request), usageOptions(request)));
41177
41236
  };
41178
41237
  const getDelivery = async (request, reply) => {
41179
41238
  const context = await contentContextFor(request);
41180
41239
  const apiKey = apiKeyOfRoute(context.snapshot, request.params.modelKey);
41181
- return sendCacheable(request, reply, await getDelivery$1(context, apiKey, request.params.id, rawQueryOf(request), usageOptions(request)));
41240
+ return sendDelivery(request, reply, await getDelivery$1(context, apiKey, request.params.id, rawQueryOf(request), usageOptions(request)));
41182
41241
  };
41183
41242
  //#endregion
41184
41243
  //#region src/controllers/deliveryWrites.ts
@@ -41846,6 +41905,16 @@ const itemParameters = (model, delivery) => [
41846
41905
  },
41847
41906
  description: "Rich-text shape: the JSON document, sanitized HTML rendered from it, or both"
41848
41907
  },
41908
+ {
41909
+ name: "publicationState",
41910
+ in: "query",
41911
+ schema: {
41912
+ type: "string",
41913
+ enum: [...PUBLICATION_STATES],
41914
+ default: "published"
41915
+ },
41916
+ description: "Drafts mode: `draft` reads saved drafts instead of published content. Needs a delivery token whose role grants Read drafts (or an admin user); responses are never cached. Cannot be combined with `snapshot`."
41917
+ },
41849
41918
  ...seoParameters(model)
41850
41919
  ] : []
41851
41920
  ];
@@ -41871,7 +41940,12 @@ const modelPaths = (model) => {
41871
41940
  type: "object",
41872
41941
  properties: {
41873
41942
  locale: { type: "string" },
41874
- snapshot: { type: "integer" }
41943
+ snapshot: { type: "integer" },
41944
+ publicationState: {
41945
+ type: "string",
41946
+ enum: ["draft"],
41947
+ description: "Present on draft reads only"
41948
+ }
41875
41949
  }
41876
41950
  };
41877
41951
  const listMeta = { allOf: [meta, {
@@ -42293,14 +42367,14 @@ const docsRoutes = async (app) => {
42293
42367
  //#endregion
42294
42368
  //#region src/db/migrations/20261001120000_create_platform_tables.ts
42295
42369
  var _20261001120000_create_platform_tables_exports = /* @__PURE__ */ __exportAll({
42296
- down: () => down$33,
42297
- up: () => up$33
42370
+ down: () => down$34,
42371
+ up: () => up$34
42298
42372
  });
42299
42373
  /**
42300
42374
  * Foundation tables shared by every package: durable versions, the job queue, the transactional outbox
42301
42375
  * and the audit log. Statuses are text with CHECK constraints so new states need only a constraint change.
42302
42376
  */
42303
- const up$33 = async (db) => {
42377
+ const up$34 = async (db) => {
42304
42378
  await db.schema.createTable("system_versions").addColumn("id", "boolean", (col) => col.primaryKey().defaultTo(true).check(sql`id`)).addColumn("schema_version", "integer", (col) => col.notNull().defaultTo(0)).addColumn("permissions_version", "integer", (col) => col.notNull().defaultTo(0)).addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).execute();
42305
42379
  await sql`insert into system_versions default values`.execute(db);
42306
42380
  await db.schema.createTable("jobs").addColumn("id", "uuid", (col) => col.primaryKey().defaultTo(sql`gen_random_uuid()`)).addColumn("type", "text", (col) => col.notNull()).addColumn("payload", "jsonb", (col) => col.notNull().defaultTo(sql`'{}'::jsonb`)).addColumn("status", "text", (col) => col.notNull().defaultTo("pending").check(sql`status in ('pending', 'running', 'succeeded', 'dead')`)).addColumn("priority", "integer", (col) => col.notNull().defaultTo(0)).addColumn("run_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).addColumn("attempts", "integer", (col) => col.notNull().defaultTo(0)).addColumn("max_attempts", "integer", (col) => col.notNull().defaultTo(10).check(sql`max_attempts > 0`)).addColumn("idempotency_key", "text").addColumn("locked_by", "text").addColumn("locked_until", "timestamptz").addColumn("checkpoint", "jsonb").addColumn("result", "jsonb").addColumn("last_error", "text").addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).addColumn("finished_at", "timestamptz").execute();
@@ -42322,7 +42396,7 @@ const up$33 = async (db) => {
42322
42396
  "occurred_at desc"
42323
42397
  ]).execute();
42324
42398
  };
42325
- const down$33 = async (db) => {
42399
+ const down$34 = async (db) => {
42326
42400
  await db.schema.dropTable("audit_events").execute();
42327
42401
  await db.schema.dropTable("outbox_events").execute();
42328
42402
  await db.schema.dropTable("jobs").execute();
@@ -42331,8 +42405,8 @@ const down$33 = async (db) => {
42331
42405
  //#endregion
42332
42406
  //#region src/db/migrations/20261001130000_create_admin_identity.ts
42333
42407
  var _20261001130000_create_admin_identity_exports = /* @__PURE__ */ __exportAll({
42334
- down: () => down$32,
42335
- up: () => up$32
42408
+ down: () => down$33,
42409
+ up: () => up$33
42336
42410
  });
42337
42411
  /**
42338
42412
  * Admin identity (package B, ADR 0005): admin users, roles and their permission grants, server-side
@@ -42341,7 +42415,7 @@ var _20261001130000_create_admin_identity_exports = /* @__PURE__ */ __exportAll(
42341
42415
  */
42342
42416
  const withTimestamps$3 = (table) => table.addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`));
42343
42417
  const uuidPrimaryKey$3 = (table) => table.addColumn("id", "uuid", (col) => col.primaryKey().defaultTo(sql`gen_random_uuid()`));
42344
- const up$32 = async (db) => {
42418
+ const up$33 = async (db) => {
42345
42419
  await withTimestamps$3(uuidPrimaryKey$3(db.schema.createTable("admin_users")).addColumn("email", "text", (col) => col.notNull().unique()).addColumn("name", "text", (col) => col.notNull().defaultTo("")).addColumn("password_hash", "text", (col) => col.notNull()).addColumn("status", "text", (col) => col.notNull().defaultTo("active").check(sql`status in ('active', 'disabled')`)).addColumn("last_login_at", "timestamptz").addColumn("password_changed_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`))).execute();
42346
42420
  await withTimestamps$3(uuidPrimaryKey$3(db.schema.createTable("admin_roles")).addColumn("key", "text", (col) => col.notNull().unique().check(sql`key ~ '^[a-z][a-z0-9-]{0,62}$'`)).addColumn("name", "text", (col) => col.notNull()).addColumn("description", "text", (col) => col.notNull().defaultTo("")).addColumn("kind", "text", (col) => col.notNull().defaultTo("admin").check(sql`kind in ('admin', 'delivery')`)).addColumn("is_system", "boolean", (col) => col.notNull().defaultTo(false)).addColumn("version", "integer", (col) => col.notNull().defaultTo(1))).execute();
42347
42421
  await withTimestamps$3(uuidPrimaryKey$3(db.schema.createTable("admin_role_permissions")).addColumn("role_id", "uuid", (col) => col.notNull().references("admin_roles.id").onDelete("cascade")).addColumn("action", "text", (col) => col.notNull()).addColumn("model_id", "text").addColumn("condition", "text", (col) => col.check(sql`condition in ('ownedByPrincipal')`)).addColumn("field_ids", sql`text[]`).addUniqueConstraint("admin_role_permissions_grant_uq", [
@@ -42361,7 +42435,7 @@ const up$32 = async (db) => {
42361
42435
  await withTimestamps$3(uuidPrimaryKey$3(db.schema.createTable("api_tokens")).addColumn("name", "text", (col) => col.notNull()).addColumn("token_hash", "text", (col) => col.notNull().unique()).addColumn("token_prefix", "text", (col) => col.notNull()).addColumn("role_id", "uuid", (col) => col.notNull().references("admin_roles.id").onDelete("restrict")).addColumn("created_by", "uuid", (col) => col.references("admin_users.id").onDelete("set null")).addColumn("expires_at", "timestamptz").addColumn("last_used_at", "timestamptz").addColumn("revoked_at", "timestamptz")).execute();
42362
42436
  await db.schema.createIndex("api_tokens_role_idx").on("api_tokens").column("role_id").execute();
42363
42437
  };
42364
- const down$32 = async (db) => {
42438
+ const down$33 = async (db) => {
42365
42439
  await db.schema.dropTable("api_tokens").execute();
42366
42440
  await db.schema.dropTable("setup_tokens").execute();
42367
42441
  await db.schema.dropTable("password_resets").execute();
@@ -42375,31 +42449,31 @@ const down$32 = async (db) => {
42375
42449
  //#endregion
42376
42450
  //#region src/db/migrations/20261001130500_create_system_settings.ts
42377
42451
  var _20261001130500_create_system_settings_exports = /* @__PURE__ */ __exportAll({
42378
- down: () => down$31,
42379
- up: () => up$31
42452
+ down: () => down$32,
42453
+ up: () => up$32
42380
42454
  });
42381
42455
  /**
42382
42456
  * Instance-wide key/value settings that must survive restarts and be shared by every instance, e.g. the
42383
42457
  * generated signing secret. General purpose: later packages add keys, not tables.
42384
42458
  */
42385
- const up$31 = async (db) => {
42459
+ const up$32 = async (db) => {
42386
42460
  await db.schema.createTable("system_settings").addColumn("key", "text", (col) => col.primaryKey()).addColumn("value", "text", (col) => col.notNull()).addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).execute();
42387
42461
  };
42388
- const down$31 = async (db) => {
42462
+ const down$32 = async (db) => {
42389
42463
  await db.schema.dropTable("system_settings").execute();
42390
42464
  };
42391
42465
  //#endregion
42392
42466
  //#region src/db/migrations/20261001140000_create_schema_tables.ts
42393
42467
  var _20261001140000_create_schema_tables_exports = /* @__PURE__ */ __exportAll({
42394
- down: () => down$30,
42395
- up: () => up$30
42468
+ down: () => down$31,
42469
+ up: () => up$31
42396
42470
  });
42397
42471
  /**
42398
42472
  * Schema registry (package D, ADR 0002): models and components (one table, `kind` discriminator), immutable
42399
42473
  * schema revisions, the per-model active pointer, schema change jobs, locales and schema settings.
42400
42474
  * Model definitions are data: creating or changing a model never runs DDL (CONTRIBUTING.md rule 1).
42401
42475
  */
42402
- const up$30 = async (db) => {
42476
+ const up$31 = async (db) => {
42403
42477
  await db.schema.createTable("models").addColumn("id", "uuid", (col) => col.primaryKey()).addColumn("kind", "text", (col) => col.notNull().check(sql`kind in ('collection', 'singleton', 'component')`)).addColumn("api_key", "text", (col) => col.notNull()).addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).addColumn("deleted_at", "timestamptz").execute();
42404
42478
  await sql`create unique index models_api_key_uq on models (lower(api_key)) where deleted_at is null`.execute(db);
42405
42479
  await db.schema.createTable("schema_revisions").addColumn("id", "uuid", (col) => col.primaryKey().defaultTo(sql`gen_random_uuid()`)).addColumn("model_id", "uuid", (col) => col.notNull().references("models.id").onDelete("restrict")).addColumn("version", "integer", (col) => col.notNull().check(sql`version > 0`)).addColumn("definition", "jsonb", (col) => col.notNull()).addColumn("hash", "text", (col) => col.notNull()).addColumn("parent_revision_id", "uuid", (col) => col.references("schema_revisions.id")).addColumn("created_by_type", "text", (col) => col.notNull()).addColumn("created_by_id", "text").addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).addUniqueConstraint("schema_revisions_model_version_uq", ["model_id", "version"]).execute();
@@ -42427,7 +42501,7 @@ const up$30 = async (db) => {
42427
42501
  await db.schema.createTable("schema_settings").addColumn("id", "boolean", (col) => col.primaryKey().defaultTo(true).check(sql`id`)).addColumn("read_only", "boolean", (col) => col.notNull().defaultTo(false)).addColumn("read_only_reason", "text").addColumn("updated_by_type", "text").addColumn("updated_by_id", "text").addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).execute();
42428
42502
  await sql`insert into schema_settings default values`.execute(db);
42429
42503
  };
42430
- const down$30 = async (db) => {
42504
+ const down$31 = async (db) => {
42431
42505
  await db.schema.dropTable("schema_settings").execute();
42432
42506
  await db.schema.dropTable("locales").execute();
42433
42507
  await db.schema.dropTable("schema_change_jobs").execute();
@@ -42439,8 +42513,8 @@ const down$30 = async (db) => {
42439
42513
  //#endregion
42440
42514
  //#region src/db/migrations/20261001150000_create_content_tables.ts
42441
42515
  var _20261001150000_create_content_tables_exports = /* @__PURE__ */ __exportAll({
42442
- down: () => down$29,
42443
- up: () => up$29
42516
+ down: () => down$30,
42517
+ up: () => up$30
42444
42518
  });
42445
42519
  /**
42446
42520
  * Content storage (package E, ADR 0001): entries, immutable content revisions, mutable heads (one per
@@ -42448,7 +42522,7 @@ var _20261001150000_create_content_tables_exports = /* @__PURE__ */ __exportAll(
42448
42522
  * and the publication log behind `?snapshot=N`. User content models never get tables of their own
42449
42523
  * (CONTRIBUTING.md rule 1): their values are JSONB keyed by stable field IDs.
42450
42524
  */
42451
- const up$29 = async (db) => {
42525
+ const up$30 = async (db) => {
42452
42526
  await db.schema.createTable("entries").addColumn("id", "uuid", (col) => col.primaryKey().defaultTo(sql`gen_random_uuid()`)).addColumn("model_id", "uuid", (col) => col.notNull().references("models.id").onDelete("restrict")).addColumn("owner_app_user_id", "uuid").addColumn("created_by_admin_id", "uuid", (col) => col.references("admin_users.id").onDelete("set null")).addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).addColumn("updated_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).addColumn("deleted_at", "timestamptz").execute();
42453
42527
  await sql`create index entries_model_idx on entries (model_id, created_at) where deleted_at is null`.execute(db);
42454
42528
  await sql`create index entries_owner_idx on entries (owner_app_user_id) where owner_app_user_id is not null`.execute(db);
@@ -42512,7 +42586,7 @@ const up$29 = async (db) => {
42512
42586
  await sql`create unique index publication_log_open_uq on publication_log (entry_id, locale) where to_seq is null`.execute(db);
42513
42587
  await db.schema.createIndex("publication_log_model_seq_idx").on("publication_log").columns(["model_id", "from_seq"]).execute();
42514
42588
  };
42515
- const down$29 = async (db) => {
42589
+ const down$30 = async (db) => {
42516
42590
  await db.schema.dropTable("publication_log").execute();
42517
42591
  await db.schema.dropTable("publication_state").execute();
42518
42592
  await db.schema.dropTable("unique_values").execute();
@@ -42525,8 +42599,8 @@ const down$29 = async (db) => {
42525
42599
  //#endregion
42526
42600
  //#region src/db/migrations/20261001160000_create_media_tables.ts
42527
42601
  var _20261001160000_create_media_tables_exports = /* @__PURE__ */ __exportAll({
42528
- down: () => down$28,
42529
- up: () => up$28
42602
+ down: () => down$29,
42603
+ up: () => up$29
42530
42604
  });
42531
42605
  /**
42532
42606
  * Media library (package G): folders, assets, image variants, references from content and upload grants.
@@ -42537,7 +42611,7 @@ const withTimestamps$2 = (table) => table.addColumn("created_at", "timestamptz",
42537
42611
  const uuidPrimaryKey$2 = (table) => table.addColumn("id", "uuid", (col) => col.primaryKey().defaultTo(sql`gen_random_uuid()`));
42538
42612
  const STORAGE_DRIVER_CHECK = sql`storage_driver in ('local', 's3')`;
42539
42613
  const VISIBILITY_CHECK = sql`visibility in ('public', 'private')`;
42540
- const up$28 = async (db) => {
42614
+ const up$29 = async (db) => {
42541
42615
  await withTimestamps$2(uuidPrimaryKey$2(db.schema.createTable("media_folders")).addColumn("parent_id", "uuid", (col) => col.references("media_folders.id").onDelete("restrict")).addColumn("name", "text", (col) => col.notNull().check(sql`length(name) between 1 and 255`)).addColumn("created_by", "uuid", (col) => col.references("admin_users.id").onDelete("set null")).addColumn("version", "integer", (col) => col.notNull().defaultTo(1))).execute();
42542
42616
  await sql`create unique index media_folders_sibling_name_uq on media_folders
42543
42617
  (coalesce(parent_id, '00000000-0000-0000-0000-000000000000'::uuid), lower(name))`.execute(db);
@@ -42566,7 +42640,7 @@ const up$28 = async (db) => {
42566
42640
  ]).execute();
42567
42641
  await db.schema.createTable("media_upload_grants").addColumn("id", "uuid", (col) => col.primaryKey().defaultTo(sql`gen_random_uuid()`)).addColumn("asset_id", "uuid", (col) => col.notNull()).addColumn("kind", "text", (col) => col.notNull().check(sql`kind in ('create', 'replace')`)).addColumn("storage_driver", "text", (col) => col.notNull().check(STORAGE_DRIVER_CHECK)).addColumn("storage_key", "text", (col) => col.notNull().unique()).addColumn("original_filename", "text", (col) => col.notNull()).addColumn("declared_mime_type", "text", (col) => col.notNull()).addColumn("max_size_bytes", "bigint", (col) => col.notNull()).addColumn("folder_id", "uuid").addColumn("visibility", "text", (col) => col.notNull().check(VISIBILITY_CHECK)).addColumn("created_by", "uuid", (col) => col.references("admin_users.id").onDelete("set null")).addColumn("created_by_principal", "text", (col) => col.notNull()).addColumn("status", "text", (col) => col.notNull().defaultTo("pending").check(sql`status in ('pending', 'consumed', 'rejected', 'expired')`)).addColumn("expires_at", "timestamptz", (col) => col.notNull()).addColumn("consumed_at", "timestamptz").addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).execute();
42568
42642
  };
42569
- const down$28 = async (db) => {
42643
+ const down$29 = async (db) => {
42570
42644
  await db.schema.dropTable("media_upload_grants").execute();
42571
42645
  await db.schema.dropTable("media_references").execute();
42572
42646
  await db.schema.dropTable("media_variants").execute();
@@ -42576,8 +42650,8 @@ const down$28 = async (db) => {
42576
42650
  //#endregion
42577
42651
  //#region src/db/migrations/20261001170000_create_publishing_tables.ts
42578
42652
  var _20261001170000_create_publishing_tables_exports = /* @__PURE__ */ __exportAll({
42579
- down: () => down$27,
42580
- up: () => up$27
42653
+ down: () => down$28,
42654
+ up: () => up$28
42581
42655
  });
42582
42656
  /**
42583
42657
  * Publishing (package H): scheduled publications, releases, webhooks and their delivery log, deployment
@@ -42616,13 +42690,13 @@ const createPreviewTables = async (db) => {
42616
42690
  await uuidPrimaryKey$1(db.schema.createTable("preview_tokens")).addColumn("token_hash", "text", (col) => col.notNull().unique()).addColumn("token_prefix", "text", (col) => col.notNull()).addColumn("model_id", "uuid", (col) => col.notNull()).addColumn("entry_id", "uuid", (col) => col.references("entries.id").onDelete("cascade")).addColumn("locale", "text").addColumn("connection_id", "uuid", (col) => col.references("deployment_connections.id").onDelete("set null")).addColumn("delivery_role_id", "uuid", (col) => col.references("admin_roles.id").onDelete("set null")).addColumn("created_by", "uuid", (col) => col.notNull().references("admin_users.id").onDelete("cascade")).addColumn("expires_at", "timestamptz", (col) => col.notNull()).addColumn("revoked_at", "timestamptz").addColumn("last_used_at", "timestamptz").addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).execute();
42617
42691
  await db.schema.createIndex("preview_tokens_entry_idx").on("preview_tokens").columns(["entry_id", "created_at desc"]).execute();
42618
42692
  };
42619
- const up$27 = async (db) => {
42693
+ const up$28 = async (db) => {
42620
42694
  await createSchedulingTables(db);
42621
42695
  await createWebhookTables(db);
42622
42696
  await createDeploymentTables(db);
42623
42697
  await createPreviewTables(db);
42624
42698
  };
42625
- const down$27 = async (db) => {
42699
+ const down$28 = async (db) => {
42626
42700
  await db.schema.dropTable("preview_tokens").execute();
42627
42701
  await db.schema.dropTable("deployment_runs").execute();
42628
42702
  await db.schema.dropTable("deployment_connections").execute();
@@ -42635,8 +42709,8 @@ const down$27 = async (db) => {
42635
42709
  //#endregion
42636
42710
  //#region src/db/migrations/20261001180000_create_app_users.ts
42637
42711
  var _20261001180000_create_app_users_exports = /* @__PURE__ */ __exportAll({
42638
- down: () => down$26,
42639
- up: () => up$26
42712
+ down: () => down$27,
42713
+ up: () => up$27
42640
42714
  });
42641
42715
  /**
42642
42716
  * App users (package I, ADR 0005): the end users of sites and apps built on Shapio, with their own roles
@@ -42687,7 +42761,7 @@ const createCredentials = async (db) => {
42687
42761
  }
42688
42762
  await db.schema.createTable("app_login_codes").addColumn("id", "uuid", (col) => col.primaryKey().defaultTo(sql`gen_random_uuid()`)).addColumn("app_user_id", "uuid", (col) => col.notNull().references("app_users.id").onDelete("cascade")).addColumn("code_hash", "text", (col) => col.notNull().unique()).addColumn("expires_at", "timestamptz", (col) => col.notNull()).addColumn("used_at", "timestamptz").addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).execute();
42689
42763
  };
42690
- const up$26 = async (db) => {
42764
+ const up$27 = async (db) => {
42691
42765
  await createUsers(db);
42692
42766
  await createRoles(db);
42693
42767
  await createCredentials(db);
@@ -42697,7 +42771,7 @@ const up$26 = async (db) => {
42697
42771
  foreign key (owner_app_user_id) references app_users (id) on delete set null
42698
42772
  `.execute(db);
42699
42773
  };
42700
- const down$26 = async (db) => {
42774
+ const down$27 = async (db) => {
42701
42775
  await sql`alter table entries drop constraint if exists entries_owner_app_user_fk`.execute(db);
42702
42776
  await db.schema.dropTable("app_login_codes").execute();
42703
42777
  await db.schema.dropTable("app_password_resets").execute();
@@ -42712,8 +42786,8 @@ const down$26 = async (db) => {
42712
42786
  //#endregion
42713
42787
  //#region src/db/migrations/20261002120000_create_extension_hook_runs.ts
42714
42788
  var _20261002120000_create_extension_hook_runs_exports = /* @__PURE__ */ __exportAll({
42715
- down: () => down$25,
42716
- up: () => up$25
42789
+ down: () => down$26,
42790
+ up: () => up$26
42717
42791
  });
42718
42792
  /**
42719
42793
  * Extension points (package K, ADR 0009): the ledger of post-commit lifecycle hooks (`afterCreate`,
@@ -42721,17 +42795,17 @@ var _20261002120000_create_extension_hook_runs_exports = /* @__PURE__ */ __expor
42721
42795
  * as the hook's own database work, so a hook re-run after a worker crash finds its row and is skipped:
42722
42796
  * exactly once for everything the hook does through its transaction.
42723
42797
  */
42724
- const up$25 = async (db) => {
42798
+ const up$26 = async (db) => {
42725
42799
  await db.schema.createTable("extension_hook_runs").addColumn("event_id", "uuid", (col) => col.notNull()).addColumn("hook", "text", (col) => col.notNull()).addColumn("job_id", "uuid").addColumn("completed_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).addPrimaryKeyConstraint("extension_hook_runs_pkey", ["event_id", "hook"]).execute();
42726
42800
  };
42727
- const down$25 = async (db) => {
42801
+ const down$26 = async (db) => {
42728
42802
  await db.schema.dropTable("extension_hook_runs").execute();
42729
42803
  };
42730
42804
  //#endregion
42731
42805
  //#region src/db/migrations/20261002130000_app_auth_token_version_and_login_code_pkce.ts
42732
42806
  var _20261002130000_app_auth_token_version_and_login_code_pkce_exports = /* @__PURE__ */ __exportAll({
42733
- down: () => down$24,
42734
- up: () => up$24
42807
+ down: () => down$25,
42808
+ up: () => up$25
42735
42809
  });
42736
42810
  /**
42737
42811
  * App users (ADR 0005):
@@ -42742,20 +42816,20 @@ var _20261002130000_app_auth_token_version_and_login_code_pkce_exports = /* @__P
42742
42816
  * sign-in; the one-time login code is only exchanged with the matching verifier. Codes live a minute, so
42743
42817
  * any pending ones (issued without a challenge) are dropped rather than given a placeholder.
42744
42818
  */
42745
- const up$24 = async (db) => {
42819
+ const up$25 = async (db) => {
42746
42820
  await db.schema.alterTable("app_users").addColumn("token_version", "integer", (col) => col.notNull().defaultTo(0)).execute();
42747
42821
  await sql`delete from app_login_codes`.execute(db);
42748
42822
  await db.schema.alterTable("app_login_codes").addColumn("code_challenge", "text", (col) => col.notNull()).execute();
42749
42823
  };
42750
- const down$24 = async (db) => {
42824
+ const down$25 = async (db) => {
42751
42825
  await db.schema.alterTable("app_login_codes").dropColumn("code_challenge").execute();
42752
42826
  await db.schema.alterTable("app_users").dropColumn("token_version").execute();
42753
42827
  };
42754
42828
  //#endregion
42755
42829
  //#region src/db/migrations/20261002180000_add_publication_log_diff_indexes.ts
42756
42830
  var _20261002180000_add_publication_log_diff_indexes_exports = /* @__PURE__ */ __exportAll({
42757
- down: () => down$23,
42758
- up: () => up$23
42831
+ down: () => down$24,
42832
+ up: () => up$24
42759
42833
  });
42760
42834
  /**
42761
42835
  * The snapshot diff (repositories/snapshotDiff.ts) finds the (entry, locale) pairs whose live period opened
@@ -42764,7 +42838,7 @@ var _20261002180000_add_publication_log_diff_indexes_exports = /* @__PURE__ */ _
42764
42838
  * - `(entry_id, locale, from_seq)`: "the row live at seq N" is the latest row starting at or before N.
42765
42839
  * Periods that opened in the range use the existing `(model_id, from_seq)` index.
42766
42840
  */
42767
- const up$23 = async (db) => {
42841
+ const up$24 = async (db) => {
42768
42842
  await sql`create index publication_log_to_seq_idx on publication_log (to_seq) where to_seq is not null`.execute(db);
42769
42843
  await db.schema.createIndex("publication_log_entry_locale_from_idx").on("publication_log").columns([
42770
42844
  "entry_id",
@@ -42772,15 +42846,15 @@ const up$23 = async (db) => {
42772
42846
  "from_seq"
42773
42847
  ]).execute();
42774
42848
  };
42775
- const down$23 = async (db) => {
42849
+ const down$24 = async (db) => {
42776
42850
  await db.schema.dropIndex("publication_log_entry_locale_from_idx").execute();
42777
42851
  await db.schema.dropIndex("publication_log_to_seq_idx").execute();
42778
42852
  };
42779
42853
  //#endregion
42780
42854
  //#region src/db/migrations/20261002180100_create_usage_tables.ts
42781
42855
  var _20261002180100_create_usage_tables_exports = /* @__PURE__ */ __exportAll({
42782
- down: () => down$22,
42783
- up: () => up$22
42856
+ down: () => down$23,
42857
+ up: () => up$23
42784
42858
  });
42785
42859
  /**
42786
42860
  * Field usage from delivery traffic (plan developer-face §5): daily counters written by the in-memory
@@ -42788,7 +42862,7 @@ var _20261002180100_create_usage_tables_exports = /* @__PURE__ */ __exportAll({
42788
42862
  * payloads. `principal_key` is `token:<id>`, `app_users` or `anonymous`. Pruned by the retention job
42789
42863
  * after USAGE_RETENTION_DAYS.
42790
42864
  */
42791
- const up$22 = async (db) => {
42865
+ const up$23 = async (db) => {
42792
42866
  await db.schema.createTable("field_reads").addColumn("day", "date", (col) => col.notNull()).addColumn("model_id", "uuid", (col) => col.notNull()).addColumn("field_path", "text", (col) => col.notNull()).addColumn("principal_key", "text", (col) => col.notNull()).addColumn("selection", "text", (col) => col.notNull().check(sql`selection in ('explicit', 'implicit')`)).addColumn("reads", "bigint", (col) => col.notNull().defaultTo(0)).addColumn("last_read_at", "timestamptz", (col) => col.notNull()).addPrimaryKeyConstraint("field_reads_pkey", [
42793
42867
  "day",
42794
42868
  "model_id",
@@ -42799,15 +42873,15 @@ const up$22 = async (db) => {
42799
42873
  await db.schema.createIndex("field_reads_model_day_idx").on("field_reads").columns(["model_id", "day"]).execute();
42800
42874
  await db.schema.createTable("token_reads").addColumn("day", "date", (col) => col.notNull()).addColumn("principal_key", "text", (col) => col.notNull()).addColumn("requests", "bigint", (col) => col.notNull().defaultTo(0)).addColumn("last_snapshot", "bigint").addColumn("last_read_at", "timestamptz", (col) => col.notNull()).addPrimaryKeyConstraint("token_reads_pkey", ["day", "principal_key"]).execute();
42801
42875
  };
42802
- const down$22 = async (db) => {
42876
+ const down$23 = async (db) => {
42803
42877
  await db.schema.dropTable("token_reads").execute();
42804
42878
  await db.schema.dropTable("field_reads").execute();
42805
42879
  };
42806
42880
  //#endregion
42807
42881
  //#region src/db/migrations/20261002180200_create_change_sets.ts
42808
42882
  var _20261002180200_create_change_sets_exports = /* @__PURE__ */ __exportAll({
42809
- down: () => down$21,
42810
- up: () => up$21
42883
+ down: () => down$22,
42884
+ up: () => up$22
42811
42885
  });
42812
42886
  /**
42813
42887
  * Change sets (developer-face plan §5, ADR 0010): schema drafts and entry publications that ship as ONE
@@ -42986,7 +43060,7 @@ const migrateReleases = async (db) => {
42986
43060
  await sql`drop table release_items`.execute(db);
42987
43061
  await sql`drop table releases`.execute(db);
42988
43062
  };
42989
- const up$21 = async (db) => {
43063
+ const up$22 = async (db) => {
42990
43064
  await createTables(db);
42991
43065
  await linkExecutionRecords(db);
42992
43066
  await allowConversionRevisions(db);
@@ -43058,7 +43132,7 @@ const restoreReleases = async (db) => {
43058
43132
  await sql`update webhooks set events = array_remove(array_remove(events, 'change_set.shipping'),
43059
43133
  'change_set.discarded')`.execute(db);
43060
43134
  };
43061
- const down$21 = async (db) => {
43135
+ const down$22 = async (db) => {
43062
43136
  await restoreReleases(db);
43063
43137
  await sql`alter table content_revisions drop constraint content_revisions_reason_check`.execute(db);
43064
43138
  await sql`alter table content_revisions add constraint content_revisions_reason_check
@@ -43079,8 +43153,8 @@ const down$21 = async (db) => {
43079
43153
  //#endregion
43080
43154
  //#region src/db/migrations/20261002190000_create_editor_presence_and_content_health.ts
43081
43155
  var _20261002190000_create_editor_presence_and_content_health_exports = /* @__PURE__ */ __exportAll({
43082
- down: () => down$20,
43083
- up: () => up$20
43156
+ down: () => down$21,
43157
+ up: () => up$21
43084
43158
  });
43085
43159
  /**
43086
43160
  * Editor experience (plan editor-experience §9):
@@ -43091,7 +43165,7 @@ var _20261002190000_create_editor_presence_and_content_health_exports = /* @__PU
43091
43165
  * - `content_health_findings`: open and recently resolved findings of the health rules per entry and
43092
43166
  * locale (`*` for the entry as a whole). An evaluation upserts what it found and resolves the rest.
43093
43167
  */
43094
- const up$20 = async (db) => {
43168
+ const up$21 = async (db) => {
43095
43169
  await sql`
43096
43170
  create unlogged table editor_presence (
43097
43171
  entry_id uuid not null,
@@ -43128,15 +43202,15 @@ const up$20 = async (db) => {
43128
43202
  await sql`create index content_health_findings_resolved_idx on content_health_findings (resolved_at)
43129
43203
  where resolved_at is not null`.execute(db);
43130
43204
  };
43131
- const down$20 = async (db) => {
43205
+ const down$21 = async (db) => {
43132
43206
  await sql`drop table content_health_findings`.execute(db);
43133
43207
  await sql`drop table editor_presence`.execute(db);
43134
43208
  };
43135
43209
  //#endregion
43136
43210
  //#region src/db/migrations/20261003120000_add_changes_ship_permission.ts
43137
43211
  var _20261003120000_add_changes_ship_permission_exports = /* @__PURE__ */ __exportAll({
43138
- down: () => down$19,
43139
- up: () => up$19
43212
+ down: () => down$20,
43213
+ up: () => up$20
43140
43214
  });
43141
43215
  /**
43142
43216
  * `changes.ship` (agentic plan §I): shipping a change set (and scheduling a ship) becomes its own global
@@ -43144,7 +43218,7 @@ var _20261003120000_add_changes_ship_permission_exports = /* @__PURE__ */ __expo
43144
43218
  * live. Every role that holds `changes.manage` today keeps what it could do: it gets `changes.ship` too.
43145
43219
  * The permissions version moves only when grants changed, so running instances reload exactly then.
43146
43220
  */
43147
- const up$19 = async (db) => {
43221
+ const up$20 = async (db) => {
43148
43222
  await sql`
43149
43223
  with granted as (
43150
43224
  insert into admin_role_permissions (role_id, action, model_id, condition, field_ids)
@@ -43158,7 +43232,7 @@ const up$19 = async (db) => {
43158
43232
  where exists (select 1 from granted)
43159
43233
  `.execute(db);
43160
43234
  };
43161
- const down$19 = async (db) => {
43235
+ const down$20 = async (db) => {
43162
43236
  await sql`
43163
43237
  with revoked as (
43164
43238
  delete from admin_role_permissions where action = 'changes.ship' returning 1
@@ -43170,8 +43244,8 @@ const down$19 = async (db) => {
43170
43244
  //#endregion
43171
43245
  //#region src/db/migrations/20261003130000_create_sites.ts
43172
43246
  var _20261003130000_create_sites_exports = /* @__PURE__ */ __exportAll({
43173
- down: () => down$18,
43174
- up: () => up$18
43247
+ down: () => down$19,
43248
+ up: () => up$19
43175
43249
  });
43176
43250
  /**
43177
43251
  * Sites (plan agentic-ecosystem §H, ADR 0011): one instance hosts many sites with one schema and one login.
@@ -43359,7 +43433,7 @@ const siteRoleAssignments = async (db) => {
43359
43433
  `.execute(db);
43360
43434
  await sql`alter table admin_invitations drop column role_ids`.execute(db);
43361
43435
  };
43362
- const up$18 = async (db) => {
43436
+ const up$19 = async (db) => {
43363
43437
  await createSites(db);
43364
43438
  await addSiteColumns(db);
43365
43439
  await tieCopiesToEntries(db);
@@ -43372,7 +43446,7 @@ const assertSingleSite = async (db) => {
43372
43446
  const { rows } = await sql`select count(*)::text as count from sites`.execute(db);
43373
43447
  if (Number(rows[0]?.count ?? 0) > 1) throw new Error("Cannot roll back sites while more than one site exists: merging them would merge their snapshot ledgers and uniqueness and widen role assignments. Delete the other sites first.");
43374
43448
  };
43375
- const down$18 = async (db) => {
43449
+ const down$19 = async (db) => {
43376
43450
  await assertSingleSite(db);
43377
43451
  await sql`alter table admin_invitations add column role_ids uuid[] not null default '{}'::uuid[]`.execute(db);
43378
43452
  await sql`
@@ -43443,8 +43517,8 @@ const down$18 = async (db) => {
43443
43517
  //#endregion
43444
43518
  //#region src/db/migrations/20261003140200_enqueue_field_index_layout.ts
43445
43519
  var _20261003140200_enqueue_field_index_layout_exports = /* @__PURE__ */ __exportAll({
43446
- down: () => down$17,
43447
- up: () => up$17
43520
+ down: () => down$18,
43521
+ up: () => up$18
43448
43522
  });
43449
43523
  /**
43450
43524
  * Field index layout v2 (sites plan §H): per-field expression indexes on `entry_heads` lead with `site_id`,
@@ -43456,7 +43530,7 @@ var _20261003140200_enqueue_field_index_layout_exports = /* @__PURE__ */ __expor
43456
43530
  */
43457
43531
  const JOB_TYPE$1 = "schema.fieldIndexLayout";
43458
43532
  const IDEMPOTENCY_KEY$1 = "schema.fieldIndexLayout:v2";
43459
- const up$17 = async (db) => {
43533
+ const up$18 = async (db) => {
43460
43534
  await sql`
43461
43535
  insert into jobs (type, payload, max_attempts, idempotency_key)
43462
43536
  select ${JOB_TYPE$1}, '{}'::jsonb, 5, ${IDEMPOTENCY_KEY$1}
@@ -43469,14 +43543,14 @@ const up$17 = async (db) => {
43469
43543
  `.execute(db);
43470
43544
  };
43471
43545
  /** The layout job is idempotent; rolling back only forgets it if it has not run yet. */
43472
- const down$17 = async (db) => {
43546
+ const down$18 = async (db) => {
43473
43547
  await sql`delete from jobs where idempotency_key = ${IDEMPOTENCY_KEY$1} and status = 'pending'`.execute(db);
43474
43548
  };
43475
43549
  //#endregion
43476
43550
  //#region src/db/migrations/20261003140500_change_set_items_site_and_preview_entry.ts
43477
43551
  var _20261003140500_change_set_items_site_and_preview_entry_exports = /* @__PURE__ */ __exportAll({
43478
- down: () => down$16,
43479
- up: () => up$16
43552
+ down: () => down$17,
43553
+ up: () => up$17
43480
43554
  });
43481
43555
  /**
43482
43556
  * Sites, publishing (plan agentic-ecosystem §H, package G5).
@@ -43487,7 +43561,7 @@ var _20261003140500_change_set_items_site_and_preview_entry_exports = /* @__PURE
43487
43561
  * - `preview_tokens.entry_id` becomes required: a preview token previews one entry of one site. Model-wide
43488
43562
  * tokens (short-lived by design) are deleted; whoever needs one opens the entry's preview again.
43489
43563
  */
43490
- const up$16 = async (db) => {
43564
+ const up$17 = async (db) => {
43491
43565
  await sql`alter table change_sets add constraint change_sets_id_site_uq unique (id, site_id)`.execute(db);
43492
43566
  await sql`alter table change_set_items add column site_id uuid`.execute(db);
43493
43567
  await sql`
@@ -43503,7 +43577,7 @@ const up$16 = async (db) => {
43503
43577
  await sql`delete from preview_tokens where entry_id is null`.execute(db);
43504
43578
  await sql`alter table preview_tokens alter column entry_id set not null`.execute(db);
43505
43579
  };
43506
- const down$16 = async (db) => {
43580
+ const down$17 = async (db) => {
43507
43581
  await sql`alter table preview_tokens alter column entry_id drop not null`.execute(db);
43508
43582
  await sql`alter table change_set_items drop constraint change_set_items_entry_site_fk`.execute(db);
43509
43583
  await sql`alter table change_set_items drop constraint change_set_items_set_site_fk`.execute(db);
@@ -43513,8 +43587,8 @@ const down$16 = async (db) => {
43513
43587
  //#endregion
43514
43588
  //#region src/db/migrations/20261003150000_widen_deployment_providers.ts
43515
43589
  var _20261003150000_widen_deployment_providers_exports = /* @__PURE__ */ __exportAll({
43516
- down: () => down$15,
43517
- up: () => up$15
43590
+ down: () => down$16,
43591
+ up: () => up$16
43518
43592
  });
43519
43593
  /**
43520
43594
  * Deploy adapters (plan agentic-ecosystem §B2): deployment connections may use the Vercel and Netlify
@@ -43529,11 +43603,11 @@ const replaceCheck = async (db, check) => {
43529
43603
  check (${check}) not valid`.execute(db);
43530
43604
  await sql`alter table deployment_connections validate constraint deployment_connections_provider_check`.execute(db);
43531
43605
  };
43532
- const up$15 = async (db) => {
43606
+ const up$16 = async (db) => {
43533
43607
  await replaceCheck(db, WIDENED);
43534
43608
  };
43535
43609
  /** Refuses while Vercel or Netlify connections exist: a down migration never deletes user data. */
43536
- const down$15 = async (db) => {
43610
+ const down$16 = async (db) => {
43537
43611
  const { rows } = await sql`
43538
43612
  select count(*)::text as count from deployment_connections where provider in ('vercel', 'netlify')
43539
43613
  `.execute(db);
@@ -43544,8 +43618,8 @@ const down$15 = async (db) => {
43544
43618
  //#endregion
43545
43619
  //#region src/db/migrations/20261003160000_create_assist_runs.ts
43546
43620
  var _20261003160000_create_assist_runs_exports = /* @__PURE__ */ __exportAll({
43547
- down: () => down$14,
43548
- up: () => up$14
43621
+ down: () => down$15,
43622
+ up: () => up$15
43549
43623
  });
43550
43624
  /**
43551
43625
  * Editor assists (plan agentic-ecosystem §A0, §I): one `assist_runs` row per assist request (who, which
@@ -43561,14 +43635,14 @@ const replaceSourceCheck = async (db, check) => {
43561
43635
  await sql`alter table change_sets add constraint change_sets_source_check check (${check}) not valid`.execute(db);
43562
43636
  await sql`alter table change_sets validate constraint change_sets_source_check`.execute(db);
43563
43637
  };
43564
- const up$14 = async (db) => {
43638
+ const up$15 = async (db) => {
43565
43639
  await db.schema.createTable("assist_runs").addColumn("id", "uuid", (col) => col.primaryKey().defaultTo(sql`gen_random_uuid()`)).addColumn("site_id", "uuid", (col) => col.notNull().references("sites.id").onDelete("cascade")).addColumn("actor_type", "text", (col) => col.notNull()).addColumn("actor_id", "text", (col) => col.notNull()).addColumn("action", "text", (col) => col.notNull().check(ACTIONS)).addColumn("rule", "text").addColumn("provider", "text", (col) => col.notNull()).addColumn("model", "text", (col) => col.notNull()).addColumn("input_tokens", "integer", (col) => col.notNull().defaultTo(0)).addColumn("output_tokens", "integer", (col) => col.notNull().defaultTo(0)).addColumn("duration_ms", "integer", (col) => col.notNull().defaultTo(0)).addColumn("status", "text", (col) => col.notNull().check(sql`status in ('queued', 'running', 'succeeded', 'failed')`)).addColumn("error_code", "text").addColumn("job_id", "uuid", (col) => col.references("jobs.id").onDelete("set null")).addColumn("created_at", "timestamptz", (col) => col.notNull().defaultTo(sql`now()`)).addColumn("finished_at", "timestamptz").execute();
43566
43640
  await db.schema.createIndex("assist_runs_site_created_idx").on("assist_runs").columns(["site_id", "created_at"]).execute();
43567
43641
  await db.schema.createIndex("assist_runs_created_idx").on("assist_runs").column("created_at").execute();
43568
43642
  await replaceSourceCheck(db, SOURCES_WIDENED);
43569
43643
  };
43570
43644
  /** Refuses while change sets opened by assist exist: a down migration never deletes user data. */
43571
- const down$14 = async (db) => {
43645
+ const down$15 = async (db) => {
43572
43646
  const { rows } = await sql`
43573
43647
  select count(*)::text as count from change_sets where source = 'assist'
43574
43648
  `.execute(db);
@@ -43580,8 +43654,8 @@ const down$14 = async (db) => {
43580
43654
  //#endregion
43581
43655
  //#region src/db/migrations/20261003170000_audit_events_seq.ts
43582
43656
  var _20261003170000_audit_events_seq_exports$2 = /* @__PURE__ */ __exportAll({
43583
- down: () => down$13,
43584
- up: () => up$13
43657
+ down: () => down$14,
43658
+ up: () => up$14
43585
43659
  });
43586
43660
  /**
43587
43661
  * `audit_events.seq`: a monotonic tiebreaker after `occurred_at`, so events written in the same instant (one
@@ -43590,17 +43664,17 @@ var _20261003170000_audit_events_seq_exports$2 = /* @__PURE__ */ __exportAll({
43590
43664
  * are numbered in the order the rewrite reads them. No index: the audit queries filter and sort by
43591
43665
  * `occurred_at` first, and `seq` only breaks ties.
43592
43666
  */
43593
- const up$13 = async (db) => {
43667
+ const up$14 = async (db) => {
43594
43668
  await db.schema.alterTable("audit_events").addColumn("seq", "bigint", (col) => col.notNull().generatedAlwaysAsIdentity()).execute();
43595
43669
  };
43596
- const down$13 = async (db) => {
43670
+ const down$14 = async (db) => {
43597
43671
  await db.schema.alterTable("audit_events").dropColumn("seq").execute();
43598
43672
  };
43599
43673
  //#endregion
43600
43674
  //#region src/db/migrations/20261004120000_models_site_scope.ts
43601
43675
  var _20261004120000_models_site_scope_exports$2 = /* @__PURE__ */ __exportAll({
43602
- down: () => down$12,
43603
- up: () => up$12
43676
+ down: () => down$13,
43677
+ up: () => up$13
43604
43678
  });
43605
43679
  /**
43606
43680
  * Per-site schema (plan site-schema): a definition belongs to one site (`models.site_id`) or is shared by every
@@ -43613,7 +43687,7 @@ var _20261004120000_models_site_scope_exports$2 = /* @__PURE__ */ __exportAll({
43613
43687
  * over `lower(api_key)` cannot hold. Share or delete them first; then `down` is lossless.
43614
43688
  */
43615
43689
  const NIL_UUID$1 = "00000000-0000-0000-0000-000000000000";
43616
- const up$12 = async (db) => {
43690
+ const up$13 = async (db) => {
43617
43691
  await sql`alter table models add column site_id uuid references sites (id)`.execute(db);
43618
43692
  await sql`create index models_site_idx on models (site_id)`.execute(db);
43619
43693
  await sql`drop index models_api_key_uq`.execute(db);
@@ -43623,7 +43697,7 @@ const up$12 = async (db) => {
43623
43697
  `.execute(db);
43624
43698
  await sql`alter table schema_drafts add column shared boolean not null default false`.execute(db);
43625
43699
  };
43626
- const down$12 = async (db) => {
43700
+ const down$13 = async (db) => {
43627
43701
  const { rows } = await sql`select count(*)::text as count from models where site_id is not null`.execute(db);
43628
43702
  if (Number(rows[0]?.count ?? 0) > 0) throw new Error("Cannot roll back per-site schema while definitions belong to a site: two sites may share an API ID. Share or delete the site definitions first.");
43629
43703
  await sql`alter table schema_drafts drop column shared`.execute(db);
@@ -43635,8 +43709,8 @@ const down$12 = async (db) => {
43635
43709
  //#endregion
43636
43710
  //#region src/db/migrations/20261004130000_site_seo_settings.ts
43637
43711
  var _20261004130000_site_seo_settings_exports$2 = /* @__PURE__ */ __exportAll({
43638
- down: () => down$11,
43639
- up: () => up$11
43712
+ down: () => down$12,
43713
+ up: () => up$12
43640
43714
  });
43641
43715
  /**
43642
43716
  * SEO defaults per site (plan seo-fields): `sites.seo_defaults` holds the site's default texts per locale, its
@@ -43647,7 +43721,7 @@ var _20261004130000_site_seo_settings_exports$2 = /* @__PURE__ */ __exportAll({
43647
43721
  * about what a site publishes) gets it; the built-in owner and admin roles get it from `ensureSystemRoles`.
43648
43722
  * The permissions version moves only when grants changed, so running instances reload exactly then.
43649
43723
  */
43650
- const up$11 = async (db) => {
43724
+ const up$12 = async (db) => {
43651
43725
  await sql`alter table sites add column seo_defaults jsonb`.execute(db);
43652
43726
  await sql`
43653
43727
  with granted as (
@@ -43662,7 +43736,7 @@ const up$11 = async (db) => {
43662
43736
  where exists (select 1 from granted)
43663
43737
  `.execute(db);
43664
43738
  };
43665
- const down$11 = async (db) => {
43739
+ const down$12 = async (db) => {
43666
43740
  await sql`
43667
43741
  with revoked as (
43668
43742
  delete from admin_role_permissions where action = 'site.settings' returning 1
@@ -43675,8 +43749,8 @@ const down$11 = async (db) => {
43675
43749
  //#endregion
43676
43750
  //#region src/db/migrations/20261005120000_entry_heads_entry_created_at.ts
43677
43751
  var _20261005120000_entry_heads_entry_created_at_exports$2 = /* @__PURE__ */ __exportAll({
43678
- down: () => down$10,
43679
- up: () => up$10
43752
+ down: () => down$11,
43753
+ up: () => up$11
43680
43754
  });
43681
43755
  /**
43682
43756
  * Delivery perf 2 (ADR 0001 amendment): the entry's `created_at` on each head (`entry_created_at`), so a list
@@ -43693,7 +43767,7 @@ const INDEX$2 = "entry_heads_entry_order_idx";
43693
43767
  const JOB_TYPE = "schema.entryOrderIndex";
43694
43768
  const IDEMPOTENCY_KEY = "schema.entryOrderIndex:v1";
43695
43769
  const BATCH$1 = 5e3;
43696
- const up$10 = async (db) => {
43770
+ const up$11 = async (db) => {
43697
43771
  await sql`alter table entry_heads add column entry_created_at timestamptz`.execute(db);
43698
43772
  for (;;) {
43699
43773
  const result = await sql`
@@ -43718,12 +43792,25 @@ const up$10 = async (db) => {
43718
43792
  create index ${sql.id(INDEX$2)} on entry_heads (site_id, model_id, state, entry_created_at desc, entry_id)
43719
43793
  `.execute(db);
43720
43794
  };
43721
- const down$10 = async (db) => {
43795
+ const down$11 = async (db) => {
43722
43796
  await sql`delete from jobs where idempotency_key = ${IDEMPOTENCY_KEY} and status = 'pending'`.execute(db);
43723
43797
  await sql`drop index if exists ${sql.id(INDEX$2)}`.execute(db);
43724
43798
  await sql`alter table entry_heads drop column entry_created_at`.execute(db);
43725
43799
  };
43726
43800
  //#endregion
43801
+ //#region src/db/migrations/20261006120000_system_versions_release.ts
43802
+ var _20261006120000_system_versions_release_exports = /* @__PURE__ */ __exportAll({
43803
+ dialectNeutral: () => true,
43804
+ down: () => down$10,
43805
+ up: () => up$10
43806
+ });
43807
+ const up$10 = async (db) => {
43808
+ await db.schema.alterTable("system_versions").addColumn("release", "text").execute();
43809
+ };
43810
+ const down$10 = async (db) => {
43811
+ await db.schema.alterTable("system_versions").dropColumn("release").execute();
43812
+ };
43813
+ //#endregion
43727
43814
  //#region src/db/migrations/mysql/0001_baseline.ts
43728
43815
  var _0001_baseline_exports$1 = /* @__PURE__ */ __exportAll({
43729
43816
  down: () => down$9,
@@ -45025,7 +45112,8 @@ const MYSQL_MIGRATIONS = {
45025
45112
  "20261003170000_audit_events_seq": _20261003170000_audit_events_seq_exports$1,
45026
45113
  "20261004120000_models_site_scope": _20261004120000_models_site_scope_exports$1,
45027
45114
  "20261004130000_site_seo_settings": _20261004130000_site_seo_settings_exports$1,
45028
- "20261005120000_entry_heads_entry_created_at": _20261005120000_entry_heads_entry_created_at_exports$1
45115
+ "20261005120000_entry_heads_entry_created_at": _20261005120000_entry_heads_entry_created_at_exports$1,
45116
+ "20261006120000_system_versions_release": _20261006120000_system_versions_release_exports
45029
45117
  };
45030
45118
  //#endregion
45031
45119
  //#region src/db/migrations/sqlite/0001_baseline.ts
@@ -46348,7 +46436,8 @@ const SQLITE_MIGRATIONS = {
46348
46436
  "20261003170000_audit_events_seq": _20261003170000_audit_events_seq_exports,
46349
46437
  "20261004120000_models_site_scope": _20261004120000_models_site_scope_exports,
46350
46438
  "20261004130000_site_seo_settings": _20261004130000_site_seo_settings_exports,
46351
- "20261005120000_entry_heads_entry_created_at": _20261005120000_entry_heads_entry_created_at_exports
46439
+ "20261005120000_entry_heads_entry_created_at": _20261005120000_entry_heads_entry_created_at_exports,
46440
+ "20261006120000_system_versions_release": _20261006120000_system_versions_release_exports
46352
46441
  };
46353
46442
  //#endregion
46354
46443
  //#region src/db/migrations/index.ts
@@ -46381,7 +46470,8 @@ const MIGRATIONS = {
46381
46470
  "20261003170000_audit_events_seq": _20261003170000_audit_events_seq_exports$2,
46382
46471
  "20261004120000_models_site_scope": _20261004120000_models_site_scope_exports$2,
46383
46472
  "20261004130000_site_seo_settings": _20261004130000_site_seo_settings_exports$2,
46384
- "20261005120000_entry_heads_entry_created_at": _20261005120000_entry_heads_entry_created_at_exports$2
46473
+ "20261005120000_entry_heads_entry_created_at": _20261005120000_entry_heads_entry_created_at_exports$2,
46474
+ "20261006120000_system_versions_release": _20261006120000_system_versions_release_exports
46385
46475
  };
46386
46476
  const PROVIDERS = {
46387
46477
  postgres: { getMigrations: () => Promise.resolve({ ...MIGRATIONS }) },
@@ -46684,6 +46774,24 @@ const parseByteRange = (header, size) => {
46684
46774
  };
46685
46775
  };
46686
46776
  //#endregion
46777
+ //#region src/media/contentDisposition.ts
46778
+ /**
46779
+ * A Content-Disposition value that names the file safely: an ASCII fallback plus the RFC 5987 UTF-8 form,
46780
+ * so quotes, semicolons or line breaks in a filename can never inject header parameters.
46781
+ */
46782
+ const contentDisposition = (filename, type = "inline") => {
46783
+ return `${type}; filename="${filename.replace(/[^\x20-\x7e]/g, "_").replace(/["\\]/g, "_")}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
46784
+ };
46785
+ /** Types a browser only ever displays passively: opening one directly cannot run script. */
46786
+ const PASSIVE_INLINE_TYPE = /^(image\/(png|jpeg|gif|webp|avif|bmp|tiff|x-icon|vnd\.microsoft\.icon)|video\/.+|audio\/.+)$/;
46787
+ /**
46788
+ * Content-Disposition to store on an object in a bucket. Public objects can be served straight from the
46789
+ * bucket or a CDN, where Shapio's sandboxing headers are absent; anything that could run as a page there
46790
+ * (SVG, HTML, XML, PDF...) is stored as an attachment, so opening it downloads it. `<img>` tags still show
46791
+ * an SVG, since embedding ignores Content-Disposition.
46792
+ */
46793
+ const bucketContentDisposition = (contentType) => PASSIVE_INLINE_TYPE.test(contentType.toLowerCase()) ? void 0 : "attachment";
46794
+ //#endregion
46687
46795
  //#region src/services/mediaFiles.ts
46688
46796
  const notFound = () => new AppError(404, "NOT_FOUND", "Media file not found");
46689
46797
  const findFile = async (key) => {
@@ -47111,6 +47219,16 @@ const insertIfAbsent = async (key, value, trx = db) => {
47111
47219
  value
47112
47220
  }).onConflict((oc) => oc.column("key").doNothing()).returning("key").executeTakeFirst() !== void 0;
47113
47221
  };
47222
+ /** Creates or replaces a setting. */
47223
+ const upsertValue = async (key, value, trx = db) => {
47224
+ await trx.insertInto("system_settings").values({
47225
+ key,
47226
+ value
47227
+ }).onConflict((oc) => oc.column("key").doUpdateSet({
47228
+ value,
47229
+ updated_at: /* @__PURE__ */ new Date()
47230
+ })).execute();
47231
+ };
47114
47232
  //#endregion
47115
47233
  //#region src/services/signingSecret.ts
47116
47234
  const SIGNING_SECRET_SETTING = "signing_secret";
@@ -47616,20 +47734,6 @@ var StartupCheckError = class extends Error {
47616
47734
  this.name = "StartupCheckError";
47617
47735
  }
47618
47736
  };
47619
- /** `host:port/database` (PostgreSQL, MySQL) or the file path (SQLite) from DATABASE_URL, never the credentials. */
47620
- const describeDatabaseTarget = (connectionString) => {
47621
- if (dialectOfUrl(connectionString) === "sqlite") {
47622
- const location = sqliteLocationOfUrl(connectionString);
47623
- return location.kind === "memory" ? "an in-memory database" : location.path;
47624
- }
47625
- try {
47626
- const url = new URL(connectionString);
47627
- const defaultPort = dialectOfUrl(connectionString) === "mysql" ? "3306" : "5432";
47628
- return `${url.hostname}:${url.port || defaultPort}${url.pathname}`;
47629
- } catch {
47630
- return "the configured database";
47631
- }
47632
- };
47633
47737
  const NETWORK_REASONS = {
47634
47738
  ECONNREFUSED: "connection refused",
47635
47739
  ENOTFOUND: "host not found",
@@ -49127,6 +49231,45 @@ const createPublishingJobHandlers = (environment) => [
49127
49231
  ];
49128
49232
  /** Outbox subscribers: webhook deliveries and deployment triggers follow domain events (ADR 0007). */
49129
49233
  const PUBLISHING_OUTBOX_SUBSCRIBERS = [webhookOutboxSubscriber, deploymentOutboxSubscriber];
49234
+ Type.Object({
49235
+ format: Type.Literal(1),
49236
+ publicUrl: Type.String(),
49237
+ basePath: Type.String(),
49238
+ media: Type.Object({
49239
+ publicBaseUrl: Type.Union([Type.String(), Type.Null()]),
49240
+ s3: Type.Union([Type.Object({
49241
+ bucket: Type.String(),
49242
+ region: Type.Union([Type.String(), Type.Null()]),
49243
+ endpoint: Type.Union([Type.String(), Type.Null()]),
49244
+ forcePathStyle: Type.Boolean()
49245
+ }), Type.Null()])
49246
+ })
49247
+ });
49248
+ const toDeliveryDescriptor = ({ server, storage }) => ({
49249
+ format: 1,
49250
+ publicUrl: server.publicUrl,
49251
+ basePath: server.basePath,
49252
+ media: {
49253
+ publicBaseUrl: storage.publicBaseUrl ?? null,
49254
+ s3: storage.s3 ? {
49255
+ bucket: storage.s3.bucket,
49256
+ region: storage.s3.region ?? null,
49257
+ endpoint: storage.s3.endpoint ?? null,
49258
+ forcePathStyle: storage.s3.forcePathStyle
49259
+ } : null
49260
+ }
49261
+ });
49262
+ /**
49263
+ * Called by the server at every start, after migrating: records its release and its delivery descriptor
49264
+ * (rewritten only when it changed), in one transaction.
49265
+ */
49266
+ const publishServerRelease = async (database, release, source) => {
49267
+ const descriptor = JSON.stringify(toDeliveryDescriptor(source));
49268
+ await database.transaction().execute(async (trx) => {
49269
+ await setRelease(release, trx);
49270
+ if (await findValue("delivery_descriptor", trx) !== descriptor) await upsertValue(DELIVERY_DESCRIPTOR_SETTING, descriptor, trx);
49271
+ });
49272
+ };
49130
49273
  //#endregion
49131
49274
  //#region src/tls/certificates.ts
49132
49275
  /**
@@ -49287,7 +49430,7 @@ const logStartupSummary = (config, logger) => {
49287
49430
  };
49288
49431
  logger.info(summary, `Shapio ${summary.version} running at ${summary.url} (mode ${summary.mode}, storage ${summary.storage}, worker ${summary.worker}, tls ${summary.tls})`);
49289
49432
  };
49290
- const createConfiguredWorker = (config, db, logger, signingSecret, extensions) => {
49433
+ const createConfiguredWorker = async (config, db, logger, signingSecret, extensions) => {
49291
49434
  const publishing = createPublishingJobEnvironment(createPublishingRuntime({
49292
49435
  db,
49293
49436
  signingSecret,
@@ -49300,7 +49443,7 @@ const createConfiguredWorker = (config, db, logger, signingSecret, extensions) =
49300
49443
  urls: createUrlBuilder(config.server),
49301
49444
  transport: createEmailTransport(config.email, logger.child({ component: "email" }))
49302
49445
  };
49303
- const storage = createMediaStorage(config.storage, { urls: createUrlBuilder(config.server) });
49446
+ const storage = await createMediaStorage(config.storage, { urls: createUrlBuilder(config.server) });
49304
49447
  return createWorker({
49305
49448
  db,
49306
49449
  handlers: createJobHandlers([
@@ -49381,6 +49524,7 @@ const startServer = async (config) => {
49381
49524
  log: logger
49382
49525
  });
49383
49526
  const signingSecret = await resolveSigningSecret(db, config.sessionSecret, logger);
49527
+ await publishServerRelease(db, SHAPIO_VERSION, config);
49384
49528
  extensions = await startExtensions(config, db, logger, project);
49385
49529
  const app = await buildApp(config, {
49386
49530
  db,
@@ -49390,7 +49534,7 @@ const startServer = async (config) => {
49390
49534
  schemaListen: config.schema.listen,
49391
49535
  ...tls.initial ? { tls: tls.initial } : {}
49392
49536
  });
49393
- const worker = config.worker.mode === "inline" ? createConfiguredWorker(config, db, logger, signingSecret, extensions) : void 0;
49537
+ const worker = config.worker.mode === "inline" ? await createConfiguredWorker(config, db, logger, signingSecret, extensions) : void 0;
49394
49538
  const tlsRuntime = tls;
49395
49539
  const extensionRuntime = extensions;
49396
49540
  let closing;
@@ -49464,7 +49608,7 @@ const startDedicatedWorker = async (config) => {
49464
49608
  }
49465
49609
  const signingSecret = await resolveSigningSecret(db, config.sessionSecret, logger);
49466
49610
  const extensions = await startExtensions(config, db, logger, project);
49467
- const worker = createConfiguredWorker(config, db, logger, signingSecret, extensions);
49611
+ const worker = await createConfiguredWorker(config, db, logger, signingSecret, extensions);
49468
49612
  await ensureRetentionScheduled(db);
49469
49613
  await ensureHealthSweepScheduled(db);
49470
49614
  worker.start();
@@ -49478,6 +49622,6 @@ const startDedicatedWorker = async (config) => {
49478
49622
  return worker;
49479
49623
  };
49480
49624
  //#endregion
49481
- export { toTypeName as $, update$8 as A, checkThemeContrast as B, BEFORE_EVENTS as C, listForAssets as D, buildEditorManifest as E, RESERVED_FIELD_API_KEYS as F, mergePulledLock as G, hashDefinition as H, RESERVED_TYPE_NAMES as I, schemaFilePath as J, parseDefinition as K, SCHEMA_KIND_DIRECTORIES as L, describeError as M, LOCK_FILE_PATH as N, listLiveByDriver as O, RESERVED_DEFINITION_API_KEYS as P, suggestPlural as Q, SITES_DIRECTORY as R, AFTER_EVENTS as S, EXTENSION_ROUTE_ROOT as T, lockCoversSite as U, foldApiKey as V, lockEntriesForSite as W, serializeDefinition as X, scopeDirectory as Y, serializeLockFile as Z, createLogger as _, loadConfig as a, createStableId as at, SITE_KEY_PATTERN as b, getPendingMigrations as c, sqliteDriverOf as ct, createMediaStorage as d, upgradeLockFile as et, createSite$1 as f, generateToken as g, SYSTEM_CLI_ACTOR as h, assertDatabaseReachable as i, SAFE_LINK_PATTERN as it, STORAGE_DRIVERS$1 as j, lockById$5 as k, migrateToLatest as l, createAdminDirect as m, startServer as n, withSiteCovered as nt, loadEnvFileIfPresent as o, validateRichText as ot, listAllSites as p, parseLockFile as q, copyObjectVerified as r, RICHTEXT_FORMAT as rt, buildApp as s, createDb as st, startDedicatedWorker as t, validateSchema as tt, SHAPIO_VERSION as u, createUrlBuilder as v, EXTENSION_JOB_PREFIX as w, loadProjectConfig as x, loopbackUrl as y, THEME_VARIANTS as z };
49625
+ export { parseLockFile as $, buildEditorManifest as A, RESERVED_DEFINITION_API_KEYS as B, loopbackUrl as C, BEFORE_EVENTS as D, AFTER_EVENTS as E, lockById$5 as F, THEME_VARIANTS as G, RESERVED_TYPE_NAMES as H, update$8 as I, hashDefinition as J, checkThemeContrast as K, STORAGE_DRIVERS$1 as L, isValidStorageKey as M, listForAssets as N, EXTENSION_JOB_PREFIX as O, listLiveByDriver as P, parseDefinition as Q, describeError as R, createUrlBuilder as S, loadProjectConfig as T, SCHEMA_KIND_DIRECTORIES as U, RESERVED_FIELD_API_KEYS as V, SITES_DIRECTORY as W, lockEntriesForSite as X, lockCoversSite as Y, mergePulledLock as Z, listAllSites as _, loadConfig as a, toTypeName as at, generateToken as b, bucketContentDisposition as c, withSiteCovered as ct, migrateToLatest as d, createStableId as dt, schemaFilePath as et, SHAPIO_VERSION as f, validateRichText as ft, createSite$1 as g, ObjectNotFoundError as h, assertDatabaseReachable as i, suggestPlural as it, encodeKeyPath as j, EXTENSION_ROUTE_ROOT as k, contentDisposition as l, RICHTEXT_FORMAT as lt, InvalidStorageKeyError as m, sqliteDriverOf as mt, startServer as n, serializeDefinition as nt, loadEnvFileIfPresent as o, upgradeLockFile as ot, createMediaStorage as p, createDb as pt, foldApiKey as q, copyObjectVerified as r, serializeLockFile as rt, buildApp as s, validateSchema as st, startDedicatedWorker as t, scopeDirectory as tt, getPendingMigrations as u, SAFE_LINK_PATTERN as ut, createAdminDirect as v, SITE_KEY_PATTERN as w, createLogger as x, SYSTEM_CLI_ACTOR as y, LOCK_FILE_PATH as z };
49482
49626
 
49483
- //# sourceMappingURL=worker-BDYDbaea.js.map
49627
+ //# sourceMappingURL=worker-BU-V4qbH.js.map