bsmnt 0.4.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/README.md +1 -0
  2. package/dist/application/add-hooks/index.d.ts.map +1 -1
  3. package/dist/application/add-hooks/index.js +1 -0
  4. package/dist/application/add-hooks/index.js.map +1 -1
  5. package/dist/application/add-integration/index.d.ts +33 -8
  6. package/dist/application/add-integration/index.d.ts.map +1 -1
  7. package/dist/application/add-integration/index.js +221 -90
  8. package/dist/application/add-integration/index.js.map +1 -1
  9. package/dist/application/create-project/build-context.d.ts +3 -1
  10. package/dist/application/create-project/build-context.d.ts.map +1 -1
  11. package/dist/application/create-project/build-context.js +2 -1
  12. package/dist/application/create-project/build-context.js.map +1 -1
  13. package/dist/application/create-project/index.d.ts +3 -1
  14. package/dist/application/create-project/index.d.ts.map +1 -1
  15. package/dist/application/create-project/index.js +4 -2
  16. package/dist/application/create-project/index.js.map +1 -1
  17. package/dist/application/create-project/map-selection-to-registry.js +1 -1
  18. package/dist/application/create-project/map-selection-to-registry.js.map +1 -1
  19. package/dist/application/install-skills/index.js +1 -1
  20. package/dist/application/install-skills/index.js.map +1 -1
  21. package/dist/application/shared/project-detection.d.ts +2 -2
  22. package/dist/application/shared/project-detection.d.ts.map +1 -1
  23. package/dist/application/shared/project-detection.js +2 -2
  24. package/dist/application/shared/project-detection.js.map +1 -1
  25. package/dist/core/create/execute-plan.d.ts +4 -0
  26. package/dist/core/create/execute-plan.d.ts.map +1 -1
  27. package/dist/core/create/execute-plan.js +8 -0
  28. package/dist/core/create/execute-plan.js.map +1 -1
  29. package/dist/core/create/internal-ids.d.ts +4 -1
  30. package/dist/core/create/internal-ids.d.ts.map +1 -1
  31. package/dist/core/create/internal-ids.js +6 -1
  32. package/dist/core/create/internal-ids.js.map +1 -1
  33. package/dist/core/create/types.d.ts +2 -0
  34. package/dist/core/create/types.d.ts.map +1 -1
  35. package/dist/domain/cms.d.ts +0 -7
  36. package/dist/domain/cms.d.ts.map +1 -1
  37. package/dist/domain/cms.js.map +1 -1
  38. package/dist/domain/forms.d.ts +17 -0
  39. package/dist/domain/forms.d.ts.map +1 -0
  40. package/dist/domain/forms.js +13 -0
  41. package/dist/domain/forms.js.map +1 -0
  42. package/dist/domain/integration.d.ts +30 -0
  43. package/dist/domain/integration.d.ts.map +1 -0
  44. package/dist/domain/integration.js +12 -0
  45. package/dist/domain/integration.js.map +1 -0
  46. package/dist/domain/skills.d.ts +58 -0
  47. package/dist/domain/skills.d.ts.map +1 -1
  48. package/dist/domain/skills.js +52 -0
  49. package/dist/domain/skills.js.map +1 -1
  50. package/dist/index.js +114 -38
  51. package/dist/index.js.map +1 -1
  52. package/dist/infrastructure/agents/register-sanity-mcp.js +9 -6
  53. package/dist/infrastructure/agents/register-sanity-mcp.js.map +1 -1
  54. package/dist/infrastructure/create/apply-package-json.d.ts.map +1 -1
  55. package/dist/infrastructure/create/apply-package-json.js +19 -0
  56. package/dist/infrastructure/create/apply-package-json.js.map +1 -1
  57. package/dist/infrastructure/create/executor.d.ts.map +1 -1
  58. package/dist/infrastructure/create/executor.js +5 -0
  59. package/dist/infrastructure/create/executor.js.map +1 -1
  60. package/dist/infrastructure/create/install-dependencies.d.ts.map +1 -1
  61. package/dist/infrastructure/create/install-dependencies.js +4 -0
  62. package/dist/infrastructure/create/install-dependencies.js.map +1 -1
  63. package/dist/infrastructure/create/lifecycle-registry.d.ts.map +1 -1
  64. package/dist/infrastructure/create/lifecycle-registry.js +2 -0
  65. package/dist/infrastructure/create/lifecycle-registry.js.map +1 -1
  66. package/dist/infrastructure/create/merge-registry.d.ts.map +1 -1
  67. package/dist/infrastructure/create/merge-registry.js +8 -0
  68. package/dist/infrastructure/create/merge-registry.js.map +1 -1
  69. package/dist/infrastructure/create/setup-agent.d.ts +2 -0
  70. package/dist/infrastructure/create/setup-agent.d.ts.map +1 -1
  71. package/dist/infrastructure/create/setup-agent.js +32 -8
  72. package/dist/infrastructure/create/setup-agent.js.map +1 -1
  73. package/dist/infrastructure/create/setup-dotenvx.d.ts +41 -0
  74. package/dist/infrastructure/create/setup-dotenvx.d.ts.map +1 -0
  75. package/dist/infrastructure/create/setup-dotenvx.js +502 -0
  76. package/dist/infrastructure/create/setup-dotenvx.js.map +1 -0
  77. package/dist/infrastructure/create/setup-sanity.d.ts +13 -1
  78. package/dist/infrastructure/create/setup-sanity.d.ts.map +1 -1
  79. package/dist/infrastructure/create/setup-sanity.js +74 -31
  80. package/dist/infrastructure/create/setup-sanity.js.map +1 -1
  81. package/dist/infrastructure/integrations/inject-integration.d.ts +3 -3
  82. package/dist/infrastructure/integrations/inject-integration.d.ts.map +1 -1
  83. package/dist/infrastructure/integrations/inject-integration.js +81 -37
  84. package/dist/infrastructure/integrations/inject-integration.js.map +1 -1
  85. package/dist/modules/features/cms/sanity/config.d.ts.map +1 -1
  86. package/dist/modules/features/cms/sanity/config.js +13 -6
  87. package/dist/modules/features/cms/sanity/config.js.map +1 -1
  88. package/dist/modules/features/cms/sanity/index.js +8 -2
  89. package/dist/modules/features/cms/sanity/index.js.map +1 -1
  90. package/dist/modules/features/cms/sanity/mergers/ci-workflow.d.ts +13 -0
  91. package/dist/modules/features/cms/sanity/mergers/ci-workflow.d.ts.map +1 -0
  92. package/dist/modules/features/cms/sanity/mergers/ci-workflow.js +38 -0
  93. package/dist/modules/features/cms/sanity/mergers/ci-workflow.js.map +1 -0
  94. package/dist/modules/features/cms/sanity/mergers/knip.d.ts +12 -0
  95. package/dist/modules/features/cms/sanity/mergers/knip.d.ts.map +1 -0
  96. package/dist/modules/features/cms/sanity/mergers/knip.js +38 -0
  97. package/dist/modules/features/cms/sanity/mergers/knip.js.map +1 -0
  98. package/dist/modules/features/cms/sanity/mergers/layout.d.ts.map +1 -1
  99. package/dist/modules/features/cms/sanity/mergers/layout.js +17 -3
  100. package/dist/modules/features/cms/sanity/mergers/layout.js.map +1 -1
  101. package/dist/modules/features/cms/sanity/mergers/sitemap.d.ts +3 -3
  102. package/dist/modules/features/cms/sanity/mergers/sitemap.d.ts.map +1 -1
  103. package/dist/modules/features/cms/sanity/mergers/sitemap.js +48 -62
  104. package/dist/modules/features/cms/sanity/mergers/sitemap.js.map +1 -1
  105. package/dist/modules/features/cms/sanity-pagebuilder/config.d.ts.map +1 -1
  106. package/dist/modules/features/cms/sanity-pagebuilder/config.js +8 -5
  107. package/dist/modules/features/cms/sanity-pagebuilder/config.js.map +1 -1
  108. package/dist/modules/features/cms/sanity-pagebuilder/index.js +2 -2
  109. package/dist/modules/features/cms/sanity-pagebuilder/index.js.map +1 -1
  110. package/dist/modules/features/env/dotenvx/config.d.ts +13 -0
  111. package/dist/modules/features/env/dotenvx/config.d.ts.map +1 -0
  112. package/dist/modules/features/env/dotenvx/config.js +47 -0
  113. package/dist/modules/features/env/dotenvx/config.js.map +1 -0
  114. package/dist/modules/features/forms/hubspot/config.d.ts +31 -0
  115. package/dist/modules/features/forms/hubspot/config.d.ts.map +1 -0
  116. package/dist/modules/features/forms/hubspot/config.js +25 -0
  117. package/dist/modules/features/forms/hubspot/config.js.map +1 -0
  118. package/dist/paths.d.ts +16 -0
  119. package/dist/paths.d.ts.map +1 -1
  120. package/dist/paths.js +16 -0
  121. package/dist/paths.js.map +1 -1
  122. package/package.json +21 -9
  123. package/src/agent-skills/json-ld/SKILL.md +85 -0
  124. package/src/agent-skills/manage-env/SKILL.md +67 -0
  125. package/src/assets/check-node-version.mjs +22 -0
  126. package/src/modules/features/cms/sanity/files/app/blog/[slug]/page.tsx +48 -21
  127. package/src/modules/features/cms/sanity/files/app/blog/page.tsx +54 -0
  128. package/src/modules/features/cms/sanity/files/app/feed.xml/route.ts +36 -0
  129. package/src/modules/features/cms/sanity/files/components/sanity/structured-data.tsx +83 -0
  130. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/components/disable-draft-mode.tsx +3 -3
  131. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/queries.ts +14 -0
  132. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/sanity.config.ts +19 -3
  133. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/sanity.types.ts +1 -0
  134. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/schemas/article.ts +7 -7
  135. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/schemas/index.ts +36 -1
  136. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/schemas/metadata.ts +12 -1
  137. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/schemas/structured-data-blocks.ts +416 -0
  138. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/schemas/structured-data.ts +100 -0
  139. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/structure.ts +37 -2
  140. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/utils/page-json-ld.ts +208 -0
  141. package/src/modules/features/cms/sanity/files/lib/scripts/setup-ci-secrets.ts +117 -0
  142. package/src/modules/features/cms/sanity/files/lib/utils/rss.ts +96 -0
  143. package/src/modules/features/cms/sanity-pagebuilder/files/.knip.json +8 -0
  144. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/[[...slug]]/route.ts +100 -0
  145. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/draft-mode/disable/route.ts +7 -0
  146. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/draft-mode/enable/route.ts +20 -0
  147. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/revalidate/route.ts +126 -0
  148. package/src/modules/features/cms/sanity-pagebuilder/files/app/feed.xml/route.ts +40 -0
  149. package/src/modules/features/cms/sanity-pagebuilder/files/app/sitemap.md/route.ts +124 -0
  150. package/src/modules/features/cms/sanity-pagebuilder/files/app/studio/[[...tool]]/page.tsx +8 -0
  151. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/draft-mode-toggle.tsx +27 -0
  152. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/rich-text.tsx +87 -0
  153. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/visual-editing.tsx +28 -0
  154. package/src/modules/features/cms/sanity-pagebuilder/files/components/ui/sanity-image/index.tsx +41 -0
  155. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/check-integration.ts +5 -0
  156. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/client.ts +27 -0
  157. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/disable-draft-mode.tsx +23 -0
  158. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-input.tsx +39 -0
  159. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-category-input.tsx +50 -0
  160. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/same-as-input.tsx +66 -0
  161. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/slug-field.tsx +18 -0
  162. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/slug-input.tsx +14 -0
  163. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/confirm-publish-action.ts +40 -0
  164. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/env.ts +34 -0
  165. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/layout.ts +67 -0
  166. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/redirects.ts +17 -0
  167. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +79 -0
  168. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/live/index.tsx +88 -0
  169. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/markdown-proxy.config.ts +50 -0
  170. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +130 -0
  171. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-category.ts +28 -0
  172. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-tree-pane.tsx +420 -0
  173. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/pinned-pages.ts +54 -0
  174. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/presentation.ts +119 -0
  175. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +407 -0
  176. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/sanity.cli.ts +29 -0
  177. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/sanity.config.ts +113 -0
  178. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/index.ts +4 -0
  179. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +89 -0
  180. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/description.ts +29 -0
  181. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +28 -0
  182. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +45 -0
  183. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/content/author.ts +70 -0
  184. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/content/blog-category.ts +55 -0
  185. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +124 -0
  186. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/company-data.ts +70 -0
  187. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/footer.ts +79 -0
  188. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/navbar.ts +75 -0
  189. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/redirect.ts +120 -0
  190. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/structured-data.ts +124 -0
  191. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/link.ts +125 -0
  192. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/logo-field.ts +9 -0
  193. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/metadata.ts +79 -0
  194. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/nav-objects.ts +192 -0
  195. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page-builder.ts +39 -0
  196. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page-folder.ts +124 -0
  197. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page.ts +242 -0
  198. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/richText.ts +63 -0
  199. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/structured-data-blocks.ts +436 -0
  200. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +51 -0
  201. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/structure.ts +330 -0
  202. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/image.ts +8 -0
  203. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/link.ts +140 -0
  204. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/page-builder-markdown.ts +81 -0
  205. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/page-json-ld.ts +208 -0
  206. package/src/modules/features/cms/sanity-pagebuilder/files/lib/scripts/sanity-typegen.ts +45 -0
  207. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/check-redirect.ts +30 -0
  208. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/json-ld.tsx +521 -0
  209. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/metadata.ts +167 -0
  210. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/rss.ts +96 -0
  211. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/sitemap.ts +40 -0
  212. package/src/modules/features/cms/sanity-pagebuilder/files/proxy.ts +81 -0
  213. package/src/modules/features/env/dotenvx/files/decrypt-env.mjs +45 -0
  214. package/src/modules/features/env/dotenvx/files/gitleaks.yml +25 -0
  215. package/src/modules/features/env/dotenvx/files/instrumentation.ts +14 -0
  216. package/src/modules/features/env/dotenvx/files/push-keys.mjs +129 -0
  217. package/src/modules/features/env/dotenvx/files/save-key.mjs +108 -0
  218. package/src/modules/features/env/dotenvx/files/setup-env.mjs +67 -0
  219. package/src/modules/features/env/dotenvx/files/vercelignore +4 -0
  220. package/src/modules/features/env/dotenvx/files/write-env.mjs +47 -0
  221. package/src/templates/next-default/.env.example +14 -6
  222. package/src/templates/next-default/.github/workflows/ci.yml +83 -0
  223. package/src/templates/next-default/.vscode/extensions.json +3 -0
  224. package/src/templates/next-default/README.md +53 -0
  225. package/src/templates/next-default/knip.json +14 -0
  226. package/src/templates/next-default/lefthook.yml +5 -0
  227. package/src/templates/next-default/lib/utils/json-ld.tsx +327 -21
  228. package/src/templates/next-default/next.config.ts +6 -0
  229. package/src/templates/next-default/package.json +8 -4
  230. package/src/templates/next-experiments/.env.example +14 -6
  231. package/src/templates/next-experiments/.github/workflows/ci.yml +83 -0
  232. package/src/templates/next-experiments/.vscode/extensions.json +3 -0
  233. package/src/templates/next-experiments/README.md +53 -0
  234. package/src/templates/next-experiments/knip.json +14 -0
  235. package/src/templates/next-experiments/lefthook.yml +30 -0
  236. package/src/templates/next-experiments/lib/utils/json-ld.tsx +327 -21
  237. package/src/templates/next-experiments/next.config.ts +6 -0
  238. package/src/templates/next-experiments/package.json +10 -2
  239. package/src/templates/next-pagebuilder/.env.example +19 -1
  240. package/src/templates/next-pagebuilder/.github/workflows/ci.yml +90 -0
  241. package/src/templates/next-pagebuilder/.vscode/extensions.json +3 -0
  242. package/src/templates/next-pagebuilder/README.md +65 -0
  243. package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +6 -1
  244. package/src/templates/next-pagebuilder/app/(content)/layout.tsx +3 -12
  245. package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +11 -10
  246. package/src/templates/next-pagebuilder/components/layout/header/index.tsx +5 -3
  247. package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +38 -26
  248. package/src/templates/next-pagebuilder/components/layout/wrapper/index.tsx +3 -11
  249. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +1 -1
  250. package/src/templates/next-pagebuilder/components/page-document/index.tsx +38 -0
  251. package/src/templates/next-pagebuilder/knip.json +27 -0
  252. package/src/templates/next-pagebuilder/lefthook.yml +5 -0
  253. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +36 -2
  254. package/src/templates/next-pagebuilder/lib/scripts/setup-ci-secrets.ts +117 -0
  255. package/src/templates/next-pagebuilder/next.config.ts +6 -0
  256. package/src/templates/next-pagebuilder/package.json +9 -3
  257. package/src/templates/next-webgl/.env.example +14 -6
  258. package/src/templates/next-webgl/.github/workflows/ci.yml +83 -0
  259. package/src/templates/next-webgl/.vscode/extensions.json +3 -0
  260. package/src/templates/next-webgl/README.md +53 -0
  261. package/src/templates/next-webgl/knip.json +13 -0
  262. package/src/templates/next-webgl/lefthook.yml +5 -0
  263. package/src/templates/next-webgl/lib/utils/json-ld.tsx +327 -21
  264. package/src/templates/next-webgl/next.config.ts +6 -0
  265. package/src/templates/next-webgl/package.json +8 -4
  266. package/src/modules/features/cms/sanity/files/lib/utils/json-ld.tsx +0 -244
