diff --git a/.changeset/app-owned-storage-composition.md b/.changeset/app-owned-storage-composition.md new file mode 100644 index 0000000..d58db76 --- /dev/null +++ b/.changeset/app-owned-storage-composition.md @@ -0,0 +1,8 @@ +--- +"@nestm/crypto": minor +--- + +Remove the storage-specific `@nestm/crypto/storage-workspace` integration and +its optional `@nestm/storage` peer. Applications now compose storage policy, +artifact record formats, authenticated path context, and searchable projections +from the generic primitives exposed by `@nestm/crypto/core`. diff --git a/README.md b/README.md index 1fdc592..72ca423 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,7 @@ Authenticated, versioned encryption for Node.js and NestJS 12. The package provides an AES-256-GCM core, envelope key providers, explicit field traversal, cloud KMS adapters, a -fail-closed bridge to `@nestm/tenant`, an optional protected-workspace bridge to `@nestm/storage`, -and opt-in HTTP and Prisma field adapters. +fail-closed bridge to `@nestm/tenant`, and opt-in HTTP and Prisma field adapters. > This package is an alpha. Pin an exact version, retain every key needed by stored ciphertext, and > test rotation and recovery before using it for production data. @@ -25,9 +24,6 @@ pnpm add @nestjs/common@next @nestjs/core@next reflect-metadata rxjs # Optional tenant bridge pnpm add @nestm/tenant -# Optional protected StorageWorkspace bridge (pin prerelease integrations exactly) -pnpm add @nestm/storage@0.1.0-alpha.9 - # Optional HTTP DTO adapter pnpm add class-transformer class-validator @@ -37,7 +33,7 @@ pnpm add @google-cloud/kms pnpm add @azure/core-auth @azure/keyvault-keys ``` -Cloud SDKs, NestJS, `@nestm/storage`, `@nestm/tenant`, and `class-transformer` are optional peers. +Cloud SDKs, NestJS, `@nestm/tenant`, and `class-transformer` are optional peers. `class-validator` is application-owned and needed only when the app uses Nest's `ValidationPipe` validation. The Prisma adapter is schema-agnostic and uses the consuming application's Prisma client, so it adds no Prisma dependency. A consumer that imports only `@nestm/crypto/core` installs none of @@ -183,101 +179,6 @@ An authenticated full frame may be emitted before a later frame or EOF fails. A that authenticated prefix and abort on failure, but agents, previews, archive/skill validation, and other side-effecting consumers must stage all plaintext until `verification` resolves. -## Protected storage workspaces - -`@nestm/crypto/storage-workspace` composes an already-authorized `StorageWorkspace` from -`@nestm/storage/workspace` with an application-supplied authenticated cipher. The adapter imports -both libraries only from this optional subpath: `@nestm/crypto/core` and the root entry point remain -independent of storage, while `@nestm/storage` remains independent of key providers and crypto -policy. - -The built-in `createCipherEngineWorkspaceCipher()` adapter uses `CipherEngine` and the bounded -`nmc1` format. It strictly encrypts the logical metadata record and body independently, and it rejects -plaintext, malformed records, wrong scope/policy/path context, and unauthenticated envelopes on read. -The backing workspace stores one versioned outer JSON record whose `metadata` and `content` members -are `nmc1` envelopes; it never receives the original body or logical content type. - -```ts -import type { StorageWorkspace } from "@nestm/storage/workspace"; -import type { CipherEngine } from "@nestm/crypto/core"; -import { - createCipherEngineWorkspaceCipher, - protectStorageWorkspace, -} from "@nestm/crypto/storage-workspace"; - -declare const mountedWorkspace: StorageWorkspace; -declare const engine: CipherEngine; -declare const canonicalOrganizationId: string; -declare const canonicalWorkspaceId: string; - -const logicalLimits = { - ...mountedWorkspace.limits, - maxReadBytes: 1024 * 1024, - maxWriteBytes: 1024 * 1024, -}; -const protectedWorkspace = protectStorageWorkspace({ - storage: mountedWorkspace, - cipher: createCipherEngineWorkspaceCipher(engine, { - allowedProviders: ["workspace-current", "workspace-previous"], - }), - scopeContext: `organization:${canonicalOrganizationId}/workspace:${canonicalWorkspaceId}`, - policyRevision: "artifact-body:v1", - limits: logicalLimits, - // This is a separate ceiling for the expanded outer record held by the backing workspace. - maxCiphertextBytes: 2 * 1024 * 1024, - // Explicitly accepts that the backing provider can search visible object paths. - pathSearch: "provider-visible", -}); - -await protectedWorkspace.writeFile("reports/result.md", "# Verified\n", { - mode: "create", - contentType: "text/markdown; charset=utf-8", -}); -const restored = await protectedWorkspace.readText("reports/result.md"); -``` - -On the initial protected view, the path, page, search, cursor, and cursor-TTL limits must match the -already-mounted backing workspace because that capability owns their enforcement. Logical -`maxReadBytes` and `maxWriteBytes` may be lower. A derived `mount()` creates a correspondingly narrowed -backing capability, so every child limit can be reduced safely. Configure `CipherEngine.maxPayloadBytes` -to cover the larger logical read/write ceiling and size `maxCiphertextBytes` for `nmc1` plus outer-record -expansion; a plaintext value within its logical limit can still exceed an undersized physical ceiling. - -`scopeContext` and `policyRevision` are authenticated domain inputs. Derive the scope from trusted, -canonical application context and keep both values reproducible for the lifetime of stored objects. -Do not change `policyRevision` merely because the active wrapping key rotates; use an explicit -read/migrate policy when changing the authenticated policy domain. The cipher adapter does not take -ownership of `CipherEngine`, so the application that created the engine must close it. - -`AuthenticatedWorkspaceCipher` is a trusted composition contract for alternative implementations. -Implementations must remain bounded, authenticate both supplied contexts, honor cancellation, and not -retain borrowed plaintext or context buffers after a call settles. The built-in adapter supplies those -properties through `CipherEngine`; registered cipher algorithms and key providers remain part of the -application's crypto trust boundary. - -The canonical logical path and record purpose are also bound automatically, so copying a raw backing -record to another path does not produce valid plaintext there. Protected `copyFile()` and `moveFile()` -therefore fail with `NOT_SUPPORTED`. An authorized application workflow must read/authenticate the -source and write a newly encrypted destination; a move may then conditionally delete the source, with -the same multi-object transactional caveats as any application-managed move. - -The bridge deliberately does not hide all storage metadata. Object paths, directory shape, existence, -ciphertext length, provider ETag/timestamps, and access patterns remain visible to the backing store. -Path search is disabled unless `pathSearch: "provider-visible"` explicitly accepts that leakage. -Applications such as artifact catalogs should keep only deliberately searchable fields—safe IDs, -titles, descriptions, status, and other chosen projections—in their own authorized catalog and store -the sensitive artifact body through this protected workspace. The bridge does not encrypt arbitrary -database columns or decide which catalog fields are safe. - -Protected list, stat, and enabled search operations authenticate file metadata before returning it. -Synthetic directories, provider-level existence, cursors, and access patterns remain storage signals, -and the file body is authenticated only by a protected read. - -`protectedWorkspace.protection` exposes a frozen, non-secret integration descriptor containing the -outer format/version, `nmc1` envelope, path binding, metadata/body protection, path-search mode, and -policy revision. It describes the configured boundary; it does not reveal provider keys, storage -coordinates, plaintext, or ciphertext. - ## Operations Both `CipherService` and `CipherEngine` provide: @@ -750,20 +651,19 @@ plaintext with `@nestm/crypto`; relabeling or importing the old ciphertext is no ## Entry points -| Entry point | Purpose | -| --------------------------------- | ----------------------------------------------------------------------------- | -| `@nestm/crypto/core` | AES-256-GCM engine, envelope codec, local AES KEK ring, contracts, and errors | -| `@nestm/crypto/files` | NMF1 bounded-memory streaming file encryption with detached wrapped keys | -| `@nestm/crypto/storage-workspace` | strict authenticated body/metadata protection for `StorageWorkspace` | -| `@nestm/crypto/fields` | purpose-decorated class traversal | -| `@nestm/crypto/tenant` | tenant-bound cipher and field services | -| `@nestm/crypto/http` | request-encryption pipe and opt-in response-decryption interceptor | -| `@nestm/crypto/prisma` | schema-agnostic tenant Prisma write processor | -| `@nestm/crypto/key-wrap/rsa` | RSA-OAEP-SHA256 wrapping with named public/private keys | -| `@nestm/crypto/kms/aws` | AWS KMS data-key generation and decrypt | -| `@nestm/crypto/kms/gcp` | Google Cloud KMS encrypt/decrypt with AAD and CRC32C validation | -| `@nestm/crypto/kms/azure` | Azure Key Vault/Managed HSM wrap/unwrap | -| `@nestm/crypto/testing` | dependency-free deterministic nonce source for tests | +| Entry point | Purpose | +| ---------------------------- | ----------------------------------------------------------------------------- | +| `@nestm/crypto/core` | AES-256-GCM engine, envelope codec, local AES KEK ring, contracts, and errors | +| `@nestm/crypto/files` | NMF1 bounded-memory streaming file encryption with detached wrapped keys | +| `@nestm/crypto/fields` | purpose-decorated class traversal | +| `@nestm/crypto/tenant` | tenant-bound cipher and field services | +| `@nestm/crypto/http` | request-encryption pipe and opt-in response-decryption interceptor | +| `@nestm/crypto/prisma` | schema-agnostic tenant Prisma write processor | +| `@nestm/crypto/key-wrap/rsa` | RSA-OAEP-SHA256 wrapping with named public/private keys | +| `@nestm/crypto/kms/aws` | AWS KMS data-key generation and decrypt | +| `@nestm/crypto/kms/gcp` | Google Cloud KMS encrypt/decrypt with AAD and CRC32C validation | +| `@nestm/crypto/kms/azure` | Azure Key Vault/Managed HSM wrap/unwrap | +| `@nestm/crypto/testing` | dependency-free deterministic nonce source for tests | Adapters accept caller-owned SDK clients as well as configuration-created clients. Caller-owned clients are never closed; owned clients are closed or released where the SDK exposes that lifecycle. Each cloud diff --git a/SECURITY.md b/SECURITY.md index ab78156..d82b68b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -78,53 +78,6 @@ Local AES and RSA providers shift key custody into the application. Load their s secret source, keep active wrapping keys separate from legacy decrypt-only keys, and retain old keys until all dependent ciphertext has been re-encrypted and verified. -## Protected-storage-workspace boundary - -`@nestm/crypto/storage-workspace` is an optional composition layer over an already-mounted, -application-authorized `StorageWorkspace`. It does not select a provider, bucket, filesystem root, -tenant, mount prefix, or key route. The application remains responsible for constructing the storage -capability and authenticated cipher independently and for deriving `scopeContext` and -`policyRevision` from stable, trusted policy state. - -The bridge stores one strict, versioned outer JSON record. Logical metadata and file content are -separate `nmc1` envelopes with distinct authenticated purposes. Their AAD binds the workspace scope, -policy revision, canonical relative path, record version, and purpose. Plaintext records, unknown -members or versions, malformed envelopes, swapped metadata/content envelopes, and a raw record copied -to another path fail closed during protected reads. Protected copy and move are intentionally denied. -An authorized application must read/authenticate the source, write/re-encrypt the destination, and—if -moving—conditionally delete the source while accounting for the lack of a cross-object transaction. -Raw backing-store mutations bypass the bridge and can cause an availability failure. - -This boundary encrypts the logical body and its protected metadata record, not every storage signal. -The backing store still observes physical keys, directory shape, object existence, ciphertext sizes, -provider ETags/timestamps, operation timing, and access patterns. Provider-visible path search is an -explicit opt-in; keep it disabled when path disclosure or equality is unacceptable. Applications that -need searchable titles, descriptions, statuses, or other projections should maintain a separate, -authorized catalog and choose those plaintext fields deliberately. This package neither encrypts -arbitrary catalog/database fields nor decides which projections are safe to expose. - -File metadata returned by protected list, stat, and search operations is authenticated. Synthetic -directories, provider-level existence, and continuation behavior still describe backing-store state, -and none of those operations proves that a body authenticates; body authentication occurs on protected -read. The read-only protection descriptor reports the configured bridge contract but is not a -remote-attestation mechanism and must not be used as authorization evidence. - -The `CipherEngine` workspace adapter is buffered. Configure independent logical plaintext and outer -ciphertext ceilings, ensure the backing workspace can hold the expanded record, and propagate abort -signals and deadlines. Use NMF1 `@nestm/crypto/files` behind a purpose-built integration when immutable -multi-gigabyte streaming objects are required; do not weaken the bounded `nmc1` bridge with an -application-side plaintext fallback. As with the core envelope, deletion and same-path rollback to an -older valid record are not prevented; applications needing freshness must enforce it in an -authenticated catalog or version policy. - -At initial composition, non-buffer limits must match the already-mounted backing workspace; only the -logical read/write limits may be lower. Derived mounts create narrowed backing capabilities and may -reduce every limit. The injected `AuthenticatedWorkspaceCipher` is trusted to enforce its own bounds, -honor the operation signal, and stop using borrowed buffers when its promise settles. Prefer the built-in -`CipherEngine` adapter unless an alternative has equivalent conformance coverage. Short invalid paths -are byte-bounded before crypto but receive their complete canonical path validation from the backing -workspace, so a rejected create/overwrite may perform cryptographic work without persisting an object. - ## Tenant boundary `@nestm/crypto/tenant` derives the canonical tenant only from `TENANT_CONTEXT_READER`. Its ordinary diff --git a/package.json b/package.json index b3192f1..a5f0d4b 100644 --- a/package.json +++ b/package.json @@ -50,11 +50,6 @@ "import": "./dist/files/index.mjs", "default": "./dist/files/index.mjs" }, - "./storage-workspace": { - "types": "./dist/storage-workspace/index.d.mts", - "import": "./dist/storage-workspace/index.mjs", - "default": "./dist/storage-workspace/index.mjs" - }, "./fields": { "types": "./dist/fields/index.d.mts", "import": "./dist/fields/index.mjs", @@ -149,7 +144,6 @@ "@azure/core-auth": ">=1.11.0 <2", "@azure/keyvault-keys": ">=4.10.2 <5", "@google-cloud/kms": ">=5.7.0 <6", - "@nestm/storage": ">=0.1.0-alpha.9 <0.2.0", "@nestm/tenant": ">=0.1.0-alpha.3 <0.2.0", "@nestjs/common": "^12.0.0-alpha.5", "@nestjs/core": "^12.0.0-alpha.5", @@ -170,9 +164,6 @@ "@google-cloud/kms": { "optional": true }, - "@nestm/storage": { - "optional": true - }, "@nestm/tenant": { "optional": true }, @@ -199,7 +190,6 @@ "@azure/keyvault-keys": "4.10.2", "@changesets/cli": "2.31.1", "@google-cloud/kms": "5.7.0", - "@nestm/storage": "0.1.0-alpha.9", "@nestm/tenant": "0.1.0-alpha.5", "@nestjs/common": "12.0.0-alpha.5", "@nestjs/core": "12.0.0-alpha.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f01205..bc14a12 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,9 +35,6 @@ importers: '@nestjs/testing': specifier: 12.0.0-alpha.5 version: 12.0.0-alpha.5(@nestjs/common@12.0.0-alpha.5(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.0.0-alpha.5(@nestjs/common@12.0.0-alpha.5(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(reflect-metadata@0.2.2)(rxjs@7.8.2)) - '@nestm/storage': - specifier: 0.1.0-alpha.9 - version: 0.1.0-alpha.9(@azure/core-auth@1.11.0(supports-color@7.2.0))(@nestjs/common@12.0.0-alpha.5(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.0.0-alpha.5(@nestjs/common@12.0.0-alpha.5(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(reflect-metadata@0.2.2)(rxjs@7.8.2))(google-auth-library@10.5.0(supports-color@7.2.0))(hono@4.13.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)(zod@4.4.3) '@nestm/tenant': specifier: 0.1.0-alpha.5 version: 0.1.0-alpha.5(@nestjs/common@12.0.0-alpha.5(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.0.0-alpha.5(@nestjs/common@12.0.0-alpha.5(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/testing@12.0.0-alpha.5(@nestjs/common@12.0.0-alpha.5(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.0.0-alpha.5(@nestjs/common@12.0.0-alpha.5(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(reflect-metadata@0.2.2)(rxjs@7.8.2)))(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -303,12 +300,6 @@ packages: engines: {node: '>=6'} hasBin: true - '@hono/node-server@2.1.1': - resolution: {integrity: sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==} - engines: {node: '>=20'} - peerDependencies: - hono: ^4 - '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} @@ -348,16 +339,6 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} - '@modelcontextprotocol/sdk@1.30.0': - resolution: {integrity: sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==} - engines: {node: '>=18'} - peerDependencies: - '@cfworker/json-schema': ^4.1.1 - zod: ^3.25 || ^4.0 - peerDependenciesMeta: - '@cfworker/json-schema': - optional: true - '@nestjs/common@12.0.0-alpha.5': resolution: {integrity: sha512-MB2bFWi/GE5TYIR1Z6Y1gp7iRk4xm9+zOlFlV5+5T3j7LNA6AR25YqibUdbgRkAbpklrHIxxj/gEFAw9yAeIwQ==} peerDependencies: @@ -402,42 +383,6 @@ packages: '@nestjs/platform-express': optional: true - '@nestm/storage@0.1.0-alpha.9': - resolution: {integrity: sha512-Hw3t+VhykWhowwrh68CIPgCiq/6TCnG8+4FML4YBsrRtRhImcrIQnUPA0aLbmMdVkRQOXv0Xlpot1lMh3txG/g==} - engines: {node: '>=22.12.0'} - peerDependencies: - '@aws-sdk/client-s3': ^3.919.0 - '@aws-sdk/lib-storage': ^3.700.0 - '@aws-sdk/s3-presigned-post': ^3.700.0 - '@aws-sdk/s3-request-presigner': ^3.700.0 - '@nestjs/common': ^12.0.0-alpha.5 - '@nestjs/core': ^12.0.0-alpha.5 - ai: '>=7.0.0 <8.0.0' - reflect-metadata: ^0.2.2 - rxjs: ^7.8.1 - zod: ^4.1.8 - peerDependenciesMeta: - '@aws-sdk/client-s3': - optional: true - '@aws-sdk/lib-storage': - optional: true - '@aws-sdk/s3-presigned-post': - optional: true - '@aws-sdk/s3-request-presigner': - optional: true - '@nestjs/common': - optional: true - '@nestjs/core': - optional: true - ai: - optional: true - reflect-metadata: - optional: true - rxjs: - optional: true - zod: - optional: true - '@nestm/tenant@0.1.0-alpha.5': resolution: {integrity: sha512-Ac/mxgnuLbAnDqBac85coK0ihDxAnWgXYwxuFB13je4TBNripJCWyJiiufwA8J2gcarGy7uo8Kscsvy8bOMDaQ==} engines: {node: '>=22.13.0'} @@ -1119,25 +1064,10 @@ packages: '@yuku-toolchain/types@0.8.4': resolution: {integrity: sha512-p7JE8flrj7ijZ/qLjHi4UwKqMarMD6zumbKXhrjp2I2iLJOuTYiQyci2U36VlXcUlNyzsY7E/mLnKCHotbzJVw==} - accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} - engines: {node: '>= 0.6'} - agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} - ajv-formats@3.0.1: - resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - - ajv@8.20.0: - resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} - ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -1186,9 +1116,6 @@ packages: ast-v8-to-istanbul@1.0.5: resolution: {integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==} - aws4fetch@1.0.20: - resolution: {integrity: sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==} - balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1202,10 +1129,6 @@ packages: bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} - body-parser@2.3.0: - resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} - engines: {node: '>=18'} - bowser@2.14.1: resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} @@ -1219,22 +1142,10 @@ packages: buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - cac@7.0.0: resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} engines: {node: '>=20.19.0'} - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -1287,41 +1198,13 @@ packages: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} - commander@15.0.0: - resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} - engines: {node: '>=22.12.0'} - consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} - content-disposition@1.1.0: - resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} - engines: {node: '>=18'} - - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - - content-type@2.1.0: - resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} - engines: {node: '>=18'} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} - - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - - cors@2.8.6: - resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} - engines: {node: '>= 0.10'} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1342,10 +1225,6 @@ packages: defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -1367,10 +1246,6 @@ packages: oxc-resolver: optional: true - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - duplexify@4.1.3: resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} @@ -1380,9 +1255,6 @@ packages: ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1396,10 +1268,6 @@ packages: resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} engines: {node: '>=14'} - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} @@ -1411,28 +1279,13 @@ packages: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - es-module-lexer@2.3.1: resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} - es-object-atoms@1.1.2: - resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} - engines: {node: '>= 0.4'} - escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -1441,41 +1294,16 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - - eventsource-parser@3.1.1: - resolution: {integrity: sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==} - engines: {node: '>=18.0.0'} - - eventsource@3.0.7: - resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} - engines: {node: '>=18.0.0'} - expect-type@1.4.0: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} - express-rate-limit@8.6.2: - resolution: {integrity: sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==} - engines: {node: '>= 16'} - peerDependencies: - express: '>= 4.11' - - express@5.2.1: - resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} - engines: {node: '>= 18'} - extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -1483,9 +1311,6 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fast-uri@3.1.5: - resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} - fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -1509,158 +1334,10 @@ packages: resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} engines: {node: '>=20'} - files-sdk@2.2.3: - resolution: {integrity: sha512-1xCCh5yBhNwVfpeLKSaU7ejmh2kesw9mnU9dus0rw9EzNuNmcIuECZ91DlD0YsEP5ojXlPaPB8G7pQ25tqdebQ==} - hasBin: true - peerDependencies: - '@anthropic-ai/claude-agent-sdk': ^0.3.0 - '@aws-sdk/client-s3': ^3.700.0 - '@aws-sdk/lib-storage': ^3.700.0 - '@aws-sdk/s3-presigned-post': ^3.700.0 - '@aws-sdk/s3-request-presigner': ^3.700.0 - '@azure/core-auth': ^1.9.0 - '@azure/identity': ^4.5.0 - '@azure/storage-blob': ^12.26.0 - '@bunny.net/storage-sdk': ^0.3.1 - '@google-cloud/storage': ^7.19.0 - '@googleapis/drive': ^20.0.0 - '@microsoft/microsoft-graph-client': ^3.0.7 - '@nestjs/common': ^10.0.0 || ^11.0.0 - '@netlify/blobs': ^10.7.4 - '@openai/agents': ^0.11.0 - '@opentelemetry/api': ^1.9.0 - '@supabase/storage-js': ^2.105.4 - '@sveltejs/kit': ^2.0.0 - '@tanstack/react-start': ^1.0.0 - '@vercel/blob': ^2.4.0 - ai: ^6.0.0 - astro: ^4.0.0 || ^5.0.0 || ^6.0.0 - basic-ftp: ^6.0.1 - box-typescript-sdk-gen: ^1.19.1 - cloudinary: ^2.10.0 - convex: ^1.16.0 - disk: ^0.8.18 - dropbox: ^10.34.0 - fastify: ^4.0.0 || ^5.0.0 - firebase-admin: ^14.1.0 - google-auth-library: ^10.0.0 - h3: ^1.0.0 - hono: ^4.0.0 - koa: ^2.0.0 || ^3.0.0 - next: ^14.0.0 || ^15.0.0 || ^16.0.0 - node-appwrite: ^26.0.0 - openai: ^6.0.0 - pocketbase: ^0.27.0 - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - ssh2-sftp-client: ^12.1.1 - svelte: ^4.0.0 || ^5.0.0 - uploadthing: ^7 - vue: ^3.4.0 - webdav: ^5.10.0 - zod: ^3.23.0 || ^4.0.0 - peerDependenciesMeta: - '@anthropic-ai/claude-agent-sdk': - optional: true - '@aws-sdk/client-s3': - optional: true - '@aws-sdk/lib-storage': - optional: true - '@aws-sdk/s3-presigned-post': - optional: true - '@aws-sdk/s3-request-presigner': - optional: true - '@azure/core-auth': - optional: true - '@azure/identity': - optional: true - '@azure/storage-blob': - optional: true - '@bunny.net/storage-sdk': - optional: true - '@google-cloud/storage': - optional: true - '@googleapis/drive': - optional: true - '@microsoft/microsoft-graph-client': - optional: true - '@nestjs/common': - optional: true - '@netlify/blobs': - optional: true - '@openai/agents': - optional: true - '@opentelemetry/api': - optional: true - '@supabase/storage-js': - optional: true - '@sveltejs/kit': - optional: true - '@tanstack/react-start': - optional: true - '@vercel/blob': - optional: true - ai: - optional: true - astro: - optional: true - basic-ftp: - optional: true - box-typescript-sdk-gen: - optional: true - cloudinary: - optional: true - convex: - optional: true - disk: - optional: true - dropbox: - optional: true - fastify: - optional: true - firebase-admin: - optional: true - google-auth-library: - optional: true - h3: - optional: true - hono: - optional: true - koa: - optional: true - next: - optional: true - node-appwrite: - optional: true - openai: - optional: true - pocketbase: - optional: true - react: - optional: true - react-dom: - optional: true - ssh2-sftp-client: - optional: true - svelte: - optional: true - uploadthing: - optional: true - vue: - optional: true - webdav: - optional: true - zod: - optional: true - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - finalhandler@2.1.1: - resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} - engines: {node: '>= 18.0.0'} - find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -1673,14 +1350,6 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - - fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} - fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -1694,9 +1363,6 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - gaxios@7.1.3: resolution: {integrity: sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==} engines: {node: '>=18'} @@ -1713,14 +1379,6 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - get-tsconfig@5.0.0-beta.5: resolution: {integrity: sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==} engines: {node: '>=20.20.0'} @@ -1750,10 +1408,6 @@ packages: resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} engines: {node: '>=14'} - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -1765,31 +1419,15 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - hasown@2.0.4: - resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} - engines: {node: '>= 0.4'} - highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} - hono@4.13.3: - resolution: {integrity: sha512-r8AO2mYHoLxSHkgafNeC/BXyb2vWRxD3jem4Ts+ptav8oTG5FIRifAjuJEmZI4bSvvc2ns0GxmIYiZnHqN3mMw==} - engines: {node: '>=16.9.0'} - hookable@6.1.1: resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} - http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -1820,14 +1458,6 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ip-address@10.5.0: - resolution: {integrity: sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==} - engines: {node: '>= 12'} - - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1844,9 +1474,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - is-subdir@1.2.0: resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} engines: {node: '>=4'} @@ -1877,9 +1504,6 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jose@6.2.10: - resolution: {integrity: sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==} - js-tokens@10.0.0: resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} @@ -1894,12 +1518,6 @@ packages: json-bigint@1.0.0: resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - - json-schema-typed@8.0.2: - resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} - jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -2028,18 +1646,6 @@ packages: engines: {node: '>= 16'} hasBin: true - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - media-typer@1.1.1: - resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} - engines: {node: '>= 0.8'} - - merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} - merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2048,14 +1654,6 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} - engines: {node: '>= 0.6'} - - mime-types@3.0.2: - resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} - engines: {node: '>=18'} - minimatch@9.0.9: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} @@ -2079,10 +1677,6 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - negotiator@1.1.0: - resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} - engines: {node: '>=18'} - node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -2104,18 +1698,10 @@ packages: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - obug@2.1.4: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -2177,10 +1763,6 @@ packages: parse5@6.0.1: resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2218,10 +1800,6 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} - pkce-challenge@5.0.1: - resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} - engines: {node: '>=16.20.0'} - postcss@8.5.26: resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} @@ -2244,19 +1822,11 @@ packages: resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} engines: {node: '>=12.0.0'} - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - publint@0.3.23: resolution: {integrity: sha512-5MQipUPcB7MWw84zLUkHrg/H/UBtk3LL+A0GngTTBSsiNJLQurMUaSIRG3edlOrRz4UFe0AOKK9TZdIWviV+jQ==} engines: {node: '>=18'} hasBin: true - qs@6.15.3: - resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} - engines: {node: '>=0.6'} - quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -2266,14 +1836,6 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - range-parser@1.3.0: - resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} - engines: {node: '>= 0.6'} - - raw-body@3.0.2: - resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} - engines: {node: '>= 0.10'} - read-yaml-file@1.1.0: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} @@ -2289,10 +1851,6 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -2300,10 +1858,6 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - ret@0.5.0: - resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} - engines: {node: '>=10'} - retry-request@8.0.4: resolution: {integrity: sha512-pI6/7eabUYkZxamkOq0g0uMxKLLGnjzhefY+vL8bVXag5rto4OU2YBTPytWLuHH7aEKD6fn7kJycQfid0Mwnkw==} engines: {node: '>=18'} @@ -2340,10 +1894,6 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} - run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -2357,10 +1907,6 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safe-regex2@5.1.1: - resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==} - hasBin: true - safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -2369,17 +1915,6 @@ packages: engines: {node: '>=10'} hasBin: true - send@1.2.1: - resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} - engines: {node: '>= 18'} - - serve-static@2.2.1: - resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} - engines: {node: '>= 18'} - - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2388,22 +1923,6 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - side-channel-list@1.0.1: - resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.1: - resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} - engines: {node: '>= 0.4'} - siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -2432,10 +1951,6 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} - std-env@4.2.0: resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} @@ -2517,10 +2032,6 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - token-types@6.1.2: resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} @@ -2566,10 +2077,6 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - type-is@2.1.0: - resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} - engines: {node: '>= 18'} - typescript@5.6.1-rc: resolution: {integrity: sha512-E3b2+1zEFu84jB0YQi9BORDjz9+jGbwwy1Zi3G0LUNw7a7cePUrHMRNy8aPh53nXpkFGVHSxIZo5vKTfYaFiBQ==} engines: {node: '>=14.17'} @@ -2602,10 +2109,6 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -2613,10 +2116,6 @@ packages: resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - verkit@0.3.2: resolution: {integrity: sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==} engines: {node: '>=18.12.0'} @@ -2759,14 +2258,6 @@ packages: yuku-parser@0.8.4: resolution: {integrity: sha512-sw41wouvT5rUmLIp87hmvm5vtF+MRSI3x6yjq6xqpYmtkQj+Ht6N7xRQ8lMhLv8N7JAzughGj0Rfi0jQRSu9HQ==} - zod-to-json-schema@3.25.2: - resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} - peerDependencies: - zod: ^3.25.28 || ^4 - - zod@4.4.3: - resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} - snapshots: '@andrewbranch/untar.js@1.0.4': {} @@ -3218,11 +2709,6 @@ snapshots: protobufjs: 7.6.5 yargs: 17.7.3 - '@hono/node-server@2.1.1(hono@4.13.3)': - dependencies: - hono: 4.13.3 - optional: true - '@inquirer/external-editor@1.0.3(@types/node@24.13.3)': dependencies: chardet: 2.2.0 @@ -3272,29 +2758,6 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 - '@modelcontextprotocol/sdk@1.30.0(supports-color@7.2.0)(zod@4.4.3)': - dependencies: - '@hono/node-server': 2.1.1(hono@4.13.3) - ajv: 8.20.0 - ajv-formats: 3.0.1(ajv@8.20.0) - content-type: 1.0.5 - cors: 2.8.6 - cross-spawn: 7.0.6 - eventsource: 3.0.7 - eventsource-parser: 3.1.1 - express: 5.2.1(supports-color@7.2.0) - express-rate-limit: 8.6.2(express@5.2.1(supports-color@7.2.0))(supports-color@7.2.0) - hono: 4.13.3 - jose: 6.2.10 - json-schema-typed: 8.0.2 - pkce-challenge: 5.0.1 - raw-body: 3.0.2 - zod: 4.4.3 - zod-to-json-schema: 3.25.2(zod@4.4.3) - transitivePeerDependencies: - - supports-color - optional: true - '@nestjs/common@12.0.0-alpha.5(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)': dependencies: file-type: 21.3.4(supports-color@7.2.0) @@ -3327,58 +2790,6 @@ snapshots: '@nestjs/core': 12.0.0-alpha.5(@nestjs/common@12.0.0-alpha.5(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 - '@nestm/storage@0.1.0-alpha.9(@azure/core-auth@1.11.0(supports-color@7.2.0))(@nestjs/common@12.0.0-alpha.5(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.0.0-alpha.5(@nestjs/common@12.0.0-alpha.5(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(reflect-metadata@0.2.2)(rxjs@7.8.2))(google-auth-library@10.5.0(supports-color@7.2.0))(hono@4.13.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)(zod@4.4.3)': - dependencies: - files-sdk: 2.2.3(@azure/core-auth@1.11.0(supports-color@7.2.0))(@nestjs/common@12.0.0-alpha.5(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(google-auth-library@10.5.0(supports-color@7.2.0))(hono@4.13.3)(supports-color@7.2.0)(zod@4.4.3) - optionalDependencies: - '@nestjs/common': 12.0.0-alpha.5(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) - '@nestjs/core': 12.0.0-alpha.5(@nestjs/common@12.0.0-alpha.5(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(reflect-metadata@0.2.2)(rxjs@7.8.2) - reflect-metadata: 0.2.2 - rxjs: 7.8.2 - zod: 4.4.3 - transitivePeerDependencies: - - '@anthropic-ai/claude-agent-sdk' - - '@azure/core-auth' - - '@azure/identity' - - '@azure/storage-blob' - - '@bunny.net/storage-sdk' - - '@cfworker/json-schema' - - '@google-cloud/storage' - - '@googleapis/drive' - - '@microsoft/microsoft-graph-client' - - '@netlify/blobs' - - '@openai/agents' - - '@opentelemetry/api' - - '@supabase/storage-js' - - '@sveltejs/kit' - - '@tanstack/react-start' - - '@vercel/blob' - - astro - - basic-ftp - - box-typescript-sdk-gen - - cloudinary - - convex - - disk - - dropbox - - fastify - - firebase-admin - - google-auth-library - - h3 - - hono - - koa - - next - - node-appwrite - - openai - - pocketbase - - react - - react-dom - - ssh2-sftp-client - - supports-color - - svelte - - uploadthing - - vue - - webdav - '@nestm/tenant@0.1.0-alpha.5(@nestjs/common@12.0.0-alpha.5(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.0.0-alpha.5(@nestjs/common@12.0.0-alpha.5(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/testing@12.0.0-alpha.5(@nestjs/common@12.0.0-alpha.5(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.0.0-alpha.5(@nestjs/common@12.0.0-alpha.5(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(reflect-metadata@0.2.2)(rxjs@7.8.2)))(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: '@nestjs/common': 12.0.0-alpha.5(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) @@ -3814,27 +3225,8 @@ snapshots: '@yuku-toolchain/types@0.8.4': {} - accepts@2.0.0: - dependencies: - mime-types: 3.0.2 - negotiator: 1.1.0 - optional: true - agent-base@7.1.4: {} - ajv-formats@3.0.1(ajv@8.20.0): - optionalDependencies: - ajv: 8.20.0 - optional: true - - ajv@8.20.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.5 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - optional: true - ansi-colors@4.1.3: {} ansi-escapes@7.3.0: @@ -3871,8 +3263,6 @@ snapshots: estree-walker: 3.0.3 js-tokens: 10.0.0 - aws4fetch@1.0.20: {} - balanced-match@1.0.2: {} base64-js@1.5.1: {} @@ -3883,21 +3273,6 @@ snapshots: bignumber.js@9.3.1: {} - body-parser@2.3.0(supports-color@7.2.0): - dependencies: - bytes: 3.1.2 - content-type: 2.1.0 - debug: 4.4.3(supports-color@7.2.0) - http-errors: 2.0.1 - iconv-lite: 0.7.3 - on-finished: 2.4.1 - qs: 6.15.3 - raw-body: 3.0.2 - type-is: 2.1.0 - transitivePeerDependencies: - - supports-color - optional: true - bowser@2.14.1: {} brace-expansion@2.1.4: @@ -3910,23 +3285,8 @@ snapshots: buffer-equal-constant-time@1.0.1: {} - bytes@3.1.2: - optional: true - cac@7.0.0: {} - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - optional: true - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - optional: true - chai@6.2.2: {} chalk@4.1.2: @@ -3979,33 +3339,10 @@ snapshots: commander@10.0.1: {} - commander@15.0.0: {} - consola@3.4.2: {} - content-disposition@1.1.0: - optional: true - - content-type@1.0.5: - optional: true - - content-type@2.1.0: - optional: true - convert-source-map@2.0.0: {} - cookie-signature@1.2.2: - optional: true - - cookie@0.7.2: - optional: true - - cors@2.8.6: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - optional: true - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -4022,9 +3359,6 @@ snapshots: defu@6.1.7: {} - depd@2.0.0: - optional: true - detect-indent@6.1.0: {} detect-libc@2.1.2: {} @@ -4035,13 +3369,6 @@ snapshots: dts-resolver@3.0.0: {} - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - optional: true - duplexify@4.1.3: dependencies: end-of-stream: 1.4.5 @@ -4055,9 +3382,6 @@ snapshots: dependencies: safe-buffer: 5.2.1 - ee-first@1.1.1: - optional: true - emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -4066,9 +3390,6 @@ snapshots: empathic@2.0.1: {} - encodeurl@2.0.0: - optional: true - end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -4080,93 +3401,22 @@ snapshots: environment@1.1.0: {} - es-define-property@1.0.1: - optional: true - - es-errors@1.3.0: - optional: true - es-module-lexer@2.3.1: {} - es-object-atoms@1.1.2: - dependencies: - es-errors: 1.3.0 - optional: true - escalade@3.2.0: {} - escape-html@1.0.3: - optional: true - esprima@4.0.1: {} estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 - etag@1.8.1: - optional: true - - eventsource-parser@3.1.1: - optional: true - - eventsource@3.0.7: - dependencies: - eventsource-parser: 3.1.1 - optional: true - expect-type@1.4.0: {} - express-rate-limit@8.6.2(express@5.2.1(supports-color@7.2.0))(supports-color@7.2.0): - dependencies: - debug: 4.4.3(supports-color@7.2.0) - express: 5.2.1(supports-color@7.2.0) - ip-address: 10.5.0 - transitivePeerDependencies: - - supports-color - optional: true - - express@5.2.1(supports-color@7.2.0): - dependencies: - accepts: 2.0.0 - body-parser: 2.3.0(supports-color@7.2.0) - content-disposition: 1.1.0 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.3(supports-color@7.2.0) - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.1(supports-color@7.2.0) - fresh: 2.0.0 - http-errors: 2.0.1 - merge-descriptors: 2.0.0 - mime-types: 3.0.2 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.15.3 - range-parser: 1.3.0 - router: 2.2.0(supports-color@7.2.0) - send: 1.2.1(supports-color@7.2.0) - serve-static: 2.2.1(supports-color@7.2.0) - statuses: 2.0.2 - type-is: 2.1.0 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - optional: true - extend@3.0.2: {} extendable-error@0.1.7: {} - fast-deep-equal@3.1.3: - optional: true - fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4177,9 +3427,6 @@ snapshots: fast-safe-stringify@2.1.1: {} - fast-uri@3.1.5: - optional: true - fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -4204,39 +3451,10 @@ snapshots: transitivePeerDependencies: - supports-color - files-sdk@2.2.3(@azure/core-auth@1.11.0(supports-color@7.2.0))(@nestjs/common@12.0.0-alpha.5(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(google-auth-library@10.5.0(supports-color@7.2.0))(hono@4.13.3)(supports-color@7.2.0)(zod@4.4.3): - dependencies: - aws4fetch: 1.0.20 - commander: 15.0.0 - picomatch: 4.0.5 - safe-regex2: 5.1.1 - optionalDependencies: - '@azure/core-auth': 1.11.0(supports-color@7.2.0) - '@modelcontextprotocol/sdk': 1.30.0(supports-color@7.2.0)(zod@4.4.3) - '@nestjs/common': 12.0.0-alpha.5(class-transformer@0.5.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) - google-auth-library: 10.5.0(supports-color@7.2.0) - hono: 4.13.3 - zod: 4.4.3 - transitivePeerDependencies: - - '@cfworker/json-schema' - - supports-color - fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 - finalhandler@2.1.1(supports-color@7.2.0): - dependencies: - debug: 4.4.3(supports-color@7.2.0) - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - optional: true - find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -4251,12 +3469,6 @@ snapshots: dependencies: fetch-blob: 3.2.0 - forwarded@0.2.0: - optional: true - - fresh@2.0.0: - optional: true - fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -4272,9 +3484,6 @@ snapshots: fsevents@2.3.3: optional: true - function-bind@1.1.2: - optional: true - gaxios@7.1.3(supports-color@7.2.0): dependencies: extend: 3.0.2 @@ -4302,26 +3511,6 @@ snapshots: get-caller-file@2.0.5: {} - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.4 - math-intrinsics: 1.1.0 - optional: true - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.2 - optional: true - get-tsconfig@5.0.0-beta.5: dependencies: resolve-pkg-maps: 1.0.0 @@ -4378,9 +3567,6 @@ snapshots: google-logging-utils@1.1.3: {} - gopd@1.2.0: - optional: true - graceful-fs@4.2.11: {} gtoken@8.0.0(supports-color@7.2.0): @@ -4392,32 +3578,12 @@ snapshots: has-flag@4.0.0: {} - has-symbols@1.1.0: - optional: true - - hasown@2.0.4: - dependencies: - function-bind: 1.1.2 - optional: true - highlight.js@10.7.3: {} - hono@4.13.3: - optional: true - hookable@6.1.1: {} html-escaper@2.0.2: {} - http-errors@2.0.1: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.2 - toidentifier: 1.0.1 - optional: true - http-proxy-agent@7.0.2(supports-color@7.2.0): dependencies: agent-base: 7.1.4 @@ -4446,12 +3612,6 @@ snapshots: inherits@2.0.4: {} - ip-address@10.5.0: - optional: true - - ipaddr.js@1.9.1: - optional: true - is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -4462,9 +3622,6 @@ snapshots: is-number@7.0.0: {} - is-promise@4.0.0: - optional: true - is-subdir@1.2.0: dependencies: better-path-resolve: 1.0.0 @@ -4494,9 +3651,6 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jose@6.2.10: - optional: true - js-tokens@10.0.0: {} js-yaml@3.15.1: @@ -4512,12 +3666,6 @@ snapshots: dependencies: bignumber.js: 9.3.1 - json-schema-traverse@1.0.0: - optional: true - - json-schema-typed@8.0.2: - optional: true - jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -4625,15 +3773,6 @@ snapshots: marked@9.1.6: {} - math-intrinsics@1.1.0: - optional: true - - media-typer@1.1.1: - optional: true - - merge-descriptors@2.0.0: - optional: true - merge2@1.4.1: {} micromatch@4.0.8: @@ -4641,14 +3780,6 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 - mime-db@1.54.0: - optional: true - - mime-types@3.0.2: - dependencies: - mime-db: 1.54.0 - optional: true - minimatch@9.0.9: dependencies: brace-expansion: 2.1.4 @@ -4667,11 +3798,6 @@ snapshots: nanoid@3.3.18: {} - negotiator@1.1.0: - dependencies: - content-type: 2.1.0 - optional: true - node-domexception@1.0.0: {} node-emoji@2.2.0: @@ -4691,16 +3817,8 @@ snapshots: object-hash@3.0.0: {} - object-inspect@1.13.4: - optional: true - obug@2.1.4: {} - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - optional: true - once@1.4.0: dependencies: wrappy: 1.0.2 @@ -4771,9 +3889,6 @@ snapshots: parse5@6.0.1: {} - parseurl@1.3.3: - optional: true - path-exists@4.0.0: {} path-key@3.1.1: {} @@ -4797,9 +3912,6 @@ snapshots: pify@4.0.1: {} - pkce-challenge@5.0.1: - optional: true - postcss@8.5.26: dependencies: nanoid: 3.3.18 @@ -4828,12 +3940,6 @@ snapshots: '@types/node': 24.13.3 long: 5.3.2 - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - optional: true - publint@0.3.23: dependencies: '@publint/pack': 0.1.6 @@ -4841,29 +3947,12 @@ snapshots: picocolors: 1.1.1 sade: 1.8.1 - qs@6.15.3: - dependencies: - es-define-property: 1.0.1 - side-channel: 1.1.1 - optional: true - quansync@0.2.11: {} quansync@1.0.0: {} queue-microtask@1.2.3: {} - range-parser@1.3.0: - optional: true - - raw-body@3.0.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.7.3 - unpipe: 1.0.0 - optional: true - read-yaml-file@1.1.0: dependencies: graceful-fs: 4.2.11 @@ -4881,15 +3970,10 @@ snapshots: require-directory@2.1.1: {} - require-from-string@2.0.2: - optional: true - resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} - ret@0.5.0: {} - retry-request@8.0.4(supports-color@7.2.0): dependencies: extend: 3.0.2 @@ -4937,17 +4021,6 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.3 '@rolldown/binding-win32-x64-msvc': 1.2.3 - router@2.2.0(supports-color@7.2.0): - dependencies: - debug: 4.4.3(supports-color@7.2.0) - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.4.2 - transitivePeerDependencies: - - supports-color - optional: true - run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -4962,82 +4035,16 @@ snapshots: safe-buffer@5.2.1: {} - safe-regex2@5.1.1: - dependencies: - ret: 0.5.0 - safer-buffer@2.1.2: {} semver@7.8.5: {} - send@1.2.1(supports-color@7.2.0): - dependencies: - debug: 4.4.3(supports-color@7.2.0) - 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.3.0 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - optional: true - - serve-static@2.2.1(supports-color@7.2.0): - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 1.2.1(supports-color@7.2.0) - transitivePeerDependencies: - - supports-color - optional: true - - setprototypeof@1.2.0: - optional: true - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} - side-channel-list@1.0.1: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - optional: true - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - optional: true - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - optional: true - - side-channel@1.1.1: - 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 - optional: true - siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -5059,9 +4066,6 @@ snapshots: stackback@0.0.2: {} - statuses@2.0.2: - optional: true - std-env@4.2.0: {} stream-events@1.0.5: @@ -5145,9 +4149,6 @@ snapshots: dependencies: is-number: 7.0.0 - toidentifier@1.0.1: - optional: true - token-types@6.1.2: dependencies: '@borewit/text-codec': 0.2.2 @@ -5185,13 +4186,6 @@ snapshots: tslib@2.8.1: {} - type-is@2.1.0: - dependencies: - content-type: 2.1.0 - media-typer: 1.1.1 - mime-types: 3.0.2 - optional: true - typescript@5.6.1-rc: {} typescript@7.0.2: @@ -5234,16 +4228,10 @@ snapshots: universalify@0.1.2: {} - unpipe@1.0.0: - optional: true - util-deprecate@1.0.2: {} validate-npm-package-name@5.0.1: {} - vary@1.1.2: - optional: true - verkit@0.3.2: {} vite@8.2.1(@types/node@24.13.3): @@ -5374,11 +4362,3 @@ snapshots: '@yuku-parser/binding-linux-x64-musl': 0.8.4 '@yuku-parser/binding-win32-arm64': 0.8.4 '@yuku-parser/binding-win32-x64': 0.8.4 - - zod-to-json-schema@3.25.2(zod@4.4.3): - dependencies: - zod: 4.4.3 - optional: true - - zod@4.4.3: - optional: true diff --git a/scripts/check-package.mjs b/scripts/check-package.mjs index 74cd589..5cb346d 100644 --- a/scripts/check-package.mjs +++ b/scripts/check-package.mjs @@ -8,7 +8,6 @@ const expectedExports = [ ".", "./core", "./files", - "./storage-workspace", "./fields", "./tenant", "./http", @@ -46,7 +45,6 @@ if (files.some((file) => file.endsWith(".map"))) { const optionalPeers = [ "@nestjs/common", "@nestjs/core", - "@nestm/storage", "@nestm/tenant", "@aws-sdk/client-kms", "@google-cloud/kms", @@ -62,31 +60,12 @@ for (const peer of optionalPeers) { throw new Error(`Integration peer must remain optional: ${peer}`); } } -if (manifest.devDependencies?.["@nestm/storage"] !== "0.1.0-alpha.9") { - throw new Error("The storage bridge must test against exactly @nestm/storage@0.1.0-alpha.9"); -} const coreSource = readFileSync(join(root, "dist/core/index.mjs"), "utf8"); for (const peer of optionalPeers) { if (coreSource.includes(peer)) { throw new Error(`The framework-neutral core unexpectedly imports optional peer ${peer}`); } } -const rootSource = readFileSync(join(root, "dist/index.mjs"), "utf8"); -if (rootSource.includes("@nestm/storage")) { - throw new Error("The root entry point unexpectedly imports optional peer @nestm/storage"); -} -for (const bridgeArtifact of [ - "dist/storage-workspace/index.mjs", - "dist/storage-workspace/index.d.mts", -]) { - const source = readFileSync(join(root, bridgeArtifact), "utf8"); - if (/from\s+["']@nestm\/storage["']/u.test(source)) { - throw new Error( - `The storage bridge must use framework-neutral @nestm/storage subpaths: ${bridgeArtifact}`, - ); - } -} - const core = await import(pathToFileURL(join(root, "dist/core/index.mjs")).href); if (Object.keys(core).length === 0) throw new Error("The core entry point exports no values"); diff --git a/scripts/test-packed-consumers.mjs b/scripts/test-packed-consumers.mjs index 40c5fa9..96f12ea 100644 --- a/scripts/test-packed-consumers.mjs +++ b/scripts/test-packed-consumers.mjs @@ -23,7 +23,6 @@ try { const tarball = join(packageDirectory, tarballName); testCoreConsumer(tarball); - testStorageWorkspaceConsumer(tarball); testIntegrationConsumer(tarball); verifyTarball(tarball); } finally { @@ -111,129 +110,6 @@ await engine.close(); } } -function testStorageWorkspaceConsumer(tarball) { - const directory = join(temporaryRoot, "storage-workspace-consumer"); - mkdirSync(directory); - writeJson(join(directory, "package.json"), { - name: "@nestm/crypto-storage-workspace-consumer", - private: true, - type: "module", - }); - run( - "npm", - [ - "install", - "--ignore-scripts", - "--no-audit", - "--no-fund", - "--legacy-peer-deps", - tarball, - "@nestm/storage@0.1.0-alpha.9", - "typescript@7.0.2", - "@types/node@24.13.3", - ], - directory, - ); - writeFileSync( - join(directory, "consumer.ts"), - `import assert from "node:assert/strict"; -import { generateKeySync } from "node:crypto"; -import { StorageClient } from "@nestm/storage/core"; -import { createMemoryStorageDriver } from "@nestm/storage/testing"; -import { mountStorageWorkspace } from "@nestm/storage/workspace"; -import { AesKeyRingProvider, CipherEngine } from "@nestm/crypto/core"; -import { - createCipherEngineWorkspaceCipher, - protectStorageWorkspace, -} from "@nestm/crypto/storage-workspace"; - -const client = new StorageClient("packed-protected-memory", createMemoryStorageDriver()); -const rawWorkspace = mountStorageWorkspace(client, { - prefix: "validation-only/protected", - permissions: ["list", "read", "search", "write", "create", "replace", "copy", "move", "delete"], - limits: { - maxReadBytes: 64 * 1024, - maxWriteBytes: 64 * 1024, - maxPageSize: 20, - maxSearchResults: 20, - maxSearchScan: 100, - }, -}); -const engine = new CipherEngine({ - defaultProvider: "packed-storage", - providers: [{ - name: "packed-storage", - provider: new AesKeyRingProvider({ - activeKeyId: "packed-storage-key", - keys: { "packed-storage-key": generateKeySync("aes", { length: 256 }) }, - }), - }], - maxPayloadBytes: 64 * 1024, -}); -const cipher = createCipherEngineWorkspaceCipher(engine, { - allowedProviders: ["packed-storage"], -}); -const logicalLimits = { - ...rawWorkspace.limits, - maxReadBytes: 8 * 1024, - maxWriteBytes: 8 * 1024, -}; -const workspace = protectStorageWorkspace({ - storage: rawWorkspace, - cipher, - scopeContext: "organization:packed/workspace:protected", - policyRevision: "packed-v1", - limits: logicalLimits, - maxCiphertextBytes: 64 * 1024, - pathSearch: "provider-visible", -}); - -assert.equal(workspace.protection.outerFormat, "nestm-protected-storage-workspace"); -assert.equal(workspace.protection.outerVersion, 1); -assert.equal(workspace.protection.body, "encrypted"); -assert.equal(workspace.protection.metadata, "encrypted"); -assert.equal(workspace.protection.pathSearch, "provider-visible"); -assert.equal(workspace.protection.policyRevision, "packed-v1"); - -const plaintext = "packed protected workspace secret"; -await workspace.writeFile("reports/result.txt", plaintext, { - // The Files SDK memory adapter deliberately lacks atomic create-if-absent. - mode: "overwrite", - contentType: "text/plain; charset=utf-8", -}); -const raw = await rawWorkspace.readText("reports/result.txt"); -assert.equal(raw.text.includes(plaintext), false); -const backingRecord: unknown = JSON.parse(raw.text); -assert.ok(typeof backingRecord === "object" && backingRecord !== null); -const record = backingRecord as Record; -assert.deepEqual(Object.keys(record), ["v", "record", "version", "metadata", "content"]); -assert.equal(record.v, 1); -const uuidV4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u; -assert.match(String(record.record), uuidV4); -assert.match(String(record.version), uuidV4); -assert.match(String(record.metadata), /^nmc1\\./u); -assert.match(String(record.content), /^nmc1\\./u); -for (const envelope of [String(record.metadata), String(record.content)]) { - for (const segment of envelope.split(".").slice(1)) { - assert.equal(Buffer.from(segment, "base64url").includes(Buffer.from(plaintext)), false); - } -} - -const restored = await workspace.readText("reports/result.txt"); -assert.equal(restored.text, plaintext); -assert.equal(restored.contentType, "text/plain; charset=utf-8"); -const page = await workspace.list({ directory: "reports" }); -assert.equal(page.entries[0]?.path, "reports/result.txt"); - -await engine.close(); -await client.onApplicationShutdown(); -`, - ); - writeTypeScriptConfig(directory); - run(join(directory, "node_modules/.bin/tsc"), ["-p", "tsconfig.json"], directory); - run(process.execPath, ["dist/consumer.js"], directory); -} - function testIntegrationConsumer(tarball) { const directory = join(temporaryRoot, "integration-consumer"); mkdirSync(directory); diff --git a/src/storage-workspace/cipher-engine-adapter.ts b/src/storage-workspace/cipher-engine-adapter.ts deleted file mode 100644 index c50a5f8..0000000 --- a/src/storage-workspace/cipher-engine-adapter.ts +++ /dev/null @@ -1,100 +0,0 @@ -import type { CipherEngine } from "../core/cipher-engine.js"; - -import type { - AuthenticatedWorkspaceCipher, - AuthenticatedWorkspaceCipherContext, - CipherEngineWorkspaceCipherOptions, -} from "./types.js"; - -/** Adapt a CipherEngine without transferring ownership of its lifecycle. */ -export function createCipherEngineWorkspaceCipher( - engine: CipherEngine, - options: CipherEngineWorkspaceCipherOptions = {}, -): AuthenticatedWorkspaceCipher { - if ( - typeof engine !== "object" || - engine === null || - typeof engine.encryptBytes !== "function" || - typeof engine.decryptBytes !== "function" || - typeof engine.defaultProvider !== "string" - ) { - throw new TypeError("A CipherEngine is required."); - } - if (typeof options !== "object" || options === null) { - throw new TypeError("CipherEngine workspace-cipher options are invalid."); - } - const provider = captureOptionalIdentifier(options.provider, "provider"); - const cipher = captureOptionalIdentifier(options.cipher, "cipher"); - const allowedProviders = captureAllowedProviders( - options.allowedProviders, - provider ?? engine.defaultProvider, - ); - const writeProvider = provider ?? engine.defaultProvider; - if (!allowedProviders.includes(writeProvider)) { - throw new TypeError("The CipherEngine write provider must be admitted for reads."); - } - for (const allowedProvider of allowedProviders) { - if (!engine.hasProvider(allowedProvider)) { - throw new TypeError("A CipherEngine allowed provider is not registered."); - } - } - - return Object.freeze({ - async decryptBytes( - envelope: string, - context: AuthenticatedWorkspaceCipherContext, - ): Promise { - return await engine.decryptBytes(envelope, { - aad: context.authenticatedData, - allowedProviders, - keyContext: context.keyContext, - ...(context.signal === undefined ? {} : { signal: context.signal }), - }); - }, - async encryptBytes( - plaintext: Uint8Array, - context: AuthenticatedWorkspaceCipherContext, - ): Promise { - return await engine.encryptBytes(plaintext, { - aad: context.authenticatedData, - keyContext: context.keyContext, - ...(cipher === undefined ? {} : { cipher }), - ...(provider === undefined ? {} : { provider }), - ...(context.signal === undefined ? {} : { signal: context.signal }), - }); - }, - } satisfies AuthenticatedWorkspaceCipher); -} - -function captureOptionalIdentifier(value: string | undefined, label: string): string | undefined { - if (value === undefined) return undefined; - if ( - typeof value !== "string" || - value.length === 0 || - value.length > 128 || - value.trim() !== value || - /\p{C}/u.test(value) - ) { - throw new TypeError(`The CipherEngine ${label} selection is invalid.`); - } - return value; -} - -function captureAllowedProviders( - values: readonly string[] | undefined, - fallback: string, -): readonly string[] { - const candidates = values === undefined ? [fallback] : [...values]; - if (candidates.length === 0) { - throw new TypeError("At least one allowed CipherEngine provider is required."); - } - const unique = new Set(); - for (const candidate of candidates) { - const captured = captureOptionalIdentifier(candidate, "allowed provider"); - if (captured === undefined || unique.has(captured)) { - throw new TypeError("CipherEngine allowed providers are invalid."); - } - unique.add(captured); - } - return Object.freeze([...unique]); -} diff --git a/src/storage-workspace/index.ts b/src/storage-workspace/index.ts deleted file mode 100644 index 67ebc13..0000000 --- a/src/storage-workspace/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export { createCipherEngineWorkspaceCipher } from "./cipher-engine-adapter.js"; -export { protectStorageWorkspace } from "./protected-storage-workspace.js"; -export type { - AuthenticatedWorkspaceCipher, - AuthenticatedWorkspaceCipherContext, - CipherEngineWorkspaceCipherOptions, - ProtectedStorageOperationOptions, - ProtectedStorageWorkspace, - ProtectedStorageWorkspaceByteFile, - ProtectedStorageWorkspacePathSearch, - ProtectedStorageWorkspaceProtection, - ProtectStorageWorkspaceOptions, -} from "./types.js"; diff --git a/src/storage-workspace/protected-storage-workspace.spec.ts b/src/storage-workspace/protected-storage-workspace.spec.ts deleted file mode 100644 index 468e5f5..0000000 --- a/src/storage-workspace/protected-storage-workspace.spec.ts +++ /dev/null @@ -1,801 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { getEventListeners } from "node:events"; -import { mkdtempSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -import { StorageClient, StorageError, StorageErrorCode } from "@nestm/storage/core"; -import { createFsStorageDriver } from "@nestm/storage/files-sdk/fs"; -import { - Aes256GcmStorageWorkspaceCursorCodec, - mountStorageWorkspace, - type StorageWorkspace, - type StorageWorkspaceLimits, - type StorageWorkspacePermission, -} from "@nestm/storage/workspace"; - -import { AesKeyRingProvider, CipherEngine } from "../core/index.js"; -import { createCipherEngineWorkspaceCipher } from "./cipher-engine-adapter.js"; -import { protectStorageWorkspace } from "./protected-storage-workspace.js"; -import type { - AuthenticatedWorkspaceCipher, - ProtectedStorageWorkspace, - ProtectStorageWorkspaceOptions, -} from "./types.js"; - -const PREFIX = "protected-workspace-fixture"; -const MAX_CIPHERTEXT_BYTES = 32 * 1024; -const BACKING_LIMITS: Readonly = Object.freeze({ - cursorTtlMs: 60_000, - maxCursorBytes: 4096, - maxPageSize: 20, - maxPathBytes: 512, - maxReadBytes: 64 * 1024, - maxSearchResults: 20, - maxSearchScan: 100, - maxWriteBytes: 64 * 1024, -}); -const LOGICAL_LIMITS: Readonly = Object.freeze({ - ...BACKING_LIMITS, - maxReadBytes: 4096, - maxWriteBytes: 4096, -}); -const ALL_PERMISSIONS = Object.freeze([ - "list", - "read", - "search", - "write", - "create", - "replace", - "copy", - "move", - "delete", -] satisfies readonly StorageWorkspacePermission[]); -const KEY = new Uint8Array(32).fill(0x41); -const temporaryRoots: string[] = []; - -afterEach(() => { - for (const root of temporaryRoots.splice(0)) { - rmSync(root, { force: true, recursive: true }); - } -}); - -interface RawOuter { - readonly v: 1; - readonly record: string; - readonly version: string; - readonly metadata: string; - readonly content: string; -} - -interface Fixture { - readonly client: StorageClient; - readonly engine: CipherEngine; - readonly cipher: AuthenticatedWorkspaceCipher; - readonly storage: StorageWorkspace; - readonly workspace: ProtectedStorageWorkspace; -} - -function engine(): CipherEngine { - return new CipherEngine({ - defaultProvider: "local", - maxPayloadBytes: LOGICAL_LIMITS.maxWriteBytes, - providers: [ - { - name: "local", - provider: new AesKeyRingProvider({ - activeKeyId: "test-key-v1", - keys: { "test-key-v1": KEY }, - }), - }, - ], - }); -} - -function backing( - client: StorageClient, - prefix = PREFIX, - permissions: readonly StorageWorkspacePermission[] = ALL_PERMISSIONS, -): StorageWorkspace { - return mountStorageWorkspace(client, { - cursor: { - codec: new Aes256GcmStorageWorkspaceCursorCodec({ - activeKeyId: "cursor-v1", - keys: { "cursor-v1": new Uint8Array(32).fill(0x43) }, - }), - mountId: `test/${prefix}`, - scope: "protected-workspace-tests", - }, - limits: BACKING_LIMITS, - permissions, - prefix, - }); -} - -function protectedView( - storage: StorageWorkspace, - cipher: AuthenticatedWorkspaceCipher, - overrides: Partial< - Pick< - ProtectStorageWorkspaceOptions, - "limits" | "maxCiphertextBytes" | "pathSearch" | "policyRevision" | "scopeContext" | "signal" - > - > = {}, -): ProtectedStorageWorkspace { - return protectStorageWorkspace({ - cipher, - limits: overrides.limits ?? LOGICAL_LIMITS, - maxCiphertextBytes: overrides.maxCiphertextBytes ?? MAX_CIPHERTEXT_BYTES, - pathSearch: overrides.pathSearch ?? "disabled", - policyRevision: overrides.policyRevision ?? "policy-v1", - scopeContext: overrides.scopeContext ?? "tenant/acme/workspace/primary", - ...(overrides.signal === undefined ? {} : { signal: overrides.signal }), - storage, - }); -} - -function fixture(): Fixture { - const root = mkdtempSync(join(tmpdir(), "nestm-protected-workspace-")); - temporaryRoots.push(root); - const client = new StorageClient( - "protected-workspace-test", - createFsStorageDriver({ adapter: { root } }), - ); - const testEngine = engine(); - const cipher = createCipherEngineWorkspaceCipher(testEngine); - const storage = backing(client); - return { - cipher, - client, - engine: testEngine, - storage, - workspace: protectedView(storage, cipher), - }; -} - -async function rawText(client: StorageClient, path: string, prefix = PREFIX): Promise { - return await client.downloadText(`${prefix}/${path}`, { - maxBytes: BACKING_LIMITS.maxReadBytes, - }); -} - -async function putRaw( - client: StorageClient, - path: string, - body: string, - prefix = PREFIX, -): Promise { - await client.upload(`${prefix}/${path}`, body, { - contentType: "application/octet-stream", - }); -} - -function parseRaw(value: string): RawOuter { - const parsed: unknown = JSON.parse(value); - if ( - typeof parsed !== "object" || - parsed === null || - !("v" in parsed) || - parsed.v !== 1 || - !("record" in parsed) || - typeof parsed.record !== "string" || - !("version" in parsed) || - typeof parsed.version !== "string" || - !("metadata" in parsed) || - typeof parsed.metadata !== "string" || - !("content" in parsed) || - typeof parsed.content !== "string" - ) { - throw new TypeError("The raw test object is malformed."); - } - return { - v: 1, - record: parsed.record, - version: parsed.version, - metadata: parsed.metadata, - content: parsed.content, - }; -} - -function mutateEnvelope(value: string): string { - const last = value.at(-1); - if (last === undefined) throw new Error("Missing envelope value."); - return `${value.slice(0, -1)}${last === "A" ? "B" : "A"}`; -} - -describe("protected StorageWorkspace", () => { - it("round-trips text and bytes while exposing only a frozen protected view", async () => { - const { client, workspace } = fixture(); - const path = "reports/private.md"; - const plaintext = "classified workspace artifact"; - - const created = await workspace.writeFile(path, plaintext, { - contentType: "text/markdown; charset=utf-8", - mode: "create", - }); - - expect(created).toMatchObject({ - contentType: "text/markdown; charset=utf-8", - kind: "file", - name: "private.md", - path, - size: new TextEncoder().encode(plaintext).byteLength, - }); - await expect(workspace.readText(path)).resolves.toMatchObject({ text: plaintext }); - await expect(workspace.readBytes(path)).resolves.toMatchObject({ - bytes: new TextEncoder().encode(plaintext), - }); - - const binary = new Uint8Array([0, 1, 2, 254, 255]); - await workspace.writeFile("payload.bin", binary, { - contentType: "application/x-fixture", - mode: "create", - }); - await expect(workspace.readBytes("payload.bin")).resolves.toMatchObject({ bytes: binary }); - await expect(workspace.readText("payload.bin")).rejects.toMatchObject({ - code: StorageErrorCode.INVALID_ARGUMENT, - }); - - const raw = await rawText(client, path); - const outer = parseRaw(raw); - expect(Object.keys(outer)).toEqual(["v", "record", "version", "metadata", "content"]); - expect(outer).toMatchObject({ v: 1 }); - expect(outer.metadata).toMatch(/^nmc1\./u); - expect(outer.content).toMatch(/^nmc1\./u); - expect(raw).not.toContain(plaintext); - expect(raw).not.toContain(path); - expect(raw).not.toContain("text/markdown"); - await expect(client.head(`${PREFIX}/${path}`)).resolves.toMatchObject({ - contentType: "application/octet-stream", - }); - - expect(workspace.protection).toEqual({ - body: "encrypted", - cipherEnvelope: "nmc1", - kind: "authenticated-encryption", - metadata: "encrypted", - outerFormat: "nestm-protected-storage-workspace", - outerVersion: 1, - pathBinding: "scope-policy-path-record-version-purpose", - pathSearch: "disabled", - policyRevision: "policy-v1", - }); - expect(Object.isFrozen(workspace)).toBe(true); - expect(Object.isFrozen(workspace.limits)).toBe(true); - expect(Object.isFrozen(workspace.protection)).toBe(true); - expect(Reflect.ownKeys(workspace)).toEqual(["limits", "protection"]); - expect(workspace.allows("copy")).toBe(false); - expect(workspace.allows("move")).toBe(false); - expect(workspace.allows("search")).toBe(false); - expect(workspace.permissions.has("copy")).toBe(false); - expect(Reflect.get(workspace, "storage")).toBeUndefined(); - expect(Reflect.get(workspace, "cipher")).toBeUndefined(); - }); - - it("rejects plaintext, noncanonical outer records, and malformed nmc1-only writes", async () => { - const { cipher, client, storage, workspace } = fixture(); - await putRaw(client, "plaintext.txt", "provider plaintext"); - await expect(workspace.readText("plaintext.txt")).rejects.toMatchObject({ - cause: undefined, - code: StorageErrorCode.PROVIDER, - key: "plaintext.txt", - message: "Protected workspace read failed.", - }); - - await workspace.writeFile("canonical.txt", "protected", { mode: "create" }); - const outer = parseRaw(await rawText(client, "canonical.txt")); - await putRaw( - client, - "reordered.txt", - JSON.stringify({ - record: outer.record, - v: outer.v, - version: outer.version, - metadata: outer.metadata, - content: outer.content, - }), - ); - await expect(workspace.stat("reordered.txt")).rejects.toMatchObject({ - code: StorageErrorCode.PROVIDER, - }); - - const invalidCipher: AuthenticatedWorkspaceCipher = Object.freeze({ - decryptBytes: async () => new Uint8Array(), - encryptBytes: async () => "not-an-nmc1-envelope", - }); - const invalid = protectedView(storage, invalidCipher, { scopeContext: "invalid-cipher" }); - await expect( - invalid.writeFile("invalid.txt", "secret", { mode: "create" }), - ).rejects.toMatchObject({ - code: StorageErrorCode.PROVIDER, - }); - await expect(client.exists(`${PREFIX}/invalid.txt`)).resolves.toBe(false); - - const readAgain = protectedView(storage, cipher); - await expect(readAgain.readText("canonical.txt")).resolves.toMatchObject({ text: "protected" }); - }); - - it("binds ciphertext to scope, policy, full mounted path, record, version, and purpose", async () => { - const { cipher, client, storage, workspace } = fixture(); - await workspace.writeFile("source.txt", "bound content", { mode: "create" }); - const sourceRaw = await rawText(client, "source.txt"); - - await putRaw(client, "replayed.txt", sourceRaw); - await expect(workspace.readText("replayed.txt")).rejects.toMatchObject({ - code: StorageErrorCode.PROVIDER, - }); - - const otherScope = protectedView(storage, cipher, { scopeContext: "tenant/other" }); - await expect(otherScope.readText("source.txt")).rejects.toMatchObject({ - code: StorageErrorCode.PROVIDER, - }); - const otherPolicy = protectedView(storage, cipher, { policyRevision: "policy-v2" }); - await expect(otherPolicy.readText("source.txt")).rejects.toMatchObject({ - code: StorageErrorCode.PROVIDER, - }); - - const versionReplay = { ...parseRaw(sourceRaw), version: randomUUID() }; - await putRaw(client, "source.txt", JSON.stringify(versionReplay)); - await expect(workspace.stat("source.txt")).rejects.toMatchObject({ - code: StorageErrorCode.PROVIDER, - }); - - const recordReplay = { ...parseRaw(sourceRaw), record: randomUUID() }; - await putRaw(client, "source.txt", JSON.stringify(recordReplay)); - await expect(workspace.stat("source.txt")).rejects.toMatchObject({ - code: StorageErrorCode.PROVIDER, - }); - - const purposeReplay = parseRaw(sourceRaw); - await putRaw( - client, - "source.txt", - JSON.stringify({ - ...purposeReplay, - metadata: purposeReplay.content, - content: purposeReplay.metadata, - }), - ); - await expect(workspace.stat("source.txt")).rejects.toMatchObject({ - code: StorageErrorCode.PROVIDER, - }); - - await putRaw(client, "source.txt", sourceRaw); - const child = workspace.mount("nested"); - await child.writeFile("mounted.txt", "same full path", { mode: "create" }); - await expect(workspace.readText("nested/mounted.txt")).resolves.toMatchObject({ - text: "same full path", - }); - }); - - it("authenticates metadata for stat/list and content for reads", async () => { - const { cipher, client, storage, workspace } = fixture(); - await workspace.writeFile("entry.txt", "authenticated body", { - contentType: "text/custom", - mode: "create", - }); - const original = parseRaw(await rawText(client, "entry.txt")); - - await putRaw( - client, - "entry.txt", - JSON.stringify({ ...original, content: mutateEnvelope(original.content) }), - ); - await expect(workspace.stat("entry.txt")).resolves.toMatchObject({ - contentType: "text/custom", - size: 18, - }); - await expect(workspace.list()).resolves.toMatchObject({ - entries: [expect.objectContaining({ contentType: "text/custom", path: "entry.txt" })], - }); - await expect(workspace.readText("entry.txt")).rejects.toMatchObject({ - code: StorageErrorCode.PROVIDER, - }); - - await putRaw( - client, - "entry.txt", - JSON.stringify({ ...original, metadata: mutateEnvelope(original.metadata) }), - ); - await expect(workspace.stat("entry.txt")).rejects.toMatchObject({ - code: StorageErrorCode.PROVIDER, - }); - await expect(workspace.list()).rejects.toMatchObject({ - code: StorageErrorCode.PROVIDER, - }); - - const metadataRewriter: AuthenticatedWorkspaceCipher = { - decryptBytes: cipher.decryptBytes.bind(cipher), - async encryptBytes(plaintext, context): Promise { - const decoded = new TextDecoder().decode(plaintext); - const marker = '"contentType":"text/plain"'; - if (!decoded.startsWith("{") || !decoded.includes(marker)) { - return await cipher.encryptBytes(plaintext, context); - } - const rewritten = new TextEncoder().encode( - decoded.replace(marker, '"contentType":" text/plain"'), - ); - try { - return await cipher.encryptBytes(rewritten, context); - } finally { - rewritten.fill(0); - } - }, - }; - const poisoned = protectedView(storage, metadataRewriter); - await poisoned.writeFile("invalid-content-type.txt", "body", { - contentType: "text/plain", - mode: "create", - }); - await expect(poisoned.stat("invalid-content-type.txt")).rejects.toMatchObject({ - code: StorageErrorCode.PROVIDER, - }); - }); - - it("preserves create, CAS replace, and explicit overwrite semantics", async () => { - const { client, workspace } = fixture(); - const created = await workspace.writeFile("cas.txt", "v1", { mode: "create" }); - const firstOuter = parseRaw(await rawText(client, "cas.txt")); - - await expect( - workspace.writeFile("cas.txt", "duplicate", { mode: "create" }), - ).rejects.toMatchObject({ - code: StorageErrorCode.CONFLICT, - message: "Protected workspace write failed.", - }); - await expect( - workspace.writeFile("cas.txt", "stale", { etag: "stale-etag", mode: "replace" }), - ).rejects.toMatchObject({ code: StorageErrorCode.CONFLICT }); - await expect(workspace.readText("cas.txt")).resolves.toMatchObject({ text: "v1" }); - - const replaced = await workspace.writeFile("cas.txt", "v2", { - etag: created.etag ?? "", - mode: "replace", - }); - const secondOuter = parseRaw(await rawText(client, "cas.txt")); - expect(secondOuter.record).toBe(firstOuter.record); - expect(secondOuter.version).not.toBe(firstOuter.version); - await expect(workspace.readText("cas.txt")).resolves.toMatchObject({ text: "v2" }); - - await workspace.writeFile("cas.txt", "v3", { mode: "overwrite" }); - const thirdOuter = parseRaw(await rawText(client, "cas.txt")); - expect(thirdOuter.record).not.toBe(secondOuter.record); - expect(thirdOuter.version).not.toBe(secondOuter.version); - await expect(workspace.readText("cas.txt")).resolves.toMatchObject({ text: "v3" }); - - await expect( - workspace.writeFile("metadata.txt", "no", { - metadata: { leaked: "plaintext" }, - mode: "create", - }), - ).rejects.toMatchObject({ code: StorageErrorCode.INVALID_ARGUMENT }); - const createWithEtag = { - etag: replaced.etag ?? "etag", - mode: "create" as const, - }; - await expect( - workspace.writeFile("etag-create.txt", "no", createWithEtag), - ).rejects.toMatchObject({ code: StorageErrorCode.INVALID_ARGUMENT }); - const overwriteWithEtag = { - etag: "unexpected", - mode: "overwrite" as const, - }; - await expect( - workspace.writeFile("etag-overwrite.txt", "no", overwriteWithEtag), - ).rejects.toMatchObject({ code: StorageErrorCode.INVALID_ARGUMENT }); - }); - - it("reads retained key revisions and writes new versions with the active key", async () => { - const { client, storage, workspace } = fixture(); - const created = await workspace.writeFile("rotation.txt", "encrypted with v1", { - mode: "create", - }); - const firstOuter = parseRaw(await rawText(client, "rotation.txt")); - - const rotatedEngine = new CipherEngine({ - defaultProvider: "local", - maxPayloadBytes: LOGICAL_LIMITS.maxWriteBytes, - providers: [ - { - name: "local", - provider: new AesKeyRingProvider({ - activeKeyId: "test-key-v2", - keys: { - "test-key-v1": KEY, - "test-key-v2": new Uint8Array(32).fill(0x42), - }, - }), - }, - ], - }); - const rotated = protectedView(storage, createCipherEngineWorkspaceCipher(rotatedEngine)); - await expect(rotated.readText("rotation.txt")).resolves.toMatchObject({ - text: "encrypted with v1", - }); - expect(rotatedEngine.inspect(firstOuter.content).keyReference).toBe("test-key-v1"); - - await rotated.writeFile("rotation.txt", "encrypted with v2", { - etag: created.etag ?? "", - mode: "replace", - }); - await expect(rotated.readText("rotation.txt")).resolves.toMatchObject({ - text: "encrypted with v2", - }); - const secondOuter = parseRaw(await rawText(client, "rotation.txt")); - expect(rotatedEngine.inspect(secondOuter.metadata).keyReference).toBe("test-key-v2"); - expect(rotatedEngine.inspect(secondOuter.content).keyReference).toBe("test-key-v2"); - }); - - it("preserves list/search query fields and makes path disclosure explicit", async () => { - const { cipher, storage, workspace } = fixture(); - await workspace.writeFile("root.txt", "root", { mode: "create" }); - await workspace.writeFile("reports/Quarter.TXT", "quarter", { mode: "create" }); - await workspace.writeFile("reports/second.txt", "second", { mode: "create" }); - - const first = await workspace.list({ directory: "reports", limit: 1, recursive: true }); - expect(first.entries).toHaveLength(1); - expect(first.entries[0]?.path).toMatch(/^reports\//u); - expect(first.cursor).toEqual(expect.any(String)); - const second = await workspace.list({ - cursor: first.cursor, - directory: "reports", - limit: 1, - recursive: true, - }); - expect(second.entries).toHaveLength(1); - expect(second.entries[0]?.path).toMatch(/^reports\//u); - - await expect(workspace.search("Quarter")).rejects.toMatchObject({ - code: StorageErrorCode.NOT_SUPPORTED, - }); - expect(workspace.permissions.has("search")).toBe(false); - - const searchable = protectedView(storage, cipher, { pathSearch: "provider-visible" }); - expect(searchable.protection.pathSearch).toBe("provider-visible"); - expect(searchable.allows("search")).toBe(true); - const results = await searchable.search("quarter", { - caseInsensitive: true, - directory: "reports", - limit: 1, - match: "substring", - }); - expect(results.entries).toEqual([ - expect.objectContaining({ path: "reports/Quarter.TXT", size: 7 }), - ]); - }); - - it("forwards conditional and unconditional delete without exposing copy/move", async () => { - const { workspace } = fixture(); - const conditional = await workspace.writeFile("conditional.txt", "delete me", { - mode: "create", - }); - await workspace.deleteFile("conditional.txt", { etag: conditional.etag ?? "" }); - await expect(workspace.stat("conditional.txt")).rejects.toMatchObject({ - code: StorageErrorCode.NOT_FOUND, - }); - - await workspace.writeFile("unconditional.txt", "delete me too", { mode: "create" }); - await workspace.deleteFile("unconditional.txt", { mode: "unconditional" }); - await expect(workspace.stat("unconditional.txt")).rejects.toMatchObject({ - code: StorageErrorCode.NOT_FOUND, - }); - - await expect( - workspace.copyFile("source.txt", "copy.txt", { mode: "overwrite" }), - ).rejects.toMatchObject({ code: StorageErrorCode.NOT_SUPPORTED }); - await expect( - workspace.moveFile("source.txt", "move.txt", { etag: "etag" }), - ).rejects.toMatchObject({ code: StorageErrorCode.NOT_SUPPORTED }); - }); - - it("narrows dependent permissions and logical limits across mounts", async () => { - const { cipher, client, storage, workspace } = fixture(); - await workspace.writeFile("child/existing.txt", "123456", { mode: "create" }); - const child = workspace.mount("child", { - limits: { - maxPageSize: 2, - maxPathBytes: 16, - maxReadBytes: 10, - maxWriteBytes: 5, - }, - permissions: ["list", "read", "create"], - }); - await expect(child.writeFile("five.txt", "12345", { mode: "create" })).resolves.toMatchObject({ - size: 5, - }); - await expect(child.writeFile("six.txt", "123456", { mode: "create" })).rejects.toMatchObject({ - code: StorageErrorCode.LIMIT_EXCEEDED, - }); - await expect(child.list({ limit: 3 })).rejects.toMatchObject({ - code: StorageErrorCode.LIMIT_EXCEEDED, - }); - await expect( - child.writeFile("this-path-is-too-long.txt", "1", { mode: "create" }), - ).rejects.toMatchObject({ code: StorageErrorCode.INVALID_ARGUMENT }); - await expect(workspace.readText("child/five.txt")).resolves.toMatchObject({ text: "12345" }); - await expect(child.readText("existing.txt")).resolves.toMatchObject({ text: "123456" }); - const reconstructed = protectedView(storage, cipher, { - limits: Object.freeze({ - ...LOGICAL_LIMITS, - maxReadBytes: 10, - maxWriteBytes: 5, - }), - }); - await expect(reconstructed.readText("child/existing.txt")).resolves.toMatchObject({ - text: "123456", - }); - expect(child.permissions).toEqual(new Set(["list", "read", "create"])); - expect(() => child.mount("forbidden", { permissions: ["copy"] })).toThrowError( - expect.objectContaining({ code: StorageErrorCode.UNAUTHORIZED }), - ); - - const listOnlyStorage = backing(client, "list-only", ["list"]); - const listOnly = protectedView(listOnlyStorage, cipher, { scopeContext: "list-only" }); - expect(listOnly.allows("list")).toBe(false); - expect(listOnly.permissions.has("list")).toBe(false); - - const replaceOnlyStorage = backing(client, "replace-only", ["replace"]); - const replaceOnly = protectedView(replaceOnlyStorage, cipher, { scopeContext: "replace-only" }); - expect(replaceOnly.allows("replace")).toBe(false); - - const searchWithoutReadStorage = backing(client, "search-only", ["search"]); - const searchWithoutRead = protectedView(searchWithoutReadStorage, cipher, { - pathSearch: "provider-visible", - scopeContext: "search-only", - }); - expect(searchWithoutRead.allows("search")).toBe(false); - }); - - it("enforces lifetime/caller cancellation and one whole-operation timeout", async () => { - const { cipher, storage } = fixture(); - const lifetime = new AbortController(); - const workspace = protectedView(storage, cipher, { signal: lifetime.signal }); - const listenersBefore = getEventListeners(lifetime.signal, "abort").length; - await workspace.writeFile("before.txt", "protected", { - mode: "create", - signal: lifetime.signal, - }); - expect(getEventListeners(lifetime.signal, "abort")).toHaveLength(listenersBefore); - const child = workspace.mount("child"); - lifetime.abort("do-not-expose-this-reason"); - const backingRead = vi.spyOn(storage, "readText"); - - await expect(workspace.readText("before.txt")).rejects.toMatchObject({ - aborted: true, - cause: undefined, - code: StorageErrorCode.ABORTED, - message: "Protected workspace read failed.", - }); - expect(backingRead).not.toHaveBeenCalled(); - await expect(child.list()).rejects.toMatchObject({ - aborted: true, - code: StorageErrorCode.ABORTED, - }); - expect(() => workspace.mount("late")).toThrowError( - expect.objectContaining({ aborted: true, code: StorageErrorCode.ABORTED }), - ); - - const caller = new AbortController(); - caller.abort({ secret: "reason" }); - const fresh = protectedView(storage, cipher); - const backingList = vi.spyOn(storage, "list"); - await expect(fresh.list({ signal: caller.signal })).rejects.toMatchObject({ - aborted: true, - cause: undefined, - code: StorageErrorCode.ABORTED, - }); - expect(backingList).not.toHaveBeenCalled(); - await expect(fresh.list({ timeout: Number.MAX_SAFE_INTEGER })).rejects.toMatchObject({ - code: StorageErrorCode.INVALID_ARGUMENT, - }); - expect(backingList).not.toHaveBeenCalled(); - - const encryptSpy = vi.fn(cipher.encryptBytes.bind(cipher)); - const guardedCipher: AuthenticatedWorkspaceCipher = { - decryptBytes: vi.fn(cipher.decryptBytes.bind(cipher)), - encryptBytes: encryptSpy, - }; - const guarded = protectedView(storage, guardedCipher, { scopeContext: "pre-aborted" }); - await expect( - guarded.writeFile("never-started.txt", "secret", { - mode: "create", - signal: caller.signal, - }), - ).rejects.toMatchObject({ code: StorageErrorCode.ABORTED }); - expect(encryptSpy).not.toHaveBeenCalled(); - - const hangingCipher: AuthenticatedWorkspaceCipher = Object.freeze({ - decryptBytes: async () => await new Promise(() => undefined), - encryptBytes: async () => await new Promise(() => undefined), - }); - const hanging = protectedView(storage, hangingCipher, { scopeContext: "timeout" }); - await expect( - hanging.writeFile("timeout.txt", "never", { mode: "create", timeout: 10 }), - ).rejects.toMatchObject({ - aborted: true, - code: StorageErrorCode.TIMEOUT, - timedOut: true, - }); - }); - - it("enforces separate logical and physical bounds", async () => { - const { cipher, storage } = fixture(); - expect(() => - protectedView(storage, cipher, { - limits: Object.freeze({ ...LOGICAL_LIMITS, maxPageSize: 1 }), - }), - ).toThrowError(/must match its backing workspace limit/u); - expect(() => - protectedView(storage, cipher, { - maxCiphertextBytes: BACKING_LIMITS.maxReadBytes + 1, - }), - ).toThrowError(/ciphertext limit/u); - - const tinyLimits: Readonly = Object.freeze({ - ...LOGICAL_LIMITS, - maxReadBytes: 8, - maxWriteBytes: 8, - }); - const tinyPhysical = protectedView(storage, cipher, { - limits: tinyLimits, - maxCiphertextBytes: 8, - scopeContext: "tiny", - }); - await expect( - tinyPhysical.writeFile("tiny.txt", "12345678", { mode: "create" }), - ).rejects.toMatchObject({ code: StorageErrorCode.LIMIT_EXCEEDED }); - await expect( - tinyPhysical.writeFile("too-large.txt", "123456789", { mode: "create" }), - ).rejects.toMatchObject({ code: StorageErrorCode.LIMIT_EXCEEDED }); - }); - - it("sanitizes backing and cipher failures at the public boundary", async () => { - const { cipher, storage, workspace } = fixture(); - vi.spyOn(storage, "readText").mockRejectedValueOnce( - new StorageError("credential=provider-secret", { - cause: new Error("provider stack secret"), - code: StorageErrorCode.PROVIDER, - key: "physical/provider/key", - operation: "download", - }), - ); - await expect(workspace.readText("logical.txt")).rejects.toMatchObject({ - cause: undefined, - code: StorageErrorCode.PROVIDER, - key: "logical.txt", - message: "Protected workspace read failed.", - operation: "read", - }); - - await workspace.writeFile("cipher.txt", "protected", { mode: "create" }); - const failingCipher: AuthenticatedWorkspaceCipher = Object.freeze({ - decryptBytes: async () => { - throw new Error("kms credential and ciphertext details"); - }, - encryptBytes: cipher.encryptBytes.bind(cipher), - }); - const failing = protectedView(storage, failingCipher); - await expect(failing.stat("cipher.txt")).rejects.toMatchObject({ - cause: undefined, - code: StorageErrorCode.PROVIDER, - message: "Protected workspace stat failed.", - }); - }); -}); - -describe("CipherEngine workspace cipher adapter", () => { - it("fails configuration that could write envelopes its read policy rejects", () => { - const testEngine = engine(); - expect(() => - createCipherEngineWorkspaceCipher(testEngine, { - allowedProviders: ["legacy"], - }), - ).toThrowError(/write provider/u); - expect(() => - createCipherEngineWorkspaceCipher(testEngine, { - allowedProviders: ["missing"], - provider: "missing", - }), - ).toThrowError(/not registered/u); - }); -}); diff --git a/src/storage-workspace/protected-storage-workspace.ts b/src/storage-workspace/protected-storage-workspace.ts deleted file mode 100644 index 3de8ef1..0000000 --- a/src/storage-workspace/protected-storage-workspace.ts +++ /dev/null @@ -1,1329 +0,0 @@ -import { randomUUID } from "node:crypto"; - -import { - StorageError, - StorageErrorCode, - isStorageError, - type StorageErrorCode as StorageErrorCodeValue, - type StorageOperationOptions, -} from "@nestm/storage/core"; -import type { - StorageWorkspace, - StorageWorkspaceBody, - StorageWorkspaceCopyOptions, - StorageWorkspaceDeleteOptions, - StorageWorkspaceEntry, - StorageWorkspaceFile, - StorageWorkspaceLimits, - StorageWorkspaceListOptions, - StorageWorkspaceMountOptions, - StorageWorkspaceMutationOptions, - StorageWorkspacePage, - StorageWorkspacePermission, - StorageWorkspaceReadOptions, - StorageWorkspaceSearchOptions, - StorageWorkspaceTextFile, - StorageWorkspaceWriteOptions, -} from "@nestm/storage/workspace"; - -import type { - AuthenticatedWorkspaceCipher, - AuthenticatedWorkspaceCipherContext, - ProtectedStorageWorkspace, - ProtectedStorageWorkspaceByteFile, - ProtectedStorageWorkspacePathSearch, - ProtectedStorageWorkspaceProtection, - ProtectStorageWorkspaceOptions, -} from "./types.js"; - -const OUTER_VERSION = 1 as const; -const OUTER_CONTENT_TYPE = "application/octet-stream"; -const CIPHER_ENVELOPE_PREFIX = "nmc1."; -const CONTEXT_DOMAIN = "nestm:crypto:protected-storage-workspace:v1"; -const MAX_SCOPE_CONTEXT_BYTES = 4096; -const MAX_POLICY_REVISION_BYTES = 256; -const MAX_CONTENT_TYPE_BYTES = 255; -const MAX_OPERATION_TIMEOUT_MS = 2_147_483_647; -const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u; -const textEncoder = new TextEncoder(); -const strictTextDecoder = new TextDecoder("utf-8", { fatal: true }); -const FORBIDDEN_PERMISSIONS = new Set(["copy", "move"]); - -type ProtectedPurpose = "metadata" | "body"; - -interface ProtectedOuterObject { - readonly v: typeof OUTER_VERSION; - readonly record: string; - readonly version: string; - readonly metadata: string; - readonly content: string; -} - -interface ProtectedMetadata { - readonly contentType: string; - readonly path: string; - readonly size: number; -} - -interface AuthenticatedMetadata { - readonly contentType: string; - readonly outer: ProtectedOuterObject; - readonly size: number; -} - -interface OperationRuntime { - readonly signal: AbortSignal | undefined; - readonly timedOut: () => boolean; - readonly close: () => void; -} - -interface CapturedOptions { - readonly storage: StorageWorkspace; - readonly cipher: AuthenticatedWorkspaceCipher; - readonly scopeContext: Uint8Array; - readonly policyRevision: string; - readonly limits: Readonly; - readonly maxCiphertextBytes: number; - readonly maxPlaintextBytes: number; - readonly signal?: AbortSignal; - readonly pathSearch: ProtectedStorageWorkspacePathSearch; -} - -/** - * Build a strict encrypted-only view over an existing StorageWorkspace. - * - * The backing workspace remains the authority for paths, CAS, cursors, and - * provider access. This wrapper owns only authenticated serialization and - * logical plaintext projection. - */ -export function protectStorageWorkspace( - options: ProtectStorageWorkspaceOptions, -): ProtectedStorageWorkspace { - const captured = captureOptions(options); - try { - captured.signal?.throwIfAborted(); - } catch { - captured.scopeContext.fill(0); - throw protectedError(StorageErrorCode.ABORTED, "protect", undefined, { - aborted: true, - }); - } - try { - return new ProtectedStorageWorkspaceImplementation(captured, ""); - } finally { - captured.scopeContext.fill(0); - } -} - -class ProtectedStorageWorkspaceImplementation implements ProtectedStorageWorkspace { - readonly #storage: StorageWorkspace; - readonly #cipher: AuthenticatedWorkspaceCipher; - readonly #scopeContext: Uint8Array; - readonly #policyRevision: string; - readonly #directory: string; - readonly #maxCiphertextBytes: number; - readonly #maxPlaintextBytes: number; - readonly #lifetimeSignal: AbortSignal | undefined; - readonly #pathSearch: ProtectedStorageWorkspacePathSearch; - readonly limits: Readonly; - readonly protection: Readonly; - - constructor(options: CapturedOptions, directory: string) { - this.#storage = options.storage; - this.#cipher = options.cipher; - this.#scopeContext = new Uint8Array(options.scopeContext); - this.#policyRevision = options.policyRevision; - this.#directory = directory; - this.#maxCiphertextBytes = options.maxCiphertextBytes; - this.#maxPlaintextBytes = options.maxPlaintextBytes; - this.#lifetimeSignal = options.signal; - this.#pathSearch = options.pathSearch; - this.limits = Object.freeze({ ...options.limits }); - this.protection = Object.freeze({ - body: "encrypted", - cipherEnvelope: "nmc1", - kind: "authenticated-encryption", - metadata: "encrypted", - outerFormat: "nestm-protected-storage-workspace", - outerVersion: OUTER_VERSION, - pathBinding: "scope-policy-path-record-version-purpose", - pathSearch: options.pathSearch, - policyRevision: options.policyRevision, - }); - Object.freeze(this); - } - - get permissions(): ReadonlySet { - return new Set( - [...this.#storage.permissions].filter((permission) => this.#permissionAllowed(permission)), - ); - } - - allows(permission: StorageWorkspacePermission): boolean { - return this.#permissionAllowed(permission); - } - - async stat(path: string, options?: StorageOperationOptions): Promise { - return await this.#operate("stat", path, options, async (runtime) => { - this.#require("read", "stat", path); - const stored = await this.#readStored(path, options, runtime.signal); - const metadata = await this.#decryptMetadata(path, stored.text, runtime.signal); - return logicalFile(path, metadata, stored); - }); - } - - async readText( - path: string, - options?: StorageWorkspaceReadOptions, - ): Promise { - return await this.#operate("read", path, options, async (runtime) => { - this.#require("read", "read", path); - const file = await this.#readBytes(path, options, runtime.signal); - try { - return { ...withoutBytes(file), text: strictTextDecoder.decode(file.bytes) }; - } catch { - throw protectedError(StorageErrorCode.INVALID_ARGUMENT, "read", path, { permanent: true }); - } finally { - file.bytes.fill(0); - } - }); - } - - async readBytes( - path: string, - options?: StorageWorkspaceReadOptions, - ): Promise { - return await this.#operate("read", path, options, async (runtime) => { - this.#require("read", "read", path); - return await this.#readBytes(path, options, runtime.signal); - }); - } - - async list(options?: StorageWorkspaceListOptions): Promise { - return await this.#operate("list", options?.directory, options, async (runtime) => { - this.#require("list", "list", options?.directory); - const page = await this.#storage.list(listOptions(options, runtime.signal)); - return await this.#authenticatePage(page, operationOptions(options, runtime.signal)); - }); - } - - async search( - query: string, - options?: StorageWorkspaceSearchOptions, - ): Promise { - return await this.#operate("search", options?.directory, options, async (runtime) => { - if (this.#pathSearch !== "provider-visible") { - throw protectedError(StorageErrorCode.NOT_SUPPORTED, "search", options?.directory, { - permanent: true, - }); - } - this.#require("search", "search", options?.directory); - const page = await this.#storage.search(query, searchOptions(options, runtime.signal)); - return await this.#authenticatePage(page, operationOptions(options, runtime.signal)); - }); - } - - async writeFile( - path: string, - body: StorageWorkspaceBody, - options: StorageWorkspaceWriteOptions, - ): Promise { - return await this.#operate("write", path, options, async (runtime) => { - const mode = captureWriteMode(options, path); - this.#require(writePermission(mode), "write", path); - assertLogicalWritePath(path, this.limits.maxPathBytes); - if (Object.hasOwn(options, "metadata")) { - throw protectedError(StorageErrorCode.INVALID_ARGUMENT, "write", path, { - permanent: true, - }); - } - const plaintext = captureBody(body, path, this.limits.maxWriteBytes); - const clearPlaintext = (): void => { - plaintext.fill(0); - }; - runtime.signal?.addEventListener("abort", clearPlaintext, { once: true }); - try { - if (plaintext.byteLength > this.limits.maxWriteBytes) { - throw protectedError(StorageErrorCode.LIMIT_EXCEEDED, "write", path, { - permanent: true, - }); - } - const contentType = captureContentType(options.contentType ?? OUTER_CONTENT_TYPE, path); - let record: string = randomUUID(); - if (mode === "replace") { - const current = await this.#readStored(path, options, runtime.signal); - const authenticated = await this.#decryptMetadata(path, current.text, runtime.signal); - record = authenticated.outer.record; - } - const outer = await this.#encryptObject( - path, - plaintext, - contentType, - record, - randomUUID(), - runtime.signal, - ); - if (textEncoder.encode(outer).byteLength > this.#maxCiphertextBytes) { - throw protectedError(StorageErrorCode.LIMIT_EXCEEDED, "write", path, { - permanent: true, - }); - } - const stored = await this.#storage.writeFile( - path, - outer, - storageWriteOptions(options, mode, runtime.signal), - ); - return logicalFile(path, { contentType, size: plaintext.byteLength }, stored); - } finally { - runtime.signal?.removeEventListener("abort", clearPlaintext); - clearPlaintext(); - } - }); - } - - async copyFile( - _source: string, - destination: string, - options: StorageWorkspaceCopyOptions, - ): Promise { - return await this.#operate("copy", destination, options, async () => { - throw protectedError(StorageErrorCode.NOT_SUPPORTED, "copy", destination, { - permanent: true, - }); - }); - } - - async moveFile( - _source: string, - destination: string, - options: StorageWorkspaceMutationOptions, - ): Promise { - return await this.#operate("move", destination, options, async () => { - throw protectedError(StorageErrorCode.NOT_SUPPORTED, "move", destination, { - permanent: true, - }); - }); - } - - async deleteFile(path: string, options: StorageWorkspaceDeleteOptions): Promise { - await this.#operate("delete", path, options, async (runtime) => { - this.#require("delete", "delete", path); - await this.#storage.deleteFile(path, storageDeleteOptions(options, runtime.signal)); - }); - } - - mount(directory: string, options?: StorageWorkspaceMountOptions): ProtectedStorageWorkspace { - this.#throwIfLifetimeAborted("mount", directory); - try { - const limits = restrictLimits(this.limits, options?.limits); - const permissions = restrictPermissions(this.permissions, options?.permissions); - const storage = this.#storage.mount(directory, { - permissions, - limits: { - ...limits, - maxReadBytes: this.#storage.limits.maxReadBytes, - maxWriteBytes: this.#storage.limits.maxWriteBytes, - }, - }); - return new ProtectedStorageWorkspaceImplementation( - { - cipher: this.#cipher, - limits, - maxCiphertextBytes: this.#maxCiphertextBytes, - maxPlaintextBytes: this.#maxPlaintextBytes, - pathSearch: this.#pathSearch, - policyRevision: this.#policyRevision, - scopeContext: this.#scopeContext, - ...(this.#lifetimeSignal === undefined ? {} : { signal: this.#lifetimeSignal }), - storage, - }, - joinPath(this.#directory, directory), - ); - } catch (error: unknown) { - throw sanitizeError(error, "mount", directory, false, this.#lifetimeSignal?.aborted === true); - } - } - - #permissionAllowed(permission: StorageWorkspacePermission): boolean { - if (FORBIDDEN_PERMISSIONS.has(permission) || !this.#storage.allows(permission)) { - return false; - } - if (permission === "search") { - return this.#pathSearch === "provider-visible" && this.#storage.allows("read"); - } - if (permission === "list" || permission === "replace") { - return this.#storage.allows("read"); - } - return true; - } - - #require( - permission: StorageWorkspacePermission, - operation: string, - path: string | undefined, - ): void { - if (!this.#permissionAllowed(permission)) { - throw protectedError(StorageErrorCode.UNAUTHORIZED, operation, path, { - permanent: true, - }); - } - } - - async #readBytes( - path: string, - options: StorageWorkspaceReadOptions | undefined, - signal: AbortSignal | undefined, - ): Promise { - const maxBytes = options?.maxBytes ?? this.limits.maxReadBytes; - assertReadLimit(maxBytes, this.limits.maxReadBytes, path); - const stored = await this.#readStored(path, options, signal); - const metadata = await this.#decryptMetadata(path, stored.text, signal); - if (metadata.size > maxBytes) { - throw protectedError(StorageErrorCode.LIMIT_EXCEEDED, "read", path, { - permanent: true, - }); - } - let plaintext: Uint8Array | undefined; - try { - plaintext = await this.#decryptValue( - metadata.outer.content, - path, - metadata.outer.record, - metadata.outer.version, - "body", - signal, - ); - if (!isStableBytes(plaintext)) { - throw new TypeError("The workspace cipher returned invalid plaintext."); - } - const bytes = new Uint8Array(plaintext); - if (bytes.byteLength !== metadata.size || bytes.byteLength > this.#maxPlaintextBytes) { - bytes.fill(0); - throw new TypeError("Protected body size is invalid."); - } - return { ...logicalFile(path, metadata, stored), bytes }; - } catch (error: unknown) { - throw authenticationError("read", path, error); - } finally { - plaintext?.fill(0); - } - } - - async #readStored( - path: string, - options: StorageOperationOptions | undefined, - signal: AbortSignal | undefined, - ): Promise { - return await this.#storage.readText(path, { - ...operationOptions(options, signal), - maxBytes: this.#maxCiphertextBytes, - }); - } - - async #authenticatePage( - page: StorageWorkspacePage, - options: StorageOperationOptions | undefined, - ): Promise { - const entries: StorageWorkspaceEntry[] = []; - for (const entry of page.entries) { - if (entry.kind === "directory") { - entries.push(entry); - continue; - } - const stored = await this.#readStored(entry.path, options, options?.signal); - const metadata = await this.#decryptMetadata(entry.path, stored.text, options?.signal); - entries.push(logicalFile(entry.path, metadata, stored)); - } - return { - entries, - ...(page.cursor === undefined ? {} : { cursor: page.cursor }), - }; - } - - async #encryptObject( - path: string, - body: Uint8Array, - contentType: string, - record: string, - version: string, - signal: AbortSignal | undefined, - ): Promise { - const fullPath = joinPath(this.#directory, path); - const metadataBytes = textEncoder.encode( - JSON.stringify({ - contentType, - path: fullPath, - size: body.byteLength, - } satisfies ProtectedMetadata), - ); - try { - const metadata = await this.#encryptValue( - metadataBytes, - path, - record, - version, - "metadata", - signal, - ); - const encryptedBody = await this.#encryptValue(body, path, record, version, "body", signal); - assertCipherEnvelope(metadata); - assertCipherEnvelope(encryptedBody); - return JSON.stringify({ - v: OUTER_VERSION, - record, - version, - metadata, - content: encryptedBody, - } satisfies ProtectedOuterObject); - } catch (error: unknown) { - throw authenticationError("write", path, error); - } finally { - metadataBytes.fill(0); - } - } - - async #decryptMetadata( - path: string, - stored: string, - signal: AbortSignal | undefined, - ): Promise { - let plaintext: Uint8Array | undefined; - try { - const outer = parseOuter(stored, this.#maxCiphertextBytes); - plaintext = await this.#decryptValue( - outer.metadata, - path, - outer.record, - outer.version, - "metadata", - signal, - ); - if (!isStableBytes(plaintext)) { - throw new TypeError("The workspace cipher returned invalid metadata."); - } - const metadata = parseMetadata( - plaintext, - joinPath(this.#directory, path), - this.#maxPlaintextBytes, - ); - return { contentType: metadata.contentType, outer, size: metadata.size }; - } catch (error: unknown) { - throw authenticationError("read", path, error); - } finally { - plaintext?.fill(0); - } - } - - #cipherContext( - path: string, - record: string, - version: string, - purpose: ProtectedPurpose, - signal: AbortSignal | undefined, - ): AuthenticatedWorkspaceCipherContext { - const shared = [ - this.#scopeContext, - textEncoder.encode(this.#policyRevision), - textEncoder.encode(joinPath(this.#directory, path)), - textEncoder.encode(record), - textEncoder.encode(version), - textEncoder.encode(purpose), - ]; - return { - authenticatedData: frameContext("aad", shared), - keyContext: frameContext("key-context", shared), - ...(signal === undefined ? {} : { signal }), - }; - } - - async #encryptValue( - plaintext: Uint8Array, - path: string, - record: string, - version: string, - purpose: ProtectedPurpose, - signal: AbortSignal | undefined, - ): Promise { - const context = this.#cipherContext(path, record, version, purpose, signal); - const clearContext = (): void => { - context.authenticatedData.fill(0); - context.keyContext.fill(0); - }; - signal?.addEventListener("abort", clearContext, { once: true }); - try { - const encrypted = await this.#cipher.encryptBytes(plaintext, context); - if (signal?.aborted) throw signal.reason; - return encrypted; - } finally { - signal?.removeEventListener("abort", clearContext); - clearContext(); - } - } - - async #decryptValue( - envelope: string, - path: string, - record: string, - version: string, - purpose: ProtectedPurpose, - signal: AbortSignal | undefined, - ): Promise { - const context = this.#cipherContext(path, record, version, purpose, signal); - const clearContext = (): void => { - context.authenticatedData.fill(0); - context.keyContext.fill(0); - }; - signal?.addEventListener("abort", clearContext, { once: true }); - try { - const plaintext = await this.#cipher.decryptBytes(envelope, context); - if (signal?.aborted) { - if (isStableBytes(plaintext)) plaintext.fill(0); - throw signal.reason; - } - return plaintext; - } finally { - signal?.removeEventListener("abort", clearContext); - clearContext(); - } - } - - async #operate( - operation: string, - path: string | undefined, - options: StorageOperationOptions | undefined, - work: (runtime: OperationRuntime) => Promise, - ): Promise { - const runtime = operationRuntime(this.#lifetimeSignal, options, operation, path); - try { - return await raceWithAbort(async () => await work(runtime), runtime.signal); - } catch (error: unknown) { - throw sanitizeError( - error, - operation, - path, - runtime.timedOut(), - runtime.signal?.aborted === true, - ); - } finally { - runtime.close(); - } - } - - #throwIfLifetimeAborted(operation: string, path?: string): void { - if (this.#lifetimeSignal?.aborted) { - throw protectedError(StorageErrorCode.ABORTED, operation, path, { - aborted: true, - }); - } - } -} - -function captureOptions(options: ProtectStorageWorkspaceOptions): CapturedOptions { - if (typeof options !== "object" || options === null) { - throw new TypeError("Protected storage-workspace options are required."); - } - const storage = options.storage; - if ( - typeof storage !== "object" || - storage === null || - typeof storage.readText !== "function" || - typeof storage.writeFile !== "function" || - typeof storage.mount !== "function" - ) { - throw new TypeError("A StorageWorkspace is required."); - } - const cipher = options.cipher; - if ( - typeof cipher !== "object" || - cipher === null || - typeof cipher.encryptBytes !== "function" || - typeof cipher.decryptBytes !== "function" - ) { - throw new TypeError("An authenticated workspace cipher is required."); - } - const signal = options.signal; - assertOptionalSignal(signal); - const policyRevision = capturePolicyRevision(options.policyRevision); - const scopeContext = captureScopeContext(options.scopeContext); - const limits = captureLimits(options.limits, storage.limits); - const maxCiphertextBytes = positiveInteger( - options.maxCiphertextBytes, - "The protected workspace ciphertext limit", - ); - const maxPlaintextBytes = Math.max(limits.maxReadBytes, limits.maxWriteBytes); - if ( - maxCiphertextBytes < maxPlaintextBytes || - maxCiphertextBytes > storage.limits.maxReadBytes || - maxCiphertextBytes > storage.limits.maxWriteBytes - ) { - scopeContext.fill(0); - throw new TypeError( - "The ciphertext limit must cover logical plaintext and fit the backing workspace read/write limits.", - ); - } - const pathSearch = options.pathSearch ?? "disabled"; - if (pathSearch !== "disabled" && pathSearch !== "provider-visible") { - scopeContext.fill(0); - throw new TypeError("Protected workspace path search is invalid."); - } - return { - cipher, - limits, - maxCiphertextBytes, - maxPlaintextBytes, - pathSearch, - policyRevision, - scopeContext, - ...(signal === undefined ? {} : { signal }), - storage, - }; -} - -function captureLimits( - value: Readonly, - backing: Readonly, -): Readonly { - if (typeof value !== "object" || value === null) { - throw new TypeError("Protected workspace logical limits are required."); - } - const result = { - cursorTtlMs: captureInitialLimit("cursorTtlMs", value, backing, false), - maxCursorBytes: captureInitialLimit("maxCursorBytes", value, backing, false), - maxPageSize: captureInitialLimit("maxPageSize", value, backing, false), - maxPathBytes: captureInitialLimit("maxPathBytes", value, backing, false), - maxReadBytes: captureInitialLimit("maxReadBytes", value, backing, true), - maxSearchResults: captureInitialLimit("maxSearchResults", value, backing, false), - maxSearchScan: captureInitialLimit("maxSearchScan", value, backing, false), - maxWriteBytes: captureInitialLimit("maxWriteBytes", value, backing, true), - } satisfies StorageWorkspaceLimits; - return Object.freeze(result); -} - -function captureInitialLimit( - key: keyof StorageWorkspaceLimits, - value: Readonly, - backing: Readonly, - allowNarrowing: boolean, -): number { - const limit = positiveInteger(value[key], `The logical ${key} limit`); - if (limit > backing[key]) { - throw new TypeError(`The logical ${key} limit cannot exceed its backing workspace limit.`); - } - if (!allowNarrowing && limit !== backing[key]) { - throw new TypeError(`The initial logical ${key} limit must match its backing workspace limit.`); - } - return limit; -} - -function captureScopeContext(value: string | Uint8Array): Uint8Array { - let bytes: Uint8Array; - if (typeof value === "string") { - bytes = boundedUtf8(value, MAX_SCOPE_CONTEXT_BYTES, "Protected workspace scope context"); - } else if (isStableBytes(value)) { - if (value.byteLength === 0 || value.byteLength > MAX_SCOPE_CONTEXT_BYTES) { - throw new TypeError("Protected workspace scope context has an invalid size."); - } - bytes = new Uint8Array(value); - } else { - throw new TypeError("Protected workspace scope context must be text or bytes."); - } - if (bytes.byteLength === 0 || bytes.byteLength > MAX_SCOPE_CONTEXT_BYTES) { - bytes.fill(0); - throw new TypeError("Protected workspace scope context has an invalid size."); - } - return bytes; -} - -function capturePolicyRevision(value: string): string { - if ( - typeof value !== "string" || - value.length === 0 || - value.length > MAX_POLICY_REVISION_BYTES || - value.trim() !== value || - /\p{C}/u.test(value) - ) { - throw new TypeError("Protected workspace policy revision is invalid."); - } - const bytes = boundedUtf8( - value, - MAX_POLICY_REVISION_BYTES, - "Protected workspace policy revision", - ); - bytes.fill(0); - return value; -} - -function captureWriteMode( - options: StorageWorkspaceWriteOptions, - path: string, -): StorageWorkspaceWriteOptions["mode"] { - if (typeof options !== "object" || options === null) { - throw protectedError(StorageErrorCode.INVALID_ARGUMENT, "write", path, { - permanent: true, - }); - } - if (options.mode !== "create" && options.mode !== "replace" && options.mode !== "overwrite") { - throw protectedError(StorageErrorCode.INVALID_ARGUMENT, "write", path, { - permanent: true, - }); - } - if ( - options.mode === "replace" && - (typeof options.etag !== "string" || options.etag.length === 0) - ) { - throw protectedError(StorageErrorCode.INVALID_ARGUMENT, "write", path, { - permanent: true, - }); - } - if (options.mode !== "replace" && Object.hasOwn(options, "etag")) { - throw protectedError(StorageErrorCode.INVALID_ARGUMENT, "write", path, { - permanent: true, - }); - } - return options.mode; -} - -function writePermission(mode: StorageWorkspaceWriteOptions["mode"]): StorageWorkspacePermission { - if (mode === "overwrite") return "write"; - return mode; -} - -function storageWriteOptions( - options: StorageWorkspaceWriteOptions, - mode: StorageWorkspaceWriteOptions["mode"], - signal: AbortSignal | undefined, -): StorageWorkspaceWriteOptions { - const operation = operationOptions(options, signal); - if (mode === "create") { - return { ...operation, contentType: OUTER_CONTENT_TYPE, mode: "create" }; - } - if (mode === "overwrite") { - return { ...operation, contentType: OUTER_CONTENT_TYPE, mode: "overwrite" }; - } - if (options.mode !== "replace") { - throw new TypeError("Protected workspace replace options are invalid."); - } - return { - ...operation, - contentType: OUTER_CONTENT_TYPE, - etag: options.etag, - mode: "replace", - }; -} - -function captureBody(body: StorageWorkspaceBody, path: string, maxBytes: number): Uint8Array { - if (typeof body === "string") { - try { - return boundedUtf8(body, maxBytes, "Protected workspace body"); - } catch { - throw protectedError(StorageErrorCode.LIMIT_EXCEEDED, "write", path, { - permanent: true, - }); - } - } - if (!isStableBytes(body)) { - throw protectedError(StorageErrorCode.INVALID_ARGUMENT, "write", path, { - permanent: true, - }); - } - if (body.byteLength > maxBytes) { - throw protectedError(StorageErrorCode.LIMIT_EXCEEDED, "write", path, { - permanent: true, - }); - } - return new Uint8Array(body); -} - -function assertLogicalWritePath(path: string, maxBytes: number): void { - if (typeof path !== "string" || path.length === 0 || path.length > maxBytes) { - throw protectedError(StorageErrorCode.INVALID_ARGUMENT, "write", undefined, { - permanent: true, - }); - } - try { - const bytes = boundedUtf8(path, maxBytes, "Protected workspace path"); - bytes.fill(0); - } catch { - throw protectedError(StorageErrorCode.INVALID_ARGUMENT, "write", undefined, { - permanent: true, - }); - } -} - -function captureContentType(value: string, path: string): string { - if (!isCanonicalContentType(value)) { - throw protectedError(StorageErrorCode.INVALID_ARGUMENT, "write", path, { - permanent: true, - }); - } - return value; -} - -function isCanonicalContentType(value: unknown): value is string { - if ( - typeof value !== "string" || - value.length === 0 || - value.length > MAX_CONTENT_TYPE_BYTES || - value.trim() !== value || - /\p{C}/u.test(value) - ) { - return false; - } - try { - const bytes = boundedUtf8(value, MAX_CONTENT_TYPE_BYTES, "Protected content type"); - bytes.fill(0); - } catch { - return false; - } - return true; -} - -function parseOuter(stored: string, maxCiphertextBytes: number): ProtectedOuterObject { - if (typeof stored !== "string" || textEncoder.encode(stored).byteLength > maxCiphertextBytes) { - throw new TypeError("Protected outer object exceeds its physical limit."); - } - const parsed: unknown = JSON.parse(stored); - if (!isRecord(parsed) || Object.keys(parsed).join(",") !== "v,record,version,metadata,content") { - throw new TypeError("Protected outer object is malformed."); - } - if ( - parsed.v !== OUTER_VERSION || - typeof parsed.record !== "string" || - !UUID_V4_PATTERN.test(parsed.record) || - typeof parsed.version !== "string" || - !UUID_V4_PATTERN.test(parsed.version) || - typeof parsed.metadata !== "string" || - typeof parsed.content !== "string" - ) { - throw new TypeError("Protected outer object is malformed."); - } - assertCipherEnvelope(parsed.metadata); - assertCipherEnvelope(parsed.content); - return { - content: parsed.content, - metadata: parsed.metadata, - record: parsed.record, - v: OUTER_VERSION, - version: parsed.version, - }; -} - -function parseMetadata( - bytes: Uint8Array, - expectedPath: string, - maxPlaintextBytes: number, -): ProtectedMetadata { - const parsed: unknown = JSON.parse(strictTextDecoder.decode(bytes)); - if (!isRecord(parsed) || Object.keys(parsed).join(",") !== "contentType,path,size") { - throw new TypeError("Protected metadata is malformed."); - } - const size = parsed.size; - if ( - !isCanonicalContentType(parsed.contentType) || - parsed.path !== expectedPath || - typeof size !== "number" || - !Number.isSafeInteger(size) || - size < 0 || - size > maxPlaintextBytes - ) { - throw new TypeError("Protected metadata is malformed."); - } - return { contentType: parsed.contentType, path: expectedPath, size }; -} - -function assertCipherEnvelope(value: string): void { - if (typeof value !== "string" || !value.startsWith(CIPHER_ENVELOPE_PREFIX)) { - throw new TypeError("The workspace cipher must return an nmc1 envelope."); - } -} - -function logicalFile( - path: string, - logical: Pick, - stored: Pick, -): StorageWorkspaceFile { - const name = path.slice(path.lastIndexOf("/") + 1); - return { - contentType: logical.contentType, - ...(stored.etag === undefined ? {} : { etag: stored.etag }), - kind: "file", - ...(stored.lastModified === undefined ? {} : { lastModified: new Date(stored.lastModified) }), - name, - path, - size: logical.size, - }; -} - -function withoutBytes(file: ProtectedStorageWorkspaceByteFile): StorageWorkspaceFile { - return { - contentType: file.contentType, - ...(file.etag === undefined ? {} : { etag: file.etag }), - kind: "file", - ...(file.lastModified === undefined ? {} : { lastModified: file.lastModified }), - name: file.name, - path: file.path, - size: file.size, - }; -} - -function assertReadLimit(value: number, maximum: number, path: string): void { - if (!Number.isSafeInteger(value) || value <= 0) { - throw protectedError(StorageErrorCode.INVALID_ARGUMENT, "read", path, { - permanent: true, - }); - } - if (value > maximum) { - throw protectedError(StorageErrorCode.LIMIT_EXCEEDED, "read", path, { - permanent: true, - }); - } -} - -function restrictLimits( - parent: Readonly, - requested: Partial | undefined, -): Readonly { - const result = { - cursorTtlMs: childLimit(requested?.cursorTtlMs, parent.cursorTtlMs), - maxCursorBytes: childLimit(requested?.maxCursorBytes, parent.maxCursorBytes), - maxPageSize: childLimit(requested?.maxPageSize, parent.maxPageSize), - maxPathBytes: childLimit(requested?.maxPathBytes, parent.maxPathBytes), - maxReadBytes: childLimit(requested?.maxReadBytes, parent.maxReadBytes), - maxSearchResults: childLimit(requested?.maxSearchResults, parent.maxSearchResults), - maxSearchScan: childLimit(requested?.maxSearchScan, parent.maxSearchScan), - maxWriteBytes: childLimit(requested?.maxWriteBytes, parent.maxWriteBytes), - } satisfies StorageWorkspaceLimits; - return Object.freeze(result); -} - -function childLimit(value: number | undefined, parent: number): number { - if (value === undefined) return parent; - if (!Number.isSafeInteger(value) || value <= 0 || value > parent) { - throw protectedError(StorageErrorCode.INVALID_ARGUMENT, "mount", undefined, { - permanent: true, - }); - } - return value; -} - -function restrictPermissions( - parent: ReadonlySet, - requested: Iterable | undefined, -): readonly StorageWorkspacePermission[] { - if (requested === undefined) return Object.freeze([...parent]); - const result: StorageWorkspacePermission[] = []; - for (const permission of requested) { - if (!parent.has(permission)) { - throw protectedError(StorageErrorCode.UNAUTHORIZED, "mount", undefined, { - permanent: true, - }); - } - result.push(permission); - } - return Object.freeze(result); -} - -function operationOptions( - options: StorageOperationOptions | undefined, - signal: AbortSignal | undefined, -): StorageOperationOptions | undefined { - if (options === undefined && signal === undefined) return undefined; - return { - ...(options?.retries === undefined ? {} : { retries: options.retries }), - ...(signal === undefined ? {} : { signal }), - }; -} - -function listOptions( - options: StorageWorkspaceListOptions | undefined, - signal: AbortSignal | undefined, -): StorageWorkspaceListOptions { - return { - ...(options?.cursor === undefined ? {} : { cursor: options.cursor }), - ...(options?.directory === undefined ? {} : { directory: options.directory }), - ...(options?.limit === undefined ? {} : { limit: options.limit }), - ...(options?.recursive === undefined ? {} : { recursive: options.recursive }), - ...operationOptions(options, signal), - }; -} - -function searchOptions( - options: StorageWorkspaceSearchOptions | undefined, - signal: AbortSignal | undefined, -): StorageWorkspaceSearchOptions { - return { - ...(options?.caseInsensitive === undefined ? {} : { caseInsensitive: options.caseInsensitive }), - ...(options?.cursor === undefined ? {} : { cursor: options.cursor }), - ...(options?.directory === undefined ? {} : { directory: options.directory }), - ...(options?.limit === undefined ? {} : { limit: options.limit }), - ...(options?.match === undefined ? {} : { match: options.match }), - ...operationOptions(options, signal), - }; -} - -function storageDeleteOptions( - options: StorageWorkspaceDeleteOptions, - signal: AbortSignal | undefined, -): StorageWorkspaceDeleteOptions { - const operation = operationOptions(options, signal); - if ("mode" in options && options.mode === "unconditional") { - return { ...operation, mode: "unconditional" }; - } - if (!("etag" in options)) { - throw new TypeError("Protected workspace delete options are invalid."); - } - return { ...operation, etag: options.etag }; -} - -function operationRuntime( - lifetimeSignal: AbortSignal | undefined, - options: StorageOperationOptions | undefined, - operation: string, - path: string | undefined, -): OperationRuntime { - if (options !== undefined && (typeof options !== "object" || options === null)) { - throw protectedError(StorageErrorCode.INVALID_ARGUMENT, operation, path, { - permanent: true, - }); - } - const callerSignal = options?.signal; - if (callerSignal !== undefined && !isAbortSignal(callerSignal)) { - throw protectedError(StorageErrorCode.INVALID_ARGUMENT, operation, path, { - permanent: true, - }); - } - const timeout = options?.timeout; - if ( - timeout !== undefined && - (!Number.isSafeInteger(timeout) || timeout <= 0 || timeout > MAX_OPERATION_TIMEOUT_MS) - ) { - throw protectedError(StorageErrorCode.INVALID_ARGUMENT, operation, path, { - permanent: true, - }); - } - const sources = [ - ...new Set( - [lifetimeSignal, callerSignal].filter( - (signal): signal is AbortSignal => signal !== undefined, - ), - ), - ]; - if (sources.length === 0 && timeout === undefined) { - return { close: () => undefined, signal: undefined, timedOut: () => false }; - } - const controller = new AbortController(); - let timeoutReached = false; - const listeners = new Map void>(); - for (const source of sources) { - const listener = (): void => controller.abort(source.reason); - listeners.set(source, listener); - source.addEventListener("abort", listener, { once: true }); - if (source.aborted) listener(); - } - const timer = - timeout === undefined - ? undefined - : setTimeout(() => { - timeoutReached = true; - controller.abort(new DOMException("The operation timed out.", "TimeoutError")); - }, timeout); - timer?.unref(); - return { - close: () => { - if (timer !== undefined) clearTimeout(timer); - for (const [source, listener] of listeners) { - source.removeEventListener("abort", listener); - } - }, - signal: controller.signal, - timedOut: () => timeoutReached, - }; -} - -async function raceWithAbort( - work: () => Promise, - signal: AbortSignal | undefined, -): Promise { - if (signal === undefined) return await work(); - if (signal.aborted) throw signal.reason; - return await new Promise((resolve, reject) => { - let settled = false; - const finish = (callback: () => void): void => { - if (settled) return; - settled = true; - signal.removeEventListener("abort", onAbort); - callback(); - }; - const onAbort = (): void => finish(() => reject(signal.reason)); - signal.addEventListener("abort", onAbort, { once: true }); - if (signal.aborted) onAbort(); - const pending = Promise.resolve().then(async () => { - if (signal.aborted) throw signal.reason; - return await work(); - }); - void pending.then( - (value) => finish(() => resolve(value)), - (error: unknown) => finish(() => reject(error)), - ); - }); -} - -function frameContext(kind: "aad" | "key-context", values: readonly Uint8Array[]): Uint8Array { - const fields = [textEncoder.encode(CONTEXT_DOMAIN), textEncoder.encode(kind), ...values]; - let size = 0; - for (const field of fields) { - if (field.byteLength > 0xffff_ffff || size > Number.MAX_SAFE_INTEGER - 4 - field.byteLength) { - throw new TypeError("Protected workspace context is too large."); - } - size += 4 + field.byteLength; - } - const framed = new Uint8Array(size); - const view = new DataView(framed.buffer); - let offset = 0; - for (const field of fields) { - view.setUint32(offset, field.byteLength, false); - offset += 4; - framed.set(field, offset); - offset += field.byteLength; - } - return framed; -} - -function sanitizeError( - error: unknown, - operation: string, - path: string | undefined, - timedOut: boolean, - aborted: boolean, -): StorageError { - if (timedOut) { - return protectedError(StorageErrorCode.TIMEOUT, operation, path, { - aborted: true, - timedOut: true, - }); - } - if (aborted) { - return protectedError(StorageErrorCode.ABORTED, operation, path, { - aborted: true, - }); - } - if (isStorageError(error)) { - return protectedError(error.code, operation, path, { - aborted: error.aborted, - permanent: error.permanent, - timedOut: error.timedOut, - }); - } - if (isAbortLike(error)) { - return protectedError(StorageErrorCode.ABORTED, operation, path, { - aborted: true, - }); - } - return protectedError(StorageErrorCode.PROVIDER, operation, path, { - permanent: true, - }); -} - -function authenticationError(operation: string, path: string, error: unknown): StorageError { - if (isStorageError(error)) { - return protectedError(error.code, operation, path, { - aborted: error.aborted, - permanent: error.permanent, - timedOut: error.timedOut, - }); - } - if (isAbortLike(error)) { - return protectedError(StorageErrorCode.ABORTED, operation, path, { aborted: true }); - } - return protectedError(StorageErrorCode.PROVIDER, operation, path, { permanent: true }); -} - -function protectedError( - code: StorageErrorCodeValue, - operation: string, - path: string | undefined, - flags: { - readonly aborted?: boolean; - readonly permanent?: boolean; - readonly timedOut?: boolean; - } = {}, -): StorageError { - return new StorageError(`Protected workspace ${operation} failed.`, { - aborted: flags.aborted === true, - code, - ...(path === undefined ? {} : { key: path }), - operation, - permanent: flags.permanent === true, - timedOut: flags.timedOut === true, - }); -} - -function isAbortLike(error: unknown): boolean { - if (typeof error !== "object" || error === null) return false; - try { - return "name" in error && error.name === "AbortError"; - } catch { - return false; - } -} - -function assertOptionalSignal(signal: AbortSignal | undefined): void { - if (signal !== undefined && !isAbortSignal(signal)) { - throw new TypeError("Protected workspace lifetime signal is invalid."); - } -} - -function isAbortSignal(value: unknown): value is AbortSignal { - if (typeof value !== "object" || value === null) return false; - try { - return ( - typeof Object.getOwnPropertyDescriptor(AbortSignal.prototype, "aborted")?.get?.call(value) === - "boolean" - ); - } catch { - return false; - } -} - -function isStableBytes(value: unknown): value is Uint8Array { - try { - if (!(value instanceof Uint8Array) || !ArrayBuffer.isView(value)) return false; - const prototype: unknown = Object.getPrototypeOf(value); - if (prototype !== Uint8Array.prototype && prototype !== Buffer.prototype) return false; - return !(value.buffer instanceof SharedArrayBuffer) && Number.isSafeInteger(value.byteLength); - } catch { - return false; - } -} - -function positiveInteger(value: number, label: string): number { - if (!Number.isSafeInteger(value) || value <= 0) { - throw new TypeError(`${label} must be a positive safe integer.`); - } - return value; -} - -function boundedUtf8(value: string, maxBytes: number, label: string): Uint8Array { - if (typeof value !== "string" || value.length > maxBytes) { - throw new TypeError(`${label} exceeds its UTF-8 byte limit.`); - } - const encodedUpperBound = value.length * 3; - const detectionBound = maxBytes === Number.MAX_SAFE_INTEGER ? maxBytes : maxBytes + 1; - const target = new Uint8Array(Math.min(encodedUpperBound, detectionBound)); - const result = textEncoder.encodeInto(value, target); - if (result.read !== value.length || result.written > maxBytes) { - target.fill(0); - throw new TypeError(`${label} exceeds its UTF-8 byte limit.`); - } - const bytes = target.slice(0, result.written); - target.fill(0); - return bytes; -} - -function joinPath(directory: string, path: string): string { - return directory.length === 0 ? path : `${directory}/${path}`; -} - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} diff --git a/src/storage-workspace/types.ts b/src/storage-workspace/types.ts deleted file mode 100644 index 5948423..0000000 --- a/src/storage-workspace/types.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { StorageOperationOptions } from "@nestm/storage/core"; -import type { - StorageWorkspace, - StorageWorkspaceFile, - StorageWorkspaceLimits, - StorageWorkspaceMountOptions, - StorageWorkspaceReadOptions, -} from "@nestm/storage/workspace"; - -/** Inputs authenticated by a workspace cipher for one protected value. */ -export interface AuthenticatedWorkspaceCipherContext { - readonly authenticatedData: Uint8Array; - readonly keyContext: Uint8Array; - readonly signal?: AbortSignal; -} - -/** - * Minimal authenticated-encryption boundary required by a protected workspace. - * - * Implementations must return canonical `nmc1` envelopes and must authenticate - * both context byte strings during decryption. Inputs and context buffers are - * borrowed for one call: implementations must stay within application-owned - * bounds, honor the supplied signal, and retain no buffer after settlement. - */ -export interface AuthenticatedWorkspaceCipher { - encryptBytes( - plaintext: Uint8Array, - context: AuthenticatedWorkspaceCipherContext, - ): Promise; - decryptBytes(envelope: string, context: AuthenticatedWorkspaceCipherContext): Promise; -} - -export interface ProtectedStorageWorkspaceByteFile extends StorageWorkspaceFile { - readonly bytes: Uint8Array; -} - -export type ProtectedStorageWorkspacePathSearch = "disabled" | "provider-visible"; - -export interface ProtectedStorageWorkspaceProtection { - readonly kind: "authenticated-encryption"; - readonly outerFormat: "nestm-protected-storage-workspace"; - readonly outerVersion: 1; - readonly cipherEnvelope: "nmc1"; - readonly metadata: "encrypted"; - readonly body: "encrypted"; - readonly pathBinding: "scope-policy-path-record-version-purpose"; - readonly pathSearch: ProtectedStorageWorkspacePathSearch; - readonly policyRevision: string; -} - -export interface ProtectedStorageWorkspace extends StorageWorkspace { - readonly protection: Readonly; - readBytes( - path: string, - options?: StorageWorkspaceReadOptions, - ): Promise; - mount(directory: string, options?: StorageWorkspaceMountOptions): ProtectedStorageWorkspace; -} - -export interface ProtectStorageWorkspaceOptions { - readonly storage: StorageWorkspace; - readonly cipher: AuthenticatedWorkspaceCipher; - /** Opaque deployment/tenant/workspace scope. It is copied and never exposed. */ - readonly scopeContext: string | Uint8Array; - /** Application-owned policy/key-domain revision authenticated with every value. */ - readonly policyRevision: string; - /** - * Logical limits exposed by the protected view. At initial composition, - * non-buffer limits must equal the backing workspace; maxReadBytes and - * maxWriteBytes may be lower. Derived mounts can narrow every limit. - */ - readonly limits: Readonly; - /** - * Physical bound for the complete encrypted outer object. It must cover the - * larger logical read/write ceiling and fit the backing read/write limits. - */ - readonly maxCiphertextBytes: number; - /** Registry-owned lifetime cancellation inherited by every derived mount. */ - readonly signal?: AbortSignal; - /** Explicitly allows cleartext path/query search at the storage provider. */ - readonly pathSearch?: ProtectedStorageWorkspacePathSearch; -} - -export interface CipherEngineWorkspaceCipherOptions { - /** Provider selected for new envelopes. Defaults to the engine provider. */ - readonly provider?: string; - /** Cipher selected for new envelopes. Defaults to the engine cipher. */ - readonly cipher?: string; - /** Providers admitted while reading, for explicit key-rotation windows. */ - readonly allowedProviders?: readonly string[]; -} - -export type ProtectedStorageOperationOptions = StorageOperationOptions; diff --git a/tsdown.config.ts b/tsdown.config.ts index 0f9d13f..37082f9 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -5,7 +5,6 @@ export default defineConfig({ "src/index.ts", "src/core/index.ts", "src/files/index.ts", - "src/storage-workspace/index.ts", "src/fields/index.ts", "src/tenant/index.ts", "src/http/index.ts", @@ -28,7 +27,6 @@ export default defineConfig({ deps: { neverBundle: [ /^@nestjs\//, - /^@nestm\/storage(?:\/|$)/, /^@nestm\/tenant(?:\/|$)/, /^@aws-sdk\/client-kms(?:\/|$)/, /^@google-cloud\/kms(?:\/|$)/,