@doxbrix/doxloop 0.1.5 → 0.2.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 (357) hide show
  1. package/CHANGELOG.md +1145 -0
  2. package/README.md +422 -227
  3. package/assets/doxbrix-preview.css +77 -15
  4. package/contracts/agent-events-v1.schema.json +1 -0
  5. package/contracts/coverage-v1.schema.json +59 -0
  6. package/contracts/drift-v1.schema.json +1 -0
  7. package/contracts/evaluation-v1.schema.json +62 -0
  8. package/contracts/quality-codes-v1.json +1 -0
  9. package/contracts/quality-report-v1.schema.json +1 -0
  10. package/contracts/validation-v1.schema.json +1 -0
  11. package/dist/agent-failure.d.ts +36 -0
  12. package/dist/agent-failure.js +98 -0
  13. package/dist/agent-isolation.d.ts +17 -0
  14. package/dist/agent-isolation.js +94 -0
  15. package/dist/agent-log.d.ts +197 -0
  16. package/dist/agent-log.js +1094 -0
  17. package/dist/agent-process.d.ts +63 -0
  18. package/dist/agent-process.js +128 -0
  19. package/dist/agent-reply.d.ts +47 -0
  20. package/dist/agent-reply.js +315 -0
  21. package/dist/agents.d.ts +24 -5
  22. package/dist/agents.js +60 -9
  23. package/dist/api-coverage.d.ts +11 -0
  24. package/dist/api-coverage.js +72 -0
  25. package/dist/application-probe.d.ts +28 -0
  26. package/dist/application-probe.js +93 -0
  27. package/dist/application-url.d.ts +14 -0
  28. package/dist/application-url.js +26 -0
  29. package/dist/args.js +15 -0
  30. package/dist/artifact-deploy.d.ts +9 -0
  31. package/dist/artifact-deploy.js +28 -8
  32. package/dist/assets.d.ts +72 -0
  33. package/dist/assets.js +382 -0
  34. package/dist/auth.d.ts +10 -0
  35. package/dist/auth.js +38 -9
  36. package/dist/author.d.ts +206 -4
  37. package/dist/author.js +1650 -52
  38. package/dist/authoring-batches.d.ts +190 -0
  39. package/dist/authoring-batches.js +489 -0
  40. package/dist/authoring-postpass.d.ts +79 -0
  41. package/dist/authoring-postpass.js +1289 -0
  42. package/dist/authoring-progress.d.ts +95 -0
  43. package/dist/authoring-progress.js +269 -0
  44. package/dist/autosync.d.ts +56 -0
  45. package/dist/autosync.js +442 -0
  46. package/dist/batch-artifacts.d.ts +81 -0
  47. package/dist/batch-artifacts.js +412 -0
  48. package/dist/batch-limits.d.ts +37 -0
  49. package/dist/batch-limits.js +80 -0
  50. package/dist/branding.d.ts +41 -0
  51. package/dist/branding.js +229 -0
  52. package/dist/bundle-upload.d.ts +26 -0
  53. package/dist/bundle-upload.js +99 -0
  54. package/dist/capture-auth.d.ts +95 -0
  55. package/dist/capture-auth.js +204 -0
  56. package/dist/capture.d.ts +48 -0
  57. package/dist/capture.js +20 -1
  58. package/dist/cli.js +839 -32
  59. package/dist/content-links.d.ts +10 -0
  60. package/dist/content-links.js +49 -0
  61. package/dist/contract-validation.d.ts +4 -0
  62. package/dist/contract-validation.js +25 -0
  63. package/dist/coverage-actions.d.ts +8 -0
  64. package/dist/coverage-actions.js +71 -0
  65. package/dist/coverage-resolutions.d.ts +16 -0
  66. package/dist/coverage-resolutions.js +58 -0
  67. package/dist/db.d.ts +37 -0
  68. package/dist/db.js +288 -0
  69. package/dist/demo.d.ts +12 -0
  70. package/dist/demo.js +122 -0
  71. package/dist/deploy-credentials.d.ts +7 -0
  72. package/dist/deploy-credentials.js +76 -0
  73. package/dist/deploy-targets/github-pages.d.ts +3 -0
  74. package/dist/deploy-targets/github-pages.js +100 -0
  75. package/dist/deploy-targets/index.d.ts +8 -0
  76. package/dist/deploy-targets/index.js +78 -0
  77. package/dist/deploy-targets/netlify.d.ts +3 -0
  78. package/dist/deploy-targets/netlify.js +49 -0
  79. package/dist/deploy-targets/types.d.ts +32 -0
  80. package/dist/deploy-targets/types.js +2 -0
  81. package/dist/deploy-targets/vercel.d.ts +3 -0
  82. package/dist/deploy-targets/vercel.js +95 -0
  83. package/dist/deploy.d.ts +9 -0
  84. package/dist/deploy.js +87 -13
  85. package/dist/deterministic-capture.d.ts +90 -0
  86. package/dist/deterministic-capture.js +435 -0
  87. package/dist/direct-edit.d.ts +40 -0
  88. package/dist/direct-edit.js +159 -0
  89. package/dist/docs-crawl.d.ts +86 -0
  90. package/dist/docs-crawl.js +536 -0
  91. package/dist/docs-site.d.ts +49 -0
  92. package/dist/docs-site.js +233 -0
  93. package/dist/doctor.js +8 -0
  94. package/dist/documentation-collections.d.ts +23 -0
  95. package/dist/documentation-collections.js +191 -0
  96. package/dist/documentation-plan.d.ts +253 -0
  97. package/dist/documentation-plan.js +2563 -0
  98. package/dist/doxbrix-build.d.ts +19 -0
  99. package/dist/doxbrix-build.js +167 -0
  100. package/dist/doxbrix-markdown.d.ts +9 -0
  101. package/dist/doxbrix-markdown.js +115 -14
  102. package/dist/drift.d.ts +10 -0
  103. package/dist/drift.js +164 -0
  104. package/dist/evaluation.d.ts +46 -0
  105. package/dist/evaluation.js +113 -0
  106. package/dist/evidence-pack.d.ts +47 -0
  107. package/dist/evidence-pack.js +358 -0
  108. package/dist/evidence.d.ts +25 -0
  109. package/dist/evidence.js +175 -0
  110. package/dist/fs.d.ts +8 -2
  111. package/dist/fs.js +41 -11
  112. package/dist/generator-api.d.ts +89 -0
  113. package/dist/generator-preflight.d.ts +27 -0
  114. package/dist/generator-preflight.js +105 -0
  115. package/dist/generator-runtime.d.ts +7 -0
  116. package/dist/generator-runtime.js +17 -1
  117. package/dist/generators.d.ts +24 -2
  118. package/dist/generators.js +54 -1
  119. package/dist/git-delivery.d.ts +17 -0
  120. package/dist/git-delivery.js +123 -0
  121. package/dist/globs.d.ts +16 -0
  122. package/dist/globs.js +65 -0
  123. package/dist/glossary.d.ts +26 -0
  124. package/dist/glossary.js +179 -0
  125. package/dist/history.d.ts +106 -0
  126. package/dist/history.js +600 -0
  127. package/dist/html-markdown.d.ts +46 -0
  128. package/dist/html-markdown.js +423 -0
  129. package/dist/interactive.js +3 -0
  130. package/dist/job-events.d.ts +74 -0
  131. package/dist/job-events.js +377 -0
  132. package/dist/keep-awake.d.ts +50 -0
  133. package/dist/keep-awake.js +123 -0
  134. package/dist/local-source-snapshot.d.ts +20 -0
  135. package/dist/local-source-snapshot.js +61 -0
  136. package/dist/mintlify-detect.d.ts +3 -0
  137. package/dist/mintlify-detect.js +18 -0
  138. package/dist/mintlify-import.d.ts +75 -0
  139. package/dist/mintlify-import.js +190 -0
  140. package/dist/navigation.d.ts +98 -0
  141. package/dist/navigation.js +310 -0
  142. package/dist/openapi.d.ts +60 -0
  143. package/dist/openapi.js +439 -0
  144. package/dist/page-editor-bridge.d.ts +3 -0
  145. package/dist/page-editor-bridge.js +109 -0
  146. package/dist/page-editor-preview.d.ts +10 -0
  147. package/dist/page-editor-preview.js +55 -0
  148. package/dist/page-extension.d.ts +9 -0
  149. package/dist/page-extension.js +15 -0
  150. package/dist/page-metadata.d.ts +28 -0
  151. package/dist/page-metadata.js +166 -0
  152. package/dist/page-operations.d.ts +34 -0
  153. package/dist/page-operations.js +215 -0
  154. package/dist/page-routes.d.ts +4 -0
  155. package/dist/page-routes.js +61 -0
  156. package/dist/pages.d.ts +20 -0
  157. package/dist/pages.js +184 -0
  158. package/dist/plan-generator.d.ts +3 -0
  159. package/dist/plan-generator.js +21 -0
  160. package/dist/plan-navigation.d.ts +11 -0
  161. package/dist/plan-navigation.js +30 -0
  162. package/dist/planning-captures.d.ts +20 -0
  163. package/dist/planning-captures.js +143 -0
  164. package/dist/planning-research.d.ts +135 -0
  165. package/dist/planning-research.js +472 -0
  166. package/dist/planning-triage.d.ts +23 -0
  167. package/dist/planning-triage.js +131 -0
  168. package/dist/preview.d.ts +24 -0
  169. package/dist/preview.js +280 -29
  170. package/dist/project-detect.d.ts +36 -0
  171. package/dist/project-detect.js +251 -0
  172. package/dist/project-import.d.ts +54 -0
  173. package/dist/project-import.js +157 -0
  174. package/dist/project-lock.d.ts +6 -0
  175. package/dist/project-lock.js +96 -0
  176. package/dist/project-registry.d.ts +25 -0
  177. package/dist/project-registry.js +79 -0
  178. package/dist/project.d.ts +35 -3
  179. package/dist/project.js +388 -38
  180. package/dist/prompts.d.ts +9 -0
  181. package/dist/prompts.js +25 -0
  182. package/dist/proposal-replay.d.ts +32 -0
  183. package/dist/proposal-replay.js +99 -0
  184. package/dist/quality-claims.d.ts +8 -0
  185. package/dist/quality-claims.js +168 -0
  186. package/dist/quality-config.d.ts +5 -0
  187. package/dist/quality-config.js +84 -0
  188. package/dist/quality-contract.d.ts +37 -0
  189. package/dist/quality-contract.js +46 -0
  190. package/dist/quality-examples.d.ts +4 -0
  191. package/dist/quality-examples.js +233 -0
  192. package/dist/quality-gates.d.ts +16 -0
  193. package/dist/quality-gates.js +192 -0
  194. package/dist/quality-links.d.ts +7 -0
  195. package/dist/quality-links.js +149 -0
  196. package/dist/quality-lint.d.ts +6 -0
  197. package/dist/quality-lint.js +124 -0
  198. package/dist/quality-rendered.d.ts +11 -0
  199. package/dist/quality-rendered.js +222 -0
  200. package/dist/quality-schema.d.ts +3 -0
  201. package/dist/quality-schema.js +71 -0
  202. package/dist/release-notes.d.ts +44 -0
  203. package/dist/release-notes.js +183 -0
  204. package/dist/remote-monitor.d.ts +16 -0
  205. package/dist/remote-monitor.js +74 -0
  206. package/dist/remote-source.d.ts +34 -0
  207. package/dist/remote-source.js +426 -0
  208. package/dist/review-diff.d.ts +82 -0
  209. package/dist/review-diff.js +400 -0
  210. package/dist/review-learning.d.ts +11 -0
  211. package/dist/review-learning.js +60 -0
  212. package/dist/review-render.d.ts +38 -0
  213. package/dist/review-render.js +224 -0
  214. package/dist/review-report.d.ts +9 -0
  215. package/dist/review-report.js +89 -0
  216. package/dist/review-ui.d.ts +14 -0
  217. package/dist/review-ui.js +1248 -0
  218. package/dist/schedule.d.ts +78 -0
  219. package/dist/schedule.js +480 -0
  220. package/dist/screen-capture-provider.d.ts +52 -0
  221. package/dist/screen-capture-provider.js +218 -0
  222. package/dist/screenshot-workflow.d.ts +167 -0
  223. package/dist/screenshot-workflow.js +1237 -0
  224. package/dist/settings.d.ts +1 -1
  225. package/dist/settings.js +95 -7
  226. package/dist/site-export.d.ts +18 -0
  227. package/dist/site-export.js +87 -0
  228. package/dist/source-connectors.d.ts +33 -0
  229. package/dist/source-connectors.js +268 -0
  230. package/dist/source-discovery.d.ts +132 -0
  231. package/dist/source-discovery.js +823 -0
  232. package/dist/source-intelligence.d.ts +9 -0
  233. package/dist/source-intelligence.js +306 -0
  234. package/dist/sync-review.d.ts +28 -0
  235. package/dist/sync-review.js +264 -0
  236. package/dist/sync-runs.d.ts +192 -0
  237. package/dist/sync-runs.js +2244 -0
  238. package/dist/sync.d.ts +35 -0
  239. package/dist/sync.js +298 -32
  240. package/dist/text-diff.d.ts +9 -0
  241. package/dist/text-diff.js +59 -0
  242. package/dist/types.d.ts +946 -1
  243. package/dist/ui/assets/doxloop-logo-light-De7Nx7j7.png +0 -0
  244. package/dist/ui/assets/index-BHBYU2aG.css +1 -0
  245. package/dist/ui/assets/index-Cq3RPQiC.js +33 -0
  246. package/dist/ui/index.html +18 -0
  247. package/dist/ui-server.d.ts +83 -0
  248. package/dist/ui-server.js +3532 -0
  249. package/dist/usage-budget.d.ts +28 -0
  250. package/dist/usage-budget.js +90 -0
  251. package/dist/validation.d.ts +25 -1
  252. package/dist/validation.js +312 -26
  253. package/dist/workspace-tools.d.ts +54 -0
  254. package/dist/workspace-tools.js +123 -0
  255. package/docs/agent-compatibility.md +49 -32
  256. package/docs/ci-and-automation.md +105 -66
  257. package/docs/doxbrix-http-api.md +8 -1
  258. package/docs/existing-documentation.md +80 -0
  259. package/docs/generation-performance.md +108 -0
  260. package/docs/generator-authoring.md +68 -5
  261. package/docs/generator-selection.md +50 -13
  262. package/docs/mintlify-import.md +71 -0
  263. package/docs/openapi-security.md +25 -0
  264. package/docs/project-format.md +331 -36
  265. package/docs/release-quality.md +158 -0
  266. package/docs/releasing.md +72 -0
  267. package/docs/review-workflows.md +51 -0
  268. package/docs/security-model.md +102 -39
  269. package/docs/troubleshooting.md +226 -93
  270. package/package.json +46 -15
  271. package/scripts/test-auto-screenshot.mjs +172 -0
  272. package/skills/doxloop-authoring/SKILL.md +242 -361
  273. package/skills/doxloop-authoring/references/existing-documentation.md +94 -0
  274. package/skills/doxloop-authoring/references/navigation-architecture.md +18 -7
  275. package/skills/doxloop-authoring/references/page-depth.md +169 -0
  276. package/skills/doxloop-authoring/references/project-format.md +106 -7
  277. package/skills/doxloop-authoring/references/quality.md +10 -0
  278. package/skills/doxloop-authoring/references/screenshot-manifest.md +113 -0
  279. package/skills/doxloop-authoring/references/screenshots.md +149 -235
  280. package/skills/doxloop-authoring/references/workflows.md +76 -0
  281. package/skills/doxloop-doxbrix/SKILL.md +38 -22
  282. package/skills/doxloop-doxbrix/references/api-endpoints.md +15 -14
  283. package/skills/doxloop-doxbrix/references/components.md +30 -3
  284. package/skills/doxloop-doxbrix/references/manifest.md +3 -2
  285. package/vendor/doxbrix-import/LICENSE +202 -0
  286. package/vendor/doxbrix-import/README.md +17 -0
  287. package/vendor/doxbrix-import/UPSTREAM.json +47 -0
  288. package/vendor/doxbrix-import/dist/docs/frontmatter.d.ts +13 -0
  289. package/vendor/doxbrix-import/dist/docs/frontmatter.js +83 -0
  290. package/vendor/doxbrix-import/dist/docs/import.d.ts +21 -0
  291. package/vendor/doxbrix-import/dist/docs/import.js +147 -0
  292. package/vendor/doxbrix-import/dist/docs/manifest.d.ts +163 -0
  293. package/vendor/doxbrix-import/dist/docs/manifest.js +64 -0
  294. package/vendor/doxbrix-import/dist/docs/project.d.ts +25 -0
  295. package/vendor/doxbrix-import/dist/docs/project.js +77 -0
  296. package/vendor/doxbrix-import/dist/docs/starter.d.ts +4 -0
  297. package/vendor/doxbrix-import/dist/docs/starter.js +11 -0
  298. package/vendor/doxbrix-import/dist/importer.d.ts +227 -0
  299. package/vendor/doxbrix-import/dist/importer.js +1567 -0
  300. package/vendor/doxbrix-import/dist/mintlify-openapi.d.ts +37 -0
  301. package/vendor/doxbrix-import/dist/mintlify-openapi.js +305 -0
  302. package/vendor/doxbrix-import/dist/safe-path.d.ts +9 -0
  303. package/vendor/doxbrix-import/dist/safe-path.js +47 -0
  304. package/dist/agents.d.ts.map +0 -1
  305. package/dist/agents.js.map +0 -1
  306. package/dist/args.d.ts.map +0 -1
  307. package/dist/args.js.map +0 -1
  308. package/dist/artifact-deploy.d.ts.map +0 -1
  309. package/dist/artifact-deploy.js.map +0 -1
  310. package/dist/auth.d.ts.map +0 -1
  311. package/dist/auth.js.map +0 -1
  312. package/dist/author.d.ts.map +0 -1
  313. package/dist/author.js.map +0 -1
  314. package/dist/capture.d.ts.map +0 -1
  315. package/dist/capture.js.map +0 -1
  316. package/dist/cli.d.ts.map +0 -1
  317. package/dist/cli.js.map +0 -1
  318. package/dist/deploy.d.ts.map +0 -1
  319. package/dist/deploy.js.map +0 -1
  320. package/dist/deployment-visibility.d.ts.map +0 -1
  321. package/dist/deployment-visibility.js.map +0 -1
  322. package/dist/doctor.d.ts.map +0 -1
  323. package/dist/doctor.js.map +0 -1
  324. package/dist/doxbrix-markdown.d.ts.map +0 -1
  325. package/dist/doxbrix-markdown.js.map +0 -1
  326. package/dist/errors.d.ts.map +0 -1
  327. package/dist/errors.js.map +0 -1
  328. package/dist/fs.d.ts.map +0 -1
  329. package/dist/fs.js.map +0 -1
  330. package/dist/generator-api.d.ts.map +0 -1
  331. package/dist/generator-api.js.map +0 -1
  332. package/dist/generator-manager.d.ts.map +0 -1
  333. package/dist/generator-manager.js.map +0 -1
  334. package/dist/generator-runtime.d.ts.map +0 -1
  335. package/dist/generator-runtime.js.map +0 -1
  336. package/dist/generators.d.ts.map +0 -1
  337. package/dist/generators.js.map +0 -1
  338. package/dist/interactive.d.ts.map +0 -1
  339. package/dist/interactive.js.map +0 -1
  340. package/dist/preview.d.ts.map +0 -1
  341. package/dist/preview.js.map +0 -1
  342. package/dist/progress.d.ts.map +0 -1
  343. package/dist/progress.js.map +0 -1
  344. package/dist/project.d.ts.map +0 -1
  345. package/dist/project.js.map +0 -1
  346. package/dist/prompts.d.ts.map +0 -1
  347. package/dist/prompts.js.map +0 -1
  348. package/dist/settings.d.ts.map +0 -1
  349. package/dist/settings.js.map +0 -1
  350. package/dist/sync.d.ts.map +0 -1
  351. package/dist/sync.js.map +0 -1
  352. package/dist/types.d.ts.map +0 -1
  353. package/dist/types.js.map +0 -1
  354. package/dist/validation.d.ts.map +0 -1
  355. package/dist/validation.js.map +0 -1
  356. package/dist/version.d.ts.map +0 -1
  357. package/dist/version.js.map +0 -1