@@ -0,0 +1,96 @@
1
+ export interface RssFeedItem {
2
+ title: string;
3
+ link: string;
4
+ description?: string | undefined;
5
+ pubDate?: string | undefined;
6
+ authors?: string[] | undefined;
7
+ category?: string | undefined;
8
+ }
9
+
10
+ export interface RssFeedConfig {
11
+ title: string;
12
+ link: string;
13
+ description: string;
14
+ feedUrl: string;
15
+ language?: string;
16
+ items: RssFeedItem[];
17
+ }
18
+
19
+ function escapeXml(str: string): string {
20
+ return str
21
+ .replace(/&/g, "&")
22
+ .replace(/</g, "&lt;")
23
+ .replace(/>/g, "&gt;")
24
+ .replace(/"/g, "&quot;")
25
+ .replace(/'/g, "&apos;");
26
+ }
27
+
28
+ export function generateRssFeed(config: RssFeedConfig): Response {
29
+ const {
30
+ title,
31
+ link,
32
+ description,
33
+ feedUrl,
34
+ language = "en-us",
35
+ items,
36
+ } = config;
37
+
38
+ const itemsXml = items
39
+ .map((item) => {
40
+ const lines = [
41
+ ` <title>${escapeXml(item.title)}</title>`,
42
+ ` <link>${escapeXml(item.link)}</link>`,
43
+ ` <guid isPermaLink="true">${escapeXml(item.link)}</guid>`,
44
+ ];
45
+
46
+ if (item.description) {
47
+ lines.push(
48
+ ` <description>${escapeXml(item.description)}</description>`,
49
+ );
50
+ }
51
+
52
+ const pubDate = item.pubDate ? new Date(item.pubDate) : null;
53
+ if (pubDate && !Number.isNaN(pubDate.getTime())) {
54
+ lines.push(` <pubDate>${pubDate.toUTCString()}</pubDate>`);
55
+ }
56
+
57
+ for (const author of item.authors ?? []) {
58
+ lines.push(` <dc:creator>${escapeXml(author)}</dc:creator>`);
59
+ }
60
+
61
+ if (item.category) {
62
+ lines.push(` <category>${escapeXml(item.category)}</category>`);
63
+ }
64
+
65
+ return ` <item>\n${lines.join("\n")}\n </item>`;
66
+ })
67
+ .join("\n");
68
+
69
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
70
+ <rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom">
71
+ <channel>
72
+ <title>${escapeXml(title)}</title>
73
+ <link>${escapeXml(link)}</link>
74
+ <description>${escapeXml(description)}</description>
75
+ <language>${language}</language>
76
+ <atom:link href="${escapeXml(feedUrl)}" rel="self" type="application/rss+xml" />
77
+ ${itemsXml}
78
+ </channel>
79
+ </rss>`;
80
+
81
+ return new Response(xml, {
82
+ headers: {
83
+ "Content-Type": "application/rss+xml",
84
+ },
85
+ });
86
+ }
87
+
88
+ export function rssErrorResponse(): Response {
89
+ return new Response(
90
+ '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel><title>Error</title></channel></rss>',
91
+ {
92
+ status: 500,
93
+ headers: { "Content-Type": "application/xml" },
94
+ },
95
+ );
96
+ }
@@ -0,0 +1,40 @@
1
+ import { PINNED_PAGE_ENTRIES } from "@/lib/integrations/sanity/pinned-pages";
2
+ import { PAGE_SITEMAP_QUERY } from "@/lib/integrations/sanity/queries";
3
+ import type { PAGE_SITEMAP_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types";
4
+ import { getBaseUrl } from "./base-url";
5
+
6
+ export const getSitemapBaseUrl = getBaseUrl;
7
+
8
+ const getNormalizedPageSlug = (slug: string | null) => {
9
+ if (!slug || slug === "/") return "";
10
+ return slug.replace(/^\/+|\/+$/g, "");
11
+ };
12
+
13
+ export const getPagePath = (slug: string | null) => {
14
+ const normalizedSlug = getNormalizedPageSlug(slug);
15
+ return normalizedSlug ? `/${normalizedSlug}` : "/";
16
+ };
17
+
18
+ export const getPageUrl = (baseUrl: string, slug: string | null) =>
19
+ new URL(getPagePath(slug), baseUrl).toString();
20
+
21
+ export const getMarkdownPath = (baseUrl: string, slug: string | null) => {
22
+ const normalizedSlug = getNormalizedPageSlug(slug);
23
+
24
+ return normalizedSlug
25
+ ? `${baseUrl}/${normalizedSlug}.md`
26
+ : `${baseUrl}/index.md`;
27
+ };
28
+
29
+ export const getSitemap = async () => {
30
+ const sanityModule = await import("@/lib/integrations/sanity/client");
31
+ const client = sanityModule?.client;
32
+
33
+ if (!client) return null;
34
+
35
+ const p = await client.fetch<PAGE_SITEMAP_QUERY_RESULT>(PAGE_SITEMAP_QUERY, {
36
+ pinnedIds: PINNED_PAGE_ENTRIES.map((entry) => entry.id),
37
+ });
38
+
39
+ return p;
40
+ };
@@ -0,0 +1,81 @@
1
+ import type { NextRequest } from "next/server";
2
+ import { NextResponse } from "next/server";
3
+ import {
4
+ acceptHeaderRoutes,
5
+ ignoredPaths,
6
+ mdExtensionRoutes,
7
+ } from "@/lib/integrations/sanity/markdown-proxy.config";
8
+
9
+ export function proxy(request: NextRequest) {
10
+ const { pathname } = request.nextUrl;
11
+ const acceptHeader = request.headers.get("accept") || "";
12
+
13
+ if (ignoredPaths.some((p) => pathname.startsWith(p))) {
14
+ return NextResponse.next();
15
+ }
16
+
17
+ if (pathname === "/" && acceptHeader.includes("text/markdown")) {
18
+ const url = request.nextUrl.clone();
19
+ url.pathname = "/api/index.md";
20
+ return NextResponse.rewrite(url);
21
+ }
22
+
23
+ // 1. Rewrite .md extension URLs to API routes
24
+ // e.g. /about.md → /api/about.md
25
+ for (const route of mdExtensionRoutes) {
26
+ const match = route.regex.exec(pathname);
27
+ const slug = match?.[1];
28
+ if (slug) {
29
+ const url = request.nextUrl.clone();
30
+ url.pathname = route.apiPath.replace("[slug]", slug);
31
+ return NextResponse.rewrite(url);
32
+ }
33
+ }
34
+
35
+ // 2. Accept: text/markdown — rewrite to markdown API route
36
+ if (acceptHeader.includes("text/markdown")) {
37
+ for (const route of acceptHeaderRoutes) {
38
+ const match = route.regex.exec(pathname);
39
+ const slug = match?.[1];
40
+ if (slug) {
41
+ const url = request.nextUrl.clone();
42
+ url.pathname = route.apiPath.replace("[slug]", slug);
43
+ return NextResponse.rewrite(url);
44
+ }
45
+ }
46
+ }
47
+
48
+ // 3. Add Link header for markdown discoverability on HTML page requests
49
+ const response = NextResponse.next();
50
+
51
+ if (pathname === "/") {
52
+ response.headers.set(
53
+ "Link",
54
+ '</index.md>; rel="alternate"; type="text/markdown"',
55
+ );
56
+ response.headers.set("Vary", "Accept");
57
+ return response;
58
+ }
59
+
60
+ for (const route of acceptHeaderRoutes) {
61
+ const match = route.regex.exec(pathname);
62
+ const slug = match?.[1];
63
+ if (slug) {
64
+ const mdUrl = route.publicPath.replace("[slug]", slug);
65
+ response.headers.set(
66
+ "Link",
67
+ `<${mdUrl}>; rel="alternate"; type="text/markdown"`,
68
+ );
69
+ response.headers.set("Vary", "Accept");
70
+ break;
71
+ }
72
+ }
73
+
74
+ return response;
75
+ }
76
+
77
+ export const config = {
78
+ matcher: [
79
+ "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|sitemap.md|robots.txt).*)",
80
+ ],
81
+ };
@@ -0,0 +1,45 @@
1
+ // Print a decrypted env file to the terminal (read-only — never writes).
2
+ //
3
+ // bun run env:view # .env (shared / development)
4
+ // bun run env:view --preview # .env.preview
5
+ // bun run env:view --prod # .env.production
6
+
7
+ import { spawnSync } from "node:child_process";
8
+ import { existsSync } from "node:fs";
9
+
10
+ // This prints decrypted secrets to stdout — never where logs are captured. Most
11
+ // CI sets `CI`; Vercel builds set `VERCEL` (not `CI`), so check both.
12
+ if (process.env.CI || process.env.VERCEL) {
13
+ console.error(
14
+ "env:view prints decrypted secrets — refusing to run in CI / on Vercel.",
15
+ );
16
+ process.exit(1);
17
+ }
18
+
19
+ const args = process.argv.slice(2);
20
+ const file =
21
+ args.includes("--prod") || args.includes("--production")
22
+ ? ".env.production"
23
+ : args.includes("--preview")
24
+ ? ".env.preview"
25
+ : ".env";
26
+
27
+ // Friendlier than dotenvx's MISSING_ENV_FILE error when the file isn't created yet.
28
+ if (!existsSync(file)) {
29
+ const flag =
30
+ file === ".env.production"
31
+ ? " --prod"
32
+ : file === ".env.preview"
33
+ ? " --preview"
34
+ : "";
35
+ console.error(
36
+ `No ${file} yet. Add values with: bun run env:set KEY value${flag}`,
37
+ );
38
+ process.exit(1);
39
+ }
40
+
41
+ // `dotenvx` resolves from node_modules/.bin when run via `bun run`.
42
+ const res = spawnSync("dotenvx", ["decrypt", "-f", file, "--stdout"], {
43
+ stdio: "inherit",
44
+ });
45
+ process.exit(res.status ?? 1);
@@ -0,0 +1,25 @@
1
+ name: gitleaks
2
+
3
+ # Scans the PR diff for committed secrets. Encrypted values and non-secret config
4
+ # pass; runs with --redact so values never reach the logs.
5
+
6
+ on:
7
+ pull_request:
8
+
9
+ permissions:
10
+ contents: read
11
+ pull-requests: write # gitleaks comments findings on the PR
12
+
13
+ jobs:
14
+ gitleaks:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ with:
19
+ fetch-depth: 0
20
+
21
+ - uses: gitleaks/gitleaks-action@v3
22
+ env:
23
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
24
+ # The action reads GITLEAKS_LICENSE; source it from your org secret.
25
+ GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE_KEY }}
@@ -0,0 +1,14 @@
1
+ // Decrypt env at runtime on Vercel serverless, where the `dotenvx run` wrapper
2
+ // doesn't run. Node only (dotenvx can't run on the edge).
3
+ export async function register() {
4
+ if (process.env.NEXT_RUNTIME !== "nodejs") return
5
+
6
+ const { config } = await import("@dotenvx/dotenvx")
7
+ const vercelEnv = process.env.VERCEL_ENV
8
+
9
+ config({
10
+ path: vercelEnv ? [".env", `.env.${vercelEnv}`] : [".env"],
11
+ overload: true, // Next pre-loads the encrypted .env; overwrite with decrypted values
12
+ ignore: ["MISSING_ENV_FILE"], // env-specific file may not exist
13
+ })
14
+ }
@@ -0,0 +1,129 @@
1
+ // Push the dotenvx private keys from .env.keys to GitHub / Vercel so encrypted
2
+ // envs decrypt in CI and at runtime. Each value is piped via stdin — never as a
3
+ // CLI argument — so it doesn't land in process listings or shell history.
4
+ //
5
+ // bun run env:setup-remote # pick a target from a menu
6
+ // bun run env:setup-remote github # gh secret set for each DOTENV_PRIVATE_KEY*
7
+ // bun run env:setup-remote vercel # vercel env add for each DOTENV_PRIVATE_KEY*
8
+
9
+ import { spawnSync } from "node:child_process";
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { createInterface } from "node:readline/promises";
12
+
13
+ const TARGETS = ["github", "vercel"];
14
+
15
+ // Collect every DOTENV_PRIVATE_KEY* assignment (base + per-environment) from
16
+ // .env.keys content, stripping surrounding quotes. Skips empty values.
17
+ export function parsePrivateKeys(content) {
18
+ const keys = [];
19
+ for (const raw of content.split("\n")) {
20
+ const match = raw.match(/^\s*(DOTENV_PRIVATE_KEY[A-Z_]*)\s*=\s*(.+?)\s*$/);
21
+ if (!match) continue;
22
+ const value = match[2].replace(/^["']|["']$/g, "");
23
+ if (value) keys.push({ name: match[1], value });
24
+ }
25
+ return keys;
26
+ }
27
+
28
+ // The base key decrypts the shared .env (loaded in every environment); a
29
+ // suffixed key (…_PRODUCTION / …_PREVIEW) decrypts only that environment's file.
30
+ export function vercelEnvsFor(name) {
31
+ const suffix = name.slice("DOTENV_PRIVATE_KEY".length).replace(/^_/, "");
32
+ return suffix
33
+ ? [suffix.toLowerCase()]
34
+ : ["production", "preview", "development"];
35
+ }
36
+
37
+ // Ask which remote when no arg was given. Never hang a non-interactive shell.
38
+ async function promptTarget() {
39
+ if (!process.stdin.isTTY) {
40
+ console.error("Usage: bun run env:setup-remote github | vercel");
41
+ process.exit(1);
42
+ }
43
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
44
+ try {
45
+ const answer = (
46
+ await rl.question(
47
+ "Push private keys to:\n 1) GitHub secrets (CI)\n 2) Vercel env (runtime)\nSelect [1/2]: ",
48
+ )
49
+ )
50
+ .trim()
51
+ .toLowerCase();
52
+ if (answer === "1" || answer === "github") return "github";
53
+ if (answer === "2" || answer === "vercel") return "vercel";
54
+ console.error("Cancelled — choose 1 (GitHub) or 2 (Vercel).");
55
+ process.exit(1);
56
+ } finally {
57
+ rl.close();
58
+ }
59
+ }
60
+
61
+ // Pipe the value through stdin so it never appears in argv.
62
+ function pipe(cmd, args, input) {
63
+ const res = spawnSync(cmd, args, {
64
+ input,
65
+ stdio: ["pipe", "inherit", "inherit"],
66
+ });
67
+ if (res.error?.code === "ENOENT") {
68
+ console.error(`\n${cmd} not found — install its CLI and sign in first.`);
69
+ process.exit(1);
70
+ }
71
+ return res.status === 0;
72
+ }
73
+
74
+ async function main() {
75
+ // Validate an explicit arg up front so a typo fails before anything else.
76
+ const arg = process.argv[2];
77
+ if (arg && !TARGETS.includes(arg)) {
78
+ console.error(`Unknown target "${arg}". Use: github | vercel`);
79
+ process.exit(1);
80
+ }
81
+
82
+ if (!existsSync(".env.keys")) {
83
+ console.error(
84
+ "No .env.keys found — run `bun run env:setup` to restore it first.",
85
+ );
86
+ process.exit(1);
87
+ }
88
+
89
+ const keys = parsePrivateKeys(readFileSync(".env.keys", "utf-8"));
90
+ if (keys.length === 0) {
91
+ console.error("No DOTENV_PRIVATE_KEY* entries found in .env.keys.");
92
+ process.exit(1);
93
+ }
94
+
95
+ const target = arg ?? (await promptTarget());
96
+
97
+ let ok = true;
98
+ for (const { name, value } of keys) {
99
+ if (target === "github") {
100
+ console.log(`→ gh secret set ${name}`);
101
+ ok = pipe("gh", ["secret", "set", name], value) && ok;
102
+ } else {
103
+ for (const env of vercelEnvsFor(name)) {
104
+ console.log(`→ vercel env add ${name} ${env}`);
105
+ ok = pipe("vercel", ["env", "add", name, env], value) && ok;
106
+ }
107
+ }
108
+ }
109
+
110
+ if (!ok) {
111
+ console.error(
112
+ target === "github"
113
+ ? "\nSome secrets failed. Check the GitHub CLI is authenticated (`gh auth login`) and the repo exists on GitHub."
114
+ : "\nSome env vars failed. Check the Vercel CLI is linked (`vercel link`); a var that already exists must be removed first (`vercel env rm NAME ENV`).",
115
+ );
116
+ process.exit(1);
117
+ }
118
+
119
+ console.log(
120
+ target === "vercel"
121
+ ? "\nDone. Redeploy for the new env vars to take effect."
122
+ : "\nDone.",
123
+ );
124
+ }
125
+
126
+ // Only run the script when invoked directly (not when imported by a test).
127
+ if (import.meta.main) {
128
+ await main();
129
+ }
@@ -0,0 +1,108 @@
1
+ // Save the private key to 1Password (Development vault) and point .env's op-ref
2
+ // at it, so teammates restore with `bun run env:setup`. The key is piped via
3
+ // stdin (JSON template), never passed as an argument.
4
+ //
5
+ // bun run env:save
6
+
7
+ import { spawnSync } from "node:child_process";
8
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { createInterface } from "node:readline/promises";
10
+
11
+ const VAULT = "Development";
12
+
13
+ function fail(message) {
14
+ console.error(message);
15
+ process.exit(1);
16
+ }
17
+
18
+ // Write the op-ref into .env (it's a plaintext line, never encrypted).
19
+ function setOpRef(ref) {
20
+ let content = existsSync(".env") ? readFileSync(".env", "utf-8") : "";
21
+ if (/^DOTENV_PRIVATE_KEY_OP_REF=.*$/m.test(content)) {
22
+ content = content.replace(
23
+ /^DOTENV_PRIVATE_KEY_OP_REF=.*$/m,
24
+ `DOTENV_PRIVATE_KEY_OP_REF=${ref}`,
25
+ );
26
+ } else {
27
+ if (content.length > 0 && !content.endsWith("\n")) content += "\n";
28
+ content += `DOTENV_PRIVATE_KEY_OP_REF=${ref}\n`;
29
+ }
30
+ writeFileSync(".env", content);
31
+ }
32
+
33
+ // 1. Read the base private key from .env.keys.
34
+ if (!existsSync(".env.keys")) {
35
+ fail("No .env.keys found — set up dotenvx (or run `bun run env:setup`) first.");
36
+ }
37
+ const keyMatch = readFileSync(".env.keys", "utf-8").match(
38
+ /^\s*DOTENV_PRIVATE_KEY\s*=\s*(.+?)\s*$/m,
39
+ );
40
+ const privateKey = keyMatch?.[1]?.replace(/^["']|["']$/g, "");
41
+ if (!privateKey) fail("No DOTENV_PRIVATE_KEY found in .env.keys.");
42
+
43
+ // 2. Require the 1Password CLI, signed in.
44
+ const whoami = spawnSync("op", ["whoami"], { stdio: "ignore" });
45
+ if (whoami.error?.code === "ENOENT") {
46
+ fail("1Password CLI not found — install `op` and run `op signin` first.");
47
+ }
48
+ if (whoami.status !== 0) {
49
+ fail("Not signed in to 1Password — run `op signin` first.");
50
+ }
51
+
52
+ // 3. Prompt for the item title (default: package.json name) and an optional tag.
53
+ if (!process.stdin.isTTY) fail("env:save is interactive — run it in a terminal.");
54
+ let defaultTitle = "";
55
+ try {
56
+ defaultTitle = JSON.parse(readFileSync("package.json", "utf-8")).name ?? "";
57
+ } catch {
58
+ // no package.json name — leave the title prompt empty
59
+ }
60
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
61
+ const title =
62
+ (
63
+ await rl.question(
64
+ `1Password item title${defaultTitle ? ` [${defaultTitle}]` : ""}: `,
65
+ )
66
+ ).trim() || defaultTitle;
67
+ const tag = (await rl.question("Tag (optional): ")).trim();
68
+ rl.close();
69
+ if (!title) fail("A title is required.");
70
+
71
+ // 4. Refuse if an item with that title already exists in the vault.
72
+ const existing = spawnSync("op", ["item", "get", title, "--vault", VAULT], {
73
+ stdio: "ignore",
74
+ });
75
+ if (existing.status === 0) {
76
+ fail(
77
+ `An item "${title}" already exists in ${VAULT} — rename or remove it first.`,
78
+ );
79
+ }
80
+
81
+ // 5. Create the item from a JSON template piped via stdin.
82
+ const template = {
83
+ title,
84
+ category: "SECURE_NOTE",
85
+ ...(tag ? { tags: [tag] } : {}),
86
+ fields: [{ label: "private-key", type: "CONCEALED", value: privateKey }],
87
+ };
88
+ const created = spawnSync(
89
+ "op",
90
+ ["item", "create", "--vault", VAULT, "--format", "json", "-"],
91
+ { input: JSON.stringify(template), encoding: "utf-8", stdio: ["pipe", "pipe", "inherit"] },
92
+ );
93
+ if (created.status !== 0) fail("\nFailed to create the 1Password item.");
94
+
95
+ // 6. Reference the item by id (stable; survives renames), falling back to title.
96
+ let itemRef = title;
97
+ try {
98
+ const id = JSON.parse(created.stdout)?.id;
99
+ if (id) itemRef = id;
100
+ } catch {
101
+ // non-JSON output — keep the title-based ref
102
+ }
103
+ const ref = `op://${VAULT}/${itemRef}/private-key`;
104
+ setOpRef(ref);
105
+
106
+ console.log(`\n✓ Saved to 1Password (${VAULT} / "${title}").`);
107
+ console.log(` Set DOTENV_PRIVATE_KEY_OP_REF=${ref} in .env.`);
108
+ console.log(" Teammates can now restore with `bun run env:setup`.");
@@ -0,0 +1,67 @@
1
+ // Restore .env.keys (dotenvx private decryption keys) from 1Password via `op`.
2
+ // Store either the full .env.keys contents (recommended — covers every
3
+ // environment) or just the DOTENV_PRIVATE_KEY value in a 1Password item field;
4
+ // the reference lives in the committed .env (DOTENV_PRIVATE_KEY_OP_REF):
5
+ //
6
+ // DOTENV_PRIVATE_KEY_OP_REF="op://Development/<item>/<field>" bun run env:setup
7
+ //
8
+ // `--auto` (run from the preinstall hook) is for onboarding: it never fails
9
+ // `bun install` — it skips when .env.keys already exists, in CI, or when the
10
+ // reference / `op` CLI is missing.
11
+
12
+ import { spawnSync } from "node:child_process";
13
+ import { existsSync, writeFileSync } from "node:fs";
14
+
15
+ const AUTO = process.argv.includes("--auto");
16
+
17
+ // In --auto mode any unmet precondition exits 0 so the install never breaks.
18
+ const stop = (msg) => {
19
+ console.error(msg);
20
+ process.exit(AUTO ? 0 : 1);
21
+ };
22
+
23
+ // Onboarding skips: nothing to do, or the wrong place to do it.
24
+ if (AUTO && existsSync(".env.keys")) process.exit(0);
25
+ if (AUTO && (process.env.CI || process.env.VERCEL)) process.exit(0);
26
+
27
+ const OP_REF = process.env.DOTENV_PRIVATE_KEY_OP_REF;
28
+ if (!OP_REF) {
29
+ stop(
30
+ "✗ set DOTENV_PRIVATE_KEY_OP_REF to the 1Password reference for .env.keys\n" +
31
+ ' e.g. DOTENV_PRIVATE_KEY_OP_REF="op://Development/<item>/<field>" bun run env:setup',
32
+ );
33
+ }
34
+
35
+ const res = spawnSync("op", ["read", OP_REF], { encoding: "utf8" });
36
+
37
+ if (res.error) {
38
+ const why =
39
+ res.error.code === "ENOENT"
40
+ ? "install the 1Password CLI (`op`) and sign in — https://developer.1password.com/docs/cli/get-started/"
41
+ : res.error.message;
42
+ stop(`✗ failed to run \`op\`: ${why}`);
43
+ }
44
+
45
+ if (res.status !== 0) {
46
+ stop(`✗ \`op read\` failed:\n${res.stderr.trim()}`);
47
+ }
48
+
49
+ const HEADER = `#/------------------!DOTENV_PRIVATE_KEYS!-------------------/
50
+ #/ private decryption keys. DO NOT commit to source control /
51
+ #/ [how it works](https://dotenvx.com/encryption) /
52
+ #/ ⛨ ARMORED KEYS: \`dotenvx armor up\` /
53
+ #/----------------------------------------------------------/
54
+
55
+ # .env
56
+ `;
57
+
58
+ const value = res.stdout.trim();
59
+
60
+ // op may hold the full .env.keys file or just the bare private key. Pass full
61
+ // contents through; wrap a bare key in the standard .env.keys format.
62
+ const contents = value.includes("DOTENV_PRIVATE_KEY")
63
+ ? `${value}\n`
64
+ : `${HEADER}DOTENV_PRIVATE_KEY=${value}\n`;
65
+
66
+ writeFileSync(".env.keys", contents);
67
+ console.info("✓ wrote .env.keys from 1Password");
@@ -0,0 +1,4 @@
1
+ # Never upload the private keys or local env files on `vercel deploy`.
2
+ # (Encrypted .env / .env.production / .env.preview ARE uploaded — they're safe.)
3
+ .env.keys
4
+ .env*.local
@@ -0,0 +1,47 @@
1
+ // Add or (re)encrypt env values without remembering the dotenvx flags.
2
+ //
3
+ // bun run env:set KEY value # encrypt one value into .env
4
+ // bun run env:set KEY value --plain # store it plaintext (e.g. NEXT_PUBLIC_*)
5
+ // bun run env:set KEY value --prod # target .env.production (--preview too)
6
+ // bun run env:encrypt-all # bulk-encrypt every plaintext value in .env
7
+ // bun run env:encrypt-all --prod # bulk-encrypt .env.production
8
+
9
+ import { spawnSync } from "node:child_process";
10
+
11
+ const [mode, ...rest] = process.argv.slice(2);
12
+
13
+ // --prod / --preview pick the per-environment file; default is the shared .env.
14
+ const ENV_FLAGS = new Set(["--prod", "--production", "--preview"]);
15
+ const file =
16
+ rest.includes("--prod") || rest.includes("--production")
17
+ ? ".env.production"
18
+ : rest.includes("--preview")
19
+ ? ".env.preview"
20
+ : ".env";
21
+
22
+ // On bulk encrypt, keep browser-public NEXT_PUBLIC_* and the 1Password pointer
23
+ // plaintext (same exclusions the scaffold uses).
24
+ const EXCLUDE = ["--exclude-key", "DOTENV_PRIVATE_KEY_OP_REF", "NEXT_PUBLIC_*"];
25
+
26
+ function run(args) {
27
+ // `dotenvx` resolves from node_modules/.bin when run via `bun run`.
28
+ const res = spawnSync("dotenvx", args, { stdio: "inherit" });
29
+ process.exit(res.status ?? 1);
30
+ }
31
+
32
+ if (mode === "encrypt") {
33
+ run(["encrypt", "-f", file, ...EXCLUDE]);
34
+ } else if (mode === "set") {
35
+ // Forward KEY, value and any dotenvx flags (e.g. --plain) straight through.
36
+ const passthrough = rest.filter((arg) => !ENV_FLAGS.has(arg));
37
+ if (passthrough.filter((arg) => !arg.startsWith("-")).length < 2) {
38
+ console.error(
39
+ "Usage: bun run env:set KEY value [--plain] [--preview|--prod]",
40
+ );
41
+ process.exit(1);
42
+ }
43
+ run(["set", ...passthrough, "-f", file]);
44
+ } else {
45
+ console.error(`Unknown env command: ${mode ?? "(none)"} (use set | encrypt)`);
46
+ process.exit(1);
47
+ }