hoi4-agent-tools 0.0.0-bootstrap.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/LICENSE +201 -0
- package/README.md +140 -2
- package/SECURITY.md +17 -0
- package/dist/bin/http.d.ts +3 -0
- package/dist/bin/http.d.ts.map +1 -0
- package/dist/bin/http.js +27 -0
- package/dist/bin/http.js.map +1 -0
- package/dist/bin/setup.d.ts +3 -0
- package/dist/bin/setup.d.ts.map +1 -0
- package/dist/bin/setup.js +188 -0
- package/dist/bin/setup.js.map +1 -0
- package/dist/bin/stdio.d.ts +3 -0
- package/dist/bin/stdio.d.ts.map +1 -0
- package/dist/bin/stdio.js +36 -0
- package/dist/bin/stdio.js.map +1 -0
- package/dist/hoi4_agent_tools/core/artifacts.d.ts +117 -0
- package/dist/hoi4_agent_tools/core/artifacts.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/artifacts.js +1047 -0
- package/dist/hoi4_agent_tools/core/artifacts.js.map +1 -0
- package/dist/hoi4_agent_tools/core/canonical.d.ts +13 -0
- package/dist/hoi4_agent_tools/core/canonical.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/canonical.js +41 -0
- package/dist/hoi4_agent_tools/core/canonical.js.map +1 -0
- package/dist/hoi4_agent_tools/core/configuration.d.ts +137 -0
- package/dist/hoi4_agent_tools/core/configuration.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/configuration.js +383 -0
- package/dist/hoi4_agent_tools/core/configuration.js.map +1 -0
- package/dist/hoi4_agent_tools/core/diagnostic-collector.d.ts +18 -0
- package/dist/hoi4_agent_tools/core/diagnostic-collector.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/diagnostic-collector.js +45 -0
- package/dist/hoi4_agent_tools/core/diagnostic-collector.js.map +1 -0
- package/dist/hoi4_agent_tools/core/diagnostics.d.ts +26 -0
- package/dist/hoi4_agent_tools/core/diagnostics.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/diagnostics.js +25 -0
- package/dist/hoi4_agent_tools/core/diagnostics.js.map +1 -0
- package/dist/hoi4_agent_tools/core/diff.d.ts +22 -0
- package/dist/hoi4_agent_tools/core/diff.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/diff.js +93 -0
- package/dist/hoi4_agent_tools/core/diff.js.map +1 -0
- package/dist/hoi4_agent_tools/core/engine.d.ts +56 -0
- package/dist/hoi4_agent_tools/core/engine.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/engine.js +319 -0
- package/dist/hoi4_agent_tools/core/engine.js.map +1 -0
- package/dist/hoi4_agent_tools/core/image-diff.d.ts +14 -0
- package/dist/hoi4_agent_tools/core/image-diff.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/image-diff.js +121 -0
- package/dist/hoi4_agent_tools/core/image-diff.js.map +1 -0
- package/dist/hoi4_agent_tools/core/index.d.ts +64 -0
- package/dist/hoi4_agent_tools/core/index.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/index.js +963 -0
- package/dist/hoi4_agent_tools/core/index.js.map +1 -0
- package/dist/hoi4_agent_tools/core/index.public.d.ts +16 -0
- package/dist/hoi4_agent_tools/core/index.public.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/index.public.js +16 -0
- package/dist/hoi4_agent_tools/core/index.public.js.map +1 -0
- package/dist/hoi4_agent_tools/core/render-budget.d.ts +51 -0
- package/dist/hoi4_agent_tools/core/render-budget.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/render-budget.js +109 -0
- package/dist/hoi4_agent_tools/core/render-budget.js.map +1 -0
- package/dist/hoi4_agent_tools/core/result.d.ts +45 -0
- package/dist/hoi4_agent_tools/core/result.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/result.js +26 -0
- package/dist/hoi4_agent_tools/core/result.js.map +1 -0
- package/dist/hoi4_agent_tools/core/scanner.d.ts +28 -0
- package/dist/hoi4_agent_tools/core/scanner.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/scanner.js +136 -0
- package/dist/hoi4_agent_tools/core/scanner.js.map +1 -0
- package/dist/hoi4_agent_tools/core/server-state.d.ts +31 -0
- package/dist/hoi4_agent_tools/core/server-state.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/server-state.js +413 -0
- package/dist/hoi4_agent_tools/core/server-state.js.map +1 -0
- package/dist/hoi4_agent_tools/core/source/encoding.d.ts +10 -0
- package/dist/hoi4_agent_tools/core/source/encoding.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/source/encoding.js +54 -0
- package/dist/hoi4_agent_tools/core/source/encoding.js.map +1 -0
- package/dist/hoi4_agent_tools/core/source/index.d.ts +7 -0
- package/dist/hoi4_agent_tools/core/source/index.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/source/index.js +7 -0
- package/dist/hoi4_agent_tools/core/source/index.js.map +1 -0
- package/dist/hoi4_agent_tools/core/source/lexer.d.ts +30 -0
- package/dist/hoi4_agent_tools/core/source/lexer.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/source/lexer.js +266 -0
- package/dist/hoi4_agent_tools/core/source/lexer.js.map +1 -0
- package/dist/hoi4_agent_tools/core/source/limits.d.ts +21 -0
- package/dist/hoi4_agent_tools/core/source/limits.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/source/limits.js +49 -0
- package/dist/hoi4_agent_tools/core/source/limits.js.map +1 -0
- package/dist/hoi4_agent_tools/core/source/localisation.d.ts +20 -0
- package/dist/hoi4_agent_tools/core/source/localisation.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/source/localisation.js +163 -0
- package/dist/hoi4_agent_tools/core/source/localisation.js.map +1 -0
- package/dist/hoi4_agent_tools/core/source/parser.d.ts +44 -0
- package/dist/hoi4_agent_tools/core/source/parser.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/source/parser.js +334 -0
- package/dist/hoi4_agent_tools/core/source/parser.js.map +1 -0
- package/dist/hoi4_agent_tools/core/source/rewrite.d.ts +10 -0
- package/dist/hoi4_agent_tools/core/source/rewrite.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/source/rewrite.js +28 -0
- package/dist/hoi4_agent_tools/core/source/rewrite.js.map +1 -0
- package/dist/hoi4_agent_tools/core/svg-text.d.ts +34 -0
- package/dist/hoi4_agent_tools/core/svg-text.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/svg-text.js +127 -0
- package/dist/hoi4_agent_tools/core/svg-text.js.map +1 -0
- package/dist/hoi4_agent_tools/core/transaction-limits.d.ts +9 -0
- package/dist/hoi4_agent_tools/core/transaction-limits.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/transaction-limits.js +9 -0
- package/dist/hoi4_agent_tools/core/transaction-limits.js.map +1 -0
- package/dist/hoi4_agent_tools/core/transactions.d.ts +162 -0
- package/dist/hoi4_agent_tools/core/transactions.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/transactions.js +1181 -0
- package/dist/hoi4_agent_tools/core/transactions.js.map +1 -0
- package/dist/hoi4_agent_tools/core/workspace.d.ts +68 -0
- package/dist/hoi4_agent_tools/core/workspace.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/core/workspace.js +794 -0
- package/dist/hoi4_agent_tools/core/workspace.js.map +1 -0
- package/dist/hoi4_agent_tools/focus/compiler.d.ts +9 -0
- package/dist/hoi4_agent_tools/focus/compiler.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/focus/compiler.js +273 -0
- package/dist/hoi4_agent_tools/focus/compiler.js.map +1 -0
- package/dist/hoi4_agent_tools/focus/continuous-render.d.ts +32 -0
- package/dist/hoi4_agent_tools/focus/continuous-render.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/focus/continuous-render.js +179 -0
- package/dist/hoi4_agent_tools/focus/continuous-render.js.map +1 -0
- package/dist/hoi4_agent_tools/focus/drift.d.ts +22 -0
- package/dist/hoi4_agent_tools/focus/drift.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/focus/drift.js +124 -0
- package/dist/hoi4_agent_tools/focus/drift.js.map +1 -0
- package/dist/hoi4_agent_tools/focus/geometry.d.ts +17 -0
- package/dist/hoi4_agent_tools/focus/geometry.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/focus/geometry.js +35 -0
- package/dist/hoi4_agent_tools/focus/geometry.js.map +1 -0
- package/dist/hoi4_agent_tools/focus/importer.d.ts +9 -0
- package/dist/hoi4_agent_tools/focus/importer.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/focus/importer.js +732 -0
- package/dist/hoi4_agent_tools/focus/importer.js.map +1 -0
- package/dist/hoi4_agent_tools/focus/index.d.ts +14 -0
- package/dist/hoi4_agent_tools/focus/index.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/focus/index.js +14 -0
- package/dist/hoi4_agent_tools/focus/index.js.map +1 -0
- package/dist/hoi4_agent_tools/focus/layout.d.ts +5 -0
- package/dist/hoi4_agent_tools/focus/layout.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/focus/layout.js +653 -0
- package/dist/hoi4_agent_tools/focus/layout.js.map +1 -0
- package/dist/hoi4_agent_tools/focus/limits.d.ts +9 -0
- package/dist/hoi4_agent_tools/focus/limits.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/focus/limits.js +9 -0
- package/dist/hoi4_agent_tools/focus/limits.js.map +1 -0
- package/dist/hoi4_agent_tools/focus/lint.d.ts +14 -0
- package/dist/hoi4_agent_tools/focus/lint.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/focus/lint.js +839 -0
- package/dist/hoi4_agent_tools/focus/lint.js.map +1 -0
- package/dist/hoi4_agent_tools/focus/model.d.ts +303 -0
- package/dist/hoi4_agent_tools/focus/model.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/focus/model.js +20 -0
- package/dist/hoi4_agent_tools/focus/model.js.map +1 -0
- package/dist/hoi4_agent_tools/focus/native-effects.d.ts +9 -0
- package/dist/hoi4_agent_tools/focus/native-effects.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/focus/native-effects.js +563 -0
- package/dist/hoi4_agent_tools/focus/native-effects.js.map +1 -0
- package/dist/hoi4_agent_tools/focus/planning.d.ts +15 -0
- package/dist/hoi4_agent_tools/focus/planning.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/focus/planning.js +241 -0
- package/dist/hoi4_agent_tools/focus/planning.js.map +1 -0
- package/dist/hoi4_agent_tools/focus/presentation.d.ts +24 -0
- package/dist/hoi4_agent_tools/focus/presentation.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/focus/presentation.js +204 -0
- package/dist/hoi4_agent_tools/focus/presentation.js.map +1 -0
- package/dist/hoi4_agent_tools/focus/render.d.ts +35 -0
- package/dist/hoi4_agent_tools/focus/render.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/focus/render.js +491 -0
- package/dist/hoi4_agent_tools/focus/render.js.map +1 -0
- package/dist/hoi4_agent_tools/focus/service.d.ts +82 -0
- package/dist/hoi4_agent_tools/focus/service.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/focus/service.js +492 -0
- package/dist/hoi4_agent_tools/focus/service.js.map +1 -0
- package/dist/hoi4_agent_tools/focus/source-update.d.ts +6 -0
- package/dist/hoi4_agent_tools/focus/source-update.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/focus/source-update.js +328 -0
- package/dist/hoi4_agent_tools/focus/source-update.js.map +1 -0
- package/dist/hoi4_agent_tools/gui/animation-manifest.d.ts +37 -0
- package/dist/hoi4_agent_tools/gui/animation-manifest.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/gui/animation-manifest.js +66 -0
- package/dist/hoi4_agent_tools/gui/animation-manifest.js.map +1 -0
- package/dist/hoi4_agent_tools/gui/assets.d.ts +71 -0
- package/dist/hoi4_agent_tools/gui/assets.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/gui/assets.js +629 -0
- package/dist/hoi4_agent_tools/gui/assets.js.map +1 -0
- package/dist/hoi4_agent_tools/gui/dds.d.ts +17 -0
- package/dist/hoi4_agent_tools/gui/dds.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/gui/dds.js +245 -0
- package/dist/hoi4_agent_tools/gui/dds.js.map +1 -0
- package/dist/hoi4_agent_tools/gui/helpers.d.ts +101 -0
- package/dist/hoi4_agent_tools/gui/helpers.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/gui/helpers.js +1072 -0
- package/dist/hoi4_agent_tools/gui/helpers.js.map +1 -0
- package/dist/hoi4_agent_tools/gui/index.d.ts +15 -0
- package/dist/hoi4_agent_tools/gui/index.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/gui/index.js +15 -0
- package/dist/hoi4_agent_tools/gui/index.js.map +1 -0
- package/dist/hoi4_agent_tools/gui/layout.d.ts +6 -0
- package/dist/hoi4_agent_tools/gui/layout.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/gui/layout.js +753 -0
- package/dist/hoi4_agent_tools/gui/layout.js.map +1 -0
- package/dist/hoi4_agent_tools/gui/limits.d.ts +25 -0
- package/dist/hoi4_agent_tools/gui/limits.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/gui/limits.js +27 -0
- package/dist/hoi4_agent_tools/gui/limits.js.map +1 -0
- package/dist/hoi4_agent_tools/gui/renderer.d.ts +18 -0
- package/dist/hoi4_agent_tools/gui/renderer.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/gui/renderer.js +388 -0
- package/dist/hoi4_agent_tools/gui/renderer.js.map +1 -0
- package/dist/hoi4_agent_tools/gui/scenario.d.ts +66 -0
- package/dist/hoi4_agent_tools/gui/scenario.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/gui/scenario.js +191 -0
- package/dist/hoi4_agent_tools/gui/scenario.js.map +1 -0
- package/dist/hoi4_agent_tools/gui/source-graph.d.ts +7 -0
- package/dist/hoi4_agent_tools/gui/source-graph.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/gui/source-graph.js +1074 -0
- package/dist/hoi4_agent_tools/gui/source-graph.js.map +1 -0
- package/dist/hoi4_agent_tools/gui/source-patch.d.ts +13 -0
- package/dist/hoi4_agent_tools/gui/source-patch.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/gui/source-patch.js +90 -0
- package/dist/hoi4_agent_tools/gui/source-patch.js.map +1 -0
- package/dist/hoi4_agent_tools/gui/studio.d.ts +86 -0
- package/dist/hoi4_agent_tools/gui/studio.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/gui/studio.js +830 -0
- package/dist/hoi4_agent_tools/gui/studio.js.map +1 -0
- package/dist/hoi4_agent_tools/gui/tga.d.ts +16 -0
- package/dist/hoi4_agent_tools/gui/tga.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/gui/tga.js +157 -0
- package/dist/hoi4_agent_tools/gui/tga.js.map +1 -0
- package/dist/hoi4_agent_tools/gui/types.d.ts +322 -0
- package/dist/hoi4_agent_tools/gui/types.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/gui/types.js +19 -0
- package/dist/hoi4_agent_tools/gui/types.js.map +1 -0
- package/dist/hoi4_agent_tools/gui/validators.d.ts +8 -0
- package/dist/hoi4_agent_tools/gui/validators.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/gui/validators.js +951 -0
- package/dist/hoi4_agent_tools/gui/validators.js.map +1 -0
- package/dist/hoi4_agent_tools/map/bmp.d.ts +62 -0
- package/dist/hoi4_agent_tools/map/bmp.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/map/bmp.js +363 -0
- package/dist/hoi4_agent_tools/map/bmp.js.map +1 -0
- package/dist/hoi4_agent_tools/map/diagnostic-limit.d.ts +5 -0
- package/dist/hoi4_agent_tools/map/diagnostic-limit.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/map/diagnostic-limit.js +21 -0
- package/dist/hoi4_agent_tools/map/diagnostic-limit.js.map +1 -0
- package/dist/hoi4_agent_tools/map/index.d.ts +8 -0
- package/dist/hoi4_agent_tools/map/index.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/map/index.js +8 -0
- package/dist/hoi4_agent_tools/map/index.js.map +1 -0
- package/dist/hoi4_agent_tools/map/limits.d.ts +13 -0
- package/dist/hoi4_agent_tools/map/limits.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/map/limits.js +13 -0
- package/dist/hoi4_agent_tools/map/limits.js.map +1 -0
- package/dist/hoi4_agent_tools/map/model.d.ts +269 -0
- package/dist/hoi4_agent_tools/map/model.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/map/model.js +1114 -0
- package/dist/hoi4_agent_tools/map/model.js.map +1 -0
- package/dist/hoi4_agent_tools/map/operations.d.ts +401 -0
- package/dist/hoi4_agent_tools/map/operations.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/map/operations.js +2717 -0
- package/dist/hoi4_agent_tools/map/operations.js.map +1 -0
- package/dist/hoi4_agent_tools/map/render.d.ts +90 -0
- package/dist/hoi4_agent_tools/map/render.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/map/render.js +759 -0
- package/dist/hoi4_agent_tools/map/render.js.map +1 -0
- package/dist/hoi4_agent_tools/map/service.d.ts +81 -0
- package/dist/hoi4_agent_tools/map/service.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/map/service.js +695 -0
- package/dist/hoi4_agent_tools/map/service.js.map +1 -0
- package/dist/hoi4_agent_tools/map/validation.d.ts +23 -0
- package/dist/hoi4_agent_tools/map/validation.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/map/validation.js +1177 -0
- package/dist/hoi4_agent_tools/map/validation.js.map +1 -0
- package/dist/hoi4_agent_tools/mcp/prompts/register.d.ts +3 -0
- package/dist/hoi4_agent_tools/mcp/prompts/register.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/mcp/prompts/register.js +50 -0
- package/dist/hoi4_agent_tools/mcp/prompts/register.js.map +1 -0
- package/dist/hoi4_agent_tools/mcp/registry/metadata.d.ts +9 -0
- package/dist/hoi4_agent_tools/mcp/registry/metadata.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/mcp/registry/metadata.js +10 -0
- package/dist/hoi4_agent_tools/mcp/registry/metadata.js.map +1 -0
- package/dist/hoi4_agent_tools/mcp/resources/register.d.ts +5 -0
- package/dist/hoi4_agent_tools/mcp/resources/register.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/mcp/resources/register.js +278 -0
- package/dist/hoi4_agent_tools/mcp/resources/register.js.map +1 -0
- package/dist/hoi4_agent_tools/mcp/security/auth.d.ts +24 -0
- package/dist/hoi4_agent_tools/mcp/security/auth.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/mcp/security/auth.js +234 -0
- package/dist/hoi4_agent_tools/mcp/security/auth.js.map +1 -0
- package/dist/hoi4_agent_tools/mcp/server/base-tools.d.ts +35 -0
- package/dist/hoi4_agent_tools/mcp/server/base-tools.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/mcp/server/base-tools.js +835 -0
- package/dist/hoi4_agent_tools/mcp/server/base-tools.js.map +1 -0
- package/dist/hoi4_agent_tools/mcp/server/create.d.ts +5 -0
- package/dist/hoi4_agent_tools/mcp/server/create.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/mcp/server/create.js +26 -0
- package/dist/hoi4_agent_tools/mcp/server/create.js.map +1 -0
- package/dist/hoi4_agent_tools/mcp/server/output-schemas.d.ts +133 -0
- package/dist/hoi4_agent_tools/mcp/server/output-schemas.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/mcp/server/output-schemas.js +127 -0
- package/dist/hoi4_agent_tools/mcp/server/output-schemas.js.map +1 -0
- package/dist/hoi4_agent_tools/mcp/server/progress.d.ts +8 -0
- package/dist/hoi4_agent_tools/mcp/server/progress.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/mcp/server/progress.js +21 -0
- package/dist/hoi4_agent_tools/mcp/server/progress.js.map +1 -0
- package/dist/hoi4_agent_tools/mcp/server/result.d.ts +232 -0
- package/dist/hoi4_agent_tools/mcp/server/result.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/mcp/server/result.js +353 -0
- package/dist/hoi4_agent_tools/mcp/server/result.js.map +1 -0
- package/dist/hoi4_agent_tools/mcp/tools/focus.d.ts +5 -0
- package/dist/hoi4_agent_tools/mcp/tools/focus.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +1059 -0
- package/dist/hoi4_agent_tools/mcp/tools/focus.js.map +1 -0
- package/dist/hoi4_agent_tools/mcp/tools/gui.d.ts +111 -0
- package/dist/hoi4_agent_tools/mcp/tools/gui.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +598 -0
- package/dist/hoi4_agent_tools/mcp/tools/gui.js.map +1 -0
- package/dist/hoi4_agent_tools/mcp/tools/map.d.ts +5 -0
- package/dist/hoi4_agent_tools/mcp/tools/map.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/mcp/tools/map.js +685 -0
- package/dist/hoi4_agent_tools/mcp/tools/map.js.map +1 -0
- package/dist/hoi4_agent_tools/mcp/transports/bounded-stdio.d.ts +58 -0
- package/dist/hoi4_agent_tools/mcp/transports/bounded-stdio.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/mcp/transports/bounded-stdio.js +181 -0
- package/dist/hoi4_agent_tools/mcp/transports/bounded-stdio.js.map +1 -0
- package/dist/hoi4_agent_tools/mcp/transports/event-store.d.ts +26 -0
- package/dist/hoi4_agent_tools/mcp/transports/event-store.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/mcp/transports/event-store.js +86 -0
- package/dist/hoi4_agent_tools/mcp/transports/event-store.js.map +1 -0
- package/dist/hoi4_agent_tools/mcp/transports/http.d.ts +28 -0
- package/dist/hoi4_agent_tools/mcp/transports/http.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/mcp/transports/http.js +744 -0
- package/dist/hoi4_agent_tools/mcp/transports/http.js.map +1 -0
- package/dist/hoi4_agent_tools/mcp/transports/protocol-gate.d.ts +15 -0
- package/dist/hoi4_agent_tools/mcp/transports/protocol-gate.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/mcp/transports/protocol-gate.js +51 -0
- package/dist/hoi4_agent_tools/mcp/transports/protocol-gate.js.map +1 -0
- package/dist/hoi4_agent_tools/runtime.d.ts +4 -0
- package/dist/hoi4_agent_tools/runtime.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/runtime.js +27 -0
- package/dist/hoi4_agent_tools/runtime.js.map +1 -0
- package/dist/hoi4_agent_tools/schemas/common.d.ts +74 -0
- package/dist/hoi4_agent_tools/schemas/common.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/schemas/common.js +51 -0
- package/dist/hoi4_agent_tools/schemas/common.js.map +1 -0
- package/dist/hoi4_agent_tools/schemas/focus.d.ts +784 -0
- package/dist/hoi4_agent_tools/schemas/focus.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/schemas/focus.js +330 -0
- package/dist/hoi4_agent_tools/schemas/focus.js.map +1 -0
- package/dist/hoi4_agent_tools/schemas/index.d.ts +5 -0
- package/dist/hoi4_agent_tools/schemas/index.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/schemas/index.js +5 -0
- package/dist/hoi4_agent_tools/schemas/index.js.map +1 -0
- package/dist/hoi4_agent_tools/schemas/map.d.ts +654 -0
- package/dist/hoi4_agent_tools/schemas/map.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/schemas/map.js +570 -0
- package/dist/hoi4_agent_tools/schemas/map.js.map +1 -0
- package/dist/hoi4_agent_tools/schemas/transaction.d.ts +259 -0
- package/dist/hoi4_agent_tools/schemas/transaction.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/schemas/transaction.js +104 -0
- package/dist/hoi4_agent_tools/schemas/transaction.js.map +1 -0
- package/dist/hoi4_agent_tools/version.d.ts +6 -0
- package/dist/hoi4_agent_tools/version.d.ts.map +1 -0
- package/dist/hoi4_agent_tools/version.js +7 -0
- package/dist/hoi4_agent_tools/version.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/docs/README.md +22 -0
- package/docs/adr/0001-runtime-and-language.md +16 -0
- package/docs/adr/0002-lossless-source-model.md +18 -0
- package/docs/adr/0003-image-and-font-codecs.md +18 -0
- package/docs/adr/0004-focus-layout.md +16 -0
- package/docs/adr/0005-offline-rendering.md +18 -0
- package/docs/adr/0006-mcp-sdk-and-protocol.md +16 -0
- package/docs/adr/0007-storage-and-transactions.md +24 -0
- package/docs/adr/0008-test-strategy.md +18 -0
- package/docs/adr/0009-packaging-and-registry.md +16 -0
- package/docs/adr/0010-unsupported-and-refusal-policy.md +18 -0
- package/docs/adr/0011-http-security.md +16 -0
- package/docs/adr/README.md +13 -0
- package/docs/architecture.md +60 -0
- package/docs/artifacts.md +133 -0
- package/docs/compatibility.md +33 -0
- package/docs/completion-report.md +69 -0
- package/docs/configuration.md +285 -0
- package/docs/focus-workflow.md +68 -0
- package/docs/gui-workflow.md +121 -0
- package/docs/limitations.md +30 -0
- package/docs/map-workflow.md +89 -0
- package/docs/performance.md +117 -0
- package/docs/release.md +200 -0
- package/docs/research/mcp-2026-07-10.md +46 -0
- package/docs/research/source-ledger.md +68 -0
- package/docs/security.md +139 -0
- package/docs/self-hosting.md +168 -0
- package/docs/testing.md +43 -0
- package/docs/third-party-notices.md +11 -0
- package/docs/transactions.md +162 -0
- package/docs/troubleshooting.md +46 -0
- package/package.json +98 -5
- package/schemas/configuration.schema.json +794 -0
- package/schemas/continuous-focus-palette.schema.json +325 -0
- package/schemas/focus-plan.schema.json +902 -0
- package/schemas/focus-planning-sidecar.schema.json +364 -0
- package/schemas/gui-animation-source.schema.json +176 -0
- package/schemas/gui-helper.schema.json +301 -0
- package/schemas/gui-scenario.schema.json +298 -0
- package/schemas/map-operation.schema.json +2284 -0
- package/schemas/operation-result.schema.json +344 -0
- package/schemas/transaction-manifest.schema.json +685 -0
- package/server.json +32 -0
|
@@ -0,0 +1,1047 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstat, mkdir, open, readFile, readdir, stat, unlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { z } from 'zod/v4';
|
|
5
|
+
import { canonicalJson, compareCodeUnits, sha256Bytes } from './canonical.js';
|
|
6
|
+
import { ServiceError } from './result.js';
|
|
7
|
+
import { containedGeneratedPath, isPortablePathSegment } from './workspace.js';
|
|
8
|
+
const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/u);
|
|
9
|
+
const artifactProvenanceSchema = z
|
|
10
|
+
.object({
|
|
11
|
+
kind: z.string(),
|
|
12
|
+
toolVersion: z.string(),
|
|
13
|
+
schemaVersion: z.string(),
|
|
14
|
+
sourceHashes: z.record(z.string(), sha256Schema),
|
|
15
|
+
renderProfile: z.record(z.string(), z.unknown()).optional(),
|
|
16
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
17
|
+
})
|
|
18
|
+
.strict();
|
|
19
|
+
const artifactManifestSchema = z
|
|
20
|
+
.object({
|
|
21
|
+
version: z.literal(2),
|
|
22
|
+
workspaceIdentity: sha256Schema,
|
|
23
|
+
ownerIdentity: sha256Schema,
|
|
24
|
+
name: z.string(),
|
|
25
|
+
mimeType: z.string(),
|
|
26
|
+
size: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
27
|
+
sha256: sha256Schema,
|
|
28
|
+
provenanceHash: sha256Schema,
|
|
29
|
+
provenance: artifactProvenanceSchema,
|
|
30
|
+
description: z.string().optional(),
|
|
31
|
+
})
|
|
32
|
+
.strict();
|
|
33
|
+
function artifactFileIdentity(value) {
|
|
34
|
+
return {
|
|
35
|
+
dev: value.dev,
|
|
36
|
+
ino: value.ino,
|
|
37
|
+
size: value.size,
|
|
38
|
+
mtimeMs: value.mtimeMs,
|
|
39
|
+
ctimeMs: value.ctimeMs,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function sameArtifactIdentity(left, right) {
|
|
43
|
+
return (left?.dev === right.dev &&
|
|
44
|
+
left.ino === right.ino &&
|
|
45
|
+
left.size === right.size &&
|
|
46
|
+
left.mtimeMs === right.mtimeMs &&
|
|
47
|
+
left.ctimeMs === right.ctimeMs);
|
|
48
|
+
}
|
|
49
|
+
function provenanceHashFor(manifest) {
|
|
50
|
+
return sha256Bytes(canonicalJson({
|
|
51
|
+
version: manifest.version,
|
|
52
|
+
workspaceIdentity: manifest.workspaceIdentity,
|
|
53
|
+
ownerIdentity: manifest.ownerIdentity,
|
|
54
|
+
name: manifest.name,
|
|
55
|
+
mimeType: manifest.mimeType,
|
|
56
|
+
size: manifest.size,
|
|
57
|
+
sha256: manifest.sha256,
|
|
58
|
+
provenance: manifest.provenance,
|
|
59
|
+
description: manifest.description ?? null,
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
function artifactContentBytes(content) {
|
|
63
|
+
return typeof content === 'string' ? Buffer.from(content, 'utf8') : Buffer.from(content);
|
|
64
|
+
}
|
|
65
|
+
function chunkArtifactName(originalSha256, index) {
|
|
66
|
+
return `chunk-${originalSha256}-${String(index).padStart(6, '0')}.part`;
|
|
67
|
+
}
|
|
68
|
+
function chunkIndexArtifactName(originalName, originalSha256) {
|
|
69
|
+
const suffix = `.${originalSha256.slice(0, 16)}.chunks.json`;
|
|
70
|
+
const prefix = originalName.slice(0, Math.max(1, 128 - suffix.length));
|
|
71
|
+
return `${prefix}${suffix}`;
|
|
72
|
+
}
|
|
73
|
+
function chunkedProvenance(provenance, role, original, details) {
|
|
74
|
+
return {
|
|
75
|
+
...provenance,
|
|
76
|
+
kind: `${provenance.kind}-${role}`,
|
|
77
|
+
metadata: {
|
|
78
|
+
...(provenance.metadata ?? {}),
|
|
79
|
+
chunkedArtifact: {
|
|
80
|
+
role,
|
|
81
|
+
originalName: original.name,
|
|
82
|
+
originalMimeType: original.mimeType,
|
|
83
|
+
originalSize: original.size,
|
|
84
|
+
originalSha256: original.sha256,
|
|
85
|
+
...details,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Keep provenance manifests bounded while retaining a deterministic commitment
|
|
92
|
+
* to every source path/hash pair. Complete inventories remain in domain evidence.
|
|
93
|
+
*/
|
|
94
|
+
export function boundedSourceHashEvidence(sourceHashes) {
|
|
95
|
+
const entries = Object.entries(sourceHashes).sort(([left], [right]) => compareCodeUnits(left, right));
|
|
96
|
+
const digest = createHash('sha256');
|
|
97
|
+
digest.update('source-hash-inventory.v1\0', 'utf8');
|
|
98
|
+
const retained = [];
|
|
99
|
+
let retainedBytes = 2;
|
|
100
|
+
let retainedCount = 0;
|
|
101
|
+
const maximumRetainedBytes = 131_072;
|
|
102
|
+
const maximumRetainedEntries = 256;
|
|
103
|
+
for (const [sourcePath, sourceHash] of entries) {
|
|
104
|
+
const pathBytes = Buffer.from(sourcePath, 'utf8');
|
|
105
|
+
digest.update(String(pathBytes.length), 'ascii');
|
|
106
|
+
digest.update(':', 'ascii');
|
|
107
|
+
digest.update(pathBytes);
|
|
108
|
+
digest.update(':', 'ascii');
|
|
109
|
+
digest.update(sourceHash, 'ascii');
|
|
110
|
+
digest.update('\n', 'ascii');
|
|
111
|
+
const entryBytes = Buffer.byteLength(JSON.stringify(sourcePath), 'utf8') +
|
|
112
|
+
Buffer.byteLength(JSON.stringify(sourceHash), 'utf8') +
|
|
113
|
+
2;
|
|
114
|
+
if (retainedCount < maximumRetainedEntries &&
|
|
115
|
+
retainedBytes + entryBytes <= maximumRetainedBytes) {
|
|
116
|
+
retained.push([sourcePath, sourceHash]);
|
|
117
|
+
retainedBytes += entryBytes;
|
|
118
|
+
retainedCount += 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
sourceHashes: Object.fromEntries(retained),
|
|
123
|
+
inventory: {
|
|
124
|
+
schemaVersion: 1,
|
|
125
|
+
algorithm: 'sha256-length-prefixed-path-digest-v1',
|
|
126
|
+
count: entries.length,
|
|
127
|
+
digest: digest.digest('hex'),
|
|
128
|
+
retainedCount,
|
|
129
|
+
truncated: retainedCount !== entries.length,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function assertManifestIntegrity(manifest, expected, workspace) {
|
|
134
|
+
if (manifest.workspaceIdentity !== workspace.workspaceIdentity ||
|
|
135
|
+
manifest.ownerIdentity !== workspace.ownerIdentity ||
|
|
136
|
+
manifest.sha256 !== expected.sha256 ||
|
|
137
|
+
manifest.provenanceHash !== expected.provenanceHash ||
|
|
138
|
+
manifest.name !== expected.name ||
|
|
139
|
+
provenanceHashFor(manifest) !== expected.provenanceHash) {
|
|
140
|
+
throw new ServiceError('ARTIFACT_MANIFEST_INTEGRITY_FAILED', 'Artifact provenance manifest does not match its immutable address');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Return the only artifact fields that may cross the MCP trust boundary. */
|
|
144
|
+
export function publicArtifactLink(artifact) {
|
|
145
|
+
return {
|
|
146
|
+
uri: artifact.uri,
|
|
147
|
+
name: artifact.name,
|
|
148
|
+
mimeType: artifact.mimeType,
|
|
149
|
+
...(artifact.size === undefined ? {} : { size: artifact.size }),
|
|
150
|
+
...(artifact.sha256 === undefined ? {} : { sha256: artifact.sha256 }),
|
|
151
|
+
...(artifact.description === undefined ? {} : { description: artifact.description }),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const safeNamePattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
155
|
+
const reservedManifestSuffix = '.manifest.json';
|
|
156
|
+
const artifactReadBufferBytes = 1_048_576;
|
|
157
|
+
const artifactPageMaxEntries = 100;
|
|
158
|
+
const artifactManifestMaxBytes = 1_048_576;
|
|
159
|
+
const artifactLogicalBatchMaxBytes = 536_870_912;
|
|
160
|
+
const artifactHashYieldBytes = 16_777_216;
|
|
161
|
+
const artifactPreflightYieldEntries = 256;
|
|
162
|
+
const ownedArtifactContent = Symbol('ownedArtifactContent');
|
|
163
|
+
const precomputedArtifactSha256 = Symbol('precomputedArtifactSha256');
|
|
164
|
+
export const chunkedArtifactIndexMimeType = 'application/json';
|
|
165
|
+
function isValidArtifactName(name) {
|
|
166
|
+
return (safeNamePattern.test(name) &&
|
|
167
|
+
isPortablePathSegment(name) &&
|
|
168
|
+
!name.toLowerCase().endsWith(reservedManifestSuffix));
|
|
169
|
+
}
|
|
170
|
+
function artifactContentByteLength(content) {
|
|
171
|
+
return typeof content === 'string' ? Buffer.byteLength(content, 'utf8') : content.byteLength;
|
|
172
|
+
}
|
|
173
|
+
async function preflightArtifactBatch(writes, limits, signal) {
|
|
174
|
+
const { maxBytes, maxEntries, maxSingleBytes, chunked } = limits;
|
|
175
|
+
if (!Number.isSafeInteger(maxBytes) ||
|
|
176
|
+
maxBytes < 1 ||
|
|
177
|
+
!Number.isSafeInteger(maxEntries) ||
|
|
178
|
+
maxEntries < 1 ||
|
|
179
|
+
!Number.isSafeInteger(maxSingleBytes) ||
|
|
180
|
+
maxSingleBytes < 1 ||
|
|
181
|
+
maxSingleBytes > maxBytes) {
|
|
182
|
+
throw new ServiceError('ARTIFACT_STORAGE_LIMIT', 'Configured artifact limits are invalid');
|
|
183
|
+
}
|
|
184
|
+
const contentLengths = [];
|
|
185
|
+
let totalBytes = 0;
|
|
186
|
+
let projectedEntries = 0;
|
|
187
|
+
for (const [writeIndex, write] of writes.entries()) {
|
|
188
|
+
signal?.throwIfAborted();
|
|
189
|
+
if (!isValidArtifactName(write.name)) {
|
|
190
|
+
throw new ServiceError('ARTIFACT_NAME_INVALID', `Invalid artifact name: ${write.name}`);
|
|
191
|
+
}
|
|
192
|
+
if (!artifactProvenanceSchema.safeParse(write.provenance).success) {
|
|
193
|
+
throw new ServiceError('ARTIFACT_PROVENANCE_INVALID', 'Artifact provenance must contain only valid SHA-256 source hashes');
|
|
194
|
+
}
|
|
195
|
+
const contentLength = artifactContentByteLength(write.content);
|
|
196
|
+
if (!Number.isSafeInteger(contentLength) || contentLength < 0) {
|
|
197
|
+
throw new ServiceError('ARTIFACT_STORAGE_LIMIT', 'Artifact content byte length is unsafe', {
|
|
198
|
+
name: write.name,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
if (!chunked && contentLength > maxSingleBytes) {
|
|
202
|
+
throw new ServiceError('ARTIFACT_SINGLE_LIMIT', 'Artifact exceeds the configured per-artifact byte limit', {
|
|
203
|
+
name: write.name,
|
|
204
|
+
mimeType: write.mimeType,
|
|
205
|
+
size: contentLength,
|
|
206
|
+
maximumBytes: maxSingleBytes,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
contentLengths.push(contentLength);
|
|
210
|
+
totalBytes += contentLength;
|
|
211
|
+
const entriesForWrite = chunked && contentLength > maxSingleBytes ? Math.ceil(contentLength / maxSingleBytes) + 1 : 1;
|
|
212
|
+
projectedEntries += entriesForWrite;
|
|
213
|
+
if (!Number.isSafeInteger(totalBytes) || !Number.isSafeInteger(projectedEntries)) {
|
|
214
|
+
throw new ServiceError('ARTIFACT_STORAGE_LIMIT', 'Artifact batch size is unsafe');
|
|
215
|
+
}
|
|
216
|
+
if ((writeIndex + 1) % artifactPreflightYieldEntries === 0) {
|
|
217
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
218
|
+
signal?.throwIfAborted();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const preparationCeiling = Math.min(maxBytes, artifactLogicalBatchMaxBytes);
|
|
222
|
+
if (totalBytes > preparationCeiling) {
|
|
223
|
+
const fixedCeilingApplies = artifactLogicalBatchMaxBytes <= maxBytes;
|
|
224
|
+
throw new ServiceError(fixedCeilingApplies ? 'ARTIFACT_LOGICAL_BATCH_LIMIT' : 'ARTIFACT_STORAGE_LIMIT', fixedCeilingApplies
|
|
225
|
+
? 'Logical artifact batch exceeds the fixed in-memory preparation ceiling'
|
|
226
|
+
: 'Logical artifact batch exceeds the configured aggregate artifact-byte limit', { size: totalBytes, maximumBytes: preparationCeiling });
|
|
227
|
+
}
|
|
228
|
+
if (projectedEntries > maxEntries) {
|
|
229
|
+
throw new ServiceError('ARTIFACT_STORAGE_LIMIT', 'Artifact batch exceeds the configured artifact-entry limit', { projectedEntries, maximumEntries: maxEntries });
|
|
230
|
+
}
|
|
231
|
+
signal?.throwIfAborted();
|
|
232
|
+
return { contentLengths, totalBytes, projectedEntries };
|
|
233
|
+
}
|
|
234
|
+
async function sha256BytesWithSignal(bytes, signal) {
|
|
235
|
+
signal?.throwIfAborted();
|
|
236
|
+
const hash = createHash('sha256');
|
|
237
|
+
for (let offset = 0; offset < bytes.length; offset += artifactHashYieldBytes) {
|
|
238
|
+
hash.update(bytes.subarray(offset, Math.min(bytes.length, offset + artifactHashYieldBytes)));
|
|
239
|
+
if (offset + artifactHashYieldBytes < bytes.length) {
|
|
240
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
241
|
+
signal?.throwIfAborted();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
signal?.throwIfAborted();
|
|
245
|
+
return hash.digest('hex');
|
|
246
|
+
}
|
|
247
|
+
function manifestAddressSortKey(fileName) {
|
|
248
|
+
const matched = /^(.*)\.([a-f0-9]{64})\.manifest\.json$/u.exec(fileName);
|
|
249
|
+
return matched === null ? fileName : `${matched[2]}/${encodeURIComponent(matched[1])}`;
|
|
250
|
+
}
|
|
251
|
+
async function fileExists(filePath) {
|
|
252
|
+
try {
|
|
253
|
+
await stat(filePath);
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (error.code === 'ENOENT')
|
|
258
|
+
return false;
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const artifactQueues = new Map();
|
|
263
|
+
async function waitForArtifactTurn(previous, signal) {
|
|
264
|
+
signal?.throwIfAborted();
|
|
265
|
+
if (signal === undefined) {
|
|
266
|
+
await previous;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
let abort;
|
|
270
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
271
|
+
abort = () => {
|
|
272
|
+
reject(new DOMException('The operation was aborted', 'AbortError'));
|
|
273
|
+
};
|
|
274
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
275
|
+
});
|
|
276
|
+
try {
|
|
277
|
+
await Promise.race([previous, aborted]);
|
|
278
|
+
}
|
|
279
|
+
finally {
|
|
280
|
+
signal.removeEventListener('abort', abort);
|
|
281
|
+
}
|
|
282
|
+
signal.throwIfAborted();
|
|
283
|
+
}
|
|
284
|
+
async function withArtifactQueue(root, action, signal) {
|
|
285
|
+
const previous = artifactQueues.get(root) ?? Promise.resolve();
|
|
286
|
+
let release;
|
|
287
|
+
const gate = new Promise((resolve) => {
|
|
288
|
+
release = resolve;
|
|
289
|
+
});
|
|
290
|
+
const tail = previous.then(() => gate);
|
|
291
|
+
artifactQueues.set(root, tail);
|
|
292
|
+
try {
|
|
293
|
+
await waitForArtifactTurn(previous, signal);
|
|
294
|
+
return await action();
|
|
295
|
+
}
|
|
296
|
+
finally {
|
|
297
|
+
release();
|
|
298
|
+
void tail.then(() => {
|
|
299
|
+
if (artifactQueues.get(root) === tail)
|
|
300
|
+
artifactQueues.delete(root);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async function artifactUsage(root, signal) {
|
|
305
|
+
let bytes = 0;
|
|
306
|
+
let entries = 0;
|
|
307
|
+
const walk = async (directory) => {
|
|
308
|
+
signal?.throwIfAborted();
|
|
309
|
+
let children;
|
|
310
|
+
try {
|
|
311
|
+
children = await readdir(directory, { withFileTypes: true });
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
if (error.code === 'ENOENT')
|
|
315
|
+
return;
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
for (const child of children) {
|
|
319
|
+
signal?.throwIfAborted();
|
|
320
|
+
if (child.isSymbolicLink()) {
|
|
321
|
+
throw new ServiceError('ARTIFACT_STORAGE_UNSAFE', 'Artifact storage contains a symbolic link or junction');
|
|
322
|
+
}
|
|
323
|
+
const candidate = await containedGeneratedPath(root, path.relative(root, path.join(directory, child.name)));
|
|
324
|
+
if (child.isDirectory())
|
|
325
|
+
await walk(candidate);
|
|
326
|
+
else if (child.isFile()) {
|
|
327
|
+
bytes += (await stat(candidate)).size;
|
|
328
|
+
if (child.name.endsWith('.manifest.json'))
|
|
329
|
+
entries += 1;
|
|
330
|
+
if (!Number.isSafeInteger(bytes)) {
|
|
331
|
+
throw new ServiceError('ARTIFACT_STORAGE_LIMIT', 'Artifact storage size is unsafe');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
await walk(await containedGeneratedPath(root));
|
|
337
|
+
return { bytes, entries };
|
|
338
|
+
}
|
|
339
|
+
async function readArtifactManifest(filePath, signal) {
|
|
340
|
+
signal?.throwIfAborted();
|
|
341
|
+
let metadata;
|
|
342
|
+
try {
|
|
343
|
+
metadata = await lstat(filePath);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
throw new ServiceError('ARTIFACT_NOT_FOUND', 'Artifact provenance manifest is unavailable');
|
|
347
|
+
}
|
|
348
|
+
signal?.throwIfAborted();
|
|
349
|
+
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
350
|
+
throw new ServiceError('ARTIFACT_MANIFEST_INVALID', 'Artifact provenance manifest is invalid');
|
|
351
|
+
}
|
|
352
|
+
if (metadata.size > artifactManifestMaxBytes) {
|
|
353
|
+
throw new ServiceError('ARTIFACT_MANIFEST_LIMIT', 'Artifact provenance manifest exceeds its fixed byte limit');
|
|
354
|
+
}
|
|
355
|
+
let text;
|
|
356
|
+
try {
|
|
357
|
+
text =
|
|
358
|
+
signal === undefined
|
|
359
|
+
? await readFile(filePath, 'utf8')
|
|
360
|
+
: await readFile(filePath, { encoding: 'utf8', signal });
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
if (error.name === 'AbortError')
|
|
364
|
+
throw error;
|
|
365
|
+
throw new ServiceError('ARTIFACT_NOT_FOUND', 'Artifact provenance manifest is unavailable');
|
|
366
|
+
}
|
|
367
|
+
signal?.throwIfAborted();
|
|
368
|
+
try {
|
|
369
|
+
const validated = artifactManifestSchema.safeParse(JSON.parse(text));
|
|
370
|
+
if (!validated.success)
|
|
371
|
+
throw new Error('invalid artifact manifest');
|
|
372
|
+
return validated.data;
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
throw new ServiceError('ARTIFACT_MANIFEST_INVALID', 'Artifact provenance manifest is invalid');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async function readVerifiedArtifactRange(filePath, expectedSha256, range, verifiedIdentity, signal) {
|
|
379
|
+
signal?.throwIfAborted();
|
|
380
|
+
if (range !== undefined &&
|
|
381
|
+
(!Number.isSafeInteger(range.offset) ||
|
|
382
|
+
range.offset < 0 ||
|
|
383
|
+
!Number.isSafeInteger(range.length) ||
|
|
384
|
+
range.length < 1)) {
|
|
385
|
+
throw new ServiceError('ARTIFACT_RANGE_INVALID', 'Artifact byte range is invalid');
|
|
386
|
+
}
|
|
387
|
+
let handle;
|
|
388
|
+
try {
|
|
389
|
+
handle = await open(filePath, 'r');
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
throw new ServiceError('ARTIFACT_NOT_FOUND', 'Artifact content is unavailable');
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
signal?.throwIfAborted();
|
|
396
|
+
const beforeIdentity = artifactFileIdentity(await handle.stat());
|
|
397
|
+
const totalSize = beforeIdentity.size;
|
|
398
|
+
const offset = range === undefined ? 0 : Math.min(totalSize, range.offset);
|
|
399
|
+
const requestedLength = range === undefined ? totalSize - offset : Math.min(range.length, artifactReadBufferBytes);
|
|
400
|
+
const selectedLength = Math.min(requestedLength, totalSize - offset);
|
|
401
|
+
const selected = Buffer.allocUnsafe(selectedLength);
|
|
402
|
+
if (sameArtifactIdentity(verifiedIdentity, beforeIdentity)) {
|
|
403
|
+
let selectedPosition = 0;
|
|
404
|
+
while (selectedPosition < selectedLength) {
|
|
405
|
+
signal?.throwIfAborted();
|
|
406
|
+
const { bytesRead } = await handle.read(selected, selectedPosition, selectedLength - selectedPosition, offset + selectedPosition);
|
|
407
|
+
if (bytesRead === 0) {
|
|
408
|
+
throw new ServiceError('ARTIFACT_READ_INCOMPLETE', 'Artifact content changed while reading');
|
|
409
|
+
}
|
|
410
|
+
selectedPosition += bytesRead;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
const scratch = Buffer.allocUnsafe(Math.min(artifactReadBufferBytes, Math.max(1, totalSize)));
|
|
415
|
+
const hash = createHash('sha256');
|
|
416
|
+
let position = 0;
|
|
417
|
+
while (position < totalSize) {
|
|
418
|
+
signal?.throwIfAborted();
|
|
419
|
+
const requested = Math.min(scratch.length, totalSize - position);
|
|
420
|
+
const { bytesRead } = await handle.read(scratch, 0, requested, position);
|
|
421
|
+
if (bytesRead === 0) {
|
|
422
|
+
throw new ServiceError('ARTIFACT_READ_INCOMPLETE', 'Artifact content changed while reading');
|
|
423
|
+
}
|
|
424
|
+
const chunk = scratch.subarray(0, bytesRead);
|
|
425
|
+
hash.update(chunk);
|
|
426
|
+
const chunkEnd = position + bytesRead;
|
|
427
|
+
const selectionEnd = offset + selectedLength;
|
|
428
|
+
const overlapStart = Math.max(position, offset);
|
|
429
|
+
const overlapEnd = Math.min(chunkEnd, selectionEnd);
|
|
430
|
+
if (overlapStart < overlapEnd) {
|
|
431
|
+
chunk.copy(selected, overlapStart - offset, overlapStart - position, overlapEnd - position);
|
|
432
|
+
}
|
|
433
|
+
position = chunkEnd;
|
|
434
|
+
}
|
|
435
|
+
if (hash.digest('hex') !== expectedSha256) {
|
|
436
|
+
throw new ServiceError('ARTIFACT_INTEGRITY_FAILED', 'Artifact hash does not match its URI');
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
signal?.throwIfAborted();
|
|
440
|
+
const afterIdentity = artifactFileIdentity(await handle.stat());
|
|
441
|
+
if (!sameArtifactIdentity(beforeIdentity, afterIdentity)) {
|
|
442
|
+
throw new ServiceError('ARTIFACT_READ_INCOMPLETE', 'Artifact content changed while reading');
|
|
443
|
+
}
|
|
444
|
+
return { bytes: selected, totalSize, identity: afterIdentity };
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
if (error instanceof ServiceError || error.name === 'AbortError')
|
|
448
|
+
throw error;
|
|
449
|
+
throw new ServiceError('ARTIFACT_READ_FAILED', 'Artifact content could not be read');
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
await handle.close();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
export class ArtifactStore {
|
|
456
|
+
maxBytes;
|
|
457
|
+
maxEntries;
|
|
458
|
+
maxSingleBytes;
|
|
459
|
+
#verifiedContent = new Map();
|
|
460
|
+
constructor(maxBytes = 536_870_912, maxEntries = 5_000, maxSingleBytes = 134_217_728) {
|
|
461
|
+
this.maxBytes = maxBytes;
|
|
462
|
+
this.maxEntries = maxEntries;
|
|
463
|
+
this.maxSingleBytes = maxSingleBytes;
|
|
464
|
+
}
|
|
465
|
+
async list(workspace, signal) {
|
|
466
|
+
signal?.throwIfAborted();
|
|
467
|
+
const usage = await artifactUsage(workspace.artifactRoot, signal);
|
|
468
|
+
if (usage.bytes > this.maxBytes || usage.entries > this.maxEntries) {
|
|
469
|
+
throw new ServiceError('ARTIFACT_STORAGE_LIMIT', 'Artifact storage exceeds the configured retention budget');
|
|
470
|
+
}
|
|
471
|
+
const results = [];
|
|
472
|
+
const walk = async (directory) => {
|
|
473
|
+
signal?.throwIfAborted();
|
|
474
|
+
let entries;
|
|
475
|
+
try {
|
|
476
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
if (error.code === 'ENOENT')
|
|
480
|
+
return;
|
|
481
|
+
throw error;
|
|
482
|
+
}
|
|
483
|
+
for (const entry of entries.sort((a, b) => compareCodeUnits(a.name, b.name))) {
|
|
484
|
+
signal?.throwIfAborted();
|
|
485
|
+
const candidate = path.join(directory, entry.name);
|
|
486
|
+
if (entry.isDirectory()) {
|
|
487
|
+
await walk(await containedGeneratedPath(workspace.artifactRoot, path.relative(workspace.artifactRoot, candidate)));
|
|
488
|
+
}
|
|
489
|
+
else if (entry.isFile() && entry.name.endsWith('.manifest.json')) {
|
|
490
|
+
const manifest = await readArtifactManifest(await containedGeneratedPath(workspace.artifactRoot, path.relative(workspace.artifactRoot, candidate)), signal);
|
|
491
|
+
if (entry.name !== `${manifest.name}.${manifest.provenanceHash}.manifest.json`) {
|
|
492
|
+
throw new ServiceError('ARTIFACT_MANIFEST_INTEGRITY_FAILED', 'Artifact provenance manifest filename does not match its contents');
|
|
493
|
+
}
|
|
494
|
+
assertManifestIntegrity(manifest, {
|
|
495
|
+
sha256: path.basename(directory),
|
|
496
|
+
provenanceHash: manifest.provenanceHash,
|
|
497
|
+
name: manifest.name,
|
|
498
|
+
}, workspace);
|
|
499
|
+
results.push({
|
|
500
|
+
uri: this.uri(workspace.id, manifest.sha256, manifest.provenanceHash, manifest.name),
|
|
501
|
+
name: manifest.name,
|
|
502
|
+
mimeType: manifest.mimeType,
|
|
503
|
+
size: manifest.size,
|
|
504
|
+
sha256: manifest.sha256,
|
|
505
|
+
...(manifest.description === undefined ? {} : { description: manifest.description }),
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
await walk(await containedGeneratedPath(workspace.artifactRoot));
|
|
511
|
+
return results.sort((a, b) => compareCodeUnits(a.uri, b.uri));
|
|
512
|
+
}
|
|
513
|
+
async listPage(workspace, options, signal) {
|
|
514
|
+
signal?.throwIfAborted();
|
|
515
|
+
if (!Number.isSafeInteger(options.limit) ||
|
|
516
|
+
options.limit < 1 ||
|
|
517
|
+
options.limit > artifactPageMaxEntries) {
|
|
518
|
+
throw new ServiceError('ARTIFACT_PAGE_INVALID', `Artifact page limit must be between 1 and ${artifactPageMaxEntries}`);
|
|
519
|
+
}
|
|
520
|
+
const usage = await artifactUsage(workspace.artifactRoot, signal);
|
|
521
|
+
if (usage.bytes > this.maxBytes || usage.entries > this.maxEntries) {
|
|
522
|
+
throw new ServiceError('ARTIFACT_STORAGE_LIMIT', 'Artifact storage exceeds the configured retention budget');
|
|
523
|
+
}
|
|
524
|
+
const artifacts = [];
|
|
525
|
+
const revision = createHash('sha256');
|
|
526
|
+
let total = 0;
|
|
527
|
+
let afterFound = options.afterUri === undefined;
|
|
528
|
+
let hasMore = false;
|
|
529
|
+
const visitManifest = async (directory, entryName) => {
|
|
530
|
+
signal?.throwIfAborted();
|
|
531
|
+
const manifest = await readArtifactManifest(await containedGeneratedPath(workspace.artifactRoot, path.relative(workspace.artifactRoot, path.join(directory, entryName))), signal);
|
|
532
|
+
if (entryName !== `${manifest.name}.${manifest.provenanceHash}.manifest.json`) {
|
|
533
|
+
throw new ServiceError('ARTIFACT_MANIFEST_INTEGRITY_FAILED', 'Artifact provenance manifest filename does not match its contents');
|
|
534
|
+
}
|
|
535
|
+
assertManifestIntegrity(manifest, {
|
|
536
|
+
sha256: path.basename(directory),
|
|
537
|
+
provenanceHash: manifest.provenanceHash,
|
|
538
|
+
name: manifest.name,
|
|
539
|
+
}, workspace);
|
|
540
|
+
const uri = this.uri(workspace.id, manifest.sha256, manifest.provenanceHash, manifest.name);
|
|
541
|
+
revision.update(`${Buffer.byteLength(uri, 'utf8')}:${uri}`);
|
|
542
|
+
total += 1;
|
|
543
|
+
if (!afterFound) {
|
|
544
|
+
if (uri === options.afterUri)
|
|
545
|
+
afterFound = true;
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (artifacts.length < options.limit) {
|
|
549
|
+
artifacts.push({
|
|
550
|
+
uri,
|
|
551
|
+
name: manifest.name,
|
|
552
|
+
mimeType: manifest.mimeType,
|
|
553
|
+
size: manifest.size,
|
|
554
|
+
sha256: manifest.sha256,
|
|
555
|
+
...(manifest.description === undefined ? {} : { description: manifest.description }),
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
hasMore = true;
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
const walk = async (directory) => {
|
|
563
|
+
signal?.throwIfAborted();
|
|
564
|
+
let entries;
|
|
565
|
+
try {
|
|
566
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
567
|
+
}
|
|
568
|
+
catch (error) {
|
|
569
|
+
if (error.code === 'ENOENT')
|
|
570
|
+
return;
|
|
571
|
+
throw error;
|
|
572
|
+
}
|
|
573
|
+
for (const entry of entries
|
|
574
|
+
.filter((candidate) => candidate.isDirectory())
|
|
575
|
+
.sort((left, right) => compareCodeUnits(left.name, right.name))) {
|
|
576
|
+
signal?.throwIfAborted();
|
|
577
|
+
await walk(await containedGeneratedPath(workspace.artifactRoot, path.relative(workspace.artifactRoot, path.join(directory, entry.name))));
|
|
578
|
+
}
|
|
579
|
+
for (const entry of entries
|
|
580
|
+
.filter((candidate) => candidate.isFile() && candidate.name.endsWith('.manifest.json'))
|
|
581
|
+
.sort((left, right) => compareCodeUnits(manifestAddressSortKey(left.name), manifestAddressSortKey(right.name)))) {
|
|
582
|
+
await visitManifest(directory, entry.name);
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
await walk(await containedGeneratedPath(workspace.artifactRoot));
|
|
586
|
+
return {
|
|
587
|
+
artifacts,
|
|
588
|
+
total,
|
|
589
|
+
revision: revision.digest('hex'),
|
|
590
|
+
afterFound,
|
|
591
|
+
hasMore,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
async describe(workspace, uri, signal) {
|
|
595
|
+
signal?.throwIfAborted();
|
|
596
|
+
const parsed = this.parseUri(workspace, uri);
|
|
597
|
+
const target = await containedGeneratedPath(workspace.artifactRoot, parsed.sha256.slice(0, 2), parsed.sha256, `${parsed.name}.${parsed.provenanceHash}.manifest.json`);
|
|
598
|
+
const manifest = await readArtifactManifest(target, signal);
|
|
599
|
+
assertManifestIntegrity(manifest, parsed, workspace);
|
|
600
|
+
return {
|
|
601
|
+
uri,
|
|
602
|
+
name: manifest.name,
|
|
603
|
+
mimeType: manifest.mimeType,
|
|
604
|
+
size: manifest.size,
|
|
605
|
+
sha256: manifest.sha256,
|
|
606
|
+
provenanceHash: manifest.provenanceHash,
|
|
607
|
+
provenance: manifest.provenance,
|
|
608
|
+
...(manifest.description === undefined ? {} : { description: manifest.description }),
|
|
609
|
+
path: await containedGeneratedPath(workspace.artifactRoot, parsed.sha256.slice(0, 2), parsed.sha256, parsed.name),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
async put(workspace, name, mimeType, content, provenance, description, signal) {
|
|
613
|
+
return this.withAtomicWrites(workspace, [
|
|
614
|
+
{
|
|
615
|
+
name,
|
|
616
|
+
mimeType,
|
|
617
|
+
content,
|
|
618
|
+
provenance,
|
|
619
|
+
...(description === undefined ? {} : { description }),
|
|
620
|
+
},
|
|
621
|
+
], ([artifact]) => {
|
|
622
|
+
if (artifact === undefined)
|
|
623
|
+
throw new Error('Artifact batch returned no result');
|
|
624
|
+
return Promise.resolve(artifact);
|
|
625
|
+
}, signal);
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Store one logical artifact without weakening the configured per-object limit.
|
|
629
|
+
* Oversized content is represented by a canonical JSON index whose ordered
|
|
630
|
+
* resource links reconstruct the exact original bytes.
|
|
631
|
+
*/
|
|
632
|
+
async putChunked(workspace, name, mimeType, content, provenance, description, signal) {
|
|
633
|
+
return this.withAtomicChunkedWrites(workspace, [
|
|
634
|
+
{
|
|
635
|
+
name,
|
|
636
|
+
mimeType,
|
|
637
|
+
content,
|
|
638
|
+
provenance,
|
|
639
|
+
...(description === undefined ? {} : { description }),
|
|
640
|
+
},
|
|
641
|
+
], ([artifact]) => {
|
|
642
|
+
if (artifact === undefined)
|
|
643
|
+
throw new Error('Artifact batch returned no result');
|
|
644
|
+
return Promise.resolve(artifact);
|
|
645
|
+
}, signal);
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Persist logical artifacts as one failure boundary. Every small write maps to
|
|
649
|
+
* one normal artifact. Every oversized write maps to an index plus bounded
|
|
650
|
+
* content chunks, while the callback receives only the logical normal/index
|
|
651
|
+
* artifacts in the caller's original order.
|
|
652
|
+
*/
|
|
653
|
+
async withAtomicChunkedWrites(workspace, writes, commit, signal) {
|
|
654
|
+
signal?.throwIfAborted();
|
|
655
|
+
const admission = await preflightArtifactBatch(writes, {
|
|
656
|
+
maxBytes: this.maxBytes,
|
|
657
|
+
maxEntries: this.maxEntries,
|
|
658
|
+
maxSingleBytes: this.maxSingleBytes,
|
|
659
|
+
chunked: true,
|
|
660
|
+
}, signal);
|
|
661
|
+
const physicalWrites = [];
|
|
662
|
+
const logicalIndexes = [];
|
|
663
|
+
for (const [writeIndex, write] of writes.entries()) {
|
|
664
|
+
signal?.throwIfAborted();
|
|
665
|
+
const expectedLength = admission.contentLengths[writeIndex];
|
|
666
|
+
const bytes = artifactContentBytes(write.content);
|
|
667
|
+
if (bytes.length !== expectedLength) {
|
|
668
|
+
throw new ServiceError('ARTIFACT_CONTENT_CHANGED', 'Artifact content length changed after batch admission', { name: write.name, expectedLength, actualLength: bytes.length });
|
|
669
|
+
}
|
|
670
|
+
signal?.throwIfAborted();
|
|
671
|
+
if (bytes.length <= this.maxSingleBytes) {
|
|
672
|
+
const sha256 = await sha256BytesWithSignal(bytes, signal);
|
|
673
|
+
logicalIndexes.push(physicalWrites.length);
|
|
674
|
+
physicalWrites.push({
|
|
675
|
+
...write,
|
|
676
|
+
content: bytes,
|
|
677
|
+
[ownedArtifactContent]: true,
|
|
678
|
+
[precomputedArtifactSha256]: sha256,
|
|
679
|
+
});
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
const originalSha256 = await sha256BytesWithSignal(bytes, signal);
|
|
683
|
+
const original = {
|
|
684
|
+
name: write.name,
|
|
685
|
+
mimeType: write.mimeType,
|
|
686
|
+
size: bytes.length,
|
|
687
|
+
sha256: originalSha256,
|
|
688
|
+
};
|
|
689
|
+
const chunkCount = Math.ceil(bytes.length / this.maxSingleBytes);
|
|
690
|
+
const chunks = [];
|
|
691
|
+
for (let index = 0; index < chunkCount; index += 1) {
|
|
692
|
+
signal?.throwIfAborted();
|
|
693
|
+
const offset = index * this.maxSingleBytes;
|
|
694
|
+
const content = bytes.subarray(offset, Math.min(bytes.length, offset + this.maxSingleBytes));
|
|
695
|
+
const name = chunkArtifactName(originalSha256, index);
|
|
696
|
+
const provenance = chunkedProvenance(write.provenance, 'chunk', original, {
|
|
697
|
+
chunkIndex: index,
|
|
698
|
+
chunkCount,
|
|
699
|
+
offset,
|
|
700
|
+
length: content.length,
|
|
701
|
+
});
|
|
702
|
+
const chunkSha256 = await sha256BytesWithSignal(content, signal);
|
|
703
|
+
const chunkWrite = {
|
|
704
|
+
name,
|
|
705
|
+
mimeType: 'application/octet-stream',
|
|
706
|
+
content,
|
|
707
|
+
provenance,
|
|
708
|
+
description: `Byte-exact chunk ${index + 1} of ${chunkCount} for ${write.name}`,
|
|
709
|
+
[ownedArtifactContent]: true,
|
|
710
|
+
[precomputedArtifactSha256]: chunkSha256,
|
|
711
|
+
};
|
|
712
|
+
const planned = this.plannedArtifactLink(workspace, chunkWrite, content, chunkSha256);
|
|
713
|
+
chunks.push({
|
|
714
|
+
index,
|
|
715
|
+
offset,
|
|
716
|
+
length: content.length,
|
|
717
|
+
uri: planned.uri,
|
|
718
|
+
name: planned.name,
|
|
719
|
+
mimeType: 'application/octet-stream',
|
|
720
|
+
size: content.length,
|
|
721
|
+
sha256: planned.sha256,
|
|
722
|
+
});
|
|
723
|
+
physicalWrites.push(chunkWrite);
|
|
724
|
+
}
|
|
725
|
+
const indexDocument = {
|
|
726
|
+
schemaVersion: 1,
|
|
727
|
+
type: 'hoi4-agent.chunked-artifact',
|
|
728
|
+
original: {
|
|
729
|
+
...original,
|
|
730
|
+
...(write.description === undefined ? {} : { description: write.description }),
|
|
731
|
+
},
|
|
732
|
+
chunks,
|
|
733
|
+
};
|
|
734
|
+
const indexContent = Buffer.from(`${canonicalJson(indexDocument)}\n`, 'utf8');
|
|
735
|
+
if (indexContent.length > this.maxSingleBytes ||
|
|
736
|
+
indexContent.length > artifactManifestMaxBytes) {
|
|
737
|
+
throw new ServiceError('ARTIFACT_CHUNK_INDEX_LIMIT', 'Chunked artifact index exceeds its fixed single-resource byte limit', {
|
|
738
|
+
name: write.name,
|
|
739
|
+
size: indexContent.length,
|
|
740
|
+
maximumBytes: Math.min(this.maxSingleBytes, artifactManifestMaxBytes),
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
const indexSha256 = await sha256BytesWithSignal(indexContent, signal);
|
|
744
|
+
const indexWrite = {
|
|
745
|
+
name: chunkIndexArtifactName(write.name, originalSha256),
|
|
746
|
+
mimeType: chunkedArtifactIndexMimeType,
|
|
747
|
+
content: indexContent,
|
|
748
|
+
provenance: chunkedProvenance(write.provenance, 'index', original, {
|
|
749
|
+
chunkCount,
|
|
750
|
+
indexSchemaVersion: 1,
|
|
751
|
+
}),
|
|
752
|
+
description: write.description === undefined
|
|
753
|
+
? `Byte-exact chunk index for ${write.name}`
|
|
754
|
+
: `${write.description} (byte-exact chunk index)`,
|
|
755
|
+
[ownedArtifactContent]: true,
|
|
756
|
+
[precomputedArtifactSha256]: indexSha256,
|
|
757
|
+
};
|
|
758
|
+
logicalIndexes.push(physicalWrites.length);
|
|
759
|
+
physicalWrites.push(indexWrite);
|
|
760
|
+
}
|
|
761
|
+
if (physicalWrites.length !== admission.projectedEntries) {
|
|
762
|
+
throw new ServiceError('ARTIFACT_STORAGE_LIMIT', 'Artifact batch entry projection did not match prepared output');
|
|
763
|
+
}
|
|
764
|
+
return this.withAtomicWrites(workspace, physicalWrites, (stored) => commit(logicalIndexes.map((index) => stored[index])), signal);
|
|
765
|
+
}
|
|
766
|
+
plannedArtifactLink(workspace, write, bytes, sha256) {
|
|
767
|
+
const manifestWithoutHash = {
|
|
768
|
+
version: 2,
|
|
769
|
+
workspaceIdentity: workspace.workspaceIdentity,
|
|
770
|
+
ownerIdentity: workspace.ownerIdentity,
|
|
771
|
+
name: write.name,
|
|
772
|
+
mimeType: write.mimeType,
|
|
773
|
+
size: bytes.length,
|
|
774
|
+
sha256,
|
|
775
|
+
provenance: write.provenance,
|
|
776
|
+
...(write.description === undefined ? {} : { description: write.description }),
|
|
777
|
+
};
|
|
778
|
+
const provenanceHash = provenanceHashFor(manifestWithoutHash);
|
|
779
|
+
return {
|
|
780
|
+
uri: this.uri(workspace.id, sha256, provenanceHash, write.name),
|
|
781
|
+
name: write.name,
|
|
782
|
+
mimeType: write.mimeType,
|
|
783
|
+
size: bytes.length,
|
|
784
|
+
sha256,
|
|
785
|
+
...(write.description === undefined ? {} : { description: write.description }),
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Persist a set of immutable artifacts as one failure boundary. Files created by
|
|
790
|
+
* this batch are removed if preparation, storage admission, a write, or the
|
|
791
|
+
* caller's commit callback fails. Pre-existing content or provenance manifests
|
|
792
|
+
* are never removed, so rollback remains safe when artifacts are shared.
|
|
793
|
+
*/
|
|
794
|
+
async withAtomicWrites(workspace, writes, commit, signal) {
|
|
795
|
+
signal?.throwIfAborted();
|
|
796
|
+
const admission = await preflightArtifactBatch(writes, {
|
|
797
|
+
maxBytes: this.maxBytes,
|
|
798
|
+
maxEntries: this.maxEntries,
|
|
799
|
+
maxSingleBytes: this.maxSingleBytes,
|
|
800
|
+
chunked: false,
|
|
801
|
+
}, signal);
|
|
802
|
+
const prepared = [];
|
|
803
|
+
for (const [writeIndex, write] of writes.entries()) {
|
|
804
|
+
const { name, mimeType, content, provenance, description } = write;
|
|
805
|
+
const internal = write;
|
|
806
|
+
signal?.throwIfAborted();
|
|
807
|
+
const bytes = internal[ownedArtifactContent] === true && Buffer.isBuffer(content)
|
|
808
|
+
? content
|
|
809
|
+
: artifactContentBytes(content);
|
|
810
|
+
const expectedLength = admission.contentLengths[writeIndex];
|
|
811
|
+
if (bytes.length !== expectedLength) {
|
|
812
|
+
throw new ServiceError('ARTIFACT_CONTENT_CHANGED', 'Artifact content length changed after batch admission', { name, expectedLength, actualLength: bytes.length });
|
|
813
|
+
}
|
|
814
|
+
signal?.throwIfAborted();
|
|
815
|
+
const sha256 = internal[precomputedArtifactSha256] ?? (await sha256BytesWithSignal(bytes, signal));
|
|
816
|
+
if (!/^[a-f0-9]{64}$/u.test(sha256)) {
|
|
817
|
+
throw new ServiceError('ARTIFACT_HASH_INVALID', 'Precomputed artifact hash is invalid');
|
|
818
|
+
}
|
|
819
|
+
signal?.throwIfAborted();
|
|
820
|
+
const manifestWithoutHash = {
|
|
821
|
+
version: 2,
|
|
822
|
+
workspaceIdentity: workspace.workspaceIdentity,
|
|
823
|
+
ownerIdentity: workspace.ownerIdentity,
|
|
824
|
+
name,
|
|
825
|
+
mimeType,
|
|
826
|
+
size: bytes.length,
|
|
827
|
+
sha256,
|
|
828
|
+
provenance,
|
|
829
|
+
...(description === undefined ? {} : { description }),
|
|
830
|
+
};
|
|
831
|
+
const provenanceHash = provenanceHashFor(manifestWithoutHash);
|
|
832
|
+
const manifest = { ...manifestWithoutHash, provenanceHash };
|
|
833
|
+
const manifestBytes = `${canonicalJson(manifest)}\n`;
|
|
834
|
+
if (Buffer.byteLength(manifestBytes, 'utf8') > this.maxSingleBytes ||
|
|
835
|
+
Buffer.byteLength(manifestBytes, 'utf8') > artifactManifestMaxBytes) {
|
|
836
|
+
throw new ServiceError('ARTIFACT_MANIFEST_LIMIT', 'Artifact provenance exceeds its fixed manifest byte limit');
|
|
837
|
+
}
|
|
838
|
+
const directory = await containedGeneratedPath(workspace.artifactRoot, sha256.slice(0, 2), sha256);
|
|
839
|
+
const target = await containedGeneratedPath(workspace.artifactRoot, sha256.slice(0, 2), sha256, name);
|
|
840
|
+
const manifestPath = await containedGeneratedPath(workspace.artifactRoot, sha256.slice(0, 2), sha256, `${name}.${provenanceHash}.manifest.json`);
|
|
841
|
+
signal?.throwIfAborted();
|
|
842
|
+
const artifact = {
|
|
843
|
+
uri: this.uri(workspace.id, sha256, provenanceHash, name),
|
|
844
|
+
name,
|
|
845
|
+
mimeType,
|
|
846
|
+
size: bytes.length,
|
|
847
|
+
sha256,
|
|
848
|
+
provenanceHash,
|
|
849
|
+
...(description === undefined ? {} : { description }),
|
|
850
|
+
path: target,
|
|
851
|
+
provenance,
|
|
852
|
+
};
|
|
853
|
+
prepared.push({ artifact, bytes, directory, target, manifestPath, manifestBytes });
|
|
854
|
+
}
|
|
855
|
+
return withArtifactQueue(workspace.artifactRoot, async () => {
|
|
856
|
+
signal?.throwIfAborted();
|
|
857
|
+
const uniqueTargets = new Map();
|
|
858
|
+
const uniqueManifests = new Map();
|
|
859
|
+
for (const artifact of prepared) {
|
|
860
|
+
signal?.throwIfAborted();
|
|
861
|
+
const duplicateTarget = uniqueTargets.get(artifact.target);
|
|
862
|
+
if (duplicateTarget !== undefined && !duplicateTarget.bytes.equals(artifact.bytes)) {
|
|
863
|
+
throw new ServiceError('ARTIFACT_HASH_COLLISION', 'Artifact batch maps different bytes to the same content path');
|
|
864
|
+
}
|
|
865
|
+
if (duplicateTarget === undefined) {
|
|
866
|
+
const exists = await fileExists(artifact.target);
|
|
867
|
+
signal?.throwIfAborted();
|
|
868
|
+
if (exists) {
|
|
869
|
+
const verificationKey = `${workspace.workspaceIdentity}\0${workspace.ownerIdentity}\0${artifact.target}\0${artifact.artifact.sha256}`;
|
|
870
|
+
try {
|
|
871
|
+
const verified = await readVerifiedArtifactRange(artifact.target, artifact.artifact.sha256, { offset: 0, length: 1 }, this.#verifiedContent.get(verificationKey), signal);
|
|
872
|
+
this.#verifiedContent.set(verificationKey, verified.identity);
|
|
873
|
+
}
|
|
874
|
+
catch (error) {
|
|
875
|
+
if (error.name === 'AbortError')
|
|
876
|
+
throw error;
|
|
877
|
+
throw new ServiceError('ARTIFACT_HASH_COLLISION', 'Artifact path contains different bytes');
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
uniqueTargets.set(artifact.target, { ...artifact, exists });
|
|
881
|
+
}
|
|
882
|
+
const duplicateManifest = uniqueManifests.get(artifact.manifestPath);
|
|
883
|
+
if (duplicateManifest !== undefined &&
|
|
884
|
+
duplicateManifest.manifestBytes !== artifact.manifestBytes) {
|
|
885
|
+
throw new ServiceError('ARTIFACT_MANIFEST_INTEGRITY_FAILED', 'Artifact batch maps different provenance to the same immutable manifest');
|
|
886
|
+
}
|
|
887
|
+
if (duplicateManifest === undefined) {
|
|
888
|
+
const exists = await fileExists(artifact.manifestPath);
|
|
889
|
+
signal?.throwIfAborted();
|
|
890
|
+
if (exists &&
|
|
891
|
+
`${canonicalJson(await readArtifactManifest(artifact.manifestPath, signal))}\n` !==
|
|
892
|
+
artifact.manifestBytes) {
|
|
893
|
+
throw new ServiceError('ARTIFACT_MANIFEST_INTEGRITY_FAILED', 'Immutable artifact provenance manifest contains different bytes');
|
|
894
|
+
}
|
|
895
|
+
uniqueManifests.set(artifact.manifestPath, { ...artifact, exists });
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
const additionalBytes = [...uniqueTargets.values()].reduce((total, artifact) => total + (artifact.exists ? 0 : artifact.bytes.length), 0) +
|
|
899
|
+
[...uniqueManifests.values()].reduce((total, artifact) => total + (artifact.exists ? 0 : Buffer.byteLength(artifact.manifestBytes, 'utf8')), 0);
|
|
900
|
+
if (!Number.isSafeInteger(additionalBytes)) {
|
|
901
|
+
throw new ServiceError('ARTIFACT_STORAGE_LIMIT', 'Artifact batch size is unsafe');
|
|
902
|
+
}
|
|
903
|
+
const usage = await artifactUsage(workspace.artifactRoot, signal);
|
|
904
|
+
const additionalEntries = [...uniqueManifests.values()].filter((artifact) => !artifact.exists).length;
|
|
905
|
+
if (usage.entries + additionalEntries > this.maxEntries ||
|
|
906
|
+
usage.bytes > this.maxBytes - additionalBytes) {
|
|
907
|
+
throw new ServiceError('ARTIFACT_STORAGE_LIMIT', 'Artifact storage retention limit has been reached');
|
|
908
|
+
}
|
|
909
|
+
const createdTargets = [];
|
|
910
|
+
const createdManifests = [];
|
|
911
|
+
try {
|
|
912
|
+
for (const artifact of uniqueTargets.values()) {
|
|
913
|
+
signal?.throwIfAborted();
|
|
914
|
+
if (artifact.exists)
|
|
915
|
+
continue;
|
|
916
|
+
await mkdir(artifact.directory, { recursive: true });
|
|
917
|
+
signal?.throwIfAborted();
|
|
918
|
+
try {
|
|
919
|
+
await writeFile(artifact.target, artifact.bytes, {
|
|
920
|
+
flag: 'wx',
|
|
921
|
+
...(signal === undefined ? {} : { signal }),
|
|
922
|
+
});
|
|
923
|
+
createdTargets.push(artifact.target);
|
|
924
|
+
}
|
|
925
|
+
catch (error) {
|
|
926
|
+
if (error.code !== 'EEXIST') {
|
|
927
|
+
createdTargets.push(artifact.target);
|
|
928
|
+
}
|
|
929
|
+
throw error;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
for (const artifact of uniqueManifests.values()) {
|
|
933
|
+
signal?.throwIfAborted();
|
|
934
|
+
if (artifact.exists)
|
|
935
|
+
continue;
|
|
936
|
+
await mkdir(artifact.directory, { recursive: true });
|
|
937
|
+
signal?.throwIfAborted();
|
|
938
|
+
try {
|
|
939
|
+
await writeFile(artifact.manifestPath, artifact.manifestBytes, {
|
|
940
|
+
flag: 'wx',
|
|
941
|
+
...(signal === undefined ? {} : { signal }),
|
|
942
|
+
});
|
|
943
|
+
createdManifests.push(artifact.manifestPath);
|
|
944
|
+
}
|
|
945
|
+
catch (error) {
|
|
946
|
+
if (error.code !== 'EEXIST') {
|
|
947
|
+
createdManifests.push(artifact.manifestPath);
|
|
948
|
+
}
|
|
949
|
+
throw error;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
signal?.throwIfAborted();
|
|
953
|
+
// The caller's commit callback is the ownership boundary. Once entered,
|
|
954
|
+
// it completes or fails under its own critical-phase rules; a late abort
|
|
955
|
+
// cannot make the store remove evidence already committed elsewhere.
|
|
956
|
+
return await commit(prepared.map(({ artifact }) => artifact));
|
|
957
|
+
}
|
|
958
|
+
catch (error) {
|
|
959
|
+
let cleanupFailed = false;
|
|
960
|
+
for (const manifestPath of createdManifests.reverse()) {
|
|
961
|
+
try {
|
|
962
|
+
await unlink(manifestPath);
|
|
963
|
+
}
|
|
964
|
+
catch (cleanupError) {
|
|
965
|
+
if (cleanupError.code !== 'ENOENT')
|
|
966
|
+
cleanupFailed = true;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
for (const target of createdTargets.reverse()) {
|
|
970
|
+
try {
|
|
971
|
+
const prefix = `${path.basename(target)}.`;
|
|
972
|
+
const hasManifest = (await readdir(path.dirname(target), { withFileTypes: true })).some((entry) => entry.isFile() &&
|
|
973
|
+
entry.name.startsWith(prefix) &&
|
|
974
|
+
entry.name.endsWith('.manifest.json'));
|
|
975
|
+
if (!hasManifest) {
|
|
976
|
+
await unlink(target);
|
|
977
|
+
const verificationPrefix = `${workspace.workspaceIdentity}\0${workspace.ownerIdentity}\0${target}\0`;
|
|
978
|
+
for (const key of this.#verifiedContent.keys()) {
|
|
979
|
+
if (key.startsWith(verificationPrefix))
|
|
980
|
+
this.#verifiedContent.delete(key);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
catch (cleanupError) {
|
|
985
|
+
if (cleanupError.code !== 'ENOENT')
|
|
986
|
+
cleanupFailed = true;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
if (cleanupFailed) {
|
|
990
|
+
throw new ServiceError('ARTIFACT_BATCH_ROLLBACK_FAILED', 'Artifact batch failed and its newly created files could not all be removed');
|
|
991
|
+
}
|
|
992
|
+
throw error;
|
|
993
|
+
}
|
|
994
|
+
}, signal);
|
|
995
|
+
}
|
|
996
|
+
async read(workspace, uri, range, signal) {
|
|
997
|
+
signal?.throwIfAborted();
|
|
998
|
+
const { sha256, provenanceHash, name } = this.parseUri(workspace, uri);
|
|
999
|
+
const target = await containedGeneratedPath(workspace.artifactRoot, sha256.slice(0, 2), sha256, name);
|
|
1000
|
+
const manifestPath = await containedGeneratedPath(workspace.artifactRoot, sha256.slice(0, 2), sha256, `${name}.${provenanceHash}.manifest.json`);
|
|
1001
|
+
const manifest = await readArtifactManifest(manifestPath, signal);
|
|
1002
|
+
assertManifestIntegrity(manifest, { sha256, provenanceHash, name }, workspace);
|
|
1003
|
+
const verificationKey = `${workspace.workspaceIdentity}\0${workspace.ownerIdentity}\0${target}\0${sha256}`;
|
|
1004
|
+
const { bytes, totalSize, identity } = await readVerifiedArtifactRange(target, sha256, range, this.#verifiedContent.get(verificationKey), signal);
|
|
1005
|
+
this.#verifiedContent.set(verificationKey, identity);
|
|
1006
|
+
return { bytes, mimeType: manifest.mimeType, name, totalSize };
|
|
1007
|
+
}
|
|
1008
|
+
uri(workspaceId, sha256, provenanceHash, name) {
|
|
1009
|
+
return `hoi4-agent://workspace/${encodeURIComponent(workspaceId)}/artifact/${sha256}/${provenanceHash}/${encodeURIComponent(name)}`;
|
|
1010
|
+
}
|
|
1011
|
+
parseUri(workspace, uri) {
|
|
1012
|
+
let parsed;
|
|
1013
|
+
try {
|
|
1014
|
+
parsed = new URL(uri);
|
|
1015
|
+
}
|
|
1016
|
+
catch {
|
|
1017
|
+
throw new ServiceError('ARTIFACT_URI_INVALID', 'Invalid artifact URI');
|
|
1018
|
+
}
|
|
1019
|
+
if (parsed.protocol !== 'hoi4-agent:' ||
|
|
1020
|
+
parsed.hostname !== 'workspace' ||
|
|
1021
|
+
parsed.username !== '' ||
|
|
1022
|
+
parsed.password !== '' ||
|
|
1023
|
+
parsed.port !== '' ||
|
|
1024
|
+
parsed.search !== '' ||
|
|
1025
|
+
parsed.hash !== '')
|
|
1026
|
+
throw new ServiceError('ARTIFACT_URI_INVALID', 'Invalid artifact URI');
|
|
1027
|
+
let segments;
|
|
1028
|
+
try {
|
|
1029
|
+
segments = parsed.pathname.split('/').filter(Boolean).map(decodeURIComponent);
|
|
1030
|
+
}
|
|
1031
|
+
catch {
|
|
1032
|
+
throw new ServiceError('ARTIFACT_URI_INVALID', 'Invalid artifact URI encoding');
|
|
1033
|
+
}
|
|
1034
|
+
if (segments.length !== 5 || segments[0] !== workspace.id || segments[1] !== 'artifact')
|
|
1035
|
+
throw new ServiceError('ARTIFACT_WORKSPACE_MISMATCH', 'Artifact URI does not belong to this workspace');
|
|
1036
|
+
const [, , sha256, provenanceHash, name] = segments;
|
|
1037
|
+
if (!/^[a-f0-9]{64}$/u.test(sha256) ||
|
|
1038
|
+
!/^[a-f0-9]{64}$/u.test(provenanceHash) ||
|
|
1039
|
+
!isValidArtifactName(name))
|
|
1040
|
+
throw new ServiceError('ARTIFACT_URI_INVALID', 'Invalid artifact URI components');
|
|
1041
|
+
if (this.uri(workspace.id, sha256, provenanceHash, name) !== parsed.href) {
|
|
1042
|
+
throw new ServiceError('ARTIFACT_URI_INVALID', 'Artifact URI is not canonical');
|
|
1043
|
+
}
|
|
1044
|
+
return { sha256: sha256, provenanceHash: provenanceHash, name: name };
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
//# sourceMappingURL=artifacts.js.map
|