@@ -0,0 +1,229 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile, writeFile } from 'node:fs/promises';
3
+ import { listAssets } from './assets.js';
4
+ import { applyDirectEdit } from './direct-edit.js';
5
+ import { DoxloopError } from './errors.js';
6
+ import { resolveContainedDirectory } from './fs.js';
7
+ import { loadGeneratorAdapter } from './generators.js';
8
+ import { loadProject, loadSiteConfig, relativePath, siteConfigPath } from './project.js';
9
+ import { validateDoxbrixTheme } from './validation.js';
10
+ /**
11
+ * The branding panel edits the Doxbrix theme block of `docs.json`: logos,
12
+ * favicon, accent colours for light and dark, page backgrounds, colour mode,
13
+ * and fonts. The preview reads `docs.json` on every request, so a saved
14
+ * change shows up on the next reload without a build. External generators
15
+ * keep their own theme configuration; the panel names the file instead.
16
+ */
17
+ export const THEME_COLOR_FIELDS = ['primaryColor', 'lightColor', 'darkColor', 'backgroundColorLight', 'backgroundColorDark'];
18
+ export const THEME_FONT_FIELDS = ['font', 'headingFont', 'codeFont'];
19
+ export const THEME_ASSET_FIELDS = ['logoLight', 'logoDark', 'favicon', 'faviconLight', 'faviconDark', 'backgroundImage'];
20
+ export const THEME_MODES = ['light', 'dark', 'system'];
21
+ export const THEME_CODE_THEMES = ['auto', 'light', 'dark'];
22
+ const THEME_FIELDS = [...THEME_COLOR_FIELDS, ...THEME_FONT_FIELDS, ...THEME_ASSET_FIELDS, 'mode', 'codeTheme', 'logoHref'];
23
+ /** Where each external generator keeps the settings a branding panel would edit. */
24
+ const THEME_CONFIG_FILES = {
25
+ docusaurus: 'docusaurus.config.js',
26
+ mkdocs: 'mkdocs.yml',
27
+ sphinx: 'conf.py',
28
+ hugo: 'hugo.toml',
29
+ vitepress: '.vitepress/config.mts',
30
+ markdoc: 'markdoc.config.mjs',
31
+ nextra: 'theme.config.tsx',
32
+ starlight: 'astro.config.mjs',
33
+ jekyll: '_config.yml',
34
+ static: 'assets/styles.css',
35
+ };
36
+ export async function readBranding(root) {
37
+ const project = await loadProject(root);
38
+ if (project.generator !== 'doxbrix') {
39
+ const adapter = await loadGeneratorAdapter(root, project);
40
+ const configFile = adapter.theme?.configFile ?? THEME_CONFIG_FILES[project.generator] ?? adapter.planning?.navigationFiles[0] ?? '';
41
+ return {
42
+ generator: project.generator,
43
+ editable: false,
44
+ reason: `${adapter.displayName} keeps its logo, colours, and fonts in ${configFile || 'its own configuration'}. Edit that file directly, or ask the agent to apply the branding.`,
45
+ configFile,
46
+ fingerprint: '',
47
+ site: { name: project.title, description: '' },
48
+ theme: {},
49
+ assets: [],
50
+ };
51
+ }
52
+ const configPath = await siteConfigPath(root, project);
53
+ const raw = await readFile(configPath, 'utf8');
54
+ const site = await loadSiteConfig(root, project);
55
+ const theme = themeObject(site.theme);
56
+ const values = {};
57
+ for (const field of THEME_FIELDS) {
58
+ const value = theme[field];
59
+ if (typeof value === 'string' && value.trim())
60
+ values[field] = value.trim();
61
+ }
62
+ const assets = (await listAssets(root)).filter((asset) => asset.kind === 'image').map((asset) => ({
63
+ path: asset.path,
64
+ name: asset.name,
65
+ publicPath: asset.publicPath,
66
+ }));
67
+ return {
68
+ generator: 'doxbrix',
69
+ editable: true,
70
+ configFile: relativePath(root, configPath),
71
+ fingerprint: fingerprint(raw),
72
+ site: { name: site.name ?? project.title, description: site.description ?? '' },
73
+ theme: values,
74
+ assets,
75
+ };
76
+ }
77
+ export async function writeBranding(root, raw) {
78
+ const input = parseWrite(raw);
79
+ const current = await readBranding(root);
80
+ if (!current.editable)
81
+ throw new DoxloopError(current.reason ?? 'Branding cannot be edited from Doxloop for this generator.', 2);
82
+ if (input.fingerprint !== current.fingerprint) {
83
+ throw new DoxloopError('The site configuration changed on disk since it was loaded. Reload the branding panel and apply your changes again.');
84
+ }
85
+ const project = await loadProject(root);
86
+ const configPath = await siteConfigPath(root, project);
87
+ const site = await loadSiteConfig(root, project);
88
+ const theme = themeObject(site.theme);
89
+ for (const [field, value] of Object.entries(input.theme ?? {})) {
90
+ if (value === null || value === '')
91
+ delete theme[field];
92
+ else
93
+ theme[field] = value;
94
+ }
95
+ const name = input.site?.name !== undefined ? input.site.name.trim() : site.name;
96
+ const description = input.site?.description !== undefined ? input.site.description.trim() : site.description;
97
+ if (input.site?.name !== undefined && !name)
98
+ throw new DoxloopError('Site name cannot be empty.', 2);
99
+ const contentRoot = await resolveContainedDirectory(root, project.contentDir, 'Documentation content directory', { allowRoot: true });
100
+ const issues = [];
101
+ await validateDoxbrixTheme(theme, contentRoot, current.configFile, issues);
102
+ const errors = issues.filter((issue) => issue.severity === 'error');
103
+ if (errors.length > 0)
104
+ throw new DoxloopError(errors.map((issue) => issue.message).join(' '), 2);
105
+ const next = {
106
+ ...site,
107
+ ...(name ? { name } : {}),
108
+ ...(description ? { description } : {}),
109
+ ...(Object.keys(theme).length > 0 ? { theme } : {}),
110
+ };
111
+ if (!description)
112
+ delete next.description;
113
+ if (Object.keys(theme).length === 0)
114
+ delete next.theme;
115
+ await applyDirectEdit(root, {
116
+ kind: 'branding',
117
+ requestText: describeChange(input, project),
118
+ files: [current.configFile],
119
+ apply: () => writeFile(configPath, `${JSON.stringify(next, null, 2)}\n`, 'utf8'),
120
+ });
121
+ return readBranding(root);
122
+ }
123
+ function parseWrite(raw) {
124
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
125
+ throw new DoxloopError('A branding write needs a fingerprint and the fields to change.', 2);
126
+ const body = raw;
127
+ if (typeof body.fingerprint !== 'string')
128
+ throw new DoxloopError('A branding write needs the fingerprint it was loaded with.', 2);
129
+ const result = { fingerprint: body.fingerprint };
130
+ if (body.site !== undefined) {
131
+ const site = (body.site && typeof body.site === 'object' ? body.site : {});
132
+ result.site = {};
133
+ if (site.name !== undefined)
134
+ result.site.name = textOrThrow(site.name, 'Site name');
135
+ if (site.description !== undefined)
136
+ result.site.description = textOrThrow(site.description, 'Site description');
137
+ }
138
+ if (body.theme !== undefined) {
139
+ const theme = (body.theme && typeof body.theme === 'object' ? body.theme : {});
140
+ result.theme = {};
141
+ for (const [key, value] of Object.entries(theme)) {
142
+ if (!THEME_FIELDS.includes(key))
143
+ throw new DoxloopError(`"${key}" is not a branding field.`, 2);
144
+ if (value === null || value === '') {
145
+ result.theme[key] = null;
146
+ continue;
147
+ }
148
+ result.theme[key] = validateField(key, textOrThrow(value, key));
149
+ }
150
+ }
151
+ return result;
152
+ }
153
+ function validateField(field, value) {
154
+ const trimmed = value.trim();
155
+ if (THEME_COLOR_FIELDS.includes(field)) {
156
+ const color = trimmed.toLowerCase();
157
+ if (!/^#[0-9a-f]{6}$/.test(color))
158
+ throw new DoxloopError(`${label(field)} must be a six-digit hex colour such as #6366f1.`, 2);
159
+ return color;
160
+ }
161
+ if (THEME_FONT_FIELDS.includes(field)) {
162
+ if (!trimmed || trimmed.length > 80 || /[<>"';{}]/.test(trimmed))
163
+ throw new DoxloopError(`${label(field)} must be a font family name.`, 2);
164
+ return trimmed;
165
+ }
166
+ if (field === 'mode') {
167
+ if (!THEME_MODES.includes(trimmed))
168
+ throw new DoxloopError('Colour mode must be light, dark, or system.', 2);
169
+ return trimmed;
170
+ }
171
+ if (field === 'codeTheme') {
172
+ if (!THEME_CODE_THEMES.includes(trimmed))
173
+ throw new DoxloopError('Code theme must be auto, light, or dark.', 2);
174
+ return trimmed;
175
+ }
176
+ if (!/^(?:https:\/\/|\/(?!\/))/i.test(trimmed) || /\s/.test(trimmed)) {
177
+ throw new DoxloopError(`${label(field)} must be an HTTPS address or a root-relative path such as /assets/logo.svg.`, 2);
178
+ }
179
+ if (trimmed.startsWith('/') && trimmed.split('/').some((part) => part === '..')) {
180
+ throw new DoxloopError(`${label(field)} cannot leave the documentation folder.`, 2);
181
+ }
182
+ return trimmed;
183
+ }
184
+ function describeChange(input, project) {
185
+ const fields = Object.keys(input.theme ?? {});
186
+ const site = Object.keys(input.site ?? {});
187
+ const parts = [...site.map((key) => `site ${key}`), ...fields.map(label)];
188
+ return parts.length > 0 ? `Updated branding: ${parts.join(', ')}` : `Updated branding for ${project.title}`;
189
+ }
190
+ function label(field) {
191
+ const words = {
192
+ primaryColor: 'Primary colour',
193
+ lightColor: 'Light-mode accent',
194
+ darkColor: 'Dark-mode accent',
195
+ backgroundColorLight: 'Light background',
196
+ backgroundColorDark: 'Dark background',
197
+ font: 'Body font',
198
+ headingFont: 'Heading font',
199
+ codeFont: 'Code font',
200
+ logoLight: 'Light logo',
201
+ logoDark: 'Dark logo',
202
+ favicon: 'Favicon',
203
+ faviconLight: 'Light favicon',
204
+ faviconDark: 'Dark favicon',
205
+ backgroundImage: 'Background image',
206
+ logoHref: 'Logo link',
207
+ mode: 'Colour mode',
208
+ codeTheme: 'Code theme',
209
+ };
210
+ return words[field] ?? field;
211
+ }
212
+ function textOrThrow(value, name) {
213
+ if (typeof value !== 'string')
214
+ throw new DoxloopError(`${name} must be text.`, 2);
215
+ if (value.length > 500)
216
+ throw new DoxloopError(`${name} is too long.`, 2);
217
+ return value;
218
+ }
219
+ function themeObject(value) {
220
+ if (typeof value === 'string')
221
+ return { mode: value };
222
+ if (value && typeof value === 'object' && !Array.isArray(value))
223
+ return { ...value };
224
+ return {};
225
+ }
226
+ function fingerprint(text) {
227
+ return createHash('sha256').update(text).digest('hex');
228
+ }
229
+ //# sourceMappingURL=branding.js.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Doxbrix runs on Vercel, which rejects function request bodies over 4.5 MB
3
+ * before the API sees them (a bare `413 FUNCTION_PAYLOAD_TOO_LARGE`). Bundles
4
+ * above this threshold are uploaded straight to storage through presigned URLs
5
+ * and committed by `uploadId`; smaller ones stay a single request, which every
6
+ * Doxbrix version accepts.
7
+ */
8
+ export declare const INLINE_BUNDLE_LIMIT_BYTES: number;
9
+ export interface PushableBundle {
10
+ manifest: unknown;
11
+ basePath: string;
12
+ pages: Array<{
13
+ path: string;
14
+ markdown: string;
15
+ }>;
16
+ media: Array<{
17
+ path: string;
18
+ base64: string;
19
+ }>;
20
+ verification?: unknown;
21
+ }
22
+ export declare function pushDeploymentBundle<T>(slug: string, bundle: PushableBundle, options: {
23
+ publish: boolean;
24
+ replace: boolean;
25
+ }, apiUrl?: string): Promise<T>;
26
+ //# sourceMappingURL=bundle-upload.d.ts.map
@@ -0,0 +1,99 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { authenticatedRequest, authenticatedRequestOptional } from './auth.js';
3
+ import { DoxloopError } from './errors.js';
4
+ /**
5
+ * Doxbrix runs on Vercel, which rejects function request bodies over 4.5 MB
6
+ * before the API sees them (a bare `413 FUNCTION_PAYLOAD_TOO_LARGE`). Bundles
7
+ * above this threshold are uploaded straight to storage through presigned URLs
8
+ * and committed by `uploadId`; smaller ones stay a single request, which every
9
+ * Doxbrix version accepts.
10
+ */
11
+ export const INLINE_BUNDLE_LIMIT_BYTES = 3.5 * 1024 * 1024;
12
+ const PLATFORM_BODY_LIMIT_BYTES = 4.5 * 1024 * 1024;
13
+ const UPLOAD_CONCURRENCY = 4;
14
+ const UPLOAD_ATTEMPTS = 3;
15
+ export async function pushDeploymentBundle(slug, bundle, options, apiUrl) {
16
+ const path = `/api/v1/projects/${encodeURIComponent(slug)}/bundle`;
17
+ const inline = JSON.stringify({ ...bundle, ...options });
18
+ const inlineBytes = Buffer.byteLength(inline);
19
+ const postInline = () => authenticatedRequest(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: inline }, apiUrl);
20
+ if (inlineBytes <= INLINE_BUNDLE_LIMIT_BYTES)
21
+ return postInline();
22
+ const media = bundle.media.map((file) => {
23
+ const data = Buffer.from(file.base64, 'base64');
24
+ return { path: file.path, data, sha256: sha256Hex(data) };
25
+ });
26
+ const staged = Buffer.from(JSON.stringify({
27
+ manifest: bundle.manifest,
28
+ basePath: bundle.basePath,
29
+ pages: bundle.pages,
30
+ ...(bundle.verification === undefined ? {} : { verification: bundle.verification }),
31
+ mediaRefs: media.map(({ path: mediaPath, sha256 }) => ({ path: mediaPath, sha256 })),
32
+ }));
33
+ const reservation = await authenticatedRequestOptional(`${path}/uploads`, {
34
+ method: 'POST',
35
+ headers: { 'Content-Type': 'application/json' },
36
+ body: JSON.stringify({
37
+ bundle: { bytes: staged.length, sha256: sha256Hex(staged) },
38
+ media: media.map(({ path: mediaPath, data, sha256 }) => ({ path: mediaPath, bytes: data.length, sha256 })),
39
+ }),
40
+ }, apiUrl);
41
+ if (!reservation) {
42
+ // This Doxbrix predates staged uploads, so the single request is all it takes.
43
+ if (inlineBytes > PLATFORM_BODY_LIMIT_BYTES) {
44
+ throw new DoxloopError(`This deployment is ${formatMiB(inlineBytes)}, but this Doxbrix server accepts at most 4.5 MB in one deployment. Update Doxbrix, or reduce the size of images in the documentation.`);
45
+ }
46
+ return postInline();
47
+ }
48
+ if (!reservation.uploadId || !reservation.bundle) {
49
+ throw new DoxloopError('Doxbrix did not return an upload location for the deployment bundle.');
50
+ }
51
+ const dataByPath = new Map(media.map((file) => [file.path, file.data]));
52
+ const uploads = reservation.media.flatMap((file) => file.status === 'upload' ? [{ upload: file.upload, data: dataByPath.get(file.path), label: file.path }] : []);
53
+ let next = 0;
54
+ const worker = async () => {
55
+ while (next < uploads.length) {
56
+ const item = uploads[next++];
57
+ await putPresigned(item.upload, item.data, item.label);
58
+ }
59
+ };
60
+ await Promise.all(Array.from({ length: Math.min(UPLOAD_CONCURRENCY, uploads.length) }, () => worker()));
61
+ await putPresigned(reservation.bundle.upload, staged, 'deployment bundle');
62
+ return authenticatedRequest(path, {
63
+ method: 'POST',
64
+ headers: { 'Content-Type': 'application/json' },
65
+ body: JSON.stringify({ uploadId: reservation.uploadId, ...options }),
66
+ }, apiUrl);
67
+ }
68
+ /** PUT bytes to a presigned storage URL. The Doxbrix token is never sent here. */
69
+ async function putPresigned(upload, data, label) {
70
+ let failure = '';
71
+ for (let attempt = 1; attempt <= UPLOAD_ATTEMPTS; attempt++) {
72
+ if (attempt > 1)
73
+ await new Promise((resolve) => setTimeout(resolve, 500 * 2 ** (attempt - 2)));
74
+ try {
75
+ const response = await fetch(upload.url, {
76
+ method: upload.method,
77
+ headers: upload.headers,
78
+ body: data,
79
+ redirect: 'error',
80
+ });
81
+ if (response.ok)
82
+ return;
83
+ failure = `${response.status} ${response.statusText}`.trim();
84
+ if (response.status < 500)
85
+ break;
86
+ }
87
+ catch (error) {
88
+ failure = error instanceof Error ? error.message : String(error);
89
+ }
90
+ }
91
+ throw new DoxloopError(`Upload of ${label} failed: ${failure}`);
92
+ }
93
+ function sha256Hex(data) {
94
+ return createHash('sha256').update(data).digest('hex');
95
+ }
96
+ function formatMiB(bytes) {
97
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
98
+ }
99
+ //# sourceMappingURL=bundle-upload.js.map
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Sign-in material for the screenshot capture browser.
3
+ *
4
+ * Applications that need a signed-in user can be documented in two ways:
5
+ * a recorded browser session (cookies and local storage saved after the user
6
+ * signs in by hand in a visible Chrome window) or saved credentials the agent
7
+ * types into the sign-in form through the capture server's secret store.
8
+ * Neither is written into the project: both live under the user's Doxloop
9
+ * config home, keyed by the project root, with owner-only permissions.
10
+ */
11
+ export declare const CAPTURE_USERNAME_SECRET = "DOXLOOP_APP_USERNAME";
12
+ export declare const CAPTURE_PASSWORD_SECRET = "DOXLOOP_APP_PASSWORD";
13
+ export interface StorageStateCookie {
14
+ name: string;
15
+ value: string;
16
+ domain: string;
17
+ path: string;
18
+ expires?: number;
19
+ httpOnly?: boolean;
20
+ secure?: boolean;
21
+ sameSite?: 'Strict' | 'Lax' | 'None';
22
+ }
23
+ /** The Playwright storage-state shape the capture server reads back. */
24
+ export interface CaptureStorageState {
25
+ cookies: StorageStateCookie[];
26
+ origins: Array<{
27
+ origin: string;
28
+ localStorage: Array<{
29
+ name: string;
30
+ value: string;
31
+ }>;
32
+ }>;
33
+ }
34
+ export interface CaptureCredentials {
35
+ username: string;
36
+ password: string;
37
+ }
38
+ export interface StoredCaptureSession {
39
+ savedAt: string;
40
+ origin: string;
41
+ state: CaptureStorageState;
42
+ }
43
+ export interface CaptureAuthStatus {
44
+ credentials?: {
45
+ username: string;
46
+ savedAt: string;
47
+ };
48
+ session?: {
49
+ savedAt: string;
50
+ origin: string;
51
+ cookies: number;
52
+ origins: number;
53
+ };
54
+ }
55
+ /** What the readiness check and prompts need to know, without the secrets. */
56
+ export interface CaptureAuthContext {
57
+ session?: CaptureStorageState;
58
+ credentials: boolean;
59
+ }
60
+ export interface CaptureAuthMaterial {
61
+ /** Playwright storage-state file passed to the capture server, when a session is saved. */
62
+ storageStatePath?: string;
63
+ /** dotenv secrets file passed to the capture server, when credentials are saved. */
64
+ secretsPath?: string;
65
+ /** Delete the per-run secrets file. The storage state stays for the next run. */
66
+ cleanup(): Promise<void>;
67
+ }
68
+ interface StoredCredentials extends CaptureCredentials {
69
+ savedAt: string;
70
+ }
71
+ export declare function captureAuthDirectory(root: string): string;
72
+ export declare function saveCaptureCredentials(root: string, credentials: CaptureCredentials): Promise<void>;
73
+ export declare function removeCaptureCredentials(root: string): Promise<void>;
74
+ export declare function loadCaptureCredentials(root: string): Promise<StoredCredentials | undefined>;
75
+ export declare function saveCaptureSession(root: string, origin: string, state: CaptureStorageState): Promise<StoredCaptureSession>;
76
+ export declare function removeCaptureSession(root: string): Promise<void>;
77
+ export declare function loadCaptureSession(root: string): Promise<StoredCaptureSession | undefined>;
78
+ export declare function captureAuthStatus(root: string): Promise<CaptureAuthStatus>;
79
+ export declare function captureAuthContext(root: string): Promise<CaptureAuthContext>;
80
+ /**
81
+ * Materialize the saved sign-in for one capture run. The storage state is
82
+ * rewritten from the stored session so the file the browser reads is always
83
+ * the plain Playwright shape; the secrets file exists only while the run does.
84
+ */
85
+ export declare function prepareCaptureAuth(root: string): Promise<CaptureAuthMaterial | undefined>;
86
+ /**
87
+ * The readiness probe is a plain HTTP request, so it carries the session
88
+ * cookies the browser would send for the URL: matching domain, matching path
89
+ * prefix, not expired, and not secure-only on a plain HTTP page.
90
+ */
91
+ export declare function sessionCookieHeader(state: CaptureStorageState | undefined, url: string): string | undefined;
92
+ export type CaptureAuthMode = 'none' | 'session' | 'credentials' | 'both';
93
+ export declare function describeCaptureAuth(context: CaptureAuthContext | CaptureAuthStatus | undefined): CaptureAuthMode;
94
+ export {};
95
+ //# sourceMappingURL=capture-auth.d.ts.map
@@ -0,0 +1,204 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { chmod, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { join, resolve } from 'node:path';
5
+ import { DoxloopError } from './errors.js';
6
+ import { pathExists } from './fs.js';
7
+ /**
8
+ * Sign-in material for the screenshot capture browser.
9
+ *
10
+ * Applications that need a signed-in user can be documented in two ways:
11
+ * a recorded browser session (cookies and local storage saved after the user
12
+ * signs in by hand in a visible Chrome window) or saved credentials the agent
13
+ * types into the sign-in form through the capture server's secret store.
14
+ * Neither is written into the project: both live under the user's Doxloop
15
+ * config home, keyed by the project root, with owner-only permissions.
16
+ */
17
+ export const CAPTURE_USERNAME_SECRET = 'DOXLOOP_APP_USERNAME';
18
+ export const CAPTURE_PASSWORD_SECRET = 'DOXLOOP_APP_PASSWORD';
19
+ export function captureAuthDirectory(root) {
20
+ const key = createHash('sha256').update(resolve(root)).digest('hex').slice(0, 20);
21
+ const home = process.env.DOXLOOP_CONFIG_HOME ?? join(homedir(), '.config', 'doxloop');
22
+ return join(home, 'capture-auth', key);
23
+ }
24
+ export async function saveCaptureCredentials(root, credentials) {
25
+ const username = credentials.username.trim();
26
+ const password = credentials.password;
27
+ if (!username)
28
+ throw new DoxloopError('Enter the sign-in username or email.');
29
+ if (!password)
30
+ throw new DoxloopError('Enter the sign-in password.');
31
+ if (/[\r\n]/.test(username))
32
+ throw new DoxloopError('The sign-in username cannot span lines.');
33
+ const stored = { username, password, savedAt: new Date().toISOString() };
34
+ await writePrivateFile(join(captureAuthDirectory(root), 'credentials.json'), JSON.stringify(stored, null, 2));
35
+ }
36
+ export async function removeCaptureCredentials(root) {
37
+ await rm(join(captureAuthDirectory(root), 'credentials.json'), { force: true });
38
+ }
39
+ export async function loadCaptureCredentials(root) {
40
+ const parsed = await readPrivateJson(join(captureAuthDirectory(root), 'credentials.json'));
41
+ if (!parsed || typeof parsed.username !== 'string' || typeof parsed.password !== 'string')
42
+ return undefined;
43
+ return { username: parsed.username, password: parsed.password, savedAt: typeof parsed.savedAt === 'string' ? parsed.savedAt : '' };
44
+ }
45
+ export async function saveCaptureSession(root, origin, state) {
46
+ if (!isStorageState(state))
47
+ throw new DoxloopError('The browser did not return a usable session.');
48
+ if (state.cookies.length === 0 && state.origins.every((item) => item.localStorage.length === 0)) {
49
+ throw new DoxloopError('The browser session holds no cookies or local storage yet. Complete the sign-in in the Chrome window before saving.');
50
+ }
51
+ const stored = { savedAt: new Date().toISOString(), origin, state };
52
+ await writePrivateFile(join(captureAuthDirectory(root), 'session.json'), JSON.stringify(stored, null, 2));
53
+ return stored;
54
+ }
55
+ export async function removeCaptureSession(root) {
56
+ const directory = captureAuthDirectory(root);
57
+ await Promise.all([
58
+ rm(join(directory, 'session.json'), { force: true }),
59
+ rm(join(directory, 'storage-state.json'), { force: true }),
60
+ ]);
61
+ }
62
+ export async function loadCaptureSession(root) {
63
+ const parsed = await readPrivateJson(join(captureAuthDirectory(root), 'session.json'));
64
+ if (!parsed || !isStorageState(parsed.state))
65
+ return undefined;
66
+ return {
67
+ savedAt: typeof parsed.savedAt === 'string' ? parsed.savedAt : '',
68
+ origin: typeof parsed.origin === 'string' ? parsed.origin : '',
69
+ state: parsed.state,
70
+ };
71
+ }
72
+ export async function captureAuthStatus(root) {
73
+ const [credentials, session] = await Promise.all([loadCaptureCredentials(root), loadCaptureSession(root)]);
74
+ return {
75
+ ...(credentials ? { credentials: { username: credentials.username, savedAt: credentials.savedAt } } : {}),
76
+ ...(session ? { session: { savedAt: session.savedAt, origin: session.origin, cookies: session.state.cookies.length, origins: session.state.origins.length } } : {}),
77
+ };
78
+ }
79
+ export async function captureAuthContext(root) {
80
+ const [credentials, session] = await Promise.all([loadCaptureCredentials(root), loadCaptureSession(root)]);
81
+ return { ...(session ? { session: session.state } : {}), credentials: Boolean(credentials) };
82
+ }
83
+ /**
84
+ * Materialize the saved sign-in for one capture run. The storage state is
85
+ * rewritten from the stored session so the file the browser reads is always
86
+ * the plain Playwright shape; the secrets file exists only while the run does.
87
+ */
88
+ export async function prepareCaptureAuth(root) {
89
+ const [credentials, session] = await Promise.all([loadCaptureCredentials(root), loadCaptureSession(root)]);
90
+ if (!credentials && !session)
91
+ return undefined;
92
+ const directory = captureAuthDirectory(root);
93
+ const material = { cleanup: async () => { } };
94
+ if (session) {
95
+ const path = join(directory, 'storage-state.json');
96
+ await writePrivateFile(path, JSON.stringify(session.state));
97
+ material.storageStatePath = path;
98
+ }
99
+ if (credentials) {
100
+ await removeStaleSecretsFiles(directory);
101
+ const path = join(directory, `secrets-${process.pid}-${Date.now()}.env`);
102
+ await writePrivateFile(path, `${CAPTURE_USERNAME_SECRET}=${dotenvValue(credentials.username)}\n${CAPTURE_PASSWORD_SECRET}=${dotenvValue(credentials.password)}\n`);
103
+ material.secretsPath = path;
104
+ material.cleanup = () => rm(path, { force: true });
105
+ }
106
+ return material;
107
+ }
108
+ /**
109
+ * A run that is killed never reaches its cleanup, so its secrets file would
110
+ * outlive it. Files named for a process that no longer exists are removed
111
+ * before a new one is written; a live run's file is left alone.
112
+ */
113
+ async function removeStaleSecretsFiles(directory) {
114
+ let entries;
115
+ try {
116
+ entries = await readdir(directory);
117
+ }
118
+ catch {
119
+ return;
120
+ }
121
+ await Promise.all(entries.map(async (entry) => {
122
+ const match = /^secrets-(\d+)-\d+\.env$/.exec(entry);
123
+ if (!match)
124
+ return;
125
+ const pid = Number(match[1]);
126
+ if (pid === process.pid || processAlive(pid))
127
+ return;
128
+ await rm(join(directory, entry), { force: true });
129
+ }));
130
+ }
131
+ function processAlive(pid) {
132
+ try {
133
+ process.kill(pid, 0);
134
+ return true;
135
+ }
136
+ catch (error) {
137
+ // EPERM means the process exists but belongs to someone else.
138
+ return error.code === 'EPERM';
139
+ }
140
+ }
141
+ /**
142
+ * The readiness probe is a plain HTTP request, so it carries the session
143
+ * cookies the browser would send for the URL: matching domain, matching path
144
+ * prefix, not expired, and not secure-only on a plain HTTP page.
145
+ */
146
+ export function sessionCookieHeader(state, url) {
147
+ if (!state)
148
+ return undefined;
149
+ const target = new URL(url);
150
+ const now = Date.now() / 1000;
151
+ const pairs = state.cookies
152
+ .filter((cookie) => cookieMatchesHost(cookie.domain, target.hostname))
153
+ .filter((cookie) => target.pathname.startsWith(cookie.path || '/'))
154
+ .filter((cookie) => cookie.expires === undefined || cookie.expires < 0 || cookie.expires > now)
155
+ .filter((cookie) => !cookie.secure || target.protocol === 'https:' || target.hostname === 'localhost' || target.hostname === '127.0.0.1')
156
+ .map((cookie) => `${cookie.name}=${cookie.value}`);
157
+ return pairs.length > 0 ? pairs.join('; ') : undefined;
158
+ }
159
+ export function describeCaptureAuth(context) {
160
+ if (!context)
161
+ return 'none';
162
+ const session = Boolean(context.session);
163
+ const credentials = 'credentials' in context && Boolean(context.credentials);
164
+ if (session && credentials)
165
+ return 'both';
166
+ if (session)
167
+ return 'session';
168
+ if (credentials)
169
+ return 'credentials';
170
+ return 'none';
171
+ }
172
+ function cookieMatchesHost(domain, hostname) {
173
+ const normalized = domain.startsWith('.') ? domain.slice(1) : domain;
174
+ return hostname === normalized || hostname.endsWith(`.${normalized}`);
175
+ }
176
+ function dotenvValue(value) {
177
+ return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r')}"`;
178
+ }
179
+ function isStorageState(value) {
180
+ if (!value || typeof value !== 'object' || Array.isArray(value))
181
+ return false;
182
+ const state = value;
183
+ return Array.isArray(state.cookies) && Array.isArray(state.origins);
184
+ }
185
+ async function writePrivateFile(path, content) {
186
+ const directory = join(path, '..');
187
+ await mkdir(directory, { recursive: true, mode: 0o700 });
188
+ await writeFile(path, `${content}\n`, { encoding: 'utf8', mode: 0o600 });
189
+ if (process.platform !== 'win32') {
190
+ await chmod(directory, 0o700);
191
+ await chmod(path, 0o600);
192
+ }
193
+ }
194
+ async function readPrivateJson(path) {
195
+ if (!(await pathExists(path)))
196
+ return undefined;
197
+ try {
198
+ return JSON.parse(await readFile(path, 'utf8'));
199
+ }
200
+ catch {
201
+ throw new DoxloopError(`Cannot read the saved capture sign-in at ${path}. Remove the file and save it again.`);
202
+ }
203
+ }
204
+ //# sourceMappingURL=capture-auth.js.map