From b0d1dbc8b1d199bc2af8dec8504feeea26bdc99a Mon Sep 17 00:00:00 2001 From: Karthik Chandrasekaran Date: Sat, 19 Sep 2026 08:31:42 -0500 Subject: [PATCH 1/6] Add configurable Azure, S3 and private IPFS storage adapters Add runtime identities, secret-source configuration, storage contract and integration tests, isolated emulators, examples and draft deployment guidance. Refs #1. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fc4430e2-35f5-48b8-bd0f-5dc6fad2a0ff --- .env.example | 32 + .github/workflows/storage-ci.yml | 40 + .github/workflows/storage-cloud-smoke.yml | 56 + .gitignore | 11 + README.md | 13 +- docs/ARCHITECTURE.md | 9 +- docs/DISTRIBUTED_STORAGE.md | 85 + docs/README.md | 15 +- docs/STORAGE_DESIGN.md | 102 + docs/STORAGE_QUICKSTART.md | 130 + docs/THREAT_MODEL.md | 7 + examples/README.md | 9 + examples/configured-storage.ts | 58 + package-lock.json | 9580 ++++++++++++++++--- package.json | 12 + scripts/run-azurite-integration.ts | 165 + scripts/run-ipfs-integration.ts | 245 + src/index.ts | 12 + src/storage/azure-blob-store.ts | 99 + src/storage/configuration.ts | 122 + src/storage/configured-blob-store.ts | 129 + src/storage/distributed-blob-store.ts | 113 + src/storage/ipfs-cluster-client.ts | 240 + src/storage/s3-blob-store.ts | 140 + src/storage/storage-validation.ts | 164 + src/web/server.ts | 28 +- test/azure-blob-store.spec.ts | 212 + test/blob-store-contract.spec.ts | 24 + test/configuration.spec.ts | 125 + test/configured-blob-store.spec.ts | 106 + test/distributed-blob-store.spec.ts | 180 + test/fixtures/ipfs/.gitattributes | 1 + test/fixtures/ipfs/README.md | 94 + test/fixtures/ipfs/cluster.sh | 7 + test/fixtures/ipfs/fixture.spec.ts | 70 + test/fixtures/ipfs/fixture.ts | 61 + test/fixtures/ipfs/kubo.sh | 22 + test/helpers/blob-store-contract.ts | 91 + test/integration/configured-storage.spec.ts | 93 + test/ipfs-cluster-client.spec.ts | 228 + test/s3-blob-store.spec.ts | 236 + test/web-server.spec.ts | 38 +- tsconfig.json | 2 +- vitest.config.ts | 3 +- vitest.integration.config.ts | 12 + 45 files changed, 11884 insertions(+), 1337 deletions(-) create mode 100644 .env.example create mode 100644 .github/workflows/storage-ci.yml create mode 100644 .github/workflows/storage-cloud-smoke.yml create mode 100644 docs/DISTRIBUTED_STORAGE.md create mode 100644 docs/STORAGE_DESIGN.md create mode 100644 docs/STORAGE_QUICKSTART.md create mode 100644 examples/configured-storage.ts create mode 100644 scripts/run-azurite-integration.ts create mode 100644 scripts/run-ipfs-integration.ts create mode 100644 src/storage/azure-blob-store.ts create mode 100644 src/storage/configuration.ts create mode 100644 src/storage/configured-blob-store.ts create mode 100644 src/storage/distributed-blob-store.ts create mode 100644 src/storage/ipfs-cluster-client.ts create mode 100644 src/storage/s3-blob-store.ts create mode 100644 src/storage/storage-validation.ts create mode 100644 test/azure-blob-store.spec.ts create mode 100644 test/blob-store-contract.spec.ts create mode 100644 test/configuration.spec.ts create mode 100644 test/configured-blob-store.spec.ts create mode 100644 test/distributed-blob-store.spec.ts create mode 100644 test/fixtures/ipfs/.gitattributes create mode 100644 test/fixtures/ipfs/README.md create mode 100644 test/fixtures/ipfs/cluster.sh create mode 100644 test/fixtures/ipfs/fixture.spec.ts create mode 100644 test/fixtures/ipfs/fixture.ts create mode 100644 test/fixtures/ipfs/kubo.sh create mode 100644 test/helpers/blob-store-contract.ts create mode 100644 test/integration/configured-storage.spec.ts create mode 100644 test/ipfs-cluster-client.spec.ts create mode 100644 test/s3-blob-store.spec.ts create mode 100644 vitest.integration.config.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c6b4c23 --- /dev/null +++ b/.env.example @@ -0,0 +1,32 @@ +# Global Patient Record Project: NONSECRET SETTINGS ONLY. +# Copy to .env.local (ignored); use Node --env-file, or set process environment. +# Never put account keys, SAS URLs, private keys, tokens, or real patient data here. +STORAGE_BACKEND=file +STORAGE_FILE_DIRECTORY=.data/web +STORAGE_EMULATOR=false + +# Azure: DefaultAzureCredential (az login, managed identity, or workload identity). +# AZURE_STORAGE_ACCOUNT_URL=https://REPLACE.blob.core.windows.net +# AZURE_STORAGE_CONTAINER=synthetic-records + +# AWS: default SDK credential chain (aws sso login or a workload role). +# S3_BUCKET=REPLACE +# AWS_REGION=us-east-1 +# STORAGE_PREFIX=synthetic-integration/ +# S3_KMS_KEY_ID=REPLACE_WITH_KEY_ARN + +# IPFS: private Kubo and Cluster APIs via authenticated HTTPS reverse proxies. +# IPFS_PRIVATE_NETWORK=true +# IPFS_API_URL=https://REPLACE +# IPFS_CLUSTER_API_URL=https://REPLACE +# IPFS_METADATA_BACKEND=azure +# IPFS_REPLICATION_MIN=2 +# IPFS_REPLICATION_MAX=3 +# IPFS_TIMEOUT_MS=30000 +# Authorization headers live OUTSIDE the repo in mounted secret files: +# IPFS_API_AUTH_FILE=REPLACE_WITH_ABSOLUTE_SECRET_FILE_PATH +# IPFS_CLUSTER_AUTH_FILE=REPLACE_WITH_ABSOLUTE_SECRET_FILE_PATH +# OR retrieve them from Key Vault with the same Azure workload identity: +# AZURE_KEY_VAULT_URL=https://REPLACE.vault.azure.net +# IPFS_API_AUTH_KEYVAULT_SECRET=ipfs-api-auth +# IPFS_CLUSTER_AUTH_KEYVAULT_SECRET=ipfs-cluster-auth diff --git a/.github/workflows/storage-ci.yml b/.github/workflows/storage-ci.yml new file mode 100644 index 0000000..8d71525 --- /dev/null +++ b/.github/workflows/storage-ci.yml @@ -0,0 +1,40 @@ +name: Storage CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + tests: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: "22" + cache: npm + - run: npm ci + - run: npm test + - run: npm run typecheck + - run: npm run build + - run: npm run test:integration:azure + private-ipfs: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: "22" + cache: npm + - run: npm ci + - run: npm run test:integration:ipfs diff --git a/.github/workflows/storage-cloud-smoke.yml b/.github/workflows/storage-cloud-smoke.yml new file mode 100644 index 0000000..9c8def3 --- /dev/null +++ b/.github/workflows/storage-cloud-smoke.yml @@ -0,0 +1,56 @@ +name: Reviewed cloud storage smoke + +on: + workflow_dispatch: + inputs: + backend: + description: Dedicated synthetic-data provider (never a patient store) + type: choice + required: true + options: [azure, s3] + +permissions: + contents: read + +jobs: + smoke: + if: github.ref == 'refs/heads/main' + runs-on: ${{ vars.STORAGE_RUNNER_LABEL || 'ubuntu-latest' }} + timeout-minutes: 15 + environment: storage-integration + permissions: + contents: read + id-token: write + env: + STORAGE_INTEGRATION: "true" + STORAGE_EMULATOR: "false" + STORAGE_BACKEND: ${{ inputs.backend }} + AZURE_STORAGE_ACCOUNT_URL: ${{ vars.AZURE_STORAGE_ACCOUNT_URL }} + AZURE_STORAGE_CONTAINER: ${{ vars.AZURE_STORAGE_CONTAINER }} + S3_BUCKET: ${{ vars.S3_BUCKET }} + STORAGE_PREFIX: synthetic-integration/ + AWS_REGION: ${{ vars.AWS_REGION }} + S3_KMS_KEY_ID: ${{ vars.S3_KMS_KEY_ID }} + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: "22" + cache: npm + - run: npm ci + - if: inputs.backend == 'azure' + uses: azure/login@8216e11d8cd9b42fe925c852af8e76311ff067ac # v2 + with: + client-id: ${{ vars.AZURE_CLIENT_ID }} + tenant-id: ${{ vars.AZURE_TENANT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + - if: inputs.backend == 's3' + uses: aws-actions/configure-aws-credentials@ff717079ee2060e4bcee96c4779b553acc87447c # v4 + with: + role-to-assume: ${{ vars.AWS_ROLE_ARN }} + aws-region: ${{ vars.AWS_REGION }} + role-session-name: synthetic-storage-integration + role-duration-seconds: 900 + - run: npm run test:integration diff --git a/.gitignore b/.gitignore index 4378ef5..de9a31a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,14 @@ coverage/ .data/ *.log *.tmp +.env +.env.* +!.env.example +.secrets/ +*.pem +*.key +*.pfx +*.p12 +swarm.key +identity.json +service.json diff --git a/README.md b/README.md index eedf7d3..4682e44 100644 --- a/README.md +++ b/README.md @@ -107,9 +107,14 @@ the reference-inspired layout and the patient-specific privacy boundaries. ## Commands +For Azure, AWS S3, private IPFS, account-free integration tests, and secret-free +configuration, see the short [storage quickstart](docs/STORAGE_QUICKSTART.md). +The [draft storage design](docs/STORAGE_DESIGN.md) describes concurrency, +metadata exposure, and remaining distributed-coordination limits. + | Command | Purpose | | --- | --- | -| `npm test` | Run all unit and integration tests | +| `npm test` | Run account-free tests; external-provider integration is opt-in | | `npm run test:coverage` | Run tests with coverage | | `npm run typecheck` | Type-check without emitting JavaScript | | `npm run build` | Compile TypeScript into `dist` | @@ -117,6 +122,10 @@ the reference-inspired layout and the patient-specific privacy boundaries. | `npm run dev` | Run the local UI with server restart on source changes | | `npm run example` | Run the synthetic end-to-end example | | `npm run example:access-denied` | Demonstrate that one factor alone cannot unlock a record | +| `npm run example:storage` | Run a synthetic flow against the configured backend | +| `npm run test:integration:azure` | Run real-SDK tests against temporary loopback Azurite | +| `npm run test:integration:ipfs` | Run the private IPFS Docker fixture (Docker engine required) | +| `npm run test:integration` | Explicit opt-in external-provider contract and encrypted-flow tests | ## Minimal usage @@ -160,7 +169,7 @@ src/ crypto/ Encryption, key splitting, signatures, and canonical hashing domain/ FHIR and provenance types service/ Enrollment, append, unlock, and verified assembly - storage/ Storage contract plus memory and local-file implementations + storage/ Provider-neutral contract, adapters, and runtime configuration web/ Local HTTP API and static-file server web/ Browser UI for enrollment, append, and timeline verification examples/ Synthetic end-to-end flows diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ed25d0e..ef0fc8a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -83,11 +83,18 @@ is displayed. ## Future milestones +Optional storage adapters now cover Azure, S3, and experimental private-IPFS +content with a separately configured CAS index. See +[storage design](STORAGE_DESIGN.md) and [setup](STORAGE_QUICKSTART.md). +The private-IPFS adapter is not a decentralized consensus implementation; +operator independence, index availability, and offline head merging remain +separate work. + - Real biometric SDK adapter with template protection and liveness evidence - Multi-finger enrollment and recovery policy - Multiple authorized clinicians with revocation - Offline branch creation and deterministic merge -- Distributed object-store adapter +- Independently operated distributed storage with resilient index coordination - FHIR profile validation and consent resources - Metadata-hiding access protocol - Independent cryptographic, clinical-safety, legal, and ethics reviews diff --git a/docs/DISTRIBUTED_STORAGE.md b/docs/DISTRIBUTED_STORAGE.md new file mode 100644 index 0000000..7c5f3f2 --- /dev/null +++ b/docs/DISTRIBUTED_STORAGE.md @@ -0,0 +1,85 @@ +# Experimental private distributed storage + +Global Patient Record Project is research software, not clinically or +production ready. `DistributedBlobStore(metadata, content)` separates a required +atomic key-to-CID index from immutable opaque content. It serializes JSON values +inside a format envelope, computes a CIDv1 raw SHA-256 identifier, and publishes +the pointer only after `content.put` confirms its replication policy. Reads +verify bytes against the pointer CID before decoding. The storage adapter does +not encrypt: callers must supply authenticated ciphertext, never plaintext FHIR, +biometrics, tokens, or derived keys. + +## Atomicity and failure model + +The injected `BlobStore` index supplies atomic create and numeric-version CAS. +Use cloud conditional-write storage or an independently consensus-backed +implementation across processes. `FileBlobStore` is only a local, +single-process demonstration. **IPFS and Cluster do not provide this +application's CAS**, and this design does not achieve independent decentralized +CAS. There is no storage fallback. + +Content is uploaded and pinned before index create/CAS. A losing concurrent +writer, failed index request, or replication timeout may leave unreachable +blocks and pins. No destructive cleanup is attempted: another writer or key may +reference the same CID. Retain and reconcile with the complete authoritative +index and backups before any operator-controlled garbage collection. A timeout +can have an ambiguous outcome. The uploading Kubo block is locally pinned to +avoid garbage collection while Cluster distributes it. + +## Client and deployment requirements + +`IpfsClusterClient` implements `DistributedContentStore`: +`put(Uint8Array): Promise` and `get(cid): Promise`. +Its options are `kuboUrl`, `clusterUrl`, optional `kuboAuthorization` and +`clusterAuthorization` (complete Authorization header values), and optional +`minReplicas`/`maxReplicas` (both default 2), `timeoutMs` (30000), +`pollIntervalMs` (250), and `maxBlobBytes` (1048575). `fetch` is an optional +injected transport for tests. Secret resolution belongs outside the client: +load headers through the application's secret-file/Key Vault configuration, +not source, command arguments, URLs, logs, or examples. + +Endpoint URLs are origins only, HTTPS by default, without credentials, paths, +queries or fragments. HTTP requires explicit `allowInsecureLocal: true` and +localhost/127.0.0.1/[::1]; use that only for isolated development. Redirects +are refused and credentials are scoped separately to each endpoint. The +deadline covers requests, body reads and polling. Bodies and raw blocks are +bounded; raw blocks must be nonempty and smaller than 1 MiB. + +The wire API is Kubo `POST /api/v0/block/put` with multipart file, raw codec and +SHA-256, then Cluster `POST /pins/{cid}` with `replication-min` and +`replication-max`. Retrieval uses Kubo `POST /api/v0/block/get`; there is no +public gateway. Kubo's returned CID and length must match local bytes. + +Replication polls Cluster `GET /pins/{cid}?local=false` and `/peers` (NDJSON +or array). A replica must report exactly `pinned`, no error, and a matching, +live Cluster/Kubo identity from the peer query. Duplicate Cluster identities +and multiple Cluster peers backed by one Kubo do not count twice. Allocation, +queued, remote, error, and stale pinned entries for unavailable peers do not +count. Require a modern **stateless pin tracker**, whose single-CID status +queries Kubo pin state; legacy cached trackers are not supported. Pin timestamps +are creation/change times, not liveness proofs, and are not counted as such. +The app cannot authenticate whether an operator actually selected that tracker. + +This is a momentary observation from trusted administrative endpoints, not +proof of ongoing durability, independent failure domains, Byzantine honesty, +or future availability. Nodes can fail immediately afterward. Monitor and +repair replication separately, test restore, and keep encrypted backups. +`minReplicas: 1, maxReplicas: 1` is only for an isolated single-node test. + +Configure and verify the private Kubo swarm **outside this application**: +runtime-mount the shared swarm key and Cluster secret, remove public bootstrap +peers, disable public discovery/routing as appropriate, and restrict all +administrative, swarm and gateway listeners with network policy. Do not commit +those secrets. Cluster's peer secret is not the Kubo swarm key. Cluster is +neither storage confidentiality nor application CAS; encryption remains +mandatory. Private peers see ciphertext, CIDs, sizes, pin membership and +timing; the index sees random object keys, CIDs and versions. + +## Upstream references + +Consult the official Kubo RPC reference (`docs.ipfs.tech/reference/kubo/rpc/`), +Cluster REST reference (`ipfscluster.io/documentation/reference/api/`), and +`ipfs-cluster/ipfs-cluster` source: `api/types.go` (`GlobalPinInfo`, `ID`, +`PinOptions.ToQuery`) and `pintracker/stateless/stateless.go` +(`Tracker.Status`, which calls `PinLsCid`). Pin dependency/container versions +and verify these wire contracts in the opt-in integration environment. diff --git a/docs/README.md b/docs/README.md index d01414e..7bcca34 100644 --- a/docs/README.md +++ b/docs/README.md @@ -226,12 +226,15 @@ interface BlobStore { The POC supplies: - `InMemoryBlobStore` for isolated tests; -- `FileBlobStore` for the local demonstration. - -An Azure Blob Storage adapter could be added without changing the cryptographic -or clinical service. A future shared storage package is one of the clearest -areas for collaboration, provided its contract preserves immutable creation and -optimistic concurrency. +- `FileBlobStore` for the local demonstration; +- `AzureBlobStore` and `S3BlobStore` for conditional cloud-object writes; +- experimental `DistributedBlobStore` for private IPFS content with a separate + compare-and-swap index. + +These adapters do not change the cryptographic or clinical service. See the +[storage quickstart](STORAGE_QUICKSTART.md) and +[draft storage design](STORAGE_DESIGN.md) for configuration, synthetic tests, +and the distributed adapter's remaining coordination dependency. ## Intentional differences from GOSQAS diff --git a/docs/STORAGE_DESIGN.md b/docs/STORAGE_DESIGN.md new file mode 100644 index 0000000..bcf174d --- /dev/null +++ b/docs/STORAGE_DESIGN.md @@ -0,0 +1,102 @@ +# Pluggable storage decision (draft) + +## Scope + +This draft addresses the storage seam behind issue #1. It adds executable cloud +and private-IPFS experiments without changing dual unlock, FHIR validation, +signatures, MACs, or provenance verification. It does not claim clinical, +regulatory, or production readiness. + +```text +PatientRecordService -> BlobStore + |-- InMemoryBlobStore (test) + |-- FileBlobStore (local, single-process) + |-- AzureBlobStore (conditional block-blob writes) + |-- S3BlobStore (conditional object writes) + `-- DistributedBlobStore + |-- private Kubo / IPFS Cluster (immutable bytes) + `-- configured BlobStore (key-to-CID index, CAS) +``` + +Each deployment chooses one backend. Cross-cloud mirroring and a +`ReplicatedBlobStore` are deliberately absent: acknowledging partial writes +across independent services is not atomic CAS, and rollback would not solve +concurrent writers. A separate consensus/repair design is required. + +## Contract and concurrency + +`create` preserves version 1 only if the opaque key is absent. `read` returns +the serialized value and logical numeric version. `compareAndSwap` must reject +missing/stale records and permit only one writer for the expected version. +Azure/S3 read the body and provider ETag together, check the logical version, +then condition the replacement on that ETag. No process-local map is used for +cloud concurrency. Native precondition failures map to contract errors; +authorization, service, malformed-object, and transport failures remain errors. + +Factory-created SDK clients do not automatically retry writes. A timed-out +request may have committed; callers must not interpret a timeout as definite +rollback or blindly re-append. Read/reconcile verified history before retrying. +The web API returns a generic conflict for known stale updates and never logs +raw provider errors, which may contain request headers. + +The distributed adapter persists new immutable content before atomically +publishing its index pointer. A failed CAS can leave unreachable encrypted +blocks. It does not unpin/delete on failure because another writer or reference +may need those blocks. Garbage collection needs a separate reachability and +retention design. + +## Availability boundary + +IPFS Cluster coordinates retention and replica placement; it is not the +application's numeric CAS authority. A private swarm and independently operated +peers are infrastructure requirements, not something a config flag proves. +The adapter's observed replication status is not a perpetual durability +guarantee. Health monitoring, independent operators, repair, funding, incident +response, and recovery exercises remain necessary. + +The index remains a dependency even if every encrypted block is replicated. +Azure/S3 make it possible to use managed infrastructure instead of a single +local file, but do not eliminate the provider/account trust boundary. This +draft therefore makes progress on issue #1; it does not fully satisfy "no +single point of failure" or guarantee ten years of service. Future work is a +reviewed consensus-backed index or signed multi-head publication/merge protocol, +not an ordinary IPNS mutable pointer pretending to provide linearizable CAS. + +## Security and metadata + +- Existing application encryption stays in place; provider-side encryption is + defense in depth, not a substitute for dual unlock. +- Cloud operators observe opaque keys, size, timing, revisions, and access + patterns. IPFS/Cluster peers additionally observe CIDs, pin placement, and + ciphertext equality. The index links opaque object keys and revisions to + CIDs. Do not add patient identifiers to blob names, tags, prefixes, or logs. +- TLS remains enabled; loopback HTTP is opt-in emulator behavior only. + Service credentials are resolved at runtime through workload identity, + Key Vault, or restricted secret mounts. No static credentials are checked in. +- Default credential chains are a convenience, not proof of least privilege; + operators must configure narrowly scoped identities and network controls. +- Authenticated encryption detects modification but does not independently + prove freshness against rollback of an entire valid storage snapshot. + Durable freshness anchors/rollback resistance remain future work. +- Encryption cannot recover lost patient/clinician factors; replicated + ciphertext is not a credential-recovery strategy. +- Storage-provider administration can still delete objects or deny service. + Permission policies, backups, retention, and periodic restore tests are + necessary operational controls. + +## Validation boundary + +Default tests use synthetic values, fake provider transports, and shared +contract scenarios. Azurite exercises the actual Azure SDK without an account. +The private Docker fixture and opt-in provider suite exercise external APIs. +Real-cloud authentication/KMS/network behavior requires the manual protected +workflow or a local approved identity; it cannot be validated without accounts. +The quickstart lists exactly which settings and provisioning steps are needed. + +## References + +- [Azure concurrency](https://learn.microsoft.com/azure/storage/blobs/concurrency-manage) +- [S3 conditional writes](https://docs.aws.amazon.com/AmazonS3/latest/userguide/conditional-writes.html) +- [IPFS persistence](https://docs.ipfs.tech/concepts/persistence/) +- [IPFS Cluster pinning](https://ipfscluster.io/documentation/guides/pinning/) +- [GitHub OIDC](https://docs.github.com/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect) diff --git a/docs/STORAGE_QUICKSTART.md b/docs/STORAGE_QUICKSTART.md new file mode 100644 index 0000000..dd10db0 --- /dev/null +++ b/docs/STORAGE_QUICKSTART.md @@ -0,0 +1,130 @@ +# Storage setup and end-to-end check + +**Synthetic data only. This remains a research POC, not a production health-record system.** +Choose one backend; the adapters do not automatically copy data between clouds. +Existing `.data/web` records are not migrated when the backend changes. +Give IPFS's key-to-CID index its own container/bucket (using the same Azure/S3 +settings), not a namespace already populated by a direct-storage adapter. + +## Without any cloud accounts + +```sh +npm ci +npm test +npm run typecheck +npm run build +npm run test:integration:azure +npm run example:storage +``` + +The Azure command starts a temporary loopback-only Azurite process, generates +throwaway credentials in memory, exercises the real Azure SDK, and cleans up. +It needs no Azure account or Docker. `example:storage` uses `.data/web` by +default; it generates fresh synthetic factors, enrolls, appends two entries, +reconnects, and checks access denial. It logs no factors or resource contents. +The factors are discarded on exit, so the remaining encrypted test records +cannot be unlocked later. Use a dedicated disposable store for this example. + +With a running Docker engine, `npm run test:integration:ipfs` runs the isolated +private distributed-storage fixture. See [distributed storage](DISTRIBUTED_STORAGE.md) +for architecture, network restrictions, availability, and coordination limits. + +## Configure Azure or AWS later + +Copy `.env.example` to **`.env.local`** (ignored), uncomment only your backend's +nonsecret settings, and replace placeholders. The app does not auto-load env files. +To load one explicitly: + +```sh +node --env-file=.env.local --import tsx examples/configured-storage.ts +node --env-file=.env.local --import tsx src/web/server.ts +``` + +| Backend | Settings | Runtime authentication | +| --- | --- | --- | +| `file` | `STORAGE_FILE_DIRECTORY` (default `.data/web`) | Local filesystem permissions; single process only | +| `azure` | `AZURE_STORAGE_ACCOUNT_URL`, `AZURE_STORAGE_CONTAINER` | `DefaultAzureCredential`: local `az login`, workload identity, or managed identity | +| `s3` | `S3_BUCKET`, `AWS_REGION`; optional `STORAGE_PREFIX`, `S3_KMS_KEY_ID` | AWS default credential chain: local SSO profile, workload role, or web identity | +| `ipfs` | Private API URLs, index backend, replication and runtime-secret settings in `.env.example` | Authenticated HTTPS APIs; cloud identity for index; secret-file or Key Vault API authorization | + +Provision the container/bucket and permissions **before** running. The application +never creates cloud resources, changes IAM/RBAC, makes containers public, or +deletes stored records. Do not put connection strings, SAS URLs, cloud access +keys, private keys, tokens, or real patient data in repository files or GitHub +secrets. Production-like configuration rejects plaintext non-loopback endpoints +and disabled TLS certificate validation. + +**Azure:** disable anonymous blob access and shared-key authorization; require +HTTPS/TLS 1.2+, use private endpoints/firewall rules, and scope blob read/write +data permissions to the test container. Use a custom data role without delete +where practical. Enable encryption, versioning/recovery, diagnostics with +payload/authorization redaction, and reviewed retention. Managed identity needs +no storage-account key. A local CLI identity must have equivalent permissions. + +**AWS:** enable Block Public Access and bucket-owner-enforced ownership; deny +non-TLS requests, use private endpoints where appropriate, and scope `s3:GetObject` +and `s3:PutObject` to the selected prefix. Grant narrowly scoped `s3:ListBucket` +if required to distinguish absent keys from access denial. For SSE-KMS, set +`S3_KMS_KEY_ID` and grant only the needed `kms:GenerateDataKey`/`kms:Decrypt` +permissions with a storage-service encryption context. Enable versioning and +reviewed recovery/replication. No application delete or bucket-admin permissions +are needed. An S3-compatible provider must support atomic conditional writes; +passing these tests is required, not assumed. + +Do not enable blanket immutable/WORM retention on mutable record-state blobs: +state updates require CAS. Separate retention policy design is still needed. + +## Explicit external-provider integration test + +Use a dedicated synthetic-data container/bucket/private cluster. Add +`STORAGE_INTEGRATION=true` to your ignored `.env.local`, then run: + +```sh +node --env-file=.env.local node_modules/vitest/vitest.mjs run --config vitest.integration.config.ts +``` + +This fails rather than silently skipping if opt-in/configuration is missing. +It exercises independent-client reads, conditional creates, missing objects, +version increments, stale/concurrent writes, encrypted enroll/append/reconnect, +wrong-factor denial, cleartext absence, and tamper rejection. +The test leaves randomly keyed encrypted fixtures; remove them using your +dedicated test-store retention policy, not application-level deletion. +Emulator success does **not** establish live IAM, KMS, networking, regional +failover, regulatory compliance, or ten-year durability. + +## Key Vault / mounted secrets + +Azure/S3 storage authentication does not need a secret vault when workload +identity is available. The IPFS API authorization headers may be retrieved by +setting `IPFS_API_AUTH_KEYVAULT_SECRET`, `IPFS_CLUSTER_AUTH_KEYVAULT_SECRET`, and +`AZURE_KEY_VAULT_URL`. Grant the workload **get only** on those secrets. +Alternatively use `IPFS_API_AUTH_FILE`/`IPFS_CLUSTER_AUTH_FILE` pointing to +read-only, access-restricted secret mounts outside the checkout. AWS Secrets +Manager or another secret-store integration can supply those mounts. Never +include both sources for the same secret. Secret values must be complete +single-line authorization headers (for example, a Bearer or Basic value). +IPFS swarm keys, cluster secrets, and peer private identities belong in the +node operators' secret management, not application configuration or GitHub. +Restart the process after rotating API credentials; hot-reload is not implemented. + +## Optional GitHub cloud smoke test after review + +`storage-ci.yml` runs account-free tests on pushes/PRs. The separate manual +`storage-cloud-smoke.yml` runs **only on `main`** after merge, behind a +`storage-integration` GitHub Environment. Configure required reviewers and +main-only deployment branch rules **before** enabling it. + +Set environment **variables**, not stored credentials: Azure account/container, +client ID, tenant ID, subscription ID; or AWS role ARN, region, bucket, and KMS +key ARN (names match the workflow). Configure Azure/AWS federated identity to +trust exactly this repository's `storage-integration` environment and the +correct OIDC audience. Do not use wildcard repository trust. GitHub issues a +short-lived token; the cloud exchanges it for temporary credentials. No +long-lived cloud credential is stored in GitHub. Do not grant the cloud identity +to PR/fork workflows, upload environment dumps, or enable SDK request tracing. + +After approval, select Azure or S3 in **Actions > Reviewed cloud storage smoke**. +Private-endpoint-only resources require a hardened runner with network access. +Set the repository variable `STORAGE_RUNNER_LABEL` to its approved runner label; +the default is `ubuntu-latest`. Account provisioning, trust, network access, +and clinical/security review cannot be supplied by this code change. diff --git a/docs/THREAT_MODEL.md b/docs/THREAT_MODEL.md index b9f6988..ef93055 100644 --- a/docs/THREAT_MODEL.md +++ b/docs/THREAT_MODEL.md @@ -45,6 +45,13 @@ ## Required production work +The optional cloud/private-IPFS adapters introduce additional metadata and +coordination boundaries. See [the storage design](STORAGE_DESIGN.md) for +ciphertext/CID correlation, the mutable index dependency, ambiguous write +outcomes, private-network requirements, and snapshot rollback limitations. +Cloud account configuration and an observed replica count do not establish +durability, least privilege, or production readiness. + Do not advance to real-patient testing without independent cryptographic review, a formal data-protection impact assessment, clinical safety analysis, jurisdiction-specific biometric review, informed-consent design, incident diff --git a/examples/README.md b/examples/README.md index 2ee8ab0..9f29884 100644 --- a/examples/README.md +++ b/examples/README.md @@ -20,3 +20,12 @@ npm run example:access-denied `access-denied.ts` demonstrates that a valid patient factor paired with an unrecognized clinician credential is rejected. + +## Configurable storage example + +`npm run example:storage` uses the selected `STORAGE_BACKEND` and only synthetic +data. It enrolls, appends two entries, reconnects, verifies lineage, and checks +wrong-clinician denial without printing secret factors or record contents. +See [storage setup](../docs/STORAGE_QUICKSTART.md) for Azure, AWS, private IPFS, +emulator testing, and runtime identities. No cloud accounts are needed for +the default file-backed example or the Azure emulator suite. diff --git a/examples/configured-storage.ts b/examples/configured-storage.ts new file mode 100644 index 0000000..e6d8a40 --- /dev/null +++ b/examples/configured-storage.ts @@ -0,0 +1,58 @@ +import assert from "node:assert/strict"; +import { randomBytes } from "node:crypto"; +import { + AccessDeniedError, + PatientRecordService, + StorageConfigurationError, + createClinicianCredential, + createConfiguredBlobStore, +} from "../src/index.js"; + +async function main(): Promise { + const store = await createConfiguredBlobStore(); + const records = new PatientRecordService(store); + const clinician = createClinicianCredential(); + const biometricToken = randomBytes(32).toString("base64url"); + const request = { clinician, biometricToken }; + await records.enroll(request); + const first = await records.append({ + ...request, + resource: { + resourceType: "Condition", + id: "synthetic-storage-example", + code: { text: "SYNTHETIC STORAGE EXAMPLE ONLY" }, + }, + }); + await records.append({ + ...request, + resource: { + resourceType: "Observation", + id: "synthetic-storage-followup", + status: "final", + code: { text: "SYNTHETIC FOLLOWUP ONLY" }, + }, + }); + + // Reconnect through another adapter; factors remain in this process only. + const reconnected = new PatientRecordService( + process.env.STORAGE_BACKEND === "memory" ? store : await createConfiguredBlobStore(), + ); + const timeline = await reconnected.assemble(request); + assert.equal(timeline.length, 2); + assert.deepEqual(timeline[1]?.event.parents, [first.eventId]); + await assert.rejects( + reconnected.assemble({ biometricToken, clinician: createClinicianCredential() }), + AccessDeniedError, + ); + console.log("Synthetic storage example completed: enrollment, append, reconnect, timeline, and access denial."); + console.log("No factors or resource contents were logged. Encrypted test objects remain in the selected store."); +} + +try { + await main(); +} catch (error) { + console.error(error instanceof StorageConfigurationError + ? error.message + : "Storage example failed. Check the backend configuration, identity permissions, and service availability."); + process.exitCode = 1; +} diff --git a/package-lock.json b/package-lock.json index e5bb847..e37d964 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,17 @@ "name": "global-private-health-records-poc", "version": "0.1.0", "license": "GPL-3.0-only", + "dependencies": { + "@aws-sdk/client-s3": "^3.1131.0", + "@azure/identity": "^4.13.3", + "@azure/keyvault-secrets": "^4.11.2", + "@azure/storage-blob": "^12.33.0", + "multiformats": "^14.0.5" + }, "devDependencies": { "@types/node": "^24.3.1", "@vitest/coverage-v8": "^3.2.4", + "azurite": "^3.37.0", "happy-dom": "^18.0.1", "tsx": "^4.20.5", "typescript": "^5.9.2", @@ -34,1891 +42,8060 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha1-fwhx2Zgk0jE31g+G/PYTD9WhtR8=", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/checksums": { + "version": "3.1001.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/checksums/-/checksums-3.1001.0.tgz", + "integrity": "sha1-Q4dm28CJPCG8qp1ocqa7kqwxl1I=", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=20.0.0" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha1-vYcITO0MeW7Ea9pJLeboPSnon8I=", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/client-s3": { + "version": "3.1131.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/client-s3/-/client-s3-3.1131.0.tgz", + "integrity": "sha512-jMw3q5sYNvWJRngkNwdS3H2WPBcdqhdrOez+7fBUB26fpH/2+TaJcDdlzdjgLTMhFTq2eOyXBSy1T/4A6CL/Lw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/checksums": "^3.1001.0", + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/credential-provider-node": "^3.972.83", + "@aws-sdk/middleware-sdk-s3": "^3.972.76", + "@aws-sdk/signature-v4-multi-region": "^3.996.46", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/fetch-http-handler": "^5.7.2", + "@smithy/node-http-handler": "^4.11.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=20.0.0" } }, - "node_modules/@babel/parser": { - "version": "7.29.8", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@babel/parser/-/parser-7.29.8.tgz", - "integrity": "sha1-llNxai8Qxne5j7xj1L+wAMMCzxc=", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/core": { + "version": "3.978.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/core/-/core-3.978.0.tgz", + "integrity": "sha1-EV5dLgPt04D+Jw1GF9sPTMdYadY=", + "license": "Apache-2.0", "dependencies": { - "@babel/types": "^7.29.8" - }, - "bin": { - "parser": "bin/babel-parser.js" + "@aws-sdk/types": "^3.974.5", + "@aws-sdk/xml-builder": "^3.972.40", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.33.3", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.17.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.0.0" + "node": ">=20.0.0" } }, - "node_modules/@babel/types": { - "version": "7.29.8", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@babel/types/-/types-7.29.8.tgz", - "integrity": "sha1-Einu8x2FFW1w+j9M2Fk3bQ6vaGM=", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.71", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.71.tgz", + "integrity": "sha1-mCYb3oW9Dy4tSnupfc5nkHLhav8=", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=20.0.0" } }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha1-u+EtyltO+YOg0K9LB7m8kOoKuro=", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.73", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.73.tgz", + "integrity": "sha1-h5Rmsj3K7EvuY1q/EQPh9fNdEi0=", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/fetch-http-handler": "^5.7.2", + "@smithy/node-http-handler": "^4.11.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", - "integrity": "sha1-v24QMDvPLnxoaXX6Uvk37Cco2Lw=", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.973.16", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.16.tgz", + "integrity": "sha1-tR2yBpuRXVY+BZacrIRfYkrtHew=", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/credential-provider-env": "^3.972.71", + "@aws-sdk/credential-provider-http": "^3.972.73", + "@aws-sdk/credential-provider-login": "^3.972.78", + "@aws-sdk/credential-provider-process": "^3.972.71", + "@aws-sdk/credential-provider-sso": "^3.973.15", + "@aws-sdk/credential-provider-web-identity": "^3.972.77", + "@aws-sdk/nested-clients": "^3.997.45", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-arm/-/android-arm-0.28.2.tgz", - "integrity": "sha1-LYTs5qTiaE2SvibuE9QnV9gxw4E=", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.78", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.78.tgz", + "integrity": "sha1-wcfKSXIBp/K9a6doP6OolsTiGPg=", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/nested-clients": "^3.997.45", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", - "integrity": "sha1-DGJGvI0sTRcqrC2z+xGQ1yvWVQQ=", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.83", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.83.tgz", + "integrity": "sha1-9OFrOh1dAjdGDBtTLeN+tlBZhdw=", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.71", + "@aws-sdk/credential-provider-http": "^3.972.73", + "@aws-sdk/credential-provider-ini": "^3.973.16", + "@aws-sdk/credential-provider-process": "^3.972.71", + "@aws-sdk/credential-provider-sso": "^3.973.15", + "@aws-sdk/credential-provider-web-identity": "^3.972.77", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-x64/-/android-x64-0.28.2.tgz", - "integrity": "sha1-/DjU1jWNjcHPU/CfdYn+Q262SAE=", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.71", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.71.tgz", + "integrity": "sha1-Wi/3K8wtemWMpkchk+ozQ3yLWk4=", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", - "integrity": "sha1-+Dr+6sHX2sAcei/QErPkUaBZH8w=", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.973.15", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.15.tgz", + "integrity": "sha1-ahORyiOfbVBdy1XqZlW4wU+tWvc=", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/nested-clients": "^3.997.45", + "@aws-sdk/token-providers": "3.1129.0", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", - "integrity": "sha1-UQFHwFWnlViNu+FP1rG4rQovMN4=", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.77", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.77.tgz", + "integrity": "sha1-XiiBEZN/y2+fAiNvoVMJXW4k0i8=", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/nested-clients": "^3.997.45", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", - "integrity": "sha1-CTuSAOzwsRW6Tl4kinSFycX4vV4=", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.76", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.76.tgz", + "integrity": "sha1-NpBnJ0d8OmOPMB61q7ebeX16600=", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/signature-v4-multi-region": "^3.996.46", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", - "integrity": "sha1-C+Irbfkl0hPoQeqHEjr134Cw+vc=", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.45", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/nested-clients/-/nested-clients-3.997.45.tgz", + "integrity": "sha1-PtDl/NJciJAlsjealDRZT+cFako=", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/signature-v4-multi-region": "^3.996.46", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/fetch-http-handler": "^5.7.2", + "@smithy/node-http-handler": "^4.11.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", - "integrity": "sha1-vrEq1yuE9y0oSIzBuO6ffrFB11M=", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.46", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.46.tgz", + "integrity": "sha1-7lev5SgtpLV5aAYe+HBtMokCZ6s=", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.5", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", - "integrity": "sha1-G9vGUc2pupmVxT7ZxxzqplCUdi0=", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@aws-sdk/token-providers": { + "version": "3.1129.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/token-providers/-/token-providers-3.1129.0.tgz", + "integrity": "sha1-pKEaeJ1yWYdMzzcz5Tj8cPiiieQ=", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/nested-clients": "^3.997.45", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", - "integrity": "sha1-uB+dVVKbRcIGpGoTghSxqmh5aWs=", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@aws-sdk/types": { + "version": "3.974.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/types/-/types-3.974.5.tgz", + "integrity": "sha1-pSaRUxc40ZG4s/0a53V8Lu7Hr+4=", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", - "integrity": "sha1-WYZnJBoEyZt27W75QKxQA4xBn5g=", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.40", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/xml-builder/-/xml-builder-3.972.40.tgz", + "integrity": "sha1-PNtfNACGCtkTAa7Lh/tRSKZtb/c=", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", - "integrity": "sha1-HFHrnOqQP1PZe1rzsYQdtw9Vlso=", - "cpu": [ - "mips64el" - ], - "dev": true, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha1-cIgC2Yf44XvfSvTeEDFmDOHN1l8=", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure-rest/core-client": { + "version": "2.9.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure-rest/core-client/-/core-client-2.9.0.tgz", + "integrity": "sha1-4MLGSgtLNk1R/w7+0S74izbNYFo=", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.24.0", + "@azure/core-tracing": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.8", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=22.0.0" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", - "integrity": "sha1-Y91h8XzrMagSJ/QT/qyKcbwsUfI=", - "cpu": [ - "ppc64" - ], - "dev": true, + "node_modules/@azure/abort-controller": { + "version": "2.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/abort-controller/-/abort-controller-2.2.0.tgz", + "integrity": "sha1-k+DoKwGGLn6jtbaZWHGdPz/SyKw=", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=22.0.0" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", - "integrity": "sha1-N2Owj95c8lqx+suOd1Lt/kX7/Cc=", - "cpu": [ - "riscv64" - ], - "dev": true, + "node_modules/@azure/core-auth": { + "version": "1.11.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/core-auth/-/core-auth-1.11.0.tgz", + "integrity": "sha1-Ha7/32LRqHHSK4ZfnzFkpj6h9XU=", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=22.0.0" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", - "integrity": "sha1-GhN/8pOoKQbrMXY4W9fo4OXPt8s=", - "cpu": [ - "s390x" - ], - "dev": true, + "node_modules/@azure/core-client": { + "version": "1.11.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/core-client/-/core-client-1.11.1.tgz", + "integrity": "sha1-2ZCHiohXdQerETGTW0c0sppjfVg=", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=22.0.0" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", - "integrity": "sha1-Jos2IRwUbKVPj+EsV4qNbviXlIU=", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@azure/core-http-compat": { + "version": "2.5.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/core-http-compat/-/core-http-compat-2.5.0.tgz", + "integrity": "sha1-14BmGln4pDL+yyt8b6LVgi3KHeo=", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@azure/abort-controller": "^2.1.2" + }, "engines": { - "node": ">=18" + "node": ">=22.0.0" + }, + "peerDependencies": { + "@azure/core-client": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", - "integrity": "sha1-Ilca2VHWK7aszILY0frVyMGsC6E=", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha1-eHEFAnog5Fx3ZRqYsBpNOwG3Wgg=", "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=18.0.0" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", - "integrity": "sha1-QvzFcpfrCgyj9fxHUpH0waP3wN4=", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@azure/core-paging": { + "version": "1.7.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/core-paging/-/core-paging-1.7.0.tgz", + "integrity": "sha1-WVl6L1Q4ujxsh4n8hw9p4OeFMag=", "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "dependencies": { + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=22.0.0" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", - "integrity": "sha1-nrMq8QSsPaz07coB9ZZmSqsMc+8=", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@azure/core-process": { + "version": "1.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/core-process/-/core-process-1.0.0.tgz", + "integrity": "sha1-w68cFyI9ZTjhs2ln3clM2uUyy4M=", "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], "engines": { - "node": ">=18" + "node": ">=22.0.0" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", - "integrity": "sha1-/r7SQC1giCJekfIPtM4lIq0KTv0=", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.25.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/core-rest-pipeline/-/core-rest-pipeline-1.25.0.tgz", + "integrity": "sha1-kupJEu27H3OV9IKaMAYxF0qoLwg=", "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=22.0.0" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", - "integrity": "sha1-hWQcPUZkKL+8zqXyHCaDZmP+9c4=", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@azure/core-tracing": { + "version": "1.4.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/core-tracing/-/core-tracing-1.4.0.tgz", + "integrity": "sha1-1lenAOJNJW0ZXmKKeV22qMxHXqs=", "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], + "dependencies": { + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=22.0.0" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", - "integrity": "sha1-pzb52JYkgQRfxMPlT1R58iyHD7Q=", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@azure/core-util": { + "version": "1.14.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/core-util/-/core-util-1.14.0.tgz", + "integrity": "sha1-fOn+V5WPEy3UIlc4VuWkXHIyuwQ=", "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18" + "node": ">=22.0.0" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", - "integrity": "sha1-7lq0D60YYgG2UqM/il6xSenkJTI=", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@azure/core-xml": { + "version": "1.6.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/core-xml/-/core-xml-1.6.0.tgz", + "integrity": "sha1-EI8JIOG834owuffoh5ibOhIIK9k=", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "fast-xml-parser": "^5.5.9", + "tslib": "^2.8.1" + }, "engines": { - "node": ">=18" + "node": ">=22.0.0" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", - "integrity": "sha1-xA0optmaEn2mcR8q/XSxHLY7Bqc=", - "cpu": [ - "ia32" - ], + "node_modules/@azure/functions": { + "version": "4.16.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/functions/-/functions-4.16.5.tgz", + "integrity": "sha1-mP18vAvEb5ZJO89Oz4iCrDYQghQ=", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@azure/functions-extensions-base": "0.3.0", + "cookie": "^0.7.0" + }, "engines": { - "node": ">=18" + "node": ">=20.0" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", - "integrity": "sha1-shr/uATMFnwTPZX0WzodwTI7moc=", - "cpu": [ - "x64" - ], + "node_modules/@azure/functions-extensions-base": { + "version": "0.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/functions-extensions-base/-/functions-extensions-base-0.3.0.tgz", + "integrity": "sha1-cGLy8GmLtkdwN4+eev+fd8Ukn04=", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=18" + "node": ">=18.0" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha1-s3Znt7wYHBaHgiWbq0JHT79StVA=", - "dev": true, - "license": "ISC", + "node_modules/@azure/identity": { + "version": "4.13.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/identity/-/identity-4.13.3.tgz", + "integrity": "sha1-93gb4IncYjHheuFFS5oekbx5e4E=", + "license": "MIT", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-process": "^1.0.0", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^6.0.0", + "open": "^10.1.0", + "tslib": "^2.2.0" }, "engines": { - "node": ">=12" + "node": ">=22.0.0" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.6", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@istanbuljs/schema/-/schema-0.1.6.tgz", - "integrity": "sha1-jcmvoqwVBssaWPiZQPHBJERsjfM=", - "dev": true, + "node_modules/@azure/keyvault-common": { + "version": "2.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/keyvault-common/-/keyvault-common-2.1.0.tgz", + "integrity": "sha1-Zw50FKK/aDcbCz0fmByboCeLd90=", "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-rest-pipeline": "^1.8.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.10.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha1-Y0Khn0Q0dRjJPkOxrGnes8Rlah8=", + "node_modules/@azure/keyvault-keys": { + "version": "4.10.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/keyvault-keys/-/keyvault-keys-4.10.2.tgz", + "integrity": "sha1-RonuPX645uaBvtDtHwVJ1uwyq2E=", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-lro": "^2.7.2", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.0", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-common": "^2.1.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha1-eg7mAfYPmaIMfHxf8MgDiMEYm9Y=", - "dev": true, + "node_modules/@azure/keyvault-secrets": { + "version": "4.11.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/keyvault-secrets/-/keyvault-secrets-4.11.2.tgz", + "integrity": "sha1-Ngm9ZSp6Nn8UU17FWfgXhkMkr8k=", "license": "MIT", + "dependencies": { + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-lro": "^2.7.2", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.0", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-common": "^2.1.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.8.1" + }, "engines": { - "node": ">=6.0.0" + "node": ">=20.0.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.6.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", - "integrity": "sha1-9MZj6GLwbcmMpNRThixGkCeJoY0=", - "dev": true, - "license": "MIT" + "node_modules/@azure/logger": { + "version": "1.4.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/logger/-/logger-1.4.0.tgz", + "integrity": "sha1-PVpif+WaJ27OdIenndkq1cUIwzk=", + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha1-2xXWeByTHzolGj2sOVAcmKYIL9A=", + "node_modules/@azure/monitor-opentelemetry": { + "version": "1.20.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/monitor-opentelemetry/-/monitor-opentelemetry-1.20.0.tgz", + "integrity": "sha1-CFM7fX3xffp44G87Wu4DS2Lmra4=", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@azure-rest/core-client": "^2.5.2", + "@azure/core-auth": "^1.10.1", + "@azure/core-rest-pipeline": "^1.22.2", + "@azure/core-tracing": "^1.2.0", + "@azure/identity": "^4.13.1", + "@azure/logger": "^1.3.0", + "@azure/monitor-opentelemetry-exporter": "1.0.0-beta.45", + "@azure/opentelemetry-instrumentation-azure-sdk": "^1.1.0-beta.1", + "@microsoft/applicationinsights-web-snippet": "^1.2.3", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/api-logs": "^0.221.0", + "@opentelemetry/core": "^2.10.0", + "@opentelemetry/instrumentation": "^0.221.0", + "@opentelemetry/instrumentation-bunyan": "^0.66.0", + "@opentelemetry/instrumentation-console": "^0.3.0", + "@opentelemetry/instrumentation-http": "^0.221.0", + "@opentelemetry/instrumentation-mongodb": "^0.74.0", + "@opentelemetry/instrumentation-mysql": "^0.67.0", + "@opentelemetry/instrumentation-pg": "^0.73.0", + "@opentelemetry/instrumentation-redis": "^0.69.0", + "@opentelemetry/instrumentation-winston": "^0.65.0", + "@opentelemetry/resource-detector-azure": "^0.29.0", + "@opentelemetry/resources": "^2.10.0", + "@opentelemetry/sdk-logs": "^0.221.0", + "@opentelemetry/sdk-metrics": "^2.10.0", + "@opentelemetry/sdk-node": "^0.221.0", + "@opentelemetry/sdk-trace-base": "^2.10.0", + "@opentelemetry/sdk-trace-node": "^2.10.0", + "@opentelemetry/semantic-conventions": "^1.43.0", + "@opentelemetry/winston-transport": "^0.31.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=22.0.0" } }, - "node_modules/@napi-rs/lzma-linux-x64-gnu": { - "version": "1.5.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", - "integrity": "sha1-5X1DBpZgeGYgOAlPs465FG3Drqk=", - "cpu": [ - "x64" - ], + "node_modules/@azure/monitor-opentelemetry-exporter": { + "version": "1.0.0-beta.45", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/monitor-opentelemetry-exporter/-/monitor-opentelemetry-exporter-1.0.0-beta.45.tgz", + "integrity": "sha1-G9v/g//cBjwX3AxPLXK+Gdd0IDI=", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@azure-rest/core-client": "^2.5.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-process": "^1.0.0", + "@azure/core-rest-pipeline": "^1.19.0", + "@azure/core-util": "^1.11.0", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/api-logs": "^0.221.0", + "@opentelemetry/core": "^2.10.0", + "@opentelemetry/resources": "^2.10.0", + "@opentelemetry/sdk-logs": "^0.221.0", + "@opentelemetry/sdk-metrics": "^2.10.0", + "@opentelemetry/sdk-trace-base": "^2.10.0", + "@opentelemetry/semantic-conventions": "^1.43.0", + "tslib": "^2.8.1" + }, "engines": { - "node": "^22.20 || ^24.12 || >=25" + "node": ">=22.0.0" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha1-p36nQvqyV3UUVDTrHSMoz1ATrDM=", + "node_modules/@azure/monitor-opentelemetry-exporter/node_modules/@opentelemetry/api-logs": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha1-ZrhAWxIv8cdu50O5vSynOACi4t8=", "dev": true, - "license": "MIT", - "optional": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, "engines": { - "node": ">=14" + "node": ">=8.0.0" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", - "integrity": "sha1-0Dum6lT57IBojRU3Y80yWi0qWvY=", - "cpu": [ - "arm" - ], + "node_modules/@azure/monitor-opentelemetry-exporter/node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha1-qnf3E450ulOZ1fO7DereDBaPALI=", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", - "integrity": "sha1-2142qoqVW0teCwJNZxIw7cXNwZE=", - "cpu": [ - "arm64" - ], + "node_modules/@azure/monitor-opentelemetry-exporter/node_modules/@opentelemetry/resources": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/resources/-/resources-2.10.0.tgz", + "integrity": "sha1-znbBwbqvzkx3vCmJCWW6H1ZnGsw=", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", - "integrity": "sha1-HtHEOSLnubXQIO9l2EAuPIHtyG4=", - "cpu": [ - "arm64" - ], + "node_modules/@azure/monitor-opentelemetry-exporter/node_modules/@opentelemetry/sdk-logs": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha1-nsDiy/tXKRe5SBAZg+CIaKaQkV4=", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", - "integrity": "sha1-Cobngr96VG509THjleJP20XINSc=", - "cpu": [ - "x64" - ], + "node_modules/@azure/monitor-opentelemetry/node_modules/@azure/opentelemetry-instrumentation-azure-sdk": { + "version": "1.1.0-beta.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/opentelemetry-instrumentation-azure-sdk/-/opentelemetry-instrumentation-azure-sdk-1.1.0-beta.1.tgz", + "integrity": "sha1-yYnrme/U2tHVDI4Sin8ppUEs7Rs=", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "dependencies": { + "@azure/core-tracing": "^1.2.0", + "@azure/logger": "^1.0.0", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.211.0", + "@opentelemetry/sdk-trace-web": "^2.0.0", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=20.0.0" + } }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", - "integrity": "sha1-gvpRxUAYW1BjyLPGOqx5sV8oAa0=", - "cpu": [ - "arm64" - ], + "node_modules/@azure/monitor-opentelemetry/node_modules/@azure/opentelemetry-instrumentation-azure-sdk/node_modules/@opentelemetry/api-logs": { + "version": "0.211.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/api-logs/-/api-logs-0.211.0.tgz", + "integrity": "sha1-MtntmJOZVqhNTi/14BWYy50o10Q=", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", - "integrity": "sha1-3x1ztWf9YuDPIcm1ff9/RL/Wljg=", - "cpu": [ - "x64" - ], + "node_modules/@azure/monitor-opentelemetry/node_modules/@azure/opentelemetry-instrumentation-azure-sdk/node_modules/@opentelemetry/instrumentation": { + "version": "0.211.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/instrumentation/-/instrumentation-0.211.0.tgz", + "integrity": "sha1-1F4g6vp1tdPoqXRaYgUzKJPFXzc=", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.211.0", + "import-in-the-middle": "^2.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", - "integrity": "sha1-pYW6BBgCeltWdpPbOYLl5XVEpMc=", - "cpu": [ - "arm" - ], + "node_modules/@azure/monitor-opentelemetry/node_modules/@opentelemetry/api-logs": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha1-ZrhAWxIv8cdu50O5vSynOACi4t8=", "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", - "integrity": "sha1-Eybw2yK2kKku/Y6qBukiaNx9G7Y=", - "cpu": [ - "arm" - ], + "node_modules/@azure/monitor-opentelemetry/node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha1-qnf3E450ulOZ1fO7DereDBaPALI=", "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", - "integrity": "sha1-lJGTn3zEOlsmqHdBf66v5puseaw=", - "cpu": [ - "arm64" - ], + "node_modules/@azure/monitor-opentelemetry/node_modules/@opentelemetry/resources": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/resources/-/resources-2.10.0.tgz", + "integrity": "sha1-znbBwbqvzkx3vCmJCWW6H1ZnGsw=", "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", - "integrity": "sha1-pT2T2sMqzGcTJK8RU5MKtaBNhjk=", - "cpu": [ - "arm64" - ], + "node_modules/@azure/monitor-opentelemetry/node_modules/@opentelemetry/sdk-logs": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha1-nsDiy/tXKRe5SBAZg+CIaKaQkV4=", "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", - "integrity": "sha1-224GFz78hwvkmi32ktDAYHQXpnk=", - "cpu": [ - "loong64" - ], + "node_modules/@azure/monitor-opentelemetry/node_modules/import-in-the-middle": { + "version": "2.0.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/import-in-the-middle/-/import-in-the-middle-2.0.6.tgz", + "integrity": "sha1-GXIze/4CDQX2teAgwTM0VnQ2Mk8=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.15.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" + } + }, + "node_modules/@azure/ms-rest-js": { + "version": "2.7.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/ms-rest-js/-/ms-rest-js-2.7.0.tgz", + "integrity": "sha1-hjkGVXf/30lGlR4dJGM06/1y1Tc=", + "deprecated": "This package has been deprecated in favor of the new Azure SDK packages from [Azure SDK for JavaScript](https://github.com/azure/azure-sdk-for-js).", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@azure/core-auth": "^1.1.4", + "abort-controller": "^3.0.0", + "form-data": "^2.5.0", + "node-fetch": "^2.6.7", + "tslib": "^1.10.0", + "tunnel": "0.0.6", + "uuid": "^8.3.2", + "xml2js": "^0.5.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", - "integrity": "sha1-pgc0o95Ae89L/kTQtkIiwLn7MLw=", - "cpu": [ - "loong64" - ], + "node_modules/@azure/ms-rest-js/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha1-zy04vcNKE0vK8QkcQfZhni9nLQA=", + "dev": true, + "license": "0BSD" + }, + "node_modules/@azure/ms-rest-js/node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha1-2UQGMfuy7YACA/rRBvJyT2LEk7c=", "dev": true, - "libc": [ - "musl" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", - "integrity": "sha1-KKZ+Fde6hjDvBEmAo5Ym4GJ9bnM=", + "node_modules/@azure/msal-browser": { + "version": "5.22.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/msal-browser/-/msal-browser-5.22.0.tgz", + "integrity": "sha1-S4Ni+o92jHE9PU7kpMCqEXzPXuo=", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.14.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.14.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/msal-common/-/msal-common-16.14.1.tgz", + "integrity": "sha1-LJh/MdqFjlROcrb6qYb+zbPeIPg=", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "6.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/msal-node/-/msal-node-6.0.1.tgz", + "integrity": "sha1-SLnVpHLxa0A706MklCJ+z8VMr9Q=", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.14.1", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@azure/opentelemetry-instrumentation-azure-sdk": { + "version": "1.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/opentelemetry-instrumentation-azure-sdk/-/opentelemetry-instrumentation-azure-sdk-1.0.0.tgz", + "integrity": "sha1-jq6h9ZGcvym6g/L6yZ2JRDbhfco=", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/core-tracing": "^1.2.0", + "@azure/logger": "^1.0.0", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.211.0", + "@opentelemetry/sdk-trace-web": "^2.0.0", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/opentelemetry-instrumentation-azure-sdk/node_modules/@opentelemetry/api-logs": { + "version": "0.211.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/api-logs/-/api-logs-0.211.0.tgz", + "integrity": "sha1-MtntmJOZVqhNTi/14BWYy50o10Q=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@azure/opentelemetry-instrumentation-azure-sdk/node_modules/@opentelemetry/instrumentation": { + "version": "0.211.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/instrumentation/-/instrumentation-0.211.0.tgz", + "integrity": "sha1-1F4g6vp1tdPoqXRaYgUzKJPFXzc=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.211.0", + "import-in-the-middle": "^2.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@azure/opentelemetry-instrumentation-azure-sdk/node_modules/import-in-the-middle": { + "version": "2.0.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/import-in-the-middle/-/import-in-the-middle-2.0.6.tgz", + "integrity": "sha1-GXIze/4CDQX2teAgwTM0VnQ2Mk8=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.15.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" + } + }, + "node_modules/@azure/storage-blob": { + "version": "12.33.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/storage-blob/-/storage-blob-12.33.0.tgz", + "integrity": "sha1-EK1FhvSSJzG4t5zIa+ytp1Z6H6s=", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.3", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.1", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/core-xml": "^1.4.5", + "@azure/logger": "^1.1.4", + "@azure/storage-common": "^12.4.1", + "events": "^3.0.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/storage-common": { + "version": "12.5.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/storage-common/-/storage-common-12.5.0.tgz", + "integrity": "sha1-nbHWmPJu9LVqVKYABGp2XqG7oxY=", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-http-compat": "^2.4.0", + "@azure/core-rest-pipeline": "^1.24.0", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.1.4", + "events": "^3.3.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha1-fwhx2Zgk0jE31g+G/PYTD9WhtR8=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha1-vYcITO0MeW7Ea9pJLeboPSnon8I=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha1-llNxai8Qxne5j7xj1L+wAMMCzxc=", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha1-Einu8x2FFW1w+j9M2Fk3bQ6vaGM=", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha1-u+EtyltO+YOg0K9LB7m8kOoKuro=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@colors/colors/-/colors-1.6.1.tgz", + "integrity": "sha1-jignJZradpZbOgUcGKWSf72sOMI=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.9", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@dabh/diagnostics/-/diagnostics-2.0.9.tgz", + "integrity": "sha1-DenocY4jq2DR+xzgwAQHnxT1rTY=", + "dev": true, + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha1-v24QMDvPLnxoaXX6Uvk37Cco2Lw=", "cpu": [ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ - "linux" - ] + "aix" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", - "integrity": "sha1-d7tVOlFJQq9UBwdWdj27RMnGyys=", + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha1-LYTs5qTiaE2SvibuE9QnV9gxw4E=", "cpu": [ - "ppc64" + "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ - "linux" - ] + "android" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", - "integrity": "sha1-758xuRfjsxDqxbhtO2Ym335UPj0=", + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha1-DGJGvI0sTRcqrC2z+xGQ1yvWVQQ=", "cpu": [ - "riscv64" + "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ - "linux" - ] + "android" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", - "integrity": "sha1-aM9hovwCFx0fpiVotzwdlC+C6Aw=", + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha1-/DjU1jWNjcHPU/CfdYn+Q262SAE=", "cpu": [ - "riscv64" + "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ - "linux" - ] + "android" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", - "integrity": "sha1-ktOTykfaDQPRwc/7PXNA8mw6U9I=", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha1-+Dr+6sHX2sAcei/QErPkUaBZH8w=", "cpu": [ - "s390x" + "arm64" ], "dev": true, - "libc": [ - "glibc" + "license": "MIT", + "optional": true, + "os": [ + "darwin" ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha1-UQFHwFWnlViNu+FP1rG4rQovMN4=", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "darwin" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", - "integrity": "sha1-9uXFxR+WrimGF/om2lRnWs1g47w=", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha1-CTuSAOzwsRW6Tl4kinSFycX4vV4=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha1-C+Irbfkl0hPoQeqHEjr134Cw+vc=", "cpu": [ "x64" ], "dev": true, - "libc": [ - "glibc" + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha1-vrEq1yuE9y0oSIzBuO6ffrFB11M=", + "cpu": [ + "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", - "integrity": "sha1-InqUnEgZCceB2KOcKA91VTzdFrw=", + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha1-G9vGUc2pupmVxT7ZxxzqplCUdi0=", "cpu": [ - "x64" + "arm64" ], "dev": true, - "libc": [ - "musl" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha1-uB+dVVKbRcIGpGoTghSxqmh5aWs=", + "cpu": [ + "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", - "integrity": "sha1-9XJB69tz07wjbnsSUpiECLITnBM=", + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha1-WYZnJBoEyZt27W75QKxQA4xBn5g=", "cpu": [ - "x64" + "loong64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" - ] + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", - "integrity": "sha1-Op/+WvcegxbdJxa1fdZCh6jB+gs=", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha1-HFHrnOqQP1PZe1rzsYQdtw9Vlso=", "cpu": [ - "arm64" + "mips64el" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openharmony" - ] + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", - "integrity": "sha1-fUpAOWrnnrweNjbB1WbH0YOLgAw=", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha1-Y91h8XzrMagSJ/QT/qyKcbwsUfI=", "cpu": [ - "arm64" + "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" - ] + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", - "integrity": "sha1-8jSrgBQdpF6+hXJ/cU6yDeLnLCo=", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha1-N2Owj95c8lqx+suOd1Lt/kX7/Cc=", "cpu": [ - "ia32" + "riscv64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" - ] + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", - "integrity": "sha1-XVpmTCPI/wUmuavXA7UP/glwPCo=", + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha1-GhN/8pOoKQbrMXY4W9fo4OXPt8s=", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha1-Jos2IRwUbKVPj+EsV4qNbviXlIU=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha1-Ilca2VHWK7aszILY0frVyMGsC6E=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha1-QvzFcpfrCgyj9fxHUpH0waP3wN4=", "cpu": [ "x64" ], "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha1-nrMq8QSsPaz07coB9ZZmSqsMc+8=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha1-/r7SQC1giCJekfIPtM4lIq0KTv0=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha1-hWQcPUZkKL+8zqXyHCaDZmP+9c4=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha1-pzb52JYkgQRfxMPlT1R58iyHD7Q=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha1-7lq0D60YYgG2UqM/il6xSenkJTI=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha1-xA0optmaEn2mcR8q/XSxHLY7Bqc=", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha1-shr/uATMFnwTPZX0WzodwTI7moc=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha1-5z/1fZeALwY5mVRfQ+uyseymXZ0=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha1-WmspDMv7GuL2d1r7dOmJi9jF1Og=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha1-s3Znt7wYHBaHgiWbq0JHT79StVA=", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha1-jcmvoqwVBssaWPiZQPHBJERsjfM=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha1-Y0Khn0Q0dRjJPkOxrGnes8Rlah8=", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha1-eg7mAfYPmaIMfHxf8MgDiMEYm9Y=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha1-9MZj6GLwbcmMpNRThixGkCeJoY0=", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha1-2xXWeByTHzolGj2sOVAcmKYIL9A=", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@js-joda/core": { + "version": "6.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@js-joda/core/-/core-6.1.0.tgz", + "integrity": "sha1-7YjQLO5WtJuGCDRn20i4TlwKqN4=", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha1-kpn4KHS6ueTH+cSNhlvsv+jWkHw=", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@microsoft/applicationinsights-web-snippet": { + "version": "1.2.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@microsoft/applicationinsights-web-snippet/-/applicationinsights-web-snippet-1.2.3.tgz", + "integrity": "sha1-ecTbbHe5aVuEeziI2sE4w7+Q1v0=", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha1-5X1DBpZgeGYgOAlPs465FG3Drqk=", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@nodable/entities": { + "version": "3.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@nodable/entities/-/entities-3.0.0.tgz", + "integrity": "sha1-aUcDvIZNMOrtVcLj3vANvWFJNnA=", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha1-wbA0beM2ulWvLVp5cIggN7rt7AU=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.220.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz", + "integrity": "sha1-su0jXeS188F2mt+l87wkfYLN+8k=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/configuration": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/configuration/-/configuration-0.221.0.tgz", + "integrity": "sha1-OdAGvk4nTGOKxIQyv9EQOAXPtYc=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "yaml": "^2.8.3" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + } + }, + "node_modules/@opentelemetry/configuration/node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha1-qnf3E450ulOZ1fO7DereDBaPALI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/context-async-hooks/-/context-async-hooks-2.10.0.tgz", + "integrity": "sha1-clms/Vk2rnE74/bTYkmt166Kylo=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.11.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.11.0.tgz", + "integrity": "sha1-l4SdOAi7plJWi31T4gyfw4vLfEk=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.221.0.tgz", + "integrity": "sha1-PDXCEyMoCRi4ygYLGnuvZLrCEko=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-logs": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/api-logs": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha1-ZrhAWxIv8cdu50O5vSynOACi4t8=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha1-qnf3E450ulOZ1fO7DereDBaPALI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.221.0.tgz", + "integrity": "sha1-BqsA2SdikoM4SOmsxc0iXyOGhGc=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-transformer": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/otlp-transformer": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/otlp-transformer/-/otlp-transformer-0.221.0.tgz", + "integrity": "sha1-6/xaZZWh43zk8gTJcRXI7YaNv7s=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/resources": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/resources/-/resources-2.10.0.tgz", + "integrity": "sha1-znbBwbqvzkx3vCmJCWW6H1ZnGsw=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/sdk-logs": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha1-nsDiy/tXKRe5SBAZg+CIaKaQkV4=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-metrics/-/sdk-metrics-2.10.0.tgz", + "integrity": "sha1-Go841FFkuj77s1Bb62jK9KPs8tk=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/sdk-trace": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz", + "integrity": "sha1-drAzEJJFKwGosvcESA4ldmQccBE=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.220.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.220.0.tgz", + "integrity": "sha1-NVvEwbfwvO1AzjZD+VE7tAU7Q94=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-exporter-base": "0.220.0", + "@opentelemetry/otlp-transformer": "0.220.0", + "@opentelemetry/sdk-logs": "0.220.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha1-Sd6GEG+GJVtHGy/wNe8QA6hRslI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.221.0.tgz", + "integrity": "sha1-ilIrLKWPTEa+AOam9V0RCKm5Phk=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-logs": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/api-logs": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha1-ZrhAWxIv8cdu50O5vSynOACi4t8=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha1-qnf3E450ulOZ1fO7DereDBaPALI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.221.0.tgz", + "integrity": "sha1-BqsA2SdikoM4SOmsxc0iXyOGhGc=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-transformer": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/otlp-transformer": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/otlp-transformer/-/otlp-transformer-0.221.0.tgz", + "integrity": "sha1-6/xaZZWh43zk8gTJcRXI7YaNv7s=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/resources/-/resources-2.10.0.tgz", + "integrity": "sha1-znbBwbqvzkx3vCmJCWW6H1ZnGsw=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-logs": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha1-nsDiy/tXKRe5SBAZg+CIaKaQkV4=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-metrics/-/sdk-metrics-2.10.0.tgz", + "integrity": "sha1-Go841FFkuj77s1Bb62jK9KPs8tk=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/sdk-trace": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz", + "integrity": "sha1-drAzEJJFKwGosvcESA4ldmQccBE=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.221.0.tgz", + "integrity": "sha1-XhlO0P+3any+CyxZJWnLEZ7enqA=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/exporter-metrics-otlp-http": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/api-logs": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha1-ZrhAWxIv8cdu50O5vSynOACi4t8=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha1-qnf3E450ulOZ1fO7DereDBaPALI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.221.0.tgz", + "integrity": "sha1-cDUx6y4946UwfWuAv5HwuLy6Y1U=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-metrics": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.221.0.tgz", + "integrity": "sha1-BqsA2SdikoM4SOmsxc0iXyOGhGc=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-transformer": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/otlp-transformer": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/otlp-transformer/-/otlp-transformer-0.221.0.tgz", + "integrity": "sha1-6/xaZZWh43zk8gTJcRXI7YaNv7s=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/resources": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/resources/-/resources-2.10.0.tgz", + "integrity": "sha1-znbBwbqvzkx3vCmJCWW6H1ZnGsw=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/sdk-logs": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha1-nsDiy/tXKRe5SBAZg+CIaKaQkV4=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-metrics/-/sdk-metrics-2.10.0.tgz", + "integrity": "sha1-Go841FFkuj77s1Bb62jK9KPs8tk=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/sdk-trace": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz", + "integrity": "sha1-drAzEJJFKwGosvcESA4ldmQccBE=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.220.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.220.0.tgz", + "integrity": "sha1-r2yD2kQP8yb4eXCZKyD3LEjWtBU=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-exporter-base": "0.220.0", + "@opentelemetry/otlp-transformer": "0.220.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-metrics": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha1-Sd6GEG+GJVtHGy/wNe8QA6hRslI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha1-geHOlG7sZhhXqdbE+lB7N1CuUA8=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.9.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", + "integrity": "sha1-QzmsY3udyZxZe6/e+wumbvc5Gnw=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { + "version": "0.220.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.220.0.tgz", + "integrity": "sha1-A8fd+QprIxkdLllmY3GgcrPrtNA=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.220.0", + "@opentelemetry/otlp-exporter-base": "0.220.0", + "@opentelemetry/otlp-transformer": "0.220.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-metrics": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha1-Sd6GEG+GJVtHGy/wNe8QA6hRslI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha1-geHOlG7sZhhXqdbE+lB7N1CuUA8=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.9.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", + "integrity": "sha1-QzmsY3udyZxZe6/e+wumbvc5Gnw=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.221.0.tgz", + "integrity": "sha1-xGCkPC1KO4/t45CIVbQcWB3k5yA=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha1-qnf3E450ulOZ1fO7DereDBaPALI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/resources": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/resources/-/resources-2.10.0.tgz", + "integrity": "sha1-znbBwbqvzkx3vCmJCWW6H1ZnGsw=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-metrics/-/sdk-metrics-2.10.0.tgz", + "integrity": "sha1-Go841FFkuj77s1Bb62jK9KPs8tk=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.221.0.tgz", + "integrity": "sha1-2S8EQxdCC4QYA9pmvbM/KNVrVOA=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-trace": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/api-logs": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha1-ZrhAWxIv8cdu50O5vSynOACi4t8=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha1-qnf3E450ulOZ1fO7DereDBaPALI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.221.0.tgz", + "integrity": "sha1-BqsA2SdikoM4SOmsxc0iXyOGhGc=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-transformer": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/otlp-transformer": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/otlp-transformer/-/otlp-transformer-0.221.0.tgz", + "integrity": "sha1-6/xaZZWh43zk8gTJcRXI7YaNv7s=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/resources": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/resources/-/resources-2.10.0.tgz", + "integrity": "sha1-znbBwbqvzkx3vCmJCWW6H1ZnGsw=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-logs": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha1-nsDiy/tXKRe5SBAZg+CIaKaQkV4=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-metrics/-/sdk-metrics-2.10.0.tgz", + "integrity": "sha1-Go841FFkuj77s1Bb62jK9KPs8tk=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-trace": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz", + "integrity": "sha1-drAzEJJFKwGosvcESA4ldmQccBE=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.220.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.220.0.tgz", + "integrity": "sha1-8mxseWRxXU7sjHC/sq8vHt5T1L8=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-exporter-base": "0.220.0", + "@opentelemetry/otlp-transformer": "0.220.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha1-Sd6GEG+GJVtHGy/wNe8QA6hRslI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha1-geHOlG7sZhhXqdbE+lB7N1CuUA8=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.221.0.tgz", + "integrity": "sha1-euqNeLlXxEyqJQIYfz+Tdxii6g8=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-trace": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/api-logs": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha1-ZrhAWxIv8cdu50O5vSynOACi4t8=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha1-qnf3E450ulOZ1fO7DereDBaPALI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.221.0.tgz", + "integrity": "sha1-BqsA2SdikoM4SOmsxc0iXyOGhGc=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-transformer": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/otlp-transformer": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/otlp-transformer/-/otlp-transformer-0.221.0.tgz", + "integrity": "sha1-6/xaZZWh43zk8gTJcRXI7YaNv7s=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/resources/-/resources-2.10.0.tgz", + "integrity": "sha1-znbBwbqvzkx3vCmJCWW6H1ZnGsw=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/sdk-logs": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha1-nsDiy/tXKRe5SBAZg+CIaKaQkV4=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-metrics/-/sdk-metrics-2.10.0.tgz", + "integrity": "sha1-Go841FFkuj77s1Bb62jK9KPs8tk=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/sdk-trace": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz", + "integrity": "sha1-drAzEJJFKwGosvcESA4ldmQccBE=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.10.0.tgz", + "integrity": "sha1-Xt8SwSoaGSVQg/lQg7HguPjZubc=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha1-qnf3E450ulOZ1fO7DereDBaPALI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/resources": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/resources/-/resources-2.10.0.tgz", + "integrity": "sha1-znbBwbqvzkx3vCmJCWW6H1ZnGsw=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/sdk-trace": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz", + "integrity": "sha1-drAzEJJFKwGosvcESA4ldmQccBE=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/instrumentation/-/instrumentation-0.221.0.tgz", + "integrity": "sha1-PUQSIfIY0g0D4YLCe+9cmcuea7U=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-bunyan": { + "version": "0.66.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/instrumentation-bunyan/-/instrumentation-bunyan-0.66.0.tgz", + "integrity": "sha1-jCXWIjs1Yi6XvvkNWZUJnMszCHk=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.221.0", + "@opentelemetry/instrumentation": "^0.221.0", + "@opentelemetry/semantic-conventions": "^1.41.1", + "@types/bunyan": "1.8.11" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-bunyan/node_modules/@opentelemetry/api-logs": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha1-ZrhAWxIv8cdu50O5vSynOACi4t8=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-console": { + "version": "0.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/instrumentation-console/-/instrumentation-console-0.3.0.tgz", + "integrity": "sha1-7PCCKinwhZKBB34vtgl6GMe+WcM=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.221.0", + "@opentelemetry/instrumentation": "^0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.1" + } + }, + "node_modules/@opentelemetry/instrumentation-console/node_modules/@opentelemetry/api-logs": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha1-ZrhAWxIv8cdu50O5vSynOACi4t8=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/instrumentation-http/-/instrumentation-http-0.221.0.tgz", + "integrity": "sha1-xZVIlBQ6QzU8vGWNxqtee2pASto=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/instrumentation": "0.221.0", + "@opentelemetry/semantic-conventions": "^1.29.0", + "forwarded-parse": "2.1.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha1-qnf3E450ulOZ1fO7DereDBaPALI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongodb": { + "version": "0.74.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.74.0.tgz", + "integrity": "sha1-8GnT+D4edDPAx2YorWSvLwVEexQ=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.221.0", + "@opentelemetry/semantic-conventions": "^1.33.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql": { + "version": "0.67.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.67.0.tgz", + "integrity": "sha1-BwiY0BShmjdKA70SQ71DxF+JOpk=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.221.0", + "@opentelemetry/semantic-conventions": "^1.33.0", + "@types/mysql": "2.15.27" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pg": { + "version": "0.73.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.73.0.tgz", + "integrity": "sha1-PnsYD3I9/cISPJzWuV58qvXakSc=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.9.0", + "@opentelemetry/instrumentation": "^0.221.0", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@opentelemetry/sql-common": "^0.42.0", + "@types/pg": "8.15.6", + "@types/pg-pool": "2.0.7" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-redis": { + "version": "0.69.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.69.0.tgz", + "integrity": "sha1-q2IlAYUq/dNjQCGhI6DGesBZa2w=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.221.0", + "@opentelemetry/redis-common": "^0.38.3", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-winston": { + "version": "0.65.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.65.0.tgz", + "integrity": "sha1-ar5mCg9BmxcWBRCaquoKBddW1NU=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.221.0", + "@opentelemetry/instrumentation": "^0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-winston/node_modules/@opentelemetry/api-logs": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha1-ZrhAWxIv8cdu50O5vSynOACi4t8=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation/node_modules/@opentelemetry/api-logs": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha1-ZrhAWxIv8cdu50O5vSynOACi4t8=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.220.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.220.0.tgz", + "integrity": "sha1-3MggQBoO/qT5CFKMVf2kyVUnCNc=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-transformer": "0.220.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha1-Sd6GEG+GJVtHGy/wNe8QA6hRslI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.221.0.tgz", + "integrity": "sha1-bOk7MecBfku+tEU2ltYHNVvVNSg=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.14.3", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/api-logs": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha1-ZrhAWxIv8cdu50O5vSynOACi4t8=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha1-qnf3E450ulOZ1fO7DereDBaPALI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.221.0.tgz", + "integrity": "sha1-BqsA2SdikoM4SOmsxc0iXyOGhGc=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-transformer": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/otlp-transformer": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/otlp-transformer/-/otlp-transformer-0.221.0.tgz", + "integrity": "sha1-6/xaZZWh43zk8gTJcRXI7YaNv7s=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/resources": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/resources/-/resources-2.10.0.tgz", + "integrity": "sha1-znbBwbqvzkx3vCmJCWW6H1ZnGsw=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/sdk-logs": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha1-nsDiy/tXKRe5SBAZg+CIaKaQkV4=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-metrics/-/sdk-metrics-2.10.0.tgz", + "integrity": "sha1-Go841FFkuj77s1Bb62jK9KPs8tk=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/sdk-trace": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz", + "integrity": "sha1-drAzEJJFKwGosvcESA4ldmQccBE=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.220.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/otlp-transformer/-/otlp-transformer-0.220.0.tgz", + "integrity": "sha1-yAerPZYCTWSw86rneIM0pC6vq3o=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-logs": "0.220.0", + "@opentelemetry/sdk-metrics": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha1-Sd6GEG+GJVtHGy/wNe8QA6hRslI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha1-geHOlG7sZhhXqdbE+lB7N1CuUA8=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.9.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", + "integrity": "sha1-QzmsY3udyZxZe6/e+wumbvc5Gnw=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-b3": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/propagator-b3/-/propagator-b3-2.10.0.tgz", + "integrity": "sha1-ZR5nCbZIfNOJ+ZBT1nV03LOkpV4=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-b3/node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha1-qnf3E450ulOZ1fO7DereDBaPALI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/propagator-jaeger/-/propagator-jaeger-2.10.0.tgz", + "integrity": "sha1-1ZF1vbopI2BgIS8j+PvKAhzR/gw=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger/node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha1-qnf3E450ulOZ1fO7DereDBaPALI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/redis-common": { + "version": "0.38.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/redis-common/-/redis-common-0.38.3.tgz", + "integrity": "sha1-MaBGSkipkcKUCGFONyXZTbfBGu4=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + } + }, + "node_modules/@opentelemetry/resource-detector-azure": { + "version": "0.29.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.29.0.tgz", + "integrity": "sha1-F7vNa3pV0ifZZZ96H1Gjk761DQo=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.37.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.11.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/resources/-/resources-2.11.0.tgz", + "integrity": "sha1-tOYLqSpPEi3JefNAKuqXE6mOrFs=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.220.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-logs/-/sdk-logs-0.220.0.tgz", + "integrity": "sha1-WQw2xenkm3gjYBtFrTm7ocomWoA=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha1-Sd6GEG+GJVtHGy/wNe8QA6hRslI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha1-geHOlG7sZhhXqdbE+lB7N1CuUA8=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.11.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-metrics/-/sdk-metrics-2.11.0.tgz", + "integrity": "sha1-JCqINQ+N3iZlNiMnON1kPErQyZA=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-node/-/sdk-node-0.221.0.tgz", + "integrity": "sha1-E+Pw4Kxq8YhOeXjesvAHssiO8e0=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/configuration": "0.221.0", + "@opentelemetry/context-async-hooks": "2.10.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/exporter-logs-otlp-grpc": "0.221.0", + "@opentelemetry/exporter-logs-otlp-http": "0.221.0", + "@opentelemetry/exporter-logs-otlp-proto": "0.221.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.221.0", + "@opentelemetry/exporter-metrics-otlp-http": "0.221.0", + "@opentelemetry/exporter-metrics-otlp-proto": "0.221.0", + "@opentelemetry/exporter-prometheus": "0.221.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.221.0", + "@opentelemetry/exporter-trace-otlp-http": "0.221.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.221.0", + "@opentelemetry/exporter-zipkin": "2.10.0", + "@opentelemetry/instrumentation": "0.221.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/propagator-b3": "2.10.0", + "@opentelemetry/propagator-jaeger": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0", + "@opentelemetry/sdk-trace-base": "2.10.0", + "@opentelemetry/sdk-trace-node": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/api-logs": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha1-ZrhAWxIv8cdu50O5vSynOACi4t8=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/core": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.10.0.tgz", + "integrity": "sha1-qnf3E450ulOZ1fO7DereDBaPALI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.221.0.tgz", + "integrity": "sha1-v8S9hJLinvxo/qbHwMtsPRivgwE=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-logs": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.221.0.tgz", + "integrity": "sha1-cDUx6y4946UwfWuAv5HwuLy6Y1U=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-metrics": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-metrics-otlp-proto": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.221.0.tgz", + "integrity": "sha1-yvFaQjaKu+SW25Jcq8Bw6rJ5BIc=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/exporter-metrics-otlp-http": "0.221.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.221.0.tgz", + "integrity": "sha1-tSimB24D3L4BiiscfleFM79N9/8=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-trace": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.221.0.tgz", + "integrity": "sha1-BqsA2SdikoM4SOmsxc0iXyOGhGc=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-transformer": "0.221.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/otlp-transformer": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/otlp-transformer/-/otlp-transformer-0.221.0.tgz", + "integrity": "sha1-6/xaZZWh43zk8gTJcRXI7YaNv7s=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/resources": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/resources/-/resources-2.10.0.tgz", + "integrity": "sha1-znbBwbqvzkx3vCmJCWW6H1ZnGsw=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-logs": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha1-nsDiy/tXKRe5SBAZg+CIaKaQkV4=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-metrics/-/sdk-metrics-2.10.0.tgz", + "integrity": "sha1-Go841FFkuj77s1Bb62jK9KPs8tk=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-trace": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz", + "integrity": "sha1-drAzEJJFKwGosvcESA4ldmQccBE=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.10.0.tgz", + "integrity": "sha1-P5v4oUwduoeQYZ4oMm98V/i5528=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.10.0.tgz", + "integrity": "sha1-ay7F8Vy5nJqsdHZDCAHR3JM+zxk=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "2.10.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/sdk-trace-base": "2.10.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace": { + "version": "2.9.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-trace/-/sdk-trace-2.9.0.tgz", + "integrity": "sha1-yCEDoRpxKzGskoG3Bvxdx20SauY=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.11.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.11.0.tgz", + "integrity": "sha1-P3JUAfz7FuWyM1wQPNyUiIKWMtE=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/sdk-trace": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/sdk-trace": { + "version": "2.11.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-trace/-/sdk-trace-2.11.0.tgz", + "integrity": "sha1-Rru6nTZnVd99b63tD3zSWvfyZcI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.11.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.11.0.tgz", + "integrity": "sha1-PhI+/y39o9qPXJOnxCsYXt/te8M=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "2.11.0", + "@opentelemetry/core": "2.11.0", + "@opentelemetry/sdk-trace-base": "2.11.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/context-async-hooks": { + "version": "2.11.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/context-async-hooks/-/context-async-hooks-2.11.0.tgz", + "integrity": "sha1-E3etPLESUQa6NtTNPEkzSE0VdpE=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-web": { + "version": "2.11.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sdk-trace-web/-/sdk-trace-web-2.11.0.tgz", + "integrity": "sha1-l8c8KEMSeMKVDZ7H3LNYKUL6HfA=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/sdk-trace-base": "2.11.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace/node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha1-Sd6GEG+GJVtHGy/wNe8QA6hRslI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace/node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha1-geHOlG7sZhhXqdbE+lB7N1CuUA8=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha1-8/Rn42wnMy8Oc17IbNzXjdbyeGU=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/sql-common": { + "version": "0.42.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/sql-common/-/sql-common-0.42.0.tgz", + "integrity": "sha1-wLOij9YyY3QciyfkroT8Y5Wi8h0=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0" + } + }, + "node_modules/@opentelemetry/winston-transport": { + "version": "0.31.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/winston-transport/-/winston-transport-0.31.0.tgz", + "integrity": "sha1-gHv9/QNkC+8mOvnY4RWNs9teeTI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.221.0", + "@opentelemetry/semantic-conventions": "^1.41.1", + "winston-transport": "4.*" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + } + }, + "node_modules/@opentelemetry/winston-transport/node_modules/@opentelemetry/api-logs": { + "version": "0.221.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha1-ZrhAWxIv8cdu50O5vSynOACi4t8=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha1-p36nQvqyV3UUVDTrHSMoz1ATrDM=", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha1-TIVzDlm5ofHzSQR9vyQpYDS7JzU=", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha1-2TFa188/MKrHC9o8BoRD3G8UNlk=", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha1-1RLLJsCuAmCR7iwRZ/G+b69chCo=", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha1-TW/ADI+2QBalyBtGnVSQRjUPEGU=", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha1-eNR2Mz2F1bHHkuJXvKdLoIDaSaQ=", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha1-0Dum6lT57IBojRU3Y80yWi0qWvY=", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha1-2142qoqVW0teCwJNZxIw7cXNwZE=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha1-HtHEOSLnubXQIO9l2EAuPIHtyG4=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha1-Cobngr96VG509THjleJP20XINSc=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha1-gvpRxUAYW1BjyLPGOqx5sV8oAa0=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha1-3x1ztWf9YuDPIcm1ff9/RL/Wljg=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha1-pYW6BBgCeltWdpPbOYLl5XVEpMc=", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha1-Eybw2yK2kKku/Y6qBukiaNx9G7Y=", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha1-lJGTn3zEOlsmqHdBf66v5puseaw=", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha1-pT2T2sMqzGcTJK8RU5MKtaBNhjk=", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha1-224GFz78hwvkmi32ktDAYHQXpnk=", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha1-pgc0o95Ae89L/kTQtkIiwLn7MLw=", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha1-KKZ+Fde6hjDvBEmAo5Ym4GJ9bnM=", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha1-d7tVOlFJQq9UBwdWdj27RMnGyys=", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha1-758xuRfjsxDqxbhtO2Ym335UPj0=", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha1-aM9hovwCFx0fpiVotzwdlC+C6Aw=", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha1-ktOTykfaDQPRwc/7PXNA8mw6U9I=", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha1-9uXFxR+WrimGF/om2lRnWs1g47w=", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha1-InqUnEgZCceB2KOcKA91VTzdFrw=", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha1-9XJB69tz07wjbnsSUpiECLITnBM=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha1-Op/+WvcegxbdJxa1fdZCh6jB+gs=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha1-fUpAOWrnnrweNjbB1WbH0YOLgAw=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha1-8jSrgBQdpF6+hXJ/cU6yDeLnLCo=", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha1-XVpmTCPI/wUmuavXA7UP/glwPCo=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha1-zRnWkTMMvVLrsTYgrLbPcUC5XoA=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@smithy/core": { + "version": "3.34.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@smithy/core/-/core-3.34.1.tgz", + "integrity": "sha512-dLcOUxz8YCv1RZUMKq6GbyUf95pLbrqh34bPvpCZ1+CByFF31BEAFewZjsGCnVsZTKdThNENfGyAgk2TJqVwSw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.5.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz", + "integrity": "sha1-2j6pY2tVZrNrB2E4LYQabY/d4U8=", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.8.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz", + "integrity": "sha1-NJRk4xaC1zmnTvpCNidWvknrKIM=", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.12.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz", + "integrity": "sha1-5NqTedIoUXuoWEUIHk5nWMoxXF0=", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.7.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@smithy/signature-v4/-/signature-v4-5.7.3.tgz", + "integrity": "sha1-dmA78ayapE4dL081iwzAz4SWHkY=", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.18.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@smithy/types/-/types-4.18.0.tgz", + "integrity": "sha1-9orKW1458fne84JyJOSh5D0jM0c=", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha1-YlFdi58ndGt2lQqDveGvgS2Rkjs=", + "dev": true, + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@types/bunyan": { + "version": "1.8.11", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/bunyan/-/bunyan-1.8.11.tgz", + "integrity": "sha1-C551eKWqI5D68SpGCCcVSQIpljg=", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha1-jpzZ4cNYH6azQaWu1ViOsoW+C0o=", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha1-ItHMnVQtNZPK6nZPl0MGqzYobuc=", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha1-M0MRlx06BxIefrkbaEpgXn7qnL0=", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha1-zz8Oh2177hWpOrkluCv1cKOQSiQ=", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha1-BSqmekjszEMJ1/AZG35BQ0uQu3g=", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mysql": { + "version": "2.15.27", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/mysql/-/mysql-2.15.27.tgz", + "integrity": "sha1-+xOw6GFNOdQvQPOBIX7DIVkV8ek=", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/node/-/node-24.13.3.tgz", + "integrity": "sha1-SfGL08ZHhm3NpRoHVsFF4UWQzhY=", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/pg": { + "version": "8.15.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/pg/-/pg-8.15.6.tgz", + "integrity": "sha1-TfdZC5rFV8vlR54AdOwVQMvdrZs=", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/pg-pool": { + "version": "2.0.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/pg-pool/-/pg-pool-2.0.7.tgz", + "integrity": "sha1-wXlFp0Ry2aO+r45m1apvyTgyhzQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/pg": "*" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.24", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/readable-stream/-/readable-stream-4.0.24.tgz", + "integrity": "sha1-eyQxv8E2fQXFNZitNglLBEcm1EI=", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha1-dP75/7qhmOuLWIvgKfOLACmcqiw=", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha1-dCt37DTVhVS5SnahTO8w1Z48Frk=", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha1-5eBtzT6S1OYi7wEpY3cH1mwo1qQ=", + "dev": true, + "license": "MIT" + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.9", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.9.tgz", + "integrity": "sha1-MyhVu9cpqzbgd5st/CTtySDRJGE=", + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/coverage-v8/-/coverage-v8-3.2.7.tgz", + "integrity": "sha1-LpzhEDRFwjeqpCCn8AWBJf5KeFQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.7", + "vitest": "3.2.7" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha1-cKNBWDg9AIw79dgC4mQzF/Cd9tg=", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha1-MxvpRMt4PGQt1CvXQ0EayiTqBGY=", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha1-KntZP44Afp2O9+c0OqMOxz/eryk=", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha1-wMCAIoGJ8fps2kD1m+CddGsKylE=", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha1-o6fhlQzpnsTPAjleIN3KQDtsgY4=", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha1-yn++5EAZUjykUDldmiKEzp7OHzE=", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha1-MCyBJiEaxN/qh7O1CFwJjW0i6J4=", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha1-6vVNU7YrrkE46AnKIlyEOabvs5I=", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha1-u89LpQdUZ/PyEx6rPP/HPC9deJU=", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha1-T68BstbTJr/u2XrqH1IiC19MGUA=", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha1-frFVexugXvGLXtDsZ1kb+rBGiO8=", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha1-48121MVI7oldPD/Y3B9sW5Ay56g=", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha1-JHyOe3ChpDsQzhTAIm/L9Y6IFdU=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha1-wETV3MUhoHZBNHJZehrLHxA8QEE=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha1-KqwA4I3603JsHUYuYNvC+DFlmkQ=", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/applicationinsights": { + "version": "3.16.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/applicationinsights/-/applicationinsights-3.16.0.tgz", + "integrity": "sha1-uhYytS5rRQDIeDiplxLY4wSTnpQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/core-auth": "^1.9.0", + "@azure/functions": "^4.11.2", + "@azure/identity": "^4.13.1", + "@azure/monitor-opentelemetry": "^1.19.0", + "@azure/monitor-opentelemetry-exporter": ">=1.0.0-beta.41 <1.0.0-c || ^1.0.0", + "@azure/opentelemetry-instrumentation-azure-sdk": "^1.0.0-beta.7", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/api-logs": "^0.220.0", + "@opentelemetry/core": "^2.9.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.220.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.220.0", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.220.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.220.0", + "@opentelemetry/otlp-exporter-base": "^0.220.0", + "@opentelemetry/resources": "^2.9.0", + "@opentelemetry/sdk-logs": "^0.220.0", + "@opentelemetry/sdk-metrics": "^2.9.0", + "@opentelemetry/sdk-trace-base": "^2.9.0", + "@opentelemetry/sdk-trace-node": "^2.9.0", + "@opentelemetry/semantic-conventions": "^1.43.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/args": { + "version": "5.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/args/-/args-5.0.3.tgz", + "integrity": "sha1-lDJW24UCGoVoS+LwiC8l15YnhwI=", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "5.0.0", + "chalk": "2.4.2", + "leven": "2.1.0", + "mri": "1.1.4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha1-9kGhlrM1aQsQcL8AtudZP+wZC/c=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha1-jrG3yG74SZhZvnYbF//ZFAbAw28=", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/async/-/async-3.2.6.tgz", + "integrity": "sha1-Gwco4Ukp1RuFtEm38G4nwRReOM4=", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true, + "license": "MIT" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha1-FX3Xfp8ZsdEjZ46T8SDm8ZMCJkE=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios": { + "version": "1.20.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/axios/-/axios-1.20.0.tgz", + "integrity": "sha1-UVUTRFqmDnHQS2UhymIQgpzLR4Y=", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha1-Sf/1hXfP7j83F2/qtMIuAPhtf3c=", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha1-KOhk4beG2+u2jbH0UvljUnhmWCc=", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/axios/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha1-xZ7yJKBP6LdU89sAY6Jeow0ABdY=", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/axios/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha1-u6vNwChZ9JhzAchW4zh85exDv3A=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/axios/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha1-OBqHG2KnNEUGYK497uRIE/cNlZo=", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/azurite": { + "version": "3.37.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/azurite/-/azurite-3.37.0.tgz", + "integrity": "sha1-kM2QzoY2YURBy5pagulLE9hnLDc=", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/ms-rest-js": "^2.7.0", + "applicationinsights": "^3.16.0", + "args": "^5.0.1", + "axios": "^1.18.1", + "etag": "^1.8.1", + "express": "^5.2.1", + "fs-extra": "^11.1.1", + "glob-to-regexp": "^0.4.1", + "jsonwebtoken": "^9.0.0", + "lokijs": "^1.5.6", + "morgan": "^1.11.0", + "multistream": "^4.1.0", + "mysql2": "^3.10.1", + "sequelize": "^6.31.0", + "stoppable": "^1.1.0", + "tedious": "^20.0.0", + "tslib": "^2.3.0", + "uri-templates": "^0.2.0", + "winston": "^3.1.0", + "xml2js": "^0.6.0" + }, + "bin": { + "azurite": "dist/src/azurite.js", + "azurite-blob": "dist/src/blob/main.js", + "azurite-queue": "dist/src/queue/main.js", + "azurite-table": "dist/src/table/main.js" + }, + "engines": { + "node": ">=22.0.0", + "vscode": "^1.39.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha1-v7EGYv7tgZaixi58aOF3IMJ0F5o=", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha1-GxtEAWClv3rUC2UPCVljSBkDkwo=", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha1-uZgnm/R844NEtPPPkW1Gebv1Hjo=", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=", + "dev": true, + "license": "MIT" + }, + "node_modules/bl": { + "version": "6.1.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/bl/-/bl-6.1.6.tgz", + "integrity": "sha1-tA866mljxnQmFqlX77dCxPuH7Ls=", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha1-ztvYoRRsE9//jasUBoAo1YwVrJE=", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha1-bYZi9NjDNgKLismqJCUbDKZLpDc=", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha1-2TicQ8Coz2o1XbRk0h4HCSpASTo=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha1-TqOb8x4wUYRSLXrXv9kTieTwy3k=", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha1-fHJDiAm1+lur9UGZofHCgaaYT88=", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha1-Ks5XhFnMj74qcKqo9S7mO2p0xsY=", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=", + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha1-87lrNBYNZDGhnXaIE1r3z7h5eIk=", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha1-iwvuuYYFrfGxKPpDhkA8AJ4CIaU=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cac/-/cac-6.7.14.tgz", + "integrity": "sha1-gE4eb1Bu42PLDjzLsJytXdmHCVk=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha1-S1QowiK+mF15w9gmV0edvgtZstY=", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha1-I43pNdKippKSjFOMfM+pEGf9Bio=", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "5.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha1-AylVJ9WL081Kp1Nj81sujZe+L0I=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/chai/-/chai-5.3.3.tgz", + "integrity": "sha1-3T2pVeJwkWpL0/Yl9LkZmWrafgY=", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha1-zUJUFnelQzPPVBpJEIwUMrRMlCQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha1-QfuyAkPlCxK+DwS43tvwdSDOhB0=", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha1-u3GFBpDh8TZWfeYp0tVHHe2kweg=", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/chalk/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true, + "license": "MIT" + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha1-JCc2ERe3DMqNyJaA6tMrFXAZyvU=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cjs-module-lexer/-/cjs-module-lexer-2.2.1.tgz", + "integrity": "sha1-qzWwPFat4F/hcMcOZ66J9gZmhHw=", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha1-DASwddsCy/5g3I5s8vVIaxo2CKo=", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha1-CCyyyJyf6GWaMRpTvWpNxTAdswQ=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha1-7dgDYornHATIWuegkG7a00tkiTc=", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha1-6Bj9ac5cz8tARZT4QpY79TFkzDc=", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha1-JpxxF9J7Ba0uU2gwqOyJXvnG0BA=", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha1-nibGPTD1NEPpSJSVshBdN7Z6hdk=", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha1-Z+FFz/UQpqaYS98RUpEdadLrnkM=", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/color/-/color-5.0.3.tgz", + "integrity": "sha1-95OQsbd44iL/u1QwTT2+rvYz+X8=", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha1-ctOmjVmMm9s68q0ehPIdiWq9TeM=", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha1-wqCah6y95pVD3m9j+jmVyCbFNqI=", + "dev": true, + "license": "MIT" + }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha1-nc9Wb/l24jNoyL1nP1w1EDq0EFg=", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/color-name/-/color-name-2.1.1.tgz", + "integrity": "sha1-hsAOyY2zZwW+W9RCgJfxOvEXWm0=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha1-22YnuXGBy4+s3851WuJvl6sHEfE=", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "2.1.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/color-name/-/color-name-2.1.1.tgz", + "integrity": "sha1-hsAOyY2zZwW+W9RCgJfxOvEXWm0=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha1-w9RaizT9cwYxoRCoolIGgrMdWn8=", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha1-89t4nHUtRVZMx+nh4LMXkNSjjhc=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha1-i3cxYmVtHRCGeEyPI6VM5tc9eRg=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha1-VWNpxHKiupEPKXmJG1JrNDYjftc=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha1-V8f8PMKTrKuf7FTXPhVpDr5KF5M=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha1-ilj+ePANzXDDcEUXWd+/rwPo7p8=", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/debug/-/debug-4.4.3.tgz", + "integrity": "sha1-xq5DLZvZZiWC/OCHCbA4xY6ePWo=", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha1-S3VtjXcKklcwCCXVKiws/5nDo0E=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/default-browser": { + "version": "5.5.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha1-F5Cv/FJoD7sR4XyrJ1LWmqKjfSw=", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha1-96fMuPUQS/jg9xujscz6Xq/bIeg=", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha1-27Ga37dG1/xtc0oGty9KANAhJV8=", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/depd/-/depd-2.0.0.tgz", + "integrity": "sha1-tpYWPMdXVg0JzyLMj60Vcbeedt8=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dottie": { + "version": "2.0.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/dottie/-/dottie-2.0.7.tgz", + "integrity": "sha1-/RtGWhIRs11WYOlbabttW1gJs8Q=", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha1-165mfh3INIL4tw/Q9u78UNow9Yo=", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha1-aWzi7Aqg5uqTo5f/zySqeEDIJ8s=", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha1-rg8PothQRe8UqBfao86azQSJ5b8=", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha1-hAyIA7DYBH9P8M+WMXazLU7z7XI=", + "dev": true, + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha1-+d2S7C1vS7wNXR5k4h1hzUZl58I=", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha1-e46omAd9fkCdOsRUdOo46vCFelg=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha1-mD6y+aZyTpMD9hrd8BHHLgngsPo=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha1-BfdaJdq5jk+x3NXhRywFRtUFfI8=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha1-kVlgFWGICoXyc0VgqQmbLDHlNyo=", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha1-otCzcyBXJN+lJdI7DD4bHKWCyZs=", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha1-8x274MGDsAptJutjJcgQwP0YvU0=", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha1-D0O9G62VW3LSTiJh46vllXzPCBY=", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha1-ARo/aYVroYnf+n3I/M6Z0qh5A+U=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha1-Z8PlSexAKkh7T8GT0ZU6UkdSNA0=", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha1-XU0+vflYPWOlMzzi3rdICrKwV4k=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/events/-/events-3.3.0.tgz", + "integrity": "sha1-Mala0Kkk4tLEGagTrrLE6HjqdAA=", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha1-JO338MxppE0AhWe6RZSrlvPDo9Y=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/express/-/express-5.2.1.tgz", + "integrity": "sha1-jyHRW20yf5K0eU7PjLCKcvlWrAQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-xml-builder": { + "version": "1.3.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fast-xml-builder/-/fast-xml-builder-1.3.1.tgz", + "integrity": "sha1-7wWzU7RVl0g9+wnUUKBiorrsOoI=", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.6.2", + "xml-naming": "^0.3.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.11.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fast-xml-parser/-/fast-xml-parser-5.11.1.tgz", + "integrity": "sha1-DuZy/M4XEMOBhbgpDXfpiAb/C/s=", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^3.0.0", + "fast-xml-builder": "^1.2.0", + "is-unsafe": "^2.0.0", + "path-expression-matcher": "^1.6.2", + "strnum": "^2.4.2", + "xml-naming": "^0.3.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha1-7Sq5Z6MxreYvGNB32uGSaE1Q01A=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha1-TZzNvGHoYpsln9ymfmWJFEjVaf0=", + "dev": true, + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha1-osUXplWYUrzbBtH4vX9Rto+tgJk=", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha1-JsrYAXlnrqhzG8QpYdBKPVmIrMw=", + "dev": true, + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha1-KEdKFZ07nRHvYgUKFO1g5N9tYbw=", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha1-Mujp7Rtoo0l777msK2rfkqY4V28=", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "2.5.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/form-data/-/form-data-2.5.6.tgz", + "integrity": "sha1-7zmz2Z4vyfJUIMDbeWL+Nsr80kQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha1-u6vNwChZ9JhzAchW4zh85exDv3A=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha1-OBqHG2KnNEUGYK497uRIE/cNlZo=", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha1-ImmTZCiq1MFcfr6XeahL8LKoGBE=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded-parse": { + "version": "2.1.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha1-CFEe3aqi3f1WuhETju598RegkyU=", + "dev": true, + "license": "MIT" + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha1-jdffahs6Gzpc8YbAWl3SZ2ImNaQ=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "11.4.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha1-+I7W0YCsnhS2GwjmeUaPCxtrRzA=", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha1-ysZAd4XQNnWipeGlMFxpezR9kNY=", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha1-LALYZNl/PqbIgwxGTL0Rq26rehw=", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha1-8GlhdpDBDIaOc7hGV0Z2T5fDR58=", + "dev": true, + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha1-T5RBKoLbMvNuOwuXQfipf+sDH34=", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha1-dD8OO2lkqTpUke0b/6rgVNf5jQE=", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha1-FQs/J0OGnvPoUewMSdFbHRTQDuE=", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/glob/-/glob-10.5.0.tgz", + "integrity": "sha1-jsA1WRnNMzjChCiiPU8k7MX+c4w=", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha1-x1KXCHyFG5pXi9IX3VmpL1n+VG4=", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha1-6D46fj8wCzTLnYf2FfoMvzV2kO4=", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha1-WJ2rEcABjQNmvmTNi/Esjb7MgyY=", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha1-mwy5/LeAh/b9fqur4lEcTT1gV04=", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha1-ifVrghe9vIgCvSmd9tfxCB1+UaE=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha1-QYPk6L8Iu24Fu7L30uDI9xLKQOM=", + "dev": true, + "license": "ISC" + }, + "node_modules/happy-dom": { + "version": "18.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/happy-dom/-/happy-dom-18.0.1.tgz", + "integrity": "sha1-j4ohmUYoQt62dfVOQFr1XztnRUA=", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "whatwg-mimetype": "^3.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/happy-dom/node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/node/-/node-20.19.43.tgz", + "integrity": "sha1-/Oz1gLpCoNtVz0BMNyyXlzw3bJc=", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/happy-dom/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha1-aR0ArzkJvpOn+qE75hs6W1DvEss=", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha1-/JxqeDoISVHQuXH+EBjegTcHozg=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha1-LNxC1AvvLltO6rfAGnPFTOerWrw=", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha1-jGLYy5C+sqrV0KW2dYGtmFTD8AM=", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha1-39YAJ9o2o238viNiYsAKWCJoFFM=", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha1-NtL2W8kJyHkAGN02+02T2myq4Gs=", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.63.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", - "integrity": "sha1-zRnWkTMMvVLrsTYgrLbPcUC5XoA=", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha1-mosfJGhmwChQlIZYX2K48sGMJw4=", "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha1-jpzZ4cNYH6azQaWu1ViOsoW+C0o=", - "dev": true, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha1-2o3+rH2hMLBcK6S1nJts1mYRprk=", "license": "MIT", "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" } }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha1-M0MRlx06BxIefrkbaEpgXn7qnL0=", + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha1-hO4S+WPn3lC8AaE+FgoHizsPQV8=", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha1-zz8Oh2177hWpOrkluCv1cKOQSiQ=", + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha1-jrehCmP/8l0VpXsAFYbRd9Gw01I=", "dev": true, - "license": "MIT" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" }, - "node_modules/@types/node": { - "version": "24.13.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/node/-/node-24.13.3.tgz", - "integrity": "sha1-SfGL08ZHhm3NpRoHVsFF4UWQzhY=", + "node_modules/import-in-the-middle": { + "version": "3.5.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/import-in-the-middle/-/import-in-the-middle-3.5.1.tgz", + "integrity": "sha512-mPKuL8bPQzecui2KK6Gb+M8JvJoHnhS1FeYGa22QopBmlevF5F0FE6ued/B5EgHDeIoMTONIpDWWFKUPOG0DBQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "undici-types": "~7.18.0" + "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^3.0.2", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@types/whatwg-mimetype": { + "node_modules/import-in-the-middle/node_modules/es-module-lexer": { "version": "3.0.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", - "integrity": "sha1-5eBtzT6S1OYi7wEpY3cH1mwo1qQ=", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/es-module-lexer/-/es-module-lexer-3.0.2.tgz", + "integrity": "sha1-jMCQl92Aqcu3lDf9QFuTXKx2zaM=", "dev": true, "license": "MIT" }, - "node_modules/@vitest/coverage-v8": { - "version": "3.2.7", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/coverage-v8/-/coverage-v8-3.2.7.tgz", - "integrity": "sha1-LpzhEDRFwjeqpCCn8AWBJf5KeFQ=", + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha1-ZappbE4tpiJbFI16FUxEk2ZjOjI=", + "dev": true, + "engines": [ + "node >= 0.4.0" + ], + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w=", + "dev": true, + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha1-v/OFQ+64mEglB5/zoqjmy9RngbM=", "dev": true, "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@bcoe/v8-coverage": "^1.0.2", - "ast-v8-to-istanbul": "^0.3.3", - "debug": "^4.4.1", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.17", - "magicast": "^0.3.5", - "std-env": "^3.9.0", - "test-exclude": "^7.0.1", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha1-kAk6oxBid9inelkQ265xdH4VogA=", + "license": "MIT", + "bin": { + "is-docker": "cli.js" }, - "peerDependencies": { - "@vitest/browser": "3.2.7", - "vitest": "3.2.7" + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@vitest/expect": { - "version": "3.2.7", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/expect/-/expect-3.2.7.tgz", - "integrity": "sha1-cKNBWDg9AIw79dgC4mQzF/Cd9tg=", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha1-8Rb4Bk/pCz94RKOJl8C3UFEmnx0=", "dev": true, "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha1-6B+6aZZi6zHb2vJnZqYdSBRxfqQ=", + "license": "MIT", "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@vitest/mocker": { - "version": "3.2.7", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/mocker/-/mocker-3.2.7.tgz", - "integrity": "sha1-MxvpRMt4PGQt1CvXQ0EayiTqBGY=", + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha1-Qv+fhCBsGZHSbev1IN1cAQQt0vM=", + "dev": true, + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true, + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha1-+sHj1TuXrVqdCunO8jifWBClwHc=", "dev": true, "license": "MIT", - "dependencies": { - "@vitest/spy": "3.2.7", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" + "engines": { + "node": ">=8" }, "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.7", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", - "integrity": "sha1-KntZP44Afp2O9+c0OqMOxz/eryk=", - "dev": true, + "node_modules/is-unsafe": { + "version": "2.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-unsafe/-/is-unsafe-2.0.2.tgz", + "integrity": "sha1-ux6tF/GqaI9kMyWLVh6YsaRaGvw=", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha1-MniXsmgyo+sRfabCdJLQTKEyWU8=", "license": "MIT", "dependencies": { - "tinyrainbow": "^2.0.0" + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@vitest/runner": { - "version": "3.2.7", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/runner/-/runner-3.2.7.tgz", - "integrity": "sha1-wMCAIoGJ8fps2kD1m+CddGsKylE=", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true, - "license": "MIT", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha1-LRZsSwZE1Do58Ev2wu3R5YXzF1Y=", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha1-kIMFusmlvRdaxqdEier9D8JEWn0=", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@vitest/utils": "3.2.7", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">=10" } }, - "node_modules/@vitest/snapshot": { - "version": "3.2.7", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/snapshot/-/snapshot-3.2.7.tgz", - "integrity": "sha1-o6fhlQzpnsTPAjleIN3KQDtsgY4=", + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha1-rK75SN93R8jrX78SZcuYD2NTpEE=", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "@vitest/pretty-format": "3.2.7", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">=10" } }, - "node_modules/@vitest/spy": { - "version": "3.2.7", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/spy/-/spy-3.2.7.tgz", - "integrity": "sha1-yn++5EAZUjykUDldmiKEzp7OHzE=", + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha1-y0U1FitXhKpiPO4hpyUs8sgHrJM=", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "tinyspy": "^4.0.3" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">=8" } }, - "node_modules/@vitest/utils": { - "version": "3.2.7", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/utils/-/utils-3.2.7.tgz", - "integrity": "sha1-MCyBJiEaxN/qh7O1CFwJjW0i6J4=", + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha1-iDOp2Jq0rN5hiJQr0cU7Y5DtWoo=", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "@vitest/pretty-format": "3.2.7", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" + "@isaacs/cliui": "^8.0.2" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/ansi-regex": { - "version": "6.3.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ansi-regex/-/ansi-regex-6.3.0.tgz", - "integrity": "sha1-JHyOe3ChpDsQzhTAIm/L9Y6IFdU=", + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha1-zTs9wEWwxARVbIHdtXVsI+WdfPU=", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha1-3/51mbSou3/jCv+NAjUjTf+3mDE=", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha1-tuMXF/Isw3MwsIHOAFHtXeU68vY=", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "universalify": "^2.0.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha1-wETV3MUhoHZBNHJZehrLHxA8QEE=", - "dev": true, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha1-bNV6sB6bCsB8uEfVPTybbuMfeuI=", "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "engines": { + "node": ">=12", + "npm": ">=6" } }, - "node_modules/assertion-error": { + "node_modules/jwa": { "version": "2.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha1-9kGhlrM1aQsQcL8AtudZP+wZC/c=", - "dev": true, + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha1-v4F20a0M1y4PP1gzhZWhPhELyAQ=", "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" } }, - "node_modules/ast-v8-to-istanbul": { - "version": "0.3.12", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", - "integrity": "sha1-jrG3yG74SZhZvnYbF//ZFAbAw28=", - "dev": true, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/jws/-/jws-4.0.1.tgz", + "integrity": "sha1-B+3Bvo+sIOZ3soPs4mFJi9OPBpA=", "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", - "js-tokens": "^10.0.0" + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" } }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha1-v7EGYv7tgZaixi58aOF3IMJ0F5o=", + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha1-4sVwo4ADiPtEQH6FFTHB1nCwYbM=", + "dev": true, + "license": "MIT" + }, + "node_modules/leven": { + "version": "2.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", "dev": true, "license": "MIT", "engines": { - "node": "18 || 20 || >=22" + "node": ">=0.10.0" } }, - "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha1-fHJDiAm1+lur9UGZofHCgaaYT88=", + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha1-/ytmwfYybVlRPeJAe/iBQ5gSdxw=", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/logform/-/logform-2.7.0.tgz", + "integrity": "sha1-z8qXUo7ykPLhJaCDloBQArLQYNE=", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^4.0.2" + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" }, "engines": { - "node": "20 || >=22" + "node": ">= 12.0.0" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cac/-/cac-6.7.14.tgz", - "integrity": "sha1-gE4eb1Bu42PLDjzLsJytXdmHCVk=", + "node_modules/logform/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha1-7GzSN0QHALwjyiMIf1E8dVCJWLA=", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=0.1.90" } }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/chai/-/chai-5.3.3.tgz", - "integrity": "sha1-3T2pVeJwkWpL0/Yl9LkZmWrafgY=", + "node_modules/lokijs": { + "version": "1.5.12", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lokijs/-/lokijs-1.5.12.tgz", + "integrity": "sha1-y1WzcAm98J7nlSpq3d1VW4k2U6A=", "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=18" - } + "license": "MIT" }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha1-JCc2ERe3DMqNyJaA6tMrFXAZyvU=", + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/long/-/long-5.3.2.tgz", + "integrity": "sha1-HYRGMJWZkmLX17f4v9SozFUWf4M=", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha1-AJXPVtxbepp8CP9bGoeW7IrRfnY=", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha1-QQ/IoXtw5ZgBPfJXwkRrfzOD8Rk=", + "dev": true, + "license": "ISC" + }, + "node_modules/lru.min": { + "version": "1.1.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lru.min/-/lru.min-1.1.5.tgz", + "integrity": "sha1-vTJIP040ImHQ6wEbBtIhnmte+Bg=", "dev": true, "license": "MIT", "engines": { - "node": ">= 16" + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha1-ctOmjVmMm9s68q0ehPIdiWq9TeM=", + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha1-VnY+wJoPqAkd8nh5/ZTRkHjADZE=", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha1-wqCah6y95pVD3m9j+jmVyCbFNqI=", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha1-ilj+ePANzXDDcEUXWd+/rwPo7p8=", + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha1-gwHDx9ZnBKB3HrG610J08OwDZzk=", "dev": true, "license": "MIT", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/debug/-/debug-4.4.3.tgz", - "integrity": "sha1-xq5DLZvZZiWC/OCHCbA4xY6ePWo=", + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha1-w8IwencSd82WODBfkVwprnQbYU4=", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "semver": "^7.5.3" }, "engines": { - "node": ">=6.0" + "node": ">=10" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha1-S3VtjXcKklcwCCXVKiws/5nDo0E=", + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha1-oN10voHiqlwvJ+Zc4oNgXuTit/k=", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha1-aWzi7Aqg5uqTo5f/zySqeEDIJ8s=", - "dev": true, - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha1-hAyIA7DYBH9P8M+WMXazLU7z7XI=", - "dev": true, - "license": "MIT" - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha1-kVlgFWGICoXyc0VgqQmbLDHlNyo=", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.28.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/esbuild/-/esbuild-0.28.2.tgz", - "integrity": "sha1-D0O9G62VW3LSTiJh46vllXzPCBY=", + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha1-bwNUAN/jq51WB7x3VGzjDML5xrg=", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, "engines": { - "node": ">=18" + "node": ">= 0.8" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.2", - "@esbuild/android-arm": "0.28.2", - "@esbuild/android-arm64": "0.28.2", - "@esbuild/android-x64": "0.28.2", - "@esbuild/darwin-arm64": "0.28.2", - "@esbuild/darwin-x64": "0.28.2", - "@esbuild/freebsd-arm64": "0.28.2", - "@esbuild/freebsd-x64": "0.28.2", - "@esbuild/linux-arm": "0.28.2", - "@esbuild/linux-arm64": "0.28.2", - "@esbuild/linux-ia32": "0.28.2", - "@esbuild/linux-loong64": "0.28.2", - "@esbuild/linux-mips64el": "0.28.2", - "@esbuild/linux-ppc64": "0.28.2", - "@esbuild/linux-riscv64": "0.28.2", - "@esbuild/linux-s390x": "0.28.2", - "@esbuild/linux-x64": "0.28.2", - "@esbuild/netbsd-arm64": "0.28.2", - "@esbuild/netbsd-x64": "0.28.2", - "@esbuild/openbsd-arm64": "0.28.2", - "@esbuild/openbsd-x64": "0.28.2", - "@esbuild/openharmony-arm64": "0.28.2", - "@esbuild/sunos-x64": "0.28.2", - "@esbuild/win32-arm64": "0.28.2", - "@esbuild/win32-ia32": "0.28.2", - "@esbuild/win32-x64": "0.28.2" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha1-Z8PlSexAKkh7T8GT0ZU6UkdSNA0=", + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha1-6pIvZgY1oiSe5WXgRJ+VHmtgOAg=", "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/expect-type": { - "version": "1.4.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/expect-type/-/expect-type-1.4.0.tgz", - "integrity": "sha1-JO338MxppE0AhWe6RZSrlvPDo9Y=", + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha1-zds+5PnGRTDf9kAjZmHULLajFPU=", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": ">=12.0.0" + "node": ">= 0.6" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha1-7Sq5Z6MxreYvGNB32uGSaE1Q01A=", + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha1-OQAtQYJXXVrwNv+hGBAPJSSy4qs=", "dev": true, "license": "MIT", - "engines": { - "node": ">=12.0.0" + "dependencies": { + "mime-db": "^1.54.0" }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "engines": { + "node": ">=18" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha1-Mujp7Rtoo0l777msK2rfkqY4V28=", + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha1-/ZVrvgt3JB6fFaxdzLHGOAYJaO8=", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" + "brace-expansion": "^5.0.8" }, "engines": { - "node": ">=14" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha1-ysZAd4XQNnWipeGlMFxpezR9kNY=", + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha1-eTibTrG7LQA6m7qH1JLyvTe9xls=", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha1-tmL9zZP2yD0/JSidoM6ByNloW5Q=", + "dev": true, + "license": "MIT" + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/moment/-/moment-2.30.1.tgz", + "integrity": "sha1-+MkcB7enhuMMWZJt9TC06slpdK4=", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": "*" } }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/glob/-/glob-10.5.0.tgz", - "integrity": "sha1-jsA1WRnNMzjChCiiPU8k7MX+c4w=", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha1-ERcnuydHNKUYrhVLXKWJKD8FiWc=", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "moment": "^2.29.4" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": "*" + } + }, + "node_modules/morgan": { + "version": "1.12.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/morgan/-/morgan-1.12.1.tgz", + "integrity": "sha512-tljKC0ex20AjO58Ob/eZ53JloycbVswbVNCHx6V6VLGzqt/w8dIynVGL0G8qVjNKwiA7sYSogCrN6QtJ82IV+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.4.1", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha1-6D46fj8wCzTLnYf2FfoMvzV2kO4=", + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/debug/-/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true, "license": "MIT" }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha1-WJ2rEcABjQNmvmTNi/Esjb7MgyY=", + "node_modules/mri": { + "version": "1.1.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mri/-/mri-1.1.4.tgz", + "integrity": "sha1-fLHdG5tAkF8frAU6viW2cg9EdEo=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ms/-/ms-2.1.3.tgz", + "integrity": "sha1-V0yBOM4dK1hh8LRFedut1gxmFbI=", + "license": "MIT" + }, + "node_modules/multiformats": { + "version": "14.0.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/multiformats/-/multiformats-14.0.5.tgz", + "integrity": "sha1-PZgZ7e+4hdkCnO3K5WX9Co8wUt8=", + "license": "Apache-2.0 OR MIT" + }, + "node_modules/multistream": { + "version": "4.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/multistream/-/multistream-4.1.0.tgz", + "integrity": "sha1-e/AN/RGVVvvBU8/z3kxtR3kJ9ag=", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "once": "^1.4.0", + "readable-stream": "^3.6.0" } }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha1-mwy5/LeAh/b9fqur4lEcTT1gV04=", + "node_modules/mysql2": { + "version": "3.24.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mysql2/-/mysql2-3.24.4.tgz", + "integrity": "sha1-d1LeDPELHSmEEDvbxg9pbGB1bzE=", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.2" + "aws-ssl-profiles": "^1.1.2", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.3", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.5.1" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">= 8.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependencies": { + "@types/node": ">= 8" } }, - "node_modules/happy-dom": { - "version": "18.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/happy-dom/-/happy-dom-18.0.1.tgz", - "integrity": "sha1-j4ohmUYoQt62dfVOQFr1XztnRUA=", + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha1-xQxpILQ/JY9ZwWrdHlZlT1zAK7U=", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "^20.0.0", - "@types/whatwg-mimetype": "^3.0.2", - "whatwg-mimetype": "^3.0.0" + "lru.min": "^1.1.0" }, "engines": { - "node": ">=20.0.0" + "node": ">=8.0.0" } }, - "node_modules/happy-dom/node_modules/@types/node": { - "version": "20.19.43", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/node/-/node-20.19.43.tgz", - "integrity": "sha1-/Oz1gLpCoNtVz0BMNyyXlzw3bJc=", + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha1-9mot4Rmf/eD88hyKXxMQaxwIGRM=", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/happy-dom/node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha1-aR0ArzkJvpOn+qE75hs6W1DvEss=", + "node_modules/native-duplexpair": { + "version": "1.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/native-duplexpair/-/native-duplexpair-1.0.0.tgz", + "integrity": "sha1-eJkHjmS/PIo9cyYBs9QP8F21j6A=", "dev": true, "license": "MIT" }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=", + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha1-FuAD0NtKwk/Z3xaO34cWJd7q498=", "dev": true, "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha1-39YAJ9o2o238viNiYsAKWCJoFFM=", + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha1-2TicQ8Coz2o1XbRk0h4HCSpASTo=", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha1-8Rb4Bk/pCz94RKOJl8C3UFEmnx0=", + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha1-0PD6bj4twdJ+/NitmdVQvalNGH0=", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha1-g3UmXiG8IND6WCwi4bE0hdbgAhM=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha1-WMjEQRblSEWtV/FKsQsDUzGErD8=", "dev": true, "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, "engines": { - "node": ">=8" + "node": ">= 0.8" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha1-WdpPkcRfX5icbkvO3Fo7Cu1w/2U=", "dev": true, - "license": "ISC" + "license": "MIT", + "engines": { + "node": ">= 0.8" + } }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha1-LRZsSwZE1Do58Ev2wu3R5YXzF1Y=", + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" + "license": "ISC", + "dependencies": { + "wrappy": "1" } }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha1-kIMFusmlvRdaxqdEier9D8JEWn0=", + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha1-4GvBdK7SFO1Y7e3lc7Qzu/gny0U=", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" + "fn.name": "1.x.x" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/open/-/open-10.2.0.tgz", + "integrity": "sha1-udhVvgB2IOgLb7BfrJgUH+Yttzw=", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha1-rK75SN93R8jrX78SZcuYD2NTpEE=", + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha1-TxRxoBCCeob5TP2bByfjbSZ95QU=", "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, + "license": "BlueOak-1.0.0" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha1-naGee+6NEt/wUT7Vt2lXeTvC6NQ=", + "dev": true, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">= 0.8" } }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha1-y0U1FitXhKpiPO4hpyUs8sgHrJM=", + "node_modules/path-expression-matcher": { + "version": "1.6.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha1-VnxzwHGX6dzvJOkO3NxXEFZZkWg=", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha1-WB9q3mWMu6ZaDTOA3ndTKVBU83U=", "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha1-iDOp2Jq0rN5hiJQr0cU7Y5DtWoo=", + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha1-eWCmaIiFlKByCxKpEdGnQqufEdI=", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/cliui": "^8.0.2" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha1-3/51mbSou3/jCv+NAjUjTf+3mDE=", + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha1-eVxCDE98pFxbiHNm9iLuDJhSzM0=", "dev": true, - "license": "MIT" + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha1-AJXPVtxbepp8CP9bGoeW7IrRfnY=", + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha1-PsvsVUIWhbcKnahyss/z4cvtFxY=", "dev": true, "license": "MIT" }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha1-QQ/IoXtw5ZgBPfJXwkRrfzOD8Rk=", + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha1-iFXFooma8HLWrAXRHkYEWtDcYF0=", "dev": true, - "license": "ISC" + "license": "MIT", + "engines": { + "node": ">= 14.16" + } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha1-VnY+wJoPqAkd8nh5/ZTRkHjADZE=", + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha1-q8Ju5PN8VsDzrg/PCwZTzE4cD9k=", "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha1-lDvUY79bcbQXARX4D478mgwOt4w=", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=4.0.0" } }, - "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha1-gwHDx9ZnBKB3HrG610J08OwDZzk=", + "node_modules/pg-protocol": { + "version": "1.16.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/pg-protocol/-/pg-protocol-1.16.0.tgz", + "integrity": "sha1-z/sAiCZWHul3CooV3CHyadcxswU=", + "dev": true, + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha1-LQJQ1jZFT3z6O2rgOC/fqAYyVKM=", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" } }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha1-w8IwencSd82WODBfkVwprnQbYU4=", + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha1-PTIa8+q5ObCDyPkpodEs2oHCa2s=", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha1-YxM2ADTMs2s9xh7L3/eBIfkP4h8=", "dev": true, "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha1-/ZVrvgt3JB6fFaxdzLHGOAYJaO8=", + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha1-bnUTV4DH4Q3zQzvyJmxVLTXIxiA=", "dev": true, - "license": "BlueOak-1.0.0", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", "dependencies": { - "brace-expansion": "^5.0.8" + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "^10 || ^12 || >=14" } }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha1-eTibTrG7LQA6m7qH1JLyvTe9xls=", + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha1-SPj84FT7xpZxmZMpuINLdyZS2C4=", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=4" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ms/-/ms-2.1.3.tgz", - "integrity": "sha1-V0yBOM4dK1hh8LRFedut1gxmFbI=", + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha1-xAs9oCIsUA/x5RxdcBS2C3lpfHo=", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha1-9mot4Rmf/eD88hyKXxMQaxwIGRM=", + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha1-UbwIYAYAXlBhxZHO5yfyUxv2Qag=", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha1-tGDILLFYdQd4iBmgaqD//bNURpU=", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha1-ejkj6OMrDuL/aJ+O7W5FXAkKxn4=", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=12.0.0" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha1-TxRxoBCCeob5TP2bByfjbSZ95QU=", + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha1-8Z/mnOqzEe65S0LnDowgcPm6ECU=", "dev": true, - "license": "BlueOak-1.0.0" + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha1-WB9q3mWMu6ZaDTOA3ndTKVBU83U=", + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha1-p0h1aK2tV3z6qn6IxJyrOrMIGro=", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha1-eWCmaIiFlKByCxKpEdGnQqufEdI=", + "node_modules/qs": { + "version": "6.16.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/qs/-/qs-6.16.0.tgz", + "integrity": "sha1-wixyOiipIPOqzc6Cifq9Q+zLef0=", "dev": true, - "license": "BlueOak-1.0.0", + "license": "BSD-3-Clause", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": ">=0.6" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha1-PsvsVUIWhbcKnahyss/z4cvtFxY=", + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha1-1/Gb6BK7YnIUcrRdO+IZ7wlXK0c=", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha1-iFXFooma8HLWrAXRHkYEWtDcYF0=", + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha1-PjraWuVWj5CV2EN2/TpJuPsAClE=", "dev": true, "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, "engines": { - "node": ">= 14.16" + "node": ">= 0.10" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha1-PTIa8+q5ObCDyPkpodEs2oHCa2s=", + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha1-VqmzbqllwAxak+8x6xEaDxEFaWc=", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } }, - "node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha1-YxM2ADTMs2s9xh7L3/eBIfkP4h8=", + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">=0.10.0" } }, - "node_modules/postcss": { - "version": "8.5.26", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/postcss/-/postcss-8.5.26.tgz", - "integrity": "sha1-bnUTV4DH4Q3zQzvyJmxVLTXIxiA=", + "node_modules/require-in-the-middle": { + "version": "8.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha1-294lh/ZpOYYm1WsgyGirh78BzOQ=", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.17", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=9.3.0 || >=8.10.0 <9.0.0" } }, + "node_modules/retry-as-promised": { + "version": "7.1.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/retry-as-promised/-/retry-as-promised-7.1.1.tgz", + "integrity": "sha1-NiYkbwTBlB/xDOvPo98Fd/2Kstc=", + "dev": true, + "license": "MIT" + }, "node_modules/rollup": { "version": "4.63.1", "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/rollup/-/rollup-4.63.1.tgz", @@ -1965,11 +8142,86 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/router/-/router-2.2.0.tgz", + "integrity": "sha1-AZvmILcRyHZBFnzHm5kJDwCxRu8=", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha1-Lp5UxGZOwxBsW1Yw4knT1llcSRE=", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha1-Hq+fqb2x/dTsdfWPnNtOa3gn7sY=", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha1-TKL444XygxxDKnGbEIo7969Cod0=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo=", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/sax/-/sax-1.6.1.tgz", + "integrity": "sha1-TCPPYIwLaTq1S0tYiOks/pd7mEM=", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/semver/-/semver-7.8.5.tgz", "integrity": "sha1-ObZGA33VDBT7RR5+TKxY7YuGP2k=", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1978,6 +8230,133 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/send/-/send-1.2.1.tgz", + "integrity": "sha1-nqt0O4dPNVD0CiaGe/KGrWDT8+0=", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/sequelize": { + "version": "6.37.8", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/sequelize/-/sequelize-6.37.8.tgz", + "integrity": "sha1-cOYsnmgqIAkAUJMQRZHlnsLQ7Sw=", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha1-IQs5GvQAJ2L4IxiP1uz8dBMCB2g=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha1-fxhqSk5fW2Y616QpT/G/N88OmKk=", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha1-ZsmiSnP5/CjL5msJ/tPTPcrxtCQ=", + "dev": true, + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2001,6 +8380,82 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha1-6gLGLgXcS+pn1EQvD7ce4ZL44Ks=", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha1-wuC1oUpUCuvuO7xsP4ZmzJtQkSc=", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha1-1rtrN5Asb+9RdOX1M/q0xzKib0I=", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha1-Ed2hnVNo5Azp7CvcH7DsvAeQ7Oo=", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/siginfo/-/siginfo-2.0.0.tgz", @@ -2021,14 +8476,47 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha1-HOVlD93YerwJnto33P8CTCZnrkY=", + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha1-HOVlD93YerwJnto33P8CTCZnrkY=", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha1-SRS5A6L4toXRf994pw6RfocuREo=", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/sql-escaper": { + "version": "1.5.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/sql-escaper/-/sql-escaper-1.5.1.tgz", + "integrity": "sha1-+67lESlNH32pL7aMofruIlWDnHs=", + "dev": true, + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": "*" } }, "node_modules/stackback": { @@ -2038,6 +8526,16 @@ "dev": true, "license": "MIT" }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha1-j3XuzvdlteHPzcCA2llAntQk44I=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/std-env/-/std-env-3.10.0.tgz", @@ -2045,6 +8543,27 @@ "dev": true, "license": "MIT" }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha1-MtpWjoPqSIsI5NfqLDvMnXUBXVs=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha1-QvEUWUpGzxqOMLCoT1bHjD7awh4=", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/string-width/-/string-width-5.1.2.tgz", @@ -2169,6 +8688,21 @@ "dev": true, "license": "MIT" }, + "node_modules/strnum": { + "version": "2.4.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/strnum/-/strnum-2.4.2.tgz", + "integrity": "sha1-r0OrUaBtBCJwI/wuQsoikhTsEPA=", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.1" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/supports-color/-/supports-color-7.2.0.tgz", @@ -2182,6 +8716,28 @@ "node": ">=8" } }, + "node_modules/tedious": { + "version": "20.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tedious/-/tedious-20.0.0.tgz", + "integrity": "sha1-Lw66Jr7keS+G0bG9V5/v7QM9o84=", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/core-auth": "^1.10.1", + "@azure/identity": "^4.13.1", + "@azure/keyvault-keys": "^4.10.2", + "@js-joda/core": "^6.0.1", + "@types/node": ">=22", + "bl": "^6.1.4", + "iconv-lite": "^0.7.0", + "js-md4": "^0.3.2", + "native-duplexpair": "^1.0.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">=22" + } + }, "node_modules/test-exclude": { "version": "7.0.2", "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/test-exclude/-/test-exclude-7.0.2.tgz", @@ -2197,6 +8753,13 @@ "node": ">=18" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha1-adycGxdEbueakr9biEu0uRJ1BvU=", + "dev": true, + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tinybench/-/tinybench-2.9.0.tgz", @@ -2258,6 +8821,46 @@ "node": ">=14.0.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha1-O+NDIaiKgg7RvYDfqjPkefu43TU=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=", + "dev": true, + "license": "MIT" + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "dev": true, + "license": "MIT" + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha1-b95wJx3G5dc8oMOyTi2Sr7dEGYQ=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha1-YS7+TtI11Wfoq6Xypfq3AoCt6D8=", + "license": "0BSD" + }, "node_modules/tsx": { "version": "4.23.13", "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tsx/-/tsx-4.23.13.tgz", @@ -2277,6 +8880,49 @@ "fsevents": "~2.3.3" } }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha1-cvExSzSlsZLbASMk3yzFh8pH+Sw=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha1-cdGnBTKTWC4WrJ8+uvGrmqSeVXA=", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha1-2TicQ8Coz2o1XbRk0h4HCSpASTo=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/typescript/-/typescript-5.9.3.tgz", @@ -2298,6 +8944,71 @@ "dev": true, "license": "MIT" }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha1-Fo78IYCWTmOG0GHglN9hr+I5sY0=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-templates": { + "version": "0.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/uri-templates/-/uri-templates-0.2.0.tgz", + "integrity": "sha1-K1eEURzJCYaHMekjPCaAl9ELSZ8=", + "dev": true, + "license": "http://geraintluff.github.io/tv4/LICENSE.txt" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha1-gNW1ztJxu5r2xEXyGhoExgbO++I=", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/validator/-/validator-13.15.35.tgz", + "integrity": "sha1-gc9FXFHxW2nY00C+WRTz+rANv38=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "7.3.6", "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vite/-/vite-7.3.6.tgz", @@ -2469,6 +9180,13 @@ } } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/whatwg-mimetype": { "version": "3.0.0", "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", @@ -2479,6 +9197,17 @@ "node": ">=12" } }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/which/-/which-2.0.2.tgz", @@ -2512,6 +9241,54 @@ "node": ">=8" } }, + "node_modules/winston": { + "version": "3.19.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/winston/-/winston-3.19.0.tgz", + "integrity": "sha1-zB0SYvX0WUaQQIXP/+c++0t6WB0=", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha1-O7o0XeECl2VOpvM1GUJFYAA7O/k=", + "dev": true, + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha1-xsNwGaz0DlF8xrlGV6JaPUqjPow=", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -2609,6 +9386,177 @@ "engines": { "node": ">=8" } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true, + "license": "ISC" + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha1-h4PU32cdTVA2W+LuTHGRegVXuqs=", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml-naming": { + "version": "0.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/xml-naming/-/xml-naming-0.3.0.tgz", + "integrity": "sha1-RsHhi/4oWEeZgt0qzPNNFudJ7aI=", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha1-3QtjAIOqCcFh4lpNCQHisqkptJk=", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha1-vpuuHIoEbnazESdyY0fQrXACvrM=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha1-u3J3n1+kZRhrH0OPZ0+jR/2121Q=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha1-f0k00PfKjFb5UxSTndzS3ZHOHVU=", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha1-d53/5ryv7FlqcXLpgyiaWIZH+qo=", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha1-kJa87r+ZDSG7MfqVFuDt4pSnfTU=", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha1-CCyyyJyf6GWaMRpTvWpNxTAdswQ=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha1-6Bj9ac5cz8tARZT4QpY79TFkzDc=", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha1-JpxxF9J7Ba0uU2gwqOyJXvnG0BA=", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha1-nibGPTD1NEPpSJSVshBdN7Z6hdk=", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } } } } diff --git a/package.json b/package.json index e6863c4..9d6c577 100644 --- a/package.json +++ b/package.json @@ -13,17 +13,29 @@ "dev": "tsx watch src/web/server.ts", "example": "tsx examples/basic-flow.ts", "example:access-denied": "tsx examples/access-denied.ts", + "example:storage": "tsx examples/configured-storage.ts", "start": "tsx src/web/server.ts", "test": "vitest run", "test:coverage": "vitest run --coverage", + "test:integration": "vitest run --config vitest.integration.config.ts", + "test:integration:azure": "tsx scripts/run-azurite-integration.ts", + "test:integration:ipfs": "tsx scripts/run-ipfs-integration.ts", "typecheck": "tsc --noEmit" }, "devDependencies": { "@types/node": "^24.3.1", "@vitest/coverage-v8": "^3.2.4", + "azurite": "^3.37.0", "happy-dom": "^18.0.1", "tsx": "^4.20.5", "typescript": "^5.9.2", "vitest": "^3.2.4" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.1131.0", + "@azure/identity": "^4.13.3", + "@azure/keyvault-secrets": "^4.11.2", + "@azure/storage-blob": "^12.33.0", + "multiformats": "^14.0.5" } } diff --git a/scripts/run-azurite-integration.ts b/scripts/run-azurite-integration.ts new file mode 100644 index 0000000..1979e4a --- /dev/null +++ b/scripts/run-azurite-integration.ts @@ -0,0 +1,165 @@ +import { randomBytes } from "node:crypto"; +import { spawn, type ChildProcess } from "node:child_process"; +import { once } from "node:events"; +import { mkdir, mkdtemp, rm } from "node:fs/promises"; +import { createServer } from "node:net"; +import { join, resolve } from "node:path"; +import { setTimeout as delay } from "node:timers/promises"; + +async function unusedPort(): Promise { + const server = createServer(); + server.listen(0, "127.0.0.1"); + await once(server, "listening"); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("No emulator port allocated."); + await new Promise((done, reject) => server.close((error) => error ? reject(error) : done())); + return address.port; +} + +async function stop(child: ChildProcess): Promise { + if (child.pid === undefined || child.exitCode !== null || child.signalCode !== null) return; + const exited = once(child, "exit"); + child.kill(); + const graceful = await Promise.race([ + exited.then(() => true), delay(3_000, false, { ref: false }), + ]); + if (!graceful) { + child.kill("SIGKILL"); + await Promise.race([ + exited, + delay(3_000, undefined, { ref: false }).then(() => { + throw new Error("Emulator child did not terminate."); + }), + ]); + } +} + +async function main(): Promise { + if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0") { + console.error("Azure integration requires TLS certificate validation to remain enabled."); + throw new Error("Unsafe TLS configuration."); + } + const environment = { ...process.env }; + delete environment.AZURE_STORAGE_CONNECTION_STRING; + delete environment.AZURE_STORAGE_SAS_TOKEN; + delete environment.AZURE_LOG_LEVEL; + delete environment.AZURITE_DB; + // Azure loggers read this variable when imported, including readiness-request logging. + delete process.env.AZURE_LOG_LEVEL; + const { BlobServiceClient, StorageSharedKeyCredential } = await import("@azure/storage-blob"); + const port = await unusedPort(); + const workspace = resolve(".data"); + await mkdir(workspace, { recursive: true }); + const directory = await mkdtemp(join(workspace, "azurite-integration-")); + const account = "syntheticaccount"; + const key = randomBytes(32).toString("base64"); + const endpoint = `http://127.0.0.1:${port}/${account}`; + const emulator = spawn(process.execPath, [ + resolve("node_modules/azurite/dist/src/blob/main.js"), + "--silent", "--disableTelemetry", "--skipApiVersionCheck", "--blobHost", "127.0.0.1", + "--blobPort", `${port}`, "--location", directory, + ], { + env: { ...environment, AZURITE_ACCOUNTS: `${account}:${key}` }, + stdio: ["ignore", "pipe", "pipe"], + }); + let emulatorStage = "loading"; + let outputTail = ""; + const observeOutput = (chunk: Buffer): void => { + // Inspect bounded output only for fixed stage markers; never print emulator logs. + outputTail = (outputTail + chunk.toString("utf8")).slice(-8192); + if (outputTail.includes("service successfully listens")) emulatorStage = "listening"; + else if (outputTail.includes("service is starting")) emulatorStage = "binding"; + else if (outputTail.includes("EADDRINUSE")) emulatorStage = "port unavailable"; + else if (outputTail.includes("MODULE_NOT_FOUND")) emulatorStage = "missing dependency"; + }; + emulator.stdout?.on("data", observeOutput); + emulator.stderr?.on("data", observeOutput); + let launchFailed = false; + let interrupted = false; + let tests: ChildProcess | undefined; + let cleanupPromise: Promise | undefined; + const cleanup = (): Promise => { + cleanupPromise ??= (async () => { + try { + if (tests) await stop(tests); + } finally { + await stop(emulator); + await rm(directory, { recursive: true, force: true }); + } + })(); + return cleanupPromise; + }; + const onSignal = (signal: NodeJS.Signals) => { + interrupted = true; + void cleanup().then( + () => process.exit(signal === "SIGINT" ? 130 : 143), + () => process.exit(1), + ); + }; + process.once("SIGINT", onSignal); + process.once("SIGTERM", onSignal); + emulator.once("error", () => { launchFailed = true; }); + try { + const client = new BlobServiceClient(endpoint, new StorageSharedKeyCredential(account, key), { + retryOptions: { maxTries: 1, tryTimeoutInMs: 1_000 }, + }); + const container = client.getContainerClient(`synthetic-${randomBytes(8).toString("hex")}`); + let ready = false; + const started = Date.now(); + const deadline = started + 120_000; + let nextProgress = started + 10_000; + console.log("Launching loopback Azurite with telemetry disabled; startup deadline is 120 seconds."); + while (Date.now() < deadline) { + if (interrupted || launchFailed || emulator.exitCode !== null || emulator.signalCode !== null) { + console.error(`Azurite startup stopped during ${emulatorStage}.`); + throw new Error("Azurite failed to start."); + } + try { + await container.createIfNotExists({ abortSignal: AbortSignal.timeout(2_000) }); + ready = true; + break; + } catch { + // Startup readiness is the only retry loop; adapter failures are not retried. + if (Date.now() >= nextProgress) { + console.log(`Waiting for Azurite readiness (${Math.floor((Date.now() - started) / 1000)} seconds; stage: ${emulatorStage}).`); + nextProgress = Date.now() + 10_000; + } + await delay(250); + } + } + if (!ready) { + console.error(`Azurite startup deadline exceeded during ${emulatorStage}.`); + throw new Error("Azurite did not become ready."); + } + if (interrupted) throw new Error("Integration cancelled."); + console.log(`Loopback Azurite ready after ${Math.ceil((Date.now() - started) / 1000)} seconds; running external-provider integration tests.`); + tests = spawn(process.execPath, [ + resolve("node_modules/vitest/vitest.mjs"), "run", "--config", "vitest.integration.config.ts", + ], { + env: { + ...environment, + STORAGE_INTEGRATION: "true", + STORAGE_BACKEND: "azure", + STORAGE_EMULATOR: "true", + AZURE_STORAGE_ACCOUNT_URL: endpoint, + AZURE_STORAGE_CONTAINER: container.containerName, + AZURE_STORAGE_EMULATOR_ACCOUNT: account, + AZURE_STORAGE_EMULATOR_KEY: key, + }, + stdio: "inherit", + }); + const [exitCode] = await once(tests, "exit"); + if (exitCode !== 0) throw new Error("Azure emulator integration tests failed."); + } finally { + await cleanup(); + process.removeListener("SIGINT", onSignal); + process.removeListener("SIGTERM", onSignal); + } +} + +try { + await main(); +} catch { + console.error("Azure emulator integration failed. Ensure dependencies are installed and loopback ports are available."); + process.exitCode = 1; +} diff --git a/scripts/run-ipfs-integration.ts b/scripts/run-ipfs-integration.ts new file mode 100644 index 0000000..03df922 --- /dev/null +++ b/scripts/run-ipfs-integration.ts @@ -0,0 +1,245 @@ +import { randomBytes } from "node:crypto"; +import { spawn, type ChildProcess } from "node:child_process"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { setTimeout as delay } from "node:timers/promises"; +import { + CLUSTER_IMAGE, KUBO_IMAGE, SECRET_INIT_SCRIPT, containerArguments, loopbackEndpoint, secretInput, +} from "../test/fixtures/ipfs/fixture.js"; + +const prefix = `gphr-ipfs-${randomBytes(10).toString("hex")}`; +const network = `${prefix}-network`; +const secretsVolume = `${prefix}-secrets`; +let metadataDirectory: string | undefined; +const containers: string[] = []; +const volumes: string[] = []; +let networkCreated = false; +let interrupted = false; +let tests: ChildProcess | undefined; +let stage = "checking Docker engine"; + +async function docker(args: string[], input?: string, timeout = 60_000): Promise { + return new Promise((done, reject) => { + const child = spawn("docker", args, { + stdio: ["pipe", "pipe", "ignore"], windowsHide: true, + }); + let output = ""; + const timer = setTimeout(() => child.kill("SIGKILL"), timeout); + child.stdout.setEncoding("utf8"); + child.stdout.on("data", (chunk: string) => { + if (output.length < 1_000_000) output += chunk; + else child.kill("SIGKILL"); + }); + // Never include Docker output, argv, config, or a captured upstream exception in errors. + child.once("error", () => { clearTimeout(timer); reject(new Error("Docker command failed.")); }); + child.once("close", (code) => { + clearTimeout(timer); + if (code === 0) done(output.trim()); + else reject(new Error("Docker command failed.")); + }); + child.stdin.on("error", () => {}); + child.stdin.end(input); + }); +} + +async function request(url: string, init: RequestInit = {}): Promise { + const response = await fetch(url, { ...init, signal: AbortSignal.timeout(10_000) }); + if (!response.ok) { + await response.body?.cancel(); + throw new Error("Fixture API request failed."); + } + return response; +} + +async function waitUntil(check: () => Promise, timeout = 120_000): Promise { + const deadline = Date.now() + timeout; + while (Date.now() < deadline) { + if (interrupted) throw new Error("Fixture interrupted."); + try { + if (await check()) return; + } catch { + // Readiness is retried; neither adapter writes nor the integration suite are retried. + } + await delay(500); + } + throw new Error("Fixture readiness deadline exceeded."); +} + +async function volume(name: string): Promise { + volumes.push(name); + await docker(["volume", "create", name]); +} + +async function node(kind: "kubo" | "cluster", index: number, bootstrap?: string): Promise { + if (interrupted) throw new Error("Fixture interrupted."); + const name = `${prefix}-${kind}${index}`; + const dataVolume = `${name}-data`; + await volume(dataVolume); + containers.push(name); + await docker(containerArguments({ + name, network, alias: `${kind}${index}`, dataVolume, secretsVolume, + scriptDirectory: resolve("test/fixtures/ipfs"), kind, + ...(bootstrap ? { bootstrap } : {}), + })); + await docker(["start", name]); + const endpoint = loopbackEndpoint(await docker(["port", name, kind === "kubo" ? "5001/tcp" : "9094/tcp"])); + await waitUntil(async () => { + const info = await (await request(`${endpoint}${kind === "kubo" ? "/api/v0/id" : "/id"}`, + kind === "kubo" ? { method: "POST" } : {})).json() as { ID?: string; id?: string }; + return typeof (kind === "kubo" ? info.ID : info.id) === "string"; + }); + return endpoint; +} + +async function cleanup(): Promise { + if (tests && tests.exitCode === null && tests.signalCode === null) { + tests.kill("SIGKILL"); + await new Promise((done) => tests!.once("close", () => done())); + } + let failed = false; + for (const name of [...containers].reverse()) { + // A failed create may leave nothing to remove; only absence is safe to ignore. + try { + await docker(["container", "inspect", "--format", "{{.Id}}", name]); + } catch { continue; } + try { await docker(["rm", "--force", "--volumes", name]); } catch { failed = true; } + } + for (const name of [...volumes].reverse()) { + try { await docker(["volume", "rm", name]); } catch { failed = true; } + } + if (networkCreated) { + try { await docker(["network", "rm", network]); } catch { failed = true; } + } + if (metadataDirectory) await rm(metadataDirectory, { recursive: true, force: true }); + if (failed) throw new Error("Fixture cleanup incomplete."); +} + +async function runSuite(kubo: string, cluster: string): Promise { + metadataDirectory = await mkdtemp(join(tmpdir(), `${prefix}-index-`)); + const environment = { ...process.env }; + for (const setting of [ + "IPFS_API_AUTH", "IPFS_API_AUTH_FILE", "IPFS_API_AUTH_KEYVAULT_SECRET", + "IPFS_CLUSTER_AUTH", "IPFS_CLUSTER_AUTH_FILE", "IPFS_CLUSTER_AUTH_KEYVAULT_SECRET", + "AZURE_KEY_VAULT_URL", "AZURE_LOG_LEVEL", + ]) delete environment[setting]; + await new Promise((done, reject) => { + tests = spawn(process.execPath, [ + resolve("node_modules/vitest/vitest.mjs"), "run", "--config", "vitest.integration.config.ts", + ], { + env: { + ...environment, STORAGE_INTEGRATION: "true", STORAGE_BACKEND: "ipfs", + STORAGE_EMULATOR: "true", IPFS_PRIVATE_NETWORK: "true", + IPFS_API_URL: kubo, IPFS_CLUSTER_API_URL: cluster, IPFS_METADATA_BACKEND: "file", + STORAGE_FILE_DIRECTORY: metadataDirectory, IPFS_REPLICATION_MIN: "2", IPFS_REPLICATION_MAX: "3", + }, + stdio: "inherit", + }); + tests.once("error", () => reject(new Error("Integration tests could not start."))); + tests.once("close", (code) => code === 0 ? done() : reject(new Error("Integration tests failed."))); + }); +} + +async function survivalProbe(kubo: string[], cluster: string): Promise { + // Opaque synthetic bytes only: no patient tokens, application keys, or plaintext records. + const bytes = randomBytes(1024); + const form = new FormData(); + form.set("file", new Blob([bytes]), "synthetic-ciphertext.bin"); + const added = await (await request(`${kubo[0]}/api/v0/add?pin=false&cid-version=1`, { + method: "POST", body: form, + })).json() as { Hash: string }; + if (!/^[a-zA-Z0-9]+$/.test(added.Hash)) throw new Error("Invalid probe CID."); + await request(`${cluster}/pins/${added.Hash}?replication-min=2&replication-max=3`, { method: "POST" }); + // Require all three concrete Kubo replicas before killing the original ingestion node. + await waitUntil(async () => { + const pins = await Promise.all(kubo.map(async (endpoint) => { + const result = await (await request(`${endpoint}/api/v0/pin/ls?arg=${added.Hash}&type=recursive`, { + method: "POST", + })).json() as { Keys?: Record }; + return result.Keys?.[added.Hash] !== undefined; + })); + return pins.every(Boolean); + }); + stage = "verifying existing ciphertext after ingress-node loss"; + await docker(["stop", "--time", "5", `${prefix}-cluster1`, `${prefix}-kubo1`]); + const received = Buffer.from(await (await request(`${kubo[1]}/api/v0/cat?arg=${added.Hash}`, { + method: "POST", + })).arrayBuffer()); + if (!bytes.equals(received)) throw new Error("Replicated ciphertext was not preserved."); + console.log("Existing synthetic ciphertext survived ingress Kubo/Cluster loss; independent replica read matched."); +} + +async function main(): Promise { + const onSignal = () => { + interrupted = true; + tests?.kill("SIGKILL"); + }; + process.once("SIGINT", onSignal); + process.once("SIGTERM", onSignal); + try { + await docker(["info", "--format", "{{.ServerVersion}}"], undefined, 15_000); + stage = "pulling pinned official images"; + await docker(["pull", KUBO_IMAGE], undefined, 300_000); + await docker(["pull", CLUSTER_IMAGE], undefined, 300_000); + if (interrupted) throw new Error("Fixture interrupted."); + stage = "creating isolated network and secret volume"; + await docker(["network", "create", "--internal", "--driver", "bridge", network]); + networkCreated = true; + await volume(secretsVolume); + const initializer = `${prefix}-secret-init`; + containers.push(initializer); + await docker([ + "create", "--name", initializer, "--network", "none", "--interactive", + "--log-driver", "none", "--read-only", "--cap-drop", "ALL", + "--security-opt", "no-new-privileges", "--user", "0:0", + "--mount", `type=volume,source=${secretsVolume},target=/secrets`, + "--entrypoint", "/bin/sh", KUBO_IMAGE, "-c", SECRET_INIT_SCRIPT, + ]); + await docker(["start", "--attach", "--interactive", initializer], secretInput()); + stage = "starting private Kubo peers"; + const kubo = [await node("kubo", 1), await node("kubo", 2), await node("kubo", 3)]; + const ids = await Promise.all(kubo.map(async (endpoint) => + (await (await request(`${endpoint}/api/v0/id`, { method: "POST" })).json() as { ID: string }).ID)); + for (let from = 0; from < 3; from++) { + for (let to = 0; to < 3; to++) { + if (from === to) continue; + const address = `/dns4/kubo${to + 1}/tcp/4001/p2p/${ids[to]}`; + await request(`${kubo[from]}/api/v0/swarm/connect?arg=${encodeURIComponent(address)}`, { method: "POST" }); + } + } + stage = "bootstrapping three Raft Cluster peers"; + const cluster = await node("cluster", 1); + const identity = await (await request(`${cluster}/id`)).json() as { id: string }; + const bootstrap = `/dns4/cluster1/tcp/9096/p2p/${identity.id}`; + await node("cluster", 2, bootstrap); + await node("cluster", 3, bootstrap); + await waitUntil(async () => { + const text = await (await request(`${cluster}/peers`)).text(); + const peers = text.trim().split("\n").map((line) => JSON.parse(line) as { id?: string; error?: string }); + return peers.length === 3 && peers.every((peer) => peer.id && !peer.error); + }); + await waitUntil(async () => { + const metrics = await (await request(`${cluster}/monitor/metrics/freespace`)).json() as { valid?: boolean }[]; + return metrics.length === 3 && metrics.every((metric) => metric.valid); + }); + stage = "running application integration tests"; + console.log("Three private Kubo/Cluster peers ready; running application integration tests."); + await runSuite(kubo[0]!, cluster); + stage = "replicating independent synthetic ciphertext probe"; + await survivalProbe(kubo, cluster); + } finally { + const failedStage = stage; + stage = "cleaning up fixture-owned resources"; + await cleanup(); + stage = failedStage; + process.removeListener("SIGINT", onSignal); + process.removeListener("SIGTERM", onSignal); + } +} + +try { + await main(); +} catch { + console.error(`Private IPFS integration failed while ${stage}. Docker engine and Linux containers are required; no daemon logs or secrets are emitted.`); + process.exitCode = 1; +} diff --git a/src/index.ts b/src/index.ts index 0e4154b..ad30c61 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,3 +34,15 @@ export { } from "./storage/blob-store.js"; export { FileBlobStore } from "./storage/file-blob-store.js"; export { InMemoryBlobStore } from "./storage/in-memory-blob-store.js"; +export { AzureBlobStore } from "./storage/azure-blob-store.js"; +export { S3BlobStore, type S3BlobStoreOptions } from "./storage/s3-blob-store.js"; +export { + DistributedBlobStore, type DistributedContentStore, +} from "./storage/distributed-blob-store.js"; +export { + IpfsClusterClient, type IpfsClusterClientOptions, +} from "./storage/ipfs-cluster-client.js"; +export { createConfiguredBlobStore } from "./storage/configured-blob-store.js"; +export { + StorageConfigurationError, type StorageBackend, type StorageEnvironment, +} from "./storage/configuration.js"; diff --git a/src/storage/azure-blob-store.ts b/src/storage/azure-blob-store.ts new file mode 100644 index 0000000..f5c914f --- /dev/null +++ b/src/storage/azure-blob-store.ts @@ -0,0 +1,99 @@ +import type { ContainerClient } from "@azure/storage-blob"; +import { + ConcurrentUpdateError, + ObjectAlreadyExistsError, + ObjectNotFoundError, + type BlobStore, + type StoredObject, +} from "./blob-store.js"; +import { + assertBlobKey, + decodeStoredObject, + disposeBody, + encodeStoredObject, + nextVersion, + readBoundedBody, + requireEtag, +} from "./storage-validation.js"; + +function azureCode(error: unknown): string | undefined { + if (typeof error !== "object" || error === null) return undefined; + if ("details" in error && typeof error.details === "object" && error.details !== null && + "errorCode" in error.details && typeof error.details.errorCode === "string") { + return error.details.errorCode; + } + return "code" in error && typeof error.code === "string" ? error.code : undefined; +} + +export class AzureBlobStore implements BlobStore { + public constructor(private readonly container: ContainerClient) {} + + async #read(key: string): Promise<{ stored: StoredObject; etag: string } | undefined> { + try { + // Body and ETag must come from the same GET, never a separate properties request. + const response = await this.container.getBlockBlobClient(key).download(0, undefined, { + maxRetryRequests: 0, + abortSignal: AbortSignal.timeout(30_000), + }); + let etag: string; + try { + etag = requireEtag(response.etag); + } catch (error) { + disposeBody(response.readableStreamBody); + throw error; + } + const text = await readBoundedBody(response.readableStreamBody, response.contentLength); + return { stored: decodeStoredObject(text), etag }; + } catch (error) { + if (azureCode(error) === "BlobNotFound" && typeof error === "object" && + error !== null && "statusCode" in error && error.statusCode === 404) return undefined; + throw error; + } + } + + public async read(key: string): Promise | undefined> { + assertBlobKey(key); + return (await this.#read(key))?.stored; + } + + public async create(key: string, value: unknown): Promise { + assertBlobKey(key); + const text = encodeStoredObject({ version: 1, value }); + try { + await this.container.getBlockBlobClient(key).upload(text, Buffer.byteLength(text), { + conditions: { ifNoneMatch: "*" }, + blobHTTPHeaders: { blobContentType: "application/json" }, + abortSignal: AbortSignal.timeout(30_000), + }); + } catch (error) { + const code = azureCode(error); + if (code === "ConditionNotMet" || code === "BlobAlreadyExists") { + throw new ObjectAlreadyExistsError("storage object"); + } + throw error; + } + } + + public async compareAndSwap(key: string, expectedVersion: number, value: unknown): Promise { + assertBlobKey(key); + const version = nextVersion(expectedVersion); + const text = encodeStoredObject({ version, value }); + const current = await this.#read(key); + if (!current) throw new ObjectNotFoundError("storage object"); + if (current.stored.version !== expectedVersion) throw new ConcurrentUpdateError("storage object"); + try { + await this.container.getBlockBlobClient(key).upload(text, Buffer.byteLength(text), { + conditions: { ifMatch: current.etag }, + blobHTTPHeaders: { blobContentType: "application/json" }, + abortSignal: AbortSignal.timeout(30_000), + }); + } catch (error) { + const code = azureCode(error); + if (code === "ConditionNotMet" || code === "BlobNotFound") { + throw new ConcurrentUpdateError("storage object"); + } + throw error; + } + return version; + } +} diff --git a/src/storage/configuration.ts b/src/storage/configuration.ts new file mode 100644 index 0000000..065a366 --- /dev/null +++ b/src/storage/configuration.ts @@ -0,0 +1,122 @@ +import { open } from "node:fs/promises"; +import { DefaultAzureCredential } from "@azure/identity"; +import { SecretClient } from "@azure/keyvault-secrets"; + +export type StorageEnvironment = Readonly>; +export type StorageBackend = "memory" | "file" | "azure" | "s3" | "ipfs"; + +export class StorageConfigurationError extends Error { + public constructor(message: string) { + super(message); + this.name = "StorageConfigurationError"; + } +} + +export function required(env: StorageEnvironment, name: string): string { + const value = env[name]?.trim(); + if (!value) { + throw new StorageConfigurationError(`Set ${name} before selecting this backend.`); + } + return value; +} + +export function flag(env: StorageEnvironment, name: string): boolean { + const value = env[name]; + if (value === undefined || value === "false") return false; + if (value === "true") return true; + throw new StorageConfigurationError(`${name} must be true or false.`); +} + +export function positiveInteger( + env: StorageEnvironment, + name: string, + fallback: number, +): number { + if (env[name] === undefined) return fallback; + const value = required(env, name); + if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value))) { + throw new StorageConfigurationError(`${name} must be a positive safe integer.`); + } + return Number(value); +} + +export function storageBackend( + env: StorageEnvironment, + name = "STORAGE_BACKEND", + fallback: StorageBackend = "file", +): StorageBackend { + const value = env[name] ?? fallback; + if (value === "memory" || value === "file" || value === "azure" || + value === "s3" || value === "ipfs") return value; + throw new StorageConfigurationError(`${name} must be memory, file, azure, s3, or ipfs.`); +} + +export function storageEndpoint( + value: string, + name: string, + allowLocalHttp = false, +): URL { + let url: URL; + try { + url = new URL(value); + } catch { + throw new StorageConfigurationError(`${name} must be a valid endpoint URL.`); + } + const local = ["127.0.0.1", "[::1]", "localhost"].includes(url.hostname); + if (url.username || url.password || url.search || url.hash || + (url.protocol !== "https:" && !(allowLocalHttp && local && url.protocol === "http:"))) { + throw new StorageConfigurationError( + `${name} requires HTTPS without embedded credentials, query, or fragment; explicit emulator mode permits loopback HTTP only.`, + ); + } + return url; +} + +// Secrets are retrieved at runtime, never encoded in the configuration file. +export async function loadRuntimeSecret( + env: StorageEnvironment, + setting: string, +): Promise { + const file = env[`${setting}_FILE`]; + const name = env[`${setting}_KEYVAULT_SECRET`]; + if (env[setting] !== undefined) { + throw new StorageConfigurationError( + `Use ${setting}_FILE or ${setting}_KEYVAULT_SECRET, not an inline secret.`, + ); + } + if (file && name) { + throw new StorageConfigurationError(`Choose one runtime secret source for ${setting}.`); + } + if (!file && !name) return undefined; + let secret: string | undefined; + if (file) { + const handle = await open(file, "r"); + try { + const stat = await handle.stat(); + if (!stat.isFile() || stat.size > 16_384) { + throw new StorageConfigurationError(`${setting} must be a small regular secret file.`); + } + secret = (await handle.readFile("utf8")).trim(); + } finally { + await handle.close(); + } + } else { + if (!name || !/^[A-Za-z0-9-]{1,127}$/.test(name)) { + throw new StorageConfigurationError(`${setting}_KEYVAULT_SECRET is invalid.`); + } + const vault = storageEndpoint(required(env, "AZURE_KEY_VAULT_URL"), "AZURE_KEY_VAULT_URL"); + const vaultSuffixes = [".vault.azure.net", ".vault.usgovcloudapi.net", ".vault.azure.cn"]; + if (vault.pathname !== "/" || (vault.port && vault.port !== "443") || + !vaultSuffixes.some((suffix) => vault.hostname.endsWith(suffix))) { + throw new StorageConfigurationError("AZURE_KEY_VAULT_URL must identify an Azure vault root."); + } + const client = new SecretClient(vault.href, new DefaultAzureCredential(), { + retryOptions: { maxRetries: 2 }, + }); + secret = (await client.getSecret(name)).value?.trim(); + } + if (!secret || secret.length > 16_384 || /[\r\n]/.test(secret)) { + throw new StorageConfigurationError(`${setting} must contain a nonempty single-line secret.`); + } + return secret; +} diff --git a/src/storage/configured-blob-store.ts b/src/storage/configured-blob-store.ts new file mode 100644 index 0000000..c50d9b4 --- /dev/null +++ b/src/storage/configured-blob-store.ts @@ -0,0 +1,129 @@ +import { resolve } from "node:path"; +import { S3Client } from "@aws-sdk/client-s3"; +import { DefaultAzureCredential } from "@azure/identity"; +import { BlobServiceClient, StorageSharedKeyCredential } from "@azure/storage-blob"; +import type { BlobStore } from "./blob-store.js"; +import { FileBlobStore } from "./file-blob-store.js"; +import { InMemoryBlobStore } from "./in-memory-blob-store.js"; +import { AzureBlobStore } from "./azure-blob-store.js"; +import { S3BlobStore } from "./s3-blob-store.js"; +import { DistributedBlobStore } from "./distributed-blob-store.js"; +import { IpfsClusterClient } from "./ipfs-cluster-client.js"; +import { + StorageConfigurationError, + flag, + loadRuntimeSecret, + positiveInteger, + required, + storageBackend, + storageEndpoint, + type StorageBackend, + type StorageEnvironment, +} from "./configuration.js"; + +export async function createConfiguredBlobStore( + env: StorageEnvironment = process.env, +): Promise { + if (env.NODE_TLS_REJECT_UNAUTHORIZED === "0" || + process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0") { + throw new StorageConfigurationError("TLS certificate validation must remain enabled."); + } + const emulator = flag(env, "STORAGE_EMULATOR"); + const backend = storageBackend(env); + + const create = (selected: Exclude): BlobStore => { + switch (selected) { + case "memory": return new InMemoryBlobStore(); + case "file": return new FileBlobStore(resolve(env.STORAGE_FILE_DIRECTORY ?? ".data/web")); + case "azure": { + if (env.AZURE_STORAGE_CONNECTION_STRING || env.AZURE_STORAGE_SAS_TOKEN) { + throw new StorageConfigurationError("Use Azure workload identity rather than connection strings or SAS tokens."); + } + const endpoint = storageEndpoint( + required(env, "AZURE_STORAGE_ACCOUNT_URL"), "AZURE_STORAGE_ACCOUNT_URL", emulator, + ); + const container = required(env, "AZURE_STORAGE_CONTAINER"); + if (!/^[a-z0-9](?:[a-z0-9]|-(?!-)){1,61}[a-z0-9]$/.test(container)) { + throw new StorageConfigurationError("AZURE_STORAGE_CONTAINER must be a valid private container name."); + } + if (emulator && !["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname)) { + throw new StorageConfigurationError("Azure emulator credentials are permitted only on loopback."); + } + const credential = emulator + ? new StorageSharedKeyCredential( + required(env, "AZURE_STORAGE_EMULATOR_ACCOUNT"), + required(env, "AZURE_STORAGE_EMULATOR_KEY"), + ) + : new DefaultAzureCredential(); + const client = new BlobServiceClient(endpoint.href, credential, { + retryOptions: { maxTries: 1, tryTimeoutInMs: 30_000 }, + }); + return new AzureBlobStore(client.getContainerClient(container)); + } + case "s3": { + const bucket = required(env, "S3_BUCKET"); + if (!/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(bucket) || + bucket.includes("..") || /^\d+\.\d+\.\d+\.\d+$/.test(bucket)) { + throw new StorageConfigurationError("S3_BUCKET must be a valid general-purpose bucket name."); + } + const endpointValue = env.S3_ENDPOINT_URL; + if (emulator && !endpointValue) { + throw new StorageConfigurationError("S3 emulator mode requires a loopback S3_ENDPOINT_URL."); + } + const endpoint = endpointValue + ? storageEndpoint(endpointValue, "S3_ENDPOINT_URL", emulator) : undefined; + if (endpoint && (endpoint.pathname !== "/" || + (emulator && !["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname)))) { + throw new StorageConfigurationError("S3_ENDPOINT_URL must be an endpoint root; emulator mode requires loopback."); + } + const client = new S3Client({ + region: required(env, "AWS_REGION"), + maxAttempts: 1, + ...(endpoint ? { endpoint: endpoint.href, forcePathStyle: true } : {}), + requestHandler: { connectionTimeout: 10_000, requestTimeout: 30_000 }, + }); + return new S3BlobStore(client, bucket, { + ...(env.STORAGE_PREFIX ? { prefix: env.STORAGE_PREFIX } : {}), + ...(env.S3_KMS_KEY_ID ? { kmsKeyId: env.S3_KMS_KEY_ID } : {}), + }); + } + } + }; + + if (backend !== "ipfs") return create(backend); + if (!flag(env, "IPFS_PRIVATE_NETWORK")) { + throw new StorageConfigurationError("IPFS_PRIVATE_NETWORK=true must acknowledge a separately provisioned private swarm."); + } + const metadataBackend = storageBackend(env, "IPFS_METADATA_BACKEND", "azure"); + if (metadataBackend === "ipfs" || metadataBackend === "memory" || + (metadataBackend === "file" && !emulator)) { + throw new StorageConfigurationError("IPFS metadata requires azure or s3; file is allowed only in local emulator mode."); + } + const kubo = storageEndpoint(required(env, "IPFS_API_URL"), "IPFS_API_URL", emulator); + const cluster = storageEndpoint(required(env, "IPFS_CLUSTER_API_URL"), "IPFS_CLUSTER_API_URL", emulator); + if (emulator && [kubo, cluster].some((url) => + !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) { + throw new StorageConfigurationError("IPFS emulator mode requires loopback endpoints."); + } + const min = positiveInteger(env, "IPFS_REPLICATION_MIN", 2); + const max = positiveInteger(env, "IPFS_REPLICATION_MAX", 3); + if (max < min || (!emulator && min < 2)) { + throw new StorageConfigurationError("IPFS replication requires max >= min and at least two copies outside emulator mode."); + } + const apiAuth = await loadRuntimeSecret(env, "IPFS_API_AUTH"); + const clusterAuth = await loadRuntimeSecret(env, "IPFS_CLUSTER_AUTH"); + if (!emulator && (!apiAuth || !clusterAuth)) { + throw new StorageConfigurationError("Remote IPFS APIs require runtime authorization from secret files or Key Vault."); + } + const content = new IpfsClusterClient({ + kuboUrl: kubo.href, + clusterUrl: cluster.href, + minReplicas: min, + maxReplicas: max, + timeoutMs: positiveInteger(env, "IPFS_TIMEOUT_MS", 30_000), + allowInsecureLocal: emulator, + ...(apiAuth ? { kuboAuthorization: apiAuth } : {}), + ...(clusterAuth ? { clusterAuthorization: clusterAuth } : {}), + }); + return new DistributedBlobStore(create(metadataBackend), content); +} diff --git a/src/storage/distributed-blob-store.ts b/src/storage/distributed-blob-store.ts new file mode 100644 index 0000000..a37b2fd --- /dev/null +++ b/src/storage/distributed-blob-store.ts @@ -0,0 +1,113 @@ +import { CID } from "multiformats/cid"; +import * as raw from "multiformats/codecs/raw"; +import { sha256 } from "multiformats/hashes/sha2"; +import { + ConcurrentUpdateError, + ObjectNotFoundError, + type BlobStore, + type StoredObject, +} from "./blob-store.js"; +import { + assertBlobKey, + decodeStoredObject, + encodeStoredObject, + nextVersion, +} from "./storage-validation.js"; + +export const MAX_DISTRIBUTED_BLOB_BYTES = 1024 * 1024 - 1; + +/** put resolves only after the configured replication threshold is observed. */ +export interface DistributedContentStore { + put(bytes: Uint8Array): Promise; + get(cid: string): Promise; +} + +export function validateRawCid(value: unknown): string { + try { + if (typeof value !== "string" || value.length > 128) throw new Error(); + const cid = CID.parse(value); + if (cid.version !== 1 || cid.code !== raw.code || + cid.multihash.code !== sha256.code || cid.multihash.size !== 32 || + cid.toString() !== value) throw new Error(); + return value; + } catch { + throw new Error("Invalid distributed content identifier."); + } +} + +export async function rawContentCid(bytes: Uint8Array): Promise { + if (!(bytes instanceof Uint8Array) || bytes.byteLength === 0 || + bytes.byteLength > MAX_DISTRIBUTED_BLOB_BYTES) { + throw new Error("Invalid distributed content size."); + } + return CID.createV1(raw.code, await sha256.digest(bytes)).toString(); +} + +function pointer(value: unknown): string { + if (typeof value !== "object" || value === null || Array.isArray(value) || + Object.keys(value).length !== 2 || !("format" in value) || + value.format !== "ipfs-raw-v1" || !("cid" in value)) { + throw new Error("Invalid distributed storage pointer."); + } + return validateRawCid(value.cid); +} + +/** Atomicity comes exclusively from the injected metadata BlobStore. */ +export class DistributedBlobStore implements BlobStore { + public constructor( + private readonly metadata: BlobStore, + private readonly content: DistributedContentStore, + ) {} + + public async read(key: string): Promise | undefined> { + assertBlobKey(key); + const stored = await this.metadata.read(key); + if (stored === undefined) return undefined; + if (!Number.isSafeInteger(stored.version) || stored.version < 1) { + throw new Error("Invalid storage object version."); + } + const cid = pointer(stored.value); + const bytes = await this.content.get(cid); + if (await rawContentCid(bytes) !== cid) { + throw new Error("Distributed content integrity check failed."); + } + let text: string; + try { + text = new TextDecoder("utf-8", { fatal: true }).decode(bytes); + } catch { + throw new Error("Invalid distributed content encoding."); + } + const decoded = decodeStoredObject(text); + if (decoded.version !== 1) throw new Error("Invalid distributed content format."); + return { version: stored.version, value: decoded.value }; + } + + public async create(key: string, value: unknown): Promise { + assertBlobKey(key); + const cid = await this.upload(value); + await this.metadata.create(key, { format: "ipfs-raw-v1", cid }); + } + + public async compareAndSwap( + key: string, + expectedVersion: number, + value: unknown, + ): Promise { + assertBlobKey(key); + nextVersion(expectedVersion); + const stored = await this.metadata.read(key); + if (!stored) throw new ObjectNotFoundError(key); + if (stored.version !== expectedVersion) throw new ConcurrentUpdateError(key); + pointer(stored.value); + const cid = await this.upload(value); + return this.metadata.compareAndSwap(key, expectedVersion, { format: "ipfs-raw-v1", cid }); + } + + private async upload(value: unknown): Promise { + const bytes = new TextEncoder().encode(encodeStoredObject({ version: 1, value })); + const expected = await rawContentCid(bytes); + const actual = validateRawCid(await this.content.put(bytes)); + if (actual !== expected) throw new Error("Distributed content integrity check failed."); + return actual; + } +} diff --git a/src/storage/ipfs-cluster-client.ts b/src/storage/ipfs-cluster-client.ts new file mode 100644 index 0000000..0f4759d --- /dev/null +++ b/src/storage/ipfs-cluster-client.ts @@ -0,0 +1,240 @@ +import { setTimeout as delay } from "node:timers/promises"; +import { + MAX_DISTRIBUTED_BLOB_BYTES, + rawContentCid, + validateRawCid, + type DistributedContentStore, +} from "./distributed-blob-store.js"; + +export interface IpfsClusterClientOptions { + kuboUrl: string; + clusterUrl: string; + kuboAuthorization?: string; + clusterAuthorization?: string; + allowInsecureLocal?: boolean; + minReplicas?: number; + maxReplicas?: number; + timeoutMs?: number; + pollIntervalMs?: number; + maxBlobBytes?: number; + fetch?: typeof globalThis.fetch; +} + +function endpoint(value: string, allowInsecureLocal: boolean): URL { + try { + const url = new URL(value); + const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname); + if (url.username || url.password || url.search || url.hash || + /[?#]/.test(value) || url.pathname !== "/" || + (url.protocol !== "https:" && + !(allowInsecureLocal && local && url.protocol === "http:"))) throw new Error(); + return url; + } catch { + throw new Error("Invalid private storage endpoint."); + } +} + +function integer(value: number, min: number, max: number): number { + if (!Number.isSafeInteger(value) || value < min || value > max) { + throw new Error("Invalid private storage configuration."); + } + return value; +} + +function authorization(value: string | undefined): string | undefined { + if (value !== undefined && + (typeof value !== "string" || value.length === 0 || + value.length > 8192 || /[^\x20-\x7e]/.test(value))) { + throw new Error("Invalid private storage authorization."); + } + return value; +} + +function record(value: unknown): Record { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + throw new Error("Invalid private storage response."); + } + return value as Record; +} + +function json(bytes: Uint8Array): unknown { + try { + return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) as unknown; + } catch { + throw new Error("Invalid private storage response."); + } +} + +function peerList(bytes: Uint8Array): unknown[] { + const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes).trim(); + try { + if (text.startsWith("[")) return JSON.parse(text) as unknown[]; + return text.split("\n").filter(Boolean).map((line) => JSON.parse(line) as unknown); + } catch { + throw new Error("Invalid private storage response."); + } +} + +/** Private Kubo RPC + Cluster REST only; never uses a gateway or fallback. */ +export class IpfsClusterClient implements DistributedContentStore { + readonly #kubo: URL; + readonly #cluster: URL; + readonly #kuboAuth: string | undefined; + readonly #clusterAuth: string | undefined; + readonly #fetch: typeof globalThis.fetch; + readonly #min: number; + readonly #max: number; + readonly #timeout: number; + readonly #poll: number; + readonly #maxBytes: number; + + public constructor(options: IpfsClusterClientOptions) { + this.#kubo = endpoint(options.kuboUrl, options.allowInsecureLocal === true); + this.#cluster = endpoint(options.clusterUrl, options.allowInsecureLocal === true); + this.#kuboAuth = authorization(options.kuboAuthorization); + this.#clusterAuth = authorization(options.clusterAuthorization); + this.#fetch = options.fetch ?? globalThis.fetch; + this.#min = integer(options.minReplicas ?? 2, 1, 1000); + this.#max = integer(options.maxReplicas ?? 2, this.#min, 1000); + this.#timeout = integer(options.timeoutMs ?? 30_000, 1, 600_000); + this.#poll = integer(options.pollIntervalMs ?? 250, 1, 60_000); + this.#maxBytes = integer(options.maxBlobBytes ?? MAX_DISTRIBUTED_BLOB_BYTES, + 1, MAX_DISTRIBUTED_BLOB_BYTES); + } + + public async put(input: Uint8Array): Promise { + if (!(input instanceof Uint8Array) || input.byteLength > this.#maxBytes) { + throw new Error("Invalid distributed content size."); + } + const bytes = Uint8Array.from(input); + const cid = await rawContentCid(bytes); + return this.bounded(async (signal) => { + const form = new FormData(); + form.append("file", new Blob([bytes]), "block"); + const result = record(json(await this.request(false, + "/api/v0/block/put?cid-codec=raw&mhtype=sha2-256&mhlen=32&pin=true&allow-big-block=false", + "POST", signal, form))); + if (validateRawCid(result.Key) !== cid || result.Size !== bytes.byteLength) { + throw new Error("Distributed content integrity check failed."); + } + await this.request(true, + `/pins/${cid}?replication-min=${this.#min}&replication-max=${this.#max}`, + "POST", signal); + while (true) { + const status = record(json(await this.request(true, `/pins/${cid}?local=false`, + "GET", signal))); + if (status.cid !== cid) throw new Error("Invalid private storage response."); + const peers = peerList(await this.request(true, "/peers", "GET", signal)); + if (this.replicaCount(status, peers) >= this.#min) return cid; + await delay(this.#poll, undefined, { signal }); + } + }); + } + + public async get(value: string): Promise { + const cid = validateRawCid(value); + return this.bounded(async (signal) => { + const bytes = await this.request(false, `/api/v0/block/get?arg=${cid}`, + "POST", signal, undefined, this.#maxBytes); + if (await rawContentCid(bytes) !== cid) { + throw new Error("Distributed content integrity check failed."); + } + return bytes; + }); + } + + private replicaCount(status: Record, peers: unknown[]): number { + const live = new Map(); + const duplicates = new Set(); + const seen = new Set(); + for (const value of peers) { + const peer = record(value); + if (typeof peer.id !== "string" || !peer.id) continue; + if (seen.has(peer.id)) duplicates.add(peer.id); + seen.add(peer.id); + if (peer.error !== "" && peer.error !== undefined) continue; + const ipfs = record(peer.ipfs); + if ((ipfs.error === "" || ipfs.error === undefined) && + typeof ipfs.id === "string" && ipfs.id) live.set(peer.id, ipfs.id); + } + const distinctIpfs = new Set(); + for (const [id, value] of Object.entries(record(status.peer_map))) { + const pin = record(value); + const ipfsId = live.get(id); + if (!duplicates.has(id) && ipfsId && pin.status === "pinned" && + (pin.error === "" || pin.error === undefined) && + pin.ipfs_peer_id === ipfsId) distinctIpfs.add(ipfsId); + } + return distinctIpfs.size; + } + + private async bounded(operation: (signal: AbortSignal) => Promise): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), this.#timeout); + let aborted: (() => void) | undefined; + try { + const deadline = new Promise((_resolve, reject) => { + aborted = () => reject(new Error("Deadline exceeded.")); + controller.signal.addEventListener("abort", aborted, { once: true }); + }); + return await Promise.race([operation(controller.signal), deadline]); + } catch { + // Never include upstream bodies, URLs, nested causes, or authentication. + throw new Error(controller.signal.aborted + ? "Private distributed storage operation timed out." + : "Private distributed storage operation failed."); + } finally { + clearTimeout(timer); + if (aborted) controller.signal.removeEventListener("abort", aborted); + } + } + + private async request( + cluster: boolean, + path: string, + method: string, + signal: AbortSignal, + body?: FormData, + maximum = 1024 * 1024, + ): Promise { + signal.throwIfAborted(); + const auth = cluster ? this.#clusterAuth : this.#kuboAuth; + const response = await this.#fetch(new URL(path, cluster ? this.#cluster : this.#kubo), { + method, signal, redirect: "error", cache: "no-store", + headers: auth === undefined ? {} : { Authorization: auth }, + ...(body === undefined ? {} : { body }), + }); + const reader = response.body?.getReader(); + const cancel = (): void => { void reader?.cancel().catch(() => undefined); }; + signal.addEventListener("abort", cancel, { once: true }); + try { + signal.throwIfAborted(); + if (!response.ok || response.redirected || !reader) throw new Error(); + const length = response.headers.get("content-length"); + const expected = length === null ? undefined : Number(length); + if (expected !== undefined && + (!/^\d+$/.test(length!) || !Number.isSafeInteger(expected) || expected > maximum)) { + throw new Error(); + } + const chunks: Uint8Array[] = []; + let size = 0; + while (true) { + signal.throwIfAborted(); + const { done, value } = await reader.read(); + if (done) { + signal.throwIfAborted(); + break; + } + size += value.byteLength; + if (size > maximum) throw new Error(); + chunks.push(value); + } + if (expected !== undefined && size !== expected) throw new Error(); + return Buffer.concat(chunks, size); + } finally { + signal.removeEventListener("abort", cancel); + void reader?.cancel().catch(() => undefined); + reader?.releaseLock(); + } + } +} diff --git a/src/storage/s3-blob-store.ts b/src/storage/s3-blob-store.ts new file mode 100644 index 0000000..2269782 --- /dev/null +++ b/src/storage/s3-blob-store.ts @@ -0,0 +1,140 @@ +import { GetObjectCommand, PutObjectCommand, type S3Client } from "@aws-sdk/client-s3"; +import { + ConcurrentUpdateError, + ObjectAlreadyExistsError, + ObjectNotFoundError, + type BlobStore, + type StoredObject, +} from "./blob-store.js"; +import { + assertBlobKey, + decodeStoredObject, + disposeBody, + encodeStoredObject, + nextVersion, + readBoundedBody, + requireEtag, +} from "./storage-validation.js"; + +export interface S3BlobStoreOptions { + prefix?: string; + kmsKeyId?: string; +} + +function errorName(error: unknown): string | undefined { + return typeof error === "object" && error !== null && "name" in error && + typeof error.name === "string" ? error.name : undefined; +} + +function isPreconditionFailure(error: unknown): boolean { + return errorStatus(error) === 412; +} + +function errorStatus(error: unknown): number | undefined { + if (typeof error === "object" && error !== null && "$metadata" in error && + typeof error.$metadata === "object" && error.$metadata !== null && + "httpStatusCode" in error.$metadata && typeof error.$metadata.httpStatusCode === "number") { + return error.$metadata.httpStatusCode; + } + return undefined; +} + +export class S3BlobStore implements BlobStore { + readonly #prefix: string; + readonly #kmsKeyId: string | undefined; + + public constructor( + private readonly client: S3Client, + private readonly bucket: string, + options: S3BlobStoreOptions = {}, + ) { + const prefix = options.prefix ?? ""; + if (prefix.length > 512 || (prefix !== "" && + !/^[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*\/?$/.test(prefix))) { + throw new Error("Invalid storage prefix."); + } + if (!bucket || bucket.length > 255 || /[\s/\\]/.test(bucket)) { + throw new Error("Invalid storage bucket."); + } + if (options.kmsKeyId !== undefined && (!options.kmsKeyId || + options.kmsKeyId.length > 2048 || /[\s\x00-\x1f\x7f]/.test(options.kmsKeyId))) { + throw new Error("Invalid storage encryption configuration."); + } + this.#prefix = prefix === "" || prefix.endsWith("/") ? prefix : `${prefix}/`; + this.#kmsKeyId = options.kmsKeyId; + } + + async #read(key: string): Promise<{ stored: StoredObject; etag: string } | undefined> { + try { + const response = await this.client.send(new GetObjectCommand({ + Bucket: this.bucket, + Key: `${this.#prefix}${key}`, + }), { abortSignal: AbortSignal.timeout(30_000) }); + let etag: string; + try { + etag = requireEtag(response.ETag); + } catch (error) { + disposeBody(response.Body); + throw error; + } + const text = await readBoundedBody(response.Body, response.ContentLength); + return { stored: decodeStoredObject(text), etag }; + } catch (error) { + if (errorName(error) === "NoSuchKey" && errorStatus(error) === 404) return undefined; + throw error; + } + } + + public async read(key: string): Promise | undefined> { + assertBlobKey(key); + return (await this.#read(key))?.stored; + } + + #write(key: string, text: string, condition: { IfNoneMatch: "*" } | { IfMatch: string }) { + return this.client.send(new PutObjectCommand({ + Bucket: this.bucket, + Key: `${this.#prefix}${key}`, + Body: text, + ContentLength: Buffer.byteLength(text), + ContentType: "application/json", + ...condition, + ...(this.#kmsKeyId ? { + ServerSideEncryption: "aws:kms" as const, + SSEKMSKeyId: this.#kmsKeyId, + } : {}), + }), { abortSignal: AbortSignal.timeout(30_000) }); + } + + public async create(key: string, value: unknown): Promise { + assertBlobKey(key); + const text = encodeStoredObject({ version: 1, value }); + try { + await this.#write(key, text, { IfNoneMatch: "*" }); + } catch (error) { + if (errorName(error) === "ConditionalRequestConflict") { + throw new ConcurrentUpdateError("storage object"); + } + if (isPreconditionFailure(error)) throw new ObjectAlreadyExistsError("storage object"); + throw error; + } + } + + public async compareAndSwap(key: string, expectedVersion: number, value: unknown): Promise { + assertBlobKey(key); + const version = nextVersion(expectedVersion); + const text = encodeStoredObject({ version, value }); + const current = await this.#read(key); + if (!current) throw new ObjectNotFoundError("storage object"); + if (current.stored.version !== expectedVersion) throw new ConcurrentUpdateError("storage object"); + try { + await this.#write(key, text, { IfMatch: current.etag }); + } catch (error) { + if (isPreconditionFailure(error) || errorName(error) === "ConditionalRequestConflict" || + errorName(error) === "NoSuchKey") { + throw new ConcurrentUpdateError("storage object"); + } + throw error; + } + return version; + } +} diff --git a/src/storage/storage-validation.ts b/src/storage/storage-validation.ts new file mode 100644 index 0000000..66e5165 --- /dev/null +++ b/src/storage/storage-validation.ts @@ -0,0 +1,164 @@ +import { Readable } from "node:stream"; +import { types } from "node:util"; +import type { StoredObject } from "./blob-store.js"; + +export const MAX_STORED_OBJECT_BYTES = 4 * 1024 * 1024; +const MAX_DEPTH = 64; +const MAX_NODES = 100_000; + +export function assertBlobKey(key: string): void { + if (typeof key !== "string" || !/^[A-Za-z0-9_-]{32,128}$/.test(key)) { + throw new Error("Invalid storage object key."); + } +} + +export function assertExpectedVersion(version: number): void { + if (!Number.isSafeInteger(version) || version < 1) { + throw new Error("Invalid storage object version."); + } +} + +export function nextVersion(version: number): number { + assertExpectedVersion(version); + if (version === Number.MAX_SAFE_INTEGER) { + throw new Error("Storage object version limit reached."); + } + return version + 1; +} + +function assertJsonValue(value: unknown): void { + const ancestors = new Set(); + let nodes = 0; + let stringBytes = 0; + const visit = (item: unknown, depth: number): void => { + if (++nodes > MAX_NODES || depth > MAX_DEPTH) { + throw new Error("Storage object exceeds limits."); + } + if (item === null || typeof item === "boolean") return; + if (typeof item === "string") { + stringBytes += Buffer.byteLength(item); + if (stringBytes > MAX_STORED_OBJECT_BYTES) { + throw new Error("Storage object exceeds limits."); + } + return; + } + if (typeof item === "number" && Number.isFinite(item) && !Object.is(item, -0)) return; + if (typeof item !== "object" || item === null || types.isProxy(item) || ancestors.has(item)) { + throw new Error("Unsupported storage object value."); + } + const array = Array.isArray(item); + if (array && Object.getPrototypeOf(item) !== Array.prototype) { + throw new Error("Unsupported storage object value."); + } + if (!array && Object.getPrototypeOf(item) !== Object.prototype && + Object.getPrototypeOf(item) !== null) { + throw new Error("Unsupported storage object value."); + } + ancestors.add(item); + const keys = Reflect.ownKeys(item); + if (keys.length > MAX_NODES) throw new Error("Storage object exceeds limits."); + if (array && (item.length > MAX_NODES || keys.length !== item.length + 1)) { + throw new Error("Unsupported storage object value."); + } + for (const key of keys) { + if (array && key === "length") continue; + if (typeof key !== "string" || + (array && !/^(0|[1-9]\d*)$/.test(key))) { + throw new Error("Unsupported storage object value."); + } + stringBytes += Buffer.byteLength(key); + if (stringBytes > MAX_STORED_OBJECT_BYTES) { + throw new Error("Storage object exceeds limits."); + } + const descriptor = Object.getOwnPropertyDescriptor(item, key); + if (!descriptor || !descriptor.enumerable || !Object.hasOwn(descriptor, "value")) { + throw new Error("Unsupported storage object value."); + } + visit(descriptor.value, depth + 1); + } + ancestors.delete(item); + }; + visit(value, 0); +} + +function assertWrapper(value: unknown): asserts value is StoredObject { + if (typeof value !== "object" || value === null || Array.isArray(value) || + !Object.hasOwn(value, "version") || !Object.hasOwn(value, "value") || + !("version" in value) || typeof value.version !== "number") { + throw new Error("Invalid stored object."); + } + assertExpectedVersion(value.version); +} + +export function encodeStoredObject(stored: StoredObject): string { + try { + assertJsonValue(stored); + assertWrapper(stored); + const text = JSON.stringify(stored); + if (Buffer.byteLength(text) > MAX_STORED_OBJECT_BYTES) { + throw new Error("Storage object exceeds limits."); + } + return text; + } catch { + // JSON exceptions can contain caller-controlled data. + throw new Error("Invalid or oversized storage object."); + } +} + +export function decodeStoredObject(text: string): StoredObject { + if (Buffer.byteLength(text) > MAX_STORED_OBJECT_BYTES) { + throw new Error("Storage object exceeds limits."); + } + try { + const value: unknown = JSON.parse(text); + assertWrapper(value); + assertJsonValue(value); + return value as StoredObject; + } catch { + throw new Error("Invalid stored object."); + } +} + +export function requireEtag(etag: string | undefined): string { + if (!etag || etag.trim() === "" || etag.trim() === "*" || etag.length > 1024 || + /[\r\n]/.test(etag)) { + throw new Error("Invalid storage object ETag."); + } + return etag; +} + +export function disposeBody(body: unknown): void { + if (body instanceof Readable) body.destroy(); +} + +export async function readBoundedBody(body: unknown, length?: number): Promise { + if (!(body instanceof Readable)) throw new Error("Invalid storage response body."); + if (length !== undefined && (!Number.isSafeInteger(length) || length < 0 || + length > MAX_STORED_OBJECT_BYTES)) { + body.destroy(); + throw new Error("Invalid storage response size."); + } + const timer = setTimeout(() => body.destroy(new Error("Storage response timed out.")), 30_000); + timer.unref(); + const chunks: Buffer[] = []; + let size = 0; + try { + for await (const chunk of body) { + if (!(chunk instanceof Uint8Array) && typeof chunk !== "string") { + throw new Error("Invalid storage response body."); + } + size += typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.byteLength; + if (size > MAX_STORED_OBJECT_BYTES || chunks.length >= MAX_NODES) { + throw new Error("Storage object exceeds limits."); + } + chunks.push(Buffer.from(chunk)); + } + if (length !== undefined && size !== length) { + throw new Error("Incomplete storage response."); + } + return new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks, size)); + } finally { + clearTimeout(timer); + body.destroy(); + } +} diff --git a/src/web/server.ts b/src/web/server.ts index 180e841..ad71676 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -6,13 +6,16 @@ import { fileURLToPath } from "node:url"; import { AccessDeniedError, + ConcurrentUpdateError, FileBlobStore, IntegrityError, InvalidFhirResourceError, PatientRecordService, RecordAlreadyExistsError, RecordNotFoundError, + StorageConfigurationError, createClinicianCredential, + createConfiguredBlobStore, type BlobStore, type ClinicianCredential, type FhirResource, @@ -245,8 +248,11 @@ function sendError(response: ServerResponse, error: unknown): void { sendJson(response, 409, { error: error.message }); return; } - - console.error("Local POC request failed:", error); + if (error instanceof ConcurrentUpdateError) { + sendJson(response, 409, { error: "Record changed concurrently. Unlock and try again." }); + return; + } + console.error("Local POC request failed; diagnostic details withheld."); sendJson(response, 500, { error: "Local POC request failed." }); } @@ -263,9 +269,17 @@ class RequestError extends Error { if (process.argv[1] === fileURLToPath(import.meta.url)) { const port = Number.parseInt(process.env.PORT ?? `${DEFAULT_PORT}`, 10); const host = process.env.HOST ?? DEFAULT_HOST; - const server = createHealthRecordServer(); - server.listen(port, host, () => { - console.log(`Global Private Health Records POC: http://${host}:${port}`); - console.log("Synthetic data only. Press Ctrl+C to stop."); - }); + try { + const store = await createConfiguredBlobStore(); + const server = createHealthRecordServer({ store }); + server.listen(port, host, () => { + console.log(`Global Patient Record Project POC: http://${host}:${port}`); + console.log("Synthetic data only. Press Ctrl+C to stop."); + }); + } catch (error) { + console.error(error instanceof StorageConfigurationError + ? error.message + : "Storage startup failed. Check backend configuration, runtime identity, and connectivity."); + process.exitCode = 1; + } } diff --git a/test/azure-blob-store.spec.ts b/test/azure-blob-store.spec.ts new file mode 100644 index 0000000..3674cae --- /dev/null +++ b/test/azure-blob-store.spec.ts @@ -0,0 +1,212 @@ +import { Readable } from "node:stream"; +import { + ContainerClient, + type HttpOperationResponse, + type IHttpClient, + type WebResource, +} from "@azure/storage-blob"; +import { describe, expect, it } from "vitest"; +import { + AzureBlobStore, + ConcurrentUpdateError, + ObjectAlreadyExistsError, + ObjectNotFoundError, +} from "../src/index.js"; + +const key = "a".repeat(43); +const ciphertext = { algorithm: "AES-256-GCM", ciphertext: "synthetic-ciphertext", nonce: "opaque" }; + +class AzureTransport implements IHttpClient { + readonly requests: WebResource[] = []; + text: string | undefined; + etag = '"revision-1"'; + omitEtag = false; + omitLength = false; + error: { status: number; code: string } | undefined; + networkError: Error | undefined; + writeError: { status: number; code: string } | undefined; + loseWriteResponse = false; + #revision = 1; + + async sendRequest(request: WebResource): Promise { + this.requests.push(request); + if (this.networkError) throw this.networkError; + const headers = request.headers.clone(); + headers.set("x-ms-request-id", "synthetic-request"); + const failure = (status: number, code: string): HttpOperationResponse => { + headers.set("content-type", "application/xml"); + headers.set("x-ms-error-code", code); + const body = `${code}Storage request failed.`; + return { request, status, headers, bodyAsText: body, readableStreamBody: Readable.from([body]) }; + }; + if (this.error) return failure(this.error.status, this.error.code); + if (request.method === "GET") { + if (this.text === undefined) return failure(404, "BlobNotFound"); + headers.set("content-type", "application/json"); + if (!this.omitEtag) headers.set("etag", this.etag); + if (!this.omitLength) headers.set("content-length", `${Buffer.byteLength(this.text)}`); + return { request, status: 200, headers, readableStreamBody: Readable.from([Buffer.from(this.text)]) }; + } + if (request.method !== "PUT") throw new Error("Unexpected storage operation."); + if (this.writeError) return failure(this.writeError.status, this.writeError.code); + if (request.headers.get("if-none-match") === "*" && this.text !== undefined) { + return failure(412, "ConditionNotMet"); + } + const ifMatch = request.headers.get("if-match"); + if (ifMatch && (ifMatch !== this.etag || this.text === undefined)) { + return failure(412, "ConditionNotMet"); + } + if (typeof request.body !== "string") throw new Error("Unexpected request body."); + this.text = request.body; + this.etag = `"revision-${++this.#revision}"`; + if (this.loseWriteResponse) throw new Error("Connection reset."); + headers.set("etag", this.etag); + headers.set("content-length", "0"); + return { request, status: 201, headers }; + } +} + +function fixture(transport = new AzureTransport()) { + const container = new ContainerClient("https://synthetic.blob.core.windows.net/objects", undefined, { + httpClient: transport, + retryOptions: { maxTries: 1 }, + }); + return { transport, store: new AzureBlobStore(container) }; +} + +describe("AzureBlobStore", () => { + it("creates immutable ciphertext wrappers and performs conditional versioned updates", async () => { + const { transport, store } = fixture(); + await store.create(key, ciphertext); + expect(JSON.parse(transport.text ?? "")).toEqual({ version: 1, value: ciphertext }); + expect(transport.requests[0]?.headers.get("if-none-match")).toBe("*"); + await expect(store.create(key, { ciphertext: "replacement" })).rejects.toBeInstanceOf(ObjectAlreadyExistsError); + expect(await store.read(key)).toEqual({ version: 1, value: ciphertext }); + const oldEtag = transport.etag; + await expect(store.compareAndSwap(key, 1, { ciphertext: "next" })).resolves.toBe(2); + expect(transport.requests.at(-1)?.headers.get("if-match")).toBe(oldEtag); + expect(await store.read(key)).toEqual({ version: 2, value: { ciphertext: "next" } }); + const puts = transport.requests.filter((request) => request.method === "PUT").length; + await expect(store.compareAndSwap(key, 1, null)).rejects.toBeInstanceOf(ConcurrentUpdateError); + expect(transport.requests.filter((request) => request.method === "PUT")).toHaveLength(puts); + }); + + it("allows only one winner across independent clients racing on one ETag", async () => { + const { transport, store } = fixture(); + const second = fixture(transport).store; + await store.create(key, ciphertext); + const results = await Promise.allSettled([ + store.compareAndSwap(key, 1, { ciphertext: "first" }), + second.compareAndSwap(key, 1, { ciphertext: "second" }), + ]); + expect(results.filter((result) => result.status === "fulfilled")).toHaveLength(1); + expect(results.find((result) => result.status === "rejected")).toMatchObject({ + reason: expect.any(ConcurrentUpdateError), + }); + expect(transport.requests.filter((request) => request.headers.get("if-match"))).toHaveLength(2); + expect((await store.read(key))?.version).toBe(2); + }); + + it("recognizes only a missing blob, not missing containers or permission failures", async () => { + const { transport, store } = fixture(); + await expect(store.read(key)).resolves.toBeUndefined(); + await expect(store.compareAndSwap(key, 1, null)).rejects.toBeInstanceOf(ObjectNotFoundError); + for (const code of ["ContainerNotFound", "AuthorizationFailure", "UnknownFailure"]) { + transport.error = { status: code === "AuthorizationFailure" ? 403 : 404, code }; + await expect(store.read(key)).rejects.toMatchObject({ details: { errorCode: code } }); + await expect(store.create(key, null)).rejects.toMatchObject({ details: { errorCode: code } }); + } + transport.error = undefined; + transport.networkError = new Error("Synthetic network failure."); + await expect(store.read(key)).rejects.toBe(transport.networkError); + }); + + it("does not hide an ambiguous committed write behind a retry or read-back", async () => { + const { transport, store } = fixture(); + transport.loseWriteResponse = true; + await expect(store.create(key, ciphertext)).rejects.toThrow("Connection reset."); + expect(transport.requests).toHaveLength(1); + expect(transport.text).toBeDefined(); + }); + + it("does not hide a committed CAS with a lost response", async () => { + const { transport, store } = fixture(); + await store.create(key, ciphertext); + transport.loseWriteResponse = true; + await expect(store.compareAndSwap(key, 1, { ciphertext: "next" })).rejects.toThrow("Connection reset."); + expect(transport.requests).toHaveLength(3); + expect(JSON.parse(transport.text ?? "").version).toBe(2); + }); + + it("maps a failed CAS condition but propagates unrelated write failures", async () => { + const { transport, store } = fixture(); + await store.create(key, ciphertext); + transport.writeError = { status: 412, code: "ConditionNotMet" }; + await expect(store.compareAndSwap(key, 1, null)).rejects.toBeInstanceOf(ConcurrentUpdateError); + transport.writeError = { status: 403, code: "AuthorizationFailure" }; + await expect(store.compareAndSwap(key, 1, null)).rejects.toMatchObject({ + details: { errorCode: "AuthorizationFailure" }, + }); + }); + + it.each([ + "not-json", + "null", + "[]", + '{"version":1}', + '{"value":null}', + '{"version":0,"value":null}', + '{"version":-1,"value":null}', + '{"version":1.5,"value":null}', + '{"version":"1","value":null}', + '{"version":9007199254740992,"value":null}', + ])("fails closed on malformed stored wrappers: %s", async (text) => { + const { transport, store } = fixture(); + transport.text = text; + await expect(store.read(key)).rejects.toThrow("Invalid stored object."); + await expect(store.compareAndSwap(key, 1, null)).rejects.toThrow(); + expect(transport.requests.every((request) => request.method === "GET")).toBe(true); + }); + + it("requires an ETag on reads and refuses unbounded streamed bodies", async () => { + const { transport, store } = fixture(); + transport.text = '{"version":1,"value":null}'; + transport.omitEtag = true; + await expect(store.read(key)).rejects.toThrow(/etag/i); + await expect(store.compareAndSwap(key, 1, null)).rejects.toThrow(/etag/i); + transport.omitEtag = false; + transport.text = "x".repeat(4 * 1024 * 1024 + 1); + await expect(store.read(key)).rejects.toThrow("size"); + transport.omitLength = true; + await expect(store.read(key)).rejects.toThrow(/length|limits/); + }); + + it("rejects unsafe keys, versions, and unsupported JSON before any network request", async () => { + const { transport, store } = fixture(); + for (const invalidKey of ["short", "../" + key, key + "/", "x".repeat(129), "é".repeat(32)]) { + await expect(store.read(invalidKey)).rejects.toThrow("key"); + await expect(store.create(invalidKey, null)).rejects.toThrow("key"); + await expect(store.compareAndSwap(invalidKey, 1, null)).rejects.toThrow("key"); + } + for (const version of [0, -1, 1.5, NaN, Infinity, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER + 1]) { + await expect(store.compareAndSwap(key, version, null)).rejects.toThrow("version"); + } + const circular: { self?: object } = {}; + circular.self = circular; + const withGetter = Object.defineProperty({}, "value", { enumerable: true, get() { throw new Error("secret"); } }); + for (const value of [undefined, { missing: undefined }, [undefined], [,], NaN, Infinity, -0, + 1n, () => null, Symbol("synthetic"), new Date(0), new Map(), circular, withGetter, + new Proxy({}, {}), "x".repeat(4 * 1024 * 1024 + 1)]) { + await expect(store.create(key, value)).rejects.toThrow("Invalid or oversized storage object."); + await expect(store.compareAndSwap(key, 1, value)).rejects.toThrow("Invalid or oversized storage object."); + } + expect(transport.requests).toHaveLength(0); + }); + + it("preserves all supported JSON values without altering ciphertext", async () => { + const { store } = fixture(); + const value = { ciphertext: "opaque+/==_-", unicode: "é", values: [null, true, false, 0, 1.25, {}, []] }; + await store.create(key, value); + expect(await store.read(key)).toEqual({ version: 1, value }); + }); +}); diff --git a/test/blob-store-contract.spec.ts b/test/blob-store-contract.spec.ts new file mode 100644 index 0000000..e05827c --- /dev/null +++ b/test/blob-store-contract.spec.ts @@ -0,0 +1,24 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterAll } from "vitest"; +import { FileBlobStore, InMemoryBlobStore } from "../src/index.js"; +import { blobStoreContract } from "./helpers/blob-store-contract.js"; + +const directories: string[] = []; +afterAll(async () => { + for (const directory of directories) { + await rm(directory, { recursive: true, force: true }); + } +}); + +blobStoreContract("memory", async () => { + const store = new InMemoryBlobStore(); + return { first: store, second: store }; +}); + +blobStoreContract("file (same-process clients)", async () => { + const directory = await mkdtemp(join(tmpdir(), "gphr-contract-")); + directories.push(directory); + return { first: new FileBlobStore(directory), second: new FileBlobStore(directory) }; +}); diff --git a/test/configuration.spec.ts b/test/configuration.spec.ts new file mode 100644 index 0000000..c441ea8 --- /dev/null +++ b/test/configuration.spec.ts @@ -0,0 +1,125 @@ +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { afterAll, describe, expect, it, vi } from "vitest"; +import { + flag, loadRuntimeSecret, positiveInteger, required, storageBackend, storageEndpoint, +} from "../src/storage/configuration.js"; + +const vault = vi.hoisted(() => ({ getSecret: vi.fn() })); +vi.mock("@azure/keyvault-secrets", () => ({ + SecretClient: class { + getSecret = vault.getSecret; + }, +})); + +const directories: string[] = []; +afterAll(async () => { + for (const directory of directories) await rm(directory, { recursive: true, force: true }); +}); + +describe("storage configuration", () => { + it("defaults to file and rejects unknown or blank backends", () => { + expect(storageBackend({})).toBe("file"); + for (const backend of ["", "typo", "S3"]) { + expect(() => storageBackend({ STORAGE_BACKEND: backend })).toThrow(); + } + for (const backend of ["file", "memory", "s3", "azure", "ipfs"]) { + expect(storageBackend({ STORAGE_BACKEND: backend })).toBe(backend); + } + }); + + it("validates required values, booleans, and integer limits", () => { + expect(() => required({}, "S3_BUCKET")).toThrow("S3_BUCKET"); + expect(() => required({ S3_BUCKET: " " }, "S3_BUCKET")).toThrow(); + expect(flag({}, "FLAG")).toBe(false); + expect(flag({ FLAG: "true" }, "FLAG")).toBe(true); + expect(() => flag({ FLAG: "1" }, "FLAG")).toThrow(); + expect(positiveInteger({}, "N", 2)).toBe(2); + expect(positiveInteger({ N: "3" }, "N", 2)).toBe(3); + for (const value of ["0", "-1", "1.2", "Infinity", "9007199254740992", ""]) { + expect(() => positiveInteger({ N: value }, "N", 2)).toThrow(); + } + }); + + it("requires secure endpoints and rejects secrets embedded in URLs", () => { + expect(storageEndpoint("https://account.blob.core.windows.net", "ENDPOINT").protocol) + .toBe("https:"); + for (const value of [ + "invalid", "ftp://example.test", "http://example.test", + "https://user:password@example.test", "https://example.test?sig=secret", + "https://example.test#secret", "http://127.0.0.1:10000", + ]) { + expect(() => storageEndpoint(value, "ENDPOINT")).toThrow(); + } + const secret = "not-for-error-output"; + expect(() => storageEndpoint(`https://${secret}@example.test`, "ENDPOINT")) + .toThrow(/^ENDPOINT requires/); + }); + + it("limits insecure emulator exceptions to loopback", () => { + for (const host of ["localhost", "127.0.0.1", "[::1]"]) { + expect(storageEndpoint(`http://${host}:10000`, "ENDPOINT", true).protocol).toBe("http:"); + } + for (const host of ["192.168.0.1", "0.0.0.0", "localhost.example.test", "example.test"]) { + expect(() => storageEndpoint(`http://${host}:10000`, "ENDPOINT", true)).toThrow(); + } + }); +}); + +describe("runtime secret sources", () => { + it("retrieves a named Key Vault secret at runtime and propagates access failures", async () => { + const env = { + IPFS_AUTH_KEYVAULT_SECRET: "synthetic-token", + AZURE_KEY_VAULT_URL: "https://synthetic.vault.azure.net", + }; + vault.getSecret.mockResolvedValueOnce({ value: "Bearer synthetic-from-vault" }); + expect(await loadRuntimeSecret(env, "IPFS_AUTH")).toBe("Bearer synthetic-from-vault"); + expect(vault.getSecret).toHaveBeenCalledWith("synthetic-token"); + vault.getSecret.mockResolvedValueOnce({}); + await expect(loadRuntimeSecret(env, "IPFS_AUTH")).rejects.toThrow("nonempty"); + vault.getSecret.mockRejectedValueOnce(new Error("synthetic access denied")); + await expect(loadRuntimeSecret(env, "IPFS_AUTH")).rejects.toThrow("access denied"); + }); + + it("permits an unset source but rejects inline and ambiguous sources", async () => { + expect(await loadRuntimeSecret({}, "IPFS_AUTH")).toBeUndefined(); + await expect(loadRuntimeSecret({ IPFS_AUTH: "secret" }, "IPFS_AUTH")) + .rejects.toThrow("not an inline secret"); + await expect(loadRuntimeSecret({ + IPFS_AUTH_FILE: "some-file", IPFS_AUTH_KEYVAULT_SECRET: "some-name", + }, "IPFS_AUTH")).rejects.toThrow("Choose one"); + }); + + it("loads a mounted secret file without requiring cloud credentials", async () => { + const directory = await mkdtemp(join(tmpdir(), "gphr-secret-test-")); + directories.push(directory); + const file = join(directory, "synthetic-token"); + await writeFile(file, "Bearer synthetic-only\n"); + expect(await loadRuntimeSecret({ IPFS_AUTH_FILE: file }, "IPFS_AUTH")) + .toBe("Bearer synthetic-only"); + await writeFile(file, "first\nsecond"); + await expect(loadRuntimeSecret({ IPFS_AUTH_FILE: file }, "IPFS_AUTH")) + .rejects.toThrow("single-line"); + await writeFile(file, ""); + await expect(loadRuntimeSecret({ IPFS_AUTH_FILE: file }, "IPFS_AUTH")).rejects.toThrow(); + await writeFile(file, "x".repeat(16_385)); + await expect(loadRuntimeSecret({ IPFS_AUTH_FILE: file }, "IPFS_AUTH")).rejects.toThrow(); + }); + + it("fails closed on missing files or malformed vault config", async () => { + await expect(loadRuntimeSecret({ IPFS_AUTH_FILE: "does-not-exist" }, "IPFS_AUTH")) + .rejects.toThrow(); + await expect(loadRuntimeSecret({ IPFS_AUTH_KEYVAULT_SECRET: "token" }, "IPFS_AUTH")) + .rejects.toThrow("AZURE_KEY_VAULT_URL"); + await expect(loadRuntimeSecret({ + IPFS_AUTH_KEYVAULT_SECRET: "bad/name", AZURE_KEY_VAULT_URL: "https://example.vault.azure.net", + }, "IPFS_AUTH")).rejects.toThrow("invalid"); + await expect(loadRuntimeSecret({ + IPFS_AUTH_KEYVAULT_SECRET: "token", AZURE_KEY_VAULT_URL: "http://example.vault.azure.net", + }, "IPFS_AUTH")).rejects.toThrow("HTTPS"); + await expect(loadRuntimeSecret({ + IPFS_AUTH_KEYVAULT_SECRET: "token", AZURE_KEY_VAULT_URL: "https://untrusted.example.test", + }, "IPFS_AUTH")).rejects.toThrow("Azure vault root"); + }); +}); diff --git a/test/configured-blob-store.spec.ts b/test/configured-blob-store.spec.ts new file mode 100644 index 0000000..6302af0 --- /dev/null +++ b/test/configured-blob-store.spec.ts @@ -0,0 +1,106 @@ +import { describe, expect, it, vi } from "vitest"; +import { + AzureBlobStore, + FileBlobStore, + InMemoryBlobStore, + S3BlobStore, + StorageConfigurationError, + createConfiguredBlobStore, +} from "../src/index.js"; + +describe("configured store selection", () => { + it("preserves the local default and supports memory explicitly", async () => { + expect(await createConfiguredBlobStore({})).toBeInstanceOf(FileBlobStore); + expect(await createConfiguredBlobStore({ STORAGE_BACKEND: "memory" })) + .toBeInstanceOf(InMemoryBlobStore); + }); + + it("constructs cloud adapters without fetching credentials or creating resources", async () => { + expect(await createConfiguredBlobStore({ + STORAGE_BACKEND: "azure", + AZURE_STORAGE_ACCOUNT_URL: "https://synthetic.blob.core.windows.net", + AZURE_STORAGE_CONTAINER: "synthetic-records", + })).toBeInstanceOf(AzureBlobStore); + expect(await createConfiguredBlobStore({ + STORAGE_BACKEND: "s3", S3_BUCKET: "synthetic-records", AWS_REGION: "us-east-1", + })).toBeInstanceOf(S3BlobStore); + }); + + it("fails closed instead of falling back when a selected backend is incomplete", async () => { + for (const env of [ + { STORAGE_BACKEND: "unknown" }, + { STORAGE_BACKEND: "azure" }, + { STORAGE_BACKEND: "s3" }, + { STORAGE_BACKEND: "ipfs" }, + { STORAGE_BACKEND: "azure", AZURE_STORAGE_CONNECTION_STRING: "not-a-real-key" }, + ]) { + await expect(createConfiguredBlobStore(env)).rejects.toBeInstanceOf(StorageConfigurationError); + } + }); + + it("never directs emulator credentials to a remote cloud endpoint", async () => { + await expect(createConfiguredBlobStore({ + STORAGE_BACKEND: "azure", STORAGE_EMULATOR: "true", + AZURE_STORAGE_ACCOUNT_URL: "https://synthetic.blob.core.windows.net", + AZURE_STORAGE_CONTAINER: "synthetic-records", + })).rejects.toThrow("loopback"); + await expect(createConfiguredBlobStore({ + STORAGE_BACKEND: "s3", STORAGE_EMULATOR: "true", + S3_BUCKET: "synthetic-records", AWS_REGION: "us-east-1", + })).rejects.toThrow("loopback"); + await expect(createConfiguredBlobStore({ + STORAGE_BACKEND: "s3", STORAGE_EMULATOR: "true", + S3_BUCKET: "synthetic-records", AWS_REGION: "us-east-1", + S3_ENDPOINT_URL: "https://remote.example.test", + })).rejects.toThrow("loopback"); + await expect(createConfiguredBlobStore({ + STORAGE_BACKEND: "ipfs", STORAGE_EMULATOR: "true", + IPFS_PRIVATE_NETWORK: "true", IPFS_METADATA_BACKEND: "file", + IPFS_API_URL: "https://remote.example.test", + IPFS_CLUSTER_API_URL: "http://127.0.0.1:9094", + })).rejects.toThrow("loopback"); + }); + + it("validates resource names and refuses TLS bypass", async () => { + await expect(createConfiguredBlobStore({ + STORAGE_BACKEND: "azure", + AZURE_STORAGE_ACCOUNT_URL: "https://synthetic.blob.core.windows.net", + AZURE_STORAGE_CONTAINER: "../invalid", + })).rejects.toThrow("container name"); + await expect(createConfiguredBlobStore({ + STORAGE_BACKEND: "s3", S3_BUCKET: "not/a/bucket", AWS_REGION: "us-east-1", + })).rejects.toThrow("bucket name"); + await expect(createConfiguredBlobStore({ NODE_TLS_REJECT_UNAUTHORIZED: "0" })) + .rejects.toThrow("TLS certificate validation"); + vi.stubEnv("NODE_TLS_REJECT_UNAUTHORIZED", "0"); + try { + await expect(createConfiguredBlobStore({})).rejects.toThrow("TLS certificate validation"); + } finally { + vi.unstubAllEnvs(); + } + }); + + it("requires a private IPFS acknowledgement and durable index outside emulator mode", async () => { + await expect(createConfiguredBlobStore({ STORAGE_BACKEND: "ipfs" })) + .rejects.toThrow("IPFS_PRIVATE_NETWORK"); + for (const backend of ["file", "memory", "ipfs"]) { + await expect(createConfiguredBlobStore({ + STORAGE_BACKEND: "ipfs", IPFS_PRIVATE_NETWORK: "true", IPFS_METADATA_BACKEND: backend, + })).rejects.toThrow("IPFS metadata"); + } + }); + + it("rejects insufficient replication and missing remote API authentication", async () => { + const base = { + STORAGE_BACKEND: "ipfs", IPFS_PRIVATE_NETWORK: "true", IPFS_METADATA_BACKEND: "azure", + IPFS_API_URL: "https://private-kubo.example.test", + IPFS_CLUSTER_API_URL: "https://private-cluster.example.test", + }; + await expect(createConfiguredBlobStore({ ...base, IPFS_REPLICATION_MIN: "1" })) + .rejects.toThrow("replication"); + await expect(createConfiguredBlobStore({ + ...base, IPFS_REPLICATION_MIN: "3", IPFS_REPLICATION_MAX: "2", + })).rejects.toThrow("replication"); + await expect(createConfiguredBlobStore(base)).rejects.toThrow("runtime authorization"); + }); +}); diff --git a/test/distributed-blob-store.spec.ts b/test/distributed-blob-store.spec.ts new file mode 100644 index 0000000..f77f495 --- /dev/null +++ b/test/distributed-blob-store.spec.ts @@ -0,0 +1,180 @@ +import { describe, expect, it } from "vitest"; +import { + MAX_DISTRIBUTED_BLOB_BYTES, + rawContentCid, + type DistributedContentStore, +} from "../src/storage/distributed-blob-store.js"; +import { + DistributedBlobStore, InMemoryBlobStore, + ConcurrentUpdateError, ObjectAlreadyExistsError, ObjectNotFoundError, +} from "../src/index.js"; + +const key = "a".repeat(32); +class Content implements DistributedContentStore { + readonly blocks = new Map(); + public async put(bytes: Uint8Array): Promise { + const cid = await rawContentCid(bytes); + this.blocks.set(cid, Uint8Array.from(bytes)); + return cid; + } + public async get(cid: string): Promise { + const bytes = this.blocks.get(cid); + if (!bytes) throw new Error("Content unavailable."); + return Uint8Array.from(bytes); + } +} + +function fixture() { + const metadata = new InMemoryBlobStore(); + const content = new Content(); + return { metadata, content, store: new DistributedBlobStore(metadata, content) }; +} + +describe("DistributedBlobStore", () => { + it("round trips opaque ciphertext without placing it in the pointer index", async () => { + const { store, metadata, content } = fixture(); + const value = { ciphertext: "synthetic-ciphertext", nonce: "opaque" }; + expect(await store.read(key)).toBeUndefined(); + await store.create(key, value); + expect(await store.read(key)).toEqual({ version: 1, value }); + expect(JSON.stringify(metadata.snapshot())).not.toContain("synthetic-ciphertext"); + expect([...content.blocks.values()].map((bytes) => Buffer.from(bytes).toString()).join("")) + .not.toContain("Patient"); + expect(await store.compareAndSwap(key, 1, null)).toBe(2); + expect(await store.read(key)).toEqual({ version: 2, value: null }); + }); + + it("preserves create, missing and stale-version errors", async () => { + const { store } = fixture(); + await expect(store.compareAndSwap(key, 1, {})).rejects.toBeInstanceOf(ObjectNotFoundError); + await store.create(key, {}); + await expect(store.create(key, {})).rejects.toBeInstanceOf(ObjectAlreadyExistsError); + await expect(store.compareAndSwap(key, 2, {})).rejects.toBeInstanceOf(ConcurrentUpdateError); + }); + + it("allows only one concurrent publisher across adapter instances", async () => { + const { store, metadata, content } = fixture(); + const other = new DistributedBlobStore(metadata, content); + await store.create(key, "old"); + const results = await Promise.allSettled([ + store.compareAndSwap(key, 1, "first"), + other.compareAndSwap(key, 1, "second"), + ]); + expect(results.filter((result) => result.status === "fulfilled")).toHaveLength(1); + expect(results.find((result) => result.status === "rejected")) + .toMatchObject({ reason: expect.any(ConcurrentUpdateError) }); + expect((await store.read(key))?.version).toBe(2); + expect(content.blocks.size).toBe(3); + }); + + it("allows only one concurrent create across adapter instances", async () => { + const { store, metadata, content } = fixture(); + const results = await Promise.allSettled([ + store.create(key, "first"), + new DistributedBlobStore(metadata, content).create(key, "second"), + ]); + expect(results.filter((result) => result.status === "fulfilled")).toHaveLength(1); + expect(results.find((result) => result.status === "rejected")) + .toMatchObject({ reason: expect.any(ObjectAlreadyExistsError) }); + }); + + it("keeps pending replication invisible to readers", async () => { + const { metadata, content } = fixture(); + let complete!: () => void; + const replication = new Promise((resolve) => { complete = resolve; }); + let started!: () => void; + const uploadStarted = new Promise((resolve) => { started = resolve; }); + const store = new DistributedBlobStore(metadata, { + put: async (bytes) => { + const cid = await content.put(bytes); + started(); + await replication; + return cid; + }, + get: content.get.bind(content), + }); + const create = store.create(key, "opaque"); + await uploadStarted; + expect(await store.read(key)).toBeUndefined(); + complete(); + await create; + expect(await store.read(key)).toEqual({ version: 1, value: "opaque" }); + }); + + it("never publishes before replication succeeds and propagates index errors", async () => { + const { metadata, content } = fixture(); + const failed = new DistributedBlobStore(metadata, { + put: async () => { throw new Error("Replication unavailable."); }, + get: content.get.bind(content), + }); + await expect(failed.create(key, {})).rejects.toThrow("Replication unavailable"); + expect(await metadata.read(key)).toBeUndefined(); + const store = new DistributedBlobStore({ + read: metadata.read.bind(metadata), + create: async () => { throw new Error("Index unavailable."); }, + compareAndSwap: metadata.compareAndSwap.bind(metadata), + }, content); + await expect(store.create(key, {})).rejects.toThrow("Index unavailable"); + expect(content.blocks.size).toBe(1); + }); + + it("rejects unavailable, truncated and modified content", async () => { + const { store, content } = fixture(); + await store.create(key, "opaque"); + const [cid, bytes] = [...content.blocks.entries()][0]!; + content.blocks.set(cid, bytes.slice(0, -1)); + await expect(store.read(key)).rejects.toThrow("integrity"); + content.blocks.set(cid, Uint8Array.from([1, 2])); + await expect(store.read(key)).rejects.toThrow("integrity"); + content.blocks.delete(cid); + await expect(store.read(key)).rejects.toThrow("unavailable"); + }); + + it("rejects content stores that return another CID", async () => { + const { metadata, content } = fixture(); + const store = new DistributedBlobStore(metadata, { + put: () => rawContentCid(Uint8Array.from([1])), + get: content.get.bind(content), + }); + await expect(store.create(key, {})).rejects.toThrow("integrity"); + expect(await metadata.read(key)).toBeUndefined(); + }); + + it.each(["../bad", "a", "a/b", "秘密".repeat(32)])("rejects key %s", async (bad) => { + const { store } = fixture(); + await expect(store.read(bad)).rejects.toThrow("key"); + await expect(store.create(bad, {})).rejects.toThrow("key"); + await expect(store.compareAndSwap(bad, 1, {})).rejects.toThrow("key"); + }); + + it.each([0, -1, 1.5, NaN, Infinity, Number.MAX_SAFE_INTEGER])( + "rejects invalid write version %s", async (version) => { + await expect(fixture().store.compareAndSwap(key, version, {})).rejects.toThrow(); + }, + ); + + it.each([null, {}, { cid: "invalid", format: "ipfs-raw-v1" }, + { cid: "invalid", format: "other" }])("rejects malformed pointer %j", async (value) => { + const { metadata, store } = fixture(); + await metadata.create(key, value); + await expect(store.read(key)).rejects.toThrow(); + await expect(store.compareAndSwap(key, 1, {})).rejects.toThrow(); + }); + + it("rejects invalid JSON values and oversized raw blocks without publication", async () => { + const { store, metadata } = fixture(); + await expect(store.create(key, undefined)).rejects.toThrow(); + await expect(store.create(key, "x".repeat(MAX_DISTRIBUTED_BLOB_BYTES))).rejects.toThrow("size"); + expect(await metadata.read(key)).toBeUndefined(); + }); + + it("rejects invalid encoding and serialized content despite correct hashes", async () => { + for (const bytes of [Uint8Array.from([255]), Buffer.from("{secret"), + Buffer.from('{"version":2,"value":null}')]) { + const { store, metadata, content } = fixture(); + const cid = await content.put(bytes); + await metadata.create(key, { format: "ipfs-raw-v1", cid }); + await expect(store.read(key)).rejects.toThrow(); + } + }); +}); diff --git a/test/fixtures/ipfs/.gitattributes b/test/fixtures/ipfs/.gitattributes new file mode 100644 index 0000000..dfdb8b7 --- /dev/null +++ b/test/fixtures/ipfs/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/test/fixtures/ipfs/README.md b/test/fixtures/ipfs/README.md new file mode 100644 index 0000000..f611f84 --- /dev/null +++ b/test/fixtures/ipfs/README.md @@ -0,0 +1,94 @@ +# Private IPFS integration fixture + +Run `npm run test:integration:ipfs` from the project root with Node.js 22+ +and a running Docker engine capable of Linux containers. No hosted account, +API token, public pinning service, or additional npm dependency is required. +Default `npm test` only runs the Docker-free fixture configuration tests. + +The runner pulls version-pinned official images `ipfs/kubo:v0.43.1` and +`ipfs/ipfs-cluster:v1.1.6`. These versions and configuration keys were checked +against official documentation and tagged source on September 18, 2026. +Image pulls require internet access; the running fixture does not. + +## Isolation and lifecycle + +- Three Kubo nodes and three Raft Cluster nodes share one unique Docker + **internal** bridge network. No host swarm, gateway, proxy, metrics, or + cluster peer ports are published. API ports use dynamically assigned + `127.0.0.1` bindings only. These unauthenticated administrative APIs are + for an isolated development/CI machine, not a shared untrusted host. +- Kubo requires `IPFS_FORCE_PNET=1`, an independently generated swarm key, + no public bootstrap peers, no DHT/routing/providing, no delegated routing + or IPNS publishing, no automatic remote configuration, no mDNS, no AutoTLS, + and no relay/NAT mapping. Peers connect explicitly by internal DNS plus + their discovered public peer IDs. +- A distinct random Cluster secret is shared among the three Cluster nodes. + Raft followers explicitly bootstrap to the first Cluster peer. Raft is + chosen to exercise this fixture's quorum scenario, not recommended as a + production topology. +- Both secrets travel on the Docker client's stdin into a uniquely named + Docker volume. They are never written to the repository or host scratch + files, passed in command arguments, or included in Docker's saved + environment. Secret files are mode `0400`. Peer private identities and + runtime configs stay in private per-node Docker volumes. +- Fresh data volumes use `volume-nocopy`, retaining root ownership for this + fixture's UID 0 processes with all capabilities dropped, rather than copying + image UID 1000 ownership that would prevent initialization. No permission-bypass + capability is granted. +- The Docker log driver is disabled. The runner never dumps daemon logs, + configuration, identities, upstream errors, or secrets. +- Only this invocation's named containers, attached anonymous volumes, + named volumes, network, and unique local metadata directory are removed. + No global prune/kill operation is used. Cleanup runs on normal failure and + catchable termination signals. Forced machine/process termination can + leave resources bearing the unique `gphr-ipfs-` prefix; review ownership + before manually removing any such resources. + +## Application suite contract + +The runner invokes the existing `vitest.integration.config.ts` with: + +```text +STORAGE_INTEGRATION=true +STORAGE_BACKEND=ipfs +STORAGE_EMULATOR=true +IPFS_PRIVATE_NETWORK=true +IPFS_API_URL=http://127.0.0.1: +IPFS_CLUSTER_API_URL=http://127.0.0.1: +IPFS_METADATA_BACKEND=file +STORAGE_FILE_DIRECTORY= +IPFS_REPLICATION_MIN=2 +IPFS_REPLICATION_MAX=3 +``` + +Readiness requires all three cluster members and allocation metrics. After +the application suite succeeds, the runner adds a separate random opaque +1024-byte synthetic ciphertext marker to Kubo 1, requests Cluster replication +with minimum 2 / maximum 3, and waits for recursive pins on **all three** +concrete Kubo peers. It then stops Kubo 1 and Cluster 1 and retrieves the same +existing CID from Kubo 2, comparing every byte. It does not rewrite the probe +after node loss. + +This proves existing block availability on an independent replica if the +live run succeeds. It does **not** prove automatic API failover, application +record retrieval after losing its index, metadata-index replication, machine +or region failure tolerance, or production/clinical readiness. The local +metadata index remains a single point of failure. + +## Official references + +- Kubo Docker and swarm-key documentation: + `https://docs.ipfs.tech/install/run-ipfs-inside-docker/` +- Tagged Kubo configuration: + `https://github.com/ipfs/kubo/blob/v0.43.1/docs/config.md` +- Cluster distribution versions: + `https://dist.ipfs.tech/ipfs-cluster-service/versions` +- Official Cluster container images and environment configuration: + `https://ipfscluster.io/documentation/deployment/automations/` +- Explicit Raft bootstrap: + `https://ipfscluster.io/documentation/deployment/bootstrap/` +- API behavior and routes: + `https://ipfscluster.io/documentation/reference/api/` + +Live validation requires Docker. A failed engine preflight is a failed +integration run, never a passing/skipped network durability test. diff --git a/test/fixtures/ipfs/cluster.sh b/test/fixtures/ipfs/cluster.sh new file mode 100644 index 0000000..5bdc90e --- /dev/null +++ b/test/fixtures/ipfs/cluster.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -eu +umask 077 +export CLUSTER_SECRET="$(cat /run/fixture-secrets/cluster-secret)" +ipfs-cluster-service init --consensus raft +unset CLUSTER_SECRET +exec ipfs-cluster-service daemon "$@" diff --git a/test/fixtures/ipfs/fixture.spec.ts b/test/fixtures/ipfs/fixture.spec.ts new file mode 100644 index 0000000..d8a8421 --- /dev/null +++ b/test/fixtures/ipfs/fixture.spec.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from "vitest"; +import { readFile } from "node:fs/promises"; +import { CLUSTER_IMAGE, KUBO_IMAGE, containerArguments, loopbackEndpoint, secretInput } from "./fixture.js"; + +describe("private IPFS Docker fixture configuration (no Docker required)", () => { + const options = { + name: "unique-kubo1", network: "unique-private", alias: "kubo1", + dataVolume: "unique-data", secretsVolume: "unique-secrets", scriptDirectory: "/project/fixture", + }; + + it.each(["kubo", "cluster"] as const)("isolates %s APIs and secret mounts", (kind) => { + const args = containerArguments({ ...options, alias: `${kind}1`, kind }); + expect(args).toContain("--read-only"); + expect(args).toContain("--cap-drop"); + expect(args).toContain("no-new-privileges"); + expect(args).toContain( + `type=volume,source=unique-data,target=${kind === "kubo" ? "/data/ipfs" : "/data/ipfs-cluster"},volume-nocopy`, + ); + expect(args).toContain("type=volume,source=unique-secrets,target=/run/fixture-secrets,readonly"); + expect(args.filter((arg) => arg.startsWith("127.0.0.1::"))).toEqual([ + `127.0.0.1::${kind === "kubo" ? "5001" : "9094"}`, + ]); + expect(args).not.toContain("--publish-all"); + expect(args.join(" ")).not.toContain("CLUSTER_SECRET="); + expect(args).toContain(kind === "kubo" ? KUBO_IMAGE : CLUSTER_IMAGE); + expect(args).not.toContain("latest"); + }); + + it("forces a private swarm and explicitly bootstraps Raft followers", () => { + expect(containerArguments({ ...options, kind: "kubo" })).toContain("IPFS_FORCE_PNET=1"); + const bootstrap = "/dns4/cluster1/tcp/9096/p2p/public-peer-id"; + const args = containerArguments({ ...options, alias: "cluster2", kind: "cluster", bootstrap }); + expect(args.slice(-2)).toEqual(["--bootstrap", bootstrap]); + expect(args).toContain("CLUSTER_IPFSHTTP_NODEMULTIADDRESS=/dns4/kubo2/tcp/5001"); + expect(args).toContain("CLUSTER_REPLICATIONFACTORMIN=2"); + expect(args).toContain("CLUSTER_REPLICATIONFACTORMAX=3"); + }); + + it("generates independent fresh swarm and cluster secrets", () => { + const first = secretInput(); + expect(first).toMatch(/^[a-f0-9]{64}\n[a-f0-9]{64}\n$/); + const [swarm, cluster] = first.split("\n"); + expect(swarm === cluster).toBe(false); + expect(first === secretInput()).toBe(false); + }); + + it("accepts only one explicit loopback mapping", () => { + expect(loopbackEndpoint("127.0.0.1:49152\n")).toBe("http://127.0.0.1:49152"); + for (const value of ["0.0.0.0:5001", "[::]:5001", "127.0.0.1:0", "127.0.0.1:65536", "127.0.0.1:5001\n0.0.0.0:5001"]) { + expect(() => loopbackEndpoint(value)).toThrow(); + } + }); + + it("disables public discovery before starting the private Kubo daemon", async () => { + const script = await readFile(new URL("./kubo.sh", import.meta.url), "utf8"); + for (const setting of [ + "export IPFS_FORCE_PNET=1", "ipfs bootstrap rm --all", + "ipfs config Routing.Type none", "ipfs config --json Provide.Enabled false", + "ipfs config --json AutoConf.Enabled false", "ipfs config --json AutoTLS.Enabled false", + "ipfs config --json Discovery.MDNS.Enabled false", + "ipfs config --json Routing.DelegatedRouters '[]'", + "ipfs config --json Ipns.DelegatedPublishers '[]'", + ]) { + expect(script.includes(setting)).toBe(true); + expect(script.indexOf(setting)).toBeLessThan(script.indexOf("exec ipfs daemon")); + } + const attributes = await readFile(new URL("./.gitattributes", import.meta.url), "utf8"); + expect(attributes).toContain("*.sh text eol=lf"); + }); +}); diff --git a/test/fixtures/ipfs/fixture.ts b/test/fixtures/ipfs/fixture.ts new file mode 100644 index 0000000..e1ef8ba --- /dev/null +++ b/test/fixtures/ipfs/fixture.ts @@ -0,0 +1,61 @@ +import { randomBytes } from "node:crypto"; + +export const KUBO_IMAGE = "ipfs/kubo:v0.43.1"; +export const CLUSTER_IMAGE = "ipfs/ipfs-cluster:v1.1.6"; + +// Secret values travel only on stdin, never Docker argv, labels, or its saved environment. +export function secretInput(): string { + return `${randomBytes(32).toString("hex")}\n${randomBytes(32).toString("hex")}\n`; +} + +export const SECRET_INIT_SCRIPT = [ + "set -eu", + "umask 077", + "read -r swarm", + "read -r cluster", + "printf '/key/swarm/psk/1.0.0/\\n/base16/\\n%s\\n' \"$swarm\" > /secrets/swarm.key", + "printf '%s\\n' \"$cluster\" > /secrets/cluster-secret", + "chmod 400 /secrets/swarm.key /secrets/cluster-secret", +].join("\n"); + +export function containerArguments(options: { + name: string; + network: string; + alias: string; + dataVolume: string; + secretsVolume: string; + scriptDirectory: string; + kind: "kubo" | "cluster"; + bootstrap?: string; +}): string[] { + const kubo = options.kind === "kubo"; + return [ + "create", "--name", options.name, "--network", options.network, + "--network-alias", options.alias, "--user", "0:0", + "--cap-drop", "ALL", "--security-opt", "no-new-privileges", + "--read-only", "--log-driver", "none", + "--mount", `type=volume,source=${options.dataVolume},target=${kubo ? "/data/ipfs" : "/data/ipfs-cluster"},volume-nocopy`, + "--mount", `type=volume,source=${options.secretsVolume},target=/run/fixture-secrets,readonly`, + "--mount", `type=bind,source=${options.scriptDirectory},target=/fixture,readonly`, + "--publish", `127.0.0.1::${kubo ? "5001" : "9094"}`, + ...(kubo + ? ["--env", "IPFS_FORCE_PNET=1"] + : [ + "--env", "CLUSTER_RESTAPI_HTTPLISTENMULTIADDRESS=/ip4/0.0.0.0/tcp/9094", + "--env", `CLUSTER_IPFSHTTP_NODEMULTIADDRESS=/dns4/${options.alias.replace("cluster", "kubo")}/tcp/5001`, + "--env", "CLUSTER_REPLICATIONFACTORMIN=2", + "--env", "CLUSTER_REPLICATIONFACTORMAX=3", + "--env", "CLUSTER_MONITORPINGINTERVAL=2s", + ]), + "--entrypoint", "/bin/sh", kubo ? KUBO_IMAGE : CLUSTER_IMAGE, + `/fixture/${options.kind}.sh`, + ...(options.bootstrap ? ["--bootstrap", options.bootstrap] : []), + ]; +} + +export function loopbackEndpoint(portOutput: string): string { + const match = /^127\.0\.0\.1:(\d+)\s*$/.exec(portOutput); + const port = Number(match?.[1]); + if (!match || port < 1 || port > 65_535) throw new Error("Non-loopback Docker API binding."); + return `http://127.0.0.1:${port}`; +} diff --git a/test/fixtures/ipfs/kubo.sh b/test/fixtures/ipfs/kubo.sh new file mode 100644 index 0000000..af41677 --- /dev/null +++ b/test/fixtures/ipfs/kubo.sh @@ -0,0 +1,22 @@ +#!/bin/sh +set -eu +umask 077 +export IPFS_FORCE_PNET=1 +ipfs init +cp /run/fixture-secrets/swarm.key "$IPFS_PATH/swarm.key" +chmod 400 "$IPFS_PATH/swarm.key" +ipfs bootstrap rm --all +ipfs config Addresses.API /ip4/0.0.0.0/tcp/5001 +ipfs config --json Addresses.Gateway '""' +ipfs config --json Addresses.Swarm '["/ip4/0.0.0.0/tcp/4001"]' +ipfs config --json Discovery.MDNS.Enabled false +ipfs config Routing.Type none +ipfs config --json Routing.DelegatedRouters '[]' +ipfs config --json Ipns.DelegatedPublishers '[]' +ipfs config --json AutoConf.Enabled false +ipfs config --json Provide.Enabled false +ipfs config --json Swarm.DisableNatPortMap true +ipfs config --json Swarm.RelayClient.Enabled false +ipfs config --json Swarm.RelayService.Enabled false +ipfs config --json AutoTLS.Enabled false +exec ipfs daemon --migrate=false diff --git a/test/helpers/blob-store-contract.ts b/test/helpers/blob-store-contract.ts new file mode 100644 index 0000000..099990b --- /dev/null +++ b/test/helpers/blob-store-contract.ts @@ -0,0 +1,91 @@ +import { randomBytes } from "node:crypto"; +import { describe, expect, it } from "vitest"; +import { + ConcurrentUpdateError, + ObjectAlreadyExistsError, + ObjectNotFoundError, + type BlobStore, +} from "../../src/index.js"; + +export interface StorePair { + first: BlobStore; + second: BlobStore; +} + +export function blobStoreContract(name: string, setup: () => Promise): void { + describe(`${name} BlobStore contract`, () => { + const key = () => randomBytes(24).toString("hex"); + + it("distinguishes a missing object from a stored null", async () => { + const { first, second } = await setup(); + const id = key(); + expect(await first.read(id)).toBeUndefined(); + await first.create(id, null); + expect(await second.read(id)).toEqual({ version: 1, value: null }); + }); + + it("creates once and never overwrites on duplicate creation", async () => { + const { first, second } = await setup(); + const id = key(); + const value = { ciphertext: "synthetic-opaque-value" }; + await first.create(id, value); + value.ciphertext = "changed-outside-store"; + await expect(second.create(id, { ciphertext: "replacement" })) + .rejects.toBeInstanceOf(ObjectAlreadyExistsError); + expect(await second.read(id)).toEqual({ + version: 1, value: { ciphertext: "synthetic-opaque-value" }, + }); + }); + + it("round-trips nested JSON without sharing mutable references", async () => { + const { first, second } = await setup(); + const id = key(); + const value = { data: [null, true, 2, { encoded: "test" }] }; + await first.create(id, value); + const read = await first.read(id); + expect(read?.value).toEqual(value); + read!.value.data.push({ encoded: "local mutation" }); + expect((await second.read(id))?.value).toEqual(value); + }); + + it("increments versions and rejects stale and missing CAS", async () => { + const { first, second } = await setup(); + const id = key(); + await expect(first.compareAndSwap(id, 1, "missing")) + .rejects.toBeInstanceOf(ObjectNotFoundError); + await first.create(id, "one"); + expect(await second.compareAndSwap(id, 1, "two")).toBe(2); + await expect(first.compareAndSwap(id, 1, "stale")) + .rejects.toBeInstanceOf(ConcurrentUpdateError); + expect(await first.read(id)).toEqual({ version: 2, value: "two" }); + expect(await first.compareAndSwap(id, 2, "three")).toBe(3); + }); + + it("allows only one concurrent create across independent clients", async () => { + const { first, second } = await setup(); + const id = key(); + const results = await Promise.allSettled([ + first.create(id, "first"), second.create(id, "second"), + ]); + expect(results.filter((result) => result.status === "fulfilled")).toHaveLength(1); + const failure = results.find((result) => result.status === "rejected"); + expect(failure).toMatchObject({ reason: expect.any(ObjectAlreadyExistsError) }); + }); + + it("allows only one writer for a version across independent clients", async () => { + const { first, second } = await setup(); + const id = key(); + await first.create(id, "initial"); + const results = await Promise.allSettled([ + first.compareAndSwap(id, 1, "first"), + second.compareAndSwap(id, 1, "second"), + ]); + expect(results.filter((result) => result.status === "fulfilled")).toHaveLength(1); + const failure = results.find((result) => result.status === "rejected"); + expect(failure).toMatchObject({ reason: expect.any(ConcurrentUpdateError) }); + const winner = await first.read(id); + expect(winner?.version).toBe(2); + expect(["first", "second"]).toContain(winner?.value); + }); + }); +} diff --git a/test/integration/configured-storage.spec.ts b/test/integration/configured-storage.spec.ts new file mode 100644 index 0000000..2f92d0b --- /dev/null +++ b/test/integration/configured-storage.spec.ts @@ -0,0 +1,93 @@ +import { randomBytes } from "node:crypto"; +import { describe, expect, it } from "vitest"; +import { + AccessDeniedError, + IntegrityError, + PatientRecordService, + RecordNotFoundError, + createClinicianCredential, + createConfiguredBlobStore, + type BlobStore, +} from "../../src/index.js"; +import { blobStoreContract } from "../helpers/blob-store-contract.js"; + +if (process.env.STORAGE_INTEGRATION !== "true") { + throw new Error("Integration tests require STORAGE_INTEGRATION=true and a dedicated synthetic-data store."); +} +if (!["azure", "s3", "ipfs"].includes(process.env.STORAGE_BACKEND ?? "")) { + throw new Error("Integration tests require an explicit azure, s3, or ipfs backend, never a fallback."); +} + +// Do not print SDK errors: their request metadata can contain authorization data. +async function safe(operation: () => Promise): Promise { + try { + return await operation(); + } catch { + throw new Error("Integration storage request failed; check connectivity, configuration, and identity permissions."); + } +} + +// Contract assertions need the exported error types, so suppress SDK transport +// details only; the adapters already translate expected contract failures. +function redacted(store: BlobStore): BlobStore { + async function call(operation: () => Promise): Promise { + try { + return await operation(); + } catch (error) { + if (error instanceof Error && + ["ObjectNotFoundError", "ObjectAlreadyExistsError", "ConcurrentUpdateError"].includes(error.name)) { + throw error; + } + throw new Error("Integration provider operation failed (transport details withheld)."); + } + } + return { + read: (key: string) => call(() => store.read(key)), + create: (key, value) => call(() => store.create(key, value)), + compareAndSwap: (key, version, value) => call(() => store.compareAndSwap(key, version, value)), + }; +} + +const connect = async () => redacted(await safe(() => createConfiguredBlobStore())); +blobStoreContract("configured external provider", async () => ({ + first: await connect(), second: await connect(), +})); + +describe("configured provider encrypted workflow", () => { + it("enrolls, appends, reconnects and refuses wrong factors", async () => { + const store = await connect(); + const records = new PatientRecordService(store); + const clinician = createClinicianCredential(); + const biometricToken = randomBytes(32).toString("base64url"); + const access = { clinician, biometricToken }; + await records.enroll(access); + const first = await records.append({ + ...access, + resource: { resourceType: "Condition", id: "synthetic-integration", code: { text: "SYNTHETIC-ONLY-MARKER" } }, + }); + const second = await records.append({ + ...access, + resource: { resourceType: "Observation", id: "synthetic-followup", status: "final", code: { text: "SYNTHETIC-ONLY-MARKER" } }, + }); + const reopened = new PatientRecordService(await connect()); + const timeline = await reopened.assemble(access); + expect(timeline).toHaveLength(2); + expect(timeline[1]?.event.parents).toEqual([first.eventId]); + expect(timeline[1]?.event.eventId).toBe(second.eventId); + for (const id of [first.fragmentId, first.eventId, second.fragmentId, second.eventId]) { + const raw = JSON.stringify(await store.read(id)); + expect(raw).not.toContain("SYNTHETIC-ONLY-MARKER"); + expect(raw).not.toContain(biometricToken); + expect(raw).not.toContain(clinician.privateKeyPem); + } + await expect(reopened.assemble({ ...access, clinician: createClinicianCredential() })) + .rejects.toBeInstanceOf(AccessDeniedError); + await expect(reopened.assemble({ ...access, biometricToken: randomBytes(32).toString("base64url") })) + .rejects.toBeInstanceOf(RecordNotFoundError); + + const stored = await store.read(first.eventId); + expect(stored).toBeDefined(); + await store.compareAndSwap(first.eventId, stored!.version, { kind: "tampered" }); + await expect(reopened.assemble(access)).rejects.toBeInstanceOf(IntegrityError); + }); +}); diff --git a/test/ipfs-cluster-client.spec.ts b/test/ipfs-cluster-client.spec.ts new file mode 100644 index 0000000..0fdc242 --- /dev/null +++ b/test/ipfs-cluster-client.spec.ts @@ -0,0 +1,228 @@ +import { describe, expect, it, vi } from "vitest"; +import { IpfsClusterClient } from "../src/index.js"; +import type { IpfsClusterClientOptions } from "../src/storage/ipfs-cluster-client.js"; +import { MAX_DISTRIBUTED_BLOB_BYTES, rawContentCid, validateRawCid } from "../src/storage/distributed-blob-store.js"; + +const bytes = Uint8Array.from([1, 2, 3]); +const base = { kuboUrl: "https://kubo.invalid", clusterUrl: "https://cluster.invalid" }; +const asJson = (value: unknown) => new Response(JSON.stringify(value)); +type Hook = (url: URL, init: RequestInit | undefined) => Response | undefined | Promise; + +async function fixture(options: Partial = {}, hook?: Hook) { + const cid = await rawContentCid(bytes); + const peers = [ + { id: "cluster-a", error: "", ipfs: { id: "ipfs-a", error: "" } }, + { id: "cluster-b", error: "", ipfs: { id: "ipfs-b", error: "" } }, + ]; + const status = { cid, peer_map: { + "cluster-a": { status: "pinned", error: "", ipfs_peer_id: "ipfs-a" }, + "cluster-b": { status: "pinned", error: "", ipfs_peer_id: "ipfs-b" }, + } }; + const fetch = vi.fn(async (input, init) => { + const url = new URL(String(input)); + const overridden = await hook?.(url, init); + if (overridden) return overridden; + if (url.pathname.endsWith("/block/put")) return asJson({ Key: cid, Size: bytes.length }); + if (url.pathname.endsWith("/block/get")) return new Response(bytes); + if (url.pathname === "/peers") return new Response(peers.map((peer) => JSON.stringify(peer)).join("\n")); + if (init?.method === "POST") return asJson({ cid }); + return asJson(status); + }); + const client = new IpfsClusterClient({ ...base, timeoutMs: 50, pollIntervalMs: 1, ...options, fetch }); + return { client, cid, fetch, status, peers }; +} + +describe("IpfsClusterClient", () => { + it("uses actual raw Kubo multipart and Cluster allocation APIs", async () => { + const { client, cid, fetch } = await fixture({ + kuboAuthorization: "Bearer kubo-test-secret", + clusterAuthorization: "Basic cluster-test-secret", + }); + expect(await client.put(bytes)).toBe(cid); + expect(Uint8Array.from(await client.get(cid))).toEqual(bytes); + const [firstUrl, firstInit] = fetch.mock.calls[0]!; + const url = new URL(String(firstUrl)); + expect(url.pathname).toBe("/api/v0/block/put"); + expect(url.searchParams.get("cid-codec")).toBe("raw"); + expect(url.searchParams.get("mhtype")).toBe("sha2-256"); + expect(url.searchParams.get("pin")).toBe("true"); + const file = (firstInit?.body as FormData).get("file") as Blob; + expect(new Uint8Array(await file.arrayBuffer())).toEqual(bytes); + const pinUrl = new URL(String(fetch.mock.calls[1]![0])); + expect(pinUrl.searchParams.get("replication-min")).toBe("2"); + expect(pinUrl.searchParams.get("replication-max")).toBe("2"); + for (const [input, init] of fetch.mock.calls) { + const request = new URL(String(input)); + expect(init?.redirect).toBe("error"); + expect(init?.signal).toBeInstanceOf(AbortSignal); + expect(init?.headers).toEqual({ Authorization: request.hostname === "kubo.invalid" + ? "Bearer kubo-test-secret" : "Basic cluster-test-secret" }); + expect(String(input)).not.toContain("secret"); + } + }); + + it("accepts array and NDJSON peer responses and explicit isolated min=1", async () => { + const { client, cid, peers } = await fixture({ minReplicas: 1, maxReplicas: 1 }, + (url) => url.pathname === "/peers" ? asJson([peers[0]]) : undefined); + expect(await client.put(bytes)).toBe(cid); + }); + + it.each(["pinning", "pin_error", "cluster_error", "remote", "unpinned"])( + "does not count %s as replicated", async (state) => { + const { client, status } = await fixture(); + status.peer_map["cluster-b"].status = state; + await expect(client.put(bytes)).rejects.toThrow("timed out"); + }, + ); + + it("rejects stale pinned status when the corresponding peer is offline", async () => { + const { client, peers } = await fixture(); + peers[1]!.error = "offline"; + await expect(client.put(bytes)).rejects.toThrow("timed out"); + }); + + it("rejects stale pinned state if Kubo is unavailable", async () => { + const { client, peers } = await fixture(); + peers[1]!.ipfs.error = "offline"; + await expect(client.put(bytes)).rejects.toThrow("timed out"); + }); + + it("does not double-count distinct Cluster peers using the same Kubo", async () => { + const { client, peers, status } = await fixture(); + peers[1]!.ipfs.id = "ipfs-a"; + status.peer_map["cluster-b"].ipfs_peer_id = "ipfs-a"; + await expect(client.put(bytes)).rejects.toThrow("timed out"); + }); + + it("does not count duplicated or identity-mismatched peers", async () => { + const { client, peers } = await fixture(); + peers.push(peers[1]!); + await expect(client.put(bytes)).rejects.toThrow("timed out"); + peers.pop(); + peers[1]!.ipfs.id = "changed"; + await expect(client.put(bytes)).rejects.toThrow("timed out"); + }); + + it.each([404, 500, 503, 302])("sanitizes HTTP %s and does not fall back", async (code) => { + const { client, cid, fetch } = await fixture({}, () => + new Response("secret-password synthetic-plaintext", { status: code })); + await expect(client.get(cid)).rejects.toThrow("Private distributed storage operation failed."); + expect(fetch).toHaveBeenCalledTimes(1); + }); + + it("sanitizes thrown fetch errors", async () => { + const { client, cid } = await fixture({}, () => { throw new Error("Bearer secret"); }); + const error = await client.get(cid).catch((reason: unknown) => reason); + expect(String(error)).not.toContain("secret"); + expect(error).not.toHaveProperty("cause"); + }); + + it("bounds both stalled fetch and body consumption", async () => { + const stalled = await fixture({ timeoutMs: 10 }, () => new Promise(() => undefined)); + await expect(stalled.client.get(stalled.cid)).rejects.toThrow("timed out"); + const cancelled = vi.fn(); + const body = await fixture({ timeoutMs: 10 }, () => + new Response(new ReadableStream({ cancel: cancelled }))); + await expect(body.client.get(body.cid)).rejects.toThrow("timed out"); + expect(cancelled).toHaveBeenCalled(); + }); + + it("rejects tampered and truncated bytes and oversized bodies", async () => { + for (const response of [ + new Response(Uint8Array.from([3, 2, 1])), + new Response(bytes.slice(0, 2)), + new Response(bytes, { headers: { "content-length": "4" } }), + new Response(bytes, { headers: { "content-length": "9999999" } }), + new Response(new Uint8Array(MAX_DISTRIBUTED_BLOB_BYTES + 1)), + ]) { + const { client, cid } = await fixture({}, () => response); + await expect(client.get(cid)).rejects.toThrow("failed"); + } + }); + + it("does not pin incorrect Kubo CID or size responses", async () => { + for (const result of [{ Key: "invalid", Size: 3 }, + { Key: await rawContentCid(Uint8Array.from([9])), Size: 3 }, + { Key: await rawContentCid(bytes), Size: 2 }]) { + const { client, fetch } = await fixture({}, () => asJson(result)); + await expect(client.put(bytes)).rejects.toThrow("failed"); + expect(fetch).toHaveBeenCalledTimes(1); + } + }); + + it("rejects malformed and mismatched Cluster status", async () => { + const { client } = await fixture({}, (url, init) => + url.pathname.startsWith("/pins/") && init?.method === "GET" + ? asJson({ cid: "invalid", peer_map: {} }) : undefined); + await expect(client.put(bytes)).rejects.toThrow("failed"); + }); + + it("fails closed on pin submission errors without polling or fallback", async () => { + const { client, fetch } = await fixture({}, (url, init) => + url.pathname.startsWith("/pins/") && init?.method === "POST" + ? new Response("secret pin error", { status: 500 }) : undefined); + await expect(client.put(bytes)).rejects.toThrow("failed"); + expect(fetch).toHaveBeenCalledTimes(2); + }); + + it("polls until actual pinning completes", async () => { + let polls = 0; + const { client, cid, status } = await fixture({}, (url, init) => { + if (url.pathname.startsWith("/pins/") && init?.method === "GET") { + status.peer_map["cluster-b"].status = ++polls < 2 ? "pinning" : "pinned"; + } + return undefined; + }); + expect(await client.put(bytes)).toBe(cid); + expect(polls).toBe(2); + }); + + it("refuses a redirected response even from an injected transport", async () => { + const response = new Response(bytes); + Object.defineProperty(response, "redirected", { value: true }); + const { client, cid } = await fixture({}, () => response); + await expect(client.get(cid)).rejects.toThrow("failed"); + }); + + it("rejects invalid CID before contacting a server", async () => { + const { client, fetch } = await fixture(); + for (const cid of ["", "../private", "QmYwAPJzv5CZsnAzt8auVZRnGi2C8AzSmNQnbqqArBpuGY"]) { + await expect(client.get(cid)).rejects.toThrow("identifier"); + expect(() => validateRawCid(cid)).toThrow(); + } + expect(fetch).not.toHaveBeenCalled(); + }); + + it("enforces write and configured read size limits", async () => { + const { client, cid, fetch } = await fixture({ maxBlobBytes: 2 }); + await expect(client.put(bytes)).rejects.toThrow("size"); + expect(fetch).not.toHaveBeenCalled(); + await expect(client.get(cid)).rejects.toThrow("failed"); + await expect(client.put(new Uint8Array())).rejects.toThrow("size"); + }); + + it.each([ + "http://kubo.invalid", "http://localhost", "https://user:secret@kubo.invalid", + "https://kubo.invalid?secret=x", "https://kubo.invalid#secret", + "https://kubo.invalid/path", "ftp://kubo.invalid", "not a URL", + ])("rejects unsafe endpoint %s", (kuboUrl) => { + expect(() => new IpfsClusterClient({ ...base, kuboUrl })).toThrow("endpoint"); + }); + + it("allows only explicitly enabled loopback HTTP", () => { + for (const host of ["localhost", "127.0.0.1", "[::1]"]) { + expect(() => new IpfsClusterClient({ ...base, kuboUrl: `http://${host}:5001`, + allowInsecureLocal: true })).not.toThrow(); + } + expect(() => new IpfsClusterClient({ ...base, kuboUrl: "http://10.0.0.1", + allowInsecureLocal: true })).toThrow("endpoint"); + }); + + it.each([ + { minReplicas: 0 }, { minReplicas: 3 }, { minReplicas: 2, maxReplicas: 1 }, + { timeoutMs: 0 }, { pollIntervalMs: 0 }, { maxBlobBytes: 1024 * 1024 }, + { kuboAuthorization: "secret\r\ninjected" }, { clusterAuthorization: "" }, + ])("rejects invalid configuration %j", (options) => { + expect(() => new IpfsClusterClient({ ...base, ...options })).toThrow(); + }); +}); diff --git a/test/s3-blob-store.spec.ts b/test/s3-blob-store.spec.ts new file mode 100644 index 0000000..9fa97ba --- /dev/null +++ b/test/s3-blob-store.spec.ts @@ -0,0 +1,236 @@ +import { Readable } from "node:stream"; +import { S3Client } from "@aws-sdk/client-s3"; +import { describe, expect, it } from "vitest"; +import { + ConcurrentUpdateError, + ObjectAlreadyExistsError, + ObjectNotFoundError, + S3BlobStore, +} from "../src/index.js"; + +const key = "s".repeat(43); +const ciphertext = { algorithm: "AES-256-GCM", ciphertext: "synthetic-ciphertext", nonce: "opaque" }; + +interface Request { + method: string; + path: string; + headers: Record; + body?: unknown; +} + +class S3Transport { + readonly requests: Request[] = []; + text: string | undefined; + etag = '"revision-1"'; + omitEtag = false; + omitLength = false; + error: { status: number; code: string } | undefined; + networkError: Error | undefined; + writeError: { status: number; code: string } | undefined; + loseWriteResponse = false; + #revision = 1; + + async handle(request: Request) { + this.requests.push(request); + if (this.networkError) throw this.networkError; + const failure = (statusCode: number, code: string) => ({ + response: { + statusCode, + headers: { "content-type": "application/xml" }, + body: Readable.from([`${code}Storage request failed.`]), + }, + }); + if (this.error) return failure(this.error.status, this.error.code); + if (request.method === "GET") { + if (this.text === undefined) return failure(404, "NoSuchKey"); + return { + response: { + statusCode: 200, + headers: { + "content-type": "application/json", + ...(this.omitEtag ? {} : { etag: this.etag }), + ...(this.omitLength ? {} : { "content-length": `${Buffer.byteLength(this.text)}` }), + }, + body: Readable.from([this.text]), + }, + }; + } + if (request.method !== "PUT") throw new Error("Unexpected storage operation."); + if (this.writeError) return failure(this.writeError.status, this.writeError.code); + if (request.headers["if-none-match"] === "*" && this.text !== undefined) { + return failure(412, "PreconditionFailed"); + } + if (request.headers["if-match"] && + (request.headers["if-match"] !== this.etag || this.text === undefined)) { + return failure(412, "PreconditionFailed"); + } + if (typeof request.body !== "string") throw new Error("Unexpected request body."); + this.text = request.body; + this.etag = `"revision-${++this.#revision}"`; + if (this.loseWriteResponse) throw new Error("Connection reset."); + return { response: { statusCode: 200, headers: { etag: this.etag }, body: Readable.from([]) } }; + } +} + +function fixture(transport = new S3Transport(), options: { prefix?: string; kmsKeyId?: string } = {}) { + const client = new S3Client({ + region: "us-east-1", + credentials: { accessKeyId: "synthetic-access-key", secretAccessKey: "synthetic-secret-key" }, + maxAttempts: 1, + requestHandler: transport, + requestChecksumCalculation: "WHEN_REQUIRED", + responseChecksumValidation: "WHEN_REQUIRED", + }); + return { transport, client, store: new S3BlobStore(client, "synthetic-bucket", options) }; +} + +describe("S3BlobStore", () => { + it("creates immutable ciphertext and uses the GET ETag for conditional updates", async () => { + const { transport, store } = fixture(); + await store.create(key, ciphertext); + expect(transport.requests[0]?.headers["if-none-match"]).toBe("*"); + expect(JSON.parse(transport.text ?? "")).toEqual({ version: 1, value: ciphertext }); + await expect(store.create(key, { ciphertext: "replacement" })).rejects.toBeInstanceOf(ObjectAlreadyExistsError); + expect(await store.read(key)).toEqual({ version: 1, value: ciphertext }); + const etag = transport.etag; + await expect(store.compareAndSwap(key, 1, { ciphertext: "next" })).resolves.toBe(2); + expect(transport.requests.at(-1)?.headers["if-match"]).toBe(etag); + expect(await store.read(key)).toEqual({ version: 2, value: { ciphertext: "next" } }); + const puts = transport.requests.filter((request) => request.method === "PUT").length; + await expect(store.compareAndSwap(key, 1, null)).rejects.toBeInstanceOf(ConcurrentUpdateError); + expect(transport.requests.filter((request) => request.method === "PUT")).toHaveLength(puts); + }); + + it("has exactly one winner when independent clients race on a version", async () => { + const { transport, store } = fixture(); + const second = fixture(transport).store; + await store.create(key, ciphertext); + const results = await Promise.allSettled([ + store.compareAndSwap(key, 1, { ciphertext: "first" }), + second.compareAndSwap(key, 1, { ciphertext: "second" }), + ]); + expect(results.filter((result) => result.status === "fulfilled")).toHaveLength(1); + expect(results.find((result) => result.status === "rejected")).toMatchObject({ + reason: expect.any(ConcurrentUpdateError), + }); + expect(transport.requests.filter((request) => request.headers["if-match"])).toHaveLength(2); + expect((await store.read(key))?.version).toBe(2); + }); + + it("distinguishes missing objects from bucket, authorization, and network errors", async () => { + const { transport, store } = fixture(); + await expect(store.read(key)).resolves.toBeUndefined(); + await expect(store.compareAndSwap(key, 1, null)).rejects.toBeInstanceOf(ObjectNotFoundError); + for (const code of ["NoSuchBucket", "AccessDenied", "NotFound"]) { + transport.error = { status: code === "AccessDenied" ? 403 : 404, code }; + await expect(store.read(key)).rejects.toMatchObject({ name: code }); + await expect(store.create(key, null)).rejects.toMatchObject({ name: code }); + } + transport.error = undefined; + transport.networkError = new Error("Synthetic network failure."); + await expect(store.read(key)).rejects.toBe(transport.networkError); + }); + + it("does not report a 409 conditional race as an immutable-object duplicate", async () => { + const { transport, store } = fixture(); + transport.writeError = { status: 409, code: "ConditionalRequestConflict" }; + await expect(store.create(key, ciphertext)).rejects.toBeInstanceOf(ConcurrentUpdateError); + transport.writeError = undefined; + await store.create(key, ciphertext); + for (const error of [ + { status: 409, code: "ConditionalRequestConflict" }, + { status: 412, code: "PreconditionFailed" }, + ]) { + transport.writeError = error; + await expect(store.compareAndSwap(key, 1, null)).rejects.toBeInstanceOf(ConcurrentUpdateError); + } + transport.writeError = { status: 403, code: "AccessDenied" }; + await expect(store.compareAndSwap(key, 1, null)).rejects.toMatchObject({ name: "AccessDenied" }); + }); + + it("propagates an ambiguous write outcome without a retry or read-back", async () => { + const { transport, store } = fixture(); + transport.loseWriteResponse = true; + await expect(store.create(key, ciphertext)).rejects.toThrow("Connection reset."); + expect(transport.requests).toHaveLength(1); + expect(transport.text).toBeDefined(); + }); + + it("rejects rather than reconciling an ambiguous committed CAS", async () => { + const { transport, store } = fixture(); + await store.create(key, ciphertext); + transport.loseWriteResponse = true; + await expect(store.compareAndSwap(key, 1, { ciphertext: "next" })).rejects.toThrow("Connection reset."); + expect(transport.requests).toHaveLength(3); + expect(JSON.parse(transport.text ?? "").version).toBe(2); + }); + + it("applies a bounded prefix and explicit SSE-KMS options to every write", async () => { + const { transport, store } = fixture(new S3Transport(), { prefix: "opaque/objects", kmsKeyId: "alias/synthetic-key" }); + await store.create(key, ciphertext); + await store.compareAndSwap(key, 1, { ciphertext: "next" }); + for (const request of transport.requests) { + expect(request.path).toBe(`/opaque/objects/${key}`); + if (request.method === "PUT") { + expect(request.headers["x-amz-server-side-encryption"]).toBe("aws:kms"); + expect(request.headers["x-amz-server-side-encryption-aws-kms-key-id"]).toBe("alias/synthetic-key"); + } + } + }); + + it("accepts the integration prefix without adding a second trailing slash", async () => { + const { transport, store } = fixture(new S3Transport(), { prefix: "synthetic-integration/" }); + await store.create(key, ciphertext); + expect(await store.read(key)).toEqual({ version: 1, value: ciphertext }); + expect(transport.requests.every((request) => request.path === `/synthetic-integration/${key}`)).toBe(true); + }); + + it.each([ + "not-json", "null", "[]", '{"version":1}', '{"value":null}', + '{"version":0,"value":null}', '{"version":1.5,"value":null}', + '{"version":"1","value":null}', '{"version":9007199254740992,"value":null}', + ])("rejects malformed serialized wrappers: %s", async (text) => { + const { transport, store } = fixture(); + transport.text = text; + await expect(store.read(key)).rejects.toThrow("Invalid stored object."); + await expect(store.compareAndSwap(key, 1, null)).rejects.toThrow(); + expect(transport.requests.every((request) => request.method === "GET")).toBe(true); + }); + + it("fails closed on absent ETags and oversized bodies even without content length", async () => { + const { transport, store } = fixture(); + transport.text = '{"version":1,"value":null}'; + transport.omitEtag = true; + await expect(store.read(key)).rejects.toThrow("ETag"); + await expect(store.compareAndSwap(key, 1, null)).rejects.toThrow("ETag"); + transport.omitEtag = false; + transport.text = "x".repeat(4 * 1024 * 1024 + 1); + await expect(store.read(key)).rejects.toThrow("size"); + transport.omitLength = true; + await expect(store.read(key)).rejects.toThrow("limits"); + }); + + it("validates keys, versions, prefixes and JSON before requests", async () => { + const { transport, client, store } = fixture(); + for (const invalidKey of ["short", "../" + key, key + "/", "x".repeat(129)]) { + await expect(store.read(invalidKey)).rejects.toThrow("key"); + await expect(store.create(invalidKey, null)).rejects.toThrow("key"); + await expect(store.compareAndSwap(invalidKey, 1, null)).rejects.toThrow("key"); + } + for (const version of [0, -1, 1.5, NaN, Infinity, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER + 1]) { + await expect(store.compareAndSwap(key, version, null)).rejects.toThrow("version"); + } + for (const prefix of ["../", "a/../b", "/root", "a//b", "a\\b", "a%2fb", "x".repeat(513)]) { + expect(() => new S3BlobStore(client, "synthetic-bucket", { prefix })).toThrow("prefix"); + } + const circular: { self?: object } = {}; + circular.self = circular; + for (const value of [undefined, { missing: undefined }, [undefined], [,], NaN, Infinity, + 1n, () => null, Symbol("synthetic"), new Date(0), new Map(), circular, + new Proxy({}, {}), "x".repeat(4 * 1024 * 1024 + 1)]) { + await expect(store.create(key, value)).rejects.toThrow("Invalid or oversized storage object."); + await expect(store.compareAndSwap(key, 1, value)).rejects.toThrow("Invalid or oversized storage object."); + } + expect(transport.requests).toHaveLength(0); + }); +}); diff --git a/test/web-server.spec.ts b/test/web-server.spec.ts index dc35c60..35c12db 100644 --- a/test/web-server.spec.ts +++ b/test/web-server.spec.ts @@ -1,18 +1,20 @@ import type { AddressInfo } from "node:net"; import { resolve } from "node:path"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { InMemoryBlobStore } from "../src/index.js"; +import { ConcurrentUpdateError, InMemoryBlobStore } from "../src/index.js"; import { createHealthRecordServer } from "../src/web/server.js"; describe("local web server", () => { let server: ReturnType; let origin: string; + let store: InMemoryBlobStore; beforeEach(async () => { + store = new InMemoryBlobStore(); server = createHealthRecordServer({ - store: new InMemoryBlobStore(), + store, webRoot: resolve("web"), }); await new Promise((resolveListen) => { @@ -23,6 +25,7 @@ describe("local web server", () => { }); afterEach(async () => { + vi.restoreAllMocks(); await new Promise((resolveClose, reject) => { server.close((error) => { if (error) { @@ -135,6 +138,35 @@ describe("local web server", () => { }); }); + it("does not disclose provider diagnostics to logs or HTTP clients", async () => { + const log = vi.spyOn(console, "error").mockImplementation(() => {}); + vi.spyOn(store, "read").mockRejectedValue(new Error("SYNTHETIC-DIAGNOSTIC-MARKER")); + const clinician = await (await post("/api/clinicians", {})).json(); + const response = await post("/api/enroll", { + clinician, biometricToken: "synthetic-provider-failure-0123456789abcdef", + }); + expect(response.status).toBe(500); + expect(await response.json()).toEqual({ error: "Local POC request failed." }); + expect(log).toHaveBeenCalledWith("Local POC request failed; diagnostic details withheld."); + expect(JSON.stringify(log.mock.calls)).not.toContain("SYNTHETIC-DIAGNOSTIC-MARKER"); + }); + + it("surfaces CAS conflicts without exposing opaque storage keys", async () => { + const clinician = await (await post("/api/clinicians", {})).json(); + const access = { clinician, biometricToken: "synthetic-cas-conflict-0123456789abcdef" }; + expect((await post("/api/enroll", access)).status).toBe(201); + vi.spyOn(store, "compareAndSwap") + .mockRejectedValue(new ConcurrentUpdateError("opaque-key-must-not-be-disclosed")); + const response = await post("/api/records", { + ...access, + resource: { resourceType: "Condition", id: "synthetic-conflict", code: { text: "Synthetic" } }, + }); + expect(response.status).toBe(409); + expect(await response.json()).toEqual({ + error: "Record changed concurrently. Unlock and try again.", + }); + }); + async function post(path: string, value: unknown): Promise { return fetch(`${origin}${path}`, { method: "POST", diff --git a/tsconfig.json b/tsconfig.json index ad5dc0d..10cffde 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,5 +15,5 @@ "skipLibCheck": true, "types": ["node", "vitest/globals"] }, - "include": ["src/**/*.ts", "test/**/*.ts", "examples/**/*.ts"] + "include": ["src/**/*.ts", "test/**/*.ts", "examples/**/*.ts", "scripts/**/*.ts"] } diff --git a/vitest.config.ts b/vitest.config.ts index 8853ee8..b843df3 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,7 +1,8 @@ -import { defineConfig } from "vitest/config"; +import { configDefaults, defineConfig } from "vitest/config"; export default defineConfig({ test: { + exclude: [...configDefaults.exclude, "test/integration/**"], coverage: { provider: "v8", reporter: ["text", "html"], diff --git a/vitest.integration.config.ts b/vitest.integration.config.ts new file mode 100644 index 0000000..d5fb64e --- /dev/null +++ b/vitest.integration.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["test/integration/**/*.spec.ts"], + environment: "node", + fileParallelism: false, + testTimeout: 120_000, + hookTimeout: 120_000, + reporters: ["default"], + }, +}); From 8b73738ce806b6185a37bef651acfa5754520685 Mon Sep 17 00:00:00 2001 From: Karthik Chandrasekaran Date: Sat, 19 Sep 2026 08:33:32 -0500 Subject: [PATCH 2/6] Add credential-safe private fixture startup diagnostics Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fc4430e2-35f5-48b8-bd0f-5dc6fad2a0ff --- scripts/run-ipfs-integration.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/run-ipfs-integration.ts b/scripts/run-ipfs-integration.ts index 03df922..b4da452 100644 --- a/scripts/run-ipfs-integration.ts +++ b/scripts/run-ipfs-integration.ts @@ -75,15 +75,26 @@ async function node(kind: "kubo" | "cluster", index: number, bootstrap?: string) if (interrupted) throw new Error("Fixture interrupted."); const name = `${prefix}-${kind}${index}`; const dataVolume = `${name}-data`; + stage = `creating ${kind} ${index} data volume`; await volume(dataVolume); containers.push(name); + stage = `creating ${kind} ${index} container`; await docker(containerArguments({ name, network, alias: `${kind}${index}`, dataVolume, secretsVolume, scriptDirectory: resolve("test/fixtures/ipfs"), kind, ...(bootstrap ? { bootstrap } : {}), })); + stage = `starting ${kind} ${index} container`; await docker(["start", name]); - const endpoint = loopbackEndpoint(await docker(["port", name, kind === "kubo" ? "5001/tcp" : "9094/tcp"])); + stage = `resolving ${kind} ${index} loopback API binding`; + const binding = await docker(["port", name, kind === "kubo" ? "5001/tcp" : "9094/tcp"]); + if (!binding) { + const state = await docker(["inspect", "--format", "{{.State.Status}} {{.State.ExitCode}}", name]); + if (/^[a-z]+ \d+$/.test(state)) console.error(`Fixture ${kind} ${index} container state: ${state}; no published API binding.`); + throw new Error("Fixture API binding unavailable."); + } + const endpoint = loopbackEndpoint(binding); + stage = `waiting for ${kind} ${index} API readiness`; await waitUntil(async () => { const info = await (await request(`${endpoint}${kind === "kubo" ? "/api/v0/id" : "/id"}`, kind === "kubo" ? { method: "POST" } : {})).json() as { ID?: string; id?: string }; From 40629e9e23ab55ced84381e4035204e57c1cb345 Mon Sep 17 00:00:00 2001 From: Karthik Chandrasekaran Date: Sat, 19 Sep 2026 09:02:48 -0500 Subject: [PATCH 3/6] Fix isolated IPFS fixture access on Linux Docker Keep the swarm on an internal bridge and expose fixture APIs through loopback-only relays. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fc4430e2-35f5-48b8-bd0f-5dc6fad2a0ff --- scripts/run-ipfs-integration.ts | 46 ++++++++++++---------- test/fixtures/ipfs/README.md | 18 +++++++-- test/fixtures/ipfs/api-relay.spec.ts | 56 +++++++++++++++++++++++++++ test/fixtures/ipfs/api-relay.ts | 57 ++++++++++++++++++++++++++++ test/fixtures/ipfs/fixture.spec.ts | 24 +++++++----- test/fixtures/ipfs/fixture.ts | 19 ++++++---- 6 files changed, 180 insertions(+), 40 deletions(-) create mode 100644 test/fixtures/ipfs/api-relay.spec.ts create mode 100644 test/fixtures/ipfs/api-relay.ts diff --git a/scripts/run-ipfs-integration.ts b/scripts/run-ipfs-integration.ts index b4da452..5b3d22d 100644 --- a/scripts/run-ipfs-integration.ts +++ b/scripts/run-ipfs-integration.ts @@ -5,8 +5,9 @@ import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { setTimeout as delay } from "node:timers/promises"; import { - CLUSTER_IMAGE, KUBO_IMAGE, SECRET_INIT_SCRIPT, containerArguments, loopbackEndpoint, secretInput, + CLUSTER_IMAGE, KUBO_IMAGE, SECRET_INIT_SCRIPT, containerArguments, isolatedIntegrationEnvironment, secretInput, } from "../test/fixtures/ipfs/fixture.js"; +import { createApiRelay, isPrivateIPv4, type ApiRelay } from "../test/fixtures/ipfs/api-relay.js"; const prefix = `gphr-ipfs-${randomBytes(10).toString("hex")}`; const network = `${prefix}-network`; @@ -14,6 +15,7 @@ const secretsVolume = `${prefix}-secrets`; let metadataDirectory: string | undefined; const containers: string[] = []; const volumes: string[] = []; +const relays: ApiRelay[] = []; let networkCreated = false; let interrupted = false; let tests: ChildProcess | undefined; @@ -86,20 +88,26 @@ async function node(kind: "kubo" | "cluster", index: number, bootstrap?: string) })); stage = `starting ${kind} ${index} container`; await docker(["start", name]); - stage = `resolving ${kind} ${index} loopback API binding`; - const binding = await docker(["port", name, kind === "kubo" ? "5001/tcp" : "9094/tcp"]); - if (!binding) { + stage = `connecting ${kind} ${index} private API through loopback relay`; + const address = await docker([ + "inspect", "--format", `{{(index .NetworkSettings.Networks "${network}").IPAddress}}`, name, + ]); + if (!isPrivateIPv4(address)) throw new Error("Fixture container lacks a private IPv4 address."); + const relay = await createApiRelay(address, kind === "kubo" ? 5001 : 9094); + relays.push(relay); + const endpoint = relay.endpoint; + stage = `waiting for ${kind} ${index} API readiness`; + try { + await waitUntil(async () => { + const info = await (await request(`${endpoint}${kind === "kubo" ? "/api/v0/id" : "/id"}`, + kind === "kubo" ? { method: "POST" } : {})).json() as { ID?: string; id?: string }; + return typeof (kind === "kubo" ? info.ID : info.id) === "string"; + }); + } catch { const state = await docker(["inspect", "--format", "{{.State.Status}} {{.State.ExitCode}}", name]); - if (/^[a-z]+ \d+$/.test(state)) console.error(`Fixture ${kind} ${index} container state: ${state}; no published API binding.`); - throw new Error("Fixture API binding unavailable."); + if (/^[a-z]+ \d+$/.test(state)) console.error(`Fixture ${kind} ${index} container state: ${state}.`); + throw new Error("Fixture API readiness failed."); } - const endpoint = loopbackEndpoint(binding); - stage = `waiting for ${kind} ${index} API readiness`; - await waitUntil(async () => { - const info = await (await request(`${endpoint}${kind === "kubo" ? "/api/v0/id" : "/id"}`, - kind === "kubo" ? { method: "POST" } : {})).json() as { ID?: string; id?: string }; - return typeof (kind === "kubo" ? info.ID : info.id) === "string"; - }); return endpoint; } @@ -109,6 +117,9 @@ async function cleanup(): Promise { await new Promise((done) => tests!.once("close", () => done())); } let failed = false; + for (const relay of relays) { + try { await relay.close(); } catch { failed = true; } + } for (const name of [...containers].reverse()) { // A failed create may leave nothing to remove; only absence is safe to ignore. try { @@ -128,12 +139,7 @@ async function cleanup(): Promise { async function runSuite(kubo: string, cluster: string): Promise { metadataDirectory = await mkdtemp(join(tmpdir(), `${prefix}-index-`)); - const environment = { ...process.env }; - for (const setting of [ - "IPFS_API_AUTH", "IPFS_API_AUTH_FILE", "IPFS_API_AUTH_KEYVAULT_SECRET", - "IPFS_CLUSTER_AUTH", "IPFS_CLUSTER_AUTH_FILE", "IPFS_CLUSTER_AUTH_KEYVAULT_SECRET", - "AZURE_KEY_VAULT_URL", "AZURE_LOG_LEVEL", - ]) delete environment[setting]; + const environment = isolatedIntegrationEnvironment(process.env); await new Promise((done, reject) => { tests = spawn(process.execPath, [ resolve("node_modules/vitest/vitest.mjs"), "run", "--config", "vitest.integration.config.ts", @@ -189,6 +195,8 @@ async function main(): Promise { process.once("SIGTERM", onSignal); try { await docker(["info", "--format", "{{.ServerVersion}}"], undefined, 15_000); + stage = "checking native Linux Docker host"; + if (process.platform !== "linux") throw new Error("Fixture requires a native Linux Docker host."); stage = "pulling pinned official images"; await docker(["pull", KUBO_IMAGE], undefined, 300_000); await docker(["pull", CLUSTER_IMAGE], undefined, 300_000); diff --git a/test/fixtures/ipfs/README.md b/test/fixtures/ipfs/README.md index f611f84..bbdb49a 100644 --- a/test/fixtures/ipfs/README.md +++ b/test/fixtures/ipfs/README.md @@ -1,7 +1,9 @@ # Private IPFS integration fixture Run `npm run test:integration:ipfs` from the project root with Node.js 22+ -and a running Docker engine capable of Linux containers. No hosted account, +and a running native Linux Docker engine on the same host (as on GitHub's +Ubuntu runners). Docker Desktop and remote Docker daemons are not supported: +the runner must be able to reach its internal bridge directly. No hosted account, API token, public pinning service, or additional npm dependency is required. Default `npm test` only runs the Docker-free fixture configuration tests. @@ -14,8 +16,11 @@ Image pulls require internet access; the running fixture does not. - Three Kubo nodes and three Raft Cluster nodes share one unique Docker **internal** bridge network. No host swarm, gateway, proxy, metrics, or - cluster peer ports are published. API ports use dynamically assigned - `127.0.0.1` bindings only. These unauthenticated administrative APIs are + cluster peer ports are published. Docker does not install published port + mappings for containers attached only to an internal network. Instead, + runner-owned TCP relays bind dynamically assigned `127.0.0.1` ports and + forward only to each container's API over the private bridge. No + internet-connected network or host networking is added. These unauthenticated administrative APIs are for an isolated development/CI machine, not a shared untrusted host. - Kubo requires `IPFS_FORCE_PNET=1`, an independently generated swarm key, no public bootstrap peers, no DHT/routing/providing, no delegated routing @@ -37,7 +42,7 @@ Image pulls require internet access; the running fixture does not. capability is granted. - The Docker log driver is disabled. The runner never dumps daemon logs, configuration, identities, upstream errors, or secrets. -- Only this invocation's named containers, attached anonymous volumes, +- Only this invocation's TCP relays/connections, named containers, attached anonymous volumes, named volumes, network, and unique local metadata directory are removed. No global prune/kill operation is used. Cleanup runs on normal failure and catchable termination signals. Forced machine/process termination can @@ -77,6 +82,11 @@ metadata index remains a single point of failure. ## Official references +- Docker internal networks permit direct host-to-container communication: + `https://docs.docker.com/reference/cli/docker/network/create/#network-internal-mode---internal` +- Docker's internal-only port-publishing limitation: + `https://github.com/moby/moby/issues/36174` + - Kubo Docker and swarm-key documentation: `https://docs.ipfs.tech/install/run-ipfs-inside-docker/` - Tagged Kubo configuration: diff --git a/test/fixtures/ipfs/api-relay.spec.ts b/test/fixtures/ipfs/api-relay.spec.ts new file mode 100644 index 0000000..5972aa3 --- /dev/null +++ b/test/fixtures/ipfs/api-relay.spec.ts @@ -0,0 +1,56 @@ +import { once } from "node:events"; +import { createServer } from "node:http"; +import { createConnection } from "node:net"; +import { describe, expect, it } from "vitest"; +import { createApiRelay, isPrivateIPv4 } from "./api-relay.js"; + +describe("private fixture API relay (no Docker required)", () => { + it("restricts container addresses to private IPv4", () => { + for (const address of ["10.0.0.1", "172.16.0.2", "172.31.255.254", "192.168.1.1"]) { + expect(isPrivateIPv4(address)).toBe(true); + } + for (const address of ["172.15.0.1", "172.32.0.1", "8.8.8.8", "0.0.0.0", "::1", "example.test"]) { + expect(isPrivateIPv4(address)).toBe(false); + } + }); + + it("rejects public relay destinations and invalid ports", async () => { + for (const address of ["8.8.8.8", "0.0.0.0", "::1", "example.test"]) { + await expect(createApiRelay(address, 5001)).rejects.toThrow("private endpoint"); + } + for (const port of [0, 65_536, 1.5, NaN]) { + await expect(createApiRelay("127.0.0.1", port)).rejects.toThrow("private endpoint"); + } + }); + + it("relays complete binary HTTP bodies using only a loopback listener", async () => { + const upstream = createServer((request, response) => { + response.setHeader("content-type", "application/octet-stream"); + request.pipe(response); + }); + upstream.listen(0, "127.0.0.1"); + await once(upstream, "listening"); + const address = upstream.address(); + if (!address || typeof address === "string") throw new Error("Test server failed."); + const relay = await createApiRelay("127.0.0.1", address.port); + try { + expect(new URL(relay.endpoint).hostname).toBe("127.0.0.1"); + const bytes = new Uint8Array(262_144).map((_, index) => index % 256); + const response = await fetch(`${relay.endpoint}/api/v0/block/put`, { + method: "POST", body: bytes, signal: AbortSignal.timeout(5_000), + }); + expect(Buffer.from(await response.arrayBuffer()).equals(Buffer.from(bytes))).toBe(true); + const client = createConnection({ host: "127.0.0.1", port: Number(new URL(relay.endpoint).port) }); + await once(client, "connect"); + const disconnected = once(client, "close"); + await relay.close(); + await disconnected; + await relay.close(); + await expect(fetch(relay.endpoint, { signal: AbortSignal.timeout(2_000) })).rejects.toThrow(); + } finally { + await relay.close(); + upstream.closeAllConnections(); + await new Promise((done) => upstream.close(() => done())); + } + }); +}); diff --git a/test/fixtures/ipfs/api-relay.ts b/test/fixtures/ipfs/api-relay.ts new file mode 100644 index 0000000..149c87a --- /dev/null +++ b/test/fixtures/ipfs/api-relay.ts @@ -0,0 +1,57 @@ +import { once } from "node:events"; +import { createConnection, createServer, isIPv4, type Socket } from "node:net"; + +export interface ApiRelay { + endpoint: string; + close(): Promise; +} + +export function isPrivateIPv4(address: string): boolean { + if (!isIPv4(address)) return false; + const [first, second] = address.split(".").map(Number); + return first === 10 || (first === 172 && second! >= 16 && second! <= 31) + || (first === 192 && second === 168); +} + +// Docker does not publish ports on an internal-only network. Its native Linux +// host can reach the bridge directly, without giving containers an egress route. +export async function createApiRelay(address: string, port: number): Promise { + if ((!isPrivateIPv4(address) && address !== "127.0.0.1") + || !Number.isInteger(port) || port < 1 || port > 65_535) { + throw new Error("Fixture relay requires a private endpoint."); + } + const sockets = new Set(); + const server = createServer((client) => { + const upstream = createConnection({ host: address, port }); + for (const socket of [client, upstream]) { + sockets.add(socket); + socket.once("close", () => sockets.delete(socket)); + socket.on("error", () => { + client.destroy(); + upstream.destroy(); + }); + socket.setTimeout(30_000, () => socket.destroy()); + } + client.once("close", () => upstream.destroy()); + client.pipe(upstream); + upstream.pipe(client); + }); + server.listen(0, "127.0.0.1"); + await once(server, "listening"); + const binding = server.address(); + if (!binding || typeof binding === "string" || binding.address !== "127.0.0.1") { + server.close(); + throw new Error("Fixture relay did not bind loopback."); + } + let closing: Promise | undefined; + return { + endpoint: `http://127.0.0.1:${binding.port}`, + close() { + closing ??= new Promise((done, reject) => { + server.close((error) => error ? reject(new Error("Fixture relay cleanup failed.")) : done()); + for (const socket of sockets) socket.destroy(); + }); + return closing; + }, + }; +} diff --git a/test/fixtures/ipfs/fixture.spec.ts b/test/fixtures/ipfs/fixture.spec.ts index d8a8421..5df9346 100644 --- a/test/fixtures/ipfs/fixture.spec.ts +++ b/test/fixtures/ipfs/fixture.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { readFile } from "node:fs/promises"; -import { CLUSTER_IMAGE, KUBO_IMAGE, containerArguments, loopbackEndpoint, secretInput } from "./fixture.js"; +import { CLUSTER_IMAGE, KUBO_IMAGE, containerArguments, isolatedIntegrationEnvironment, secretInput } from "./fixture.js"; describe("private IPFS Docker fixture configuration (no Docker required)", () => { const options = { @@ -17,10 +17,9 @@ describe("private IPFS Docker fixture configuration (no Docker required)", () => `type=volume,source=unique-data,target=${kind === "kubo" ? "/data/ipfs" : "/data/ipfs-cluster"},volume-nocopy`, ); expect(args).toContain("type=volume,source=unique-secrets,target=/run/fixture-secrets,readonly"); - expect(args.filter((arg) => arg.startsWith("127.0.0.1::"))).toEqual([ - `127.0.0.1::${kind === "kubo" ? "5001" : "9094"}`, - ]); + expect(args).not.toContain("--publish"); expect(args).not.toContain("--publish-all"); + expect(args).toContain("unique-private"); expect(args.join(" ")).not.toContain("CLUSTER_SECRET="); expect(args).toContain(kind === "kubo" ? KUBO_IMAGE : CLUSTER_IMAGE); expect(args).not.toContain("latest"); @@ -44,11 +43,18 @@ describe("private IPFS Docker fixture configuration (no Docker required)", () => expect(first === secretInput()).toBe(false); }); - it("accepts only one explicit loopback mapping", () => { - expect(loopbackEndpoint("127.0.0.1:49152\n")).toBe("http://127.0.0.1:49152"); - for (const value of ["0.0.0.0:5001", "[::]:5001", "127.0.0.1:0", "127.0.0.1:65536", "127.0.0.1:5001\n0.0.0.0:5001"]) { - expect(() => loopbackEndpoint(value)).toThrow(); - } + it("does not inherit production auth providers or a cloud backup backend", () => { + const excluded = [ + "IPFS_API_AUTH", "IPFS_API_AUTH_FILE", "IPFS_API_AUTH_KEYVAULT_SECRET", + "IPFS_CLUSTER_AUTH", "IPFS_CLUSTER_AUTH_FILE", "IPFS_CLUSTER_AUTH_KEYVAULT_SECRET", + "IPFS_API_AUTH_AWS_SECRET_ID", "IPFS_CLUSTER_AUTH_AWS_SECRET_ID", "IPFS_BACKUP_BACKEND", + "AZURE_KEY_VAULT_URL", "AZURE_LOG_LEVEL", + ]; + const inherited = Object.fromEntries(excluded.map((key) => [key, "synthetic-test-setting"])); + inherited.PATH = "synthetic-path"; + const environment = isolatedIntegrationEnvironment(inherited); + expect(environment).toEqual({ PATH: "synthetic-path" }); + for (const key of excluded) expect(inherited[key]).toBe("synthetic-test-setting"); }); it("disables public discovery before starting the private Kubo daemon", async () => { diff --git a/test/fixtures/ipfs/fixture.ts b/test/fixtures/ipfs/fixture.ts index e1ef8ba..40bf763 100644 --- a/test/fixtures/ipfs/fixture.ts +++ b/test/fixtures/ipfs/fixture.ts @@ -3,6 +3,17 @@ import { randomBytes } from "node:crypto"; export const KUBO_IMAGE = "ipfs/kubo:v0.43.1"; export const CLUSTER_IMAGE = "ipfs/ipfs-cluster:v1.1.6"; +export function isolatedIntegrationEnvironment(inherited: NodeJS.ProcessEnv): NodeJS.ProcessEnv { + const environment = { ...inherited }; + for (const setting of [ + "IPFS_API_AUTH", "IPFS_API_AUTH_FILE", "IPFS_API_AUTH_KEYVAULT_SECRET", + "IPFS_CLUSTER_AUTH", "IPFS_CLUSTER_AUTH_FILE", "IPFS_CLUSTER_AUTH_KEYVAULT_SECRET", + "IPFS_API_AUTH_AWS_SECRET_ID", "IPFS_CLUSTER_AUTH_AWS_SECRET_ID", "IPFS_BACKUP_BACKEND", + "AZURE_KEY_VAULT_URL", "AZURE_LOG_LEVEL", + ]) delete environment[setting]; + return environment; +} + // Secret values travel only on stdin, never Docker argv, labels, or its saved environment. export function secretInput(): string { return `${randomBytes(32).toString("hex")}\n${randomBytes(32).toString("hex")}\n`; @@ -37,7 +48,6 @@ export function containerArguments(options: { "--mount", `type=volume,source=${options.dataVolume},target=${kubo ? "/data/ipfs" : "/data/ipfs-cluster"},volume-nocopy`, "--mount", `type=volume,source=${options.secretsVolume},target=/run/fixture-secrets,readonly`, "--mount", `type=bind,source=${options.scriptDirectory},target=/fixture,readonly`, - "--publish", `127.0.0.1::${kubo ? "5001" : "9094"}`, ...(kubo ? ["--env", "IPFS_FORCE_PNET=1"] : [ @@ -52,10 +62,3 @@ export function containerArguments(options: { ...(options.bootstrap ? ["--bootstrap", options.bootstrap] : []), ]; } - -export function loopbackEndpoint(portOutput: string): string { - const match = /^127\.0\.0\.1:(\d+)\s*$/.exec(portOutput); - const port = Number(match?.[1]); - if (!match || port < 1 || port > 65_535) throw new Error("Non-loopback Docker API binding."); - return `http://127.0.0.1:${port}`; -} From e9f26d4d1b3af1be06c2087c01bc383284a1166b Mon Sep 17 00:00:00 2001 From: Karthik Chandrasekaran Date: Sat, 19 Sep 2026 09:08:45 -0500 Subject: [PATCH 4/6] Separate Cluster membership and allocation readiness diagnostics Report only aggregate healthy-peer and freespace counts, never daemon logs or identities. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fc4430e2-35f5-48b8-bd0f-5dc6fad2a0ff --- scripts/run-ipfs-integration.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/scripts/run-ipfs-integration.ts b/scripts/run-ipfs-integration.ts index 5b3d22d..73296be 100644 --- a/scripts/run-ipfs-integration.ts +++ b/scripts/run-ipfs-integration.ts @@ -232,14 +232,27 @@ async function main(): Promise { const bootstrap = `/dns4/cluster1/tcp/9096/p2p/${identity.id}`; await node("cluster", 2, bootstrap); await node("cluster", 3, bootstrap); + stage = "waiting for three healthy Cluster members"; + let membershipSummary = ""; await waitUntil(async () => { const text = await (await request(`${cluster}/peers`)).text(); - const peers = text.trim().split("\n").map((line) => JSON.parse(line) as { id?: string; error?: string }); - return peers.length === 3 && peers.every((peer) => peer.id && !peer.error); + const peers = text.trim().split("\n").map((line) => + JSON.parse(line) as { id?: string; error?: string; ipfs?: { id?: string; error?: string } }); + const healthy = peers.filter((peer) => peer.id && !peer.error && peer.ipfs?.id && !peer.ipfs.error).length; + const summary = `Cluster members observed: ${peers.length}; healthy Kubo connections: ${healthy}.`; + if (summary !== membershipSummary) console.log(summary); + membershipSummary = summary; + return peers.length === 3 && healthy === 3; }); + stage = "waiting for three valid Cluster freespace metrics"; + let metricSummary = ""; await waitUntil(async () => { const metrics = await (await request(`${cluster}/monitor/metrics/freespace`)).json() as { valid?: boolean }[]; - return metrics.length === 3 && metrics.every((metric) => metric.valid); + const valid = metrics.filter((metric) => metric.valid === true).length; + const summary = `Cluster freespace metrics observed: ${metrics.length}; valid: ${valid}.`; + if (summary !== metricSummary) console.log(summary); + metricSummary = summary; + return metrics.length === 3 && valid === 3; }); stage = "running application integration tests"; console.log("Three private Kubo/Cluster peers ready; running application integration tests."); From f7886beedd61d428d3d4fbfdb1645763a2a29678 Mon Sep 17 00:00:00 2001 From: Karthik Chandrasekaran Date: Sat, 19 Sep 2026 09:14:17 -0500 Subject: [PATCH 5/6] Segregate pluggable providers and add reviewed AWS hybrid storage Add DynamoDB metadata, synchronous encrypted-content backups and explicit recovery, runtime Secrets Manager references, and an opt-in EC2/EBS/S3/KMS foundation. Keep provider SDKs outside the record service and preserve dual unlock. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fc4430e2-35f5-48b8-bd0f-5dc6fad2a0ff --- .env.example | 7 + .github/workflows/storage-cloud-smoke.yml | 14 +- README.md | 5 + docs/AWS_HYBRID_STORAGE.md | 533 +++++++++++++++++ docs/DISTRIBUTED_STORAGE.md | 25 +- docs/README.md | 5 + docs/SEQUENCE_FLOWS.md | 2 +- docs/STORAGE_DESIGN.md | 26 +- docs/STORAGE_QUICKSTART.md | 47 +- examples/config/aws-hybrid.env.example | 19 + examples/restore-content.ts | 20 + infra/aws/runtime/app.env.example | 20 + infra/aws/runtime/direct-api-auth.env.example | 8 + infra/aws/runtime/ipfs-cluster.service | 34 ++ infra/aws/runtime/kubo.service | 36 ++ infra/aws/storage-foundation.json | 564 ++++++++++++++++++ package-lock.json | 101 ++++ package.json | 3 + scripts/run-ipfs-integration.ts | 31 +- src/index.ts | 25 +- src/service/patient-record-service.ts | 2 +- src/storage/README.md | 80 +++ .../{ => adapters/azure}/azure-blob-store.ts | 4 +- src/storage/adapters/azure/provider.ts | 30 + .../adapters/dynamodb/dynamodb-blob-store.ts | 146 +++++ src/storage/adapters/dynamodb/provider.ts | 34 ++ .../{ => adapters/file}/file-blob-store.ts | 2 +- src/storage/adapters/file/provider.ts | 6 + .../adapters/ipfs/backed-up-content-store.ts | 66 ++ .../ipfs}/distributed-blob-store.ts | 32 +- .../ipfs}/ipfs-cluster-client.ts | 0 src/storage/adapters/ipfs/provider.ts | 57 ++ .../memory}/in-memory-blob-store.ts | 2 +- src/storage/adapters/memory/provider.ts | 4 + src/storage/adapters/s3/provider.ts | 32 + .../{ => adapters/s3}/s3-blob-store.ts | 4 +- src/storage/config/factory.ts | 44 ++ src/storage/config/provider.ts | 18 + src/storage/config/secrets.ts | 73 +++ src/storage/config/settings.ts | 68 +++ src/storage/configuration.ts | 122 ---- src/storage/configured-blob-store.ts | 129 ---- src/storage/{ => core}/blob-store.ts | 0 src/storage/{ => core}/storage-validation.ts | 0 test/configured-blob-store.spec.ts | 106 ---- test/fixtures/ipfs/README.md | 2 +- test/fixtures/ipfs/fixture.spec.ts | 4 +- test/fixtures/ipfs/fixture.ts | 2 +- test/fixtures/ipfs/kubo.sh | 2 +- test/infrastructure/aws.spec.ts | 301 ++++++++++ test/integration/configured-storage.spec.ts | 18 +- .../azure}/azure-blob-store.spec.ts | 2 +- test/storage/config/factory.spec.ts | 244 ++++++++ .../config/settings-and-secrets.spec.ts} | 49 +- .../core}/blob-store-contract.spec.ts | 4 +- .../dynamodb/dynamodb-blob-store.spec.ts | 366 ++++++++++++ .../file}/file-blob-store.spec.ts | 2 +- .../ipfs/backed-up-content-store.spec.ts | 179 ++++++ .../ipfs}/distributed-blob-store.spec.ts | 4 +- .../ipfs}/ipfs-cluster-client.spec.ts | 5 +- test/{ => storage/s3}/s3-blob-store.spec.ts | 2 +- 61 files changed, 3343 insertions(+), 429 deletions(-) create mode 100644 docs/AWS_HYBRID_STORAGE.md create mode 100644 examples/config/aws-hybrid.env.example create mode 100644 examples/restore-content.ts create mode 100644 infra/aws/runtime/app.env.example create mode 100644 infra/aws/runtime/direct-api-auth.env.example create mode 100644 infra/aws/runtime/ipfs-cluster.service create mode 100644 infra/aws/runtime/kubo.service create mode 100644 infra/aws/storage-foundation.json create mode 100644 src/storage/README.md rename src/storage/{ => adapters/azure}/azure-blob-store.ts (97%) create mode 100644 src/storage/adapters/azure/provider.ts create mode 100644 src/storage/adapters/dynamodb/dynamodb-blob-store.ts create mode 100644 src/storage/adapters/dynamodb/provider.ts rename src/storage/{ => adapters/file}/file-blob-store.ts (98%) create mode 100644 src/storage/adapters/file/provider.ts create mode 100644 src/storage/adapters/ipfs/backed-up-content-store.ts rename src/storage/{ => adapters/ipfs}/distributed-blob-store.ts (82%) rename src/storage/{ => adapters/ipfs}/ipfs-cluster-client.ts (100%) create mode 100644 src/storage/adapters/ipfs/provider.ts rename src/storage/{ => adapters/memory}/in-memory-blob-store.ts (97%) create mode 100644 src/storage/adapters/memory/provider.ts create mode 100644 src/storage/adapters/s3/provider.ts rename src/storage/{ => adapters/s3}/s3-blob-store.ts (98%) create mode 100644 src/storage/config/factory.ts create mode 100644 src/storage/config/provider.ts create mode 100644 src/storage/config/secrets.ts create mode 100644 src/storage/config/settings.ts delete mode 100644 src/storage/configuration.ts delete mode 100644 src/storage/configured-blob-store.ts rename src/storage/{ => core}/blob-store.ts (100%) rename src/storage/{ => core}/storage-validation.ts (100%) delete mode 100644 test/configured-blob-store.spec.ts create mode 100644 test/infrastructure/aws.spec.ts rename test/{ => storage/azure}/azure-blob-store.spec.ts (99%) create mode 100644 test/storage/config/factory.spec.ts rename test/{configuration.spec.ts => storage/config/settings-and-secrets.spec.ts} (70%) rename test/{ => storage/core}/blob-store-contract.spec.ts (82%) create mode 100644 test/storage/dynamodb/dynamodb-blob-store.spec.ts rename test/{ => storage/file}/file-blob-store.spec.ts (98%) create mode 100644 test/storage/ipfs/backed-up-content-store.spec.ts rename test/{ => storage/ipfs}/distributed-blob-store.spec.ts (98%) rename test/{ => storage/ipfs}/ipfs-cluster-client.spec.ts (98%) rename test/{ => storage/s3}/s3-blob-store.spec.ts (99%) diff --git a/.env.example b/.env.example index c6b4c23..ceb4f06 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,8 @@ STORAGE_EMULATOR=false # IPFS_API_URL=https://REPLACE # IPFS_CLUSTER_API_URL=https://REPLACE # IPFS_METADATA_BACKEND=azure +# Optional synchronous backup before metadata publication: +# IPFS_BACKUP_BACKEND=s3 # IPFS_REPLICATION_MIN=2 # IPFS_REPLICATION_MAX=3 # IPFS_TIMEOUT_MS=30000 @@ -30,3 +32,8 @@ STORAGE_EMULATOR=false # AZURE_KEY_VAULT_URL=https://REPLACE.vault.azure.net # IPFS_API_AUTH_KEYVAULT_SECRET=ipfs-api-auth # IPFS_CLUSTER_AUTH_KEYVAULT_SECRET=ipfs-cluster-auth +# OR retrieve headers using an AWS workload role: +# IPFS_API_AUTH_AWS_SECRET_ID=REPLACE_WITH_SECRET_ARN +# IPFS_CLUSTER_AUTH_AWS_SECRET_ID=REPLACE_WITH_SECRET_ARN +# AWS hybrid: IPFS_METADATA_BACKEND=dynamodb, IPFS_BACKUP_BACKEND=s3 +# DYNAMODB_TABLE=REPLACE_WITH_METADATA_TABLE diff --git a/.github/workflows/storage-cloud-smoke.yml b/.github/workflows/storage-cloud-smoke.yml index 9c8def3..e36676d 100644 --- a/.github/workflows/storage-cloud-smoke.yml +++ b/.github/workflows/storage-cloud-smoke.yml @@ -7,7 +7,7 @@ on: description: Dedicated synthetic-data provider (never a patient store) type: choice required: true - options: [azure, s3] + options: [azure, s3, dynamodb, aws-hybrid] permissions: contents: read @@ -24,13 +24,21 @@ jobs: env: STORAGE_INTEGRATION: "true" STORAGE_EMULATOR: "false" - STORAGE_BACKEND: ${{ inputs.backend }} + STORAGE_BACKEND: ${{ inputs.backend == 'aws-hybrid' && 'ipfs' || inputs.backend }} AZURE_STORAGE_ACCOUNT_URL: ${{ vars.AZURE_STORAGE_ACCOUNT_URL }} AZURE_STORAGE_CONTAINER: ${{ vars.AZURE_STORAGE_CONTAINER }} S3_BUCKET: ${{ vars.S3_BUCKET }} STORAGE_PREFIX: synthetic-integration/ AWS_REGION: ${{ vars.AWS_REGION }} S3_KMS_KEY_ID: ${{ vars.S3_KMS_KEY_ID }} + DYNAMODB_TABLE: ${{ vars.DYNAMODB_TABLE }} + IPFS_PRIVATE_NETWORK: "true" + IPFS_METADATA_BACKEND: dynamodb + IPFS_BACKUP_BACKEND: s3 + IPFS_API_URL: ${{ vars.IPFS_API_URL }} + IPFS_CLUSTER_API_URL: ${{ vars.IPFS_CLUSTER_API_URL }} + IPFS_API_AUTH_AWS_SECRET_ID: ${{ vars.IPFS_API_AUTH_AWS_SECRET_ID }} + IPFS_CLUSTER_AUTH_AWS_SECRET_ID: ${{ vars.IPFS_CLUSTER_AUTH_AWS_SECRET_ID }} steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: @@ -46,7 +54,7 @@ jobs: client-id: ${{ vars.AZURE_CLIENT_ID }} tenant-id: ${{ vars.AZURE_TENANT_ID }} subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} - - if: inputs.backend == 's3' + - if: inputs.backend != 'azure' uses: aws-actions/configure-aws-credentials@ff717079ee2060e4bcee96c4779b553acc87447c # v4 with: role-to-assume: ${{ vars.AWS_ROLE_ARN }} diff --git a/README.md b/README.md index 4682e44..9e4c7db 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,10 @@ For Azure, AWS S3, private IPFS, account-free integration tests, and secret-free configuration, see the short [storage quickstart](docs/STORAGE_QUICKSTART.md). The [draft storage design](docs/STORAGE_DESIGN.md) describes concurrency, metadata exposure, and remaining distributed-coordination limits. +Provider implementations are isolated in +[`src/storage/adapters/`](src/storage/README.md). An optional +[AWS hybrid foundation](docs/AWS_HYBRID_STORAGE.md) combines EC2/Kubo/EBS, +DynamoDB metadata, and SSE-KMS encrypted S3 backups. | Command | Purpose | | --- | --- | @@ -126,6 +130,7 @@ metadata exposure, and remaining distributed-coordination limits. | `npm run test:integration:azure` | Run real-SDK tests against temporary loopback Azurite | | `npm run test:integration:ipfs` | Run the private IPFS Docker fixture (Docker engine required) | | `npm run test:integration` | Explicit opt-in external-provider contract and encrypted-flow tests | +| `npm run storage:restore -- OBJECT_KEY` | Explicitly restore verified encrypted IPFS content from its configured backup | ## Minimal usage diff --git a/docs/AWS_HYBRID_STORAGE.md b/docs/AWS_HYBRID_STORAGE.md new file mode 100644 index 0000000..7b200d4 --- /dev/null +++ b/docs/AWS_HYBRID_STORAGE.md @@ -0,0 +1,533 @@ +# AWS hybrid storage foundation + +**Global Patient Record Project — synthetic-only research POC.** This is not +clinical, regulatory, production, disaster-recovery, or ten-year-service +certification. No AWS account, credentials, live deployment, or cloud validation +was used to implement this sample. Review the draft before changing settings. +Nothing here creates a GitHub deployment workflow or stores secrets in GitHub. + +## What is implemented + +`infra/aws/storage-foundation.json` is a manually deployable CloudFormation +foundation, with `CreateNode=false` by default: + +```text +unchanged dual-unlock application cryptography + -> authenticated ciphertext + -> private Kubo / IPFS Cluster -> optional EC2 -> encrypted EBS + -> opaque lookup / CAS metadata -> DynamoDB (customer-managed SSE-KMS) + -> encrypted content backup -> private versioned S3 (SSE-KMS) +``` + +The optional node uses an **operator-reviewed, prebuilt AMI**. CloudFormation +does not install Kubo, initialize a repository, format a disk, distribute a swarm +key, create TLS endpoints, join peers, or establish a working private cluster. +The service files under `infra/aws/runtime` are hardening examples for that AMI, +not unattended bootstrap installers. No executable is downloaded by this stack. +The AMI/runtime acceptance checklist below is a deployment prerequisite. + +| Resource | Implemented boundary | +| --- | --- | +| DynamoDB | One string partition key `key`, on-demand billing, PITR, deletion protection, separate rotating KMS key; no TTL/index/scan permission | +| S3 | All public access blocked, ACLs disabled, versioning, separate rotating KMS key, TLS-only policy, explicit correct SSE-KMS headers required | +| Retention | Table, bucket, bucket policy, keys and separate data EBS retained on stack deletion/replacement; **no completed-object/version expiry** | +| EC2, opt-in | Private subnet, explicit no public IPv4, no SSH key or SSH ingress, IMDSv2 required, metadata hop limit 1 | +| EBS | Encrypted gp3 root and separate encrypted data volume; only data volume has resource-level Retain policies | +| Networking | Only HTTPS from supplied client SG; egress HTTPS only to endpoint SG and regional S3/DynamoDB prefix lists | +| Instance role | EC2 trust; GetItem/PutItem on one table; GetObject/PutObject on `records/*`; ListBucket on this dedicated backup bucket; service/context-scoped KMS use; optional exact-ARN external bootstrap/API secrets | + +These resource properties follow the AWS references [1–8]. Retain is not a +backup or an IAM delete denial: retained resources still cost money and leave +CloudFormation management when removed from a stack [8]. Root volume deletion +is intentional; **all durable Kubo/Cluster state must be on `/srv/ipfs`**. +The sole S3 lifecycle action aborts incomplete multipart uploads after seven +days; it does not expire completed objects [6]. Versioning is not Object Lock: +an administrator can still delete versions or weaken the policy. WORM/Object +Lock, AWS Backup vault policies and legal retention requirements need separate +review; no retention-law claim is made here. + +### Encryption and disclosure are separate issues + +KMS keys in this stack protect AWS infrastructure **at rest**. They are not +`patientDataKey`, a patient factor, a clinician private key, or a replacement +for dual unlock. Keep the existing authenticated ciphertext, signatures and +authorization checks unchanged. The application role cannot export raw KMS key +material; its KMS data operations are limited to service-mediated use. The +table-creation role, not the application, authorizes DynamoDB maintenance +grants [9]. S3 SSE-KMS protects already-encrypted content [7]. + +AWS and authorized operators can still observe object identifiers, CIDs, sizes, +versions, item access patterns, network relationships, timestamps and key-use +metadata. A CID is **not an access-control secret**. Do not put names, +demographics, biometric material, plaintext FHIR, or patient factors into table +keys, bucket keys, tags, secret names, logs, endpoint hostnames, or stack +parameters. Do not run a public IPFS gateway or publish private CIDs. + +## Before an account-enabled deployment + +1. Obtain an approved account/Region, budget and named operational custodians. + Separate deployment, runtime, recovery and key-administration duties. + Establish MFA-backed operator sessions and an approved short-lived AWS + credential source on the operator workstation. Never commit access keys. +2. Review the JSON, policy tests, current AWS documentation and a change set. + Re-check service availability and IAM in the chosen partition/Region. + The example is a single-Region design, not a global-table/replication setup. +3. For `CreateNode=true`, provide an existing VPC and **private IPv4 subnet**. + Verify the subnet belongs to the VPC and its AZ equals `AvailabilityZone`. + Reject IPv6 autoassignment/public routing in this foundation. Supply + existing client and endpoint SGs in the same VPC. The template checks for + required nonempty parameters, not actual topology or SG membership. +4. Install regional **S3 and DynamoDB gateway endpoints** on the subnet route + table. Supply their correct AWS-managed prefix-list IDs. Constrain endpoint + policies to the output bucket/table and intended roles. Create a + Secrets Manager interface endpoint with private DNS when fetching secrets. + Its SG must allow inbound TCP 443 from the output node SG. DNS resolution, + VPC DNS settings, NACLs and endpoint routing must work. S3/DynamoDB invoke KMS + server-side; these operations do not require a node KMS endpoint. Add one + only if separately approved management/runtime components call KMS. +5. The node has **no internet/NAT egress and no peer traffic rules**. Prebake + dependencies, certificates/trust roots and binaries. Add separately reviewed + SG-to-SG/private-overlay rules for chosen Kubo and Cluster peer transports + before joining other nodes. Never replace the rules with `0.0.0.0/0`, + `::/0`, public RPC, or unrestricted TCP/UDP. Default ports to investigate + are Kubo swarm 4001 and Cluster peer RPC 9096; pin the actual transports in + the reviewed runtime, and do not expose Kubo 5001, gateway 8080, Cluster + REST 9094 or proxy 9095 remotely [10–11]. +6. The role deliberately has **no SSM session, shell, ECR, CloudWatch logs, + snapshot, object-delete or IAM permissions**. If management + uses SSM, separately review required instance/session IAM permissions, + endpoint SGs and `ssm`/`ssmmessages` endpoints for the selected Region and + agent. Do not assume Session Manager works from this template alone. + Keep remote command/session output from capturing secrets or CIDs. +7. Supply an approved x86_64 AMI with a root mapping matching `RootDeviceName`, + no extra AMI data volumes, and root size no greater than the template's + 32 GiB. Record the AMI owner, immutable ID, source build, pinned binary + versions/digests, provenance and patch review. Do not clone private node + identities or bake secret values into the image. + +### IAM and key-policy review + +The operator needs permission to create/describe/execute the designated +CloudFormation change set and `iam:PassRole` on the designated deployment +execution role, with `iam:PassedToService=cloudformation.amazonaws.com`. +That execution role trusts **`cloudformation.amazonaws.com`**, not GitHub and +not arbitrary AWS accounts. Prefer a dedicated review/deploy path; do not grant +runtime credentials to CloudFormation. + +The execution role needs resource-management permissions for the resource +types in this template: DynamoDB table/PITR settings; S3 bucket, encryption, +versioning, public-access, ownership, lifecycle and policy settings; KMS keys; +IAM role/inline policy/instance-profile creation; and, when enabled, EC2 +launch templates, instances, volumes/attachments and security groups. +Scope to the selected account/Region, stack resources/naming and supported +request tags. Account for rollback/update permissions and AWS actions which +require `Resource: "*"`; do not copy the runtime policy as a deployment policy. +For node launch, restrict `iam:PassRole` to the generated node role and +`iam:PassedToService=ec2.amazonaws.com`. + +KMS policies use AWS's standard account-root **IAM delegation** statement [12]. +`Resource: "*"` in each key policy means that key, not every account key. +This is deliberately not a wildcard public principal, but account administrators +can delegate broad access: audit IAM and consider SCPs/permissions boundaries. +The execution role must be allowed to create/manage keys and authorize service +use on the new key ARNs. For the DynamoDB key, table creation requires +`kms:Encrypt`, `kms:Decrypt`, `kms:ReEncrypt*`, `kms:GenerateDataKey*`, +`kms:DescribeKey` and `kms:CreateGrant` through DynamoDB [9]. +For EBS launch/attachment, authorize the needed key cryptographic/describe +operations and `kms:CreateGrant` for EC2, with +`kms:GrantIsForAWSResource=true` for the grant statement [13]. +Do not give these provisioning/grant permissions to the application. +Keep service `kms:ViaService` conditions on service-mediated operations, not +on unrelated KMS administration calls. This stack does not configure global +tables; revisit the documented cross-Region ViaService requirements before +adding them [9]. + +`NodeRole` always exists, even with compute disabled, and trusts EC2 only. +The default runtime placement is a host process on the approved instance. +Containers must not evade the IMDS hop limit: use a separately reviewed +credential-delivery design if running the application in containers. +For another compute service, create an independently reviewed workload role +with equivalent narrowly scoped permissions and the correct service/OIDC +trust; do not broaden this node trust or use permanent keys. + +`s3:ListBucket` is scoped to the **single dedicated backup bucket ARN**, not all +buckets. S3 needs bucket-list authorization to distinguish a nonexistent +GetObject key (404) from access denied (403) [17]. The adapter must not treat +403 as a missing object. This grant also permits enumerating that bucket's +object names, so do not mix other datasets into the bucket. Object read/write +and KMS use remain scoped to `records/*`; version enumeration and deletion +remain recovery-only. No `s3:prefix` request condition is applied to this +missing-key grant: GetObject is not a ListObjects request carrying a prefix. +Validate the missing-key/error contract with the actual role and endpoint +policies before enabling the application. + +### External secrets: identifiers in configuration, values outside GitHub + +Create secrets separately using an approved secrets-management workflow. +Optionally provide `RuntimeSecretArn` **and** `RuntimeSecretKmsKeyArn`; they +must name an existing secret and customer-managed key in this account/Region. +The external key policy must delegate decrypt to the generated node role. +The optional policy only permits reading this single ARN, with KMS decrypt +through Secrets Manager and matching `SecretARN` encryption context [14]. +Do not send secret values to a CloudFormation parameter, dynamic reference, +UserData, command line, shell history, console, GitHub variable or GitHub secret. + +The approved AMI must contain a tested secret-mounting agent, running with its +instance role. For example, an **operator-defined JSON secret contract** can +provide private swarm material, Cluster secret and two authorization headers. +This repository does not implement that host agent or prescribe its JSON schema: +review it with the AMI, and fail closed if values are absent/invalid. The agent +must retrieve directly into memory, atomically write restricted files in a +memory-backed `/run/global-patient-record-secrets`, and never print a response +or log secret-bearing errors. Require root ownership, restrictive directories +and mode 0400/0440 with the minimal service group; prohibit symlink substitution. +Disable swap or use separately reviewed encrypted swap, and disable core dumps. +Rotation must atomically replace credentials and restart/reload the consumers; +test unauthorized/expired credentials and rollback without revealing values. + +Mount authorization header values as single-line files: + +```text +IPFS_API_AUTH_FILE=/run/global-patient-record-secrets/kubo-authorization +IPFS_CLUSTER_AUTH_FILE=/run/global-patient-record-secrets/cluster-authorization +``` + +These are supported runtime settings; do not put the actual header in the +environment. For direct application retrieval instead, use the alternative +`infra/aws/runtime/direct-api-auth.env.example` fragment and **remove** the two +`*_FILE` settings. Configure: + +```text +IPFS_API_AUTH_AWS_SECRET_ID= +IPFS_CLUSTER_AUTH_AWS_SECRET_ID= +AWS_REGION= +``` + +Each referenced `SecretString` must hold the **complete single-line +Authorization header**, not JSON or only a bare token. The application uses +the AWS SDK default role/credential chain; no access-key environment variables +are needed on the approved EC2 host. Do not configure file, Key Vault and +Secrets Manager sources simultaneously for the same setting. + +Supply the corresponding optional CloudFormation parameters +`ApiAuthorizationSecretArn` and `ClusterAuthorizationSecretArn`. Each enables +only `secretsmanager:GetSecretValue` for that exact ARN. If either secret uses a +customer-managed key, also supply its +`ApiAuthorizationSecretKmsKeyArn` or `ClusterAuthorizationSecretKmsKeyArn`: +these grant decrypt on the designated key only through Secrets Manager with +the matching secret encryption context. Leave the key parameter empty only +when using the AWS-managed Secrets Manager key. Each external customer key's +policy must authorize the node role. All referenced secrets must exist in the +selected account/Region, and direct retrieval requires the Secrets Manager +endpoint connectivity described above. The two header-secret permissions are +separate from `RuntimeSecretArn`, which can still serve the approved host +bootstrap/mount agent. No secret is created or secret value accepted by this +template. + +Kubo's swarm file and Cluster's service configuration contain infrastructure +secrets and must also be mounted/provisioned privately. Patient factors and +clinician private keys remain outside this infrastructure-secret bundle. + +## Manual review, validation and deployment + +Offline check, from the repository root, requires only existing dependencies: + +```powershell +npm test -- test/infrastructure/aws.spec.ts +``` + +This parses JSON and checks selected policy/security invariants. It is **not** +CloudFormation schema validation, IAM simulation, AMI verification, a private +cluster join test, or evidence that AWS accepted/deployed the stack. + +Only after review and account provisioning, run the following yourself. +Use your approved Region/profile; authenticate without storing credentials in +the repository. In these PowerShell examples, replace all uppercase identifiers. + +```powershell +aws cloudformation validate-template ` + --template-body file://infra\aws\storage-foundation.json ` + --region REVIEWED_REGION --profile REVIEWED_PROFILE + +aws cloudformation create-change-set ` + --stack-name global-patient-record-research ` + --change-set-name reviewed-foundation --change-set-type CREATE ` + --template-body file://infra\aws\storage-foundation.json ` + --capabilities CAPABILITY_IAM ` + --role-arn REVIEWED_CLOUDFORMATION_EXECUTION_ROLE_ARN ` + --parameters ParameterKey=CreateNode,ParameterValue=false ` + --region REVIEWED_REGION --profile REVIEWED_PROFILE + +aws cloudformation wait change-set-create-complete ` + --stack-name global-patient-record-research --change-set-name reviewed-foundation ` + --region REVIEWED_REGION --profile REVIEWED_PROFILE + +aws cloudformation describe-change-set ` + --stack-name global-patient-record-research --change-set-name reviewed-foundation ` + --region REVIEWED_REGION --profile REVIEWED_PROFILE +``` + +Stop here for human approval. Check generated IAM, deletion/replacement impacts, +retained-resource billing, key ownership and rollback. After approval: + +```powershell +aws cloudformation execute-change-set ` + --stack-name global-patient-record-research --change-set-name reviewed-foundation ` + --region REVIEWED_REGION --profile REVIEWED_PROFILE + +aws cloudformation wait stack-create-complete ` + --stack-name global-patient-record-research ` + --region REVIEWED_REGION --profile REVIEWED_PROFILE + +aws cloudformation describe-stacks --stack-name global-patient-record-research ` + --query "Stacks[0].Outputs" --region REVIEWED_REGION --profile REVIEWED_PROFILE +``` + +This creates the storage foundation and role, **not a runnable IPFS deployment**. +Outputs contain only identifiers/nonsecret settings; still avoid publishing +infrastructure inventory. For the optional node, create a second `UPDATE` +change set, with the same template/role/capability, and explicitly supply: + +```text +CreateNode=true +VpcId= +PrivateSubnetId= +AvailabilityZone= +ApprovedAmiId= +RootDeviceName= +ClientSecurityGroupId= +EndpointSecurityGroupId= +S3PrefixListId= +DynamoDbPrefixListId= +RuntimeSecretArn= +RuntimeSecretKmsKeyArn= +ApiAuthorizationSecretArn= +ApiAuthorizationSecretKmsKeyArn= +ClusterAuthorizationSecretArn= +ClusterAuthorizationSecretKmsKeyArn= +``` + +Use `--parameters ParameterKey=NAME,ParameterValue=VALUE ...` for these nonsecret +identifiers. Review and execute as above, using `stack-update-complete` instead +of `stack-create-complete`. Approve replacement separately: retained EBS does +**not** mean a replacement node automatically remounts/rejoins safely. + +### Approved AMI/runtime acceptance checklist + +* The host agent waits for the **output `DataVolumeId`** attachment, resolves + that exact EBS ID to its device, and rejects the root disk or an unexpected + serial/volume. Nitro device names need not match `/dev/sdf` [15]. + Use EBS NVMe identification/serial tools and confirm with the EC2 attachment + inventory. **Never format “the next disk”, `/dev/nvme1n1`, or `/dev/sdf` + based only on a guessed device name.** +* No sample automatically formats storage. An authorized operator may initialize + only a positively identified, new, empty volume after checking existing + signatures/partitions and mount use. A recovered/retained volume is never + reformatted. Mount a reviewed filesystem by UUID at `/srv/ipfs` with + nodev/nosuid restrictions, appropriate ownership and a fail-closed mount unit. + Verify the mount after reboot. Snapshot/quiesce before changing it. +* Generate unique node identities at first provisioning, not at AMI build. + Initialize pinned-version Kubo and Cluster configurations on the data volume + through the approved provisioning workflow. The provided units do not + initialize defaults. A missing mount/config/swarm key prevents startup. +* Set Kubo API to loopback, disable its gateway, remove public bootstrap peers, + restrict discovery/routing/announcements to the private network and enforce + the shared private-swarm key. `LIBP2P_FORCE_PNET=1` is also set in the sample + unit. Configure peer addresses to the private routed/overlay network only. + Inspect the actual effective configuration for the exact baked version [10]. +* Configure Cluster REST/IPFS proxy as loopback-only (or disable unused proxy), + private peer membership, a nonempty separate Cluster secret and reviewed + consensus/trust/replication settings. Keep `service.json`, `identity.json`, + Kubo identity and swarm material private. Cluster configuration may include + secrets and node identities; do not paste it into an issue or logs [11]. +* Put an operator-managed **authenticated HTTPS reverse proxy** on private port + 443, with distinct private DNS names for Kubo and Cluster. It must validate + authorization, use trusted TLS, limit application RPC routes/methods, sizes and + timeouts, and never log request bodies, authorization headers or CID-bearing + URLs. No public load balancer or gateway. Auth failures must not reach RPC. +* Install the example units only after inspecting their paths against the AMI. + Secrets must exist before services start, with restart/rotation ordering + managed by the approved host agent. Keep swap/core/log disclosure controls. + Their stdout/stderr are suppressed deliberately; add only reviewed, + non-identifying health metrics, not unfiltered application/node logs. +* Verify unauthorized HTTPS requests fail, trusted clients authenticate, + plaintext/TLS-bypass and raw ports cannot be reached, IMDSv1 fails, node + secrets are unreadable by other users, and an unrelated IAM principal cannot + access data/keys. Test these **in the account**; offline tests cannot. + +### Application configuration + +Use `examples/config/aws-hybrid.env.example` for the application quickstart or +copy `infra/aws/runtime/app.env.example` into your separately managed deployment +configuration, substituting these output values: + +| Setting | Output | +| --- | --- | +| `STORAGE_BACKEND` | `StorageBackend` (`ipfs`) | +| `IPFS_METADATA_BACKEND` | `IpfsMetadataBackend` (`dynamodb`) | +| `DYNAMODB_TABLE` | `DynamoDbTable` | +| `AWS_REGION` | `AwsRegion` | +| `IPFS_BACKUP_BACKEND` | `IpfsBackupBackend` (`s3`) | +| `S3_BUCKET` | `S3Bucket` | +| `S3_KMS_KEY_ID` | `S3KmsKeyId` (full key ARN) | +| `STORAGE_PREFIX` | `StoragePrefix` (`records/`) | +| `IPFS_PRIVATE_NETWORK` | `IpfsPrivateNetwork` (`true`) | + +Also provide actual HTTPS `IPFS_API_URL`, `IPFS_CLUSTER_API_URL` and the mounted +authorization-file paths. Do not use the `.invalid` example hostnames as actual +endpoints. Keep `STORAGE_PREFIX=records/` unless the corresponding IAM and KMS +encryption-context scopes are changed in a reviewed change set. Bucket keys +are disabled because the KMS context policy deliberately scopes to object ARNs. +Send explicit SSE-KMS/key-ARN headers: bucket defaults alone do not satisfy +the upload policy. + +**The application requires at least two IPFS replicas outside emulator mode.** +The single optional EC2 instance is only one node; it cannot by itself satisfy +the example `IPFS_REPLICATION_MIN=2`, `IPFS_REPLICATION_MAX=3`. Provision +additional independently persisted peers and approve private peer connectivity +before enabling writes. Do not use emulator mode or weaken replica checks to +make this one-node foundation appear highly available. + +## Backup, recovery and operation + +There is no atomic transaction spanning Cluster, S3 and DynamoDB. Treat failed +writes and partially completed operations as reconciliation work. The +application's provider-neutral `BackedUpContentStore` writes its configured +backup synchronously as part of a successful content write; it copies +authenticated ciphertext, not plaintext. `IPFS_BACKUP_BACKEND=s3` requires +`S3_KMS_KEY_ID` outside emulator mode. The wrapper uses the `BlobStore` +interface, not S3 SDK calls in the service or crypto layers. +It does not schedule DynamoDB/EBS backups, export a recovery catalog, replicate +accounts, or recover patient factors for you. Track and alarm on failures +without placing record identifiers or CIDs into general-purpose logs. + +1. **Establish a recovery baseline.** After account validation, exercise a + synthetic write/read/CAS, confirm required replicas and S3 encrypted versions, + and record an access-controlled inventory relating opaque metadata keys, + CIDs/content hashes and S3 object/version IDs. Do not infer that a versioned + content bucket backs up the metadata table. +2. **Metadata protection.** Verify PITR enabled and a usable recovery window. + DynamoDB PITR is a rolling window of up to 35 days, not archival ten-year + retention [16]. Schedule separately reviewed on-demand/AWS Backup copies + with appropriate retention and custodian/account separation. Preserve key + availability for tables **and historical backups**. Schedule protected + EBS snapshots and Cluster state backups as well; this stack has no schedule. +3. **Choose a consistent cut.** Quiesce writes or establish a verified recovery + point/catalog. Choose the metadata time and ensure all referenced encrypted + objects/versions exist. An S3 content backup alone cannot reconstruct lost + identifier-to-CID mappings or CAS heads. A metadata-only PITR restore can + refer to objects that require corresponding version/block recovery. +4. **Restore metadata into a new table** with an isolated recovery role, not + the runtime role. Inspect the latest/earliest restorable times, then use: + + ```powershell + aws dynamodb restore-table-to-point-in-time ` + --source-table-name SOURCE_TABLE --target-table-name RECOVERY_TABLE ` + --restore-date-time REVIEWED_UTC_TIMESTAMP ` + --region REVIEWED_REGION --profile REVIEWED_RECOVERY_PROFILE + ``` + + Wait for ACTIVE, verify encryption/key access, re-enable PITR and deletion + protection on the restored table, and reapply IAM, tags, endpoint policies, + alarms and backup schedules as necessary [16]. The app role is scoped to + the original table: explicitly review the new table ARN and KMS context in + its policy before cutover. Do not silently change only `DYNAMODB_TABLE`. +5. **Restore blocks/ciphertext and pins.** If the retained volume is healthy, + attach it only to an approved replacement in the **same AZ**, with services + stopped; mount without formatting. For another AZ, restore a reviewed EBS + snapshot into that AZ or repopulate content on new disks [3,15]. For S3 + recovery, an isolated recovery role may need ListBucketVersions and + GetObjectVersion on the exact bucket/prefix plus decrypt on the historical + key. Those extra permissions are intentionally absent from the node role. + Resolve delete markers/older versions under review; the normal adapter + reads current versions, not arbitrary historical version IDs. +6. **Verify before publication.** Restore the exact encrypted content and + compare its content hash/CID, re-add/re-pin through the private endpoints, + and confirm the required replica count. Use the repository's + explicit operator command: + + ```powershell + npm run storage:restore -- OPAQUE_OBJECT_KEY + ``` + + Run with the reviewed runtime configuration and role/secret access. + `DistributedBlobStore.restore(opaqueObjectKey)` resolves the existing + metadata reference and repairs from encrypted backup, verifies the CID and + pin completion, and **does not change the metadata index**. It cannot recover + a lost index; restore metadata first. Do not treat a successful S3 GET as a + verified cluster recovery. Preserve node identities + appropriately, avoiding a live duplicate identity. Independently verify + restored metadata, version/CAS heads, signatures and parent links with + synthetic test records before approving traffic/cutover. +7. **Drill and reconcile.** Test loss of a node/AZ, inaccessible KMS keys, + unavailable S3/DynamoDB, wrong secret/TLS credentials, missing blocks and a + metadata rollback. Document measured RPO/RTO and repair gaps. Retain source + volumes/tables and key access until restored data has been verified and a + deliberate cleanup is approved. Do not blindly toggle `CreateNode` off/on: + retained disks/keys may become unmanaged and new resources are not a restore. + +The default runtime role cannot enumerate the table, delete backups, perform +PITR restore or disable keys. Recovery custodians need a separately approved +role with precisely those operations required for the chosen drill, bounded +to source/destination resources and old/new keys. Root/admin permissions are +not a routine recovery plan. + +### Resilience limits and long-term posture + +One EC2 plus one AZ-pinned EBS volume remains a single point of failure. +Additional peers need independent volumes, availability zones, monitored pin +health, reviewed quorum/replication and independent custodians. For meaningful +provider/account independence, maintain tested encrypted copies and metadata +exports with independent operators, plus a separate application-key custody +and recovery plan. Do not export clinical plaintext to accomplish this. + +This AWS foundation is **not fully decentralized**: it depends on the account, +Region, IAM, KMS, DynamoDB and S3 remaining available and authorized. Multiple +AZs do not remove the account/KMS dependency. Key deletion or account closure +can make retained ciphertext inaccessible; rotation is not a key-deletion +plan [7,9,12]. AWS retention flags do not promise ten years of service. +Fund ongoing storage/egress/operations, patch pinned software through review, +renew certificates, rotate secrets, monitor drift and billing, designate +successor custodians, and repeat recovery/migration drills for the desired +retention horizon. Review crypto agility and migration before obsolete +software/algorithms or organizational changes make recovery impractical. + +## Official references + +Reviewed September 19, 2026; re-check before a live deployment. These references +describe service behavior, not successful validation of this sample. + +1. EC2 launch templates and IMDS properties: + `https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-ec2-launchtemplate-metadataoptions.html` +2. EC2 root EBS mapping: + `https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-ec2-launchtemplate-ebs.html` +3. Separate EC2 volume resource and AZ: + `https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-ec2-volume.html` +4. DynamoDB table properties: + `https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-dynamodb-table.html` +5. S3 bucket properties: + `https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-s3-bucket.html` +6. S3 lifecycle behavior: + `https://docs.aws.amazon.com/AmazonS3/latest/userguide/intro-lifecycle-rules.html` +7. S3 SSE-KMS: + `https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html` +8. CloudFormation Retain: + `https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-attribute-deletionpolicy.html` +9. DynamoDB KMS permissions, grants and contexts: + `https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/encryption.usagenotes.html` +10. Kubo configuration (validate against the exact reviewed release): + `https://github.com/ipfs/kubo/blob/master/docs/config.md` +11. Cluster configuration, identities, secrets, replication and ports: + `https://ipfscluster.io/documentation/reference/configuration/` +12. KMS default key policy/account delegation: + `https://docs.aws.amazon.com/kms/latest/developerguide/key-policy-default.html` +13. EBS KMS requirements: + `https://docs.aws.amazon.com/ebs/latest/userguide/ebs-encryption-requirements.html` +14. Secrets Manager KMS encryption: + `https://docs.aws.amazon.com/secretsmanager/latest/userguide/security-encryption.html` +15. EBS NVMe device identification: + `https://docs.aws.amazon.com/ebs/latest/userguide/identify-nvme-ebs-device.html` +16. DynamoDB PITR: + `https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/PointInTimeRecovery_Howitworks.html` +17. S3 GetObject missing-key permission behavior: + `https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html` diff --git a/docs/DISTRIBUTED_STORAGE.md b/docs/DISTRIBUTED_STORAGE.md index 7c5f3f2..ab39bea 100644 --- a/docs/DISTRIBUTED_STORAGE.md +++ b/docs/DISTRIBUTED_STORAGE.md @@ -35,7 +35,7 @@ Its options are `kuboUrl`, `clusterUrl`, optional `kuboAuthorization` and `minReplicas`/`maxReplicas` (both default 2), `timeoutMs` (30000), `pollIntervalMs` (250), and `maxBlobBytes` (1048575). `fetch` is an optional injected transport for tests. Secret resolution belongs outside the client: -load headers through the application's secret-file/Key Vault configuration, +load headers through the application's secret-file, Key Vault, or Secrets Manager configuration, not source, command arguments, URLs, logs, or examples. Endpoint URLs are origins only, HTTPS by default, without credentials, paths, @@ -75,6 +75,29 @@ neither storage confidentiality nor application CAS; encryption remains mandatory. Private peers see ciphertext, CIDs, sizes, pin membership and timing; the index sees random object keys, CIDs and versions. +## Optional durable backup + +Set `IPFS_BACKUP_BACKEND=s3` (and `S3_KMS_KEY_ID` outside emulator mode) to +wrap private content storage in `BackedUpContentStore`. The wrapper depends on +`BlobStore`, not an AWS SDK, so another registered durable object-store provider +can replace S3. Each upload must finish primary replication **and** immutable +backup creation before the index pointer can be published. Failures propagate; +there is no best-effort success or background-only backup promise. + +Backups contain the application's already-encrypted bytes and a verifiable +CID, wrapped in JSON/base64. For AWS, S3 SSE-KMS adds a second encryption layer. +KMS never receives the patient token, clinician private key, or reconstructed +record data key. Backup deduplication verifies the existing immutable bytes. + +Reads do not silently fall back. An operator may call `DistributedBlobStore.restore` +with an opaque object key (or run `npm run storage:restore -- OBJECT_KEY`). +It verifies backup bytes against the CID and republishes/pins them without +changing metadata. Concurrent index changes may require another recovery pass. +Recover the separate metadata index first if it is lost; this operation does +not enumerate backups, roll back state, recover identities, or repair missing +clinical factors. Restoring an old provider snapshot without a freshness anchor +remains a separate rollback risk. + ## Upstream references Consult the official Kubo RPC reference (`docs.ipfs.tech/reference/kubo/rpc/`), diff --git a/docs/README.md b/docs/README.md index 7bcca34..8511ead 100644 --- a/docs/README.md +++ b/docs/README.md @@ -228,6 +228,7 @@ The POC supplies: - `InMemoryBlobStore` for isolated tests; - `FileBlobStore` for the local demonstration; - `AzureBlobStore` and `S3BlobStore` for conditional cloud-object writes; +- `DynamoDbBlobStore` for strongly consistent, conditional metadata writes; - experimental `DistributedBlobStore` for private IPFS content with a separate compare-and-swap index. @@ -388,6 +389,10 @@ For an architectural review with the GOSQAS team, the most useful questions are: - [Biometric simulation](BIOMETRIC_SIMULATION.md) - [Global Patient Record UX analysis](GLOBAL_PATIENT_RECORD_UX_ANALYSIS.md) - [Provenance design notes](PROVENANCE_DESIGN_NOTES.md) +- [Storage setup and end-to-end check](STORAGE_QUICKSTART.md) +- [Provider layout and extensions](../src/storage/README.md) +- [Private distributed storage](DISTRIBUTED_STORAGE.md) +- [Optional AWS hybrid foundation](AWS_HYBRID_STORAGE.md) - Original concept documents in this directory ## Terminology note diff --git a/docs/SEQUENCE_FLOWS.md b/docs/SEQUENCE_FLOWS.md index 7d99711..29d3a42 100644 --- a/docs/SEQUENCE_FLOWS.md +++ b/docs/SEQUENCE_FLOWS.md @@ -311,7 +311,7 @@ sequenceDiagram `src/service/patient-record-service.ts` - Cryptographic primitives: `src/crypto/primitives.ts` - Provenance types: `src/domain/provenance.ts` -- Storage abstraction: `src/storage/blob-store.ts` +- Storage abstraction: `src/storage/core/blob-store.ts` - Local API: `src/web/server.ts` - Browser biometric simulation and workflow: `web/app.js` - Security assumptions: `docs/THREAT_MODEL.md` diff --git a/docs/STORAGE_DESIGN.md b/docs/STORAGE_DESIGN.md index bcf174d..f54fbe5 100644 --- a/docs/STORAGE_DESIGN.md +++ b/docs/STORAGE_DESIGN.md @@ -13,6 +13,7 @@ PatientRecordService -> BlobStore |-- FileBlobStore (local, single-process) |-- AzureBlobStore (conditional block-blob writes) |-- S3BlobStore (conditional object writes) + |-- DynamoDbBlobStore (strong reads, conditional metadata) `-- DistributedBlobStore |-- private Kubo / IPFS Cluster (immutable bytes) `-- configured BlobStore (key-to-CID index, CAS) @@ -23,6 +24,21 @@ Each deployment chooses one backend. Cross-cloud mirroring and a across independent services is not atomic CAS, and rollback would not solve concurrent writers. A separate consensus/repair design is required. +The contract lives in `src/storage/core/`, generic selection and secrets in +`src/storage/config/`, and each implementation plus its construction/config +factory in `src/storage/adapters/{provider}/`. Provider-specific tests mirror +these folders. Additional typed factories can be registered at the application +composition root; selection does not require changing the service or adding +another provider-specific branch to the generic factory. + +The optional AWS path runs Kubo on EC2 with encrypted EBS persistence, uses +DynamoDB for the atomic index, and synchronously copies immutable encrypted +content into SSE-KMS S3 before index publication. The backup wrapper consumes +the same provider-neutral `BlobStore`, so neither it nor the record service +imports an S3 SDK. KMS protects AWS storage encryption keys and does not replace +dual unlock. See [AWS foundation](AWS_HYBRID_STORAGE.md) for infrastructure and +failure-domain limits. + ## Contract and concurrency `create` preserves version 1 only if the opaque key is absent. `read` returns @@ -33,6 +49,13 @@ then condition the replacement on that ETag. No process-local map is used for cloud concurrency. Native precondition failures map to contract errors; authorization, service, malformed-object, and transport failures remain errors. +DynamoDB uses strongly consistent reads and a conditional item replacement +against the expected version in a single-region table. It requires a string +partition key named `key` and no sort key. Serialized payloads are limited to +350 KiB, so this adapter is intended primarily for small IPFS metadata pointers, +not large encrypted content or backups. DAX and eventually consistent +multi-region global-table writes are not supported CAS authorities. + Factory-created SDK clients do not automatically retry writes. A timed-out request may have committed; callers must not interpret a timeout as definite rollback or blindly re-append. Read/reconcile verified history before retrying. @@ -72,7 +95,8 @@ not an ordinary IPNS mutable pointer pretending to provide linearizable CAS. CIDs. Do not add patient identifiers to blob names, tags, prefixes, or logs. - TLS remains enabled; loopback HTTP is opt-in emulator behavior only. Service credentials are resolved at runtime through workload identity, - Key Vault, or restricted secret mounts. No static credentials are checked in. + Key Vault, AWS Secrets Manager, or restricted secret mounts. No static + credentials are checked in. - Default credential chains are a convenience, not proof of least privilege; operators must configure narrowly scoped identities and network controls. - Authenticated encryption detects modification but does not independently diff --git a/docs/STORAGE_QUICKSTART.md b/docs/STORAGE_QUICKSTART.md index dd10db0..3921b0c 100644 --- a/docs/STORAGE_QUICKSTART.md +++ b/docs/STORAGE_QUICKSTART.md @@ -5,6 +5,10 @@ Choose one backend; the adapters do not automatically copy data between clouds. Existing `.data/web` records are not migrated when the backend changes. Give IPFS's key-to-CID index its own container/bucket (using the same Azure/S3 settings), not a namespace already populated by a direct-storage adapter. +Provider implementations and setup are segregated under +`src/storage/adapters/`. See the [extension guide](../src/storage/README.md) +for the folder layout and registering a custom adapter without editing the +record service or built-in factory. ## Without any cloud accounts @@ -25,8 +29,10 @@ reconnects, and checks access denial. It logs no factors or resource contents. The factors are discarded on exit, so the remaining encrypted test records cannot be unlocked later. Use a dedicated disposable store for this example. -With a running Docker engine, `npm run test:integration:ipfs` runs the isolated -private distributed-storage fixture. See [distributed storage](DISTRIBUTED_STORAGE.md) +On a native Linux Docker host (including the Ubuntu CI runner), +`npm run test:integration:ipfs` runs the isolated private distributed-storage +fixture. Docker Desktop and remote Docker daemons are not supported by this +fixture. See [distributed storage](DISTRIBUTED_STORAGE.md) for architecture, network restrictions, availability, and coordination limits. ## Configure Azure or AWS later @@ -45,7 +51,8 @@ node --env-file=.env.local --import tsx src/web/server.ts | `file` | `STORAGE_FILE_DIRECTORY` (default `.data/web`) | Local filesystem permissions; single process only | | `azure` | `AZURE_STORAGE_ACCOUNT_URL`, `AZURE_STORAGE_CONTAINER` | `DefaultAzureCredential`: local `az login`, workload identity, or managed identity | | `s3` | `S3_BUCKET`, `AWS_REGION`; optional `STORAGE_PREFIX`, `S3_KMS_KEY_ID` | AWS default credential chain: local SSO profile, workload role, or web identity | -| `ipfs` | Private API URLs, index backend, replication and runtime-secret settings in `.env.example` | Authenticated HTTPS APIs; cloud identity for index; secret-file or Key Vault API authorization | +| `dynamodb` | `DYNAMODB_TABLE`, `AWS_REGION`; optional `STORAGE_PREFIX` | AWS role/SSO credential chain; intended as IPFS's atomic metadata index | +| `ipfs` | Private API URLs, index backend, replication and runtime-secret settings in `.env.example` | Authenticated HTTPS APIs; cloud identity for index; secret-file, Key Vault, or Secrets Manager API authorization | Provision the container/bucket and permissions **before** running. The application never creates cloud resources, changes IAM/RBAC, makes containers public, or @@ -100,8 +107,12 @@ setting `IPFS_API_AUTH_KEYVAULT_SECRET`, `IPFS_CLUSTER_AUTH_KEYVAULT_SECRET`, an `AZURE_KEY_VAULT_URL`. Grant the workload **get only** on those secrets. Alternatively use `IPFS_API_AUTH_FILE`/`IPFS_CLUSTER_AUTH_FILE` pointing to read-only, access-restricted secret mounts outside the checkout. AWS Secrets -Manager or another secret-store integration can supply those mounts. Never -include both sources for the same secret. Secret values must be complete +Manager can supply them directly with `IPFS_API_AUTH_AWS_SECRET_ID` and +`IPFS_CLUSTER_AUTH_AWS_SECRET_ID` plus `AWS_REGION`, using the workload role. +Grant only `secretsmanager:GetSecretValue` on those secret ARNs and the +necessary KMS decrypt permission when their secret encryption requires it. +Another secret-store integration can supply the mounted files. Never +configure more than one source for the same secret. Secret values must be complete single-line authorization headers (for example, a Bearer or Basic value). IPFS swarm keys, cluster secrets, and peer private identities belong in the node operators' secret management, not application configuration or GitHub. @@ -123,8 +134,32 @@ short-lived token; the cloud exchanges it for temporary credentials. No long-lived cloud credential is stored in GitHub. Do not grant the cloud identity to PR/fork workflows, upload environment dumps, or enable SDK request tracing. -After approval, select Azure or S3 in **Actions > Reviewed cloud storage smoke**. +After approval, select Azure, S3, DynamoDB, or `aws-hybrid` in +**Actions > Reviewed cloud storage smoke**. Hybrid mode selects private IPFS +with DynamoDB metadata and synchronous SSE-KMS S3 backups; it additionally +needs the private API URLs and Secrets Manager header-secret ARNs as variables. Private-endpoint-only resources require a hardened runner with network access. Set the repository variable `STORAGE_RUNNER_LABEL` to its approved runner label; the default is `ubuntu-latest`. Account provisioning, trust, network access, and clinical/security review cannot be supplied by this code change. + +## AWS hybrid and recovery + +See [AWS deployment](AWS_HYBRID_STORAGE.md) and +`examples/config/aws-hybrid.env.example`. Kubo persists encrypted content on +EC2-attached encrypted EBS; DynamoDB holds atomic key-to-CID metadata; S3 keeps +immutable application-encrypted backup bytes, additionally protected by SSE-KMS. +Backup failure prevents metadata publication. KMS protects AWS at-rest storage +keys, **not** the patient/clinician dual-unlock factors or reconstructed record key. + +Recovery is explicit, not a read fallback that could hide corruption: + +```sh +node --env-file=.env.local --import tsx examples/restore-content.ts OPAQUE_OBJECT_KEY +``` + +This verifies the backup CID, republishes it to the private cluster, and waits +for replication without modifying metadata. Restore lost DynamoDB metadata +from its separate PITR/backups first; content backups alone cannot recreate the +index or recover lost patient factors. A single EC2/EBS node is still a single +failure domain. diff --git a/examples/config/aws-hybrid.env.example b/examples/config/aws-hybrid.env.example new file mode 100644 index 0000000..6b6cde8 --- /dev/null +++ b/examples/config/aws-hybrid.env.example @@ -0,0 +1,19 @@ +# NONSECRET configuration: fill these using the reviewed AWS stack outputs. +# Copy to an ignored .env.local file; never add actual secret values. +STORAGE_BACKEND=ipfs +STORAGE_EMULATOR=false +IPFS_PRIVATE_NETWORK=true +IPFS_METADATA_BACKEND=dynamodb +IPFS_BACKUP_BACKEND=s3 +AWS_REGION=us-east-1 +DYNAMODB_TABLE=REPLACE_WITH_METADATA_TABLE +S3_BUCKET=REPLACE_WITH_ENCRYPTED_BACKUP_BUCKET +S3_KMS_KEY_ID=REPLACE_WITH_BACKUP_KMS_KEY_ARN +STORAGE_PREFIX=records/ +IPFS_API_URL=https://REPLACE_WITH_PRIVATE_KUBO_API +IPFS_CLUSTER_API_URL=https://REPLACE_WITH_PRIVATE_CLUSTER_API +IPFS_REPLICATION_MIN=2 +IPFS_REPLICATION_MAX=3 +IPFS_TIMEOUT_MS=30000 +IPFS_API_AUTH_AWS_SECRET_ID=REPLACE_WITH_AUTH_HEADER_SECRET_ARN +IPFS_CLUSTER_AUTH_AWS_SECRET_ID=REPLACE_WITH_AUTH_HEADER_SECRET_ARN diff --git a/examples/restore-content.ts b/examples/restore-content.ts new file mode 100644 index 0000000..2969a84 --- /dev/null +++ b/examples/restore-content.ts @@ -0,0 +1,20 @@ +import { + DistributedBlobStore, StorageConfigurationError, createConfiguredBlobStore, +} from "../src/index.js"; + +try { + const key = process.argv[2]; + if (!key || !/^[A-Za-z0-9_-]{32,128}$/.test(key)) { + throw new StorageConfigurationError("Provide one opaque storage object key to restore."); + } + const store = await createConfiguredBlobStore(); + if (!(store instanceof DistributedBlobStore)) { + throw new StorageConfigurationError("Restoration requires STORAGE_BACKEND=ipfs with a configured backup."); + } + await store.restore(key); + console.log("Encrypted content restored and replication policy confirmed. Metadata was not changed."); +} catch (error) { + console.error(error instanceof StorageConfigurationError + ? error.message : "Encrypted content restore failed; no metadata update was attempted."); + process.exitCode = 1; +} diff --git a/infra/aws/runtime/app.env.example b/infra/aws/runtime/app.env.example new file mode 100644 index 0000000..e2e1fb2 --- /dev/null +++ b/infra/aws/runtime/app.env.example @@ -0,0 +1,20 @@ +# Nonsecret configuration only; replace identifiers with reviewed stack outputs. +STORAGE_BACKEND=ipfs +IPFS_METADATA_BACKEND=dynamodb +DYNAMODB_TABLE=REPLACE_WITH_DynamoDbTable +AWS_REGION=REPLACE_WITH_AwsRegion +IPFS_BACKUP_BACKEND=s3 +S3_BUCKET=REPLACE_WITH_S3Bucket +S3_KMS_KEY_ID=REPLACE_WITH_S3KmsKeyId +STORAGE_PREFIX=records/ +IPFS_PRIVATE_NETWORK=true +# Provision private DNS, a trusted TLS certificate and authenticated reverse +# proxy independently. These URLs are NOT generated by CloudFormation. +IPFS_API_URL=https://kubo.internal.example.invalid +IPFS_CLUSTER_API_URL=https://cluster.internal.example.invalid +IPFS_REPLICATION_MIN=2 +IPFS_REPLICATION_MAX=3 +IPFS_API_AUTH_FILE=/run/global-patient-record-secrets/kubo-authorization +IPFS_CLUSTER_AUTH_FILE=/run/global-patient-record-secrets/cluster-authorization +# Never put access keys, factors, private keys, swarm keys or bearer tokens here. +# See docs/AWS_HYBRID_STORAGE.md for external secret-file injection. diff --git a/infra/aws/runtime/direct-api-auth.env.example b/infra/aws/runtime/direct-api-auth.env.example new file mode 100644 index 0000000..a773e68 --- /dev/null +++ b/infra/aws/runtime/direct-api-auth.env.example @@ -0,0 +1,8 @@ +# Alternative auth fragment, NOT a complete app configuration. +# Remove IPFS_API_AUTH_FILE and IPFS_CLUSTER_AUTH_FILE from app.env.example +# before adding these settings. Do not configure multiple sources per setting. +# AWS_REGION remains required in the base app configuration. +# Values below are secret ARNs only; SecretString holds the complete single-line +# Authorization header. Never put header values or AWS access keys here. +IPFS_API_AUTH_AWS_SECRET_ID=REPLACE_WITH_ApiAuthorizationSecretArn +IPFS_CLUSTER_AUTH_AWS_SECRET_ID=REPLACE_WITH_ClusterAuthorizationSecretArn diff --git a/infra/aws/runtime/ipfs-cluster.service b/infra/aws/runtime/ipfs-cluster.service new file mode 100644 index 0000000..a9f11d0 --- /dev/null +++ b/infra/aws/runtime/ipfs-cluster.service @@ -0,0 +1,34 @@ +[Unit] +Description=Global Patient Record Project private IPFS Cluster (approved AMI only) +After=network-online.target kubo.service +Requires=kubo.service +RequiresMountsFor=/srv/ipfs +ConditionPathIsMountPoint=/srv/ipfs +ConditionPathExists=/srv/ipfs/cluster/service.json +ConditionPathExists=/srv/ipfs/cluster/identity.json + +[Service] +Type=simple +User=ipfs +Group=ipfs +Environment=IPFS_CLUSTER_PATH=/srv/ipfs/cluster +ExecStart=/opt/global-patient-record/bin/ipfs-cluster-service daemon +Restart=on-failure +RestartSec=10 +UMask=0077 +NoNewPrivileges=true +PrivateTmp=true +PrivateDevices=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/srv/ipfs/cluster +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true +RestrictSUIDSGID=true +LimitCORE=0 +StandardOutput=null +StandardError=null + +[Install] +WantedBy=multi-user.target diff --git a/infra/aws/runtime/kubo.service b/infra/aws/runtime/kubo.service new file mode 100644 index 0000000..59e0e7f --- /dev/null +++ b/infra/aws/runtime/kubo.service @@ -0,0 +1,36 @@ +[Unit] +Description=Global Patient Record Project private Kubo (approved AMI only) +After=network-online.target +Wants=network-online.target +RequiresMountsFor=/srv/ipfs +ConditionPathIsMountPoint=/srv/ipfs +ConditionPathExists=/srv/ipfs/kubo/config +ConditionPathExists=/srv/ipfs/kubo/swarm.key + +[Service] +Type=simple +User=ipfs +Group=ipfs +Environment=IPFS_PATH=/srv/ipfs/kubo +Environment=LIBP2P_FORCE_PNET=1 +ExecStartPre=/usr/bin/test -s /srv/ipfs/kubo/swarm.key +ExecStart=/opt/global-patient-record/bin/ipfs daemon +Restart=on-failure +RestartSec=10 +UMask=0077 +NoNewPrivileges=true +PrivateTmp=true +PrivateDevices=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/srv/ipfs/kubo +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true +RestrictSUIDSGID=true +LimitCORE=0 +StandardOutput=null +StandardError=null + +[Install] +WantedBy=multi-user.target diff --git a/infra/aws/storage-foundation.json b/infra/aws/storage-foundation.json new file mode 100644 index 0000000..274e7da --- /dev/null +++ b/infra/aws/storage-foundation.json @@ -0,0 +1,564 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Global Patient Record Project: synthetic-only research storage foundation. No runtime bootstrap or public IPFS. Review before manual deployment.", + "Parameters": { + "CreateNode": { + "Type": "String", + "Default": "false", + "AllowedValues": ["false", "true"], + "Description": "Opt in only after the approved AMI and private network/runtime prerequisites are complete." + }, + "VpcId": { + "Type": "String", + "Default": "", + "AllowedPattern": "^$|^vpc-[0-9a-f]+$" + }, + "PrivateSubnetId": { + "Type": "String", + "Default": "", + "AllowedPattern": "^$|^subnet-[0-9a-f]+$" + }, + "AvailabilityZone": { + "Type": "String", + "Default": "", + "Description": "Must exactly match PrivateSubnetId; retained EBS cannot move between AZs." + }, + "ApprovedAmiId": { + "Type": "String", + "Default": "", + "AllowedPattern": "^$|^ami-[0-9a-f]+$", + "Description": "Reviewed x86_64 EBS-root AMI with pinned Kubo/Cluster, host runtime and secret mounting agent already baked; no extra AMI data volumes." + }, + "RootDeviceName": { + "Type": "String", + "Default": "/dev/xvda", + "AllowedValues": ["/dev/xvda", "/dev/sda1"], + "Description": "Must match the approved AMI root device mapping." + }, + "InstanceType": { + "Type": "String", + "Default": "t3.small", + "AllowedValues": ["t3.small", "t3.medium", "m6i.large"] + }, + "DataVolumeGiB": { + "Type": "Number", + "Default": 100, + "MinValue": 20, + "MaxValue": 16384 + }, + "ClientSecurityGroupId": { + "Type": "String", + "Default": "", + "AllowedPattern": "^$|^sg-[0-9a-f]+$", + "Description": "Existing application/operator private-client SG allowed to reach the authenticated HTTPS proxy only." + }, + "EndpointSecurityGroupId": { + "Type": "String", + "Default": "", + "AllowedPattern": "^$|^sg-[0-9a-f]+$", + "Description": "Existing interface VPC endpoint SG; its inbound HTTPS must allow the output NodeSecurityGroupId." + }, + "S3PrefixListId": { + "Type": "String", + "Default": "", + "AllowedPattern": "^$|^pl-[0-9a-f]+$", + "Description": "AWS-managed regional S3 prefix list attached to the subnet gateway endpoint." + }, + "DynamoDbPrefixListId": { + "Type": "String", + "Default": "", + "AllowedPattern": "^$|^pl-[0-9a-f]+$", + "Description": "AWS-managed regional DynamoDB prefix list attached to the subnet gateway endpoint." + }, + "RuntimeSecretArn": { + "Type": "String", + "Default": "", + "AllowedPattern": "^$|^arn:[a-z0-9-]+:secretsmanager:[a-z0-9-]+:[0-9]{12}:secret:[A-Za-z0-9/_+=.@-]+$", + "Description": "Optional existing same-account regional secret ARN, not a secret value. Contains private-network/API bootstrap material, NEVER patient factors or clinical plaintext." + }, + "RuntimeSecretKmsKeyArn": { + "Type": "String", + "Default": "", + "AllowedPattern": "^$|^arn:[a-z0-9-]+:kms:[a-z0-9-]+:[0-9]{12}:key/[a-zA-Z0-9-]+$", + "Description": "Existing customer-managed key for RuntimeSecretArn. Supply both ARN parameters or neither; key policy must permit the node role." + }, + "ApiAuthorizationSecretArn": { + "Type": "String", + "Default": "", + "AllowedPattern": "^$|^arn:[a-z0-9-]+:secretsmanager:[a-z0-9-]+:[0-9]{12}:secret:[A-Za-z0-9/_+=.@-]+$", + "Description": "Optional existing Kubo API secret: SecretString is the complete single-line Authorization header, not JSON." + }, + "ApiAuthorizationSecretKmsKeyArn": { + "Type": "String", + "Default": "", + "AllowedPattern": "^$|^arn:[a-z0-9-]+:kms:[a-z0-9-]+:[0-9]{12}:key/[a-zA-Z0-9-]+$", + "Description": "Optional customer-managed key for ApiAuthorizationSecretArn. Leave empty for the AWS-managed Secrets Manager key." + }, + "ClusterAuthorizationSecretArn": { + "Type": "String", + "Default": "", + "AllowedPattern": "^$|^arn:[a-z0-9-]+:secretsmanager:[a-z0-9-]+:[0-9]{12}:secret:[A-Za-z0-9/_+=.@-]+$", + "Description": "Optional existing Cluster API secret: SecretString is the complete single-line Authorization header, not JSON." + }, + "ClusterAuthorizationSecretKmsKeyArn": { + "Type": "String", + "Default": "", + "AllowedPattern": "^$|^arn:[a-z0-9-]+:kms:[a-z0-9-]+:[0-9]{12}:key/[a-zA-Z0-9-]+$", + "Description": "Optional customer-managed key for ClusterAuthorizationSecretArn. Leave empty for the AWS-managed Secrets Manager key." + } + }, + "Rules": { + "NodePrerequisites": { + "RuleCondition": {"Fn::Equals": [{"Ref": "CreateNode"}, "true"]}, + "Assertions": [ + { + "Assert": {"Fn::And": [ + {"Fn::Not": [{"Fn::Equals": [{"Ref": "VpcId"}, ""]}]}, + {"Fn::Not": [{"Fn::Equals": [{"Ref": "PrivateSubnetId"}, ""]}]}, + {"Fn::Not": [{"Fn::Equals": [{"Ref": "AvailabilityZone"}, ""]}]}, + {"Fn::Not": [{"Fn::Equals": [{"Ref": "ApprovedAmiId"}, ""]}]}, + {"Fn::Not": [{"Fn::Equals": [{"Ref": "ClientSecurityGroupId"}, ""]}]}, + {"Fn::Not": [{"Fn::Equals": [{"Ref": "EndpointSecurityGroupId"}, ""]}]}, + {"Fn::Not": [{"Fn::Equals": [{"Ref": "S3PrefixListId"}, ""]}]}, + {"Fn::Not": [{"Fn::Equals": [{"Ref": "DynamoDbPrefixListId"}, ""]}]} + ]}, + "AssertDescription": "Node creation requires reviewed AMI, matching VPC/subnet/AZ and private client/endpoint/prefix-list networking." + } + ] + }, + "ApiAuthorizationKeyRequiresSecret": { + "Assertions": [{ + "Assert": {"Fn::Or": [ + {"Fn::Equals": [{"Ref": "ApiAuthorizationSecretKmsKeyArn"}, ""]}, + {"Fn::Not": [{"Fn::Equals": [{"Ref": "ApiAuthorizationSecretArn"}, ""]}]} + ]}, + "AssertDescription": "A Kubo authorization secret key ARN requires its secret ARN." + }] + }, + "ClusterAuthorizationKeyRequiresSecret": { + "Assertions": [{ + "Assert": {"Fn::Or": [ + {"Fn::Equals": [{"Ref": "ClusterAuthorizationSecretKmsKeyArn"}, ""]}, + {"Fn::Not": [{"Fn::Equals": [{"Ref": "ClusterAuthorizationSecretArn"}, ""]}]} + ]}, + "AssertDescription": "A Cluster authorization secret key ARN requires its secret ARN." + }] + }, + "SecretKeyPair": { + "Assertions": [{ + "Assert": {"Fn::Or": [ + {"Fn::And": [ + {"Fn::Equals": [{"Ref": "RuntimeSecretArn"}, ""]}, + {"Fn::Equals": [{"Ref": "RuntimeSecretKmsKeyArn"}, ""]} + ]}, + {"Fn::And": [ + {"Fn::Not": [{"Fn::Equals": [{"Ref": "RuntimeSecretArn"}, ""]}]}, + {"Fn::Not": [{"Fn::Equals": [{"Ref": "RuntimeSecretKmsKeyArn"}, ""]}]} + ]} + ]}, + "AssertDescription": "Supply both external runtime-secret ARN and its KMS key ARN, or neither." + }] + } + }, + "Conditions": { + "NodeEnabled": {"Fn::Equals": [{"Ref": "CreateNode"}, "true"]}, + "HasRuntimeSecret": {"Fn::Not": [{"Fn::Equals": [{"Ref": "RuntimeSecretArn"}, ""]}]}, + "HasApiAuthorizationSecret": {"Fn::Not": [{"Fn::Equals": [{"Ref": "ApiAuthorizationSecretArn"}, ""]}]}, + "HasApiAuthorizationSecretKey": {"Fn::Not": [{"Fn::Equals": [{"Ref": "ApiAuthorizationSecretKmsKeyArn"}, ""]}]}, + "HasClusterAuthorizationSecret": {"Fn::Not": [{"Fn::Equals": [{"Ref": "ClusterAuthorizationSecretArn"}, ""]}]}, + "HasClusterAuthorizationSecretKey": {"Fn::Not": [{"Fn::Equals": [{"Ref": "ClusterAuthorizationSecretKmsKeyArn"}, ""]}]} + }, + "Resources": { + "BackupKey": { + "Type": "AWS::KMS::Key", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "Description": "Global Patient Record Project S3 infrastructure encryption, not application record keys", + "EnableKeyRotation": true, + "PendingWindowInDays": 30, + "KeyPolicy": { + "Version": "2012-10-17", + "Statement": [{ + "Sid": "AccountDelegatesThroughIAM", + "Effect": "Allow", + "Principal": {"AWS": {"Fn::Sub": "arn:${AWS::Partition}:iam::${AWS::AccountId}:root"}}, + "Action": "kms:*", + "Resource": "*" + }] + } + } + }, + "MetadataKey": { + "Type": "AWS::KMS::Key", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "Description": "Global Patient Record Project DynamoDB infrastructure encryption", + "EnableKeyRotation": true, + "PendingWindowInDays": 30, + "KeyPolicy": { + "Version": "2012-10-17", + "Statement": [{ + "Sid": "AccountDelegatesThroughIAM", + "Effect": "Allow", + "Principal": {"AWS": {"Fn::Sub": "arn:${AWS::Partition}:iam::${AWS::AccountId}:root"}}, + "Action": "kms:*", + "Resource": "*" + }] + } + } + }, + "VolumeKey": { + "Type": "AWS::KMS::Key", + "Condition": "NodeEnabled", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "Description": "Global Patient Record Project EBS infrastructure encryption", + "EnableKeyRotation": true, + "PendingWindowInDays": 30, + "KeyPolicy": { + "Version": "2012-10-17", + "Statement": [{ + "Sid": "AccountDelegatesThroughIAM", + "Effect": "Allow", + "Principal": {"AWS": {"Fn::Sub": "arn:${AWS::Partition}:iam::${AWS::AccountId}:root"}}, + "Action": "kms:*", + "Resource": "*" + }] + } + } + }, + "MetadataTable": { + "Type": "AWS::DynamoDB::Table", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "AttributeDefinitions": [{"AttributeName": "key", "AttributeType": "S"}], + "KeySchema": [{"AttributeName": "key", "KeyType": "HASH"}], + "BillingMode": "PAY_PER_REQUEST", + "DeletionProtectionEnabled": true, + "PointInTimeRecoverySpecification": {"PointInTimeRecoveryEnabled": true}, + "SSESpecification": { + "SSEEnabled": true, + "SSEType": "KMS", + "KMSMasterKeyId": {"Fn::GetAtt": ["MetadataKey", "Arn"]} + } + } + }, + "BackupBucket": { + "Type": "AWS::S3::Bucket", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "OwnershipControls": {"Rules": [{"ObjectOwnership": "BucketOwnerEnforced"}]}, + "VersioningConfiguration": {"Status": "Enabled"}, + "BucketEncryption": {"ServerSideEncryptionConfiguration": [{ + "BucketKeyEnabled": false, + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "aws:kms", + "KMSMasterKeyID": {"Fn::GetAtt": ["BackupKey", "Arn"]} + } + }]}, + "LifecycleConfiguration": {"Rules": [{ + "Id": "AbortIncompleteUploadsOnly", + "Status": "Enabled", + "AbortIncompleteMultipartUpload": {"DaysAfterInitiation": 7} + }]} + } + }, + "BackupBucketPolicy": { + "Type": "AWS::S3::BucketPolicy", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "Bucket": {"Ref": "BackupBucket"}, + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "DenyNonTLS", + "Effect": "Deny", + "Principal": "*", + "Action": "s3:*", + "Resource": [ + {"Fn::GetAtt": ["BackupBucket", "Arn"]}, + {"Fn::Sub": "${BackupBucket.Arn}/*"} + ], + "Condition": {"Bool": {"aws:SecureTransport": "false"}} + }, + { + "Sid": "RequireExplicitKmsEncryption", + "Effect": "Deny", + "Principal": "*", + "Action": "s3:PutObject", + "Resource": {"Fn::Sub": "${BackupBucket.Arn}/*"}, + "Condition": {"StringNotEquals": {"s3:x-amz-server-side-encryption": "aws:kms"}} + }, + { + "Sid": "RequireBackupKey", + "Effect": "Deny", + "Principal": "*", + "Action": "s3:PutObject", + "Resource": {"Fn::Sub": "${BackupBucket.Arn}/*"}, + "Condition": {"StringNotEquals": { + "s3:x-amz-server-side-encryption-aws-kms-key-id": {"Fn::GetAtt": ["BackupKey", "Arn"]} + }} + } + ] + } + } + }, + "NodeRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Principal": {"Service": "ec2.amazonaws.com"}, + "Action": "sts:AssumeRole" + }] + }, + "Policies": [{ + "PolicyName": "OpaqueStorageOnly", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "MetadataReadAndConditionalWrite", + "Effect": "Allow", + "Action": ["dynamodb:GetItem", "dynamodb:PutItem"], + "Resource": {"Fn::GetAtt": ["MetadataTable", "Arn"]} + }, + { + "Sid": "EncryptedContentBackup", + "Effect": "Allow", + "Action": ["s3:GetObject", "s3:PutObject"], + "Resource": {"Fn::Sub": "${BackupBucket.Arn}/records/*"} + }, + { + "Sid": "BackupMissingKeySemantics", + "Effect": "Allow", + "Action": ["s3:ListBucket"], + "Resource": {"Fn::GetAtt": ["BackupBucket", "Arn"]} + }, + { + "Sid": "BackupEncryptionViaS3", + "Effect": "Allow", + "Action": ["kms:Decrypt", "kms:GenerateDataKey"], + "Resource": {"Fn::GetAtt": ["BackupKey", "Arn"]}, + "Condition": { + "StringEquals": { + "kms:ViaService": {"Fn::Sub": "s3.${AWS::Region}.${AWS::URLSuffix}"}, + "kms:CallerAccount": {"Ref": "AWS::AccountId"} + }, + "StringLike": { + "kms:EncryptionContext:aws:s3:arn": {"Fn::Sub": "${BackupBucket.Arn}/records/*"} + } + } + }, + { + "Sid": "ReadTableKeyViaDynamoDB", + "Effect": "Allow", + "Action": ["kms:Decrypt"], + "Resource": {"Fn::GetAtt": ["MetadataKey", "Arn"]}, + "Condition": { + "StringEquals": { + "kms:ViaService": {"Fn::Sub": "dynamodb.${AWS::Region}.${AWS::URLSuffix}"}, + "kms:CallerAccount": {"Ref": "AWS::AccountId"}, + "kms:EncryptionContext:aws:dynamodb:tableName": {"Ref": "MetadataTable"} + } + } + }, + {"Fn::If": ["HasRuntimeSecret", { + "Sid": "ReadOneExternalRuntimeSecret", + "Effect": "Allow", + "Action": ["secretsmanager:GetSecretValue"], + "Resource": {"Ref": "RuntimeSecretArn"} + }, {"Ref": "AWS::NoValue"}]}, + {"Fn::If": ["HasRuntimeSecret", { + "Sid": "DecryptRuntimeSecretViaSecretsManager", + "Effect": "Allow", + "Action": ["kms:Decrypt"], + "Resource": {"Ref": "RuntimeSecretKmsKeyArn"}, + "Condition": {"StringEquals": { + "kms:ViaService": {"Fn::Sub": "secretsmanager.${AWS::Region}.${AWS::URLSuffix}"}, + "kms:CallerAccount": {"Ref": "AWS::AccountId"}, + "kms:EncryptionContext:SecretARN": {"Ref": "RuntimeSecretArn"} + }} + }, {"Ref": "AWS::NoValue"}]}, + {"Fn::If": ["HasApiAuthorizationSecret", { + "Sid": "ReadKuboAuthorizationSecret", + "Effect": "Allow", + "Action": ["secretsmanager:GetSecretValue"], + "Resource": {"Ref": "ApiAuthorizationSecretArn"} + }, {"Ref": "AWS::NoValue"}]}, + {"Fn::If": ["HasApiAuthorizationSecretKey", { + "Sid": "DecryptKuboAuthorizationViaSecretsManager", + "Effect": "Allow", + "Action": ["kms:Decrypt"], + "Resource": {"Ref": "ApiAuthorizationSecretKmsKeyArn"}, + "Condition": {"StringEquals": { + "kms:ViaService": {"Fn::Sub": "secretsmanager.${AWS::Region}.${AWS::URLSuffix}"}, + "kms:CallerAccount": {"Ref": "AWS::AccountId"}, + "kms:EncryptionContext:SecretARN": {"Ref": "ApiAuthorizationSecretArn"} + }} + }, {"Ref": "AWS::NoValue"}]}, + {"Fn::If": ["HasClusterAuthorizationSecret", { + "Sid": "ReadClusterAuthorizationSecret", + "Effect": "Allow", + "Action": ["secretsmanager:GetSecretValue"], + "Resource": {"Ref": "ClusterAuthorizationSecretArn"} + }, {"Ref": "AWS::NoValue"}]}, + {"Fn::If": ["HasClusterAuthorizationSecretKey", { + "Sid": "DecryptClusterAuthorizationViaSecretsManager", + "Effect": "Allow", + "Action": ["kms:Decrypt"], + "Resource": {"Ref": "ClusterAuthorizationSecretKmsKeyArn"}, + "Condition": {"StringEquals": { + "kms:ViaService": {"Fn::Sub": "secretsmanager.${AWS::Region}.${AWS::URLSuffix}"}, + "kms:CallerAccount": {"Ref": "AWS::AccountId"}, + "kms:EncryptionContext:SecretARN": {"Ref": "ClusterAuthorizationSecretArn"} + }} + }, {"Ref": "AWS::NoValue"}]} + ] + } + }] + } + }, + "NodeProfile": { + "Type": "AWS::IAM::InstanceProfile", + "Condition": "NodeEnabled", + "Properties": {"Roles": [{"Ref": "NodeRole"}]} + }, + "NodeSecurityGroup": { + "Type": "AWS::EC2::SecurityGroup", + "Condition": "NodeEnabled", + "Properties": { + "GroupDescription": "Private authenticated HTTPS only; no public gateway, SSH or raw RPC", + "VpcId": {"Ref": "VpcId"}, + "SecurityGroupIngress": [{ + "IpProtocol": "tcp", + "FromPort": 443, + "ToPort": 443, + "SourceSecurityGroupId": {"Ref": "ClientSecurityGroupId"} + }], + "SecurityGroupEgress": [ + { + "IpProtocol": "tcp", + "FromPort": 443, + "ToPort": 443, + "DestinationSecurityGroupId": {"Ref": "EndpointSecurityGroupId"} + }, + { + "IpProtocol": "tcp", + "FromPort": 443, + "ToPort": 443, + "DestinationPrefixListId": {"Ref": "S3PrefixListId"} + }, + { + "IpProtocol": "tcp", + "FromPort": 443, + "ToPort": 443, + "DestinationPrefixListId": {"Ref": "DynamoDbPrefixListId"} + } + ] + } + }, + "NodeLaunchTemplate": { + "Type": "AWS::EC2::LaunchTemplate", + "Condition": "NodeEnabled", + "Properties": { + "LaunchTemplateData": { + "ImageId": {"Ref": "ApprovedAmiId"}, + "InstanceType": {"Ref": "InstanceType"}, + "IamInstanceProfile": {"Arn": {"Fn::GetAtt": ["NodeProfile", "Arn"]}}, + "MetadataOptions": { + "HttpEndpoint": "enabled", + "HttpTokens": "required", + "HttpPutResponseHopLimit": 1, + "HttpProtocolIpv6": "disabled", + "InstanceMetadataTags": "disabled" + }, + "NetworkInterfaces": [{ + "DeviceIndex": 0, + "AssociatePublicIpAddress": false, + "DeleteOnTermination": true, + "SubnetId": {"Ref": "PrivateSubnetId"}, + "Groups": [{"Ref": "NodeSecurityGroup"}] + }], + "BlockDeviceMappings": [{ + "DeviceName": {"Ref": "RootDeviceName"}, + "Ebs": { + "Encrypted": true, + "KmsKeyId": {"Fn::GetAtt": ["VolumeKey", "Arn"]}, + "VolumeType": "gp3", + "VolumeSize": 32, + "DeleteOnTermination": true + } + }] + } + } + }, + "Node": { + "Type": "AWS::EC2::Instance", + "Condition": "NodeEnabled", + "Properties": { + "AvailabilityZone": {"Ref": "AvailabilityZone"}, + "LaunchTemplate": { + "LaunchTemplateId": {"Ref": "NodeLaunchTemplate"}, + "Version": {"Fn::GetAtt": ["NodeLaunchTemplate", "LatestVersionNumber"]} + }, + "Tags": [{"Key": "Name", "Value": "global-patient-record-research-node"}] + } + }, + "DataVolume": { + "Type": "AWS::EC2::Volume", + "Condition": "NodeEnabled", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "AvailabilityZone": {"Ref": "AvailabilityZone"}, + "Encrypted": true, + "KmsKeyId": {"Fn::GetAtt": ["VolumeKey", "Arn"]}, + "Size": {"Ref": "DataVolumeGiB"}, + "VolumeType": "gp3", + "Tags": [{"Key": "Name", "Value": "global-patient-record-private-ipfs"}] + } + }, + "DataVolumeAttachment": { + "Type": "AWS::EC2::VolumeAttachment", + "Condition": "NodeEnabled", + "Properties": { + "Device": "/dev/sdf", + "InstanceId": {"Ref": "Node"}, + "VolumeId": {"Ref": "DataVolume"} + } + } + }, + "Outputs": { + "StorageBackend": {"Value": "ipfs"}, + "IpfsMetadataBackend": {"Value": "dynamodb"}, + "DynamoDbTable": {"Value": {"Ref": "MetadataTable"}}, + "AwsRegion": {"Value": {"Ref": "AWS::Region"}}, + "IpfsBackupBackend": {"Value": "s3"}, + "S3Bucket": {"Value": {"Ref": "BackupBucket"}}, + "S3KmsKeyId": {"Value": {"Fn::GetAtt": ["BackupKey", "Arn"]}}, + "StoragePrefix": {"Value": "records/"}, + "IpfsPrivateNetwork": {"Value": "true"}, + "NodeRoleArn": {"Value": {"Fn::GetAtt": ["NodeRole", "Arn"]}}, + "MetadataKeyArn": {"Value": {"Fn::GetAtt": ["MetadataKey", "Arn"]}}, + "NodeInstanceId": {"Condition": "NodeEnabled", "Value": {"Ref": "Node"}}, + "NodeSecurityGroupId": {"Condition": "NodeEnabled", "Value": {"Ref": "NodeSecurityGroup"}}, + "DataVolumeId": {"Condition": "NodeEnabled", "Value": {"Ref": "DataVolume"}}, + "VolumeKeyArn": {"Condition": "NodeEnabled", "Value": {"Fn::GetAtt": ["VolumeKey", "Arn"]}} + } +} diff --git a/package-lock.json b/package-lock.json index e37d964..a49819a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,9 @@ "version": "0.1.0", "license": "GPL-3.0-only", "dependencies": { + "@aws-sdk/client-dynamodb": "^3.1131.0", "@aws-sdk/client-s3": "^3.1131.0", + "@aws-sdk/client-secrets-manager": "^3.1131.0", "@azure/identity": "^4.13.3", "@azure/keyvault-secrets": "^4.11.2", "@azure/storage-blob": "^12.33.0", @@ -58,6 +60,27 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/client-dynamodb": { + "version": "3.1131.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/client-dynamodb/-/client-dynamodb-3.1131.0.tgz", + "integrity": "sha512-o0iuLR+wTo6+PnC/IhJL0xdryniu70lU/CGENJWLkm9JbXeMlwwfhNmOsPgvCv1CoWDtKRLSolLduSzkRU3EtQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/credential-provider-node": "^3.972.83", + "@aws-sdk/dynamodb-codec": "^3.973.45", + "@aws-sdk/middleware-endpoint-discovery": "^3.972.30", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/fetch-http-handler": "^5.7.2", + "@smithy/node-http-handler": "^4.11.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/client-s3": { "version": "3.1131.0", "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/client-s3/-/client-s3-3.1131.0.tgz", @@ -80,6 +103,25 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/client-secrets-manager": { + "version": "3.1131.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.1131.0.tgz", + "integrity": "sha512-SdzP8opX5sDyOH8xx9x0prVHfsDqEGLEcLefGdSLOR4tlSjrWfCKqoxfHFsFBbfRxYkaFAkxJHy4H35SBFedZw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/credential-provider-node": "^3.972.83", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/fetch-http-handler": "^5.7.2", + "@smithy/node-http-handler": "^4.11.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/core": { "version": "3.978.0", "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/core/-/core-3.978.0.tgz", @@ -247,6 +289,50 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/dynamodb-codec": { + "version": "3.973.45", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.973.45.tgz", + "integrity": "sha512-WMnbfe3igJy14RRzEnLDW9G2V0WbrXdnp2UYAGYJIgbB1wR33bBle0dmbVnejs6sHJWfP2N1cAAx/QxXnoyK8w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/endpoint-cache": { + "version": "3.972.11", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/endpoint-cache/-/endpoint-cache-3.972.11.tgz", + "integrity": "sha1-DUpS1jB+JA0PQExkn5pYt3NRHQM=", + "license": "Apache-2.0", + "dependencies": { + "mnemonist": "0.38.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-endpoint-discovery": { + "version": "3.972.30", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.972.30.tgz", + "integrity": "sha1-WjkNF9mLCC/RTR8/nJ7KFk7tz6k=", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/endpoint-cache": "^3.972.11", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/middleware-sdk-s3": { "version": "3.972.76", "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.76.tgz", @@ -7423,6 +7509,15 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mnemonist": { + "version": "0.38.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mnemonist/-/mnemonist-0.38.3.tgz", + "integrity": "sha1-Nex5wcH0NXz9ov4mRlnCd1zNfZ0=", + "license": "MIT", + "dependencies": { + "obliterator": "^1.6.1" + } + }, "node_modules/module-details-from-path": { "version": "1.0.4", "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/module-details-from-path/-/module-details-from-path-1.0.4.tgz", @@ -7664,6 +7759,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obliterator": { + "version": "1.6.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/obliterator/-/obliterator-1.6.1.tgz", + "integrity": "sha1-3qA+irgh9sTZaimeF672o6+ZTvM=", + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/on-finished/-/on-finished-2.4.1.tgz", diff --git a/package.json b/package.json index 9d6c577..5e58f23 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "example": "tsx examples/basic-flow.ts", "example:access-denied": "tsx examples/access-denied.ts", "example:storage": "tsx examples/configured-storage.ts", + "storage:restore": "tsx examples/restore-content.ts", "start": "tsx src/web/server.ts", "test": "vitest run", "test:coverage": "vitest run --coverage", @@ -32,7 +33,9 @@ "vitest": "^3.2.4" }, "dependencies": { + "@aws-sdk/client-dynamodb": "^3.1131.0", "@aws-sdk/client-s3": "^3.1131.0", + "@aws-sdk/client-secrets-manager": "^3.1131.0", "@azure/identity": "^4.13.3", "@azure/keyvault-secrets": "^4.11.2", "@azure/storage-blob": "^12.33.0", diff --git a/scripts/run-ipfs-integration.ts b/scripts/run-ipfs-integration.ts index 73296be..361d473 100644 --- a/scripts/run-ipfs-integration.ts +++ b/scripts/run-ipfs-integration.ts @@ -73,6 +73,21 @@ async function volume(name: string): Promise { await docker(["volume", "create", name]); } +async function waitForMembers(cluster: string, expected: number): Promise { + stage = `waiting for ${expected} healthy Cluster members`; + let previous = ""; + await waitUntil(async () => { + const text = await (await request(`${cluster}/peers`)).text(); + const peers = text.trim().split("\n").map((line) => + JSON.parse(line) as { id?: string; error?: string; ipfs?: { id?: string; error?: string } }); + const healthy = peers.filter((peer) => peer.id && !peer.error && peer.ipfs?.id && !peer.ipfs.error).length; + const summary = `Cluster members observed: ${peers.length}; healthy Kubo connections: ${healthy}.`; + if (summary !== previous) console.log(summary); + previous = summary; + return peers.length === expected && healthy === expected; + }); +} + async function node(kind: "kubo" | "cluster", index: number, bootstrap?: string): Promise { if (interrupted) throw new Error("Fixture interrupted."); const name = `${prefix}-${kind}${index}`; @@ -230,20 +245,12 @@ async function main(): Promise { const cluster = await node("cluster", 1); const identity = await (await request(`${cluster}/id`)).json() as { id: string }; const bootstrap = `/dns4/cluster1/tcp/9096/p2p/${identity.id}`; + await waitForMembers(cluster, 1); await node("cluster", 2, bootstrap); + // An HTTP listener is ready before its Raft membership change has committed. + await waitForMembers(cluster, 2); await node("cluster", 3, bootstrap); - stage = "waiting for three healthy Cluster members"; - let membershipSummary = ""; - await waitUntil(async () => { - const text = await (await request(`${cluster}/peers`)).text(); - const peers = text.trim().split("\n").map((line) => - JSON.parse(line) as { id?: string; error?: string; ipfs?: { id?: string; error?: string } }); - const healthy = peers.filter((peer) => peer.id && !peer.error && peer.ipfs?.id && !peer.ipfs.error).length; - const summary = `Cluster members observed: ${peers.length}; healthy Kubo connections: ${healthy}.`; - if (summary !== membershipSummary) console.log(summary); - membershipSummary = summary; - return peers.length === 3 && healthy === 3; - }); + await waitForMembers(cluster, 3); stage = "waiting for three valid Cluster freespace metrics"; let metricSummary = ""; await waitUntil(async () => { diff --git a/src/index.ts b/src/index.ts index ad30c61..1b0e31e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,18 +31,25 @@ export { ObjectNotFoundError, type BlobStore, type StoredObject, -} from "./storage/blob-store.js"; -export { FileBlobStore } from "./storage/file-blob-store.js"; -export { InMemoryBlobStore } from "./storage/in-memory-blob-store.js"; -export { AzureBlobStore } from "./storage/azure-blob-store.js"; -export { S3BlobStore, type S3BlobStoreOptions } from "./storage/s3-blob-store.js"; +} from "./storage/core/blob-store.js"; +export { FileBlobStore } from "./storage/adapters/file/file-blob-store.js"; +export { InMemoryBlobStore } from "./storage/adapters/memory/in-memory-blob-store.js"; +export { AzureBlobStore } from "./storage/adapters/azure/azure-blob-store.js"; +export { S3BlobStore, type S3BlobStoreOptions } from "./storage/adapters/s3/s3-blob-store.js"; +export { + DynamoDbBlobStore, type DynamoDbBlobStoreOptions, +} from "./storage/adapters/dynamodb/dynamodb-blob-store.js"; export { DistributedBlobStore, type DistributedContentStore, -} from "./storage/distributed-blob-store.js"; +} from "./storage/adapters/ipfs/distributed-blob-store.js"; export { IpfsClusterClient, type IpfsClusterClientOptions, -} from "./storage/ipfs-cluster-client.js"; -export { createConfiguredBlobStore } from "./storage/configured-blob-store.js"; +} from "./storage/adapters/ipfs/ipfs-cluster-client.js"; +export { BackedUpContentStore } from "./storage/adapters/ipfs/backed-up-content-store.js"; +export { createConfiguredBlobStore } from "./storage/config/factory.js"; export { StorageConfigurationError, type StorageBackend, type StorageEnvironment, -} from "./storage/configuration.js"; +} from "./storage/config/settings.js"; +export type { + StorageProviderContext, StorageProviderFactory, StorageProviderRegistry, StorageFactoryOptions, +} from "./storage/config/provider.js"; diff --git a/src/service/patient-record-service.ts b/src/service/patient-record-service.ts index 3333cdb..48b4a43 100644 --- a/src/service/patient-record-service.ts +++ b/src/service/patient-record-service.ts @@ -39,7 +39,7 @@ import { ObjectAlreadyExistsError, type BlobStore, type StoredObject, -} from "../storage/blob-store.js"; +} from "../storage/core/blob-store.js"; import { AccessDeniedError, IntegrityError, diff --git a/src/storage/README.md b/src/storage/README.md new file mode 100644 index 0000000..6f0e1f1 --- /dev/null +++ b/src/storage/README.md @@ -0,0 +1,80 @@ +# Pluggable storage + +```text +storage/ + core/ BlobStore contract, errors, serialization validation + config/ Registry/factory, shared settings, runtime secret loading + adapters/ + memory/ In-memory adapter + provider factory + file/ Local-file adapter + provider factory + azure/ Azure SDK adapter + Azure configuration/identity + s3/ S3 SDK adapter + AWS configuration/identity + dynamodb/ Strongly consistent metadata + conditional writes + ipfs/ Distributed adapter, private client, composed index setup +``` + +Every adapter owns its provider-specific construction in `provider.ts`. The +central factory is a registry, not a growing provider-specific switch statement. +Only the selected provider factory (and its declared dependencies) is invoked. +The shared contract and record service have no cloud-SDK dependencies. +The package's `src/index.ts` exports remain stable; callers need not depend on +these internal directory paths. Tests mirror the providers under `test/storage/`. + +## Select an implementation + +Set `STORAGE_BACKEND=file|azure|s3|dynamodb|ipfs|memory` and that provider's nonsecret +settings. The web entry point and configured example use the same factory: + +```ts +import { createConfiguredBlobStore, PatientRecordService } from "./src/index.js"; + +const store = await createConfiguredBlobStore(process.env); +const records = new PatientRecordService(store); +``` + +Switching the setting changes the backend on the next application start. +It does not migrate data, mirror between backends, or change an existing +service instance's storage underneath in-flight operations. + +## Add another provider without changing the service or built-in factory + +Implement `BlobStore` in a new adapter folder. Supply a typed provider factory +through the application's composition root: + +```ts +import { + createConfiguredBlobStore, + type StorageProviderFactory, +} from "./src/index.js"; +import { CustomBlobStore } from "./custom-storage.js"; + +const custom: StorageProviderFactory = ({ environment }) => + new CustomBlobStore(environment.CUSTOM_STORAGE_ENDPOINT); + +const store = await createConfiguredBlobStore(process.env, { + providers: { custom }, +}); +// STORAGE_BACKEND=custom selects it. +``` + +`CustomBlobStore` above is the implementer's adapter, not a built-in class. +Validate its settings and use runtime identity/secret mounts; never store +credentials in source. A provider that composes another provider can call +`context.createStore(name)`; dependency cycles and unknown providers fail closed. +Additional provider names cannot overwrite built-ins. Custom factories are +trusted application code, not modules loaded from arbitrary environment paths. + +Reuse `test/helpers/blob-store-contract.ts` against independently constructed +clients. An adapter must preserve immutable create, atomic CAS, missing-object +semantics and explicit failures. Custom metadata providers for IPFS must be +durable and atomic; registering one does not prove either property. +Keep provider IAM/SDK concerns in its own folder, not the service, shared +contract, or another provider's implementation. + +For the AWS hybrid, select IPFS, `IPFS_METADATA_BACKEND=dynamodb`, and +`IPFS_BACKUP_BACKEND=s3`. The IPFS backup composition consumes only `BlobStore`; +it imports no S3 SDK. Other durable backup/index providers can be supplied +through the same registry. See the AWS runbook for KMS and EC2/EBS requirements. + +See [secure setup](../../docs/STORAGE_QUICKSTART.md) for account-free examples, +integration tests, Key Vault/mounted secrets, and later cloud configuration. diff --git a/src/storage/azure-blob-store.ts b/src/storage/adapters/azure/azure-blob-store.ts similarity index 97% rename from src/storage/azure-blob-store.ts rename to src/storage/adapters/azure/azure-blob-store.ts index f5c914f..c1d675f 100644 --- a/src/storage/azure-blob-store.ts +++ b/src/storage/adapters/azure/azure-blob-store.ts @@ -5,7 +5,7 @@ import { ObjectNotFoundError, type BlobStore, type StoredObject, -} from "./blob-store.js"; +} from "../../core/blob-store.js"; import { assertBlobKey, decodeStoredObject, @@ -14,7 +14,7 @@ import { nextVersion, readBoundedBody, requireEtag, -} from "./storage-validation.js"; +} from "../../core/storage-validation.js"; function azureCode(error: unknown): string | undefined { if (typeof error !== "object" || error === null) return undefined; diff --git a/src/storage/adapters/azure/provider.ts b/src/storage/adapters/azure/provider.ts new file mode 100644 index 0000000..87e626f --- /dev/null +++ b/src/storage/adapters/azure/provider.ts @@ -0,0 +1,30 @@ +import { DefaultAzureCredential } from "@azure/identity"; +import { BlobServiceClient, StorageSharedKeyCredential } from "@azure/storage-blob"; +import type { StorageProviderFactory } from "../../config/provider.js"; +import { StorageConfigurationError, required, storageEndpoint } from "../../config/settings.js"; +import { AzureBlobStore } from "./azure-blob-store.js"; + +export const createStore: StorageProviderFactory = ({ environment: env, emulator }) => { + if (env.AZURE_STORAGE_CONNECTION_STRING || env.AZURE_STORAGE_SAS_TOKEN) { + throw new StorageConfigurationError("Use Azure workload identity rather than connection strings or SAS tokens."); + } + const endpoint = storageEndpoint( + required(env, "AZURE_STORAGE_ACCOUNT_URL"), "AZURE_STORAGE_ACCOUNT_URL", emulator, + ); + const container = required(env, "AZURE_STORAGE_CONTAINER"); + if (!/^[a-z0-9](?:[a-z0-9]|-(?!-)){1,61}[a-z0-9]$/.test(container)) { + throw new StorageConfigurationError("AZURE_STORAGE_CONTAINER must be a valid private container name."); + } + if (emulator && !["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname)) { + throw new StorageConfigurationError("Azure emulator credentials are permitted only on loopback."); + } + const credential = emulator + ? new StorageSharedKeyCredential( + required(env, "AZURE_STORAGE_EMULATOR_ACCOUNT"), required(env, "AZURE_STORAGE_EMULATOR_KEY"), + ) + : new DefaultAzureCredential(); + const client = new BlobServiceClient(endpoint.href, credential, { + retryOptions: { maxTries: 1, tryTimeoutInMs: 30_000 }, + }); + return new AzureBlobStore(client.getContainerClient(container)); +}; diff --git a/src/storage/adapters/dynamodb/dynamodb-blob-store.ts b/src/storage/adapters/dynamodb/dynamodb-blob-store.ts new file mode 100644 index 0000000..3f8df7e --- /dev/null +++ b/src/storage/adapters/dynamodb/dynamodb-blob-store.ts @@ -0,0 +1,146 @@ +import { + GetItemCommand, + PutItemCommand, + type AttributeValue, + type DynamoDBClient, +} from "@aws-sdk/client-dynamodb"; +import { + ConcurrentUpdateError, + ObjectAlreadyExistsError, + ObjectNotFoundError, + type BlobStore, + type StoredObject, +} from "../../core/blob-store.js"; +import { + assertBlobKey, + assertExpectedVersion, + decodeStoredObject, + encodeStoredObject, + nextVersion, +} from "../../core/storage-validation.js"; + +export const MAX_DYNAMODB_PAYLOAD_BYTES = 350 * 1024; + +export interface DynamoDbBlobStoreOptions { + prefix?: string; +} + +function assertPayloadSize(payload: string): void { + if (Buffer.byteLength(payload, "utf8") > MAX_DYNAMODB_PAYLOAD_BYTES) { + throw new Error("Storage object exceeds item size limit."); + } +} + +function conditionFailed(error: unknown): boolean { + return typeof error === "object" && error !== null && + "name" in error && error.name === "ConditionalCheckFailedException"; +} + +function stringAttribute(attribute: AttributeValue | undefined): string { + if (attribute === undefined || attribute === null || typeof attribute.S !== "string" || + Object.keys(attribute).length !== 1) { + throw new Error("Invalid stored item."); + } + return attribute.S; +} + +/** Requires a single-region table with only the string partition key `key`. */ +export class DynamoDbBlobStore implements BlobStore { + readonly #prefix: string; + + public constructor( + private readonly client: DynamoDBClient, + private readonly tableName: string, + options: DynamoDbBlobStoreOptions = {}, + ) { + if (!/^[A-Za-z0-9_.-]{3,255}$/.test(tableName)) { + throw new Error("Invalid storage table."); + } + const prefix = options.prefix ?? ""; + if (prefix.length > 512 || (prefix !== "" && + !/^[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*\/?$/.test(prefix))) { + throw new Error("Invalid storage prefix."); + } + this.#prefix = prefix === "" || prefix.endsWith("/") ? prefix : `${prefix}/`; + } + + public async read(key: string): Promise | undefined> { + assertBlobKey(key); + const qualifiedKey = `${this.#prefix}${key}`; + const response = await this.client.send(new GetItemCommand({ + TableName: this.tableName, + Key: { key: { S: qualifiedKey } }, + ConsistentRead: true, + }), { abortSignal: AbortSignal.timeout(30_000) }); + const item = response.Item; + if (item === undefined) return undefined; + if (item === null || !Object.hasOwn(item, "key") || !Object.hasOwn(item, "version") || + !Object.hasOwn(item, "payload") || stringAttribute(item.key) !== qualifiedKey) { + throw new Error("Invalid stored item."); + } + const rowVersion = item.version?.N; + if (typeof rowVersion !== "string" || !/^[1-9]\d{0,15}$/.test(rowVersion) || + Object.keys(item.version ?? {}).length !== 1) { + throw new Error("Invalid stored item version."); + } + const version = Number(rowVersion); + assertExpectedVersion(version); + const payload = stringAttribute(item.payload); + assertPayloadSize(payload); + const stored = decodeStoredObject(payload); + if (stored.version !== version) throw new Error("Stored item version mismatch."); + return stored; + } + + public async create(key: string, value: unknown): Promise { + assertBlobKey(key); + const payload = encodeStoredObject({ version: 1, value }); + assertPayloadSize(payload); + try { + await this.client.send(new PutItemCommand({ + TableName: this.tableName, + Item: { + key: { S: `${this.#prefix}${key}` }, + version: { N: "1" }, + payload: { S: payload }, + }, + ConditionExpression: "attribute_not_exists(#key)", + ExpressionAttributeNames: { "#key": "key" }, + ReturnValues: "NONE", + ReturnValuesOnConditionCheckFailure: "NONE", + }), { abortSignal: AbortSignal.timeout(30_000) }); + } catch (error) { + if (conditionFailed(error)) throw new ObjectAlreadyExistsError("storage object"); + throw error; + } + } + + public async compareAndSwap(key: string, expectedVersion: number, value: unknown): Promise { + assertBlobKey(key); + const version = nextVersion(expectedVersion); + const payload = encodeStoredObject({ version, value }); + assertPayloadSize(payload); + const current = await this.read(key); + if (current === undefined) throw new ObjectNotFoundError("storage object"); + if (current.version !== expectedVersion) throw new ConcurrentUpdateError("storage object"); + try { + await this.client.send(new PutItemCommand({ + TableName: this.tableName, + Item: { + key: { S: `${this.#prefix}${key}` }, + version: { N: String(version) }, + payload: { S: payload }, + }, + ConditionExpression: "#version = :expected", + ExpressionAttributeNames: { "#version": "version" }, + ExpressionAttributeValues: { ":expected": { N: String(expectedVersion) } }, + ReturnValues: "NONE", + ReturnValuesOnConditionCheckFailure: "NONE", + }), { abortSignal: AbortSignal.timeout(30_000) }); + } catch (error) { + if (conditionFailed(error)) throw new ConcurrentUpdateError("storage object"); + throw error; + } + return version; + } +} diff --git a/src/storage/adapters/dynamodb/provider.ts b/src/storage/adapters/dynamodb/provider.ts new file mode 100644 index 0000000..f8c7931 --- /dev/null +++ b/src/storage/adapters/dynamodb/provider.ts @@ -0,0 +1,34 @@ +import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; +import type { StorageProviderFactory } from "../../config/provider.js"; +import { StorageConfigurationError, required, storageEndpoint } from "../../config/settings.js"; +import { DynamoDbBlobStore } from "./dynamodb-blob-store.js"; + +export const createStore: StorageProviderFactory = ({ environment: env, emulator }) => { + if (env.NODE_TLS_REJECT_UNAUTHORIZED === "0" || + process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0") { + throw new StorageConfigurationError("TLS certificate validation must remain enabled."); + } + const tableName = required(env, "DYNAMODB_TABLE"); + if (!/^[A-Za-z0-9_.-]{3,255}$/.test(tableName)) { + throw new StorageConfigurationError("DYNAMODB_TABLE must be a valid table name."); + } + const endpointValue = env.DYNAMODB_ENDPOINT_URL; + if (emulator && !endpointValue) { + throw new StorageConfigurationError("DynamoDB emulator mode requires a loopback DYNAMODB_ENDPOINT_URL."); + } + const endpoint = endpointValue + ? storageEndpoint(endpointValue, "DYNAMODB_ENDPOINT_URL", emulator) : undefined; + if (endpoint && (endpoint.pathname !== "/" || + (emulator && !["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname)))) { + throw new StorageConfigurationError("DYNAMODB_ENDPOINT_URL must be an endpoint root; emulator mode requires loopback."); + } + const client = new DynamoDBClient({ + region: required(env, "AWS_REGION"), + maxAttempts: 1, + ...(endpoint ? { endpoint: endpoint.href } : {}), + requestHandler: { connectionTimeout: 10_000, requestTimeout: 30_000 }, + }); + return new DynamoDbBlobStore(client, tableName, { + ...(env.STORAGE_PREFIX ? { prefix: env.STORAGE_PREFIX } : {}), + }); +}; diff --git a/src/storage/file-blob-store.ts b/src/storage/adapters/file/file-blob-store.ts similarity index 98% rename from src/storage/file-blob-store.ts rename to src/storage/adapters/file/file-blob-store.ts index 20966aa..ff3e216 100644 --- a/src/storage/file-blob-store.ts +++ b/src/storage/adapters/file/file-blob-store.ts @@ -7,7 +7,7 @@ import { ObjectNotFoundError, type BlobStore, type StoredObject, -} from "./blob-store.js"; +} from "../../core/blob-store.js"; const SAFE_KEY = /^[A-Za-z0-9_-]{32,128}$/; const fileLocks = new Map>(); diff --git a/src/storage/adapters/file/provider.ts b/src/storage/adapters/file/provider.ts new file mode 100644 index 0000000..cd00f5e --- /dev/null +++ b/src/storage/adapters/file/provider.ts @@ -0,0 +1,6 @@ +import { resolve } from "node:path"; +import type { StorageProviderFactory } from "../../config/provider.js"; +import { FileBlobStore } from "./file-blob-store.js"; + +export const createStore: StorageProviderFactory = ({ environment }) => + new FileBlobStore(resolve(environment.STORAGE_FILE_DIRECTORY ?? ".data/web")); diff --git a/src/storage/adapters/ipfs/backed-up-content-store.ts b/src/storage/adapters/ipfs/backed-up-content-store.ts new file mode 100644 index 0000000..ac512c7 --- /dev/null +++ b/src/storage/adapters/ipfs/backed-up-content-store.ts @@ -0,0 +1,66 @@ +import { ObjectAlreadyExistsError, type BlobStore } from "../../core/blob-store.js"; +import { + MAX_DISTRIBUTED_BLOB_BYTES, rawContentCid, validateRawCid, type DistributedContentStore, +} from "./distributed-blob-store.js"; + +/** Backup completion is part of publication, not an asynchronous best-effort task. */ +export class BackedUpContentStore implements DistributedContentStore { + public constructor( + private readonly primary: DistributedContentStore, + private readonly backup: BlobStore, + ) {} + + public async put(input: Uint8Array): Promise { + if (!(input instanceof Uint8Array) || input.byteLength > MAX_DISTRIBUTED_BLOB_BYTES) { + throw new Error("Invalid distributed content size."); + } + const bytes = Uint8Array.from(input); + const cid = await rawContentCid(bytes); + const published = await this.primary.put(bytes); + if (published !== cid) throw new Error("Primary content integrity check failed."); + try { + await this.backup.create(cid, { + format: "ipfs-backup-v1", + cid, + bytes: Buffer.from(bytes).toString("base64"), + }); + } catch (error) { + if (!(error instanceof ObjectAlreadyExistsError)) throw error; + // A duplicate is safe only when the existing immutable backup is intact. + await this.backupBytes(cid); + } + return cid; + } + + public get(cid: string): Promise { + // Do not hide a missing/corrupt primary with an implicit recovery path. + return this.primary.get(validateRawCid(cid)); + } + + public async restore(value: string): Promise { + const cid = validateRawCid(value); + const bytes = await this.backupBytes(cid); + if (await this.primary.put(bytes) !== cid) { + throw new Error("Restored content integrity check failed."); + } + return cid; + } + + private async backupBytes(cid: string): Promise { + const stored = await this.backup.read(cid); + if (!stored || stored.version !== 1) throw new Error("Immutable content backup is unavailable."); + const value = stored.value; + if (typeof value !== "object" || value === null || Array.isArray(value) || + !("format" in value) || value.format !== "ipfs-backup-v1" || + !("cid" in value) || value.cid !== cid || !("bytes" in value) || + typeof value.bytes !== "string" || + value.bytes.length > 4 * Math.ceil(MAX_DISTRIBUTED_BLOB_BYTES / 3)) { + throw new Error("Invalid content backup."); + } + const bytes = Buffer.from(value.bytes, "base64"); + if (bytes.toString("base64") !== value.bytes || await rawContentCid(bytes) !== cid) { + throw new Error("Backup content integrity check failed."); + } + return bytes; + } +} diff --git a/src/storage/distributed-blob-store.ts b/src/storage/adapters/ipfs/distributed-blob-store.ts similarity index 82% rename from src/storage/distributed-blob-store.ts rename to src/storage/adapters/ipfs/distributed-blob-store.ts index a37b2fd..8404ee7 100644 --- a/src/storage/distributed-blob-store.ts +++ b/src/storage/adapters/ipfs/distributed-blob-store.ts @@ -6,13 +6,13 @@ import { ObjectNotFoundError, type BlobStore, type StoredObject, -} from "./blob-store.js"; +} from "../../core/blob-store.js"; import { assertBlobKey, decodeStoredObject, encodeStoredObject, nextVersion, -} from "./storage-validation.js"; +} from "../../core/storage-validation.js"; export const MAX_DISTRIBUTED_BLOB_BYTES = 1024 * 1024 - 1; @@ -20,6 +20,7 @@ export const MAX_DISTRIBUTED_BLOB_BYTES = 1024 * 1024 - 1; export interface DistributedContentStore { put(bytes: Uint8Array): Promise; get(cid: string): Promise; + restore?(cid: string): Promise; } export function validateRawCid(value: unknown): string { @@ -60,13 +61,9 @@ export class DistributedBlobStore implements BlobStore { ) {} public async read(key: string): Promise | undefined> { - assertBlobKey(key); - const stored = await this.metadata.read(key); + const stored = await this.readPointer(key); if (stored === undefined) return undefined; - if (!Number.isSafeInteger(stored.version) || stored.version < 1) { - throw new Error("Invalid storage object version."); - } - const cid = pointer(stored.value); + const { cid } = stored; const bytes = await this.content.get(cid); if (await rawContentCid(bytes) !== cid) { throw new Error("Distributed content integrity check failed."); @@ -82,6 +79,15 @@ export class DistributedBlobStore implements BlobStore { return { version: stored.version, value: decoded.value }; } + public async restore(key: string): Promise { + if (!this.content.restore) throw new Error("Distributed storage has no configured backup restore capability."); + const stored = await this.readPointer(key); + if (!stored) throw new ObjectNotFoundError("storage object"); + if (await this.content.restore(stored.cid) !== stored.cid) { + throw new Error("Restored content integrity check failed."); + } + } + public async create(key: string, value: unknown): Promise { assertBlobKey(key); const cid = await this.upload(value); @@ -110,4 +116,14 @@ export class DistributedBlobStore implements BlobStore { if (actual !== expected) throw new Error("Distributed content integrity check failed."); return actual; } + + private async readPointer(key: string): Promise<{ version: number; cid: string } | undefined> { + assertBlobKey(key); + const stored = await this.metadata.read(key); + if (!stored) return undefined; + if (!Number.isSafeInteger(stored.version) || stored.version < 1) { + throw new Error("Invalid storage object version."); + } + return { version: stored.version, cid: pointer(stored.value) }; + } } diff --git a/src/storage/ipfs-cluster-client.ts b/src/storage/adapters/ipfs/ipfs-cluster-client.ts similarity index 100% rename from src/storage/ipfs-cluster-client.ts rename to src/storage/adapters/ipfs/ipfs-cluster-client.ts diff --git a/src/storage/adapters/ipfs/provider.ts b/src/storage/adapters/ipfs/provider.ts new file mode 100644 index 0000000..c064c83 --- /dev/null +++ b/src/storage/adapters/ipfs/provider.ts @@ -0,0 +1,57 @@ +import type { StorageProviderFactory } from "../../config/provider.js"; +import { + StorageConfigurationError, flag, positiveInteger, providerName, required, storageEndpoint, +} from "../../config/settings.js"; +import { loadRuntimeSecret } from "../../config/secrets.js"; +import { DistributedBlobStore, type DistributedContentStore } from "./distributed-blob-store.js"; +import { IpfsClusterClient } from "./ipfs-cluster-client.js"; +import { BackedUpContentStore } from "./backed-up-content-store.js"; + +export const createStore: StorageProviderFactory = async ({ + environment: env, emulator, createStore: createDependency, +}) => { + if (!flag(env, "IPFS_PRIVATE_NETWORK")) { + throw new StorageConfigurationError("IPFS_PRIVATE_NETWORK=true must acknowledge a separately provisioned private swarm."); + } + const metadataBackend = providerName(env, "IPFS_METADATA_BACKEND", "azure"); + if (metadataBackend === "ipfs" || metadataBackend === "memory" || + (metadataBackend === "file" && !emulator)) { + throw new StorageConfigurationError("IPFS metadata requires a durable atomic provider; file is allowed only in local emulator mode."); + } + const kubo = storageEndpoint(required(env, "IPFS_API_URL"), "IPFS_API_URL", emulator); + const cluster = storageEndpoint(required(env, "IPFS_CLUSTER_API_URL"), "IPFS_CLUSTER_API_URL", emulator); + if (emulator && [kubo, cluster].some((url) => + !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) { + throw new StorageConfigurationError("IPFS emulator mode requires loopback endpoints."); + } + const min = positiveInteger(env, "IPFS_REPLICATION_MIN", 2); + const max = positiveInteger(env, "IPFS_REPLICATION_MAX", 3); + if (max < min || (!emulator && min < 2)) { + throw new StorageConfigurationError("IPFS replication requires max >= min and at least two copies outside emulator mode."); + } + const apiAuth = await loadRuntimeSecret(env, "IPFS_API_AUTH"); + const clusterAuth = await loadRuntimeSecret(env, "IPFS_CLUSTER_AUTH"); + if (!emulator && (!apiAuth || !clusterAuth)) { + throw new StorageConfigurationError("Remote IPFS APIs require runtime authorization from secret files, Key Vault, or Secrets Manager."); + } + let content: DistributedContentStore = new IpfsClusterClient({ + kuboUrl: kubo.href, + clusterUrl: cluster.href, + minReplicas: min, + maxReplicas: max, + timeoutMs: positiveInteger(env, "IPFS_TIMEOUT_MS", 30_000), + allowInsecureLocal: emulator, + ...(apiAuth ? { kuboAuthorization: apiAuth } : {}), + ...(clusterAuth ? { clusterAuthorization: clusterAuth } : {}), + }); + if (env.IPFS_BACKUP_BACKEND !== undefined) { + const backupBackend = providerName(env, "IPFS_BACKUP_BACKEND"); + if (backupBackend === "ipfs" || backupBackend === "dynamodb" || + (!emulator && ["file", "memory"].includes(backupBackend))) { + throw new StorageConfigurationError("IPFS backups require a separate durable object store."); + } + if (!emulator && backupBackend === "s3") required(env, "S3_KMS_KEY_ID"); + content = new BackedUpContentStore(content, await createDependency(backupBackend)); + } + return new DistributedBlobStore(await createDependency(metadataBackend), content); +}; diff --git a/src/storage/in-memory-blob-store.ts b/src/storage/adapters/memory/in-memory-blob-store.ts similarity index 97% rename from src/storage/in-memory-blob-store.ts rename to src/storage/adapters/memory/in-memory-blob-store.ts index e1fa1cc..34f1407 100644 --- a/src/storage/in-memory-blob-store.ts +++ b/src/storage/adapters/memory/in-memory-blob-store.ts @@ -4,7 +4,7 @@ import { ObjectNotFoundError, type BlobStore, type StoredObject, -} from "./blob-store.js"; +} from "../../core/blob-store.js"; export class InMemoryBlobStore implements BlobStore { readonly #objects = new Map(); diff --git a/src/storage/adapters/memory/provider.ts b/src/storage/adapters/memory/provider.ts new file mode 100644 index 0000000..788cf5e --- /dev/null +++ b/src/storage/adapters/memory/provider.ts @@ -0,0 +1,4 @@ +import type { StorageProviderFactory } from "../../config/provider.js"; +import { InMemoryBlobStore } from "./in-memory-blob-store.js"; + +export const createStore: StorageProviderFactory = () => new InMemoryBlobStore(); diff --git a/src/storage/adapters/s3/provider.ts b/src/storage/adapters/s3/provider.ts new file mode 100644 index 0000000..46c2dd0 --- /dev/null +++ b/src/storage/adapters/s3/provider.ts @@ -0,0 +1,32 @@ +import { S3Client } from "@aws-sdk/client-s3"; +import type { StorageProviderFactory } from "../../config/provider.js"; +import { StorageConfigurationError, required, storageEndpoint } from "../../config/settings.js"; +import { S3BlobStore } from "./s3-blob-store.js"; + +export const createStore: StorageProviderFactory = ({ environment: env, emulator }) => { + const bucket = required(env, "S3_BUCKET"); + if (!/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(bucket) || + bucket.includes("..") || /^\d+\.\d+\.\d+\.\d+$/.test(bucket)) { + throw new StorageConfigurationError("S3_BUCKET must be a valid general-purpose bucket name."); + } + const endpointValue = env.S3_ENDPOINT_URL; + if (emulator && !endpointValue) { + throw new StorageConfigurationError("S3 emulator mode requires a loopback S3_ENDPOINT_URL."); + } + const endpoint = endpointValue + ? storageEndpoint(endpointValue, "S3_ENDPOINT_URL", emulator) : undefined; + if (endpoint && (endpoint.pathname !== "/" || + (emulator && !["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname)))) { + throw new StorageConfigurationError("S3_ENDPOINT_URL must be an endpoint root; emulator mode requires loopback."); + } + const client = new S3Client({ + region: required(env, "AWS_REGION"), + maxAttempts: 1, + ...(endpoint ? { endpoint: endpoint.href, forcePathStyle: true } : {}), + requestHandler: { connectionTimeout: 10_000, requestTimeout: 30_000 }, + }); + return new S3BlobStore(client, bucket, { + ...(env.STORAGE_PREFIX ? { prefix: env.STORAGE_PREFIX } : {}), + ...(env.S3_KMS_KEY_ID ? { kmsKeyId: env.S3_KMS_KEY_ID } : {}), + }); +}; diff --git a/src/storage/s3-blob-store.ts b/src/storage/adapters/s3/s3-blob-store.ts similarity index 98% rename from src/storage/s3-blob-store.ts rename to src/storage/adapters/s3/s3-blob-store.ts index 2269782..d77e76b 100644 --- a/src/storage/s3-blob-store.ts +++ b/src/storage/adapters/s3/s3-blob-store.ts @@ -5,7 +5,7 @@ import { ObjectNotFoundError, type BlobStore, type StoredObject, -} from "./blob-store.js"; +} from "../../core/blob-store.js"; import { assertBlobKey, decodeStoredObject, @@ -14,7 +14,7 @@ import { nextVersion, readBoundedBody, requireEtag, -} from "./storage-validation.js"; +} from "../../core/storage-validation.js"; export interface S3BlobStoreOptions { prefix?: string; diff --git a/src/storage/config/factory.ts b/src/storage/config/factory.ts new file mode 100644 index 0000000..b171fba --- /dev/null +++ b/src/storage/config/factory.ts @@ -0,0 +1,44 @@ +import type { BlobStore } from "../core/blob-store.js"; +import type { StorageFactoryOptions, StorageProviderRegistry } from "./provider.js"; +import { StorageConfigurationError, flag, providerName, type StorageEnvironment } from "./settings.js"; + +const builtinProviders: StorageProviderRegistry = Object.freeze({ + memory: async (context) => (await import("../adapters/memory/provider.js")).createStore(context), + file: async (context) => (await import("../adapters/file/provider.js")).createStore(context), + azure: async (context) => (await import("../adapters/azure/provider.js")).createStore(context), + s3: async (context) => (await import("../adapters/s3/provider.js")).createStore(context), + dynamodb: async (context) => (await import("../adapters/dynamodb/provider.js")).createStore(context), + ipfs: async (context) => (await import("../adapters/ipfs/provider.js")).createStore(context), +}); + +export async function createConfiguredBlobStore( + env: StorageEnvironment = process.env, + options: StorageFactoryOptions = {}, +): Promise { + if (env.NODE_TLS_REJECT_UNAUTHORIZED === "0" || + process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0") { + throw new StorageConfigurationError("TLS certificate validation must remain enabled."); + } + const environment = Object.freeze({ ...env }); + const emulator = flag(environment, "STORAGE_EMULATOR"); + for (const [name, factory] of Object.entries(options.providers ?? {})) { + providerName({ STORAGE_BACKEND: name }); + if (Object.hasOwn(builtinProviders, name) || typeof factory !== "function") { + throw new StorageConfigurationError("Additional providers must be functions with distinct non-builtin names."); + } + } + const providers = Object.freeze({ ...builtinProviders, ...options.providers }); + const create = async (name: string, ancestors: ReadonlySet): Promise => { + if (!Object.hasOwn(providers, name)) { + throw new StorageConfigurationError("Unknown storage backend. Select a built-in or register an additional provider."); + } + if (ancestors.has(name)) throw new StorageConfigurationError("Storage provider dependency cycle."); + const factory = providers[name]!; + return factory({ + environment, + emulator, + createStore: (dependency) => create(dependency, new Set([...ancestors, name])), + }); + }; + return create(providerName(environment), new Set()); +} diff --git a/src/storage/config/provider.ts b/src/storage/config/provider.ts new file mode 100644 index 0000000..5725e87 --- /dev/null +++ b/src/storage/config/provider.ts @@ -0,0 +1,18 @@ +import type { BlobStore } from "../core/blob-store.js"; +import type { StorageEnvironment } from "./settings.js"; + +export interface StorageProviderContext { + readonly environment: StorageEnvironment; + readonly emulator: boolean; + createStore(provider: string): Promise; +} + +export type StorageProviderFactory = + (context: StorageProviderContext) => BlobStore | Promise; + +export type StorageProviderRegistry = Readonly>; + +export interface StorageFactoryOptions { + /** Additional providers; built-in names cannot be overridden accidentally. */ + providers?: StorageProviderRegistry; +} diff --git a/src/storage/config/secrets.ts b/src/storage/config/secrets.ts new file mode 100644 index 0000000..de77cac --- /dev/null +++ b/src/storage/config/secrets.ts @@ -0,0 +1,73 @@ +import { open } from "node:fs/promises"; +import { + StorageConfigurationError, required, storageEndpoint, type StorageEnvironment, +} from "./settings.js"; + +// Secrets are retrieved at runtime, never encoded in the configuration file. +export async function loadRuntimeSecret( + env: StorageEnvironment, + setting: string, +): Promise { + const file = env[`${setting}_FILE`]; + const name = env[`${setting}_KEYVAULT_SECRET`]; + const awsSecret = env[`${setting}_AWS_SECRET_ID`]; + if (env[setting] !== undefined) { + throw new StorageConfigurationError( + `Use a mounted file, Key Vault, or AWS Secrets Manager reference for ${setting}, not an inline secret.`, + ); + } + if ([file, name, awsSecret].filter(Boolean).length > 1) { + throw new StorageConfigurationError(`Choose one runtime secret source for ${setting}.`); + } + if (!file && !name && !awsSecret) return undefined; + let secret: string | undefined; + if (file) { + const handle = await open(file, "r"); + try { + const stat = await handle.stat(); + if (!stat.isFile() || stat.size > 16_384) { + throw new StorageConfigurationError(`${setting} must be a small regular secret file.`); + } + secret = (await handle.readFile("utf8")).trim(); + } finally { + await handle.close(); + } + } else if (awsSecret) { + if (awsSecret.length > 2048 || /[\s\x00-\x1f\x7f]/.test(awsSecret)) { + throw new StorageConfigurationError(`${setting}_AWS_SECRET_ID is invalid.`); + } + const { SecretsManagerClient, GetSecretValueCommand } = await import("@aws-sdk/client-secrets-manager"); + const client = new SecretsManagerClient({ + region: required(env, "AWS_REGION"), + maxAttempts: 1, + requestHandler: { connectionTimeout: 10_000, requestTimeout: 30_000 }, + }); + try { + secret = (await client.send(new GetSecretValueCommand({ SecretId: awsSecret }), { + abortSignal: AbortSignal.timeout(30_000), + })).SecretString?.trim(); + } finally { + client.destroy(); + } + } else { + if (!name || !/^[A-Za-z0-9-]{1,127}$/.test(name)) { + throw new StorageConfigurationError(`${setting}_KEYVAULT_SECRET is invalid.`); + } + const vault = storageEndpoint(required(env, "AZURE_KEY_VAULT_URL"), "AZURE_KEY_VAULT_URL"); + const vaultSuffixes = [".vault.azure.net", ".vault.usgovcloudapi.net", ".vault.azure.cn"]; + if (vault.pathname !== "/" || (vault.port && vault.port !== "443") || + !vaultSuffixes.some((suffix) => vault.hostname.endsWith(suffix))) { + throw new StorageConfigurationError("AZURE_KEY_VAULT_URL must identify an Azure vault root."); + } + const { DefaultAzureCredential } = await import("@azure/identity"); + const { SecretClient } = await import("@azure/keyvault-secrets"); + const client = new SecretClient(vault.href, new DefaultAzureCredential(), { + retryOptions: { maxRetries: 2 }, + }); + secret = (await client.getSecret(name)).value?.trim(); + } + if (!secret || secret.length > 16_384 || /[\r\n]/.test(secret)) { + throw new StorageConfigurationError(`${setting} must contain a nonempty single-line secret.`); + } + return secret; +} diff --git a/src/storage/config/settings.ts b/src/storage/config/settings.ts new file mode 100644 index 0000000..80f6230 --- /dev/null +++ b/src/storage/config/settings.ts @@ -0,0 +1,68 @@ +export type StorageEnvironment = Readonly>; +export type StorageBackend = "memory" | "file" | "azure" | "s3" | "dynamodb" | "ipfs"; + +export class StorageConfigurationError extends Error { + public constructor(message: string) { + super(message); + this.name = "StorageConfigurationError"; + } +} + +export function required(env: StorageEnvironment, name: string): string { + const value = env[name]?.trim(); + if (!value) { + throw new StorageConfigurationError(`Set ${name} before selecting this backend.`); + } + return value; +} + +export function flag(env: StorageEnvironment, name: string): boolean { + const value = env[name]; + if (value === undefined || value === "false") return false; + if (value === "true") return true; + throw new StorageConfigurationError(`${name} must be true or false.`); +} + +export function positiveInteger( + env: StorageEnvironment, + name: string, + fallback: number, +): number { + if (env[name] === undefined) return fallback; + const value = required(env, name); + if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value))) { + throw new StorageConfigurationError(`${name} must be a positive safe integer.`); + } + return Number(value); +} + +export function providerName( + env: StorageEnvironment, + name = "STORAGE_BACKEND", + fallback = "file", +): string { + const value = env[name] ?? fallback; + if (/^[a-z][a-z0-9-]{0,63}$/.test(value)) return value; + throw new StorageConfigurationError(`${name} must be a lowercase provider name.`); +} + +export function storageEndpoint( + value: string, + name: string, + allowLocalHttp = false, +): URL { + let url: URL; + try { + url = new URL(value); + } catch { + throw new StorageConfigurationError(`${name} must be a valid endpoint URL.`); + } + const local = ["127.0.0.1", "[::1]", "localhost"].includes(url.hostname); + if (url.username || url.password || url.search || url.hash || + (url.protocol !== "https:" && !(allowLocalHttp && local && url.protocol === "http:"))) { + throw new StorageConfigurationError( + `${name} requires HTTPS without embedded credentials, query, or fragment; explicit emulator mode permits loopback HTTP only.`, + ); + } + return url; +} diff --git a/src/storage/configuration.ts b/src/storage/configuration.ts deleted file mode 100644 index 065a366..0000000 --- a/src/storage/configuration.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { open } from "node:fs/promises"; -import { DefaultAzureCredential } from "@azure/identity"; -import { SecretClient } from "@azure/keyvault-secrets"; - -export type StorageEnvironment = Readonly>; -export type StorageBackend = "memory" | "file" | "azure" | "s3" | "ipfs"; - -export class StorageConfigurationError extends Error { - public constructor(message: string) { - super(message); - this.name = "StorageConfigurationError"; - } -} - -export function required(env: StorageEnvironment, name: string): string { - const value = env[name]?.trim(); - if (!value) { - throw new StorageConfigurationError(`Set ${name} before selecting this backend.`); - } - return value; -} - -export function flag(env: StorageEnvironment, name: string): boolean { - const value = env[name]; - if (value === undefined || value === "false") return false; - if (value === "true") return true; - throw new StorageConfigurationError(`${name} must be true or false.`); -} - -export function positiveInteger( - env: StorageEnvironment, - name: string, - fallback: number, -): number { - if (env[name] === undefined) return fallback; - const value = required(env, name); - if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value))) { - throw new StorageConfigurationError(`${name} must be a positive safe integer.`); - } - return Number(value); -} - -export function storageBackend( - env: StorageEnvironment, - name = "STORAGE_BACKEND", - fallback: StorageBackend = "file", -): StorageBackend { - const value = env[name] ?? fallback; - if (value === "memory" || value === "file" || value === "azure" || - value === "s3" || value === "ipfs") return value; - throw new StorageConfigurationError(`${name} must be memory, file, azure, s3, or ipfs.`); -} - -export function storageEndpoint( - value: string, - name: string, - allowLocalHttp = false, -): URL { - let url: URL; - try { - url = new URL(value); - } catch { - throw new StorageConfigurationError(`${name} must be a valid endpoint URL.`); - } - const local = ["127.0.0.1", "[::1]", "localhost"].includes(url.hostname); - if (url.username || url.password || url.search || url.hash || - (url.protocol !== "https:" && !(allowLocalHttp && local && url.protocol === "http:"))) { - throw new StorageConfigurationError( - `${name} requires HTTPS without embedded credentials, query, or fragment; explicit emulator mode permits loopback HTTP only.`, - ); - } - return url; -} - -// Secrets are retrieved at runtime, never encoded in the configuration file. -export async function loadRuntimeSecret( - env: StorageEnvironment, - setting: string, -): Promise { - const file = env[`${setting}_FILE`]; - const name = env[`${setting}_KEYVAULT_SECRET`]; - if (env[setting] !== undefined) { - throw new StorageConfigurationError( - `Use ${setting}_FILE or ${setting}_KEYVAULT_SECRET, not an inline secret.`, - ); - } - if (file && name) { - throw new StorageConfigurationError(`Choose one runtime secret source for ${setting}.`); - } - if (!file && !name) return undefined; - let secret: string | undefined; - if (file) { - const handle = await open(file, "r"); - try { - const stat = await handle.stat(); - if (!stat.isFile() || stat.size > 16_384) { - throw new StorageConfigurationError(`${setting} must be a small regular secret file.`); - } - secret = (await handle.readFile("utf8")).trim(); - } finally { - await handle.close(); - } - } else { - if (!name || !/^[A-Za-z0-9-]{1,127}$/.test(name)) { - throw new StorageConfigurationError(`${setting}_KEYVAULT_SECRET is invalid.`); - } - const vault = storageEndpoint(required(env, "AZURE_KEY_VAULT_URL"), "AZURE_KEY_VAULT_URL"); - const vaultSuffixes = [".vault.azure.net", ".vault.usgovcloudapi.net", ".vault.azure.cn"]; - if (vault.pathname !== "/" || (vault.port && vault.port !== "443") || - !vaultSuffixes.some((suffix) => vault.hostname.endsWith(suffix))) { - throw new StorageConfigurationError("AZURE_KEY_VAULT_URL must identify an Azure vault root."); - } - const client = new SecretClient(vault.href, new DefaultAzureCredential(), { - retryOptions: { maxRetries: 2 }, - }); - secret = (await client.getSecret(name)).value?.trim(); - } - if (!secret || secret.length > 16_384 || /[\r\n]/.test(secret)) { - throw new StorageConfigurationError(`${setting} must contain a nonempty single-line secret.`); - } - return secret; -} diff --git a/src/storage/configured-blob-store.ts b/src/storage/configured-blob-store.ts deleted file mode 100644 index c50d9b4..0000000 --- a/src/storage/configured-blob-store.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { resolve } from "node:path"; -import { S3Client } from "@aws-sdk/client-s3"; -import { DefaultAzureCredential } from "@azure/identity"; -import { BlobServiceClient, StorageSharedKeyCredential } from "@azure/storage-blob"; -import type { BlobStore } from "./blob-store.js"; -import { FileBlobStore } from "./file-blob-store.js"; -import { InMemoryBlobStore } from "./in-memory-blob-store.js"; -import { AzureBlobStore } from "./azure-blob-store.js"; -import { S3BlobStore } from "./s3-blob-store.js"; -import { DistributedBlobStore } from "./distributed-blob-store.js"; -import { IpfsClusterClient } from "./ipfs-cluster-client.js"; -import { - StorageConfigurationError, - flag, - loadRuntimeSecret, - positiveInteger, - required, - storageBackend, - storageEndpoint, - type StorageBackend, - type StorageEnvironment, -} from "./configuration.js"; - -export async function createConfiguredBlobStore( - env: StorageEnvironment = process.env, -): Promise { - if (env.NODE_TLS_REJECT_UNAUTHORIZED === "0" || - process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0") { - throw new StorageConfigurationError("TLS certificate validation must remain enabled."); - } - const emulator = flag(env, "STORAGE_EMULATOR"); - const backend = storageBackend(env); - - const create = (selected: Exclude): BlobStore => { - switch (selected) { - case "memory": return new InMemoryBlobStore(); - case "file": return new FileBlobStore(resolve(env.STORAGE_FILE_DIRECTORY ?? ".data/web")); - case "azure": { - if (env.AZURE_STORAGE_CONNECTION_STRING || env.AZURE_STORAGE_SAS_TOKEN) { - throw new StorageConfigurationError("Use Azure workload identity rather than connection strings or SAS tokens."); - } - const endpoint = storageEndpoint( - required(env, "AZURE_STORAGE_ACCOUNT_URL"), "AZURE_STORAGE_ACCOUNT_URL", emulator, - ); - const container = required(env, "AZURE_STORAGE_CONTAINER"); - if (!/^[a-z0-9](?:[a-z0-9]|-(?!-)){1,61}[a-z0-9]$/.test(container)) { - throw new StorageConfigurationError("AZURE_STORAGE_CONTAINER must be a valid private container name."); - } - if (emulator && !["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname)) { - throw new StorageConfigurationError("Azure emulator credentials are permitted only on loopback."); - } - const credential = emulator - ? new StorageSharedKeyCredential( - required(env, "AZURE_STORAGE_EMULATOR_ACCOUNT"), - required(env, "AZURE_STORAGE_EMULATOR_KEY"), - ) - : new DefaultAzureCredential(); - const client = new BlobServiceClient(endpoint.href, credential, { - retryOptions: { maxTries: 1, tryTimeoutInMs: 30_000 }, - }); - return new AzureBlobStore(client.getContainerClient(container)); - } - case "s3": { - const bucket = required(env, "S3_BUCKET"); - if (!/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(bucket) || - bucket.includes("..") || /^\d+\.\d+\.\d+\.\d+$/.test(bucket)) { - throw new StorageConfigurationError("S3_BUCKET must be a valid general-purpose bucket name."); - } - const endpointValue = env.S3_ENDPOINT_URL; - if (emulator && !endpointValue) { - throw new StorageConfigurationError("S3 emulator mode requires a loopback S3_ENDPOINT_URL."); - } - const endpoint = endpointValue - ? storageEndpoint(endpointValue, "S3_ENDPOINT_URL", emulator) : undefined; - if (endpoint && (endpoint.pathname !== "/" || - (emulator && !["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname)))) { - throw new StorageConfigurationError("S3_ENDPOINT_URL must be an endpoint root; emulator mode requires loopback."); - } - const client = new S3Client({ - region: required(env, "AWS_REGION"), - maxAttempts: 1, - ...(endpoint ? { endpoint: endpoint.href, forcePathStyle: true } : {}), - requestHandler: { connectionTimeout: 10_000, requestTimeout: 30_000 }, - }); - return new S3BlobStore(client, bucket, { - ...(env.STORAGE_PREFIX ? { prefix: env.STORAGE_PREFIX } : {}), - ...(env.S3_KMS_KEY_ID ? { kmsKeyId: env.S3_KMS_KEY_ID } : {}), - }); - } - } - }; - - if (backend !== "ipfs") return create(backend); - if (!flag(env, "IPFS_PRIVATE_NETWORK")) { - throw new StorageConfigurationError("IPFS_PRIVATE_NETWORK=true must acknowledge a separately provisioned private swarm."); - } - const metadataBackend = storageBackend(env, "IPFS_METADATA_BACKEND", "azure"); - if (metadataBackend === "ipfs" || metadataBackend === "memory" || - (metadataBackend === "file" && !emulator)) { - throw new StorageConfigurationError("IPFS metadata requires azure or s3; file is allowed only in local emulator mode."); - } - const kubo = storageEndpoint(required(env, "IPFS_API_URL"), "IPFS_API_URL", emulator); - const cluster = storageEndpoint(required(env, "IPFS_CLUSTER_API_URL"), "IPFS_CLUSTER_API_URL", emulator); - if (emulator && [kubo, cluster].some((url) => - !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) { - throw new StorageConfigurationError("IPFS emulator mode requires loopback endpoints."); - } - const min = positiveInteger(env, "IPFS_REPLICATION_MIN", 2); - const max = positiveInteger(env, "IPFS_REPLICATION_MAX", 3); - if (max < min || (!emulator && min < 2)) { - throw new StorageConfigurationError("IPFS replication requires max >= min and at least two copies outside emulator mode."); - } - const apiAuth = await loadRuntimeSecret(env, "IPFS_API_AUTH"); - const clusterAuth = await loadRuntimeSecret(env, "IPFS_CLUSTER_AUTH"); - if (!emulator && (!apiAuth || !clusterAuth)) { - throw new StorageConfigurationError("Remote IPFS APIs require runtime authorization from secret files or Key Vault."); - } - const content = new IpfsClusterClient({ - kuboUrl: kubo.href, - clusterUrl: cluster.href, - minReplicas: min, - maxReplicas: max, - timeoutMs: positiveInteger(env, "IPFS_TIMEOUT_MS", 30_000), - allowInsecureLocal: emulator, - ...(apiAuth ? { kuboAuthorization: apiAuth } : {}), - ...(clusterAuth ? { clusterAuthorization: clusterAuth } : {}), - }); - return new DistributedBlobStore(create(metadataBackend), content); -} diff --git a/src/storage/blob-store.ts b/src/storage/core/blob-store.ts similarity index 100% rename from src/storage/blob-store.ts rename to src/storage/core/blob-store.ts diff --git a/src/storage/storage-validation.ts b/src/storage/core/storage-validation.ts similarity index 100% rename from src/storage/storage-validation.ts rename to src/storage/core/storage-validation.ts diff --git a/test/configured-blob-store.spec.ts b/test/configured-blob-store.spec.ts deleted file mode 100644 index 6302af0..0000000 --- a/test/configured-blob-store.spec.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { - AzureBlobStore, - FileBlobStore, - InMemoryBlobStore, - S3BlobStore, - StorageConfigurationError, - createConfiguredBlobStore, -} from "../src/index.js"; - -describe("configured store selection", () => { - it("preserves the local default and supports memory explicitly", async () => { - expect(await createConfiguredBlobStore({})).toBeInstanceOf(FileBlobStore); - expect(await createConfiguredBlobStore({ STORAGE_BACKEND: "memory" })) - .toBeInstanceOf(InMemoryBlobStore); - }); - - it("constructs cloud adapters without fetching credentials or creating resources", async () => { - expect(await createConfiguredBlobStore({ - STORAGE_BACKEND: "azure", - AZURE_STORAGE_ACCOUNT_URL: "https://synthetic.blob.core.windows.net", - AZURE_STORAGE_CONTAINER: "synthetic-records", - })).toBeInstanceOf(AzureBlobStore); - expect(await createConfiguredBlobStore({ - STORAGE_BACKEND: "s3", S3_BUCKET: "synthetic-records", AWS_REGION: "us-east-1", - })).toBeInstanceOf(S3BlobStore); - }); - - it("fails closed instead of falling back when a selected backend is incomplete", async () => { - for (const env of [ - { STORAGE_BACKEND: "unknown" }, - { STORAGE_BACKEND: "azure" }, - { STORAGE_BACKEND: "s3" }, - { STORAGE_BACKEND: "ipfs" }, - { STORAGE_BACKEND: "azure", AZURE_STORAGE_CONNECTION_STRING: "not-a-real-key" }, - ]) { - await expect(createConfiguredBlobStore(env)).rejects.toBeInstanceOf(StorageConfigurationError); - } - }); - - it("never directs emulator credentials to a remote cloud endpoint", async () => { - await expect(createConfiguredBlobStore({ - STORAGE_BACKEND: "azure", STORAGE_EMULATOR: "true", - AZURE_STORAGE_ACCOUNT_URL: "https://synthetic.blob.core.windows.net", - AZURE_STORAGE_CONTAINER: "synthetic-records", - })).rejects.toThrow("loopback"); - await expect(createConfiguredBlobStore({ - STORAGE_BACKEND: "s3", STORAGE_EMULATOR: "true", - S3_BUCKET: "synthetic-records", AWS_REGION: "us-east-1", - })).rejects.toThrow("loopback"); - await expect(createConfiguredBlobStore({ - STORAGE_BACKEND: "s3", STORAGE_EMULATOR: "true", - S3_BUCKET: "synthetic-records", AWS_REGION: "us-east-1", - S3_ENDPOINT_URL: "https://remote.example.test", - })).rejects.toThrow("loopback"); - await expect(createConfiguredBlobStore({ - STORAGE_BACKEND: "ipfs", STORAGE_EMULATOR: "true", - IPFS_PRIVATE_NETWORK: "true", IPFS_METADATA_BACKEND: "file", - IPFS_API_URL: "https://remote.example.test", - IPFS_CLUSTER_API_URL: "http://127.0.0.1:9094", - })).rejects.toThrow("loopback"); - }); - - it("validates resource names and refuses TLS bypass", async () => { - await expect(createConfiguredBlobStore({ - STORAGE_BACKEND: "azure", - AZURE_STORAGE_ACCOUNT_URL: "https://synthetic.blob.core.windows.net", - AZURE_STORAGE_CONTAINER: "../invalid", - })).rejects.toThrow("container name"); - await expect(createConfiguredBlobStore({ - STORAGE_BACKEND: "s3", S3_BUCKET: "not/a/bucket", AWS_REGION: "us-east-1", - })).rejects.toThrow("bucket name"); - await expect(createConfiguredBlobStore({ NODE_TLS_REJECT_UNAUTHORIZED: "0" })) - .rejects.toThrow("TLS certificate validation"); - vi.stubEnv("NODE_TLS_REJECT_UNAUTHORIZED", "0"); - try { - await expect(createConfiguredBlobStore({})).rejects.toThrow("TLS certificate validation"); - } finally { - vi.unstubAllEnvs(); - } - }); - - it("requires a private IPFS acknowledgement and durable index outside emulator mode", async () => { - await expect(createConfiguredBlobStore({ STORAGE_BACKEND: "ipfs" })) - .rejects.toThrow("IPFS_PRIVATE_NETWORK"); - for (const backend of ["file", "memory", "ipfs"]) { - await expect(createConfiguredBlobStore({ - STORAGE_BACKEND: "ipfs", IPFS_PRIVATE_NETWORK: "true", IPFS_METADATA_BACKEND: backend, - })).rejects.toThrow("IPFS metadata"); - } - }); - - it("rejects insufficient replication and missing remote API authentication", async () => { - const base = { - STORAGE_BACKEND: "ipfs", IPFS_PRIVATE_NETWORK: "true", IPFS_METADATA_BACKEND: "azure", - IPFS_API_URL: "https://private-kubo.example.test", - IPFS_CLUSTER_API_URL: "https://private-cluster.example.test", - }; - await expect(createConfiguredBlobStore({ ...base, IPFS_REPLICATION_MIN: "1" })) - .rejects.toThrow("replication"); - await expect(createConfiguredBlobStore({ - ...base, IPFS_REPLICATION_MIN: "3", IPFS_REPLICATION_MAX: "2", - })).rejects.toThrow("replication"); - await expect(createConfiguredBlobStore(base)).rejects.toThrow("runtime authorization"); - }); -}); diff --git a/test/fixtures/ipfs/README.md b/test/fixtures/ipfs/README.md index bbdb49a..43a1b97 100644 --- a/test/fixtures/ipfs/README.md +++ b/test/fixtures/ipfs/README.md @@ -22,7 +22,7 @@ Image pulls require internet access; the running fixture does not. forward only to each container's API over the private bridge. No internet-connected network or host networking is added. These unauthenticated administrative APIs are for an isolated development/CI machine, not a shared untrusted host. -- Kubo requires `IPFS_FORCE_PNET=1`, an independently generated swarm key, +- Kubo requires `LIBP2P_FORCE_PNET=1`, an independently generated swarm key, no public bootstrap peers, no DHT/routing/providing, no delegated routing or IPNS publishing, no automatic remote configuration, no mDNS, no AutoTLS, and no relay/NAT mapping. Peers connect explicitly by internal DNS plus diff --git a/test/fixtures/ipfs/fixture.spec.ts b/test/fixtures/ipfs/fixture.spec.ts index 5df9346..717cb9f 100644 --- a/test/fixtures/ipfs/fixture.spec.ts +++ b/test/fixtures/ipfs/fixture.spec.ts @@ -26,7 +26,7 @@ describe("private IPFS Docker fixture configuration (no Docker required)", () => }); it("forces a private swarm and explicitly bootstraps Raft followers", () => { - expect(containerArguments({ ...options, kind: "kubo" })).toContain("IPFS_FORCE_PNET=1"); + expect(containerArguments({ ...options, kind: "kubo" })).toContain("LIBP2P_FORCE_PNET=1"); const bootstrap = "/dns4/cluster1/tcp/9096/p2p/public-peer-id"; const args = containerArguments({ ...options, alias: "cluster2", kind: "cluster", bootstrap }); expect(args.slice(-2)).toEqual(["--bootstrap", bootstrap]); @@ -60,7 +60,7 @@ describe("private IPFS Docker fixture configuration (no Docker required)", () => it("disables public discovery before starting the private Kubo daemon", async () => { const script = await readFile(new URL("./kubo.sh", import.meta.url), "utf8"); for (const setting of [ - "export IPFS_FORCE_PNET=1", "ipfs bootstrap rm --all", + "export LIBP2P_FORCE_PNET=1", "ipfs bootstrap rm --all", "ipfs config Routing.Type none", "ipfs config --json Provide.Enabled false", "ipfs config --json AutoConf.Enabled false", "ipfs config --json AutoTLS.Enabled false", "ipfs config --json Discovery.MDNS.Enabled false", diff --git a/test/fixtures/ipfs/fixture.ts b/test/fixtures/ipfs/fixture.ts index 40bf763..15055ee 100644 --- a/test/fixtures/ipfs/fixture.ts +++ b/test/fixtures/ipfs/fixture.ts @@ -49,7 +49,7 @@ export function containerArguments(options: { "--mount", `type=volume,source=${options.secretsVolume},target=/run/fixture-secrets,readonly`, "--mount", `type=bind,source=${options.scriptDirectory},target=/fixture,readonly`, ...(kubo - ? ["--env", "IPFS_FORCE_PNET=1"] + ? ["--env", "LIBP2P_FORCE_PNET=1"] : [ "--env", "CLUSTER_RESTAPI_HTTPLISTENMULTIADDRESS=/ip4/0.0.0.0/tcp/9094", "--env", `CLUSTER_IPFSHTTP_NODEMULTIADDRESS=/dns4/${options.alias.replace("cluster", "kubo")}/tcp/5001`, diff --git a/test/fixtures/ipfs/kubo.sh b/test/fixtures/ipfs/kubo.sh index af41677..e8e166b 100644 --- a/test/fixtures/ipfs/kubo.sh +++ b/test/fixtures/ipfs/kubo.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu umask 077 -export IPFS_FORCE_PNET=1 +export LIBP2P_FORCE_PNET=1 ipfs init cp /run/fixture-secrets/swarm.key "$IPFS_PATH/swarm.key" chmod 400 "$IPFS_PATH/swarm.key" diff --git a/test/infrastructure/aws.spec.ts b/test/infrastructure/aws.spec.ts new file mode 100644 index 0000000..7df813e --- /dev/null +++ b/test/infrastructure/aws.spec.ts @@ -0,0 +1,301 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const templateUrl = new URL("../../infra/aws/storage-foundation.json", import.meta.url); +const raw = readFileSync(templateUrl, "utf8"); +// This is a local policy regression check, not the CloudFormation service/schema validator. +const template = JSON.parse(raw); +const resources = template.Resources; +const launch = resources.NodeLaunchTemplate.Properties.LaunchTemplateData; +const statements = resources.NodeRole.Properties.Policies[0].PolicyDocument.Statement; +const policy = (sid: string) => statements.find((item: { Sid?: string }) => item.Sid === sid); +const bucketStatements = resources.BackupBucketPolicy.Properties.PolicyDocument.Statement; + +describe("review-only AWS hybrid storage foundation", () => { + it("parses offline and defaults to no compute or bootstrap", () => { + expect(template.AWSTemplateFormatVersion).toBe("2010-09-09"); + expect(template.Parameters.CreateNode.Default).toBe("false"); + expect(template.Conditions.NodeEnabled).toEqual({ + "Fn::Equals": [{ Ref: "CreateNode" }, "true"], + }); + for (const name of [ + "Node", "NodeLaunchTemplate", "NodeProfile", "NodeSecurityGroup", + "DataVolume", "DataVolumeAttachment", "VolumeKey", + ]) { + expect(resources[name].Condition).toBe("NodeEnabled"); + } + expect(raw).not.toMatch(/"UserData"|"KeyName"|"AWS::IAM::AccessKey"/); + expect(launch.ImageId).toEqual({ Ref: "ApprovedAmiId" }); + expect(template.Parameters.ApprovedAmiId.Default).toBe(""); + expect(template.Rules.NodePrerequisites.Assertions[0].Assert["Fn::And"]).toHaveLength(8); + }); + + it("requires IMDSv2, no public IP and no IPv6 metadata", () => { + expect(launch.MetadataOptions).toEqual({ + HttpEndpoint: "enabled", + HttpTokens: "required", + HttpPutResponseHopLimit: 1, + HttpProtocolIpv6: "disabled", + InstanceMetadataTags: "disabled", + }); + expect(launch.NetworkInterfaces).toEqual([{ + DeviceIndex: 0, + AssociatePublicIpAddress: false, + DeleteOnTermination: true, + SubnetId: { Ref: "PrivateSubnetId" }, + Groups: [{ Ref: "NodeSecurityGroup" }], + }]); + expect(raw).not.toMatch(/AWS::EC2::EIP|"Ipv6AddressCount"|"Ipv6Addresses"/); + }); + + it("only exposes HTTPS to an existing private client SG", () => { + const sg = resources.NodeSecurityGroup.Properties; + expect(sg.SecurityGroupIngress).toEqual([{ + IpProtocol: "tcp", FromPort: 443, ToPort: 443, + SourceSecurityGroupId: { Ref: "ClientSecurityGroupId" }, + }]); + expect(sg.SecurityGroupEgress).toHaveLength(3); + for (const rule of sg.SecurityGroupEgress) { + expect(rule.IpProtocol).toBe("tcp"); + expect(rule.FromPort).toBe(443); + expect(rule.ToPort).toBe(443); + expect(rule.CidrIp).toBeUndefined(); + expect(rule.CidrIpv6).toBeUndefined(); + expect(rule.DestinationSecurityGroupId ?? rule.DestinationPrefixListId).toBeDefined(); + } + expect(raw).not.toMatch(/0\.0\.0\.0\/0|::\/0/); + }); + + it("encrypts root and separately retained data volumes with the EBS key", () => { + const root = launch.BlockDeviceMappings[0].Ebs; + expect(root.Encrypted).toBe(true); + expect(root.KmsKeyId).toEqual({ "Fn::GetAtt": ["VolumeKey", "Arn"] }); + expect(root.VolumeType).toBe("gp3"); + expect(root).not.toHaveProperty("DeletionPolicy"); + expect(root).not.toHaveProperty("UpdateReplacePolicy"); + const volume = resources.DataVolume; + expect(volume.Type).toBe("AWS::EC2::Volume"); + expect(volume.Properties.Encrypted).toBe(true); + expect(volume.Properties.KmsKeyId).toEqual(root.KmsKeyId); + expect(volume.Properties.AvailabilityZone).toEqual(resources.Node.Properties.AvailabilityZone); + expect(resources.DataVolumeAttachment.Properties.VolumeId).toEqual({ Ref: "DataVolume" }); + }); + + it("retains data and key resources on deletion and replacement", () => { + for (const name of [ + "BackupKey", "MetadataKey", "VolumeKey", "BackupBucket", + "BackupBucketPolicy", "MetadataTable", "DataVolume", + ]) { + expect(resources[name].DeletionPolicy).toBe("Retain"); + expect(resources[name].UpdateReplacePolicy).toBe("Retain"); + } + for (const name of ["BackupKey", "MetadataKey", "VolumeKey"]) { + expect(resources[name].Properties.EnableKeyRotation).toBe(true); + expect(resources[name].Properties.PendingWindowInDays).toBe(30); + expect(resources[name].Properties.KeyPolicy.Statement).toEqual([{ + Sid: "AccountDelegatesThroughIAM", + Effect: "Allow", + Principal: { AWS: { "Fn::Sub": "arn:${AWS::Partition}:iam::${AWS::AccountId}:root" } }, + Action: "kms:*", + Resource: "*", + }]); + } + }); + + it("provides the adapter's string key table with PITR and deletion protection", () => { + const table = resources.MetadataTable.Properties; + expect(table.AttributeDefinitions).toEqual([{ AttributeName: "key", AttributeType: "S" }]); + expect(table.KeySchema).toEqual([{ AttributeName: "key", KeyType: "HASH" }]); + expect(table.BillingMode).toBe("PAY_PER_REQUEST"); + expect(table.PointInTimeRecoverySpecification.PointInTimeRecoveryEnabled).toBe(true); + expect(table.DeletionProtectionEnabled).toBe(true); + expect(table.SSESpecification).toEqual({ + SSEEnabled: true, SSEType: "KMS", + KMSMasterKeyId: { "Fn::GetAtt": ["MetadataKey", "Arn"] }, + }); + expect(table.TimeToLiveSpecification).toBeUndefined(); + }); + + it("keeps backups private, versioned, SSE-KMS encrypted and without automatic expiry", () => { + const bucket = resources.BackupBucket.Properties; + expect(bucket.PublicAccessBlockConfiguration).toEqual({ + BlockPublicAcls: true, BlockPublicPolicy: true, + IgnorePublicAcls: true, RestrictPublicBuckets: true, + }); + expect(bucket.OwnershipControls.Rules).toEqual([{ ObjectOwnership: "BucketOwnerEnforced" }]); + expect(bucket.VersioningConfiguration.Status).toBe("Enabled"); + expect(bucket.BucketEncryption.ServerSideEncryptionConfiguration).toEqual([{ + BucketKeyEnabled: false, + ServerSideEncryptionByDefault: { + SSEAlgorithm: "aws:kms", KMSMasterKeyID: { "Fn::GetAtt": ["BackupKey", "Arn"] }, + }, + }]); + expect(bucket.LifecycleConfiguration.Rules).toEqual([{ + Id: "AbortIncompleteUploadsOnly", Status: "Enabled", + AbortIncompleteMultipartUpload: { DaysAfterInitiation: 7 }, + }]); + }); + + it("denies non-TLS access and uploads without the explicit designated KMS key", () => { + expect(bucketStatements).toHaveLength(3); + for (const statement of bucketStatements) { + expect(statement.Effect).toBe("Deny"); + expect(statement.Principal).toBe("*"); + } + expect(bucketStatements[0].Action).toBe("s3:*"); + expect(bucketStatements[0].Condition).toEqual({ Bool: { "aws:SecureTransport": "false" } }); + expect(bucketStatements[0].Resource).toEqual([ + { "Fn::GetAtt": ["BackupBucket", "Arn"] }, { "Fn::Sub": "${BackupBucket.Arn}/*" }, + ]); + expect(bucketStatements[1].Condition.StringNotEquals).toEqual({ + "s3:x-amz-server-side-encryption": "aws:kms", + }); + expect(bucketStatements[2].Condition.StringNotEquals).toEqual({ + "s3:x-amz-server-side-encryption-aws-kms-key-id": { "Fn::GetAtt": ["BackupKey", "Arn"] }, + }); + }); + + it("limits node trust to EC2 and data access to one table and backup prefix", () => { + expect(resources.NodeRole.Properties.AssumeRolePolicyDocument.Statement).toEqual([{ + Effect: "Allow", Principal: { Service: "ec2.amazonaws.com" }, Action: "sts:AssumeRole", + }]); + expect(policy("MetadataReadAndConditionalWrite")).toMatchObject({ + Action: ["dynamodb:GetItem", "dynamodb:PutItem"], + Resource: { "Fn::GetAtt": ["MetadataTable", "Arn"] }, + }); + expect(policy("EncryptedContentBackup")).toMatchObject({ + Action: ["s3:GetObject", "s3:PutObject"], + Resource: { "Fn::Sub": "${BackupBucket.Arn}/records/*" }, + }); + expect(policy("BackupMissingKeySemantics")).toEqual({ + Sid: "BackupMissingKeySemantics", Effect: "Allow", + Action: ["s3:ListBucket"], Resource: { "Fn::GetAtt": ["BackupBucket", "Arn"] }, + }); + const runtimePolicy = JSON.stringify(statements); + expect(runtimePolicy).not.toMatch(/s3:Delete|dynamodb:Delete|dynamodb:Scan|iam:|kms:\*|kms:CreateGrant|kms:ScheduleKeyDeletion/); + expect(runtimePolicy).not.toContain('"Resource":"*"'); + expect(resources.NodeRole.Properties.ManagedPolicyArns).toBeUndefined(); + }); + + it("only authorizes KMS decryption through the relevant AWS service and context", () => { + const s3 = policy("BackupEncryptionViaS3"); + expect(s3.Resource).toEqual({ "Fn::GetAtt": ["BackupKey", "Arn"] }); + expect(s3.Condition.StringEquals["kms:ViaService"]).toEqual({ + "Fn::Sub": "s3.${AWS::Region}.${AWS::URLSuffix}", + }); + expect(s3.Condition.StringLike["kms:EncryptionContext:aws:s3:arn"]).toEqual({ + "Fn::Sub": "${BackupBucket.Arn}/records/*", + }); + const dynamo = policy("ReadTableKeyViaDynamoDB"); + expect(dynamo.Action).toEqual(["kms:Decrypt"]); + expect(dynamo.Resource).toEqual({ "Fn::GetAtt": ["MetadataKey", "Arn"] }); + expect(dynamo.Condition.StringEquals["kms:ViaService"]).toEqual({ + "Fn::Sub": "dynamodb.${AWS::Region}.${AWS::URLSuffix}", + }); + expect(dynamo.Condition.StringEquals["kms:EncryptionContext:aws:dynamodb:tableName"]).toEqual({ + Ref: "MetadataTable", + }); + }); + + it("accepts external secret identifiers only, with optional narrowly scoped access", () => { + expect(template.Parameters.RuntimeSecretArn.Default).toBe(""); + expect(template.Parameters.RuntimeSecretKmsKeyArn.Default).toBe(""); + expect(template.Rules.SecretKeyPair).toBeDefined(); + const optional = statements.filter((item: Record) => item["Fn::If"]); + expect(optional).toHaveLength(6); + for (const item of optional) { + expect(template.Conditions[item["Fn::If"][0]]).toBeDefined(); + expect(item["Fn::If"][2]).toEqual({ Ref: "AWS::NoValue" }); + } + expect(optional[0]["Fn::If"][1]).toMatchObject({ + Action: ["secretsmanager:GetSecretValue"], Resource: { Ref: "RuntimeSecretArn" }, + }); + expect(optional[1]["Fn::If"][1].Condition.StringEquals).toMatchObject({ + "kms:ViaService": { "Fn::Sub": "secretsmanager.${AWS::Region}.${AWS::URLSuffix}" }, + "kms:EncryptionContext:SecretARN": { Ref: "RuntimeSecretArn" }, + }); + expect(raw).not.toMatch(/"SecretString"\s*:|"SecretBinary"\s*:|GenerateSecretString|AKIA[0-9A-Z]{16}|BEGIN .*PRIVATE KEY/); + expect(JSON.stringify(template.Outputs)).not.toMatch(/RuntimeSecret|AUTH|patientDataKey/); + }); + + it("supports separate direct AWS authorization secrets without wildcard access", () => { + const optional = statements.filter((item: Record) => item["Fn::If"]); + for (const [name, sid] of [ + ["ApiAuthorization", "KuboAuthorization"], + ["ClusterAuthorization", "ClusterAuthorization"], + ]) { + expect(template.Parameters[`${name}SecretArn`].Default).toBe(""); + expect(template.Parameters[`${name}SecretKmsKeyArn`].Default).toBe(""); + expect(template.Rules[`${name}KeyRequiresSecret`]).toBeDefined(); + const read = optional.find((item: Record) => + item["Fn::If"][1].Sid === `Read${sid}Secret`); + expect(read["Fn::If"][0]).toBe(`Has${name}Secret`); + expect(read["Fn::If"][1]).toMatchObject({ + Action: ["secretsmanager:GetSecretValue"], Resource: { Ref: `${name}SecretArn` }, + }); + const decrypt = optional.find((item: Record) => + item["Fn::If"][1].Sid === `Decrypt${sid}ViaSecretsManager`); + expect(decrypt["Fn::If"][0]).toBe(`Has${name}SecretKey`); + expect(decrypt["Fn::If"][1]).toMatchObject({ + Action: ["kms:Decrypt"], Resource: { Ref: `${name}SecretKmsKeyArn` }, + Condition: { StringEquals: { + "kms:ViaService": { "Fn::Sub": "secretsmanager.${AWS::Region}.${AWS::URLSuffix}" }, + "kms:EncryptionContext:SecretARN": { Ref: `${name}SecretArn` }, + } }, + }); + } + const fragment = readFileSync( + new URL("../../infra/aws/runtime/direct-api-auth.env.example", import.meta.url), "utf8", + ).split(/\r?\n/).filter((line) => line && !line.startsWith("#")); + expect(fragment).toEqual([ + "IPFS_API_AUTH_AWS_SECRET_ID=REPLACE_WITH_ApiAuthorizationSecretArn", + "IPFS_CLUSTER_AUTH_AWS_SECRET_ID=REPLACE_WITH_ClusterAuthorizationSecretArn", + ]); + }); + + it("keeps application environment examples aligned with nonsecret outputs", () => { + const env = Object.fromEntries(readFileSync( + new URL("../../infra/aws/runtime/app.env.example", import.meta.url), "utf8", + ).split(/\r?\n/).filter((line) => line && !line.startsWith("#")) + .map((line) => [line.slice(0, line.indexOf("=")), line.slice(line.indexOf("=") + 1)])); + const mapping = { + STORAGE_BACKEND: "StorageBackend", IPFS_METADATA_BACKEND: "IpfsMetadataBackend", + DYNAMODB_TABLE: "DynamoDbTable", AWS_REGION: "AwsRegion", + IPFS_BACKUP_BACKEND: "IpfsBackupBackend", S3_BUCKET: "S3Bucket", + S3_KMS_KEY_ID: "S3KmsKeyId", STORAGE_PREFIX: "StoragePrefix", + IPFS_PRIVATE_NETWORK: "IpfsPrivateNetwork", + }; + for (const [setting, output] of Object.entries(mapping)) { + const value = template.Outputs[output].Value; + expect(env[setting]).toBe(typeof value === "string" ? value : `REPLACE_WITH_${output}`); + } + expect(env.IPFS_API_URL).toMatch(/^https:\/\/.+\.invalid$/); + expect(env.IPFS_CLUSTER_API_URL).toMatch(/^https:\/\/.+\.invalid$/); + expect(env.IPFS_REPLICATION_MIN).toBe("2"); + expect(env.IPFS_API_AUTH_FILE).toMatch(/^\/run\//); + expect(env.IPFS_CLUSTER_AUTH_FILE).toMatch(/^\/run\//); + expect(env.IPFS_API_AUTH).toBeUndefined(); + expect(env.IPFS_CLUSTER_AUTH).toBeUndefined(); + expect(env.AWS_ACCESS_KEY_ID).toBeUndefined(); + expect(env.AWS_SECRET_ACCESS_KEY).toBeUndefined(); + }); + + it("ships fail-closed AMI service examples rather than automatic formatting or downloads", () => { + for (const name of ["kubo", "ipfs-cluster"]) { + const unit = readFileSync( + new URL(`../../infra/aws/runtime/${name}.service`, import.meta.url), "utf8", + ); + expect(unit).toContain("ConditionPathIsMountPoint=/srv/ipfs"); + expect(unit).toContain("User=ipfs"); + expect(unit).toContain("NoNewPrivileges=true"); + expect(unit).toContain("LimitCORE=0"); + expect(unit).toContain("StandardOutput=null"); + expect(unit).not.toMatch(/mkfs|curl|wget|docker pull|:latest/); + } + const kubo = readFileSync( + new URL("../../infra/aws/runtime/kubo.service", import.meta.url), "utf8", + ); + expect(kubo).toContain("Environment=LIBP2P_FORCE_PNET=1"); + expect(kubo).toContain("ExecStartPre=/usr/bin/test -s /srv/ipfs/kubo/swarm.key"); + }); +}); diff --git a/test/integration/configured-storage.spec.ts b/test/integration/configured-storage.spec.ts index 2f92d0b..3bb999c 100644 --- a/test/integration/configured-storage.spec.ts +++ b/test/integration/configured-storage.spec.ts @@ -2,6 +2,7 @@ import { randomBytes } from "node:crypto"; import { describe, expect, it } from "vitest"; import { AccessDeniedError, + DistributedBlobStore, IntegrityError, PatientRecordService, RecordNotFoundError, @@ -14,8 +15,8 @@ import { blobStoreContract } from "../helpers/blob-store-contract.js"; if (process.env.STORAGE_INTEGRATION !== "true") { throw new Error("Integration tests require STORAGE_INTEGRATION=true and a dedicated synthetic-data store."); } -if (!["azure", "s3", "ipfs"].includes(process.env.STORAGE_BACKEND ?? "")) { - throw new Error("Integration tests require an explicit azure, s3, or ipfs backend, never a fallback."); +if (!["azure", "s3", "ipfs", "dynamodb"].includes(process.env.STORAGE_BACKEND ?? "")) { + throw new Error("Integration tests require an explicit external backend, never a fallback."); } // Do not print SDK errors: their request metadata can contain authorization data. @@ -54,6 +55,18 @@ blobStoreContract("configured external provider", async () => ({ })); describe("configured provider encrypted workflow", () => { + if (process.env.STORAGE_BACKEND === "ipfs" && process.env.IPFS_BACKUP_BACKEND) { + it("verifies encrypted backup restore without changing the metadata version", async () => { + const store = await safe(() => createConfiguredBlobStore()); + if (!(store instanceof DistributedBlobStore)) throw new Error("Expected distributed backend."); + const key = randomBytes(24).toString("hex"); + await safe(() => store.create(key, { ciphertext: randomBytes(32).toString("base64") })); + const before = await safe(() => store.read(key)); + await safe(() => store.restore(key)); + expect(await safe(() => store.read(key))).toEqual(before); + }); + } + it("enrolls, appends, reconnects and refuses wrong factors", async () => { const store = await connect(); const records = new PatientRecordService(store); @@ -65,6 +78,7 @@ describe("configured provider encrypted workflow", () => { ...access, resource: { resourceType: "Condition", id: "synthetic-integration", code: { text: "SYNTHETIC-ONLY-MARKER" } }, }); + const second = await records.append({ ...access, resource: { resourceType: "Observation", id: "synthetic-followup", status: "final", code: { text: "SYNTHETIC-ONLY-MARKER" } }, diff --git a/test/azure-blob-store.spec.ts b/test/storage/azure/azure-blob-store.spec.ts similarity index 99% rename from test/azure-blob-store.spec.ts rename to test/storage/azure/azure-blob-store.spec.ts index 3674cae..03e6b5a 100644 --- a/test/azure-blob-store.spec.ts +++ b/test/storage/azure/azure-blob-store.spec.ts @@ -11,7 +11,7 @@ import { ConcurrentUpdateError, ObjectAlreadyExistsError, ObjectNotFoundError, -} from "../src/index.js"; +} from "../../../src/index.js"; const key = "a".repeat(43); const ciphertext = { algorithm: "AES-256-GCM", ciphertext: "synthetic-ciphertext", nonce: "opaque" }; diff --git a/test/storage/config/factory.spec.ts b/test/storage/config/factory.spec.ts new file mode 100644 index 0000000..9dda17d --- /dev/null +++ b/test/storage/config/factory.spec.ts @@ -0,0 +1,244 @@ +import { describe, expect, it, vi } from "vitest"; +import * as secrets from "../../../src/storage/config/secrets.js"; +import { + AzureBlobStore, + DistributedBlobStore, + DynamoDbBlobStore, + FileBlobStore, + InMemoryBlobStore, + S3BlobStore, + StorageConfigurationError, + createConfiguredBlobStore, +} from "../../../src/index.js"; + +vi.mock("@azure/identity", () => ({ + DefaultAzureCredential: class { + async getToken(): Promise { + throw new Error("Provider construction must not fetch credentials."); + } + }, +})); + +describe("configured store selection", () => { + it("composes independently registered IPFS index and backup providers", async () => { + const index = vi.fn(() => new InMemoryBlobStore()); + const backup = vi.fn(() => new InMemoryBlobStore()); + const store = await createConfiguredBlobStore({ + STORAGE_BACKEND: "ipfs", STORAGE_EMULATOR: "true", IPFS_PRIVATE_NETWORK: "true", + IPFS_API_URL: "http://127.0.0.1:5001", IPFS_CLUSTER_API_URL: "http://127.0.0.1:9094", + IPFS_METADATA_BACKEND: "test-index", IPFS_BACKUP_BACKEND: "test-backup", + }, { providers: { "test-index": index, "test-backup": backup } }); + expect(store).toBeInstanceOf(DistributedBlobStore); + expect(index).toHaveBeenCalledOnce(); + expect(backup).toHaveBeenCalledOnce(); + }); + + it("rejects recursive or metadata-only backup providers", async () => { + for (const provider of ["ipfs", "dynamodb"]) { + await expect(createConfiguredBlobStore({ + STORAGE_BACKEND: "ipfs", STORAGE_EMULATOR: "true", IPFS_PRIVATE_NETWORK: "true", + IPFS_API_URL: "http://127.0.0.1:5001", IPFS_CLUSTER_API_URL: "http://127.0.0.1:9094", + IPFS_METADATA_BACKEND: "file", IPFS_BACKUP_BACKEND: provider, + })).rejects.toThrow("separate durable object store"); + } + }); + + it("requires SSE-KMS for remote S3 backups without fetching cloud credentials", async () => { + const authorization = vi.spyOn(secrets, "loadRuntimeSecret").mockResolvedValue("synthetic-header"); + const env = { + STORAGE_BACKEND: "ipfs", IPFS_PRIVATE_NETWORK: "true", + IPFS_API_URL: "https://private-kubo.example.test", + IPFS_CLUSTER_API_URL: "https://private-cluster.example.test", + IPFS_METADATA_BACKEND: "dynamodb", IPFS_BACKUP_BACKEND: "s3", + DYNAMODB_TABLE: "synthetic-metadata", S3_BUCKET: "synthetic-backups", + AWS_REGION: "us-east-1", + }; + try { + await expect(createConfiguredBlobStore(env)).rejects.toThrow("S3_KMS_KEY_ID"); + expect(await createConfiguredBlobStore({ + ...env, S3_KMS_KEY_ID: "alias/synthetic-backup", + })).toBeInstanceOf(DistributedBlobStore); + } finally { + authorization.mockRestore(); + } + }); + + it("selects a registered implementation without touching unused providers", async () => { + const instance = new InMemoryBlobStore(); + const selected = vi.fn(() => instance); + const unused = vi.fn(() => { throw new Error("Must not initialize an unused provider."); }); + expect(await createConfiguredBlobStore( + { STORAGE_BACKEND: "custom" }, + { providers: { custom: selected, unused } }, + )).toBe(instance); + expect(selected).toHaveBeenCalledOnce(); + expect(unused).not.toHaveBeenCalled(); + }); + + it("allows composition through the registry and snapshots configuration", async () => { + const env = { STORAGE_BACKEND: "composed", STORAGE_FILE_DIRECTORY: ".data/custom" }; + const store = await createConfiguredBlobStore(env, { + providers: { + composed: async ({ environment, emulator, createStore }) => { + expect(Object.isFrozen(environment)).toBe(true); + expect(emulator).toBe(false); + expect(environment.STORAGE_FILE_DIRECTORY).toBe(".data/custom"); + return createStore("memory"); + }, + }, + }); + expect(store).toBeInstanceOf(InMemoryBlobStore); + }); + + it("rejects self-dependencies, transitive cycles, and unknown dependencies", async () => { + await expect(createConfiguredBlobStore({ STORAGE_BACKEND: "cycle" }, { + providers: { cycle: ({ createStore }) => createStore("cycle") }, + })).rejects.toThrow("dependency cycle"); + await expect(createConfiguredBlobStore({ STORAGE_BACKEND: "first" }, { + providers: { + first: ({ createStore }) => createStore("second"), + second: ({ createStore }) => createStore("first"), + }, + })).rejects.toThrow("dependency cycle"); + await expect(createConfiguredBlobStore({ STORAGE_BACKEND: "custom" }, { + providers: { custom: ({ createStore }) => createStore("missing") }, + })).rejects.toThrow("Unknown storage backend"); + }); + + it("does not silently override built-ins or accept inherited provider names", async () => { + await expect(createConfiguredBlobStore({}, { + providers: { azure: () => new InMemoryBlobStore() }, + })).rejects.toThrow("distinct non-builtin names"); + await expect(createConfiguredBlobStore({ STORAGE_BACKEND: "constructor" })) + .rejects.toThrow("Unknown storage backend"); + await expect(createConfiguredBlobStore({}, { + providers: { "../bad": () => new InMemoryBlobStore() }, + })).rejects.toThrow("lowercase provider name"); + }); + + it("propagates selected-provider failures without falling back", async () => { + const error = new Error("synthetic provider unavailable"); + await expect(createConfiguredBlobStore({ STORAGE_BACKEND: "custom" }, { + providers: { custom: () => { throw error; } }, + })).rejects.toBe(error); + }); + + it("does not require cloud configuration when the local provider is selected", async () => { + expect(await createConfiguredBlobStore({ + STORAGE_BACKEND: "file", + AZURE_STORAGE_ACCOUNT_URL: "not-a-url", + S3_BUCKET: "../invalid", + IPFS_API_URL: "http://untrusted.test", + })).toBeInstanceOf(FileBlobStore); + }); + + it("preserves the local default and supports memory explicitly", async () => { + expect(await createConfiguredBlobStore({})).toBeInstanceOf(FileBlobStore); + expect(await createConfiguredBlobStore({ STORAGE_BACKEND: "memory" })) + .toBeInstanceOf(InMemoryBlobStore); + }); + + it("constructs cloud adapters without fetching credentials or creating resources", async () => { + expect(await createConfiguredBlobStore({ + STORAGE_BACKEND: "azure", + AZURE_STORAGE_ACCOUNT_URL: "https://synthetic.blob.core.windows.net", + AZURE_STORAGE_CONTAINER: "synthetic-records", + })).toBeInstanceOf(AzureBlobStore); + expect(await createConfiguredBlobStore({ + STORAGE_BACKEND: "s3", S3_BUCKET: "synthetic-records", AWS_REGION: "us-east-1", + })).toBeInstanceOf(S3BlobStore); + expect(await createConfiguredBlobStore({ + STORAGE_BACKEND: "dynamodb", DYNAMODB_TABLE: "synthetic-metadata", AWS_REGION: "us-east-1", + })).toBeInstanceOf(DynamoDbBlobStore); + }, 15_000); + + it("composes the AWS hybrid without cloud calls at construction", async () => { + expect(await createConfiguredBlobStore({ + STORAGE_BACKEND: "ipfs", STORAGE_EMULATOR: "true", IPFS_PRIVATE_NETWORK: "true", + IPFS_API_URL: "http://127.0.0.1:5001", IPFS_CLUSTER_API_URL: "http://127.0.0.1:9094", + IPFS_METADATA_BACKEND: "dynamodb", IPFS_BACKUP_BACKEND: "s3", + DYNAMODB_TABLE: "synthetic-metadata", DYNAMODB_ENDPOINT_URL: "http://127.0.0.1:8000", + S3_BUCKET: "synthetic-backups", S3_ENDPOINT_URL: "http://127.0.0.1:9000", + AWS_REGION: "us-east-1", + })).toBeInstanceOf(DistributedBlobStore); + }); + + it("fails closed instead of falling back when a selected backend is incomplete", async () => { + for (const env of [ + { STORAGE_BACKEND: "unknown" }, + { STORAGE_BACKEND: "azure" }, + { STORAGE_BACKEND: "s3" }, + { STORAGE_BACKEND: "dynamodb" }, + { STORAGE_BACKEND: "ipfs" }, + { STORAGE_BACKEND: "azure", AZURE_STORAGE_CONNECTION_STRING: "not-a-real-key" }, + ]) { + await expect(createConfiguredBlobStore(env)).rejects.toBeInstanceOf(StorageConfigurationError); + } + }); + + it("never directs emulator credentials to a remote cloud endpoint", async () => { + await expect(createConfiguredBlobStore({ + STORAGE_BACKEND: "azure", STORAGE_EMULATOR: "true", + AZURE_STORAGE_ACCOUNT_URL: "https://synthetic.blob.core.windows.net", + AZURE_STORAGE_CONTAINER: "synthetic-records", + })).rejects.toThrow("loopback"); + await expect(createConfiguredBlobStore({ + STORAGE_BACKEND: "s3", STORAGE_EMULATOR: "true", + S3_BUCKET: "synthetic-records", AWS_REGION: "us-east-1", + })).rejects.toThrow("loopback"); + await expect(createConfiguredBlobStore({ + STORAGE_BACKEND: "s3", STORAGE_EMULATOR: "true", + S3_BUCKET: "synthetic-records", AWS_REGION: "us-east-1", + S3_ENDPOINT_URL: "https://remote.example.test", + })).rejects.toThrow("loopback"); + await expect(createConfiguredBlobStore({ + STORAGE_BACKEND: "ipfs", STORAGE_EMULATOR: "true", + IPFS_PRIVATE_NETWORK: "true", IPFS_METADATA_BACKEND: "file", + IPFS_API_URL: "https://remote.example.test", + IPFS_CLUSTER_API_URL: "http://127.0.0.1:9094", + })).rejects.toThrow("loopback"); + }); + + it("validates resource names and refuses TLS bypass", async () => { + await expect(createConfiguredBlobStore({ + STORAGE_BACKEND: "azure", + AZURE_STORAGE_ACCOUNT_URL: "https://synthetic.blob.core.windows.net", + AZURE_STORAGE_CONTAINER: "../invalid", + })).rejects.toThrow("container name"); + await expect(createConfiguredBlobStore({ + STORAGE_BACKEND: "s3", S3_BUCKET: "not/a/bucket", AWS_REGION: "us-east-1", + })).rejects.toThrow("bucket name"); + await expect(createConfiguredBlobStore({ NODE_TLS_REJECT_UNAUTHORIZED: "0" })) + .rejects.toThrow("TLS certificate validation"); + vi.stubEnv("NODE_TLS_REJECT_UNAUTHORIZED", "0"); + try { + await expect(createConfiguredBlobStore({})).rejects.toThrow("TLS certificate validation"); + } finally { + vi.unstubAllEnvs(); + } + }); + + it("requires a private IPFS acknowledgement and durable index outside emulator mode", async () => { + await expect(createConfiguredBlobStore({ STORAGE_BACKEND: "ipfs" })) + .rejects.toThrow("IPFS_PRIVATE_NETWORK"); + for (const backend of ["file", "memory", "ipfs"]) { + await expect(createConfiguredBlobStore({ + STORAGE_BACKEND: "ipfs", IPFS_PRIVATE_NETWORK: "true", IPFS_METADATA_BACKEND: backend, + })).rejects.toThrow("IPFS metadata"); + } + }); + + it("rejects insufficient replication and missing remote API authentication", async () => { + const base = { + STORAGE_BACKEND: "ipfs", IPFS_PRIVATE_NETWORK: "true", IPFS_METADATA_BACKEND: "azure", + IPFS_API_URL: "https://private-kubo.example.test", + IPFS_CLUSTER_API_URL: "https://private-cluster.example.test", + }; + await expect(createConfiguredBlobStore({ ...base, IPFS_REPLICATION_MIN: "1" })) + .rejects.toThrow("replication"); + await expect(createConfiguredBlobStore({ + ...base, IPFS_REPLICATION_MIN: "3", IPFS_REPLICATION_MAX: "2", + })).rejects.toThrow("replication"); + await expect(createConfiguredBlobStore(base)).rejects.toThrow("runtime authorization"); + }); +}); diff --git a/test/configuration.spec.ts b/test/storage/config/settings-and-secrets.spec.ts similarity index 70% rename from test/configuration.spec.ts rename to test/storage/config/settings-and-secrets.spec.ts index c441ea8..e5c1ab1 100644 --- a/test/configuration.spec.ts +++ b/test/storage/config/settings-and-secrets.spec.ts @@ -3,10 +3,24 @@ import { join } from "node:path"; import { tmpdir } from "node:os"; import { afterAll, describe, expect, it, vi } from "vitest"; import { - flag, loadRuntimeSecret, positiveInteger, required, storageBackend, storageEndpoint, -} from "../src/storage/configuration.js"; + flag, positiveInteger, required, providerName, storageEndpoint, +} from "../../../src/storage/config/settings.js"; +import { loadRuntimeSecret } from "../../../src/storage/config/secrets.js"; const vault = vi.hoisted(() => ({ getSecret: vi.fn() })); +const aws = vi.hoisted(() => ({ send: vi.fn(), destroy: vi.fn() })); +vi.mock("@aws-sdk/client-secrets-manager", () => ({ + SecretsManagerClient: class { + send = aws.send; + destroy = aws.destroy; + }, + GetSecretValueCommand: class { + constructor(public input: { SecretId: string }) {} + }, +})); +vi.mock("@azure/identity", () => ({ + DefaultAzureCredential: class {}, +})); vi.mock("@azure/keyvault-secrets", () => ({ SecretClient: class { getSecret = vault.getSecret; @@ -19,14 +33,15 @@ afterAll(async () => { }); describe("storage configuration", () => { - it("defaults to file and rejects unknown or blank backends", () => { - expect(storageBackend({})).toBe("file"); - for (const backend of ["", "typo", "S3"]) { - expect(() => storageBackend({ STORAGE_BACKEND: backend })).toThrow(); + it("defaults to file and validates provider names independently of registration", () => { + expect(providerName({})).toBe("file"); + for (const backend of ["", "../provider", "S3", "__proto__", "x".repeat(65)]) { + expect(() => providerName({ STORAGE_BACKEND: backend })).toThrow(); } for (const backend of ["file", "memory", "s3", "azure", "ipfs"]) { - expect(storageBackend({ STORAGE_BACKEND: backend })).toBe(backend); + expect(providerName({ STORAGE_BACKEND: backend })).toBe(backend); } + expect(providerName({ STORAGE_BACKEND: "custom-provider" })).toBe("custom-provider"); }); it("validates required values, booleans, and integer limits", () => { @@ -68,6 +83,26 @@ describe("storage configuration", () => { }); describe("runtime secret sources", () => { + it("retrieves AWS secret strings with runtime identity and releases the client", async () => { + const env = { IPFS_AUTH_AWS_SECRET_ID: "synthetic-header", AWS_REGION: "us-east-1" }; + aws.send.mockResolvedValueOnce({ SecretString: "Bearer synthetic-from-aws" }); + expect(await loadRuntimeSecret(env, "IPFS_AUTH")).toBe("Bearer synthetic-from-aws"); + expect(aws.send).toHaveBeenCalledWith( + expect.objectContaining({ input: { SecretId: "synthetic-header" } }), + expect.objectContaining({ abortSignal: expect.any(AbortSignal) }), + ); + expect(aws.destroy).toHaveBeenCalled(); + aws.send.mockResolvedValueOnce({ SecretBinary: new Uint8Array([1, 2]) }); + await expect(loadRuntimeSecret(env, "IPFS_AUTH")).rejects.toThrow("nonempty"); + aws.send.mockRejectedValueOnce(new Error("synthetic AWS access denied")); + await expect(loadRuntimeSecret(env, "IPFS_AUTH")).rejects.toThrow("access denied"); + await expect(loadRuntimeSecret({ + ...env, IPFS_AUTH_FILE: "some-file", + }, "IPFS_AUTH")).rejects.toThrow("Choose one"); + await expect(loadRuntimeSecret({ IPFS_AUTH_AWS_SECRET_ID: "synthetic" }, "IPFS_AUTH")) + .rejects.toThrow("AWS_REGION"); + }); + it("retrieves a named Key Vault secret at runtime and propagates access failures", async () => { const env = { IPFS_AUTH_KEYVAULT_SECRET: "synthetic-token", diff --git a/test/blob-store-contract.spec.ts b/test/storage/core/blob-store-contract.spec.ts similarity index 82% rename from test/blob-store-contract.spec.ts rename to test/storage/core/blob-store-contract.spec.ts index e05827c..fa6ffd7 100644 --- a/test/blob-store-contract.spec.ts +++ b/test/storage/core/blob-store-contract.spec.ts @@ -2,8 +2,8 @@ import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterAll } from "vitest"; -import { FileBlobStore, InMemoryBlobStore } from "../src/index.js"; -import { blobStoreContract } from "./helpers/blob-store-contract.js"; +import { FileBlobStore, InMemoryBlobStore } from "../../../src/index.js"; +import { blobStoreContract } from "../../helpers/blob-store-contract.js"; const directories: string[] = []; afterAll(async () => { diff --git a/test/storage/dynamodb/dynamodb-blob-store.spec.ts b/test/storage/dynamodb/dynamodb-blob-store.spec.ts new file mode 100644 index 0000000..ca45d2d --- /dev/null +++ b/test/storage/dynamodb/dynamodb-blob-store.spec.ts @@ -0,0 +1,366 @@ +import { Readable } from "node:stream"; +import { DynamoDBClient, type AttributeValue } from "@aws-sdk/client-dynamodb"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + ConcurrentUpdateError, + DynamoDbBlobStore, + ObjectAlreadyExistsError, + ObjectNotFoundError, + StorageConfigurationError, + type StorageEnvironment, +} from "../../../src/index.js"; +import { + MAX_DYNAMODB_PAYLOAD_BYTES, +} from "../../../src/storage/adapters/dynamodb/dynamodb-blob-store.js"; +import { createStore } from "../../../src/storage/adapters/dynamodb/provider.js"; +import { blobStoreContract } from "../../helpers/blob-store-contract.js"; + +const key = "d".repeat(43); +const ciphertext = { algorithm: "AES-256-GCM", ciphertext: "synthetic+/==_-", nonce: "opaque" }; + +interface Request { + headers: Record; + body?: unknown; +} + +interface RequestBody { + TableName: string; + Key?: Record; + Item?: Record; + ConsistentRead?: boolean; + ConditionExpression?: string; + ExpressionAttributeNames?: Record; + ExpressionAttributeValues?: Record; + ReturnValues?: string; + ReturnValuesOnConditionCheckFailure?: string; +} + +class DynamoTransport { + readonly requests: { operation: string; input: RequestBody }[] = []; + readonly items = new Map>(); + error: string | undefined; + writeError: string | undefined; + networkError: Error | undefined; + loseWriteResponse = false; + deleteBeforeWrite = false; + + async handle(request: Request) { + const text = typeof request.body === "string" ? request.body + : request.body instanceof Uint8Array ? new TextDecoder().decode(request.body) : undefined; + if (text === undefined) throw new Error("Unexpected request body."); + const input = JSON.parse(text) as RequestBody; + const operation = request.headers["x-amz-target"]?.split(".").at(-1) ?? ""; + this.requests.push({ operation, input }); + const response = (body: object, statusCode = 200) => ({ + response: { + statusCode, + headers: { "content-type": "application/x-amz-json-1.0" }, + body: Readable.from([Buffer.from(JSON.stringify(body))]), + }, + }); + const failure = (name: string) => response({ + __type: `com.amazonaws.dynamodb.v20120810#${name}`, + message: "Storage request failed.", + }, 400); + if (this.networkError) throw this.networkError; + if (this.error) return failure(this.error); + if (operation === "GetItem") { + const id = input.Key?.key?.S; + if (!id) throw new Error("Missing request key."); + const item = this.items.get(id); + return response(item === undefined ? {} : { Item: item }); + } + if (operation !== "PutItem" || !input.Item?.key?.S) { + throw new Error("Unexpected storage request."); + } + if (this.writeError) return failure(this.writeError); + const id = input.Item.key.S; + if (this.deleteBeforeWrite) this.items.delete(id); + if (input.ConditionExpression === "attribute_not_exists(#key)") { + if (input.ExpressionAttributeNames?.["#key"] !== "key") throw new Error("Invalid create condition."); + if (this.items.has(id)) return failure("ConditionalCheckFailedException"); + } else if (input.ConditionExpression === "#version = :expected") { + if (input.ExpressionAttributeNames?.["#version"] !== "version" || + !input.ExpressionAttributeValues?.[":expected"]?.N) throw new Error("Invalid CAS condition."); + if (this.items.get(id)?.version?.N !== input.ExpressionAttributeValues[":expected"].N) { + return failure("ConditionalCheckFailedException"); + } + } else { + throw new Error("Unconditional writes are prohibited."); + } + this.items.set(id, input.Item); + if (this.loseWriteResponse) throw new Error("Connection reset."); + return response({}); + } +} + +function fixture(transport = new DynamoTransport(), prefix?: string) { + const client = new DynamoDBClient({ + region: "us-east-1", + credentials: { accessKeyId: "synthetic-access-key", secretAccessKey: "synthetic-secret-key" }, + maxAttempts: 1, + requestHandler: transport, + }); + return { + transport, client, + store: new DynamoDbBlobStore(client, "synthetic-table", prefix === undefined ? {} : { prefix }), + }; +} + +function row(version = 1, value: unknown = ciphertext): Record { + return { + key: { S: key }, + version: { N: String(version) }, + payload: { S: JSON.stringify({ version, value }) }, + }; +} + +afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllEnvs(); +}); + +blobStoreContract("DynamoDB independent SDK clients", async () => { + const { transport, store } = fixture(); + return { first: store, second: fixture(transport).store }; +}); + +describe("DynamoDbBlobStore", () => { + it("persists exact opaque JSON and uses strongly consistent reads and conditional writes", async () => { + const { store, transport } = fixture(); + await store.create(key, ciphertext); + expect(transport.items.get(key)).toEqual(row()); + expect(transport.requests[0]?.input).toMatchObject({ + TableName: "synthetic-table", + ConditionExpression: "attribute_not_exists(#key)", + ExpressionAttributeNames: { "#key": "key" }, + ReturnValues: "NONE", + ReturnValuesOnConditionCheckFailure: "NONE", + }); + expect(await store.read(key)).toEqual({ version: 1, value: ciphertext }); + await expect(store.compareAndSwap(key, 1, { ciphertext: "next" })).resolves.toBe(2); + for (const request of transport.requests.filter((request) => request.operation === "GetItem")) { + expect(request.input).toMatchObject({ ConsistentRead: true, Key: { key: { S: key } } }); + } + expect(transport.requests.at(-1)?.input).toMatchObject({ + ConditionExpression: "#version = :expected", + ExpressionAttributeNames: { "#version": "version" }, + ExpressionAttributeValues: { ":expected": { N: "1" } }, + Item: { version: { N: "2" } }, + }); + }); + + it("maps creation and CAS conflicts without mutating the winner", async () => { + const { store, transport } = fixture(); + await store.create(key, ciphertext); + await expect(store.create(key, null)).rejects.toBeInstanceOf(ObjectAlreadyExistsError); + transport.writeError = "ConditionalCheckFailedException"; + await expect(store.compareAndSwap(key, 1, null)).rejects.toBeInstanceOf(ConcurrentUpdateError); + expect(transport.items.get(key)).toEqual(row()); + }); + + it("has one winner when both independent clients read the same version", async () => { + const { transport, store } = fixture(); + await store.create(key, ciphertext); + const second = fixture(transport).store; + const results = await Promise.allSettled([ + store.compareAndSwap(key, 1, { ciphertext: "first" }), + second.compareAndSwap(key, 1, { ciphertext: "second" }), + ]); + expect(results.filter((result) => result.status === "fulfilled")).toHaveLength(1); + expect(results.find((result) => result.status === "rejected")).toMatchObject({ + reason: expect.any(ConcurrentUpdateError), + }); + expect(transport.requests.filter((request) => request.input.ConditionExpression === "#version = :expected")) + .toHaveLength(2); + }); + + it("does not recreate an item deleted between read and conditional update", async () => { + const { transport, store } = fixture(); + await store.create(key, ciphertext); + transport.deleteBeforeWrite = true; + await expect(store.compareAndSwap(key, 1, null)).rejects.toBeInstanceOf(ConcurrentUpdateError); + expect(transport.items.has(key)).toBe(false); + }); + + it("does not confuse missing items with missing tables, permissions, or throttling", async () => { + const { transport, store } = fixture(); + await expect(store.read(key)).resolves.toBeUndefined(); + await expect(store.compareAndSwap(key, 1, null)).rejects.toBeInstanceOf(ObjectNotFoundError); + for (const error of ["ResourceNotFoundException", "AccessDeniedException", "ProvisionedThroughputExceededException"]) { + transport.error = error; + const before = transport.requests.length; + await expect(store.read(key)).rejects.toMatchObject({ name: error }); + await expect(store.create(key, null)).rejects.toMatchObject({ name: error }); + await expect(store.compareAndSwap(key, 1, null)).rejects.toMatchObject({ name: error }); + expect(transport.requests.length - before).toBe(3); + } + }); + + it("propagates transport and CAS write errors without read-back or retries", async () => { + const { transport, store } = fixture(); + transport.networkError = new Error("Synthetic network failure."); + await expect(store.read(key)).rejects.toBe(transport.networkError); + await expect(store.create(key, ciphertext)).rejects.toBe(transport.networkError); + transport.networkError = undefined; + await store.create(key, ciphertext); + for (const name of ["ResourceNotFoundException", "AccessDeniedException", "ProvisionedThroughputExceededException"]) { + transport.writeError = name; + const before = transport.requests.length; + await expect(store.compareAndSwap(key, 1, null)).rejects.toMatchObject({ name }); + expect(transport.requests.length - before).toBe(2); + } + }); + + it("fails ambiguous create and CAS outcomes even when the write was committed", async () => { + const { transport, store } = fixture(); + transport.loseWriteResponse = true; + await expect(store.create(key, ciphertext)).rejects.toThrow("Connection reset."); + expect(transport.requests).toHaveLength(1); + expect(transport.items.get(key)).toEqual(row()); + await expect(store.compareAndSwap(key, 1, { ciphertext: "next" })).rejects.toThrow("Connection reset."); + expect(transport.requests).toHaveLength(3); + expect(transport.items.get(key)?.version).toEqual({ N: "2" }); + }); + + it.each([ + {}, { ...row(), key: { S: "other" } }, { ...row(), key: { N: "1" } }, + { key: { S: key }, version: { N: "1" } }, + { ...row(), payload: { N: "1" } }, { ...row(), version: { S: "1" } }, + { ...row(), version: { N: "0" } }, { ...row(), version: { N: "-1" } }, + { ...row(), version: { N: "1.5" } }, { ...row(), version: { N: "NaN" } }, + { ...row(), version: { N: "9007199254740992" } }, + { ...row(), version: { N: "2" } }, { ...row(), payload: { S: "not-json" } }, + { ...row(), payload: { S: '{"version":1}' } }, + { ...row(), payload: { S: '{"version":0,"value":null}' } }, + ])("rejects malformed or mismatched items before CAS writes (%#)", async (item) => { + const { transport, store } = fixture(); + transport.items.set(key, item); + await expect(store.read(key)).rejects.toThrow(); + await expect(store.compareAndSwap(key, 1, null)).rejects.toThrow(); + expect(transport.requests.every((request) => request.operation === "GetItem")).toBe(true); + }); + + it("accepts exactly 350 KiB of UTF-8 payload and rejects oversized reads and writes", async () => { + const { transport, store } = fixture(); + const overhead = Buffer.byteLength(JSON.stringify({ version: 1, value: "" })); + const exact = "x".repeat(MAX_DYNAMODB_PAYLOAD_BYTES - overhead); + await store.create(key, exact); + expect(Buffer.byteLength(transport.items.get(key)?.payload?.S ?? "")).toBe(MAX_DYNAMODB_PAYLOAD_BYTES); + expect((await store.read(key))?.value).toBe(exact); + const before = transport.requests.length; + for (const value of [exact + "x", "é".repeat(MAX_DYNAMODB_PAYLOAD_BYTES / 2)]) { + await expect(store.create(key, value)).rejects.toThrow("size limit"); + await expect(store.compareAndSwap(key, 1, value)).rejects.toThrow("size limit"); + } + expect(transport.requests).toHaveLength(before); + transport.items.set(key, row(1, exact + "x")); + await expect(store.read(key)).rejects.toThrow("size limit"); + }); + + it("normalizes bounded prefixes and preserves opaque keys", async () => { + for (const prefix of ["synthetic-integration", "synthetic-integration/", "opaque/nested/"]) { + const { transport, store } = fixture(new DynamoTransport(), prefix); + await store.create(key, ciphertext); + expect(await store.read(key)).toEqual({ version: 1, value: ciphertext }); + const qualified = `${prefix.replace(/\/$/, "")}/${key}`; + expect(transport.items.has(qualified)).toBe(true); + } + const { client } = fixture(); + for (const prefix of ["../", "a/../b", "/root", "a//b", "a\\b", "a%2fb", "x".repeat(513)]) { + expect(() => new DynamoDbBlobStore(client, "synthetic-table", { prefix })).toThrow("prefix"); + } + for (const table of ["ab", "x".repeat(256), "table/name", "table name"]) { + expect(() => new DynamoDbBlobStore(client, table)).toThrow("table"); + } + }); + + it("rejects invalid keys, versions, and unsupported JSON before network I/O", async () => { + const { transport, store } = fixture(); + for (const badKey of ["short", "../" + key, key + "/", "x".repeat(129), "é".repeat(32)]) { + await expect(store.read(badKey)).rejects.toThrow("key"); + await expect(store.create(badKey, null)).rejects.toThrow("key"); + await expect(store.compareAndSwap(badKey, 1, null)).rejects.toThrow("key"); + } + for (const version of [0, -1, 1.5, NaN, Infinity, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER + 1]) { + await expect(store.compareAndSwap(key, version, null)).rejects.toThrow("version"); + } + const circular: { self?: object } = {}; + circular.self = circular; + for (const value of [undefined, { missing: undefined }, [undefined], [,], NaN, Infinity, -0, + 1n, () => null, Symbol("synthetic"), new Date(0), new Map(), circular, new Proxy({}, {})]) { + await expect(store.create(key, value)).rejects.toThrow("Invalid or oversized storage object."); + await expect(store.compareAndSwap(key, 1, value)).rejects.toThrow("Invalid or oversized storage object."); + } + expect(transport.requests).toHaveLength(0); + }); + + it("allows the final safe version but rejects its increment", async () => { + const { transport, store } = fixture(); + transport.items.set(key, row(Number.MAX_SAFE_INTEGER - 1)); + expect(await store.compareAndSwap(key, Number.MAX_SAFE_INTEGER - 1, null)).toBe(Number.MAX_SAFE_INTEGER); + expect((await store.read(key))?.version).toBe(Number.MAX_SAFE_INTEGER); + const before = transport.requests.length; + await expect(store.compareAndSwap(key, Number.MAX_SAFE_INTEGER, null)).rejects.toThrow("version limit"); + expect(transport.requests).toHaveLength(before); + }); +}); + +function configured(environment: StorageEnvironment, emulator = false) { + return createStore({ + environment, emulator, + createStore: async () => { throw new Error("Unexpected nested provider."); }, + }); +} + +describe("DynamoDB provider", () => { + const environment = { DYNAMODB_TABLE: "synthetic-table", AWS_REGION: "us-east-1" }; + + it("uses the default credential provider and disables retries", async () => { + const send = vi.spyOn(DynamoDBClient.prototype, "send").mockImplementation(async () => ({ $metadata: {} })); + const store = await configured(environment); + expect(store).toBeInstanceOf(DynamoDbBlobStore); + await expect(store.read(key)).resolves.toBeUndefined(); + const client = send.mock.contexts[0]; + if (!(client instanceof DynamoDBClient)) throw new Error("Missing configured SDK client."); + expect(await client.config.maxAttempts()).toBe(1); + expect(await client.config.region()).toBe("us-east-1"); + expect(typeof client.config.credentials).toBe("function"); + client.destroy(); + }); + + it("accepts explicit secure endpoints and loopback emulators with prefixes", async () => { + for (const [endpoint, emulator] of [ + ["https://dynamodb.us-east-1.amazonaws.com", false], + ["http://127.0.0.1:8000", true], + ["http://localhost:8000", true], + ["http://[::1]:8000", true], + ] as const) { + const store = await configured({ ...environment, DYNAMODB_ENDPOINT_URL: endpoint, STORAGE_PREFIX: "synthetic/" }, emulator); + expect(store).toBeInstanceOf(DynamoDbBlobStore); + } + }); + + it.each([ + { DYNAMODB_TABLE: "", AWS_REGION: "us-east-1" }, + { DYNAMODB_TABLE: "synthetic-table", AWS_REGION: "" }, + { ...environment, DYNAMODB_TABLE: "invalid/table" }, + { ...environment, DYNAMODB_ENDPOINT_URL: "http://remote.invalid" }, + { ...environment, DYNAMODB_ENDPOINT_URL: "http://127.0.0.1:8000" }, + { ...environment, DYNAMODB_ENDPOINT_URL: "https://example.invalid/path" }, + { ...environment, DYNAMODB_ENDPOINT_URL: "https://example.invalid/?query=value" }, + { ...environment, DYNAMODB_ENDPOINT_URL: "https://user:synthetic@example.invalid/" }, + { ...environment, NODE_TLS_REJECT_UNAUTHORIZED: "0" }, + ])("fails closed on invalid provider configuration (%#)", async (env) => { + await expect(async () => configured(env)).rejects.toBeInstanceOf(StorageConfigurationError); + }); + + it("requires loopback endpoints for emulator mode and keeps process TLS enabled", async () => { + await expect(async () => configured(environment, true)).rejects.toBeInstanceOf(StorageConfigurationError); + await expect(async () => configured({ + ...environment, DYNAMODB_ENDPOINT_URL: "https://remote.invalid", + }, true)).rejects.toBeInstanceOf(StorageConfigurationError); + vi.stubEnv("NODE_TLS_REJECT_UNAUTHORIZED", "0"); + await expect(async () => configured(environment)).rejects.toBeInstanceOf(StorageConfigurationError); + }); +}); diff --git a/test/file-blob-store.spec.ts b/test/storage/file/file-blob-store.spec.ts similarity index 98% rename from test/file-blob-store.spec.ts rename to test/storage/file/file-blob-store.spec.ts index cd2f6f1..ade4313 100644 --- a/test/file-blob-store.spec.ts +++ b/test/storage/file/file-blob-store.spec.ts @@ -8,7 +8,7 @@ import { ConcurrentUpdateError, FileBlobStore, ObjectAlreadyExistsError, -} from "../src/index.js"; +} from "../../../src/index.js"; const temporaryDirectories: string[] = []; diff --git a/test/storage/ipfs/backed-up-content-store.spec.ts b/test/storage/ipfs/backed-up-content-store.spec.ts new file mode 100644 index 0000000..1fa83d6 --- /dev/null +++ b/test/storage/ipfs/backed-up-content-store.spec.ts @@ -0,0 +1,179 @@ +import { randomBytes } from "node:crypto"; +import { describe, expect, it, vi } from "vitest"; +import { + BackedUpContentStore, DistributedBlobStore, InMemoryBlobStore, PatientRecordService, + createClinicianCredential, type DistributedContentStore, +} from "../../../src/index.js"; +import { + rawContentCid, MAX_DISTRIBUTED_BLOB_BYTES, +} from "../../../src/storage/adapters/ipfs/distributed-blob-store.js"; + +class TestContent implements DistributedContentStore { + public readonly blocks = new Map(); + public async put(bytes: Uint8Array): Promise { + const cid = await rawContentCid(bytes); + this.blocks.set(cid, Uint8Array.from(bytes)); + return cid; + } + public async get(cid: string): Promise { + const bytes = this.blocks.get(cid); + if (!bytes) throw new Error("Synthetic primary unavailable."); + return Uint8Array.from(bytes); + } +} + +function fixture() { + const primary = new TestContent(); + const backup = new InMemoryBlobStore(); + const metadata = new InMemoryBlobStore(); + const content = new BackedUpContentStore(primary, backup); + const store = new DistributedBlobStore(metadata, content); + return { primary, backup, metadata, content, store }; +} + +describe("durable encrypted content backup composition", () => { + it("requires primary and backup completion before publishing a pointer", async () => { + const { store, metadata, primary, backup } = fixture(); + const order: string[] = []; + const put = primary.put.bind(primary); + vi.spyOn(primary, "put").mockImplementation(async (bytes) => { + order.push("primary"); return put(bytes); + }); + const save = backup.create.bind(backup); + vi.spyOn(backup, "create").mockImplementation(async (key, value) => { + order.push("backup"); return save(key, value); + }); + const publish = metadata.create.bind(metadata); + vi.spyOn(metadata, "create").mockImplementation(async (key, value) => { + order.push("metadata"); return publish(key, value); + }); + await store.create("a".repeat(32), { ciphertext: "synthetic" }); + expect(order).toEqual(["primary", "backup", "metadata"]); + expect(Object.keys(backup.snapshot())).toHaveLength(1); + }); + + it("leaves new pointers absent and old state untouched when backup writes fail", async () => { + const { store, metadata, backup } = fixture(); + const existing = "a".repeat(32); + await store.create(existing, { ciphertext: "initial" }); + const before = metadata.snapshot(); + vi.spyOn(backup, "create").mockRejectedValue(new Error("Synthetic backup unavailable.")); + await expect(store.create("b".repeat(32), { ciphertext: "new" })).rejects.toThrow("backup unavailable"); + await expect(store.compareAndSwap(existing, 1, { ciphertext: "changed" })).rejects.toThrow(); + expect(metadata.snapshot()).toEqual(before); + expect(await store.read(existing)).toEqual({ version: 1, value: { ciphertext: "initial" } }); + }); + + it("does not claim success after primary failures or invalid returned CIDs", async () => { + const { content, primary, backup } = fixture(); + const bytes = randomBytes(32); + vi.spyOn(primary, "put").mockRejectedValueOnce(new Error("Synthetic pin failure.")); + await expect(content.put(bytes)).rejects.toThrow("pin failure"); + expect(backup.snapshot()).toEqual({}); + vi.spyOn(primary, "put").mockResolvedValueOnce(await rawContentCid(randomBytes(32))); + await expect(content.put(bytes)).rejects.toThrow("integrity"); + expect(backup.snapshot()).toEqual({}); + }); + + it("does not publish metadata after an ambiguous backup response", async () => { + const { store, backup, metadata } = fixture(); + const create = backup.create.bind(backup); + vi.spyOn(backup, "create").mockImplementation(async (key, value) => { + await create(key, value); + throw new Error("Synthetic backup response lost after commit."); + }); + await expect(store.create("a".repeat(32), { ciphertext: "synthetic" })) + .rejects.toThrow("response lost"); + expect(Object.keys(backup.snapshot())).toHaveLength(1); + expect(metadata.snapshot()).toEqual({}); + }); + + it("deduplicates matching backups but rejects changed or missing duplicate backups", async () => { + const { content, backup } = fixture(); + const bytes = randomBytes(32); + const cid = await content.put(bytes); + expect(await content.put(bytes)).toBe(cid); + expect((await backup.read(cid))?.version).toBe(1); + const original = await backup.read(cid); + await backup.compareAndSwap(cid, 1, original!.value); + await expect(content.put(bytes)).rejects.toThrow("Immutable content backup"); + }); + + it("requires explicit restore and never masks missing primary data", async () => { + const { store, metadata, primary } = fixture(); + const key = "a".repeat(32); + await store.create(key, { ciphertext: "synthetic" }); + const before = metadata.snapshot(); + primary.blocks.clear(); + await expect(store.read(key)).rejects.toThrow("primary unavailable"); + await store.restore(key); + expect(await store.read(key)).toEqual({ version: 1, value: { ciphertext: "synthetic" } }); + expect(metadata.snapshot()).toEqual(before); + }); + + it("rejects corrupt backup bytes and failed restore replication without updating metadata", async () => { + const { store, metadata, backup, primary } = fixture(); + const key = "a".repeat(32); + await store.create(key, { ciphertext: "synthetic" }); + const before = metadata.snapshot(); + const read = backup.read.bind(backup); + vi.spyOn(backup, "read").mockImplementation(async (cid: string) => { + const stored = await read(cid); + if (stored && typeof stored.value === "object" && stored.value !== null && "bytes" in stored.value) { + stored.value.bytes = randomBytes(32).toString("base64"); + } + return stored; + }); + primary.blocks.clear(); + await expect(store.restore(key)).rejects.toThrow("integrity"); + expect(primary.blocks.size).toBe(0); + expect(metadata.snapshot()).toEqual(before); + vi.restoreAllMocks(); + vi.spyOn(primary, "put").mockRejectedValue(new Error("Synthetic restore pin failure.")); + await expect(store.restore(key)).rejects.toThrow("restore pin failure"); + expect(metadata.snapshot()).toEqual(before); + }); + + it("refuses missing keys, unavailable backups, and restores without a backup provider", async () => { + const { store, content, primary } = fixture(); + await expect(store.restore("a".repeat(32))).rejects.toThrow("Object not found"); + await expect(content.restore(await rawContentCid(randomBytes(32)))).rejects.toThrow("unavailable"); + const withoutBackup = new DistributedBlobStore(new InMemoryBlobStore(), primary); + await expect(withoutBackup.restore("a".repeat(32))).rejects.toThrow("no configured backup"); + }); + + it("bounds backup size before uploading", async () => { + const { content, primary } = fixture(); + await expect(content.put(new Uint8Array(MAX_DISTRIBUTED_BLOB_BYTES + 1))).rejects.toThrow("size"); + await expect(content.put(new Uint8Array())).rejects.toThrow("size"); + expect(primary.blocks.size).toBe(0); + }); + + it("restores an encrypted record without persisting patient factors or plaintext in backups", async () => { + const { store, primary, backup } = fixture(); + const records = new PatientRecordService(store); + const access = { clinician: createClinicianCredential(), biometricToken: randomBytes(32).toString("base64url") }; + await records.enroll(access); + const entry = await records.append({ + ...access, + resource: { resourceType: "Condition", id: "synthetic-backup-record", code: { text: "SYNTHETIC-BACKUP-MARKER" } }, + }); + const snapshot = backup.snapshot(); + for (const stored of Object.values(snapshot)) { + const value = stored.value; + if (typeof value !== "object" || value === null || !("bytes" in value) || + typeof value.bytes !== "string") throw new Error("Invalid fixture."); + const decoded = Buffer.from(value.bytes, "base64").toString("utf8"); + expect(decoded).not.toContain("SYNTHETIC-BACKUP-MARKER"); + expect(decoded).not.toContain(access.biometricToken); + expect(decoded).not.toContain(access.clinician.privateKeyPem); + } + const rawEvent = await store.read(entry.eventId); + expect(rawEvent).toBeDefined(); + const cid = await rawContentCid(new TextEncoder().encode(JSON.stringify({ version: 1, value: rawEvent!.value }))); + primary.blocks.delete(cid); + await expect(records.assemble(access)).rejects.toThrow(); + await store.restore(entry.eventId); + expect(await records.assemble(access)).toHaveLength(1); + }); +}); diff --git a/test/distributed-blob-store.spec.ts b/test/storage/ipfs/distributed-blob-store.spec.ts similarity index 98% rename from test/distributed-blob-store.spec.ts rename to test/storage/ipfs/distributed-blob-store.spec.ts index f77f495..616f3fc 100644 --- a/test/distributed-blob-store.spec.ts +++ b/test/storage/ipfs/distributed-blob-store.spec.ts @@ -3,11 +3,11 @@ import { MAX_DISTRIBUTED_BLOB_BYTES, rawContentCid, type DistributedContentStore, -} from "../src/storage/distributed-blob-store.js"; +} from "../../../src/storage/adapters/ipfs/distributed-blob-store.js"; import { DistributedBlobStore, InMemoryBlobStore, ConcurrentUpdateError, ObjectAlreadyExistsError, ObjectNotFoundError, -} from "../src/index.js"; +} from "../../../src/index.js"; const key = "a".repeat(32); class Content implements DistributedContentStore { diff --git a/test/ipfs-cluster-client.spec.ts b/test/storage/ipfs/ipfs-cluster-client.spec.ts similarity index 98% rename from test/ipfs-cluster-client.spec.ts rename to test/storage/ipfs/ipfs-cluster-client.spec.ts index 0fdc242..dbfce53 100644 --- a/test/ipfs-cluster-client.spec.ts +++ b/test/storage/ipfs/ipfs-cluster-client.spec.ts @@ -1,7 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -import { IpfsClusterClient } from "../src/index.js"; -import type { IpfsClusterClientOptions } from "../src/storage/ipfs-cluster-client.js"; -import { MAX_DISTRIBUTED_BLOB_BYTES, rawContentCid, validateRawCid } from "../src/storage/distributed-blob-store.js"; +import { IpfsClusterClient, type IpfsClusterClientOptions } from "../../../src/index.js"; +import { MAX_DISTRIBUTED_BLOB_BYTES, rawContentCid, validateRawCid } from "../../../src/storage/adapters/ipfs/distributed-blob-store.js"; const bytes = Uint8Array.from([1, 2, 3]); const base = { kuboUrl: "https://kubo.invalid", clusterUrl: "https://cluster.invalid" }; diff --git a/test/s3-blob-store.spec.ts b/test/storage/s3/s3-blob-store.spec.ts similarity index 99% rename from test/s3-blob-store.spec.ts rename to test/storage/s3/s3-blob-store.spec.ts index 9fa97ba..f069d1b 100644 --- a/test/s3-blob-store.spec.ts +++ b/test/storage/s3/s3-blob-store.spec.ts @@ -6,7 +6,7 @@ import { ObjectAlreadyExistsError, ObjectNotFoundError, S3BlobStore, -} from "../src/index.js"; +} from "../../../src/index.js"; const key = "s".repeat(43); const ciphertext = { algorithm: "AES-256-GCM", ciphertext: "synthetic-ciphertext", nonce: "opaque" }; From 1cf6145fe12c2459663a67e13ddba0a92dc2b882 Mon Sep 17 00:00:00 2001 From: Karthik Chandrasekaran Date: Sat, 19 Sep 2026 09:17:10 -0500 Subject: [PATCH 6/6] Exercise configured backups in private IPFS integration Keep emulator backups in fixture-owned temporary files and clarify the separate federated smoke-test role and prefix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fc4430e2-35f5-48b8-bd0f-5dc6fad2a0ff --- docs/AWS_HYBRID_STORAGE.md | 8 ++++++++ docs/STORAGE_QUICKSTART.md | 9 ++++++++- scripts/run-ipfs-integration.ts | 1 + test/fixtures/ipfs/README.md | 10 ++++++++-- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/docs/AWS_HYBRID_STORAGE.md b/docs/AWS_HYBRID_STORAGE.md index 7b200d4..987b08e 100644 --- a/docs/AWS_HYBRID_STORAGE.md +++ b/docs/AWS_HYBRID_STORAGE.md @@ -150,6 +150,14 @@ For another compute service, create an independently reviewed workload role with equivalent narrowly scoped permissions and the correct service/OIDC trust; do not broaden this node trust or use permanent keys. +The repository's optional manual cloud smoke workflow is separate from this +deployment path. It uses a protected GitHub environment, a separate federated +role, and `synthetic-integration/`, not this node role's `records/` prefix. +Use dedicated synthetic resources, and scope that role's object permissions +and S3 KMS encryption context to the workflow prefix. The EC2-only `NodeRoleArn` +is not a valid value for the workflow's `AWS_ROLE_ARN`. See the +[storage quickstart](STORAGE_QUICKSTART.md) before enabling it. + `s3:ListBucket` is scoped to the **single dedicated backup bucket ARN**, not all buckets. S3 needs bucket-list authorization to distinguish a nonexistent GetObject key (404) from access denied (403) [17]. The adapter must not treat diff --git a/docs/STORAGE_QUICKSTART.md b/docs/STORAGE_QUICKSTART.md index 3921b0c..15a9431 100644 --- a/docs/STORAGE_QUICKSTART.md +++ b/docs/STORAGE_QUICKSTART.md @@ -32,7 +32,8 @@ cannot be unlocked later. Use a dedicated disposable store for this example. On a native Linux Docker host (including the Ubuntu CI runner), `npm run test:integration:ipfs` runs the isolated private distributed-storage fixture. Docker Desktop and remote Docker daemons are not supported by this -fixture. See [distributed storage](DISTRIBUTED_STORAGE.md) +fixture. It uses temporary file metadata/backups, including verified content +restore, never an inherited cloud-backup setting. See [distributed storage](DISTRIBUTED_STORAGE.md) for architecture, network restrictions, availability, and coordination limits. ## Configure Azure or AWS later @@ -134,6 +135,12 @@ short-lived token; the cloud exchanges it for temporary credentials. No long-lived cloud credential is stored in GitHub. Do not grant the cloud identity to PR/fork workflows, upload environment dumps, or enable SDK request tracing. +The workflow deliberately uses `synthetic-integration/`. Its AWS federated role +needs object permissions and an S3 KMS encryption-context scope for that prefix. +Do not set `AWS_ROLE_ARN` to the foundation's EC2-only `NodeRoleArn`: that role +trusts EC2, not GitHub, and its object permissions cover `records/`. +Use separately reviewed synthetic resources and OIDC permissions. + After approval, select Azure, S3, DynamoDB, or `aws-hybrid` in **Actions > Reviewed cloud storage smoke**. Hybrid mode selects private IPFS with DynamoDB metadata and synchronous SSE-KMS S3 backups; it additionally diff --git a/scripts/run-ipfs-integration.ts b/scripts/run-ipfs-integration.ts index 361d473..38df19e 100644 --- a/scripts/run-ipfs-integration.ts +++ b/scripts/run-ipfs-integration.ts @@ -163,6 +163,7 @@ async function runSuite(kubo: string, cluster: string): Promise { ...environment, STORAGE_INTEGRATION: "true", STORAGE_BACKEND: "ipfs", STORAGE_EMULATOR: "true", IPFS_PRIVATE_NETWORK: "true", IPFS_API_URL: kubo, IPFS_CLUSTER_API_URL: cluster, IPFS_METADATA_BACKEND: "file", + IPFS_BACKUP_BACKEND: "file", STORAGE_FILE_DIRECTORY: metadataDirectory, IPFS_REPLICATION_MIN: "2", IPFS_REPLICATION_MAX: "3", }, stdio: "inherit", diff --git a/test/fixtures/ipfs/README.md b/test/fixtures/ipfs/README.md index 43a1b97..3f78d03 100644 --- a/test/fixtures/ipfs/README.md +++ b/test/fixtures/ipfs/README.md @@ -61,12 +61,18 @@ IPFS_PRIVATE_NETWORK=true IPFS_API_URL=http://127.0.0.1: IPFS_CLUSTER_API_URL=http://127.0.0.1: IPFS_METADATA_BACKEND=file -STORAGE_FILE_DIRECTORY= +IPFS_BACKUP_BACKEND=file +STORAGE_FILE_DIRECTORY= IPFS_REPLICATION_MIN=2 IPFS_REPLICATION_MAX=3 ``` -Readiness requires all three cluster members and allocation metrics. After +The isolated file backend holds index pointers and CID-addressed synthetic +backups. The suite includes explicit backup restoration with an unchanged +metadata version. This is not an S3 or AWS integration result. + +Readiness requires each Raft member to finish joining before starting the next, +then all three allocation metrics. After the application suite succeeds, the runner adds a separate random opaque 1024-byte synthetic ciphertext marker to Kubo 1, requests Cluster replication with minimum 2 / maximum 3, and waits for recursive pins on **all three**