@warlock.js/core 5.19.1 → 5.21.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.
- package/CHANGELOG.md +1136 -1073
- package/esm/benchmark/benchmark.mjs +34 -25
- package/esm/benchmark/benchmark.mjs.map +1 -1
- package/esm/benchmark/profiler.d.mts.map +1 -1
- package/esm/benchmark/profiler.mjs +8 -3
- package/esm/benchmark/profiler.mjs.map +1 -1
- package/esm/bootstrap.d.mts.map +1 -1
- package/esm/bootstrap.mjs +3 -0
- package/esm/bootstrap.mjs.map +1 -1
- package/esm/cache/database-cache-driver.d.mts +2 -2
- package/esm/cache/database-cache-driver.d.mts.map +1 -1
- package/esm/cache/database-cache-driver.mjs +3 -2
- package/esm/cache/database-cache-driver.mjs.map +1 -1
- package/esm/cli/cli-commands.manager.mjs +16 -14
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/commands/add.command.mjs +1 -1
- package/esm/cli/commands/add.command.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs +3 -2
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -1
- package/esm/cli/commands/generate/generate.command.mjs +20 -0
- package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/column-dsl-parser.mjs +4 -2
- package/esm/cli/commands/generate/generators/column-dsl-parser.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/controller.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/controller.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/migration.generator.mjs +22 -6
- package/esm/cli/commands/generate/generators/migration.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/model.generator.mjs +22 -7
- package/esm/cli/commands/generate/generators/model.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/module.generator.mjs +8 -6
- package/esm/cli/commands/generate/generators/module.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/repository.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/repository.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/resource.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/resource.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/service.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/service.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/templates/stubs.mjs +7 -7
- package/esm/cli/commands/generate/templates/stubs.mjs.map +1 -1
- package/esm/cli/commands/generate/utils/writer.mjs +25 -1
- package/esm/cli/commands/generate/utils/writer.mjs.map +1 -1
- package/esm/cli/commands/routes/routes-command.action.mjs +5 -1
- package/esm/cli/commands/routes/routes-command.action.mjs.map +1 -1
- package/esm/cli/commands/routes/with-console-on-stderr.mjs +24 -0
- package/esm/cli/commands/routes/with-console-on-stderr.mjs.map +1 -0
- package/esm/cli/commands/routes.command.mjs +3 -4
- package/esm/cli/commands/routes.command.mjs.map +1 -1
- package/esm/cli/commands/start-production.command.mjs +5 -5
- package/esm/cli/commands/start-production.command.mjs.map +1 -1
- package/esm/cli/commands/storage-put.action.mjs +2 -1
- package/esm/cli/commands/storage-put.action.mjs.map +1 -1
- package/esm/cli/commands/update.command.mjs +1 -0
- package/esm/cli/commands/update.command.mjs.map +1 -1
- package/esm/cli/parse-cli-args.mjs +13 -1
- package/esm/cli/parse-cli-args.mjs.map +1 -1
- package/esm/commands/cli-command.d.mts.map +1 -1
- package/esm/commands/cli-command.mjs +4 -2
- package/esm/commands/cli-command.mjs.map +1 -1
- package/esm/commands/index.mjs +3 -0
- package/esm/config/config-key-from-path.mjs +20 -0
- package/esm/config/config-key-from-path.mjs.map +1 -0
- package/esm/config/config-loader.d.mts.map +1 -1
- package/esm/config/config-loader.mjs +2 -3
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/connectors/cache-connector.d.mts.map +1 -1
- package/esm/connectors/cache-connector.mjs +5 -0
- package/esm/connectors/cache-connector.mjs.map +1 -1
- package/esm/connectors/connectors-manager.d.mts +4 -0
- package/esm/connectors/connectors-manager.d.mts.map +1 -1
- package/esm/connectors/connectors-manager.mjs +30 -2
- package/esm/connectors/connectors-manager.mjs.map +1 -1
- package/esm/connectors/single-server-warnings.mjs +33 -0
- package/esm/connectors/single-server-warnings.mjs.map +1 -0
- package/esm/connectors/socket-connector.d.mts +29 -0
- package/esm/connectors/socket-connector.d.mts.map +1 -1
- package/esm/connectors/socket-connector.mjs +77 -9
- package/esm/connectors/socket-connector.mjs.map +1 -1
- package/esm/connectors/storage.connector.d.mts.map +1 -1
- package/esm/connectors/storage.connector.mjs +6 -0
- package/esm/connectors/storage.connector.mjs.map +1 -1
- package/esm/connectors/types.d.mts +4 -2
- package/esm/connectors/types.d.mts.map +1 -1
- package/esm/connectors/types.mjs.map +1 -1
- package/esm/database/models/database-log/database-log.d.mts +47 -24
- package/esm/database/models/database-log/database-log.d.mts.map +1 -1
- package/esm/database/models/database-log/database-log.mjs +36 -4
- package/esm/database/models/database-log/database-log.mjs.map +1 -1
- package/esm/database/seed-command-action.mjs +10 -2
- package/esm/database/seed-command-action.mjs.map +1 -1
- package/esm/dev-server/development-server.mjs +15 -4
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/file-event-handler.mjs +26 -6
- package/esm/dev-server/file-event-handler.mjs.map +1 -1
- package/esm/dev-server/file-manager.d.mts.map +1 -1
- package/esm/dev-server/file-manager.mjs +2 -2
- package/esm/dev-server/file-manager.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +3 -1
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/files-watcher.mjs +64 -5
- package/esm/dev-server/files-watcher.mjs.map +1 -1
- package/esm/dev-server/health-checker/files-healthcare.manager.mjs +31 -9
- package/esm/dev-server/health-checker/files-healthcare.manager.mjs.map +1 -1
- package/esm/dev-server/health-checker/workers/eslint-health.worker.mjs +11 -7
- package/esm/dev-server/health-checker/workers/eslint-health.worker.mjs.map +1 -1
- package/esm/dev-server/loader/load-hook.mjs +1 -1
- package/esm/dev-server/loader/register-loader.mjs +1 -1
- package/esm/dev-server/manifest-manager.d.mts.map +1 -1
- package/esm/dev-server/manifest-manager.mjs +12 -6
- package/esm/dev-server/manifest-manager.mjs.map +1 -1
- package/esm/dev-server/parse-imports.mjs +9 -0
- package/esm/dev-server/parse-imports.mjs.map +1 -1
- package/esm/dev-server/register-dev-shortcuts.mjs +2 -1
- package/esm/dev-server/register-dev-shortcuts.mjs.map +1 -1
- package/esm/dev-server/route-types-publisher.mjs +60 -0
- package/esm/dev-server/route-types-publisher.mjs.map +1 -0
- package/esm/dev-server/shortcuts.mjs +2 -2
- package/esm/dev-server/shortcuts.mjs.map +1 -1
- package/esm/dev-server/special-file-patterns.mjs +18 -0
- package/esm/dev-server/special-file-patterns.mjs.map +1 -0
- package/esm/dev-server/special-files-collector.d.mts.map +1 -1
- package/esm/dev-server/special-files-collector.mjs +2 -5
- package/esm/dev-server/special-files-collector.mjs.map +1 -1
- package/esm/dev-server/start-development-server.mjs +1 -2
- package/esm/dev-server/start-development-server.mjs.map +1 -1
- package/esm/dev-server/supervisor.mjs +23 -3
- package/esm/dev-server/supervisor.mjs.map +1 -1
- package/esm/dev-server/tsconfig-manager.mjs +22 -12
- package/esm/dev-server/tsconfig-manager.mjs.map +1 -1
- package/esm/dev-server/type-generator.mjs +10 -9
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/encryption/encrypt.d.mts.map +1 -1
- package/esm/encryption/encrypt.mjs +16 -5
- package/esm/encryption/encrypt.mjs.map +1 -1
- package/esm/encryption/hash.d.mts.map +1 -1
- package/esm/encryption/hash.mjs +2 -0
- package/esm/encryption/hash.mjs.map +1 -1
- package/esm/encryption/password.d.mts.map +1 -1
- package/esm/encryption/password.mjs +3 -1
- package/esm/encryption/password.mjs.map +1 -1
- package/esm/generations/features/index.mjs +1 -1
- package/esm/generations/features/mysql.feature.mjs +9 -2
- package/esm/generations/features/mysql.feature.mjs.map +1 -1
- package/esm/generations/features/test.feature.mjs +2 -1
- package/esm/generations/features/test.feature.mjs.map +1 -1
- package/esm/generations/features/web.feature.mjs +4 -2
- package/esm/generations/features/web.feature.mjs.map +1 -1
- package/esm/http/build-rate-limit-options.d.mts +37 -0
- package/esm/http/build-rate-limit-options.d.mts.map +1 -0
- package/esm/http/build-rate-limit-options.mjs +37 -0
- package/esm/http/build-rate-limit-options.mjs.map +1 -0
- package/esm/http/csrf-origin-policy.d.mts.map +1 -1
- package/esm/http/csrf-origin-policy.mjs +5 -2
- package/esm/http/csrf-origin-policy.mjs.map +1 -1
- package/esm/http/flush-pending-cookies.mjs +52 -0
- package/esm/http/flush-pending-cookies.mjs.map +1 -0
- package/esm/http/middleware/cache-response-middleware.d.mts.map +1 -1
- package/esm/http/middleware/cache-response-middleware.mjs +4 -2
- package/esm/http/middleware/cache-response-middleware.mjs.map +1 -1
- package/esm/http/middleware/idempotency.middleware.d.mts +5 -0
- package/esm/http/middleware/idempotency.middleware.d.mts.map +1 -1
- package/esm/http/middleware/idempotency.middleware.mjs +35 -3
- package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +14 -3
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/rate-limit.middleware.d.mts +4 -3
- package/esm/http/middleware/rate-limit.middleware.d.mts.map +1 -1
- package/esm/http/middleware/rate-limit.middleware.mjs +4 -3
- package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
- package/esm/http/middleware/utils/cidr-match.d.mts +1 -1
- package/esm/http/middleware/utils/cidr-match.d.mts.map +1 -1
- package/esm/http/middleware/utils/cidr-match.mjs +11 -2
- package/esm/http/middleware/utils/cidr-match.mjs.map +1 -1
- package/esm/http/middleware/utils/idempotency-key.mjs +1 -1
- package/esm/http/middleware/utils/idempotency-key.mjs.map +1 -1
- package/esm/http/plugins.d.mts.map +1 -1
- package/esm/http/plugins.mjs +13 -5
- package/esm/http/plugins.mjs.map +1 -1
- package/esm/http/request.d.mts +12 -1
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +35 -7
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts +16 -0
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +75 -37
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/types.d.mts +26 -14
- package/esm/http/types.d.mts.map +1 -1
- package/esm/http/uploads/uploaded-file.controller.mjs +1 -1
- package/esm/http/uploads-config.mjs +2 -1
- package/esm/http/uploads-config.mjs.map +1 -1
- package/esm/http/uploads-types.d.mts +3 -3
- package/esm/image/image.d.mts +1 -1
- package/esm/image/image.d.mts.map +1 -1
- package/esm/image/image.mjs +25 -15
- package/esm/image/image.mjs.map +1 -1
- package/esm/index.d.mts +6 -4
- package/esm/index.mjs +7 -4
- package/esm/logger/logger.mjs +6 -1
- package/esm/logger/logger.mjs.map +1 -1
- package/esm/mail/config.d.mts.map +1 -1
- package/esm/mail/config.mjs +31 -21
- package/esm/mail/config.mjs.map +1 -1
- package/esm/mail/react-mail.mjs +6 -3
- package/esm/mail/react-mail.mjs.map +1 -1
- package/esm/mail/send-mail.d.mts.map +1 -1
- package/esm/mail/send-mail.mjs +40 -19
- package/esm/mail/send-mail.mjs.map +1 -1
- package/esm/production/assert-generated-imports.mjs +1 -0
- package/esm/production/assert-generated-imports.mjs.map +1 -1
- package/esm/production/build-contributions.mjs +3 -0
- package/esm/production/build-contributions.mjs.map +1 -1
- package/esm/production/esbuild-plugins.mjs +3 -2
- package/esm/production/esbuild-plugins.mjs.map +1 -1
- package/esm/production/production-builder.mjs +49 -37
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/production-supervisor.mjs +12 -5
- package/esm/production/production-supervisor.mjs.map +1 -1
- package/esm/production/route-registration-child.d.mts +1 -0
- package/esm/production/route-registration-child.mjs +65 -0
- package/esm/production/route-registration-child.mjs.map +1 -0
- package/esm/production/route-registration-snapshot.mjs +166 -0
- package/esm/production/route-registration-snapshot.mjs.map +1 -0
- package/esm/repositories/adapters/cascade/cascade-adapter.d.mts +3 -1
- package/esm/repositories/adapters/cascade/cascade-adapter.d.mts.map +1 -1
- package/esm/repositories/adapters/cascade/cascade-adapter.mjs +22 -12
- package/esm/repositories/adapters/cascade/cascade-adapter.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/cascade-query-builder.d.mts +3 -3
- package/esm/repositories/adapters/cascade/cascade-query-builder.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/filter-applicator.mjs +88 -61
- package/esm/repositories/adapters/cascade/filter-applicator.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/index.d.mts +1 -1
- package/esm/repositories/adapters/cascade/index.mjs +1 -1
- package/esm/repositories/contracts/query-builder.contract.d.mts +3 -3
- package/esm/repositories/contracts/types.d.mts +5 -0
- package/esm/repositories/contracts/types.d.mts.map +1 -1
- package/esm/repositories/index.d.mts +1 -1
- package/esm/repositories/index.mjs +1 -1
- package/esm/repositories/repository.manager.d.mts +65 -3
- package/esm/repositories/repository.manager.d.mts.map +1 -1
- package/esm/repositories/repository.manager.mjs +146 -57
- package/esm/repositories/repository.manager.mjs.map +1 -1
- package/esm/resource/resource-field-builder.d.mts.map +1 -1
- package/esm/resource/resource-field-builder.mjs +1 -0
- package/esm/resource/resource-field-builder.mjs.map +1 -1
- package/esm/resource/resource.d.mts +10 -3
- package/esm/resource/resource.d.mts.map +1 -1
- package/esm/resource/resource.mjs +19 -5
- package/esm/resource/resource.mjs.map +1 -1
- package/esm/restful/restful.d.mts +12 -5
- package/esm/restful/restful.d.mts.map +1 -1
- package/esm/restful/restful.mjs +47 -16
- package/esm/restful/restful.mjs.map +1 -1
- package/esm/router/build-route-rate-limit.mjs +22 -0
- package/esm/router/build-route-rate-limit.mjs.map +1 -0
- package/esm/router/dev-dispatch.mjs +60 -0
- package/esm/router/dev-dispatch.mjs.map +1 -0
- package/esm/router/route-builder.mjs +4 -1
- package/esm/router/route-builder.mjs.map +1 -1
- package/esm/router/route-registry.d.mts.map +1 -1
- package/esm/router/route-registry.mjs +2 -2
- package/esm/router/route-registry.mjs.map +1 -1
- package/esm/router/router.d.mts +1 -1
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +57 -39
- package/esm/router/router.mjs.map +1 -1
- package/esm/socket/index.d.mts +1 -1
- package/esm/socket/types.d.mts +25 -3
- package/esm/socket/types.d.mts.map +1 -1
- package/esm/storage/drivers/cloud-driver.d.mts +26 -0
- package/esm/storage/drivers/cloud-driver.d.mts.map +1 -1
- package/esm/storage/drivers/cloud-driver.mjs +100 -16
- package/esm/storage/drivers/cloud-driver.mjs.map +1 -1
- package/esm/storage/drivers/do-spaces-driver.d.mts +7 -0
- package/esm/storage/drivers/do-spaces-driver.d.mts.map +1 -1
- package/esm/storage/drivers/do-spaces-driver.mjs +9 -2
- package/esm/storage/drivers/do-spaces-driver.mjs.map +1 -1
- package/esm/storage/drivers/local-driver.d.mts +16 -5
- package/esm/storage/drivers/local-driver.d.mts.map +1 -1
- package/esm/storage/drivers/local-driver.mjs +71 -28
- package/esm/storage/drivers/local-driver.mjs.map +1 -1
- package/esm/storage/index.d.mts +3 -1
- package/esm/storage/index.mjs +4 -2
- package/esm/storage/scoped-storage.d.mts +27 -1
- package/esm/storage/scoped-storage.d.mts.map +1 -1
- package/esm/storage/scoped-storage.mjs +40 -8
- package/esm/storage/scoped-storage.mjs.map +1 -1
- package/esm/storage/storage.d.mts +11 -5
- package/esm/storage/storage.d.mts.map +1 -1
- package/esm/storage/storage.mjs +39 -16
- package/esm/storage/storage.mjs.map +1 -1
- package/esm/storage/types.d.mts +43 -4
- package/esm/storage/types.d.mts.map +1 -1
- package/esm/storage/utils/classify-ip-address.d.mts +13 -0
- package/esm/storage/utils/classify-ip-address.d.mts.map +1 -0
- package/esm/storage/utils/classify-ip-address.mjs +98 -0
- package/esm/storage/utils/classify-ip-address.mjs.map +1 -0
- package/esm/storage/utils/safe-fetch.d.mts +3 -12
- package/esm/storage/utils/safe-fetch.d.mts.map +1 -1
- package/esm/storage/utils/safe-fetch.mjs +3 -48
- package/esm/storage/utils/safe-fetch.mjs.map +1 -1
- package/esm/storage/utils/storage-capability-error.d.mts +25 -0
- package/esm/storage/utils/storage-capability-error.d.mts.map +1 -0
- package/esm/storage/utils/storage-capability-error.mjs +26 -0
- package/esm/storage/utils/storage-capability-error.mjs.map +1 -0
- package/esm/updater/update-warlock-packages.mjs +11 -1
- package/esm/updater/update-warlock-packages.mjs.map +1 -1
- package/esm/use-cases/use-case-broadcast.mjs +7 -1
- package/esm/use-cases/use-case-broadcast.mjs.map +1 -1
- package/esm/use-cases/use-cases-registry.mjs +21 -2
- package/esm/use-cases/use-cases-registry.mjs.map +1 -1
- package/esm/utils/database-log.mjs +0 -3
- package/esm/utils/database-log.mjs.map +1 -1
- package/esm/utils/queue.d.mts +6 -4
- package/esm/utils/queue.d.mts.map +1 -1
- package/esm/utils/queue.mjs +20 -9
- package/esm/utils/queue.mjs.map +1 -1
- package/esm/validation/database/exists-except-current-id.mjs +6 -2
- package/esm/validation/database/exists-except-current-id.mjs.map +1 -1
- package/esm/validation/database/unique-except-current-id.mjs +6 -2
- package/esm/validation/database/unique-except-current-id.mjs.map +1 -1
- package/esm/validation/init.mjs +6 -2
- package/esm/validation/init.mjs.map +1 -1
- package/esm/validation/validateAll.mjs +3 -2
- package/esm/validation/validateAll.mjs.map +1 -1
- package/esm/warlock-config/types.d.mts +14 -1
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/esm/warlock-config/warlock-config.manager.d.mts.map +1 -1
- package/esm/warlock-config/warlock-config.manager.mjs +3 -1
- package/esm/warlock-config/warlock-config.manager.mjs.map +1 -1
- package/esm/warlock-config/warn-deprecated-config.mjs +25 -0
- package/esm/warlock-config/warn-deprecated-config.mjs.map +1 -0
- package/llms-full.txt +71 -42
- package/llms.txt +41 -41
- package/package.json +21 -15
- package/skills/add-connector/SKILL.md +5 -1
- package/skills/benchmark-code/SKILL.md +1 -1
- package/skills/build-restful/SKILL.md +1 -1
- package/skills/build-url/SKILL.md +1 -1
- package/skills/configure-app/SKILL.md +7 -1
- package/skills/create-controller/SKILL.md +1 -1
- package/skills/create-module/SKILL.md +1 -1
- package/skills/define-resource/SKILL.md +1 -1
- package/skills/encrypt-data/SKILL.md +1 -1
- package/skills/hash-password/SKILL.md +1 -1
- package/skills/health-checks/SKILL.md +1 -1
- package/skills/index.md +30 -0
- package/skills/lower-stage3-decorators/SKILL.md +1 -1
- package/skills/process-image/SKILL.md +1 -1
- package/skills/register-route/SKILL.md +1 -1
- package/skills/request-memo/SKILL.md +1 -1
- package/skills/request-tracing/SKILL.md +1 -1
- package/skills/resolve-path/SKILL.md +1 -1
- package/skills/retry-operation/SKILL.md +1 -1
- package/skills/run-app/SKILL.md +1 -1
- package/skills/send-mail/SKILL.md +1 -1
- package/skills/send-response/SKILL.md +1 -1
- package/skills/store-file/SKILL.md +11 -1
- package/skills/test-http/SKILL.md +1 -1
- package/skills/test-service/SKILL.md +1 -1
- package/skills/update-packages/SKILL.md +1 -1
- package/skills/upload-file/SKILL.md +1 -1
- package/skills/use-app-context/SKILL.md +1 -1
- package/skills/use-localization/SKILL.md +1 -1
- package/skills/use-middleware/SKILL.md +7 -2
- package/skills/use-model-transformers/SKILL.md +1 -1
- package/skills/use-repository/SKILL.md +1 -1
- package/skills/use-request-locals/SKILL.md +1 -1
- package/skills/validate-input/SKILL.md +1 -1
- package/skills/warlock-conventions/SKILL.md +1 -1
- package/skills/warlock-doctor/SKILL.md +1 -1
- package/skills/warlock-routes/SKILL.md +1 -1
- package/skills/wire-socket/SKILL.md +5 -1
- package/skills/write-cli-command/SKILL.md +1 -1
- package/skills/write-middleware/SKILL.md +1 -1
- package/skills/write-seeder/SKILL.md +1 -1
- package/skills/write-use-case/SKILL.md +1 -1
- package/esm/_virtual/_rolldown/runtime.mjs +0 -7
package/esm/storage/types.d.mts
CHANGED
|
@@ -54,11 +54,12 @@ type CloudStorageDriverOptions = {
|
|
|
54
54
|
/**
|
|
55
55
|
* Access key ID
|
|
56
56
|
*/
|
|
57
|
-
accessKeyId
|
|
57
|
+
accessKeyId?: string;
|
|
58
58
|
/**
|
|
59
59
|
* Secret access key
|
|
60
|
+
* Omit both keys to use the AWS default credential provider chain
|
|
60
61
|
*/
|
|
61
|
-
secretAccessKey
|
|
62
|
+
secretAccessKey?: string;
|
|
62
63
|
/**
|
|
63
64
|
* Custom endpoint URL
|
|
64
65
|
* Optional - derived from provider if not specified
|
|
@@ -539,6 +540,14 @@ type StorageDriverConfig = {
|
|
|
539
540
|
* Secret key for signing local temporary URLs
|
|
540
541
|
*/
|
|
541
542
|
signatureKey?: string;
|
|
543
|
+
/**
|
|
544
|
+
* Storage path prefix (local and cloud drivers)
|
|
545
|
+
*/
|
|
546
|
+
prefix?: string;
|
|
547
|
+
/**
|
|
548
|
+
* URL prefix for local temporary URLs
|
|
549
|
+
*/
|
|
550
|
+
temporaryUrlPrefix?: string;
|
|
542
551
|
/**
|
|
543
552
|
* Bucket name (required for cloud drivers)
|
|
544
553
|
*/
|
|
@@ -548,11 +557,11 @@ type StorageDriverConfig = {
|
|
|
548
557
|
*/
|
|
549
558
|
region?: string;
|
|
550
559
|
/**
|
|
551
|
-
* Access key ID (
|
|
560
|
+
* Access key ID (optional: omit both keys to use the AWS default credential chain)
|
|
552
561
|
*/
|
|
553
562
|
accessKeyId?: string;
|
|
554
563
|
/**
|
|
555
|
-
* Secret access key (
|
|
564
|
+
* Secret access key (optional, see accessKeyId)
|
|
556
565
|
*/
|
|
557
566
|
secretAccessKey?: string;
|
|
558
567
|
/**
|
|
@@ -639,6 +648,16 @@ interface StorageDriverContract {
|
|
|
639
648
|
* @returns Raw file data object
|
|
640
649
|
*/
|
|
641
650
|
put(file: Buffer | UploadedFile | string | Readable, location: string, options?: PutOptions): Promise<StorageFileData>;
|
|
651
|
+
/**
|
|
652
|
+
* Atomically store a file only if nothing exists at `location`.
|
|
653
|
+
*
|
|
654
|
+
* OPTIONAL: drivers that cannot guarantee atomic create-if-absent must
|
|
655
|
+
* leave it undefined. Of N concurrent callers for one location exactly one
|
|
656
|
+
* gets file data; the rest get `null` and write nothing. `null` means ONLY
|
|
657
|
+
* "already exists" - every other failure throws. A string `file` is the
|
|
658
|
+
* CONTENT (unlike `put()`, where a string is a source path).
|
|
659
|
+
*/
|
|
660
|
+
putIfAbsent?(file: Buffer | string, location: string, options?: PutOptions): Promise<StorageFileData | null>;
|
|
642
661
|
/**
|
|
643
662
|
* Store a file from a readable stream
|
|
644
663
|
*
|
|
@@ -858,6 +877,26 @@ interface ScopedStorageContract {
|
|
|
858
877
|
* @returns StorageFile instance
|
|
859
878
|
*/
|
|
860
879
|
put(file: Buffer | UploadedFile | string | Readable, location: string, options?: PutOptions): Promise<StorageFile>;
|
|
880
|
+
/**
|
|
881
|
+
* Upload a local file by path. `put()` never treats a string as a path, so
|
|
882
|
+
* this is the only way to store a file from the server's filesystem.
|
|
883
|
+
*
|
|
884
|
+
* @param localPath - Path of the local file to upload
|
|
885
|
+
* @param location - Destination path in storage
|
|
886
|
+
* @param options - Storage options
|
|
887
|
+
*/
|
|
888
|
+
putFromPath(localPath: string, location: string, options?: PutOptions): Promise<StorageFile>;
|
|
889
|
+
/**
|
|
890
|
+
* Atomically store a file only if nothing exists at `location`.
|
|
891
|
+
*
|
|
892
|
+
* @returns StorageFile, or `null` when the location already exists
|
|
893
|
+
* @throws StorageCapabilityError when the driver lacks `putIfAbsent`
|
|
894
|
+
*/
|
|
895
|
+
putIfAbsent(file: Buffer | string, location: string, options?: PutOptions): Promise<StorageFile | null>;
|
|
896
|
+
/**
|
|
897
|
+
* Whether the active driver implements `putIfAbsent()`
|
|
898
|
+
*/
|
|
899
|
+
supportsPutIfAbsent(): boolean;
|
|
861
900
|
/**
|
|
862
901
|
* Store a file from stream
|
|
863
902
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/storage/types.ts"],"mappings":";;;;;;;;KASY,yBAAA;EAAyB;;;;EAKnC,IAAA;EAiBA;;;;AAWY;AAQd;;EA3BE,MAAA;EA2BmC;;;;;;EAnBnC,SAAA;
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/storage/types.ts"],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region ../core/src/storage/utils/classify-ip-address.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* True when `ip` is a private, loopback, link-local, unique-local,
|
|
4
|
+
* carrier-grade-NAT, unspecified, or otherwise non-public address — the set
|
|
5
|
+
* an SSRF guard must refuse. IPv6 is normalised first, so every spelling of
|
|
6
|
+
* an embedded IPv4 (mapped, hex-mapped, compatible, NAT64, 6to4) is judged
|
|
7
|
+
* by the IPv4 it carries. A non-IP string returns `false` (the caller
|
|
8
|
+
* resolves hostnames via DNS first).
|
|
9
|
+
*/
|
|
10
|
+
declare function isPrivateOrReservedIp(ip: string): boolean;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { isPrivateOrReservedIp };
|
|
13
|
+
//# sourceMappingURL=classify-ip-address.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classify-ip-address.d.mts","names":[],"sources":["../../../../../../../../core/src/storage/utils/classify-ip-address.ts"],"mappings":";;AAwKA;;;;AAAgD;;;iBAAhC,qBAAA,CAAsB,EAAU"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
|
|
3
|
+
//#region ../core/src/storage/utils/classify-ip-address.ts
|
|
4
|
+
/** Parse a dotted-quad into four octets, or `null` if malformed. */
|
|
5
|
+
function parseIpv4(ip) {
|
|
6
|
+
const parts = ip.split(".");
|
|
7
|
+
if (parts.length !== 4) return null;
|
|
8
|
+
const octets = parts.map((part) => Number(part));
|
|
9
|
+
if (octets.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return null;
|
|
10
|
+
return octets;
|
|
11
|
+
}
|
|
12
|
+
function isPrivateIpv4(ip) {
|
|
13
|
+
const octets = parseIpv4(ip);
|
|
14
|
+
if (!octets) return true;
|
|
15
|
+
const [a, b] = octets;
|
|
16
|
+
return a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 192 && b === 0 || a === 198 && (b === 18 || b === 19) || a === 198 && b === 51 || a === 203 && b === 0 || a >= 224;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Expand an IPv6 literal into its eight 16-bit groups (`::` expanded, a
|
|
20
|
+
* trailing dotted-quad folded into two groups, zone id dropped). Returns
|
|
21
|
+
* `null` when it cannot be parsed.
|
|
22
|
+
*/
|
|
23
|
+
function expandIpv6(ip) {
|
|
24
|
+
let text = (ip.toLowerCase().split("%")[0] ?? "").trim();
|
|
25
|
+
if (text.startsWith("[") && text.endsWith("]")) text = text.slice(1, -1);
|
|
26
|
+
const dotted = text.match(/^(.*:)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
27
|
+
if (dotted) {
|
|
28
|
+
const octets = parseIpv4(dotted[2]);
|
|
29
|
+
if (!octets) return null;
|
|
30
|
+
const [a, b, c, d] = octets;
|
|
31
|
+
text = `${dotted[1]}${(a << 8 | b).toString(16)}:${(c << 8 | d).toString(16)}`;
|
|
32
|
+
}
|
|
33
|
+
const halves = text.split("::");
|
|
34
|
+
if (halves.length > 2) return null;
|
|
35
|
+
const toGroups = (part) => part === "" ? [] : part.split(":");
|
|
36
|
+
const head = toGroups(halves[0]);
|
|
37
|
+
const tail = halves.length === 2 ? toGroups(halves[1]) : [];
|
|
38
|
+
let groups;
|
|
39
|
+
if (halves.length === 2) {
|
|
40
|
+
const missing = 8 - head.length - tail.length;
|
|
41
|
+
if (missing < 1) return null;
|
|
42
|
+
groups = [
|
|
43
|
+
...head,
|
|
44
|
+
...new Array(missing).fill("0"),
|
|
45
|
+
...tail
|
|
46
|
+
];
|
|
47
|
+
} else groups = head;
|
|
48
|
+
if (groups.length !== 8) return null;
|
|
49
|
+
const values = groups.map((group) => /^[0-9a-f]{1,4}$/.test(group) ? Number.parseInt(group, 16) : NaN);
|
|
50
|
+
return values.some((n) => Number.isNaN(n)) ? null : values;
|
|
51
|
+
}
|
|
52
|
+
/** Build a dotted-quad from two 16-bit groups. */
|
|
53
|
+
function toIpv4(high, low) {
|
|
54
|
+
return `${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Extract the IPv4 address embedded in an IPv6 one — IPv4-mapped
|
|
58
|
+
* (`::ffff:a.b.c.d` / `::ffff:XXXX:XXXX`), IPv4-translated (`::ffff:0:a.b.c.d`),
|
|
59
|
+
* IPv4-compatible (`::a.b.c.d`), NAT64 (`64:ff9b::/96`) and 6to4
|
|
60
|
+
* (`2002::/16`) — or `null` when there is none.
|
|
61
|
+
*/
|
|
62
|
+
function extractEmbeddedIpv4(g) {
|
|
63
|
+
const zero = (from, to) => g.slice(from, to).every((n) => n === 0);
|
|
64
|
+
if (zero(0, 5) && g[5] === 65535) return toIpv4(g[6], g[7]);
|
|
65
|
+
if (zero(0, 4) && g[4] === 65535 && g[5] === 0) return toIpv4(g[6], g[7]);
|
|
66
|
+
if (zero(0, 6)) return toIpv4(g[6], g[7]);
|
|
67
|
+
if (g[0] === 100 && g[1] === 65435 && zero(2, 6)) return toIpv4(g[6], g[7]);
|
|
68
|
+
if (g[0] === 8194) return toIpv4(g[1], g[2]);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function isPrivateIpv6(ip) {
|
|
72
|
+
const groups = expandIpv6(ip);
|
|
73
|
+
if (!groups) return true;
|
|
74
|
+
const embedded = extractEmbeddedIpv4(groups);
|
|
75
|
+
if (embedded !== null) return isPrivateIpv4(embedded);
|
|
76
|
+
const head = groups[0];
|
|
77
|
+
if ((head & 65024) === 64512) return true;
|
|
78
|
+
if ((head & 65472) === 65152) return true;
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* True when `ip` is a private, loopback, link-local, unique-local,
|
|
83
|
+
* carrier-grade-NAT, unspecified, or otherwise non-public address — the set
|
|
84
|
+
* an SSRF guard must refuse. IPv6 is normalised first, so every spelling of
|
|
85
|
+
* an embedded IPv4 (mapped, hex-mapped, compatible, NAT64, 6to4) is judged
|
|
86
|
+
* by the IPv4 it carries. A non-IP string returns `false` (the caller
|
|
87
|
+
* resolves hostnames via DNS first).
|
|
88
|
+
*/
|
|
89
|
+
function isPrivateOrReservedIp(ip) {
|
|
90
|
+
const family = isIP(ip);
|
|
91
|
+
if (family === 4) return isPrivateIpv4(ip);
|
|
92
|
+
if (family === 6) return isPrivateIpv6(ip);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
//#endregion
|
|
97
|
+
export { isPrivateOrReservedIp };
|
|
98
|
+
//# sourceMappingURL=classify-ip-address.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classify-ip-address.mjs","names":[],"sources":["../../../../../../../../core/src/storage/utils/classify-ip-address.ts"],"sourcesContent":["import { isIP } from \"node:net\";\n\n/**\n * Dependency-free IP classifier for SSRF guards. Imports node built-ins only\n * so other packages (e.g. `@warlock.js/ai`) can reuse it verbatim.\n */\n\ntype Ipv4Octets = [number, number, number, number];\n\n/** Parse a dotted-quad into four octets, or `null` if malformed. */\nfunction parseIpv4(ip: string): Ipv4Octets | null {\n const parts = ip.split(\".\");\n if (parts.length !== 4) {\n return null;\n }\n\n const octets = parts.map((part) => Number(part));\n if (octets.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) {\n return null;\n }\n\n return octets as Ipv4Octets;\n}\n\nfunction isPrivateIpv4(ip: string): boolean {\n const octets = parseIpv4(ip);\n if (!octets) {\n return true; // unparseable → refuse, fail closed\n }\n\n const [a, b] = octets;\n\n return (\n a === 0 || // 0.0.0.0/8 \"this network\"\n a === 10 || // 10.0.0.0/8 private\n a === 127 || // 127.0.0.0/8 loopback\n (a === 100 && b >= 64 && b <= 127) || // 100.64.0.0/10 CGNAT\n (a === 169 && b === 254) || // 169.254.0.0/16 link-local + metadata\n (a === 172 && b >= 16 && b <= 31) || // 172.16.0.0/12 private\n (a === 192 && b === 168) || // 192.168.0.0/16 private\n (a === 192 && b === 0) || // 192.0.0.0/24 + 192.0.2.0/24 (IETF/test)\n (a === 198 && (b === 18 || b === 19)) || // 198.18.0.0/15 benchmarking\n (a === 198 && b === 51) || // 198.51.100.0/24 test-net-2\n (a === 203 && b === 0) || // 203.0.113.0/24 test-net-3\n a >= 224 // 224.0.0.0/4 multicast + 240.0.0.0/4 reserved + 255.255.255.255\n );\n}\n\n/**\n * Expand an IPv6 literal into its eight 16-bit groups (`::` expanded, a\n * trailing dotted-quad folded into two groups, zone id dropped). Returns\n * `null` when it cannot be parsed.\n */\nfunction expandIpv6(ip: string): number[] | null {\n let text = (ip.toLowerCase().split(\"%\")[0] ?? \"\").trim();\n if (text.startsWith(\"[\") && text.endsWith(\"]\")) {\n text = text.slice(1, -1);\n }\n\n const dotted = text.match(/^(.*:)(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})$/);\n if (dotted) {\n const octets = parseIpv4(dotted[2] as string);\n if (!octets) {\n return null;\n }\n const [a, b, c, d] = octets;\n text = `${dotted[1]}${((a << 8) | b).toString(16)}:${((c << 8) | d).toString(16)}`;\n }\n\n const halves = text.split(\"::\");\n if (halves.length > 2) {\n return null;\n }\n\n const toGroups = (part: string): string[] => (part === \"\" ? [] : part.split(\":\"));\n const head = toGroups(halves[0] as string);\n const tail = halves.length === 2 ? toGroups(halves[1] as string) : [];\n\n let groups: string[];\n if (halves.length === 2) {\n const missing = 8 - head.length - tail.length;\n if (missing < 1) {\n return null;\n }\n groups = [...head, ...new Array<string>(missing).fill(\"0\"), ...tail];\n } else {\n groups = head;\n }\n\n if (groups.length !== 8) {\n return null;\n }\n\n const values = groups.map((group) =>\n /^[0-9a-f]{1,4}$/.test(group) ? Number.parseInt(group, 16) : Number.NaN,\n );\n\n return values.some((n) => Number.isNaN(n)) ? null : values;\n}\n\n/** Build a dotted-quad from two 16-bit groups. */\nfunction toIpv4(high: number, low: number): string {\n return `${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`;\n}\n\n/**\n * Extract the IPv4 address embedded in an IPv6 one — IPv4-mapped\n * (`::ffff:a.b.c.d` / `::ffff:XXXX:XXXX`), IPv4-translated (`::ffff:0:a.b.c.d`),\n * IPv4-compatible (`::a.b.c.d`), NAT64 (`64:ff9b::/96`) and 6to4\n * (`2002::/16`) — or `null` when there is none.\n */\nfunction extractEmbeddedIpv4(g: number[]): string | null {\n const zero = (from: number, to: number) => g.slice(from, to).every((n) => n === 0);\n\n if (zero(0, 5) && g[5] === 0xffff) {\n return toIpv4(g[6] as number, g[7] as number); // ::ffff:0:0/96\n }\n if (zero(0, 4) && g[4] === 0xffff && g[5] === 0) {\n return toIpv4(g[6] as number, g[7] as number); // ::ffff:0:0:0/96 (SIIT)\n }\n if (zero(0, 6)) {\n return toIpv4(g[6] as number, g[7] as number); // ::/96 (compatible)\n }\n if (g[0] === 0x64 && g[1] === 0xff9b && zero(2, 6)) {\n return toIpv4(g[6] as number, g[7] as number); // 64:ff9b::/96 NAT64\n }\n if (g[0] === 0x2002) {\n return toIpv4(g[1] as number, g[2] as number); // 2002::/16 6to4\n }\n\n return null;\n}\n\n// Every fallback returns `true` (\"private — refuse\"): this is an SSRF guard,\n// so an address we cannot classify must be refused, never let through.\nfunction isPrivateIpv6(ip: string): boolean {\n const groups = expandIpv6(ip);\n if (!groups) {\n return true;\n }\n\n const embedded = extractEmbeddedIpv4(groups);\n if (embedded !== null) {\n return isPrivateIpv4(embedded); // also covers :: (0.0.0.0) and ::1 (0.0.0.1)\n }\n\n const head = groups[0] as number;\n\n // fc00::/7 unique-local\n if ((head & 0xfe00) === 0xfc00) {\n return true;\n }\n // fe80::/10 link-local\n if ((head & 0xffc0) === 0xfe80) {\n return true;\n }\n\n return false;\n}\n\n/**\n * True when `ip` is a private, loopback, link-local, unique-local,\n * carrier-grade-NAT, unspecified, or otherwise non-public address — the set\n * an SSRF guard must refuse. IPv6 is normalised first, so every spelling of\n * an embedded IPv4 (mapped, hex-mapped, compatible, NAT64, 6to4) is judged\n * by the IPv4 it carries. A non-IP string returns `false` (the caller\n * resolves hostnames via DNS first).\n */\nexport function isPrivateOrReservedIp(ip: string): boolean {\n const family = isIP(ip);\n if (family === 4) {\n return isPrivateIpv4(ip);\n }\n if (family === 6) {\n return isPrivateIpv6(ip);\n }\n return false;\n}\n"],"mappings":";;;;AAUA,SAAS,UAAU,IAA+B;CAChD,MAAM,QAAQ,GAAG,MAAM,GAAG;CAC1B,IAAI,MAAM,WAAW,GACnB,OAAO;CAGT,MAAM,SAAS,MAAM,KAAK,SAAS,OAAO,IAAI,CAAC;CAC/C,IAAI,OAAO,MAAM,MAAM,CAAC,OAAO,UAAU,CAAC,KAAK,IAAI,KAAK,IAAI,GAAG,GAC7D,OAAO;CAGT,OAAO;AACT;AAEA,SAAS,cAAc,IAAqB;CAC1C,MAAM,SAAS,UAAU,EAAE;CAC3B,IAAI,CAAC,QACH,OAAO;CAGT,MAAM,CAAC,GAAG,KAAK;CAEf,OACE,MAAM,KACN,MAAM,MACN,MAAM,OACL,MAAM,OAAO,KAAK,MAAM,KAAK,OAC7B,MAAM,OAAO,MAAM,OACnB,MAAM,OAAO,KAAK,MAAM,KAAK,MAC7B,MAAM,OAAO,MAAM,OACnB,MAAM,OAAO,MAAM,KACnB,MAAM,QAAQ,MAAM,MAAM,MAAM,OAChC,MAAM,OAAO,MAAM,MACnB,MAAM,OAAO,MAAM,KACpB,KAAK;AAET;;;;;;AAOA,SAAS,WAAW,IAA6B;CAC/C,IAAI,QAAQ,GAAG,YAAY,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,GAAE,CAAE,KAAK;CACvD,IAAI,KAAK,WAAW,GAAG,KAAK,KAAK,SAAS,GAAG,GAC3C,OAAO,KAAK,MAAM,GAAG,EAAE;CAGzB,MAAM,SAAS,KAAK,MAAM,6CAA6C;CACvE,IAAI,QAAQ;EACV,MAAM,SAAS,UAAU,OAAO,EAAY;EAC5C,IAAI,CAAC,QACH,OAAO;EAET,MAAM,CAAC,GAAG,GAAG,GAAG,KAAK;EACrB,OAAO,GAAG,OAAO,MAAO,KAAK,IAAK,EAAC,CAAE,SAAS,EAAE,EAAE,IAAK,KAAK,IAAK,EAAC,CAAE,SAAS,EAAE;CACjF;CAEA,MAAM,SAAS,KAAK,MAAM,IAAI;CAC9B,IAAI,OAAO,SAAS,GAClB,OAAO;CAGT,MAAM,YAAY,SAA4B,SAAS,KAAK,CAAC,IAAI,KAAK,MAAM,GAAG;CAC/E,MAAM,OAAO,SAAS,OAAO,EAAY;CACzC,MAAM,OAAO,OAAO,WAAW,IAAI,SAAS,OAAO,EAAY,IAAI,CAAC;CAEpE,IAAI;CACJ,IAAI,OAAO,WAAW,GAAG;EACvB,MAAM,UAAU,IAAI,KAAK,SAAS,KAAK;EACvC,IAAI,UAAU,GACZ,OAAO;EAET,SAAS;GAAC,GAAG;GAAM,GAAG,IAAI,MAAc,OAAO,CAAC,CAAC,KAAK,GAAG;GAAG,GAAG;EAAI;CACrE,OACE,SAAS;CAGX,IAAI,OAAO,WAAW,GACpB,OAAO;CAGT,MAAM,SAAS,OAAO,KAAK,UACzB,kBAAkB,KAAK,KAAK,IAAI,OAAO,SAAS,OAAO,EAAE,IAAI,GAC/D;CAEA,OAAO,OAAO,MAAM,MAAM,OAAO,MAAM,CAAC,CAAC,IAAI,OAAO;AACtD;;AAGA,SAAS,OAAO,MAAc,KAAqB;CACjD,OAAO,GAAG,QAAQ,EAAE,GAAG,OAAO,IAAK,GAAG,OAAO,EAAE,GAAG,MAAM;AAC1D;;;;;;;AAQA,SAAS,oBAAoB,GAA4B;CACvD,MAAM,QAAQ,MAAc,OAAe,EAAE,MAAM,MAAM,EAAE,CAAC,CAAC,OAAO,MAAM,MAAM,CAAC;CAEjF,IAAI,KAAK,GAAG,CAAC,KAAK,EAAE,OAAO,OACzB,OAAO,OAAO,EAAE,IAAc,EAAE,EAAY;CAE9C,IAAI,KAAK,GAAG,CAAC,KAAK,EAAE,OAAO,SAAU,EAAE,OAAO,GAC5C,OAAO,OAAO,EAAE,IAAc,EAAE,EAAY;CAE9C,IAAI,KAAK,GAAG,CAAC,GACX,OAAO,OAAO,EAAE,IAAc,EAAE,EAAY;CAE9C,IAAI,EAAE,OAAO,OAAQ,EAAE,OAAO,SAAU,KAAK,GAAG,CAAC,GAC/C,OAAO,OAAO,EAAE,IAAc,EAAE,EAAY;CAE9C,IAAI,EAAE,OAAO,MACX,OAAO,OAAO,EAAE,IAAc,EAAE,EAAY;CAG9C,OAAO;AACT;AAIA,SAAS,cAAc,IAAqB;CAC1C,MAAM,SAAS,WAAW,EAAE;CAC5B,IAAI,CAAC,QACH,OAAO;CAGT,MAAM,WAAW,oBAAoB,MAAM;CAC3C,IAAI,aAAa,MACf,OAAO,cAAc,QAAQ;CAG/B,MAAM,OAAO,OAAO;CAGpB,KAAK,OAAO,WAAY,OACtB,OAAO;CAGT,KAAK,OAAO,WAAY,OACtB,OAAO;CAGT,OAAO;AACT;;;;;;;;;AAUA,SAAgB,sBAAsB,IAAqB;CACzD,MAAM,SAAS,KAAK,EAAE;CACtB,IAAI,WAAW,GACb,OAAO,cAAc,EAAE;CAEzB,IAAI,WAAW,GACb,OAAO,cAAc,EAAE;CAEzB,OAAO;AACT"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isPrivateOrReservedIp } from "./classify-ip-address.mjs";
|
|
2
|
+
|
|
1
3
|
//#region ../core/src/storage/utils/safe-fetch.d.ts
|
|
2
4
|
/**
|
|
3
5
|
* Options controlling a guarded outbound fetch. Every field is optional;
|
|
@@ -30,17 +32,6 @@ type SafeFetchOptions = {
|
|
|
30
32
|
*/
|
|
31
33
|
fetch?: typeof fetch;
|
|
32
34
|
};
|
|
33
|
-
/**
|
|
34
|
-
* True when `ip` is a private, loopback, link-local, unique-local,
|
|
35
|
-
* carrier-grade-NAT, unspecified, or otherwise non-public address — the set
|
|
36
|
-
* an SSRF guard must refuse. Accepts IPv4 and IPv6 literals (including
|
|
37
|
-
* IPv4-mapped IPv6 like `::ffff:169.254.169.254`). A non-IP string returns
|
|
38
|
-
* `false` (the caller resolves hostnames via DNS first).
|
|
39
|
-
*
|
|
40
|
-
* The cloud-metadata endpoint `169.254.169.254` is covered by the IPv4
|
|
41
|
-
* link-local range `169.254.0.0/16`.
|
|
42
|
-
*/
|
|
43
|
-
declare function isPrivateOrReservedIp(ip: string): boolean;
|
|
44
35
|
/**
|
|
45
36
|
* Result of {@link safeFetchToBuffer}: the (capped) body plus the response's
|
|
46
37
|
* content-type, so callers can derive a MIME type and enforce a non-empty
|
|
@@ -74,5 +65,5 @@ type SafeFetchResult = {
|
|
|
74
65
|
*/
|
|
75
66
|
declare function safeFetchToBuffer(rawUrl: string, options?: SafeFetchOptions): Promise<SafeFetchResult>;
|
|
76
67
|
//#endregion
|
|
77
|
-
export { SafeFetchOptions, SafeFetchResult,
|
|
68
|
+
export { SafeFetchOptions, SafeFetchResult, safeFetchToBuffer };
|
|
78
69
|
//# sourceMappingURL=safe-fetch.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe-fetch.d.mts","names":[],"sources":["../../../../../../../../core/src/storage/utils/safe-fetch.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"safe-fetch.d.mts","names":[],"sources":["../../../../../../../../core/src/storage/utils/safe-fetch.ts"],"mappings":";;;;;AAoCA;;;;;KAAY,gBAAA;EAaV;;;;EARA,iBAAA;EAkBoB;AA2GtB;;;;EAvHE,QAAA,WAwHQ;EAtHR,SAAA;EAwHA;;;;EAnHA,cAAA;EA2IoB;;;;EAtIpB,KAAA,UAAe,KAAK;AAAA;;;;;;KA2GV,eAAA;EACV,MAAA,EAAQ,MAAM;EACd,WAAA;EACA,MAAA;EACA,UAAA;EACA,EAAA;AAAA;;;;;;;;;;;;;;;;;;;;iBAsBoB,iBAAA,CACpB,MAAA,UACA,OAAA,GAAS,gBAAA,GACR,OAAA,CAAQ,eAAA"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { StorageError } from "./storage-error.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { isPrivateOrReservedIp } from "./classify-ip-address.mjs";
|
|
3
3
|
import { lookup } from "node:dns/promises";
|
|
4
|
+
import { isIP } from "node:net";
|
|
4
5
|
|
|
5
6
|
//#region ../core/src/storage/utils/safe-fetch.ts
|
|
6
7
|
/**
|
|
@@ -43,52 +44,6 @@ function resolveFetchPolicy(options = {}) {
|
|
|
43
44
|
fetch: options.fetch ?? globalThis.fetch
|
|
44
45
|
};
|
|
45
46
|
}
|
|
46
|
-
/** Parse a dotted-quad into four octets, or `null` if malformed. */
|
|
47
|
-
function parseIpv4(ip) {
|
|
48
|
-
const parts = ip.split(".");
|
|
49
|
-
if (parts.length !== 4) return null;
|
|
50
|
-
const octets = parts.map((part) => Number(part));
|
|
51
|
-
if (octets.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return null;
|
|
52
|
-
return octets;
|
|
53
|
-
}
|
|
54
|
-
function isPrivateIpv4(ip) {
|
|
55
|
-
const octets = parseIpv4(ip);
|
|
56
|
-
if (!octets) return true;
|
|
57
|
-
const [a, b] = octets;
|
|
58
|
-
return a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 192 && b === 0 || a === 198 && (b === 18 || b === 19) || a === 198 && b === 51 || a === 203 && b === 0 || a >= 224;
|
|
59
|
-
}
|
|
60
|
-
function isPrivateIpv6(ip) {
|
|
61
|
-
const normalized = ip.toLowerCase().split("%")[0];
|
|
62
|
-
if (normalized === void 0) return true;
|
|
63
|
-
const v4 = normalized.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
64
|
-
if (v4) {
|
|
65
|
-
const dottedQuad = v4[1];
|
|
66
|
-
return dottedQuad === void 0 ? true : isPrivateIpv4(dottedQuad);
|
|
67
|
-
}
|
|
68
|
-
if (normalized === "::1" || normalized === "::") return true;
|
|
69
|
-
const firstGroup = normalized.split(":")[0];
|
|
70
|
-
if (firstGroup === void 0) return true;
|
|
71
|
-
const head = firstGroup === "" ? 0 : Number.parseInt(firstGroup, 16);
|
|
72
|
-
if ((head & 65024) === 64512) return true;
|
|
73
|
-
if ((head & 65472) === 65152) return true;
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* True when `ip` is a private, loopback, link-local, unique-local,
|
|
78
|
-
* carrier-grade-NAT, unspecified, or otherwise non-public address — the set
|
|
79
|
-
* an SSRF guard must refuse. Accepts IPv4 and IPv6 literals (including
|
|
80
|
-
* IPv4-mapped IPv6 like `::ffff:169.254.169.254`). A non-IP string returns
|
|
81
|
-
* `false` (the caller resolves hostnames via DNS first).
|
|
82
|
-
*
|
|
83
|
-
* The cloud-metadata endpoint `169.254.169.254` is covered by the IPv4
|
|
84
|
-
* link-local range `169.254.0.0/16`.
|
|
85
|
-
*/
|
|
86
|
-
function isPrivateOrReservedIp(ip) {
|
|
87
|
-
const family = isIP(ip);
|
|
88
|
-
if (family === 4) return isPrivateIpv4(ip);
|
|
89
|
-
if (family === 6) return isPrivateIpv6(ip);
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
47
|
/** Strip the `[ ]` IPv6 brackets `URL.hostname` keeps. */
|
|
93
48
|
function stripBrackets(host) {
|
|
94
49
|
return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
@@ -253,5 +208,5 @@ async function readBodyCapped(response, maxBytes, rawUrl) {
|
|
|
253
208
|
}
|
|
254
209
|
|
|
255
210
|
//#endregion
|
|
256
|
-
export {
|
|
211
|
+
export { safeFetchToBuffer };
|
|
257
212
|
//# sourceMappingURL=safe-fetch.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe-fetch.mjs","names":[],"sources":["../../../../../../../../core/src/storage/utils/safe-fetch.ts"],"sourcesContent":["import { lookup } from \"node:dns/promises\";\nimport { isIP } from \"@warlock.js/seal\";\nimport { StorageError } from \"./storage-error\";\n\n/**\n * SSRF / resource-exhaustion guard for storage's outbound downloads\n * (`putFromUrl`).\n *\n * NOTE: this MIRRORS `@warlock.js/ai`'s `src/security/private-ip.ts` and\n * `src/security/outbound-policy.ts`. It is duplicated here intentionally\n * because `@warlock.js/core` cannot depend on `@warlock.js/ai`. Pending\n * extraction of a shared security package, keep the two copies in sync.\n */\n\n/** 50 MiB — default cap on a downloaded body (storage files run larger than AI payloads). */\nconst DEFAULT_MAX_BYTES = 50 * 1024 * 1024;\n/** 30s — default per-request timeout. */\nconst DEFAULT_TIMEOUT_MS = 30_000;\n/**\n * Cap on the number of re-validated redirect hops. Mirrors\n * `@warlock.js/ai`'s `DEFAULT_MAX_REDIRECTS`\n * (ai/src/security/outbound-policy.ts:15).\n */\nconst DEFAULT_MAX_REDIRECTS = 5;\n\n/** 3xx statuses whose `Location` a follow re-issues — mirrors ai/src/security/outbound-policy.ts:18. */\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\n\n/**\n * Options controlling a guarded outbound fetch. Every field is optional;\n * {@link resolveFetchPolicy} fills safe defaults. The defaults are\n * deliberately strict (https+http, private-IP deny ON, 30s timeout,\n * 50 MiB cap) so a caller that does not tune them still gets a hardened\n * request.\n */\nexport type SafeFetchOptions = {\n /**\n * Whether to ALLOW private / loopback / link-local / metadata hosts.\n * Default `false` — the SSRF guard is on (private hosts are denied).\n */\n allowPrivateHosts?: boolean;\n /**\n * Maximum response body size in bytes. A declared `content-length` over\n * this fails fast; otherwise the body is read with a running cap and the\n * stream is aborted on overflow. Default `52_428_800` (50 MiB).\n */\n maxBytes?: number;\n /** Per-request timeout in milliseconds. Default `30_000`. */\n timeoutMs?: number;\n /**\n * URL schemes permitted for the request, compared case-insensitively.\n * Default `[\"https\", \"http\"]`.\n */\n allowedSchemes?: string[];\n /**\n * Injected `fetch` implementation (for tests or proxies). Defaults to the\n * global `fetch`.\n */\n fetch?: typeof fetch;\n};\n\n/** {@link SafeFetchOptions} with every default resolved — never partial. */\ntype ResolvedFetchPolicy = {\n allowPrivateHosts: boolean;\n maxBytes: number;\n timeoutMs: number;\n allowedSchemes: string[];\n fetch: typeof fetch;\n};\n\n/**\n * Fill a {@link SafeFetchOptions} with strict defaults: https+http,\n * private-IP deny on, 30s timeout, 50 MiB cap, global `fetch`.\n */\nfunction resolveFetchPolicy(options: SafeFetchOptions = {}): ResolvedFetchPolicy {\n return {\n allowPrivateHosts: options.allowPrivateHosts ?? false,\n maxBytes: options.maxBytes ?? DEFAULT_MAX_BYTES,\n timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n allowedSchemes: options.allowedSchemes ?? [\"https\", \"http\"],\n fetch: options.fetch ?? globalThis.fetch,\n };\n}\n\n/** Parse a dotted-quad into four octets, or `null` if malformed. */\nfunction parseIpv4(ip: string): [number, number, number, number] | null {\n const parts = ip.split(\".\");\n if (parts.length !== 4) {\n return null;\n }\n\n const octets = parts.map((part) => Number(part));\n if (octets.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) {\n return null;\n }\n\n return octets as [number, number, number, number];\n}\n\nfunction isPrivateIpv4(ip: string): boolean {\n const octets = parseIpv4(ip);\n if (!octets) {\n return true; // unparseable → refuse, fail closed\n }\n\n const [a, b] = octets;\n\n return (\n a === 0 || // 0.0.0.0/8 \"this network\"\n a === 10 || // 10.0.0.0/8 private\n a === 127 || // 127.0.0.0/8 loopback\n (a === 100 && b >= 64 && b <= 127) || // 100.64.0.0/10 CGNAT\n (a === 169 && b === 254) || // 169.254.0.0/16 link-local + metadata\n (a === 172 && b >= 16 && b <= 31) || // 172.16.0.0/12 private\n (a === 192 && b === 168) || // 192.168.0.0/16 private\n (a === 192 && b === 0) || // 192.0.0.0/24 + 192.0.2.0/24 (IETF/test)\n (a === 198 && (b === 18 || b === 19)) || // 198.18.0.0/15 benchmarking\n (a === 198 && b === 51) || // 198.51.100.0/24 test-net-2\n (a === 203 && b === 0) || // 203.0.113.0/24 test-net-3\n a >= 224 // 224.0.0.0/4 multicast + 240.0.0.0/4 reserved + 255.255.255.255\n );\n}\n\nfunction isPrivateIpv6(ip: string): boolean {\n const normalized = ip.toLowerCase().split(\"%\")[0]; // drop zone id\n\n // EVERY fallback in this function returns `true`, and that direction is the\n // whole point: this is an SSRF guard, and `true` means \"private — refuse\".\n //\n // `split` on a non-empty string always yields a first element, so none of\n // these is reachable today. But the safe answer for an address we could not\n // classify is to REFUSE it, not to let it through. A `?? \"\"` here would fall\n // past every check below and return `false` — reporting an unparseable\n // address as public, which is a fail-OPEN in the one function whose job is\n // to keep requests off the internal network.\n if (normalized === undefined) {\n return true;\n }\n\n // IPv4-mapped / -embedded (::ffff:a.b.c.d, ::a.b.c.d) — defer to the v4\n // check on the trailing dotted-quad so an inward-mapped address is caught.\n const v4 = normalized.match(/(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})$/);\n if (v4) {\n const dottedQuad = v4[1];\n\n // Capture group 1 exists whenever this pattern matched. Refusing rather\n // than defaulting, for the reason above.\n return dottedQuad === undefined ? true : isPrivateIpv4(dottedQuad);\n }\n\n if (normalized === \"::1\" || normalized === \"::\") {\n return true; // loopback / unspecified\n }\n\n // Expand only the leading group enough to classify the reserved blocks.\n const firstGroup = normalized.split(\":\")[0];\n\n if (firstGroup === undefined) {\n return true;\n }\n\n const head = firstGroup === \"\" ? 0 : Number.parseInt(firstGroup, 16);\n\n // fc00::/7 unique-local (fc.. / fd..)\n if ((head & 0xfe00) === 0xfc00) {\n return true;\n }\n // fe80::/10 link-local\n if ((head & 0xffc0) === 0xfe80) {\n return true;\n }\n\n return false;\n}\n\n/**\n * True when `ip` is a private, loopback, link-local, unique-local,\n * carrier-grade-NAT, unspecified, or otherwise non-public address — the set\n * an SSRF guard must refuse. Accepts IPv4 and IPv6 literals (including\n * IPv4-mapped IPv6 like `::ffff:169.254.169.254`). A non-IP string returns\n * `false` (the caller resolves hostnames via DNS first).\n *\n * The cloud-metadata endpoint `169.254.169.254` is covered by the IPv4\n * link-local range `169.254.0.0/16`.\n */\nexport function isPrivateOrReservedIp(ip: string): boolean {\n const family = isIP(ip);\n if (family === 4) {\n return isPrivateIpv4(ip);\n }\n if (family === 6) {\n return isPrivateIpv6(ip);\n }\n return false;\n}\n\n/** Strip the `[ ]` IPv6 brackets `URL.hostname` keeps. */\nfunction stripBrackets(host: string): string {\n return host.startsWith(\"[\") && host.endsWith(\"]\") ? host.slice(1, -1) : host;\n}\n\n/**\n * Reject when `host` is — or resolves to — a private / reserved address.\n * IP literals are checked directly; hostnames are resolved via DNS and every\n * returned address is checked (a public name pointing inward is caught). A\n * resolution failure fails closed.\n */\nasync function assertHostNotPrivate(host: string, rawUrl: string): Promise<void> {\n if (isIP(host) !== 0) {\n if (isPrivateOrReservedIp(host)) {\n throw new StorageError(`outbound request blocked — \"${host}\" is a private/reserved address`, {\n context: { url: rawUrl, address: host },\n });\n }\n return;\n }\n\n let addresses: Array<{ address: string }>;\n try {\n addresses = await lookup(host, { all: true });\n } catch (cause) {\n throw new StorageError(\n `outbound request blocked — could not resolve host \"${host}\" to verify it is public`,\n { cause, context: { url: rawUrl, host } },\n );\n }\n\n for (const { address } of addresses) {\n if (isPrivateOrReservedIp(address)) {\n throw new StorageError(\n `outbound request blocked — host \"${host}\" resolves to a private/reserved address (${address})`,\n { context: { url: rawUrl, host, address } },\n );\n }\n }\n}\n\n/**\n * Validate a URL against the policy BEFORE any network call: scheme\n * allowlist and (when private hosts are not allowed) a DNS resolution that\n * rejects private / loopback / link-local / metadata addresses — the SSRF\n * guard. Returns the parsed `URL` on success; throws {@link StorageError}\n * otherwise.\n */\nasync function assertUrlAllowed(rawUrl: string, policy: ResolvedFetchPolicy): Promise<URL> {\n let url: URL;\n try {\n url = new URL(rawUrl);\n } catch {\n throw new StorageError(`outbound request blocked — invalid URL: ${rawUrl}`, {\n context: { url: rawUrl },\n });\n }\n\n const scheme = url.protocol.replace(/:$/, \"\").toLowerCase();\n if (!policy.allowedSchemes.some((s) => s.toLowerCase() === scheme)) {\n throw new StorageError(\n `outbound request blocked — scheme \"${scheme}\" is not allowed (allowed: ${policy.allowedSchemes.join(\", \")})`,\n { context: { url: rawUrl, scheme } },\n );\n }\n\n if (!policy.allowPrivateHosts) {\n const host = stripBrackets(url.hostname);\n await assertHostNotPrivate(host, rawUrl);\n }\n\n return url;\n}\n\n/**\n * Result of {@link safeFetchToBuffer}: the (capped) body plus the response's\n * content-type, so callers can derive a MIME type and enforce a non-empty\n * content-type check.\n */\nexport type SafeFetchResult = {\n buffer: Buffer;\n contentType: string | null;\n status: number;\n statusText: string;\n ok: boolean;\n};\n\n/**\n * SSRF-guarded download: validates the URL (scheme allowlist + post-DNS\n * private-IP deny), performs the request with a timeout via\n * `AbortController`, then streams the body with a hard byte cap — aborting\n * the moment the running total exceeds `maxBytes`. A declared\n * `content-length` over the cap fails fast.\n *\n * Redirects are NEVER delegated to the platform: every hop is issued with\n * `redirect: \"manual\"` and its `Location` is re-run through\n * {@link assertUrlAllowed} — including the DNS-resolution private-IP check\n * — before being followed (capped at {@link DEFAULT_MAX_REDIRECTS}), so a\n * 3xx from an allowed host cannot smuggle the request to a private /\n * metadata target. Mirrors `@warlock.js/ai`'s `guardedFetch`\n * (ai/src/security/outbound-policy.ts:188-284); duplicated per this\n * module's file-header note since core cannot depend on ai.\n *\n * @throws {StorageError} On a blocked URL (initial or redirect target),\n * a redirect-hop bound overflow, timeout, or oversized body.\n */\nexport async function safeFetchToBuffer(\n rawUrl: string,\n options: SafeFetchOptions = {},\n): Promise<SafeFetchResult> {\n const policy = resolveFetchPolicy(options);\n let url = await assertUrlAllowed(rawUrl, policy);\n\n const controller = new AbortController();\n const timer = setTimeout(() => {\n controller.abort(\n new StorageError(`outbound request timed out after ${policy.timeoutMs}ms`, {\n context: { url: rawUrl, timeoutMs: policy.timeoutMs },\n }),\n );\n }, policy.timeoutMs);\n\n try {\n let response: Response;\n\n for (let hop = 0; ; hop++) {\n response = await policy.fetch(url, { signal: controller.signal, redirect: \"manual\" });\n\n const location = response.headers.get(\"location\");\n if (!REDIRECT_STATUSES.has(response.status) || location === null) {\n break;\n }\n\n if (hop >= DEFAULT_MAX_REDIRECTS) {\n throw new StorageError(\n `outbound request blocked — more than ${DEFAULT_MAX_REDIRECTS} redirects`,\n { context: { url: rawUrl, maxRedirects: DEFAULT_MAX_REDIRECTS } },\n );\n }\n\n let target: URL;\n try {\n target = new URL(location, url);\n } catch {\n throw new StorageError(\n `outbound request blocked — invalid redirect Location: ${location}`,\n {\n context: { url: url.toString(), location },\n },\n );\n }\n\n // Discard the interim body so the connection can be reused.\n if (response.body) {\n await response.body.cancel().catch(() => undefined);\n }\n\n // The redirect target gets the SAME scheme + private-IP validation\n // (incl. DNS resolution) as the initial URL — this is what closes\n // the redirect-based SSRF bypass: a 302 from an allowed host to\n // 169.254.169.254 is re-validated here, not blindly followed.\n url = await assertUrlAllowed(target.toString(), policy);\n }\n\n const declared = Number(response.headers.get(\"content-length\"));\n if (Number.isFinite(declared) && declared > policy.maxBytes) {\n throw new StorageError(\n `outbound response body too large — declared ${declared} bytes exceeds the ${policy.maxBytes}-byte cap`,\n { context: { url: rawUrl, declared, maxBytes: policy.maxBytes } },\n );\n }\n\n const buffer = await readBodyCapped(response, policy.maxBytes, rawUrl);\n\n return {\n buffer,\n contentType: response.headers.get(\"content-type\"),\n status: response.status,\n statusText: response.statusText,\n ok: response.ok,\n };\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Read a response body into a Buffer with a hard byte cap. The stream is\n * read chunk-by-chunk and cancelled the moment the running total exceeds\n * `maxBytes`. Throws {@link StorageError} on overflow.\n */\nasync function readBodyCapped(\n response: Response,\n maxBytes: number,\n rawUrl: string,\n): Promise<Buffer> {\n if (!response.body) {\n const arrayBuffer = await response.arrayBuffer();\n if (arrayBuffer.byteLength > maxBytes) {\n throw new StorageError(`outbound response body exceeded the ${maxBytes}-byte cap`, {\n context: { url: rawUrl, maxBytes },\n });\n }\n return Buffer.from(arrayBuffer);\n }\n\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n if (!value) {\n continue;\n }\n\n total += value.byteLength;\n if (total > maxBytes) {\n await reader.cancel();\n throw new StorageError(`outbound response body exceeded the ${maxBytes}-byte cap`, {\n context: { url: rawUrl, maxBytes },\n });\n }\n chunks.push(value);\n }\n\n return Buffer.concat(chunks);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"safe-fetch.mjs","names":[],"sources":["../../../../../../../../core/src/storage/utils/safe-fetch.ts"],"sourcesContent":["import { lookup } from \"node:dns/promises\";\nimport { isIP } from \"node:net\";\nimport { isPrivateOrReservedIp } from \"./classify-ip-address\";\nimport { StorageError } from \"./storage-error\";\n\n/**\n * SSRF / resource-exhaustion guard for storage's outbound downloads\n * (`putFromUrl`).\n *\n * NOTE: this MIRRORS `@warlock.js/ai`'s `src/security/private-ip.ts` and\n * `src/security/outbound-policy.ts`. It is duplicated here intentionally\n * because `@warlock.js/core` cannot depend on `@warlock.js/ai`. Pending\n * extraction of a shared security package, keep the two copies in sync.\n */\n\n/** 50 MiB — default cap on a downloaded body (storage files run larger than AI payloads). */\nconst DEFAULT_MAX_BYTES = 50 * 1024 * 1024;\n/** 30s — default per-request timeout. */\nconst DEFAULT_TIMEOUT_MS = 30_000;\n/**\n * Cap on the number of re-validated redirect hops. Mirrors\n * `@warlock.js/ai`'s `DEFAULT_MAX_REDIRECTS`\n * (ai/src/security/outbound-policy.ts:15).\n */\nconst DEFAULT_MAX_REDIRECTS = 5;\n\n/** 3xx statuses whose `Location` a follow re-issues — mirrors ai/src/security/outbound-policy.ts:18. */\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\n\n/**\n * Options controlling a guarded outbound fetch. Every field is optional;\n * {@link resolveFetchPolicy} fills safe defaults. The defaults are\n * deliberately strict (https+http, private-IP deny ON, 30s timeout,\n * 50 MiB cap) so a caller that does not tune them still gets a hardened\n * request.\n */\nexport type SafeFetchOptions = {\n /**\n * Whether to ALLOW private / loopback / link-local / metadata hosts.\n * Default `false` — the SSRF guard is on (private hosts are denied).\n */\n allowPrivateHosts?: boolean;\n /**\n * Maximum response body size in bytes. A declared `content-length` over\n * this fails fast; otherwise the body is read with a running cap and the\n * stream is aborted on overflow. Default `52_428_800` (50 MiB).\n */\n maxBytes?: number;\n /** Per-request timeout in milliseconds. Default `30_000`. */\n timeoutMs?: number;\n /**\n * URL schemes permitted for the request, compared case-insensitively.\n * Default `[\"https\", \"http\"]`.\n */\n allowedSchemes?: string[];\n /**\n * Injected `fetch` implementation (for tests or proxies). Defaults to the\n * global `fetch`.\n */\n fetch?: typeof fetch;\n};\n\n/** {@link SafeFetchOptions} with every default resolved — never partial. */\ntype ResolvedFetchPolicy = {\n allowPrivateHosts: boolean;\n maxBytes: number;\n timeoutMs: number;\n allowedSchemes: string[];\n fetch: typeof fetch;\n};\n\n/**\n * Fill a {@link SafeFetchOptions} with strict defaults: https+http,\n * private-IP deny on, 30s timeout, 50 MiB cap, global `fetch`.\n */\nfunction resolveFetchPolicy(options: SafeFetchOptions = {}): ResolvedFetchPolicy {\n return {\n allowPrivateHosts: options.allowPrivateHosts ?? false,\n maxBytes: options.maxBytes ?? DEFAULT_MAX_BYTES,\n timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n allowedSchemes: options.allowedSchemes ?? [\"https\", \"http\"],\n fetch: options.fetch ?? globalThis.fetch,\n };\n}\n\nexport { isPrivateOrReservedIp };\n\n/** Strip the `[ ]` IPv6 brackets `URL.hostname` keeps. */\nfunction stripBrackets(host: string): string {\n return host.startsWith(\"[\") && host.endsWith(\"]\") ? host.slice(1, -1) : host;\n}\n\n/**\n * Reject when `host` is — or resolves to — a private / reserved address.\n * IP literals are checked directly; hostnames are resolved via DNS and every\n * returned address is checked (a public name pointing inward is caught). A\n * resolution failure fails closed.\n */\nasync function assertHostNotPrivate(host: string, rawUrl: string): Promise<void> {\n if (isIP(host) !== 0) {\n if (isPrivateOrReservedIp(host)) {\n throw new StorageError(`outbound request blocked — \"${host}\" is a private/reserved address`, {\n context: { url: rawUrl, address: host },\n });\n }\n return;\n }\n\n let addresses: Array<{ address: string }>;\n try {\n addresses = await lookup(host, { all: true });\n } catch (cause) {\n throw new StorageError(\n `outbound request blocked — could not resolve host \"${host}\" to verify it is public`,\n { cause, context: { url: rawUrl, host } },\n );\n }\n\n for (const { address } of addresses) {\n if (isPrivateOrReservedIp(address)) {\n throw new StorageError(\n `outbound request blocked — host \"${host}\" resolves to a private/reserved address (${address})`,\n { context: { url: rawUrl, host, address } },\n );\n }\n }\n}\n\n/**\n * Validate a URL against the policy BEFORE any network call: scheme\n * allowlist and (when private hosts are not allowed) a DNS resolution that\n * rejects private / loopback / link-local / metadata addresses — the SSRF\n * guard. Returns the parsed `URL` on success; throws {@link StorageError}\n * otherwise.\n */\nasync function assertUrlAllowed(rawUrl: string, policy: ResolvedFetchPolicy): Promise<URL> {\n let url: URL;\n try {\n url = new URL(rawUrl);\n } catch {\n throw new StorageError(`outbound request blocked — invalid URL: ${rawUrl}`, {\n context: { url: rawUrl },\n });\n }\n\n const scheme = url.protocol.replace(/:$/, \"\").toLowerCase();\n if (!policy.allowedSchemes.some((s) => s.toLowerCase() === scheme)) {\n throw new StorageError(\n `outbound request blocked — scheme \"${scheme}\" is not allowed (allowed: ${policy.allowedSchemes.join(\", \")})`,\n { context: { url: rawUrl, scheme } },\n );\n }\n\n if (!policy.allowPrivateHosts) {\n const host = stripBrackets(url.hostname);\n await assertHostNotPrivate(host, rawUrl);\n }\n\n return url;\n}\n\n/**\n * Result of {@link safeFetchToBuffer}: the (capped) body plus the response's\n * content-type, so callers can derive a MIME type and enforce a non-empty\n * content-type check.\n */\nexport type SafeFetchResult = {\n buffer: Buffer;\n contentType: string | null;\n status: number;\n statusText: string;\n ok: boolean;\n};\n\n/**\n * SSRF-guarded download: validates the URL (scheme allowlist + post-DNS\n * private-IP deny), performs the request with a timeout via\n * `AbortController`, then streams the body with a hard byte cap — aborting\n * the moment the running total exceeds `maxBytes`. A declared\n * `content-length` over the cap fails fast.\n *\n * Redirects are NEVER delegated to the platform: every hop is issued with\n * `redirect: \"manual\"` and its `Location` is re-run through\n * {@link assertUrlAllowed} — including the DNS-resolution private-IP check\n * — before being followed (capped at {@link DEFAULT_MAX_REDIRECTS}), so a\n * 3xx from an allowed host cannot smuggle the request to a private /\n * metadata target. Mirrors `@warlock.js/ai`'s `guardedFetch`\n * (ai/src/security/outbound-policy.ts:188-284); duplicated per this\n * module's file-header note since core cannot depend on ai.\n *\n * @throws {StorageError} On a blocked URL (initial or redirect target),\n * a redirect-hop bound overflow, timeout, or oversized body.\n */\nexport async function safeFetchToBuffer(\n rawUrl: string,\n options: SafeFetchOptions = {},\n): Promise<SafeFetchResult> {\n const policy = resolveFetchPolicy(options);\n let url = await assertUrlAllowed(rawUrl, policy);\n\n const controller = new AbortController();\n const timer = setTimeout(() => {\n controller.abort(\n new StorageError(`outbound request timed out after ${policy.timeoutMs}ms`, {\n context: { url: rawUrl, timeoutMs: policy.timeoutMs },\n }),\n );\n }, policy.timeoutMs);\n\n try {\n let response: Response;\n\n for (let hop = 0; ; hop++) {\n response = await policy.fetch(url, { signal: controller.signal, redirect: \"manual\" });\n\n const location = response.headers.get(\"location\");\n if (!REDIRECT_STATUSES.has(response.status) || location === null) {\n break;\n }\n\n if (hop >= DEFAULT_MAX_REDIRECTS) {\n throw new StorageError(\n `outbound request blocked — more than ${DEFAULT_MAX_REDIRECTS} redirects`,\n { context: { url: rawUrl, maxRedirects: DEFAULT_MAX_REDIRECTS } },\n );\n }\n\n let target: URL;\n try {\n target = new URL(location, url);\n } catch {\n throw new StorageError(\n `outbound request blocked — invalid redirect Location: ${location}`,\n {\n context: { url: url.toString(), location },\n },\n );\n }\n\n // Discard the interim body so the connection can be reused.\n if (response.body) {\n await response.body.cancel().catch(() => undefined);\n }\n\n // The redirect target gets the SAME scheme + private-IP validation\n // (incl. DNS resolution) as the initial URL — this is what closes\n // the redirect-based SSRF bypass: a 302 from an allowed host to\n // 169.254.169.254 is re-validated here, not blindly followed.\n url = await assertUrlAllowed(target.toString(), policy);\n }\n\n const declared = Number(response.headers.get(\"content-length\"));\n if (Number.isFinite(declared) && declared > policy.maxBytes) {\n throw new StorageError(\n `outbound response body too large — declared ${declared} bytes exceeds the ${policy.maxBytes}-byte cap`,\n { context: { url: rawUrl, declared, maxBytes: policy.maxBytes } },\n );\n }\n\n const buffer = await readBodyCapped(response, policy.maxBytes, rawUrl);\n\n return {\n buffer,\n contentType: response.headers.get(\"content-type\"),\n status: response.status,\n statusText: response.statusText,\n ok: response.ok,\n };\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Read a response body into a Buffer with a hard byte cap. The stream is\n * read chunk-by-chunk and cancelled the moment the running total exceeds\n * `maxBytes`. Throws {@link StorageError} on overflow.\n */\nasync function readBodyCapped(\n response: Response,\n maxBytes: number,\n rawUrl: string,\n): Promise<Buffer> {\n if (!response.body) {\n const arrayBuffer = await response.arrayBuffer();\n if (arrayBuffer.byteLength > maxBytes) {\n throw new StorageError(`outbound response body exceeded the ${maxBytes}-byte cap`, {\n context: { url: rawUrl, maxBytes },\n });\n }\n return Buffer.from(arrayBuffer);\n }\n\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n if (!value) {\n continue;\n }\n\n total += value.byteLength;\n if (total > maxBytes) {\n await reader.cancel();\n throw new StorageError(`outbound response body exceeded the ${maxBytes}-byte cap`, {\n context: { url: rawUrl, maxBytes },\n });\n }\n chunks.push(value);\n }\n\n return Buffer.concat(chunks);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { StorageError } from "./storage-error.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../core/src/storage/utils/storage-capability-error.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Thrown when an operation needs an optional driver capability that the
|
|
6
|
+
* active driver does not implement (e.g. `putIfAbsent()` on a driver that
|
|
7
|
+
* cannot guarantee an atomic create-if-absent).
|
|
8
|
+
*
|
|
9
|
+
* Callers can probe first (`storage.supportsPutIfAbsent()`) or branch on
|
|
10
|
+
* `instanceof StorageCapabilityError`.
|
|
11
|
+
*/
|
|
12
|
+
declare class StorageCapabilityError extends StorageError {
|
|
13
|
+
/**
|
|
14
|
+
* The missing capability (e.g. "putIfAbsent").
|
|
15
|
+
*/
|
|
16
|
+
readonly capability: string;
|
|
17
|
+
/**
|
|
18
|
+
* Name of the driver that lacks the capability.
|
|
19
|
+
*/
|
|
20
|
+
readonly driverName: string;
|
|
21
|
+
constructor(capability: string, driverName: string);
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { StorageCapabilityError };
|
|
25
|
+
//# sourceMappingURL=storage-capability-error.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage-capability-error.d.mts","names":[],"sources":["../../../../../../../../core/src/storage/utils/storage-capability-error.ts"],"mappings":";;;;;AAUA;;;;;;cAAa,sBAAA,SAA+B,YAAY;;;;WAItC,UAAA;EAOyC;;;EAAA,SAFzC,UAAA;cAEG,UAAA,UAAoB,UAAA;AAAA"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { StorageError } from "./storage-error.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../core/src/storage/utils/storage-capability-error.ts
|
|
4
|
+
/**
|
|
5
|
+
* Thrown when an operation needs an optional driver capability that the
|
|
6
|
+
* active driver does not implement (e.g. `putIfAbsent()` on a driver that
|
|
7
|
+
* cannot guarantee an atomic create-if-absent).
|
|
8
|
+
*
|
|
9
|
+
* Callers can probe first (`storage.supportsPutIfAbsent()`) or branch on
|
|
10
|
+
* `instanceof StorageCapabilityError`.
|
|
11
|
+
*/
|
|
12
|
+
var StorageCapabilityError = class extends StorageError {
|
|
13
|
+
constructor(capability, driverName) {
|
|
14
|
+
super(`Storage driver "${driverName}" does not support "${capability}". Use a driver that implements it, or check supportsPutIfAbsent() first.`, { context: {
|
|
15
|
+
capability,
|
|
16
|
+
driver: driverName
|
|
17
|
+
} });
|
|
18
|
+
this.name = "StorageCapabilityError";
|
|
19
|
+
this.capability = capability;
|
|
20
|
+
this.driverName = driverName;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
export { StorageCapabilityError };
|
|
26
|
+
//# sourceMappingURL=storage-capability-error.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage-capability-error.mjs","names":[],"sources":["../../../../../../../../core/src/storage/utils/storage-capability-error.ts"],"sourcesContent":["import { StorageError } from \"./storage-error\";\n\n/**\n * Thrown when an operation needs an optional driver capability that the\n * active driver does not implement (e.g. `putIfAbsent()` on a driver that\n * cannot guarantee an atomic create-if-absent).\n *\n * Callers can probe first (`storage.supportsPutIfAbsent()`) or branch on\n * `instanceof StorageCapabilityError`.\n */\nexport class StorageCapabilityError extends StorageError {\n /**\n * The missing capability (e.g. \"putIfAbsent\").\n */\n public readonly capability: string;\n\n /**\n * Name of the driver that lacks the capability.\n */\n public readonly driverName: string;\n\n public constructor(capability: string, driverName: string) {\n super(\n `Storage driver \"${driverName}\" does not support \"${capability}\". ` +\n `Use a driver that implements it, or check supportsPutIfAbsent() first.`,\n { context: { capability, driver: driverName } },\n );\n this.name = \"StorageCapabilityError\";\n this.capability = capability;\n this.driverName = driverName;\n }\n}\n"],"mappings":";;;;;;;;;;;AAUA,IAAa,yBAAb,cAA4C,aAAa;CAWvD,AAAO,YAAY,YAAoB,YAAoB;EACzD,MACE,mBAAmB,WAAW,sBAAsB,WAAW,4EAE/D,EAAE,SAAS;GAAE;GAAY,QAAQ;EAAW,EAAE,CAChD;EACA,KAAK,OAAO;EACZ,KAAK,aAAa;EAClB,KAAK,aAAa;CACpB;AACF"}
|
|
@@ -54,6 +54,14 @@ async function updateWarlockPackages(options = {}) {
|
|
|
54
54
|
updates: []
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
+
const failedLookups = resolved.filter((dependency) => !dependency.latest);
|
|
58
|
+
if (failedLookups.length > 0) {
|
|
59
|
+
console.log(`${colors.yellow("⚠")} Could not look up ${failedLookups.map((dependency) => dependency.name).join(", ")} — no versions were changed, to keep @warlock.js packages in lockstep. Try again.`);
|
|
60
|
+
return {
|
|
61
|
+
outcome: "registry-unreachable",
|
|
62
|
+
updates: []
|
|
63
|
+
};
|
|
64
|
+
}
|
|
57
65
|
const updates = resolvePackageUpdates(resolved);
|
|
58
66
|
if (updates.length === 0) {
|
|
59
67
|
console.log(`${colors.green("✓")} All @warlock.js packages are already up to date.`);
|
|
@@ -154,9 +162,11 @@ function resolvePackageUpdates(resolved) {
|
|
|
154
162
|
function parseSpec(spec) {
|
|
155
163
|
const match = spec.trim().match(/^(\^|~)?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/);
|
|
156
164
|
if (!match) return;
|
|
165
|
+
const version = match[2];
|
|
166
|
+
if (version === void 0) return;
|
|
157
167
|
return {
|
|
158
168
|
operator: match[1] ?? "",
|
|
159
|
-
version
|
|
169
|
+
version
|
|
160
170
|
};
|
|
161
171
|
}
|
|
162
172
|
/** Write the resolved bumps back into the package.json object in place. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update-warlock-packages.mjs","names":[],"sources":["../../../../../../../core/src/updater/update-warlock-packages.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { fileExistsAsync, getJsonFileAsync, putJsonFileAsync } from \"@warlock.js/fs\";\nimport { execSync } from \"node:child_process\";\nimport { rootPath } from \"../utils\";\nimport { fetchLatestVersion } from \"../utils/npm-registry\";\nimport { isNewerVersion } from \"../utils/version-compare\";\nimport { detectPackageManager, getInstallCommand } from \"./package-manager\";\n\n/** Scope prefix that identifies a Warlock framework package. */\nconst WARLOCK_SCOPE = \"@warlock.js/\";\n\n/** The dependency maps in package.json we update, in display order. */\nconst DEPENDENCY_SECTIONS = [\"dependencies\", \"devDependencies\"] as const;\n\ntype DependencySection = (typeof DEPENDENCY_SECTIONS)[number];\n\ntype DependencyMap = Record<string, string>;\n\ntype PackageJson = Record<string, unknown> & {\n dependencies?: DependencyMap;\n devDependencies?: DependencyMap;\n};\n\n/** A `@warlock.js/*` dependency found in package.json. */\ntype WarlockDependency = {\n name: string;\n section: DependencySection;\n /** The current spec exactly as written, e.g. `^4.2.0`. */\n current: string;\n};\n\n/** A dependency whose latest version has been resolved from the registry. */\nexport type ResolvedDependency = WarlockDependency & {\n /** Latest version from npm, or `undefined` when the lookup failed. */\n latest: string | undefined;\n};\n\n/** A concrete version bump to write back into package.json. */\nexport type PackageUpdate = {\n name: string;\n section: DependencySection;\n /** Existing spec, e.g. `^4.2.0`. */\n from: string;\n /** New spec with the original range operator preserved, e.g. `^4.3.0`. */\n to: string;\n};\n\n/** Options accepted by {@link updateWarlockPackages}. */\nexport type UpdateWarlockPackagesOptions = {\n /** Run the package manager install after rewriting versions (default true). */\n install?: boolean;\n /**\n * Report what would change without touching package.json or node_modules.\n * Resolves with the `outdated` outcome instead of `updated`.\n */\n dryRun?: boolean;\n};\n\n/**\n * How an update run ended. Callers branch on this instead of re-deriving the\n * situation from the printed output — the dev-server `u` shortcut needs to\n * know whether restarting is warranted, and whether it may offer a retry.\n */\nexport type UpdateOutcome =\n /** No package.json at the project root — nothing to do. */\n | \"no-package-json\"\n /** package.json carries no `@warlock.js/*` dependency. */\n | \"no-packages\"\n /** Every registry lookup failed — offline, DNS down, or npm unreachable. */\n | \"registry-unreachable\"\n /** Every package is already at (or ahead of) its latest published version. */\n | \"up-to-date\"\n /** Newer versions exist, and a dry run left everything untouched. */\n | \"outdated\"\n /** Versions were rewritten (and installed, unless `install: false`). */\n | \"updated\"\n /** Versions were rewritten but the package manager install failed. */\n | \"install-failed\";\n\n/** The result of an update run. */\nexport type UpdateWarlockPackagesResult = {\n outcome: UpdateOutcome;\n /** The bumps written to package.json — empty unless `outcome` wrote any. */\n updates: PackageUpdate[];\n /** The install failure, present only for `install-failed`. */\n error?: Error;\n};\n\n/**\n * Update every `@warlock.js/*` package listed in the project's root\n * package.json to its latest published version, then reconcile `node_modules`\n * via the detected package manager.\n *\n * The original range operator on each dependency (`^`, `~`, or an exact pin)\n * is preserved; specs that aren't a plain semver — `workspace:*`, `*`,\n * `latest`, git/file URLs — are left untouched. Only genuine upgrades are\n * written, so re-running on an already-current project is a no-op.\n *\n * Resolves rather than throws on the two failures a caller is expected to\n * survive — an unreachable registry and a failed install — so the dev-server\n * shortcut can report them and keep serving.\n */\nexport async function updateWarlockPackages(\n options: UpdateWarlockPackagesOptions = {},\n): Promise<UpdateWarlockPackagesResult> {\n const runInstall = options.install ?? true;\n const packageJsonPath = rootPath(\"package.json\");\n\n if (!(await fileExistsAsync(packageJsonPath))) {\n console.log(`${colors.red(\"✖\")} No package.json found at the project root.`);\n return { outcome: \"no-package-json\", updates: [] };\n }\n\n const packageJson = (await getJsonFileAsync(packageJsonPath)) as PackageJson;\n const dependencies = collectWarlockDependencies(packageJson);\n\n if (dependencies.length === 0) {\n console.log(`${colors.yellow(\"⚠\")} No @warlock.js packages found in package.json.`);\n return { outcome: \"no-packages\", updates: [] };\n }\n\n console.log(\n `${colors.cyan(\"›\")} Checking ${colors.bold(String(dependencies.length))} ` +\n `@warlock.js package(s) for updates…`,\n );\n\n const resolved = await resolveLatestVersions(dependencies);\n\n // Distinguish \"nothing newer\" from \"we never got an answer\". Without this,\n // an offline run reports every package as up to date, which is a lie.\n if (isRegistryUnreachable(resolved)) {\n console.log(\n `${colors.yellow(\"⚠\")} Could not reach the npm registry — ` +\n `no versions were changed. Check your connection and try again.`,\n );\n return { outcome: \"registry-unreachable\", updates: [] };\n }\n\n const updates = resolvePackageUpdates(resolved);\n\n if (updates.length === 0) {\n console.log(`${colors.green(\"✓\")} All @warlock.js packages are already up to date.`);\n return { outcome: \"up-to-date\", updates: [] };\n }\n\n if (options.dryRun) {\n printUpdates(updates, { pending: true });\n return { outcome: \"outdated\", updates };\n }\n\n applyUpdates(packageJson, updates);\n await putJsonFileAsync(packageJsonPath, packageJson);\n\n printUpdates(updates);\n\n if (!runInstall) {\n console.log(\n colors.dim(\"Skipped install (--no-install). Run your package manager to apply the changes.\"),\n );\n return { outcome: \"updated\", updates };\n }\n\n try {\n await installDependencies();\n } catch (error) {\n console.log(\n `${colors.red(\"✖\")} The install failed. package.json is already updated — ` +\n `re-run your package manager install to finish.`,\n );\n return { outcome: \"install-failed\", updates, error: error as Error };\n }\n\n return { outcome: \"updated\", updates };\n}\n\n/**\n * Whether *every* lookup came back empty, which in practice means the\n * registry was never reached. A single failed package (unpublished, renamed,\n * private) leaves the rest resolvable and is not treated as an outage.\n */\nexport function isRegistryUnreachable(resolved: ResolvedDependency[]): boolean {\n return resolved.length > 0 && resolved.every((dependency) => !dependency.latest);\n}\n\n/** Collect every `@warlock.js/*` dependency across the relevant sections. */\nfunction collectWarlockDependencies(packageJson: PackageJson): WarlockDependency[] {\n const dependencies: WarlockDependency[] = [];\n\n for (const section of DEPENDENCY_SECTIONS) {\n const map = packageJson[section];\n\n if (!map) {\n continue;\n }\n\n for (const [name, current] of Object.entries(map)) {\n if (name.startsWith(WARLOCK_SCOPE)) {\n dependencies.push({ name, section, current });\n }\n }\n }\n\n return dependencies;\n}\n\n/** Resolve each dependency's latest version from the registry in parallel. */\nasync function resolveLatestVersions(\n dependencies: WarlockDependency[],\n): Promise<ResolvedDependency[]> {\n return Promise.all(\n dependencies.map(async (dependency) => ({\n ...dependency,\n latest: await fetchLatestVersion(dependency.name),\n })),\n );\n}\n\n/**\n * Turn resolved dependencies into the concrete set of version bumps to write.\n * Pure (no I/O) so it can be unit-tested directly: skips lookups that failed,\n * non-semver specs, and anything already at (or ahead of) the latest version,\n * and preserves the original range operator on everything it does rewrite.\n */\nexport function resolvePackageUpdates(resolved: ResolvedDependency[]): PackageUpdate[] {\n const updates: PackageUpdate[] = [];\n\n for (const dependency of resolved) {\n if (!dependency.latest) {\n continue;\n }\n\n const parsed = parseSpec(dependency.current);\n\n if (!parsed) {\n continue;\n }\n\n if (!isNewerVersion(dependency.latest, parsed.version)) {\n continue;\n }\n\n updates.push({\n name: dependency.name,\n section: dependency.section,\n from: dependency.current,\n to: `${parsed.operator}${dependency.latest}`,\n });\n }\n\n return updates;\n}\n\n/**\n * Split a dependency spec into its range operator and concrete version.\n * Returns `undefined` for specs we won't touch — `workspace:*`, `*`,\n * `latest`, multi-part ranges, or git/file URLs — leaving them exactly as\n * the author wrote them.\n */\nfunction parseSpec(spec: string): { operator: string; version: string } | undefined {\n const match = spec.trim().match(/^(\\^|~)?(\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?)$/);\n\n if (!match) {\n return undefined;\n }\n\n return { operator: match[1] ?? \"\", version: match[2] };\n}\n\n/** Write the resolved bumps back into the package.json object in place. */\nfunction applyUpdates(packageJson: PackageJson, updates: PackageUpdate[]): void {\n for (const update of updates) {\n const map = packageJson[update.section];\n\n if (map) {\n map[update.name] = update.to;\n }\n }\n}\n\n/** Print the version bumps — applied, or pending on a dry run. */\nfunction printUpdates(updates: PackageUpdate[], { pending = false } = {}): void {\n const headline = pending\n ? `${colors.yellow(\"↑\")} ${colors.bold(String(updates.length))} package(s) can be updated:`\n : `${colors.green(\"✓\")} Updated ${colors.bold(String(updates.length))} package(s):`;\n\n console.log(headline);\n\n for (const update of updates) {\n console.log(\n ` ${colors.cyan(update.name)} ` +\n `${colors.dim(update.from)} ${colors.dim(\"→\")} ${colors.greenBright(update.to)}`,\n );\n }\n}\n\n/** Reconcile node_modules to the rewritten package.json via the project PM. */\nasync function installDependencies(): Promise<void> {\n const packageManager = await detectPackageManager();\n const installCommand = getInstallCommand(packageManager);\n\n console.log(`${colors.cyan(\"›\")} Running ${colors.bold(installCommand)}…`);\n\n execSync(installCommand, { cwd: process.cwd(), stdio: \"inherit\" });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"update-warlock-packages.mjs","names":[],"sources":["../../../../../../../core/src/updater/update-warlock-packages.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { fileExistsAsync, getJsonFileAsync, putJsonFileAsync } from \"@warlock.js/fs\";\nimport { execSync } from \"node:child_process\";\nimport { rootPath } from \"../utils\";\nimport { fetchLatestVersion } from \"../utils/npm-registry\";\nimport { isNewerVersion } from \"../utils/version-compare\";\nimport { detectPackageManager, getInstallCommand } from \"./package-manager\";\n\n/** Scope prefix that identifies a Warlock framework package. */\nconst WARLOCK_SCOPE = \"@warlock.js/\";\n\n/** The dependency maps in package.json we update, in display order. */\nconst DEPENDENCY_SECTIONS = [\"dependencies\", \"devDependencies\"] as const;\n\ntype DependencySection = (typeof DEPENDENCY_SECTIONS)[number];\n\ntype DependencyMap = Record<string, string>;\n\ntype PackageJson = Record<string, unknown> & {\n dependencies?: DependencyMap;\n devDependencies?: DependencyMap;\n};\n\n/** A `@warlock.js/*` dependency found in package.json. */\ntype WarlockDependency = {\n name: string;\n section: DependencySection;\n /** The current spec exactly as written, e.g. `^4.2.0`. */\n current: string;\n};\n\n/** A dependency whose latest version has been resolved from the registry. */\nexport type ResolvedDependency = WarlockDependency & {\n /** Latest version from npm, or `undefined` when the lookup failed. */\n latest: string | undefined;\n};\n\n/** A concrete version bump to write back into package.json. */\nexport type PackageUpdate = {\n name: string;\n section: DependencySection;\n /** Existing spec, e.g. `^4.2.0`. */\n from: string;\n /** New spec with the original range operator preserved, e.g. `^4.3.0`. */\n to: string;\n};\n\n/** Options accepted by {@link updateWarlockPackages}. */\nexport type UpdateWarlockPackagesOptions = {\n /** Run the package manager install after rewriting versions (default true). */\n install?: boolean;\n /**\n * Report what would change without touching package.json or node_modules.\n * Resolves with the `outdated` outcome instead of `updated`.\n */\n dryRun?: boolean;\n};\n\n/**\n * How an update run ended. Callers branch on this instead of re-deriving the\n * situation from the printed output — the dev-server `u` shortcut needs to\n * know whether restarting is warranted, and whether it may offer a retry.\n */\nexport type UpdateOutcome =\n /** No package.json at the project root — nothing to do. */\n | \"no-package-json\"\n /** package.json carries no `@warlock.js/*` dependency. */\n | \"no-packages\"\n /** Every registry lookup failed — offline, DNS down, or npm unreachable. */\n | \"registry-unreachable\"\n /** Every package is already at (or ahead of) its latest published version. */\n | \"up-to-date\"\n /** Newer versions exist, and a dry run left everything untouched. */\n | \"outdated\"\n /** Versions were rewritten (and installed, unless `install: false`). */\n | \"updated\"\n /** Versions were rewritten but the package manager install failed. */\n | \"install-failed\";\n\n/** The result of an update run. */\nexport type UpdateWarlockPackagesResult = {\n outcome: UpdateOutcome;\n /** The bumps written to package.json — empty unless `outcome` wrote any. */\n updates: PackageUpdate[];\n /** The install failure, present only for `install-failed`. */\n error?: Error;\n};\n\n/**\n * Update every `@warlock.js/*` package listed in the project's root\n * package.json to its latest published version, then reconcile `node_modules`\n * via the detected package manager.\n *\n * The original range operator on each dependency (`^`, `~`, or an exact pin)\n * is preserved; specs that aren't a plain semver — `workspace:*`, `*`,\n * `latest`, git/file URLs — are left untouched. Only genuine upgrades are\n * written, so re-running on an already-current project is a no-op.\n *\n * Resolves rather than throws on the two failures a caller is expected to\n * survive — an unreachable registry and a failed install — so the dev-server\n * shortcut can report them and keep serving.\n */\nexport async function updateWarlockPackages(\n options: UpdateWarlockPackagesOptions = {},\n): Promise<UpdateWarlockPackagesResult> {\n const runInstall = options.install ?? true;\n const packageJsonPath = rootPath(\"package.json\");\n\n if (!(await fileExistsAsync(packageJsonPath))) {\n console.log(`${colors.red(\"✖\")} No package.json found at the project root.`);\n return { outcome: \"no-package-json\", updates: [] };\n }\n\n const packageJson = (await getJsonFileAsync(packageJsonPath)) as PackageJson;\n const dependencies = collectWarlockDependencies(packageJson);\n\n if (dependencies.length === 0) {\n console.log(`${colors.yellow(\"⚠\")} No @warlock.js packages found in package.json.`);\n return { outcome: \"no-packages\", updates: [] };\n }\n\n console.log(\n `${colors.cyan(\"›\")} Checking ${colors.bold(String(dependencies.length))} ` +\n `@warlock.js package(s) for updates…`,\n );\n\n const resolved = await resolveLatestVersions(dependencies);\n\n // Distinguish \"nothing newer\" from \"we never got an answer\". Without this,\n // an offline run reports every package as up to date, which is a lie.\n if (isRegistryUnreachable(resolved)) {\n console.log(\n `${colors.yellow(\"⚠\")} Could not reach the npm registry — ` +\n `no versions were changed. Check your connection and try again.`,\n );\n return { outcome: \"registry-unreachable\", updates: [] };\n }\n\n // A partial failure would bump some family members and leave others behind,\n // a lockstep drift reported as success. Refuse the whole run instead.\n const failedLookups = resolved.filter((dependency) => !dependency.latest);\n\n if (failedLookups.length > 0) {\n console.log(\n `${colors.yellow(\"⚠\")} Could not look up ${failedLookups.map((dependency) => dependency.name).join(\", \")} — ` +\n `no versions were changed, to keep @warlock.js packages in lockstep. Try again.`,\n );\n return { outcome: \"registry-unreachable\", updates: [] };\n }\n\n const updates = resolvePackageUpdates(resolved);\n\n if (updates.length === 0) {\n console.log(`${colors.green(\"✓\")} All @warlock.js packages are already up to date.`);\n return { outcome: \"up-to-date\", updates: [] };\n }\n\n if (options.dryRun) {\n printUpdates(updates, { pending: true });\n return { outcome: \"outdated\", updates };\n }\n\n applyUpdates(packageJson, updates);\n await putJsonFileAsync(packageJsonPath, packageJson);\n\n printUpdates(updates);\n\n if (!runInstall) {\n console.log(\n colors.dim(\"Skipped install (--no-install). Run your package manager to apply the changes.\"),\n );\n return { outcome: \"updated\", updates };\n }\n\n try {\n await installDependencies();\n } catch (error) {\n console.log(\n `${colors.red(\"✖\")} The install failed. package.json is already updated — ` +\n `re-run your package manager install to finish.`,\n );\n return { outcome: \"install-failed\", updates, error: error as Error };\n }\n\n return { outcome: \"updated\", updates };\n}\n\n/**\n * Whether *every* lookup came back empty, which in practice means the\n * registry was never reached. A single failed package (unpublished, renamed,\n * private) leaves the rest resolvable and is not treated as an outage.\n */\nexport function isRegistryUnreachable(resolved: ResolvedDependency[]): boolean {\n return resolved.length > 0 && resolved.every((dependency) => !dependency.latest);\n}\n\n/** Collect every `@warlock.js/*` dependency across the relevant sections. */\nfunction collectWarlockDependencies(packageJson: PackageJson): WarlockDependency[] {\n const dependencies: WarlockDependency[] = [];\n\n for (const section of DEPENDENCY_SECTIONS) {\n const map = packageJson[section];\n\n if (!map) {\n continue;\n }\n\n for (const [name, current] of Object.entries(map)) {\n if (name.startsWith(WARLOCK_SCOPE)) {\n dependencies.push({ name, section, current });\n }\n }\n }\n\n return dependencies;\n}\n\n/** Resolve each dependency's latest version from the registry in parallel. */\nasync function resolveLatestVersions(\n dependencies: WarlockDependency[],\n): Promise<ResolvedDependency[]> {\n return Promise.all(\n dependencies.map(async (dependency) => ({\n ...dependency,\n latest: await fetchLatestVersion(dependency.name),\n })),\n );\n}\n\n/**\n * Turn resolved dependencies into the concrete set of version bumps to write.\n * Pure (no I/O) so it can be unit-tested directly: skips lookups that failed,\n * non-semver specs, and anything already at (or ahead of) the latest version,\n * and preserves the original range operator on everything it does rewrite.\n */\nexport function resolvePackageUpdates(resolved: ResolvedDependency[]): PackageUpdate[] {\n const updates: PackageUpdate[] = [];\n\n for (const dependency of resolved) {\n if (!dependency.latest) {\n continue;\n }\n\n const parsed = parseSpec(dependency.current);\n\n if (!parsed) {\n continue;\n }\n\n if (!isNewerVersion(dependency.latest, parsed.version)) {\n continue;\n }\n\n updates.push({\n name: dependency.name,\n section: dependency.section,\n from: dependency.current,\n to: `${parsed.operator}${dependency.latest}`,\n });\n }\n\n return updates;\n}\n\n/**\n * Split a dependency spec into its range operator and concrete version.\n * Returns `undefined` for specs we won't touch — `workspace:*`, `*`,\n * `latest`, multi-part ranges, or git/file URLs — leaving them exactly as\n * the author wrote them.\n */\nfunction parseSpec(spec: string): { operator: string; version: string } | undefined {\n const match = spec.trim().match(/^(\\^|~)?(\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?)$/);\n\n if (!match) {\n return undefined;\n }\n\n const version = match[2];\n\n if (version === undefined) {\n return undefined;\n }\n\n return { operator: match[1] ?? \"\", version };\n}\n\n/** Write the resolved bumps back into the package.json object in place. */\nfunction applyUpdates(packageJson: PackageJson, updates: PackageUpdate[]): void {\n for (const update of updates) {\n const map = packageJson[update.section];\n\n if (map) {\n map[update.name] = update.to;\n }\n }\n}\n\n/** Print the version bumps — applied, or pending on a dry run. */\nfunction printUpdates(updates: PackageUpdate[], { pending = false } = {}): void {\n const headline = pending\n ? `${colors.yellow(\"↑\")} ${colors.bold(String(updates.length))} package(s) can be updated:`\n : `${colors.green(\"✓\")} Updated ${colors.bold(String(updates.length))} package(s):`;\n\n console.log(headline);\n\n for (const update of updates) {\n console.log(\n ` ${colors.cyan(update.name)} ` +\n `${colors.dim(update.from)} ${colors.dim(\"→\")} ${colors.greenBright(update.to)}`,\n );\n }\n}\n\n/** Reconcile node_modules to the rewritten package.json via the project PM. */\nasync function installDependencies(): Promise<void> {\n const packageManager = await detectPackageManager();\n const installCommand = getInstallCommand(packageManager);\n\n console.log(`${colors.cyan(\"›\")} Running ${colors.bold(installCommand)}…`);\n\n execSync(installCommand, { cwd: process.cwd(), stdio: \"inherit\" });\n}\n"],"mappings":";;;;;;;;;;;AASA,MAAM,gBAAgB;;AAGtB,MAAM,sBAAsB,CAAC,gBAAgB,iBAAiB;;;;;;;;;;;;;;;AA0F9D,eAAsB,sBACpB,UAAwC,CAAC,GACH;CACtC,MAAM,aAAa,QAAQ,WAAW;CACtC,MAAM,kBAAkB,SAAS,cAAc;CAE/C,IAAI,CAAE,MAAM,gBAAgB,eAAe,GAAI;EAC7C,QAAQ,IAAI,GAAG,OAAO,IAAI,GAAG,EAAE,4CAA4C;EAC3E,OAAO;GAAE,SAAS;GAAmB,SAAS,CAAC;EAAE;CACnD;CAEA,MAAM,cAAe,MAAM,iBAAiB,eAAe;CAC3D,MAAM,eAAe,2BAA2B,WAAW;CAE3D,IAAI,aAAa,WAAW,GAAG;EAC7B,QAAQ,IAAI,GAAG,OAAO,OAAO,GAAG,EAAE,gDAAgD;EAClF,OAAO;GAAE,SAAS;GAAe,SAAS,CAAC;EAAE;CAC/C;CAEA,QAAQ,IACN,GAAG,OAAO,KAAK,GAAG,EAAE,YAAY,OAAO,KAAK,OAAO,aAAa,MAAM,CAAC,EAAE,qCAE3E;CAEA,MAAM,WAAW,MAAM,sBAAsB,YAAY;CAIzD,IAAI,sBAAsB,QAAQ,GAAG;EACnC,QAAQ,IACN,GAAG,OAAO,OAAO,GAAG,EAAE,mGAExB;EACA,OAAO;GAAE,SAAS;GAAwB,SAAS,CAAC;EAAE;CACxD;CAIA,MAAM,gBAAgB,SAAS,QAAQ,eAAe,CAAC,WAAW,MAAM;CAExE,IAAI,cAAc,SAAS,GAAG;EAC5B,QAAQ,IACN,GAAG,OAAO,OAAO,GAAG,EAAE,qBAAqB,cAAc,KAAK,eAAe,WAAW,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,kFAE3G;EACA,OAAO;GAAE,SAAS;GAAwB,SAAS,CAAC;EAAE;CACxD;CAEA,MAAM,UAAU,sBAAsB,QAAQ;CAE9C,IAAI,QAAQ,WAAW,GAAG;EACxB,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,kDAAkD;EACnF,OAAO;GAAE,SAAS;GAAc,SAAS,CAAC;EAAE;CAC9C;CAEA,IAAI,QAAQ,QAAQ;EAClB,aAAa,SAAS,EAAE,SAAS,KAAK,CAAC;EACvC,OAAO;GAAE,SAAS;GAAY;EAAQ;CACxC;CAEA,aAAa,aAAa,OAAO;CACjC,MAAM,iBAAiB,iBAAiB,WAAW;CAEnD,aAAa,OAAO;CAEpB,IAAI,CAAC,YAAY;EACf,QAAQ,IACN,OAAO,IAAI,gFAAgF,CAC7F;EACA,OAAO;GAAE,SAAS;GAAW;EAAQ;CACvC;CAEA,IAAI;EACF,MAAM,oBAAoB;CAC5B,SAAS,OAAO;EACd,QAAQ,IACN,GAAG,OAAO,IAAI,GAAG,EAAE,sGAErB;EACA,OAAO;GAAE,SAAS;GAAkB;GAAgB;EAAe;CACrE;CAEA,OAAO;EAAE,SAAS;EAAW;CAAQ;AACvC;;;;;;AAOA,SAAgB,sBAAsB,UAAyC;CAC7E,OAAO,SAAS,SAAS,KAAK,SAAS,OAAO,eAAe,CAAC,WAAW,MAAM;AACjF;;AAGA,SAAS,2BAA2B,aAA+C;CACjF,MAAM,eAAoC,CAAC;CAE3C,KAAK,MAAM,WAAW,qBAAqB;EACzC,MAAM,MAAM,YAAY;EAExB,IAAI,CAAC,KACH;EAGF,KAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,GAAG,GAC9C,IAAI,KAAK,WAAW,aAAa,GAC/B,aAAa,KAAK;GAAE;GAAM;GAAS;EAAQ,CAAC;CAGlD;CAEA,OAAO;AACT;;AAGA,eAAe,sBACb,cAC+B;CAC/B,OAAO,QAAQ,IACb,aAAa,IAAI,OAAO,gBAAgB;EACtC,GAAG;EACH,QAAQ,MAAM,mBAAmB,WAAW,IAAI;CAClD,EAAE,CACJ;AACF;;;;;;;AAQA,SAAgB,sBAAsB,UAAiD;CACrF,MAAM,UAA2B,CAAC;CAElC,KAAK,MAAM,cAAc,UAAU;EACjC,IAAI,CAAC,WAAW,QACd;EAGF,MAAM,SAAS,UAAU,WAAW,OAAO;EAE3C,IAAI,CAAC,QACH;EAGF,IAAI,CAAC,eAAe,WAAW,QAAQ,OAAO,OAAO,GACnD;EAGF,QAAQ,KAAK;GACX,MAAM,WAAW;GACjB,SAAS,WAAW;GACpB,MAAM,WAAW;GACjB,IAAI,GAAG,OAAO,WAAW,WAAW;EACtC,CAAC;CACH;CAEA,OAAO;AACT;;;;;;;AAQA,SAAS,UAAU,MAAiE;CAClF,MAAM,QAAQ,KAAK,KAAK,CAAC,CAAC,MAAM,8CAA8C;CAE9E,IAAI,CAAC,OACH;CAGF,MAAM,UAAU,MAAM;CAEtB,IAAI,YAAY,QACd;CAGF,OAAO;EAAE,UAAU,MAAM,MAAM;EAAI;CAAQ;AAC7C;;AAGA,SAAS,aAAa,aAA0B,SAAgC;CAC9E,KAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,MAAM,YAAY,OAAO;EAE/B,IAAI,KACF,IAAI,OAAO,QAAQ,OAAO;CAE9B;AACF;;AAGA,SAAS,aAAa,SAA0B,EAAE,UAAU,UAAU,CAAC,GAAS;CAC9E,MAAM,WAAW,UACb,GAAG,OAAO,OAAO,GAAG,EAAE,GAAG,OAAO,KAAK,OAAO,QAAQ,MAAM,CAAC,EAAE,+BAC7D,GAAG,OAAO,MAAM,GAAG,EAAE,WAAW,OAAO,KAAK,OAAO,QAAQ,MAAM,CAAC,EAAE;CAExE,QAAQ,IAAI,QAAQ;CAEpB,KAAK,MAAM,UAAU,SACnB,QAAQ,IACN,MAAM,OAAO,KAAK,OAAO,IAAI,EAAE,IAC1B,OAAO,IAAI,OAAO,IAAI,EAAE,GAAG,OAAO,IAAI,GAAG,EAAE,GAAG,OAAO,YAAY,OAAO,EAAE,GACjF;AAEJ;;AAGA,eAAe,sBAAqC;CAElD,MAAM,iBAAiB,kBAAkB,MADZ,qBAAqB,CACK;CAEvD,QAAQ,IAAI,GAAG,OAAO,KAAK,GAAG,EAAE,WAAW,OAAO,KAAK,cAAc,EAAE,EAAE;CAEzE,SAAS,gBAAgB;EAAE,KAAK,QAAQ,IAAI;EAAG,OAAO;CAAU,CAAC;AACnE"}
|