From 3312a47a6ec2b5f553139dcfcc387801fa767bbd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 04:48:33 +0000 Subject: [PATCH 1/2] chore(deps-dev): bump the dev-dependencies group across 1 directory with 10 updates Bumps the dev-dependencies group with 10 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.5.3` | `2.5.9` | | [@changesets/cli](https://github.com/changesets/changesets/tree/HEAD/packages/cli) | `2.31.0` | `2.31.1` | | [turbo](https://github.com/vercel/turborepo) | `2.10.4` | `2.10.10` | | [@neon-rs/cli](https://github.com/neon-bindings/neon-rs) | `0.1.82` | `0.2.6` | | [@tsconfig/node22](https://github.com/tsconfig/bases/tree/HEAD/bases) | `22.0.5` | `22.0.6` | | [@clerk/backend](https://github.com/clerk/javascript/tree/HEAD/packages/backend) | `3.11.3` | `3.16.7` | | [@supabase/postgrest-js](https://github.com/supabase/supabase-js/tree/HEAD/packages/core/postgrest-js) | `2.110.2` | `2.112.3` | | [@supabase/supabase-js](https://github.com/supabase/supabase-js/tree/HEAD/packages/core/supabase-js) | `2.110.2` | `2.112.3` | | [fta-cli](https://github.com/sgb-io/fta) | `3.0.0` | `3.0.1` | | [@types/semver](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/semver) | `7.7.1` | `7.8.0` | Updates `@biomejs/biome` from 2.5.3 to 2.5.9 - [Release notes](https://github.com/biomejs/biome/releases) - [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md) - [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.5.9/packages/@biomejs/biome) Updates `@changesets/cli` from 2.31.0 to 2.31.1 - [Release notes](https://github.com/changesets/changesets/releases) - [Changelog](https://github.com/changesets/changesets/blob/@changesets/cli@2.31.1/packages/cli/CHANGELOG.md) - [Commits](https://github.com/changesets/changesets/commits/@changesets/cli@2.31.1/packages/cli) Updates `turbo` from 2.10.4 to 2.10.10 - [Release notes](https://github.com/vercel/turborepo/releases) - [Changelog](https://github.com/vercel/turborepo/blob/main/RELEASE.md) - [Commits](https://github.com/vercel/turborepo/compare/v2.10.4...v2.10.10) Updates `@neon-rs/cli` from 0.1.82 to 0.2.6 - [Release notes](https://github.com/neon-bindings/neon-rs/releases) - [Commits](https://github.com/neon-bindings/neon-rs/compare/v0.1.82...v0.2.6) Updates `@tsconfig/node22` from 22.0.5 to 22.0.6 - [Commits](https://github.com/tsconfig/bases/commits/HEAD/bases) Updates `@clerk/backend` from 3.11.3 to 3.16.7 - [Release notes](https://github.com/clerk/javascript/releases) - [Changelog](https://github.com/clerk/javascript/blob/main/packages/backend/CHANGELOG.md) - [Commits](https://github.com/clerk/javascript/commits/@clerk/backend@3.16.7/packages/backend) Updates `@supabase/postgrest-js` from 2.110.2 to 2.112.3 - [Release notes](https://github.com/supabase/supabase-js/releases) - [Changelog](https://github.com/supabase/supabase-js/blob/master/packages/core/postgrest-js/CHANGELOG.md) - [Commits](https://github.com/supabase/supabase-js/commits/v2.112.3/packages/core/postgrest-js) Updates `@supabase/supabase-js` from 2.110.2 to 2.112.3 - [Release notes](https://github.com/supabase/supabase-js/releases) - [Changelog](https://github.com/supabase/supabase-js/blob/master/packages/core/supabase-js/CHANGELOG.md) - [Commits](https://github.com/supabase/supabase-js/commits/v2.112.3/packages/core/supabase-js) Updates `fta-cli` from 3.0.0 to 3.0.1 - [Release notes](https://github.com/sgb-io/fta/releases) - [Changelog](https://github.com/sgb-io/fta/blob/main/CHANGELOG.md) - [Commits](https://github.com/sgb-io/fta/compare/v3.0.0...v3.0.1) Updates `@types/semver` from 7.7.1 to 7.8.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/semver) --- updated-dependencies: - dependency-name: "@biomejs/biome" dependency-version: 2.5.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: "@changesets/cli" dependency-version: 2.31.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: "@clerk/backend" dependency-version: 3.16.6 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: "@neon-rs/cli" dependency-version: 0.2.6 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: "@supabase/postgrest-js" dependency-version: 2.112.3 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: "@supabase/supabase-js" dependency-version: 2.112.3 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: "@tsconfig/node22" dependency-version: 22.0.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: "@types/semver" dependency-version: 7.8.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: fta-cli dependency-version: 3.0.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: turbo dependency-version: 2.10.10 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] --- e2e/package.json | 2 +- package.json | 6 +- packages/protect-ffi/package.json | 6 +- packages/stack-drizzle/package.json | 2 +- packages/stack-supabase/package.json | 6 +- packages/stack/package.json | 8 +- packages/test-kit/package.json | 2 +- pnpm-lock.yaml | 459 ++++++++++----------------- 8 files changed, 182 insertions(+), 309 deletions(-) diff --git a/e2e/package.json b/e2e/package.json index bf5f0577c..03d3bf485 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -14,7 +14,7 @@ "@cipherstash/wizard": "workspace:*" }, "devDependencies": { - "@types/semver": "7.7.1", + "@types/semver": "7.8.0", "semver": "^7.8.0", "typescript": "catalog:repo", "vitest": "catalog:repo", diff --git a/package.json b/package.json index 87769b6c8..18f1d96bc 100644 --- a/package.json +++ b/package.json @@ -42,12 +42,12 @@ "test:scripts": "vitest run --config scripts/vitest.config.mjs" }, "devDependencies": { - "@biomejs/biome": "^2.5.3", - "@changesets/cli": "^2.31.0", + "@biomejs/biome": "^2.5.9", + "@changesets/cli": "^2.31.1", "@types/node": "^22.20.1", "js-yaml": "^4.3.1", "rimraf": "^6.1.3", - "turbo": "2.10.4", + "turbo": "2.10.10", "vitest": "catalog:repo" }, "packageManager": "pnpm@10.33.2", diff --git a/packages/protect-ffi/package.json b/packages/protect-ffi/package.json index 0df409c8c..0edef5ffa 100644 --- a/packages/protect-ffi/package.json +++ b/packages/protect-ffi/package.json @@ -91,9 +91,9 @@ "prefix": "protect-ffi-" }, "devDependencies": { - "@biomejs/biome": "^2.5.3", - "@neon-rs/cli": "^0.1.82", - "@tsconfig/node22": "^22.0.0", + "@biomejs/biome": "^2.5.9", + "@neon-rs/cli": "^0.2.6", + "@tsconfig/node22": "^22.0.6", "@types/node": "^22.20.1", "typescript": "catalog:repo", "vitest": "catalog:repo" diff --git a/packages/stack-drizzle/package.json b/packages/stack-drizzle/package.json index 3684ab1c1..738969ef2 100644 --- a/packages/stack-drizzle/package.json +++ b/packages/stack-drizzle/package.json @@ -65,7 +65,7 @@ "devDependencies": { "@cipherstash/protect-ffi": "workspace:*", "@cipherstash/test-kit": "workspace:*", - "fta-cli": "3.0.0", + "fta-cli": "3.0.1", "dotenv": "17.4.2", "drizzle-orm": "^0.45.2", "postgres": "^3.4.8", diff --git a/packages/stack-supabase/package.json b/packages/stack-supabase/package.json index 86411a45e..b6a304817 100644 --- a/packages/stack-supabase/package.json +++ b/packages/stack-supabase/package.json @@ -78,9 +78,9 @@ "devDependencies": { "@cipherstash/protect-ffi": "workspace:*", "@cipherstash/test-kit": "workspace:*", - "fta-cli": "3.0.0", - "@supabase/postgrest-js": "2.110.2", - "@supabase/supabase-js": "^2.110.2", + "fta-cli": "3.0.1", + "@supabase/postgrest-js": "2.112.3", + "@supabase/supabase-js": "^2.112.3", "@types/pg": "^8.23.1", "fast-check": "^4.9.0", "dotenv": "17.4.2", diff --git a/packages/stack/package.json b/packages/stack/package.json index c84d64096..0b8a954c7 100644 --- a/packages/stack/package.json +++ b/packages/stack/package.json @@ -205,16 +205,16 @@ "devDependencies": { "@cipherstash/eql": "workspace:^", "@clack/prompts": "^1.7.0", - "@clerk/backend": "3.11.3", - "@supabase/postgrest-js": "2.110.2", - "@supabase/supabase-js": "^2.110.2", + "@clerk/backend": "3.16.7", + "@supabase/postgrest-js": "2.112.3", + "@supabase/supabase-js": "^2.112.3", "@types/pg": "^8.23.1", "@types/uuid": "^11.0.0", "dotenv": "17.4.2", "drizzle-orm": "^0.45.2", "execa": "^9.5.2", "fast-check": "^4.9.0", - "fta-cli": "3.0.0", + "fta-cli": "3.0.1", "json-schema-to-typescript": "^15.0.2", "pg": "8.23.0", "postgres": "^3.4.8", diff --git a/packages/test-kit/package.json b/packages/test-kit/package.json index 60b273a40..7d2fb8ac0 100644 --- a/packages/test-kit/package.json +++ b/packages/test-kit/package.json @@ -19,7 +19,7 @@ "@cipherstash/stack": "workspace:*" }, "devDependencies": { - "@clerk/backend": "3.11.3", + "@clerk/backend": "3.16.7", "typescript": "catalog:repo", "vitest": "catalog:repo" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 095ae7e59..a3ea84908 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,11 +68,11 @@ importers: .: devDependencies: '@biomejs/biome': - specifier: ^2.5.3 - version: 2.5.3 + specifier: ^2.5.9 + version: 2.5.9 '@changesets/cli': - specifier: ^2.31.0 - version: 2.31.0(@types/node@22.20.1) + specifier: ^2.31.1 + version: 2.31.1(@types/node@22.20.1) '@types/node': specifier: ^22.20.1 version: 22.20.1 @@ -83,8 +83,8 @@ importers: specifier: ^6.1.3 version: 6.1.3 turbo: - specifier: 2.10.4 - version: 2.10.4 + specifier: 2.10.10 + version: 2.10.10 vitest: specifier: catalog:repo version: 3.2.7(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0) @@ -102,8 +102,8 @@ importers: version: link:../packages/cli devDependencies: '@types/semver': - specifier: 7.7.1 - version: 7.7.1 + specifier: 7.8.0 + version: 7.8.0 semver: specifier: ^7.8.0 version: 7.8.5 @@ -368,14 +368,14 @@ importers: version: 0.2.6 devDependencies: '@biomejs/biome': - specifier: ^2.5.3 - version: 2.5.3 + specifier: ^2.5.9 + version: 2.5.9 '@neon-rs/cli': - specifier: ^0.1.82 - version: 0.1.82 + specifier: ^0.2.6 + version: 0.2.6 '@tsconfig/node22': - specifier: ^22.0.0 - version: 22.0.5 + specifier: ^22.0.6 + version: 22.0.6 '@types/node': specifier: ^22.20.1 version: 22.20.1 @@ -445,14 +445,14 @@ importers: specifier: ^1.7.0 version: 1.7.0 '@clerk/backend': - specifier: 3.11.3 - version: 3.11.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: 3.16.7 + version: 3.16.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@supabase/postgrest-js': - specifier: 2.110.2 - version: 2.110.2 + specifier: 2.112.3 + version: 2.112.3 '@supabase/supabase-js': - specifier: ^2.110.2 - version: 2.110.2 + specifier: ^2.112.3 + version: 2.112.3 '@types/pg': specifier: ^8.23.1 version: 8.23.1 @@ -472,8 +472,8 @@ importers: specifier: ^4.9.0 version: 4.9.0 fta-cli: - specifier: 3.0.0 - version: 3.0.0 + specifier: 3.0.1 + version: 3.0.1 json-schema-to-typescript: specifier: ^15.0.2 version: 15.0.4 @@ -537,8 +537,8 @@ importers: specifier: ^0.45.2 version: 0.45.2(@types/pg@8.23.1)(gel@2.2.0)(mysql2@3.16.0)(pg@8.23.0)(postgres@3.4.9) fta-cli: - specifier: 3.0.0 - version: 3.0.0 + specifier: 3.0.1 + version: 3.0.1 postgres: specifier: ^3.4.8 version: 3.4.9 @@ -614,11 +614,11 @@ importers: specifier: workspace:* version: link:../test-kit '@supabase/postgrest-js': - specifier: 2.110.2 - version: 2.110.2 + specifier: 2.112.3 + version: 2.112.3 '@supabase/supabase-js': - specifier: ^2.110.2 - version: 2.110.2 + specifier: ^2.112.3 + version: 2.112.3 '@types/pg': specifier: ^8.23.1 version: 8.23.1 @@ -629,8 +629,8 @@ importers: specifier: ^4.9.0 version: 4.9.0 fta-cli: - specifier: 3.0.0 - version: 3.0.0 + specifier: 3.0.1 + version: 3.0.1 pg: specifier: 8.23.0 version: 8.23.0 @@ -654,8 +654,8 @@ importers: version: link:../stack devDependencies: '@clerk/backend': - specifier: 3.11.3 - version: 3.11.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: 3.16.7 + version: 3.16.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) typescript: specifier: catalog:repo version: 5.9.3 @@ -801,67 +801,63 @@ packages: '@ark/util@0.56.2': resolution: {integrity: sha512-9kU2sUE38FZEGG7l3hamYMBieLYEJh2L1mrYD2eXpT+78EnQSV1bhjxJhnxGBMSTbtwpBSDNSK+K60WvaI/DTQ==} - '@babel/runtime@7.29.2': - resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.29.7': resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} - '@biomejs/biome@2.5.3': - resolution: {integrity: sha512-MrJswFdei9EfDwwUy2tQrPDpK0AO+RmMFvBoaaJ6ayBc3sUbHdCE+XG5N8vp+5So41ZupZJQm0roHFFhMGVD7A==} + '@biomejs/biome@2.5.9': + resolution: {integrity: sha512-KkgCvdHB4IhtpHpF564plA9jo6fDOwWGQ/3jvreLzgOtRLEDoPqr7QO9qejNA8jKwDsSkAKr77hqBHnyUbIw4g==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.5.3': - resolution: {integrity: sha512-QhYP9muVQ0nUO5zztFuPbEwi4+94sJWVjaZds9aMi1l/KNZBiUjdiSUrGHsTaMGDXrYl+r4AS2sUKfgH3w+V3g==} + '@biomejs/cli-darwin-arm64@2.5.9': + resolution: {integrity: sha512-am22pX2aBqznqq1eMyIj/bZ++riF3Lk6ct7cbv+gQK0csFhr+d8O0RkOi2FF2qSgFgANbqNkIZ0/PxlnW2pLFg==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.5.3': - resolution: {integrity: sha512-NC1Ss13UaW7QZX+y8j44bF7AP0jSJdBl6iRhe0MAkvaSqZy+mWg3GaXsrb+eSoHoGDBtaXWEbMVV0iVN2cZ7cQ==} + '@biomejs/cli-darwin-x64@2.5.9': + resolution: {integrity: sha512-l44KWDHLDvEnD0N/XcrVs7VXb3A18xL7QS3WB0eL93wbmk529ffIG55vleGCqaunpRUjLrdnjK05Qki1dsjylg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.5.3': - resolution: {integrity: sha512-fccix0w6xp6csCXgxeC0dU/3ecgRQal0y+cv2SP9ajNlhe7Yrk2Ug7UDe2j9AT9ZDYitkXpvUKgZjjuoYeP4Vg==} + '@biomejs/cli-linux-arm64-musl@2.5.9': + resolution: {integrity: sha512-7ImVPwBLCtkmpR5esd8RHhTqW94f0JLJQum6AneYcy94jRm18TaPPm7slaigGzFhfgt3QiD1Vj52LKmBAnKizA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [musl] - '@biomejs/cli-linux-arm64@2.5.3': - resolution: {integrity: sha512-ksx1KWeyYW18ILL04msF/J4ZBtBDN33znYK8Z/aNv/vlBVxL9/g3mGP+omgHJKy4+KWbK87vcmmpmurfNjSgiA==} + '@biomejs/cli-linux-arm64@2.5.9': + resolution: {integrity: sha512-ICaK+IYaVZvKbBxX2rwrPT0DdUDMnE9Vm3nQGe+mltQPmUg19pONzkPWGdY4FCsoreDETWDynvdt4ysCbF5gNQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [glibc] - '@biomejs/cli-linux-x64-musl@2.5.3': - resolution: {integrity: sha512-O/yU9YKRUiHhmcjF2f38PSjseVk3G4VLWYc0G2HWpzdBVREV6G8IGWIVEFf7MFPfWIzNUIvPsEjeAZQIOgnLcQ==} + '@biomejs/cli-linux-x64-musl@2.5.9': + resolution: {integrity: sha512-RXGaD0o1/pTTguYw1aeDJh9ad6Lfrui0fI7mBderTyGr7WuUJkBIttgLkR3XJyoxOkkgfBDspaUT8wXArTqLZw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [musl] - '@biomejs/cli-linux-x64@2.5.3': - resolution: {integrity: sha512-yMkJtilsgvILDcVkh187aVLTb64xYsrxYajx5kym+r1ULkO5HUOfu9AYKLGQbOVLwJtT2utNw7hhFNg+17mUYA==} + '@biomejs/cli-linux-x64@2.5.9': + resolution: {integrity: sha512-z22Q/zFYSvbIJfW1CbfZPu4X8PddS6Qd2ORbc6h+aT6EcwAxUF3m6fA4HjNvA3TU4X0dTJRwNPB165ES3PJXzg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [glibc] - '@biomejs/cli-win32-arm64@2.5.3': - resolution: {integrity: sha512-cX5z+GYwRcqEok0AH3KSfQGgqYd0Nomfp6Fbe1uiTtELE38hdH2k842wQ9wLNaF/JJ7r4rjJQ4VR+ce+fRmQbw==} + '@biomejs/cli-win32-arm64@2.5.9': + resolution: {integrity: sha512-nHK+/HHC+D0ogAHUxomgoSTdjImb6fmNNVTKmf0tyu4eDL1DqPKIHc+i+UL8+b0RnAu8224qo8F2tCVnaT0A3w==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.5.3': - resolution: {integrity: sha512-ExSaJWi4/u6+GXCszlSKpWSjKNbDseAYqqkCznsCsZ/4uidZ/BEqsCc5/3ctlq6dfIubdIIRSVLC/PG9xPl70Q==} + '@biomejs/cli-win32-x64@2.5.9': + resolution: {integrity: sha512-Yiq0H56LjXSSw/hd9YkXgSLQfzyDJzbzU2TezozxyNw+uKWAqOtqGVvBfzKRRDiaFF5avGAhHdWKx7LtDOShUw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -872,56 +868,6 @@ packages: '@byteslice/result@0.3.0': resolution: {integrity: sha512-nIVjzVJ5LEUsj5jZ196OQ+XFYDPw74JVmHrsKs0g/iX1c8llydLWKbseMKmAtcEnpRj8hdsEue5H/naz7wdC4A==} - '@cargo-messages/android-arm-eabi@0.1.81': - resolution: {integrity: sha512-cpRbgb56e9LmAj96Tixtz9/bTlaJAeplWNNv4obu+eqQyZd3ZjX04TJd9fM1bjHDGVyR9GTVlgBsbQEntIGkwg==} - cpu: [arm] - os: [android] - - '@cargo-messages/darwin-arm64@0.1.81': - resolution: {integrity: sha512-OwGqsw+tbJx37a/vH4T8R9qkrrFYoTIOnckbA9+MhQodE2FSWyk3HvLh+z8jjl+QZa1RSOU9Ax6gt/46h0BiTg==} - cpu: [arm64] - os: [darwin] - - '@cargo-messages/darwin-x64@0.1.81': - resolution: {integrity: sha512-Iu761bPk25Ce6yUdDCjjeVuT8/xbBmczyaNB7oYBmAZEE5rshvCJ42TqSShYYP+S7pKkN42nBhkFooaZrqaz9g==} - cpu: [x64] - os: [darwin] - - '@cargo-messages/linux-arm-gnueabihf@0.1.81': - resolution: {integrity: sha512-iIuy7KTJAEhbiqlIlcxQOdW6opI6M9LXlgd/jdsHbP2FjmTyhTLnd3JCJ6JeAeidwknCDs+CFlaVmPxTKSytsg==} - cpu: [arm] - os: [linux] - - '@cargo-messages/linux-arm64-gnu@0.1.81': - resolution: {integrity: sha512-QPQRsHj9m/9ga8wRBlLh8t2AXyr40+/H55FIKVj7zIjV++waY/TbSTPofDZQhMycd5VSGLKztfhahiCO7c/RAQ==} - cpu: [arm64] - os: [linux] - - '@cargo-messages/linux-arm64-musl@0.1.81': - resolution: {integrity: sha512-9O0ATesIOjDTz2L01OtlGHYwP86I31/mzXMC+ryQkzbbIKj7KUiSqmEc29I14I517UYO8/sMeow6q6MVBpehlA==} - cpu: [arm64] - os: [linux] - - '@cargo-messages/linux-x64-gnu@0.1.81': - resolution: {integrity: sha512-wEFYxCdtHNiEvp5KEg17CfRCUdfRTtkL+1GASROyvYEUV6DQf6TXxfHuuWg2xlVxh5fqiTGFSRfiqFrCDL/xrw==} - cpu: [x64] - os: [linux] - - '@cargo-messages/linux-x64-musl@0.1.81': - resolution: {integrity: sha512-zi5pKIh60oPwOCDQapAZ3Mya4y56MI2BoGvY8JtztYaXTorGmAoyf6jjb50Wt+HfoYYjM3OlPt03XMlCFZJnIQ==} - cpu: [x64] - os: [linux] - - '@cargo-messages/win32-arm64-msvc@0.1.81': - resolution: {integrity: sha512-oyiT8AYLoguF7cFOMYDsPv3eirzBcFafOOfRsFyd3+wmaPTl/DdbCq446oThRmSAsEGJpzhzj7TafcnXMBkHbg==} - cpu: [arm64] - os: [win32] - - '@cargo-messages/win32-x64-msvc@0.1.81': - resolution: {integrity: sha512-B5Ukf4AohtIv27uCP/AgM+7vYwQ4RacI6m8ZBr2XKeSrjZXcXguzlZd+wD7bD5+wa0capvXKUskZDnpG/DcYiA==} - cpu: [x64] - os: [win32] - '@changesets/apply-release-plan@7.1.1': resolution: {integrity: sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==} @@ -931,8 +877,8 @@ packages: '@changesets/changelog-git@0.2.1': resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} - '@changesets/cli@2.31.0': - resolution: {integrity: sha512-AhI4enNTgHu2IZr6K4WZyf0EPch4XVMn1yOMFmCD9gsfBGqMYaHXls5HyDv6/CL5axVQABz68eG30eCtbr2wFg==} + '@changesets/cli@2.31.1': + resolution: {integrity: sha512-uO05WTcRBwuVOJVSW8Cmpqw6q0WDL53ajGCMyszutvOe5toOnunbpM4jZzf+qxBOz7i0AzopZ8diBuewjmF40w==} hasBin: true '@changesets/config@3.1.4': @@ -1041,10 +987,6 @@ packages: resolution: {integrity: sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==} engines: {node: '>= 20.12.0'} - '@clerk/backend@3.11.3': - resolution: {integrity: sha512-takrEE0nliUz39e8Ct1YG471eBcuU2I1ZStRIU9U0UAIuiNAxNTNuaVZSXaZeK4QTuJtdQhoGy8rvzxr/10Btw==} - engines: {node: '>=20.9.0'} - '@clerk/backend@3.16.7': resolution: {integrity: sha512-iPy+vXxDDgBdma6EBQ43ymGxrHKSI2ZwUgY2orF5cWvcG5h61hTZJYapO9TsHp39PbMcgiC/S5U785jq+S3y6Q==} engines: {node: '>=20.9.0'} @@ -1064,18 +1006,6 @@ packages: react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 react-dom: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 - '@clerk/shared@4.25.2': - resolution: {integrity: sha512-v7zgDh0eVLl3KzRHbKYTsPQ+4Xcx8A2oiU5iZ9WhME024WudwkjjPJRK12RycY0VA1Sj8i4Pi5zEpecqB/gp1Q==} - engines: {node: '>=20.9.0'} - peerDependencies: - react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 - react-dom: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - '@clerk/shared@4.29.2': resolution: {integrity: sha512-9c9Mc1oqumsqo+JY5R37O1ipwcG3RmwPK9oadBLL9E4VxZXthXVaFI9u+1/I4BSFGA/B9BO+17tkVDL4G0Wpbg==} engines: {node: '>=20.9.0'} @@ -1459,8 +1389,8 @@ packages: '@cfworker/json-schema': optional: true - '@neon-rs/cli@0.1.82': - resolution: {integrity: sha512-QrlGPQp9KOGuMvjjua79lEV2QTcE16m8JatG5ITdQpBAwRQpDw5xab57W9130y2iUEfMzYtp7v6pcN1fUB0Exg==} + '@neon-rs/cli@0.2.6': + resolution: {integrity: sha512-SC8xYNOH1dTnDiB2pykFhREl74gs6TF4jr2qFp0h9fwriRSoxic/CnVN2d7ikpc3XatOK1QnwfQCCmyv31x9/A==} hasBin: true '@neon-rs/load@0.2.6': @@ -1748,69 +1678,74 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@supabase/auth-js@2.110.2': - resolution: {integrity: sha512-Qj7a6EDP+AMMQFWqGv+qFa8r6re//dk+qQI5bA0KK+PZmnI3JPu97TDeNt6SMiQ2FkklP79hP2yDFYSnA989OA==} + '@supabase/auth-js@2.112.3': + resolution: {integrity: sha512-NA0rsgAlWZPvbhw8aUdmgfpHVgUAcd8zK5ov43l++o1bLIPXZhRiAlRobhwF5AatQuovpqxsMH50F4oyyV4XZw==} engines: {node: '>=22.0.0'} - '@supabase/functions-js@2.110.2': - resolution: {integrity: sha512-ZjjqrXpxM9/rE+eAtZxiK45EWy9EBoJQ322Q5Y75LccYQNh212neHTgXP/o4MIzmH0LNXT8UzvTZtQOfOzyoeQ==} + '@supabase/functions-js@2.112.3': + resolution: {integrity: sha512-gfv481mTOVWtZIJgXupxZpni2V2UWPf6jeF/jOK7HdMHdH+mt6sU0sHHwf0POsPip8ltlulu9OUHgwVzl5ddRw==} engines: {node: '>=22.0.0'} - '@supabase/phoenix@0.4.4': - resolution: {integrity: sha512-Gt0pqoXuIqX/8dvG0OKp/wMCobXNH3klNbUPBNyOfN0YA1IswrM3HyWFMOPk1Jy+BRaIyDPcFx4jLBwHNmlyfQ==} + '@supabase/phoenix@0.4.5': + resolution: {integrity: sha512-aAn9H9ovVyeApKy11OWOrrOGq8DV68yWeH4ud2lN9fzn4aO8Zb5GLL9m1pUg9nLqIcT+ZDfAcsZe0E/nqdv2lw==} - '@supabase/postgrest-js@2.110.2': - resolution: {integrity: sha512-++LBmcIMwCtgO4tISQUmo9+2xkRwHQqS8ZKMCnhXLe9P8k8YQRXuMoh/RiSzQSoev8gqet0W7yOboW0cUxnt0Q==} + '@supabase/postgrest-js@2.112.3': + resolution: {integrity: sha512-+Mf6uCpzr00bqxwX8hTK2X2L9eAL/1vuOjdEjx6upz9ulb0RmQT16XeU/JkMUlVHw/B46ZnPa2busY4Kd9YCzw==} engines: {node: '>=22.0.0'} - '@supabase/realtime-js@2.110.2': - resolution: {integrity: sha512-z3jTOTPgyn6E3r6dVOOQ10He4yAMB2czjFw7xVdX3s16MHElna5rY1gVaePs0NIo6xvtMYbtmOXlFaFt/ePLpg==} + '@supabase/realtime-js@2.112.3': + resolution: {integrity: sha512-E6wljXWs7DUOloyIB69i3YFInWE6IyCvgTAbQ0KYxOHv26FdA1KzEXTuzxrYEdf70t406Z9BRwUlGyclGF2FXA==} engines: {node: '>=22.0.0'} - '@supabase/storage-js@2.110.2': - resolution: {integrity: sha512-EhsRSwSnmQefKJsAxoRUZ0hvHr92ECM8DDGAKR5z0HdoJx4heI60PjHUTruVNZxKX6XeobLGDyLud020Bw1iwg==} + '@supabase/storage-js@2.112.3': + resolution: {integrity: sha512-oSK61tzlUvg+BWPqpKQCu9qqonsO26btaoAR9D6Gest2aj7xUqToj9rKyaoYOJczkhg9BjqA1REbYy9tPI4bDA==} engines: {node: '>=22.0.0'} - '@supabase/supabase-js@2.110.2': - resolution: {integrity: sha512-r9q9w4ZQ6mOjh36aqUNFSisBF611vzpO8JphBESr2Q1SWvmGFQeI7Jq7Y+PaNMZ6Zszz+S2yTlJStCpnaMSnQg==} + '@supabase/supabase-js@2.112.3': + resolution: {integrity: sha512-Jv1bxVQmEJNkjvPEhFaKjPzsh+Ozyew6lWGD+SoYcsclDEP1z7yEvKvfUQfzy0DkxRIQnZNxmmWtAzw5XLTQoA==} engines: {node: '>=22.0.0'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0' + peerDependenciesMeta: + '@opentelemetry/api': + optional: true '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - '@tanstack/query-core@5.101.2': - resolution: {integrity: sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==} + '@tanstack/query-core@5.101.4': + resolution: {integrity: sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==} - '@tsconfig/node22@22.0.5': - resolution: {integrity: sha512-hLf2ld+sYN/BtOJjHUWOk568dvjFQkHnLNa6zce25GIH+vxKfvTgm3qpaH6ToF5tu/NN0IH66s+Bb5wElHrLcw==} + '@tsconfig/node22@22.0.6': + resolution: {integrity: sha512-/5thavHAnWDZwH2H6eEn/T8pBEBhtuKaWGMslsj82kJfvzQgOgEMK7X4goBbIUjgVtAzPTtM9Ml64LA0uxO6Iw==} - '@turbo/darwin-64@2.10.4': - resolution: {integrity: sha512-m1MUEI4MJ69r5CwfMYxmHi0H0rrgiYCBOp0tgBZ9x/YVvOb5uu/lRIDyDwdtH054R2yWeQaIigUGu6aCX9f8cA==} + '@turbo/darwin-64@2.10.10': + resolution: {integrity: sha512-gFDD+wRP5hWxBRghGyEbjpbLOY7aIU/wvsnKdMM7odQcp/wHMrnI83p0FyxxMRZnFH9ZD+S59MvcpOC5b+nrCA==} cpu: [x64] os: [darwin] - '@turbo/darwin-arm64@2.10.4': - resolution: {integrity: sha512-VQ1Yxs5zkPT+2z7t1P4mvn6JmcKLkOCAsPuK9XbOvuVj0DlTlETfIXNisX0771v/vTWHOQqiwoGi+TtAUq8efw==} + '@turbo/darwin-arm64@2.10.10': + resolution: {integrity: sha512-VZYsxZ6yjyDosUqtiroAVSXPLmx/qBxdHJgIxdMH9RyNmLdOLOWtJnYMnI4qckwCgQMK85G3fu94/xk5+iBCgw==} cpu: [arm64] os: [darwin] - '@turbo/linux-64@2.10.4': - resolution: {integrity: sha512-IzV1QovmwX7mfGnVinmE++2IB8tbeo38weltiuH5zNqwCTBjLs/DytyRKx+bmnhHdXIq9SheR8p0Nip/LBUPHg==} + '@turbo/linux-64@2.10.10': + resolution: {integrity: sha512-lAvW+yEnmsCKMEIwNugjozawvYytHKPhU0kfLBizu83MIs8OUb9KobYvkZ56L5akSM6K7+gBFLEIfQkaceh90g==} cpu: [x64] - os: [linux] + os: [android, linux] - '@turbo/linux-arm64@2.10.4': - resolution: {integrity: sha512-rfujSQkP5aYiRn0PgTM7F00WkJCP/bKDVZbOx3WmrZwa/vHA0bplhCl328kpX7VI9HH2vI90ISGwuSVgJgoqTw==} + '@turbo/linux-arm64@2.10.10': + resolution: {integrity: sha512-MSJ+NkRTd79Z9+YEZpUV9VOWVOOigFhE+v/ETNYJEuTJp3r00y9YgFvDXrmM+DP8Kal6tk3U6xSugD2/Ojh+Jg==} cpu: [arm64] - os: [linux] + os: [android, linux] - '@turbo/windows-64@2.10.4': - resolution: {integrity: sha512-NnspP7Wd5fa3Wwnqv9bKfhegqZzuHBgbPxdZU/idTLQcazx/vgKu95JlCx2YHY0hdvKCnPcARrDwM+KEUmaO7A==} + '@turbo/windows-64@2.10.10': + resolution: {integrity: sha512-ycWpXDkUfnDFDY9d+4Qna/UZotDB0wj+s9agrlmNt0Q7a3XHORhK8GPKJdzgzeutXu9EW5P/jyabTEHlohuDXw==} cpu: [x64] os: [win32] - '@turbo/windows-arm64@2.10.4': - resolution: {integrity: sha512-Iv02YgOpaEShc2OkG7mgCJ2pEw1RUKiKbs0h8W5wAf4jZ5vpmraTEjuGTgHRuOORQnC1GN3KHo5WB+hu1abRMA==} + '@turbo/windows-arm64@2.10.10': + resolution: {integrity: sha512-PMk6zQN0csUFklLe+1hz/5G9uU1YmV0cEIey2R/bSeA6o69qcBTlN4A3jOqkgenOO5dOpMHmq2sEUZo8r1+Ssg==} cpu: [arm64] os: [win32] @@ -1844,8 +1779,8 @@ packages: '@types/pg@8.23.1': resolution: {integrity: sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==} - '@types/semver@7.7.1': - resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + '@types/semver@7.8.0': + resolution: {integrity: sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ==} '@types/uuid@11.0.0': resolution: {integrity: sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA==} @@ -2007,8 +1942,8 @@ packages: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} - chardet@2.1.1: - resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + chardet@2.2.0: + resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} check-error@2.1.3: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} @@ -2418,8 +2353,8 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - fta-cli@3.0.0: - resolution: {integrity: sha512-SBmoqIwbN7PLDmwmrPgjr6Z6/S9jPhNz5TCPmEVFkIaeloc/T2WXLeeXqhG1+C0UQxpOfGrC7CUb4friqbc2kQ==} + fta-cli@3.0.1: + resolution: {integrity: sha512-Nm03+vurWkzWj/SEiOof+8zDAbp/0+gDttW5+gIVe4Qxg4uOTlQm+IbHyXM+QptCfywj0/h4U37NsnybaG9z6Q==} hasBin: true function-bind@1.1.2: @@ -2491,8 +2426,8 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} - human-id@4.1.3: - resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} + human-id@4.2.0: + resolution: {integrity: sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA==} hasBin: true human-signals@8.0.1: @@ -2503,10 +2438,6 @@ packages: resolution: {integrity: sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==} engines: {node: '>=20.0.0'} - iconv-lite@0.7.2: - resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} - engines: {node: '>=0.10.0'} - iconv-lite@0.7.3: resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} engines: {node: '>=0.10.0'} @@ -3397,8 +3328,8 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - turbo@2.10.4: - resolution: {integrity: sha512-GQpduILaKjoaGljw097ScsSyKTtZSY7cZ3bJktzfTkPMyCf3ShKLuXK2IaOEN2Plziml+ArR7WJ1m+V4VbnaKQ==} + turbo@2.10.10: + resolution: {integrity: sha512-/90KTW+USzvYOPmafRZHVKLBsHXQ5810Ao/HdtJYAqguIhZ+XruS6eIUjqJUDtrSxaZYynNFht68qckGKAOWTA==} hasBin: true typanion@3.14.0: @@ -3639,79 +3570,47 @@ snapshots: '@ark/util@0.56.2': {} - '@babel/runtime@7.29.2': {} - '@babel/runtime@7.29.7': {} - '@biomejs/biome@2.5.3': + '@biomejs/biome@2.5.9': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.5.3 - '@biomejs/cli-darwin-x64': 2.5.3 - '@biomejs/cli-linux-arm64': 2.5.3 - '@biomejs/cli-linux-arm64-musl': 2.5.3 - '@biomejs/cli-linux-x64': 2.5.3 - '@biomejs/cli-linux-x64-musl': 2.5.3 - '@biomejs/cli-win32-arm64': 2.5.3 - '@biomejs/cli-win32-x64': 2.5.3 + '@biomejs/cli-darwin-arm64': 2.5.9 + '@biomejs/cli-darwin-x64': 2.5.9 + '@biomejs/cli-linux-arm64': 2.5.9 + '@biomejs/cli-linux-arm64-musl': 2.5.9 + '@biomejs/cli-linux-x64': 2.5.9 + '@biomejs/cli-linux-x64-musl': 2.5.9 + '@biomejs/cli-win32-arm64': 2.5.9 + '@biomejs/cli-win32-x64': 2.5.9 - '@biomejs/cli-darwin-arm64@2.5.3': + '@biomejs/cli-darwin-arm64@2.5.9': optional: true - '@biomejs/cli-darwin-x64@2.5.3': + '@biomejs/cli-darwin-x64@2.5.9': optional: true - '@biomejs/cli-linux-arm64-musl@2.5.3': + '@biomejs/cli-linux-arm64-musl@2.5.9': optional: true - '@biomejs/cli-linux-arm64@2.5.3': + '@biomejs/cli-linux-arm64@2.5.9': optional: true - '@biomejs/cli-linux-x64-musl@2.5.3': + '@biomejs/cli-linux-x64-musl@2.5.9': optional: true - '@biomejs/cli-linux-x64@2.5.3': + '@biomejs/cli-linux-x64@2.5.9': optional: true - '@biomejs/cli-win32-arm64@2.5.3': + '@biomejs/cli-win32-arm64@2.5.9': optional: true - '@biomejs/cli-win32-x64@2.5.3': + '@biomejs/cli-win32-x64@2.5.9': optional: true '@byteslice/result@0.2.0': {} '@byteslice/result@0.3.0': {} - '@cargo-messages/android-arm-eabi@0.1.81': - optional: true - - '@cargo-messages/darwin-arm64@0.1.81': - optional: true - - '@cargo-messages/darwin-x64@0.1.81': - optional: true - - '@cargo-messages/linux-arm-gnueabihf@0.1.81': - optional: true - - '@cargo-messages/linux-arm64-gnu@0.1.81': - optional: true - - '@cargo-messages/linux-arm64-musl@0.1.81': - optional: true - - '@cargo-messages/linux-x64-gnu@0.1.81': - optional: true - - '@cargo-messages/linux-x64-musl@0.1.81': - optional: true - - '@cargo-messages/win32-arm64-msvc@0.1.81': - optional: true - - '@cargo-messages/win32-x64-msvc@0.1.81': - optional: true - '@changesets/apply-release-plan@7.1.1': dependencies: '@changesets/config': 3.1.4 @@ -3741,7 +3640,7 @@ snapshots: dependencies: '@changesets/types': 6.1.0 - '@changesets/cli@2.31.0(@types/node@22.20.1)': + '@changesets/cli@2.31.1(@types/node@22.20.1)': dependencies: '@changesets/apply-release-plan': 7.1.1 '@changesets/assemble-release-plan': 6.0.10 @@ -3852,7 +3751,7 @@ snapshots: dependencies: '@changesets/types': 6.1.0 fs-extra: 7.0.1 - human-id: 4.1.3 + human-id: 4.2.0 prettier: 2.8.8 '@cipherstash/auth-darwin-arm64@0.42.0': @@ -3896,15 +3795,6 @@ snapshots: fast-wrap-ansi: 0.2.0 sisteransi: 1.0.5 - '@clerk/backend@3.11.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@clerk/shared': 4.25.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - standardwebhooks: 1.0.0 - tslib: 2.8.1 - transitivePeerDependencies: - - react - - react-dom - '@clerk/backend@3.16.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@clerk/shared': 4.29.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -3932,19 +3822,9 @@ snapshots: react-dom: 19.2.3(react@19.2.3) tslib: 2.8.1 - '@clerk/shared@4.25.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@tanstack/query-core': 5.101.2 - dequal: 2.0.3 - glob-to-regexp: 0.4.1 - js-cookie: 3.0.7 - optionalDependencies: - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - '@clerk/shared@4.29.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/query-core': 5.101.2 + '@tanstack/query-core': 5.101.4 dequal: 2.0.3 glob-to-regexp: 0.4.1 js-cookie: 3.0.7 @@ -4148,8 +4028,8 @@ snapshots: '@inquirer/external-editor@1.0.3(@types/node@22.20.1)': dependencies: - chardet: 2.1.1 - iconv-lite: 0.7.2 + chardet: 2.2.0 + iconv-lite: 0.7.3 optionalDependencies: '@types/node': 22.20.1 @@ -4184,7 +4064,7 @@ snapshots: '@manypkg/get-packages@1.1.3': dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 '@changesets/types': 4.1.0 '@manypkg/find-root': 1.1.0 fs-extra: 8.1.0 @@ -4213,18 +4093,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@neon-rs/cli@0.1.82': - optionalDependencies: - '@cargo-messages/android-arm-eabi': 0.1.81 - '@cargo-messages/darwin-arm64': 0.1.81 - '@cargo-messages/darwin-x64': 0.1.81 - '@cargo-messages/linux-arm-gnueabihf': 0.1.81 - '@cargo-messages/linux-arm64-gnu': 0.1.81 - '@cargo-messages/linux-arm64-musl': 0.1.81 - '@cargo-messages/linux-x64-gnu': 0.1.81 - '@cargo-messages/linux-x64-musl': 0.1.81 - '@cargo-messages/win32-arm64-msvc': 0.1.81 - '@cargo-messages/win32-x64-msvc': 0.1.81 + '@neon-rs/cli@0.2.6': {} '@neon-rs/load@0.2.6': {} @@ -4517,62 +4386,62 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@supabase/auth-js@2.110.2': + '@supabase/auth-js@2.112.3': dependencies: tslib: 2.8.1 - '@supabase/functions-js@2.110.2': + '@supabase/functions-js@2.112.3': dependencies: tslib: 2.8.1 - '@supabase/phoenix@0.4.4': {} + '@supabase/phoenix@0.4.5': {} - '@supabase/postgrest-js@2.110.2': + '@supabase/postgrest-js@2.112.3': dependencies: tslib: 2.8.1 - '@supabase/realtime-js@2.110.2': + '@supabase/realtime-js@2.112.3': dependencies: - '@supabase/phoenix': 0.4.4 + '@supabase/phoenix': 0.4.5 tslib: 2.8.1 - '@supabase/storage-js@2.110.2': + '@supabase/storage-js@2.112.3': dependencies: iceberg-js: 0.8.1 tslib: 2.8.1 - '@supabase/supabase-js@2.110.2': + '@supabase/supabase-js@2.112.3': dependencies: - '@supabase/auth-js': 2.110.2 - '@supabase/functions-js': 2.110.2 - '@supabase/postgrest-js': 2.110.2 - '@supabase/realtime-js': 2.110.2 - '@supabase/storage-js': 2.110.2 + '@supabase/auth-js': 2.112.3 + '@supabase/functions-js': 2.112.3 + '@supabase/postgrest-js': 2.112.3 + '@supabase/realtime-js': 2.112.3 + '@supabase/storage-js': 2.112.3 '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 - '@tanstack/query-core@5.101.2': {} + '@tanstack/query-core@5.101.4': {} - '@tsconfig/node22@22.0.5': {} + '@tsconfig/node22@22.0.6': {} - '@turbo/darwin-64@2.10.4': + '@turbo/darwin-64@2.10.10': optional: true - '@turbo/darwin-arm64@2.10.4': + '@turbo/darwin-arm64@2.10.10': optional: true - '@turbo/linux-64@2.10.4': + '@turbo/linux-64@2.10.10': optional: true - '@turbo/linux-arm64@2.10.4': + '@turbo/linux-arm64@2.10.10': optional: true - '@turbo/windows-64@2.10.4': + '@turbo/windows-64@2.10.10': optional: true - '@turbo/windows-arm64@2.10.4': + '@turbo/windows-arm64@2.10.10': optional: true '@types/chai@5.2.3': @@ -4611,7 +4480,7 @@ snapshots: pg-protocol: 1.16.0 pg-types: 2.2.0 - '@types/semver@7.7.1': {} + '@types/semver@7.8.0': {} '@types/uuid@11.0.0': dependencies: @@ -4635,6 +4504,14 @@ snapshots: optionalDependencies: vite: 7.3.6(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0) + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@26.2.0)(jiti@2.7.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 3.2.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@26.2.0)(jiti@2.7.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0) + '@vitest/pretty-format@3.2.7': dependencies: tinyrainbow: 2.0.0 @@ -4787,7 +4664,7 @@ snapshots: loupe: 3.2.1 pathval: 2.0.1 - chardet@2.1.1: {} + chardet@2.2.0: {} check-error@2.1.3: {} @@ -5106,7 +4983,7 @@ snapshots: fsevents@2.3.3: optional: true - fta-cli@3.0.0: {} + fta-cli@3.0.1: {} function-bind@1.1.2: {} @@ -5195,16 +5072,12 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 - human-id@4.1.3: {} + human-id@4.2.0: {} human-signals@8.0.1: {} iceberg-js@0.8.1: {} - iconv-lite@0.7.2: - dependencies: - safer-buffer: 2.1.2 - iconv-lite@0.7.3: dependencies: safer-buffer: 2.1.2 @@ -6036,14 +5909,14 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - turbo@2.10.4: + turbo@2.10.10: optionalDependencies: - '@turbo/darwin-64': 2.10.4 - '@turbo/darwin-arm64': 2.10.4 - '@turbo/linux-64': 2.10.4 - '@turbo/linux-arm64': 2.10.4 - '@turbo/windows-64': 2.10.4 - '@turbo/windows-arm64': 2.10.4 + '@turbo/darwin-64': 2.10.10 + '@turbo/darwin-arm64': 2.10.10 + '@turbo/linux-64': 2.10.10 + '@turbo/linux-arm64': 2.10.10 + '@turbo/windows-64': 2.10.10 + '@turbo/windows-arm64': 2.10.10 typanion@3.14.0: {} @@ -6197,7 +6070,7 @@ snapshots: dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.7 - '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@26.2.0)(jiti@2.7.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.23.12)(yaml@2.9.0)) '@vitest/pretty-format': 3.2.7 '@vitest/runner': 3.2.7 '@vitest/snapshot': 3.2.7 From 1808110bb25fedc278325e4a05b0de6107a5e071 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Wed, 26 Aug 2026 09:35:54 +1000 Subject: [PATCH 2/2] chore(deps-dev): apply Biome 2.5.9 formatting and safe lint fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The @biomejs/biome 2.5.3 -> 2.5.9 bump in this group changes formatter output (indentation of chained expressions) and tightens several lint rules, so the bump alone fails the Lint (Biome) job and the biome format check inside @cipherstash/protect-ffi's test chain. This commit is the output of 'pnpm run code:fix' under 2.5.9 — safe fixes only, no manual edits. 'pnpm run code:check' now exits 0 with zero errors (200 warnings, which CI allows). Claude-Session: https://claude.ai/code/session_01PS9J6pu3FmmQvJHxwTVJUc --- packages/cli/src/__tests__/config.test.ts | 59 +-- .../cli/src/__tests__/database-url.test.ts | 36 +- .../cli/src/__tests__/release-train.test.ts | 51 +- .../src/__tests__/rewrite-migrations.test.ts | 111 +++-- .../__tests__/skill-supabase-apply.test.ts | 87 ++-- .../commands/eql/__tests__/migration.test.ts | 43 +- .../src/commands/eql/__tests__/repair.test.ts | 86 ++-- .../placeholder-client-fixture.test.ts | 40 +- .../__tests__/utils-codegen-drizzle.test.ts | 33 +- .../init/__tests__/utils-codegen.test.ts | 26 +- .../init/lib/__tests__/install-skills.test.ts | 26 +- .../init/lib/__tests__/rollout-state.test.ts | 13 +- .../steps/__tests__/install-skills.test.ts | 23 +- packages/cli/src/db/__tests__/config.test.ts | 22 +- .../e2e/doctor-missing-binary.e2e.test.ts | 51 +- .../tests/e2e/runner-aware-help.e2e.test.ts | 52 +- .../cli/tests/e2e/v2-retirement.e2e.test.ts | 19 +- .../integration-tests/tests/eql-v3.test.ts | 97 ++-- .../integration-tests/tests/json-bulk.test.ts | 106 ++--- .../integration-tests/tests/json.test.ts | 108 ++--- .../integration-tests/tests/scalar.test.ts | 63 ++- .../stack-drizzle/__tests__/codec.test.ts | 59 ++- .../stack-drizzle/__tests__/column.test.ts | 43 +- .../stack-drizzle/__tests__/operators.test.ts | 186 ++++---- .../__tests__/sql-dialect.test.ts | 21 +- .../stack-drizzle/__tests__/types.test.ts | 21 +- .../null-persistence.integration.test.ts | 84 ++-- .../operator-lowering-v3-order-range.test.ts | 37 +- .../__tests__/supabase-helpers.test.ts | 82 ++-- packages/stack/__tests__/cjs-require.test.ts | 110 ++--- .../stack/__tests__/diagnostics-entry.test.ts | 62 +-- .../dynamodb/v2-type-coverage.test.ts | 29 +- .../encrypt-lock-context-guards.test.ts | 131 ++--- .../encrypt-query-searchable-json.test.ts | 74 +-- .../__tests__/encrypt-query-stevec.test.ts | 46 +- .../stack/__tests__/match-bloom-live.test.ts | 50 +- packages/stack/__tests__/schema-v3.test.ts | 101 ++-- .../stack/__tests__/selector-path.test.ts | 25 +- .../stack/__tests__/v3-matrix/matrix.test.ts | 46 +- .../wasm-inline-result-contract.test.ts | 86 ++-- .../stack/__tests__/wasm-inline-v3.test.ts | 25 +- .../shared/match-bloom.integration.test.ts | 30 +- .../shared/matrix-crypto.integration.test.ts | 45 +- .../shared/matrix-sql.integration.test.ts | 447 +++++++++--------- .../v2-decrypt-compat.integration.test.ts | 37 +- .../v2-decrypt-compat.integration.test.ts | 33 +- packages/test-kit/src/run-family-suite.ts | 73 +-- packages/test-kit/src/run-json-suite.ts | 15 +- .../wizard/src/__tests__/interface.test.ts | 31 +- .../src/__tests__/rewrite-migrations.test.ts | 80 ++-- .../bench-index-expressions.test.mjs | 43 +- .../__tests__/cargo-publish-opt-out.test.mjs | 61 ++- scripts/__tests__/cli-vitest-alias.test.mjs | 11 +- .../__tests__/frozen-publisher-docs.test.mjs | 26 +- .../lint-no-eql-registry-pins.test.mjs | 17 +- .../__tests__/neon-dist-crate-name.test.mjs | 19 +- .../__tests__/turbo-skills-inputs.test.mjs | 35 +- .../__tests__/vitest-shared-alias.test.mjs | 11 +- 58 files changed, 1798 insertions(+), 1686 deletions(-) diff --git a/packages/cli/src/__tests__/config.test.ts b/packages/cli/src/__tests__/config.test.ts index 5189bc2f7..13fe63c5c 100644 --- a/packages/cli/src/__tests__/config.test.ts +++ b/packages/cli/src/__tests__/config.test.ts @@ -54,34 +54,37 @@ describe('loadStashConfig', () => { it.each([ ['stash', `Cannot find module 'stash'`], ['@cipherstash/stack', `Cannot find package '@cipherstash/stack'`], - ])('translates a missing `%s` module into actionable guidance (#579)', async (pkg, message) => { - fs.writeFileSync( - path.join(tmpDir, 'stash.config.ts'), - `import 'stash'\nexport default {}`, - ) - process.cwd = () => tmpDir - vi.spyOn(process, 'exit').mockImplementation(() => { - throw new Error('process.exit') - }) - const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - - const moduleErr = Object.assign(new Error(message), { - code: 'MODULE_NOT_FOUND', - }) - const { createJiti } = await import('jiti') - vi.mocked(createJiti).mockReturnValue({ - import: vi.fn().mockRejectedValue(moduleErr), - } as never) - - const { loadStashConfig } = await import('@/config/index.ts') - await expect(loadStashConfig()).rejects.toThrow('process.exit') - - const output = errorSpy.mock.calls.map((c) => c.join(' ')).join('\n') - expect(output).toContain(`\`${pkg}\` is not installed`) - expect(output).toContain('stash init') - // The raw jiti stack trace must NOT be forwarded to the user. - expect(output).not.toContain('Failed to load') - }) + ])( + 'translates a missing `%s` module into actionable guidance (#579)', + async (pkg, message) => { + fs.writeFileSync( + path.join(tmpDir, 'stash.config.ts'), + `import 'stash'\nexport default {}`, + ) + process.cwd = () => tmpDir + vi.spyOn(process, 'exit').mockImplementation(() => { + throw new Error('process.exit') + }) + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + + const moduleErr = Object.assign(new Error(message), { + code: 'MODULE_NOT_FOUND', + }) + const { createJiti } = await import('jiti') + vi.mocked(createJiti).mockReturnValue({ + import: vi.fn().mockRejectedValue(moduleErr), + } as never) + + const { loadStashConfig } = await import('@/config/index.ts') + await expect(loadStashConfig()).rejects.toThrow('process.exit') + + const output = errorSpy.mock.calls.map((c) => c.join(' ')).join('\n') + expect(output).toContain(`\`${pkg}\` is not installed`) + expect(output).toContain('stash init') + // The raw jiti stack trace must NOT be forwarded to the user. + expect(output).not.toContain('Failed to load') + }, + ) it('still surfaces the raw error for unrelated config load failures', async () => { fs.writeFileSync(path.join(tmpDir, 'stash.config.ts'), 'export default {}') diff --git a/packages/cli/src/__tests__/database-url.test.ts b/packages/cli/src/__tests__/database-url.test.ts index e5afb2f58..1d0ee564b 100644 --- a/packages/cli/src/__tests__/database-url.test.ts +++ b/packages/cli/src/__tests__/database-url.test.ts @@ -258,25 +258,23 @@ describe('resolveDatabaseUrl — prompt source', () => { }) describe('resolveDatabaseUrl — CI guard', () => { - it.each([ - 'true', - 'TRUE', - '1', - ' true ', - ])('does not prompt and exits 1 when CI=%j (truthy)', async (ciValue) => { - process.env.CI = ciValue - Object.defineProperty(process.stdin, 'isTTY', { - value: true, - configurable: true, - }) - const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => { - throw new Error('process.exit') - }) as never) - await expect(resolveDatabaseUrl()).rejects.toThrow('process.exit') - expect(exitSpy).toHaveBeenCalledWith(1) - expect(clack.text).not.toHaveBeenCalled() - expect(clack.log.error).toHaveBeenCalledWith(messages.db.urlMissingCi) - }) + it.each(['true', 'TRUE', '1', ' true '])( + 'does not prompt and exits 1 when CI=%j (truthy)', + async (ciValue) => { + process.env.CI = ciValue + Object.defineProperty(process.stdin, 'isTTY', { + value: true, + configurable: true, + }) + const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => { + throw new Error('process.exit') + }) as never) + await expect(resolveDatabaseUrl()).rejects.toThrow('process.exit') + expect(exitSpy).toHaveBeenCalledWith(1) + expect(clack.text).not.toHaveBeenCalled() + expect(clack.log.error).toHaveBeenCalledWith(messages.db.urlMissingCi) + }, + ) it('does not prompt when stdin is not a TTY (e.g. piped)', async () => { Object.defineProperty(process.stdin, 'isTTY', { diff --git a/packages/cli/src/__tests__/release-train.test.ts b/packages/cli/src/__tests__/release-train.test.ts index 4a322eac1..800b7d0dd 100644 --- a/packages/cli/src/__tests__/release-train.test.ts +++ b/packages/cli/src/__tests__/release-train.test.ts @@ -93,32 +93,31 @@ describe('release train coverage', () => { expect(skillFiles.length).toBeGreaterThan(0) }) - it.each( - skillFiles, - )('$skill pins release-train packages at a stable version on the current major', ({ - body, - }) => { - // Exact pins only (`pkg@1.2.3`, optionally `npm:`-prefixed and with a - // trailing subpath). A range spec (`^1.0.0`) carries its own semantics - // and is not a pin to check. - const PIN = - /(?:npm:)?(stash|@cipherstash\/[a-z0-9-]+)@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/g - - for (const [, pkg, pinned] of body.matchAll(PIN)) { - if (!isReleaseTrainPackage(pkg)) continue - const current = manifestVersion(pkg) - - expect( - pinned, - `${pkg}@${pinned} pins a prerelease — skills ship to customers, so pin the stable release (${stableVersion(current)})`, - ).not.toContain('-') - - expect( - pinned.split('.')[0], - `${pkg}@${pinned} is off this release line (${current}) — update the pin and the prose around it`, - ).toBe(stableVersion(current).split('.')[0]) - } - }) + it.each(skillFiles)( + '$skill pins release-train packages at a stable version on the current major', + ({ body }) => { + // Exact pins only (`pkg@1.2.3`, optionally `npm:`-prefixed and with a + // trailing subpath). A range spec (`^1.0.0`) carries its own semantics + // and is not a pin to check. + const PIN = + /(?:npm:)?(stash|@cipherstash\/[a-z0-9-]+)@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/g + + for (const [, pkg, pinned] of body.matchAll(PIN)) { + if (!isReleaseTrainPackage(pkg)) continue + const current = manifestVersion(pkg) + + expect( + pinned, + `${pkg}@${pinned} pins a prerelease — skills ship to customers, so pin the stable release (${stableVersion(current)})`, + ).not.toContain('-') + + expect( + pinned.split('.')[0], + `${pkg}@${pinned} is off this release line (${current}) — update the pin and the prose around it`, + ).toBe(stableVersion(current).split('.')[0]) + } + }, + ) }) it('every train manifest exists and carries a version (what tsup will embed)', () => { diff --git a/packages/cli/src/__tests__/rewrite-migrations.test.ts b/packages/cli/src/__tests__/rewrite-migrations.test.ts index c3ca5f5af..0eda4a28d 100644 --- a/packages/cli/src/__tests__/rewrite-migrations.test.ts +++ b/packages/cli/src/__tests__/rewrite-migrations.test.ts @@ -238,26 +238,26 @@ describe('rewriteEncryptedAlterColumns', () => { // DOMAIN_RE is derived from ENCRYPTED_DOMAIN, so drift between the two can't // silently leave a domain unrewritten. Prove every domain the alternation // recognises is actually extracted into the emitted ADD COLUMN. - it.each([ - ...V3_DOMAINS, - 'eql_v2_encrypted', - ])('extracts the bare domain %s from a mangled ALTER', async (domain) => { - declarePlaintext('"t"', 'c') - const filePath = path.join(tmpDir, '0015_drift.sql') - fs.writeFileSync( - filePath, - `ALTER TABLE "t" ALTER COLUMN "c" SET DATA TYPE "undefined"."${domain}";\n`, - ) + it.each([...V3_DOMAINS, 'eql_v2_encrypted'])( + 'extracts the bare domain %s from a mangled ALTER', + async (domain) => { + declarePlaintext('"t"', 'c') + const filePath = path.join(tmpDir, '0015_drift.sql') + fs.writeFileSync( + filePath, + `ALTER TABLE "t" ALTER COLUMN "c" SET DATA TYPE "undefined"."${domain}";\n`, + ) - const { rewritten } = await rewriteEncryptedAlterColumns(tmpDir) + const { rewritten } = await rewriteEncryptedAlterColumns(tmpDir) - expect(rewritten).toEqual([filePath]) - const updated = fs.readFileSync(filePath, 'utf-8') - expect(updated).toContain( - `ALTER TABLE "t" ADD COLUMN "c_encrypted" "public"."${domain}";`, - ) - expect(updated).not.toContain('SET DATA TYPE') - }) + expect(rewritten).toEqual([filePath]) + const updated = fs.readFileSync(filePath, 'utf-8') + expect(updated).toContain( + `ALTER TABLE "t" ADD COLUMN "c_encrypted" "public"."${domain}";`, + ) + expect(updated).not.toContain('SET DATA TYPE') + }, + ) // The mangled forms are the cross product of what `dataType()` returns and // which drizzle-kit era renders it. Verified against drizzle-kit 0.24.2, @@ -312,22 +312,25 @@ describe('rewriteEncryptedAlterColumns', () => { 'dotted inside "undefined", drizzle-kit >=0.31.0', '"undefined"."public.eql_v2_encrypted"', ], - ])('rewrites the previously unmatched v2 %s form', async (_label, emitted) => { - declarePlaintext('"users"', 'email') - const filePath = path.join(tmpDir, '0009_v2form.sql') - fs.writeFileSync( - filePath, - `ALTER TABLE "users" ALTER COLUMN "email" SET DATA TYPE ${emitted};\n`, - ) + ])( + 'rewrites the previously unmatched v2 %s form', + async (_label, emitted) => { + declarePlaintext('"users"', 'email') + const filePath = path.join(tmpDir, '0009_v2form.sql') + fs.writeFileSync( + filePath, + `ALTER TABLE "users" ALTER COLUMN "email" SET DATA TYPE ${emitted};\n`, + ) - await rewriteEncryptedAlterColumns(tmpDir) + await rewriteEncryptedAlterColumns(tmpDir) - const updated = fs.readFileSync(filePath, 'utf-8') - expect(updated).toContain( - 'ALTER TABLE "users" ADD COLUMN "email_encrypted" "public"."eql_v2_encrypted";', - ) - expect(updated).not.toContain('SET DATA TYPE') - }) + const updated = fs.readFileSync(filePath, 'utf-8') + expect(updated).toContain( + 'ALTER TABLE "users" ADD COLUMN "email_encrypted" "public"."eql_v2_encrypted";', + ) + expect(updated).not.toContain('SET DATA TYPE') + }, + ) it('names the target domain in the guidance comment', async () => { declarePlaintext('"users"', 'email') @@ -925,26 +928,30 @@ describe('rewriteEncryptedAlterColumns', () => { it.each([ ['tagged', 'price$usd$cents'], ['untagged', 'price$$cents'], - ])('does not treat a %s dollar delimiter inside an unquoted identifier as a dollar-quoted body', async (_kind, identifier) => { - declarePlaintext('"users"', 'email') - const filePath = path.join(tmpDir, `0033_${_kind}-identifier.sql`) - fs.writeFileSync( - filePath, - [ - `SELECT ${identifier} FROM "prices";`, - 'ALTER TABLE "users" ALTER COLUMN "email" SET DATA TYPE eql_v3_text_search;', - '', - ].join('\n'), - ) - - const { rewritten, skipped } = await rewriteEncryptedAlterColumns(tmpDir) - - expect(rewritten).toEqual([filePath]) - expect(skipped).toEqual([]) - const updated = fs.readFileSync(filePath, 'utf-8') - expect(updated).toContain('ADD COLUMN "email_encrypted"') - expect(updated).not.toContain('SET DATA TYPE') - }) + ])( + 'does not treat a %s dollar delimiter inside an unquoted identifier as a dollar-quoted body', + async (_kind, identifier) => { + declarePlaintext('"users"', 'email') + const filePath = path.join(tmpDir, `0033_${_kind}-identifier.sql`) + fs.writeFileSync( + filePath, + [ + `SELECT ${identifier} FROM "prices";`, + 'ALTER TABLE "users" ALTER COLUMN "email" SET DATA TYPE eql_v3_text_search;', + '', + ].join('\n'), + ) + + const { rewritten, skipped } = + await rewriteEncryptedAlterColumns(tmpDir) + + expect(rewritten).toEqual([filePath]) + expect(skipped).toEqual([]) + const updated = fs.readFileSync(filePath, 'utf-8') + expect(updated).toContain('ADD COLUMN "email_encrypted"') + expect(updated).not.toContain('SET DATA TYPE') + }, + ) }) /** diff --git a/packages/cli/src/__tests__/skill-supabase-apply.test.ts b/packages/cli/src/__tests__/skill-supabase-apply.test.ts index 68f79fac9..7883340ab 100644 --- a/packages/cli/src/__tests__/skill-supabase-apply.test.ts +++ b/packages/cli/src/__tests__/skill-supabase-apply.test.ts @@ -36,30 +36,29 @@ describe('skills — Supabase apply commands', () => { expect(SKILL_FILES.length).toBeGreaterThan(0) }) - it.each( - SKILL_FILES, - )('$skill never presents a bare `supabase migration up` as the remote apply', ({ - body, - }) => { - // Collapse wrapping and drop markdown emphasis first: the qualifier - // routinely lands on the next source line or arrives as `**local**`, and - // either would fail the match on formatting rather than content. - const prose = body.replace(/\s+/g, ' ').replace(/[*`_]/g, '') + it.each(SKILL_FILES)( + '$skill never presents a bare `supabase migration up` as the remote apply', + ({ body }) => { + // Collapse wrapping and drop markdown emphasis first: the qualifier + // routinely lands on the next source line or arrives as `**local**`, and + // either would fail the match on formatting rather than content. + const prose = body.replace(/\s+/g, ' ').replace(/[*`_]/g, '') - for (const match of prose.matchAll(/supabase migration up/g)) { - // Only what immediately follows the command counts. A window wide - // enough to find a "local database" elsewhere in the sentence accepts - // the very wording this guard rejects — "apply with supabase migration - // up (or supabase db reset locally, once the local database exists)" - // qualifies the other command, not this one. - const following = prose.slice(match.index + match[0].length) + for (const match of prose.matchAll(/supabase migration up/g)) { + // Only what immediately follows the command counts. A window wide + // enough to find a "local database" elsewhere in the sentence accepts + // the very wording this guard rejects — "apply with supabase migration + // up (or supabase db reset locally, once the local database exists)" + // qualifies the other command, not this one. + const following = prose.slice(match.index + match[0].length) - expect( - following.slice(0, 60), - '`supabase migration up` applies to the LOCAL database — add `--linked`, say "applies to the local database", or use `supabase db push` for remote', - ).toMatch(/^(?: --linked\b| applies to the local database\b)/i) - } - }) + expect( + following.slice(0, 60), + '`supabase migration up` applies to the LOCAL database — add `--linked`, say "applies to the local database", or use `supabase db push` for remote', + ).toMatch(/^(?: --linked\b| applies to the local database\b)/i) + } + }, + ) /** * `supabase migration repair --status applied ` writes a ledger row @@ -78,26 +77,30 @@ describe('skills — Supabase apply commands', () => { * because it is created by the bundle's closing statements and so cannot * resolve on a half-applied install, unlike the `eql_v3` schema itself. */ - it.each( - SKILL_FILES, - )('$skill never recommends the ledger-only repair without a check above it', ({ - body, - }) => { - // Same wrap-collapsing as above, minus the `_` strip: the marker here is - // `eql_v3.version()`, which that strip would turn into `eqlv3.version()`. - const prose = body.replace(/\s+/g, ' ').replace(/[*`]/g, '') + it.each(SKILL_FILES)( + '$skill never recommends the ledger-only repair without a check above it', + ({ body }) => { + // Same wrap-collapsing as above, minus the `_` strip: the marker here is + // `eql_v3.version()`, which that strip would turn into `eqlv3.version()`. + const prose = body.replace(/\s+/g, ' ').replace(/[*`]/g, '') - for (const match of prose.matchAll(/migration repair --status applied/g)) { - // A paragraph's worth of lead-in. Wide enough for the sentence that - // introduces the check plus the one that explains what its output means, - // narrow enough that an `eql_v3.version()` mention elsewhere in the - // document cannot stand in for one attached to this recommendation. - const preceding = prose.slice(Math.max(0, match.index - 700), match.index) + for (const match of prose.matchAll( + /migration repair --status applied/g, + )) { + // A paragraph's worth of lead-in. Wide enough for the sentence that + // introduces the check plus the one that explains what its output means, + // narrow enough that an `eql_v3.version()` mention elsewhere in the + // document cannot stand in for one attached to this recommendation. + const preceding = prose.slice( + Math.max(0, match.index - 700), + match.index, + ) - expect( - preceding, - '`migration repair --status applied` writes a ledger row for SQL that may never have run — an unrecoverable state on a remote without EQL. Print the `select eql_v3.version()` check above this recommendation, not after it', - ).toContain('eql_v3.version()') - } - }) + expect( + preceding, + '`migration repair --status applied` writes a ledger row for SQL that may never have run — an unrecoverable state on a remote without EQL. Print the `select eql_v3.version()` check above this recommendation, not after it', + ).toContain('eql_v3.version()') + } + }, + ) }) diff --git a/packages/cli/src/commands/eql/__tests__/migration.test.ts b/packages/cli/src/commands/eql/__tests__/migration.test.ts index d46fda8aa..eb0ada154 100644 --- a/packages/cli/src/commands/eql/__tests__/migration.test.ts +++ b/packages/cli/src/commands/eql/__tests__/migration.test.ts @@ -1151,29 +1151,28 @@ describe('eqlMigrationCommand — Drizzle', () => { // work a partial sweep did complete (#786). A throw that is not an object — // or not one carrying those arrays — must fall through to the plain "could // not sweep" message and still fail closed, not crash on a property read. - it.each([ - null, - undefined, - 'rewrite failed', - ])('handles a non-object sweep failure without masking it: %s', async (failure) => { - const out = join(tmp, 'drizzle') - mkdirSync(out, { recursive: true }) - spawnMock.mockImplementation(() => { - writeFileSync(join(out, '0000_install-eql.sql'), '') - return { status: 0, stdout: '', stderr: '' } - }) - rewriteMock.spy.mockRejectedValueOnce(failure) + it.each([null, undefined, 'rewrite failed'])( + 'handles a non-object sweep failure without masking it: %s', + async (failure) => { + const out = join(tmp, 'drizzle') + mkdirSync(out, { recursive: true }) + spawnMock.mockImplementation(() => { + writeFileSync(join(out, '0000_install-eql.sql'), '') + return { status: 0, stdout: '', stderr: '' } + }) + rewriteMock.spy.mockRejectedValueOnce(failure) - await expect( - eqlMigrationCommand({ drizzle: true, out }), - ).rejects.toBeInstanceOf(CliExit) - expect(clack.log.warn).toHaveBeenCalledWith( - expect.stringContaining('Could not sweep'), - ) - expect(clack.log.error).toHaveBeenCalledWith( - expect.stringContaining('unsafe or unverified SQL'), - ) - }) + await expect( + eqlMigrationCommand({ drizzle: true, out }), + ).rejects.toBeInstanceOf(CliExit) + expect(clack.log.warn).toHaveBeenCalledWith( + expect.stringContaining('Could not sweep'), + ) + expect(clack.log.error).toHaveBeenCalledWith( + expect.stringContaining('unsafe or unverified SQL'), + ) + }, + ) /** * A sweep that threw PART WAY through has already written staged twins to diff --git a/packages/cli/src/commands/eql/__tests__/repair.test.ts b/packages/cli/src/commands/eql/__tests__/repair.test.ts index a3eda6313..5f1c38ab4 100644 --- a/packages/cli/src/commands/eql/__tests__/repair.test.ts +++ b/packages/cli/src/commands/eql/__tests__/repair.test.ts @@ -548,28 +548,33 @@ describe('eqlRepairCommand — applied migrations', () => { it.each([ ['undefined_table', '42P01'], ['invalid_schema_name', '3F000'], - ])('does not claim nothing is applied when the ledger (%s) is absent', async (_l, code) => { - const out = join(tmp, 'drizzle') - writeBrokenCorpus(out) - pgMock.query.mockRejectedValue( - Object.assign(new Error('relation does not exist'), { code }), - ) - - await eqlRepairCommand({ - drizzle: true, - out, - databaseUrl: 'postgres://user:pw@localhost:5432/app', - }) - - expect(clack.log.info).not.toHaveBeenCalledWith( - messages.eql.repairNothingApplied, - ) - const warned = clack.log.warn.mock.calls.map((c) => String(c[0])).join('\n') - // Names where it looked, and the config key that would move it — a - // warning the user cannot act on is not much better than silence. - expect(warned).toContain('drizzle.__drizzle_migrations') - expect(warned).toContain('--migrations-table') - }) + ])( + 'does not claim nothing is applied when the ledger (%s) is absent', + async (_l, code) => { + const out = join(tmp, 'drizzle') + writeBrokenCorpus(out) + pgMock.query.mockRejectedValue( + Object.assign(new Error('relation does not exist'), { code }), + ) + + await eqlRepairCommand({ + drizzle: true, + out, + databaseUrl: 'postgres://user:pw@localhost:5432/app', + }) + + expect(clack.log.info).not.toHaveBeenCalledWith( + messages.eql.repairNothingApplied, + ) + const warned = clack.log.warn.mock.calls + .map((c) => String(c[0])) + .join('\n') + // Names where it looked, and the config key that would move it — a + // warning the user cannot act on is not much better than silence. + expect(warned).toContain('drizzle.__drizzle_migrations') + expect(warned).toContain('--migrations-table') + }, + ) /** * Applied files carry TWO kinds of statement, and they need different words. @@ -647,23 +652,26 @@ describe('eqlRepairCommand — applied migrations', () => { ['too many parts', 'db.drizzle.__drizzle_migrations'], ['an empty part', 'drizzle.'], ['a quoted identifier', '"drizzle"."__drizzle_migrations"'], - ])('rejects a --migrations-table containing %s', async (_label, migrationsTable) => { - const out = join(tmp, 'drizzle') - const broken = writeBrokenCorpus(out) - - await expect( - eqlRepairCommand({ - drizzle: true, - out, - databaseUrl: 'postgres://user:pw@localhost:5432/app', - migrationsTable, - }), - ).rejects.toBeInstanceOf(CliExit) - - // Never reached the database, and never touched the migration. - expect(pgMock.query).not.toHaveBeenCalled() - expect(readFileSync(broken, 'utf-8')).toBe(BROKEN_ALTER) - }) + ])( + 'rejects a --migrations-table containing %s', + async (_label, migrationsTable) => { + const out = join(tmp, 'drizzle') + const broken = writeBrokenCorpus(out) + + await expect( + eqlRepairCommand({ + drizzle: true, + out, + databaseUrl: 'postgres://user:pw@localhost:5432/app', + migrationsTable, + }), + ).rejects.toBeInstanceOf(CliExit) + + // Never reached the database, and never touched the migration. + expect(pgMock.query).not.toHaveBeenCalled() + expect(readFileSync(broken, 'utf-8')).toBe(BROKEN_ALTER) + }, + ) // A bare table name is the common case — drizzle's `migrations.table` without // a `migrations.schema`. It must resolve via search_path, not be forced into diff --git a/packages/cli/src/commands/init/__tests__/placeholder-client-fixture.test.ts b/packages/cli/src/commands/init/__tests__/placeholder-client-fixture.test.ts index c3ddbf548..718ecaaee 100644 --- a/packages/cli/src/commands/init/__tests__/placeholder-client-fixture.test.ts +++ b/packages/cli/src/commands/init/__tests__/placeholder-client-fixture.test.ts @@ -50,25 +50,25 @@ describe('the scaffolded client fixtures match the generator', () => { }) describe('the scaffold compiles because it declares a table', () => { - it.each([ - 'generic.generated.ts', - 'drizzle.generated.ts', - ])('%s passes a non-empty schema set', (file) => { - const body = fixture(file) - // The empty form is a hard TS2769 against both overloads — `Encryption` - // requires at least one table by design (S-6), so the scaffold cannot go - // back to `schemas: []` without breaking every project it is written into. - expect(body).not.toContain('Encryption({ schemas: [] })') - expect(body).toContain('schemas: [placeholderTable]') - }) + it.each(['generic.generated.ts', 'drizzle.generated.ts'])( + '%s passes a non-empty schema set', + (file) => { + const body = fixture(file) + // The empty form is a hard TS2769 against both overloads — `Encryption` + // requires at least one table by design (S-6), so the scaffold cannot go + // back to `schemas: []` without breaking every project it is written into. + expect(body).not.toContain('Encryption({ schemas: [] })') + expect(body).toContain('schemas: [placeholderTable]') + }, + ) - it.each([ - 'generic.generated.ts', - 'drizzle.generated.ts', - ])('%s uses the sentinel name the config loader refuses', (file) => { - // `loadEncryptConfig` exits 1 when this is the only table left, so the - // two must agree — otherwise the user gets a confusing "table not found" - // from whichever command runs next instead of "you never replaced this". - expect(fixture(file)).toContain(`'${PLACEHOLDER_TABLE_NAME}'`) - }) + it.each(['generic.generated.ts', 'drizzle.generated.ts'])( + '%s uses the sentinel name the config loader refuses', + (file) => { + // `loadEncryptConfig` exits 1 when this is the only table left, so the + // two must agree — otherwise the user gets a confusing "table not found" + // from whichever command runs next instead of "you never replaced this". + expect(fixture(file)).toContain(`'${PLACEHOLDER_TABLE_NAME}'`) + }, + ) }) diff --git a/packages/cli/src/commands/init/__tests__/utils-codegen-drizzle.test.ts b/packages/cli/src/commands/init/__tests__/utils-codegen-drizzle.test.ts index d30246f14..53d1ad0c9 100644 --- a/packages/cli/src/commands/init/__tests__/utils-codegen-drizzle.test.ts +++ b/packages/cli/src/commands/init/__tests__/utils-codegen-drizzle.test.ts @@ -34,24 +34,25 @@ const GENERATED_SOURCES: Array<[string, string]> = [ ['generatePlaceholderClient', generatePlaceholderClient('drizzle')], ] -describe.each( - GENERATED_SOURCES, -)('drizzle scaffold — %s', (_name, generated) => { - it('names the drizzle package root, never the removed ./v3 subpath', () => { - expect(generated).toContain('@cipherstash/stack-drizzle') - expect(generated).not.toContain('@cipherstash/stack-drizzle/v3') - }) +describe.each(GENERATED_SOURCES)( + 'drizzle scaffold — %s', + (_name, generated) => { + it('names the drizzle package root, never the removed ./v3 subpath', () => { + expect(generated).toContain('@cipherstash/stack-drizzle') + expect(generated).not.toContain('@cipherstash/stack-drizzle/v3') + }) - it('uses the de-suffixed export names, never the removed *V3 aliases', () => { - expect(generated).not.toContain('extractEncryptionSchemaV3') - expect(generated).not.toContain('createEncryptionOperatorsV3') - }) + it('uses the de-suffixed export names, never the removed *V3 aliases', () => { + expect(generated).not.toContain('extractEncryptionSchemaV3') + expect(generated).not.toContain('createEncryptionOperatorsV3') + }) - it('never scaffolds the removed EQL v2 authoring surface', () => { - expect(generated).not.toContain('encryptedType') - expect(generated).not.toContain('eql_v2_encrypted') - }) -}) + it('never scaffolds the removed EQL v2 authoring surface', () => { + expect(generated).not.toContain('encryptedType') + expect(generated).not.toContain('eql_v2_encrypted') + }) + }, +) describe('generatePlaceholderClient (drizzle)', () => { const placeholder = generatePlaceholderClient('drizzle') diff --git a/packages/cli/src/commands/init/__tests__/utils-codegen.test.ts b/packages/cli/src/commands/init/__tests__/utils-codegen.test.ts index 21d0d929c..68e134514 100644 --- a/packages/cli/src/commands/init/__tests__/utils-codegen.test.ts +++ b/packages/cli/src/commands/init/__tests__/utils-codegen.test.ts @@ -94,16 +94,16 @@ const ALL_DOMAINS: import('../types.js').V3Domain[] = [ 'Json', ] -describe.each([ - 'postgresql', - 'drizzle', -] as const)('generateClientFromSchemas domain round-trip (%s)', (integration) => { - it.each(ALL_DOMAINS)('emits types.%s verbatim', (domain) => { - const s: SchemaDef[] = [ - { tableName: 'x', columns: [{ name: 'c', domain }] }, - ] - expect(generateClientFromSchemas(integration, s)).toContain( - `c: types.${domain}('c'),`, - ) - }) -}) +describe.each(['postgresql', 'drizzle'] as const)( + 'generateClientFromSchemas domain round-trip (%s)', + (integration) => { + it.each(ALL_DOMAINS)('emits types.%s verbatim', (domain) => { + const s: SchemaDef[] = [ + { tableName: 'x', columns: [{ name: 'c', domain }] }, + ] + expect(generateClientFromSchemas(integration, s)).toContain( + `c: types.${domain}('c'),`, + ) + }) + }, +) diff --git a/packages/cli/src/commands/init/lib/__tests__/install-skills.test.ts b/packages/cli/src/commands/init/lib/__tests__/install-skills.test.ts index 46940b53e..770f415f0 100644 --- a/packages/cli/src/commands/init/lib/__tests__/install-skills.test.ts +++ b/packages/cli/src/commands/init/lib/__tests__/install-skills.test.ts @@ -104,22 +104,22 @@ describe('SKILL_MAP', () => { // `dist/*.d.ts` and the Postgres catalog. `postgresql` is that path; // Supabase shares it (Edge Functions are the flagship WASM-entry use, and // its migrations/RPC are hand-written SQL). - it.each([ - 'postgresql', - 'supabase', - ] as const)('%s includes the raw-SQL and edge skills', (integration) => { - expect(SKILL_MAP[integration]).toContain('stash-postgres') - expect(SKILL_MAP[integration]).toContain('stash-edge') - }) + it.each(['postgresql', 'supabase'] as const)( + '%s includes the raw-SQL and edge skills', + (integration) => { + expect(SKILL_MAP[integration]).toContain('stash-postgres') + expect(SKILL_MAP[integration]).toContain('stash-edge') + }, + ) // The ORM integrations emit correctly-typed operands themselves, so they // get cross-links from their own skills rather than the full install. - it.each([ - 'drizzle', - 'prisma-next', - ] as const)('%s does not install the raw-SQL skill', (integration) => { - expect(SKILL_MAP[integration]).not.toContain('stash-postgres') - }) + it.each(['drizzle', 'prisma-next'] as const)( + '%s does not install the raw-SQL skill', + (integration) => { + expect(SKILL_MAP[integration]).not.toContain('stash-postgres') + }, + ) }) describe('skillsFor', () => { diff --git a/packages/cli/src/commands/init/lib/__tests__/rollout-state.test.ts b/packages/cli/src/commands/init/lib/__tests__/rollout-state.test.ts index f7a2f5105..1c4f6903f 100644 --- a/packages/cli/src/commands/init/lib/__tests__/rollout-state.test.ts +++ b/packages/cli/src/commands/init/lib/__tests__/rollout-state.test.ts @@ -26,13 +26,12 @@ describe('classifyPhase', () => { expect(classifyPhase('dual-writing')).toBe('cutover') }) - it.each([ - 'backfilling', - 'backfilled', - 'cut-over', - ] as MigrationPhase[])('%s classifies as cutover (mid-cutover work)', (phase) => { - expect(classifyPhase(phase)).toBe('cutover') - }) + it.each(['backfilling', 'backfilled', 'cut-over'] as MigrationPhase[])( + '%s classifies as cutover (mid-cutover work)', + (phase) => { + expect(classifyPhase(phase)).toBe('cutover') + }, + ) it('dropped classifies as completed', () => { expect(classifyPhase('dropped')).toBe('completed') diff --git a/packages/cli/src/commands/init/steps/__tests__/install-skills.test.ts b/packages/cli/src/commands/init/steps/__tests__/install-skills.test.ts index 751f84e05..7b3fb1916 100644 --- a/packages/cli/src/commands/init/steps/__tests__/install-skills.test.ts +++ b/packages/cli/src/commands/init/steps/__tests__/install-skills.test.ts @@ -113,18 +113,17 @@ describe('skillDestinations', () => { // These handoffs inline the skill bodies into AGENTS.md instead of copying // directories, and `init` performs no handoff — so there is nothing to write. - it.each([ - 'agents-md', - 'lovable', - 'wizard', - ] as const)('installs no directories for --target %s', (target) => { - expect( - skillDestinations( - env({ cli: { claudeCode: true, codex: false } }), - target, - ), - ).toEqual([]) - }) + it.each(['agents-md', 'lovable', 'wizard'] as const)( + 'installs no directories for --target %s', + (target) => { + expect( + skillDestinations( + env({ cli: { claudeCode: true, codex: false } }), + target, + ), + ).toEqual([]) + }, + ) }) describe('guessIntegration', () => { diff --git a/packages/cli/src/db/__tests__/config.test.ts b/packages/cli/src/db/__tests__/config.test.ts index 38b8a1dba..63c6059b6 100644 --- a/packages/cli/src/db/__tests__/config.test.ts +++ b/packages/cli/src/db/__tests__/config.test.ts @@ -73,18 +73,16 @@ describe('buildPgClientConfig', () => { expect(String(stderr.mock.calls[0]?.[0])).toContain('NOT authenticated') }) - it.each([ - 'require', - 'prefer', - 'verify-ca', - 'verify-full', - ])('sslmode=%s verifies fully and strips the param', (mode) => { - const config = buildPgClientConfig( - `postgres://u@db.example.com/app?sslmode=${mode}`, - ) - expect(ssl(config).rejectUnauthorized).toBe(true) - expect(config.connectionString).not.toContain('sslmode') - }) + it.each(['require', 'prefer', 'verify-ca', 'verify-full'])( + 'sslmode=%s verifies fully and strips the param', + (mode) => { + const config = buildPgClientConfig( + `postgres://u@db.example.com/app?sslmode=${mode}`, + ) + expect(ssl(config).rejectUnauthorized).toBe(true) + expect(config.connectionString).not.toContain('sslmode') + }, + ) it('honours sslrootcert= as the sole trust anchor (libpq semantics)', () => { const dir = mkdtempSync(join(tmpdir(), 'stash-ca-')) diff --git a/packages/cli/tests/e2e/doctor-missing-binary.e2e.test.ts b/packages/cli/tests/e2e/doctor-missing-binary.e2e.test.ts index aa144e647..ad7c3bd19 100644 --- a/packages/cli/tests/e2e/doctor-missing-binary.e2e.test.ts +++ b/packages/cli/tests/e2e/doctor-missing-binary.e2e.test.ts @@ -105,31 +105,32 @@ Module.syncBuiltinESMExports() const currentTarget = `${process.platform}-${process.arch}` describe('stash doctor — a platform binary is missing', () => { - it.each( - TARGETS.map((t) => [t.pkg, t] as const), - )('fails the %s row, names the package and exits non-zero', async (_pkg, target) => { - // Wider than the 100-col default so the note's `Missing package:` line is - // asserted as written rather than as clack happened to wrap it. - const r = render(['doctor'], { - env: { NODE_OPTIONS: hideBinaryOf(target) }, - cols: 140, - }) - const { exitCode } = await r.exit - - expect( - exitCode, - `stash doctor passed with no ${target.pkg} binary installed:\n${r.output}`, - ).toBe(1) - expect(r.output).toContain( - `${target.label} — ${messages.doctor.nativeBinaryMissing}`, - ) - expect(r.output).toContain(messages.doctor.problemsFound) - - // The guidance, not just a red row: the platform package by name is the - // one piece of it a user cannot work out for themselves. - expect(r.output).toContain(`${target.pkg}-${currentTarget}`) - expect(r.output).not.toContain('Fatal error') - }) + it.each(TARGETS.map((t) => [t.pkg, t] as const))( + 'fails the %s row, names the package and exits non-zero', + async (_pkg, target) => { + // Wider than the 100-col default so the note's `Missing package:` line is + // asserted as written rather than as clack happened to wrap it. + const r = render(['doctor'], { + env: { NODE_OPTIONS: hideBinaryOf(target) }, + cols: 140, + }) + const { exitCode } = await r.exit + + expect( + exitCode, + `stash doctor passed with no ${target.pkg} binary installed:\n${r.output}`, + ).toBe(1) + expect(r.output).toContain( + `${target.label} — ${messages.doctor.nativeBinaryMissing}`, + ) + expect(r.output).toContain(messages.doctor.problemsFound) + + // The guidance, not just a red row: the platform package by name is the + // one piece of it a user cannot work out for themselves. + expect(r.output).toContain(`${target.pkg}-${currentTarget}`) + expect(r.output).not.toContain('Fatal error') + }, + ) it('fails only the row whose binary is missing', async () => { // Non-vacuity for the pair above. Both probes went through diff --git a/packages/cli/tests/e2e/runner-aware-help.e2e.test.ts b/packages/cli/tests/e2e/runner-aware-help.e2e.test.ts index e8b1fa093..aacf94ea6 100644 --- a/packages/cli/tests/e2e/runner-aware-help.e2e.test.ts +++ b/packages/cli/tests/e2e/runner-aware-help.e2e.test.ts @@ -23,35 +23,31 @@ const cases = [ ] as const describe('--help — runner-aware Usage + Examples', () => { - it.each( - cases, - )('with npm_config_user_agent=$ua, renders "$label stash"', async ({ - ua, - label, - }) => { - const r = await run(['--help'], { env: { npm_config_user_agent: ua } }) - expect(r.exitCode).toBe(0) - // Usage line must use the right runner. The leader is stable - // (`messages.cli.usagePrefix === 'Usage: '`) so we assert on the - // suffix the renderer composes at runtime. - expect(r.output).toContain(`Usage: ${label} stash`) - // At least one of the Examples lines must surface the same runner. - expect(r.output).toContain(`${label} stash init`) - expect(r.output).toContain(`${label} stash eql install`) - }) + it.each(cases)( + 'with npm_config_user_agent=$ua, renders "$label stash"', + async ({ ua, label }) => { + const r = await run(['--help'], { env: { npm_config_user_agent: ua } }) + expect(r.exitCode).toBe(0) + // Usage line must use the right runner. The leader is stable + // (`messages.cli.usagePrefix === 'Usage: '`) so we assert on the + // suffix the renderer composes at runtime. + expect(r.output).toContain(`Usage: ${label} stash`) + // At least one of the Examples lines must surface the same runner. + expect(r.output).toContain(`${label} stash init`) + expect(r.output).toContain(`${label} stash eql install`) + }, + ) }) describe('auth — runner-aware Usage + Examples', () => { - it.each( - cases, - )('with npm_config_user_agent=$ua, renders "$label stash auth"', async ({ - ua, - label, - }) => { - // `auth` with no subcommand prints the auth HELP and exits 0. - const r = await run(['auth'], { env: { npm_config_user_agent: ua } }) - expect(r.exitCode).toBe(0) - expect(r.output).toContain(`Usage: ${label} stash auth`) - expect(r.output).toContain(`${label} stash auth login`) - }) + it.each(cases)( + 'with npm_config_user_agent=$ua, renders "$label stash auth"', + async ({ ua, label }) => { + // `auth` with no subcommand prints the auth HELP and exits 0. + const r = await run(['auth'], { env: { npm_config_user_agent: ua } }) + expect(r.exitCode).toBe(0) + expect(r.output).toContain(`Usage: ${label} stash auth`) + expect(r.output).toContain(`${label} stash auth login`) + }, + ) }) diff --git a/packages/cli/tests/e2e/v2-retirement.e2e.test.ts b/packages/cli/tests/e2e/v2-retirement.e2e.test.ts index eeaa73dca..3f0f9f8fc 100644 --- a/packages/cli/tests/e2e/v2-retirement.e2e.test.ts +++ b/packages/cli/tests/e2e/v2-retirement.e2e.test.ts @@ -30,15 +30,18 @@ describe('retired EQL v2 CLI surface', () => { ['--no-proxy', '--no-proxy'], ['--proxy=true', '--proxy'], ['--no-proxy=true', '--no-proxy'], - ] as const)('rejects retired init flag `%s` before init work', async (arg, flag) => { - const result = await runPiped(['init', arg], { timeoutMs: 2_000 }) + ] as const)( + 'rejects retired init flag `%s` before init work', + async (arg, flag) => { + const result = await runPiped(['init', arg], { timeoutMs: 2_000 }) - expect(result.timedOut).toBe(false) - expect(result.exitCode).toBe(1) - expect(output(result)).toContain(flag) - expect(output(result)).toMatch(/removed|retired/i) - expect(output(result)).toMatch(/EQL v3/i) - }) + expect(result.timedOut).toBe(false) + expect(result.exitCode).toBe(1) + expect(output(result)).toContain(flag) + expect(output(result)).toMatch(/removed|retired/i) + expect(output(result)).toMatch(/EQL v3/i) + }, + ) it.each([ '--eql-version=2', diff --git a/packages/protect-ffi/integration-tests/tests/eql-v3.test.ts b/packages/protect-ffi/integration-tests/tests/eql-v3.test.ts index c6d890196..2eecfee4b 100644 --- a/packages/protect-ffi/integration-tests/tests/eql-v3.test.ts +++ b/packages/protect-ffi/integration-tests/tests/eql-v3.test.ts @@ -219,35 +219,35 @@ describe('eql v3 scalar round-trips', async () => { }, ] - test.each(cases)('round-trips $column as eql_v3.$domain', async ({ - column, - plaintext, - expected, - domain, - keys, - }) => { - const ciphertext = await encrypt(client, { - plaintext, - column, - table: 'v3users', - }) - - const payload = expectV3Scalar(ciphertext) - expect( - Object.keys(payload).sort(), - `the config must map ${column} onto eql_v3.${domain}`, - ).toEqual(keys) - - const decrypted = await decrypt(client, { ciphertext }) - if (expected !== undefined) { - expect(decrypted).toBe(expected) - } else if (typeof plaintext === 'number' && !Number.isInteger(plaintext)) { - // decimal decrypts to a string representation - expect(Number(decrypted)).toBeCloseTo(plaintext) - } else { - expect(decrypted).toBe(plaintext) - } - }) + test.each(cases)( + 'round-trips $column as eql_v3.$domain', + async ({ column, plaintext, expected, domain, keys }) => { + const ciphertext = await encrypt(client, { + plaintext, + column, + table: 'v3users', + }) + + const payload = expectV3Scalar(ciphertext) + expect( + Object.keys(payload).sort(), + `the config must map ${column} onto eql_v3.${domain}`, + ).toEqual(keys) + + const decrypted = await decrypt(client, { ciphertext }) + if (expected !== undefined) { + expect(decrypted).toBe(expected) + } else if ( + typeof plaintext === 'number' && + !Number.isInteger(plaintext) + ) { + // decimal decrypts to a string representation + expect(Number(decrypted)).toBeCloseTo(plaintext) + } else { + expect(decrypted).toBe(plaintext) + } + }, + ) test('round-trips a date column as eql_v3.date_ord_ore', async () => { const d = new Date('2024-03-01T00:00:00.000Z') @@ -488,28 +488,23 @@ describe('eql v3 query encryption', async () => { }, ] - test.each( - scalarQueryCases, - )('scalar $indexType query on $column returns a term-only eql_v3.query_$domain operand', async ({ - column, - plaintext, - indexType, - domain, - keys, - }) => { - const result = await encryptQuery(client, { - plaintext, - column, - table: 'v3users', - indexType, - }) - - const operand = expectV3QueryOperand(result) - expect( - Object.keys(operand).sort(), - `the operand must target eql_v3.query_${domain}`, - ).toEqual(keys) - }) + test.each(scalarQueryCases)( + 'scalar $indexType query on $column returns a term-only eql_v3.query_$domain operand', + async ({ column, plaintext, indexType, domain, keys }) => { + const result = await encryptQuery(client, { + plaintext, + column, + table: 'v3users', + indexType, + }) + + const operand = expectV3QueryOperand(result) + expect( + Object.keys(operand).sort(), + `the operand must target eql_v3.query_${domain}`, + ).toEqual(keys) + }, + ) test('selector query returns the bare selector hash as a string', async () => { const selector = await encryptQuery(client, { diff --git a/packages/protect-ffi/integration-tests/tests/json-bulk.test.ts b/packages/protect-ffi/integration-tests/tests/json-bulk.test.ts index 15e1f2b5b..bd8f309d7 100644 --- a/packages/protect-ffi/integration-tests/tests/json-bulk.test.ts +++ b/packages/protect-ffi/integration-tests/tests/json-bulk.test.ts @@ -28,68 +28,68 @@ const userProfile: UserColumn = { describe.each([ { encryptConfig: jsonOpaque, description: 'opaque' }, { encryptConfig: jsonSteVec, description: 'ste_vec' }, -])('Can round-trip encrypt & decrypt JSON', async ({ - encryptConfig, - description, -}) => { - describe(`using ${description} config`, () => { - test('object', async () => { - const client = await newClient({ encryptConfig }) - const plaintexts = [ - { foo: 'bar', baz: 123 }, - { foo: 'baz', baz: 456 }, - ] +])( + 'Can round-trip encrypt & decrypt JSON', + async ({ encryptConfig, description }) => { + describe(`using ${description} config`, () => { + test('object', async () => { + const client = await newClient({ encryptConfig }) + const plaintexts = [ + { foo: 'bar', baz: 123 }, + { foo: 'baz', baz: 456 }, + ] - const ciphertexts = await encryptBulk(client, { - plaintexts: plaintexts.map((plaintext) => ({ - plaintext, - ...userProfile, - })), + const ciphertexts = await encryptBulk(client, { + plaintexts: plaintexts.map((plaintext) => ({ + plaintext, + ...userProfile, + })), + }) + const decrypted = await decryptBulk(client, { + ciphertexts: ciphertexts.map((ciphertext) => ({ ciphertext })), + }) + expect(decrypted).toEqual(plaintexts) }) - const decrypted = await decryptBulk(client, { - ciphertexts: ciphertexts.map((ciphertext) => ({ ciphertext })), - }) - expect(decrypted).toEqual(plaintexts) - }) - test('array object mixed', async () => { - const client = await newClient({ encryptConfig }) - const plaintexts = [[1, 2, 3], { foo: 'baz', baz: 456 }] + test('array object mixed', async () => { + const client = await newClient({ encryptConfig }) + const plaintexts = [[1, 2, 3], { foo: 'baz', baz: 456 }] - const ciphertexts = await encryptBulk(client, { - plaintexts: plaintexts.map((plaintext) => ({ - plaintext, - ...userProfile, - })), - }) + const ciphertexts = await encryptBulk(client, { + plaintexts: plaintexts.map((plaintext) => ({ + plaintext, + ...userProfile, + })), + }) + + const decrypted = await decryptBulk(client, { + ciphertexts: ciphertexts.map((ciphertext) => ({ ciphertext })), + }) - const decrypted = await decryptBulk(client, { - ciphertexts: ciphertexts.map((ciphertext) => ({ ciphertext })), + expect(decrypted).toEqual(plaintexts) }) - expect(decrypted).toEqual(plaintexts) - }) + test('nested variants', async () => { + const client = await newClient({ encryptConfig }) + const plaintexts = [ + { foo: 'bar', baz: [1, 2, 3] }, + { foo: 'bar', baz: [{ qux: 'quux' }] }, + { foo: 'bar', baz: { qux: 'quux' } }, + ] - test('nested variants', async () => { - const client = await newClient({ encryptConfig }) - const plaintexts = [ - { foo: 'bar', baz: [1, 2, 3] }, - { foo: 'bar', baz: [{ qux: 'quux' }] }, - { foo: 'bar', baz: { qux: 'quux' } }, - ] + const ciphertexts = await encryptBulk(client, { + plaintexts: plaintexts.map((plaintext) => ({ + plaintext, + ...userProfile, + })), + }) - const ciphertexts = await encryptBulk(client, { - plaintexts: plaintexts.map((plaintext) => ({ - plaintext, - ...userProfile, - })), - }) + const decrypted = await decryptBulk(client, { + ciphertexts: ciphertexts.map((ciphertext) => ({ ciphertext })), + }) - const decrypted = await decryptBulk(client, { - ciphertexts: ciphertexts.map((ciphertext) => ({ ciphertext })), + expect(decrypted).toEqual(plaintexts) }) - - expect(decrypted).toEqual(plaintexts) }) - }) -}) + }, +) diff --git a/packages/protect-ffi/integration-tests/tests/json.test.ts b/packages/protect-ffi/integration-tests/tests/json.test.ts index 32d7aa6ba..a92d42ad5 100644 --- a/packages/protect-ffi/integration-tests/tests/json.test.ts +++ b/packages/protect-ffi/integration-tests/tests/json.test.ts @@ -27,82 +27,82 @@ const userProfile: UserColumn = { describe.each([ { encryptConfig: jsonOpaque, description: 'opaque' }, { encryptConfig: jsonSteVec, description: 'ste_vec' }, -])('Can round-trip encrypt & decrypt JSON', ({ - encryptConfig, - description, -}) => { - describe(`using ${description} config`, () => { - test('object', async ({ annotate }) => { - const client = await newClient({ encryptConfig }) - const originalPlaintext = { foo: 'bar', baz: 123 } - - const ciphertext = await encrypt(client, { - plaintext: originalPlaintext, - ...userProfile, +])( + 'Can round-trip encrypt & decrypt JSON', + ({ encryptConfig, description }) => { + describe(`using ${description} config`, () => { + test('object', async ({ annotate }) => { + const client = await newClient({ encryptConfig }) + const originalPlaintext = { foo: 'bar', baz: 123 } + + const ciphertext = await encrypt(client, { + plaintext: originalPlaintext, + ...userProfile, + }) + + const decrypted = await decrypt(client, { ciphertext }) + + expect(decrypted).toEqual(originalPlaintext) }) - const decrypted = await decrypt(client, { ciphertext }) + test('array', async () => { + const client = await newClient({ encryptConfig }) + const originalPlaintext = [1, 2, 3] - expect(decrypted).toEqual(originalPlaintext) - }) + const ciphertext = await encrypt(client, { + plaintext: originalPlaintext, + ...userProfile, + }) - test('array', async () => { - const client = await newClient({ encryptConfig }) - const originalPlaintext = [1, 2, 3] + const decrypted = await decrypt(client, { ciphertext }) - const ciphertext = await encrypt(client, { - plaintext: originalPlaintext, - ...userProfile, + expect(decrypted).toEqual(originalPlaintext) }) - const decrypted = await decrypt(client, { ciphertext }) + test('nested array within object', async () => { + const client = await newClient({ encryptConfig }) + const originalPlaintext = { foo: 'bar', baz: [1, 2, 3] } - expect(decrypted).toEqual(originalPlaintext) - }) + const ciphertext = await encrypt(client, { + plaintext: originalPlaintext, + ...userProfile, + }) - test('nested array within object', async () => { - const client = await newClient({ encryptConfig }) - const originalPlaintext = { foo: 'bar', baz: [1, 2, 3] } + const decrypted = await decrypt(client, { ciphertext }) - const ciphertext = await encrypt(client, { - plaintext: originalPlaintext, - ...userProfile, + expect(decrypted).toEqual(originalPlaintext) }) - const decrypted = await decrypt(client, { ciphertext }) + test('nested object within object', async () => { + const client = await newClient({ encryptConfig }) + const originalPlaintext = { foo: 'bar', baz: { qux: 'quux' } } - expect(decrypted).toEqual(originalPlaintext) - }) + const ciphertext = await encrypt(client, { + plaintext: originalPlaintext, + ...userProfile, + }) - test('nested object within object', async () => { - const client = await newClient({ encryptConfig }) - const originalPlaintext = { foo: 'bar', baz: { qux: 'quux' } } + const decrypted = await decrypt(client, { ciphertext }) - const ciphertext = await encrypt(client, { - plaintext: originalPlaintext, - ...userProfile, + expect(decrypted).toEqual(originalPlaintext) }) - const decrypted = await decrypt(client, { ciphertext }) + test('nested object within array', async () => { + const client = await newClient({ encryptConfig }) + const originalPlaintext = { foo: 'bar', baz: [{ qux: 'quux' }] } - expect(decrypted).toEqual(originalPlaintext) - }) + const ciphertext = await encrypt(client, { + plaintext: originalPlaintext, + ...userProfile, + }) - test('nested object within array', async () => { - const client = await newClient({ encryptConfig }) - const originalPlaintext = { foo: 'bar', baz: [{ qux: 'quux' }] } + const decrypted = await decrypt(client, { ciphertext }) - const ciphertext = await encrypt(client, { - plaintext: originalPlaintext, - ...userProfile, + expect(decrypted).toEqual(originalPlaintext) }) - - const decrypted = await decrypt(client, { ciphertext }) - - expect(decrypted).toEqual(originalPlaintext) }) - }) -}) + }, +) describe('SteVec output structure', () => { test('encrypted output has expected fields', async () => { diff --git a/packages/protect-ffi/integration-tests/tests/scalar.test.ts b/packages/protect-ffi/integration-tests/tests/scalar.test.ts index 7c93669f9..be7dc7a6a 100644 --- a/packages/protect-ffi/integration-tests/tests/scalar.test.ts +++ b/packages/protect-ffi/integration-tests/tests/scalar.test.ts @@ -50,45 +50,44 @@ const cases: { { identifier: numberColumn, plaintext: 123.456, expected: 123.456 }, ] -describe.each(cases)('encrypt and decrypt', ({ - identifier, - plaintext, - expected, -}) => { - describe(`using column ${identifier.column} with ${typeof plaintext} value`, () => { - test('can round-trip encrypt and decrypt a string', async () => { - const client = await newClient({ encryptConfig }) - const ciphertext = await encrypt(client, { - plaintext, - ...identifier, +describe.each(cases)( + 'encrypt and decrypt', + ({ identifier, plaintext, expected }) => { + describe(`using column ${identifier.column} with ${typeof plaintext} value`, () => { + test('can round-trip encrypt and decrypt a string', async () => { + const client = await newClient({ encryptConfig }) + const ciphertext = await encrypt(client, { + plaintext, + ...identifier, + }) + + expect(isEncrypted(ciphertext)).toBe(true) + + const decrypted = await decrypt(client, { ciphertext }) + expect(decrypted).toBe(expected) }) - expect(isEncrypted(ciphertext)).toBe(true) + test('can explicitly pass in undefined for optional fields', async () => { + const client = await newClient({ encryptConfig }) - const decrypted = await decrypt(client, { ciphertext }) - expect(decrypted).toBe(expected) - }) + const ciphertext = await encrypt(client, { + plaintext, + lockContext: undefined, + unverifiedContext: undefined, + ...identifier, + }) - test('can explicitly pass in undefined for optional fields', async () => { - const client = await newClient({ encryptConfig }) + const decrypted = await decrypt(client, { + ciphertext, + lockContext: undefined, + unverifiedContext: undefined, + }) - const ciphertext = await encrypt(client, { - plaintext, - lockContext: undefined, - unverifiedContext: undefined, - ...identifier, + expect(decrypted).toBe(expected) }) - - const decrypted = await decrypt(client, { - ciphertext, - lockContext: undefined, - unverifiedContext: undefined, - }) - - expect(decrypted).toBe(expected) }) - }) -}) + }, +) describe('bigint plaintexts', () => { const I64_MAX = 2n ** 63n - 1n diff --git a/packages/stack-drizzle/__tests__/codec.test.ts b/packages/stack-drizzle/__tests__/codec.test.ts index 6d44985a4..0abfb1dd9 100644 --- a/packages/stack-drizzle/__tests__/codec.test.ts +++ b/packages/stack-drizzle/__tests__/codec.test.ts @@ -83,44 +83,39 @@ describe('v3 codec', () => { // wrong-shape payload was returned as-is — `v3FromDriver('5')` yielded `5` // typed as an envelope, which then reached `decrypt` as garbage. describe('malformed and non-envelope payloads', () => { - it.each([ - '{bad', - '', - '{"v":3,', - ])('throws EqlV3CodecError on unparseable jsonb %j', (raw) => { - expect(() => v3FromDriver(raw)).toThrow(EqlV3CodecError) - expect(() => v3FromDriver(raw)).toThrow(/Failed to parse/) - }) + it.each(['{bad', '', '{"v":3,'])( + 'throws EqlV3CodecError on unparseable jsonb %j', + (raw) => { + expect(() => v3FromDriver(raw)).toThrow(EqlV3CodecError) + expect(() => v3FromDriver(raw)).toThrow(/Failed to parse/) + }, + ) // The parse failure is the only codec path with an upstream error to // preserve. Flattening it to a message string discards the SyntaxError and // its stack, so a caller debugging a corrupt column loses where parsing // actually broke. - it.each([ - '{bad', - '', - '{"v":3,', - ])('preserves the underlying SyntaxError as `cause` for %j', (raw) => { - let thrown: unknown - try { - v3FromDriver(raw) - } catch (error) { - thrown = error - } - - expect(thrown).toBeInstanceOf(EqlV3CodecError) - expect((thrown as EqlV3CodecError).cause).toBeInstanceOf(SyntaxError) - }) + it.each(['{bad', '', '{"v":3,'])( + 'preserves the underlying SyntaxError as `cause` for %j', + (raw) => { + let thrown: unknown + try { + v3FromDriver(raw) + } catch (error) { + thrown = error + } + + expect(thrown).toBeInstanceOf(EqlV3CodecError) + expect((thrown as EqlV3CodecError).cause).toBeInstanceOf(SyntaxError) + }, + ) - it.each([ - '5', - '"a string"', - 'true', - '[]', - '[{"v":3,"c":"ct"}]', - ])('throws on valid JSON %j that is not an envelope', (raw) => { - expect(() => v3FromDriver(raw)).toThrow(EqlV3CodecError) - }) + it.each(['5', '"a string"', 'true', '[]', '[{"v":3,"c":"ct"}]'])( + 'throws on valid JSON %j that is not an envelope', + (raw) => { + expect(() => v3FromDriver(raw)).toThrow(EqlV3CodecError) + }, + ) it('throws on an object missing the schema version', () => { expect(() => v3FromDriver({ c: 'ct' })).toThrow(/missing "v"/) diff --git a/packages/stack-drizzle/__tests__/column.test.ts b/packages/stack-drizzle/__tests__/column.test.ts index dfca7f9b8..1fee78d19 100644 --- a/packages/stack-drizzle/__tests__/column.test.ts +++ b/packages/stack-drizzle/__tests__/column.test.ts @@ -158,27 +158,28 @@ describe('makeEqlV3Column', () => { expect(builder?.getEqlType()).toBe('public.eql_v3_integer_ord') }) - it.each( - typedEntries(V3_MATRIX), - )('%s round-trips through makeEqlV3Column and pgTable', (eqlType, spec) => { - const columnName = slug(eqlType) - const builder = spec.builder(columnName) - const column = makeEqlV3Column(builder) - - expect(builder.getEqlType()).toBe(eqlType) - expect(builder).toBeInstanceOf(spec.ColumnClass) - expect(isEqlV3Column(column)).toBe(true) - expect(getEqlV3Column(columnName, column)?.getEqlType()).toBe(eqlType) - - const table = pgTable('users', { [columnName]: column } as never) - const pgColumn = (table as Record)[ - columnName - ] - // getSQLType() is the bare (unqualified) domain — what drizzle-kit emits — - // while recovery still yields the qualified builder identity. - expect(pgColumn?.getSQLType()).toBe(slug(eqlType)) - expect(getEqlV3Column(columnName, pgColumn)?.getEqlType()).toBe(eqlType) - }) + it.each(typedEntries(V3_MATRIX))( + '%s round-trips through makeEqlV3Column and pgTable', + (eqlType, spec) => { + const columnName = slug(eqlType) + const builder = spec.builder(columnName) + const column = makeEqlV3Column(builder) + + expect(builder.getEqlType()).toBe(eqlType) + expect(builder).toBeInstanceOf(spec.ColumnClass) + expect(isEqlV3Column(column)).toBe(true) + expect(getEqlV3Column(columnName, column)?.getEqlType()).toBe(eqlType) + + const table = pgTable('users', { [columnName]: column } as never) + const pgColumn = (table as Record)[ + columnName + ] + // getSQLType() is the bare (unqualified) domain — what drizzle-kit emits — + // while recovery still yields the qualified builder identity. + expect(pgColumn?.getSQLType()).toBe(slug(eqlType)) + expect(getEqlV3Column(columnName, pgColumn)?.getEqlType()).toBe(eqlType) + }, + ) // The `toDriver`/`fromDriver` closures wired into `customType` (column.ts) are // otherwise only tested via the standalone codec functions, so the wiring diff --git a/packages/stack-drizzle/__tests__/operators.test.ts b/packages/stack-drizzle/__tests__/operators.test.ts index bc2456173..f9709b19e 100644 --- a/packages/stack-drizzle/__tests__/operators.test.ts +++ b/packages/stack-drizzle/__tests__/operators.test.ts @@ -139,21 +139,22 @@ const users = pgTable('users', { }) describe('createEncryptionOperators - equality', () => { - it.each( - equalityDomains, - )('%s eq emits the latest two-arg eql_v3.eq with a query-term operand', async (eqlType, spec) => { - const { ops, encryptQuery, render } = setup() - const q = render(await ops.eq(matrixColumn(eqlType), sampleFor(spec))) + it.each(equalityDomains)( + '%s eq emits the latest two-arg eql_v3.eq with a query-term operand', + async (eqlType, spec) => { + const { ops, encryptQuery, render } = setup() + const q = render(await ops.eq(matrixColumn(eqlType), sampleFor(spec))) - expect(q.sql).toContain( - `eql_v3.eq("matrix_users"."${slug(eqlType)}", $1::${qcast(eqlType)})`, - ) - expect(q.params).toEqual([TERM_JSON]) - expect(encryptQuery.mock.calls[0]?.[1]?.column.getName()).toBe( - slug(eqlType), - ) - expect(encryptQuery.mock.calls[0]?.[1]?.queryType).toBe('equality') - }) + expect(q.sql).toContain( + `eql_v3.eq("matrix_users"."${slug(eqlType)}", $1::${qcast(eqlType)})`, + ) + expect(q.params).toEqual([TERM_JSON]) + expect(encryptQuery.mock.calls[0]?.[1]?.column.getName()).toBe( + slug(eqlType), + ) + expect(encryptQuery.mock.calls[0]?.[1]?.queryType).toBe('equality') + }, + ) it.each(equalityDomains)('%s ne emits eql_v3.neq', async (eqlType, spec) => { const { ops, encryptQuery, render } = setup() @@ -250,38 +251,42 @@ describe('createEncryptionOperators - comparison & range', () => { } }) - it.each( - orderDomains, - )('%s between emits a bounded range with two query-term operands', async (eqlType, spec) => { - const { ops, render } = setup() - const value = sampleFor(spec) - const q = render(await ops.between(matrixColumn(eqlType), value, value)) - - expect(q.sql).toContain( - `eql_v3.gte("matrix_users"."${slug(eqlType)}", $1::${qcast(eqlType)})`, - ) - expect(q.sql).toContain( - `eql_v3.lte("matrix_users"."${slug(eqlType)}", $2::${qcast(eqlType)})`, - ) - expect(q.params).toEqual([TERM_JSON, TERM_JSON]) - }) + it.each(orderDomains)( + '%s between emits a bounded range with two query-term operands', + async (eqlType, spec) => { + const { ops, render } = setup() + const value = sampleFor(spec) + const q = render(await ops.between(matrixColumn(eqlType), value, value)) - it.each( - orderDomains, - )('%s notBetween wraps the range in NOT (...)', async (eqlType, spec) => { - const { ops, render } = setup() - const value = sampleFor(spec) - const q = render(await ops.notBetween(matrixColumn(eqlType), value, value)) + expect(q.sql).toContain( + `eql_v3.gte("matrix_users"."${slug(eqlType)}", $1::${qcast(eqlType)})`, + ) + expect(q.sql).toContain( + `eql_v3.lte("matrix_users"."${slug(eqlType)}", $2::${qcast(eqlType)})`, + ) + expect(q.params).toEqual([TERM_JSON, TERM_JSON]) + }, + ) + + it.each(orderDomains)( + '%s notBetween wraps the range in NOT (...)', + async (eqlType, spec) => { + const { ops, render } = setup() + const value = sampleFor(spec) + const q = render( + await ops.notBetween(matrixColumn(eqlType), value, value), + ) - expect(q.sql).toMatch(/^not \(/i) - expect(q.sql).toContain( - `eql_v3.gte("matrix_users"."${slug(eqlType)}", $1::${qcast(eqlType)})`, - ) - expect(q.sql).toContain( - `eql_v3.lte("matrix_users"."${slug(eqlType)}", $2::${qcast(eqlType)})`, - ) - expect(q.params).toEqual([TERM_JSON, TERM_JSON]) - }) + expect(q.sql).toMatch(/^not \(/i) + expect(q.sql).toContain( + `eql_v3.gte("matrix_users"."${slug(eqlType)}", $1::${qcast(eqlType)})`, + ) + expect(q.sql).toContain( + `eql_v3.lte("matrix_users"."${slug(eqlType)}", $2::${qcast(eqlType)})`, + ) + expect(q.params).toEqual([TERM_JSON, TERM_JSON]) + }, + ) // Every other `between` case passes identical bounds against a constant // encrypt stub, so the operand never reaches an assertion and a min/max @@ -334,59 +339,64 @@ describe('createEncryptionOperators - comparison & range', () => { ) }) - it.each( - orderDomains, - )('%s asc / desc extract the ord term', (eqlType, spec) => { - const { ops, render } = setup() - // eql-3.0.0 splits the extractor by ordering flavour: `ord_term` for the - // OPE-backed `_ord` domains, `ord_term_ore` for the block-ORE `_ord_ore`. - const fn = spec.indexes.ore ? 'ord_term_ore' : 'ord_term' - const ascq = render(ops.asc(matrixColumn(eqlType))) - expect(ascq.sql).toContain( - `eql_v3.${fn}("matrix_users"."${slug(eqlType)}")`, - ) - expect(ascq.sql.toLowerCase()).toContain('asc') + it.each(orderDomains)( + '%s asc / desc extract the ord term', + (eqlType, spec) => { + const { ops, render } = setup() + // eql-3.0.0 splits the extractor by ordering flavour: `ord_term` for the + // OPE-backed `_ord` domains, `ord_term_ore` for the block-ORE `_ord_ore`. + const fn = spec.indexes.ore ? 'ord_term_ore' : 'ord_term' + const ascq = render(ops.asc(matrixColumn(eqlType))) + expect(ascq.sql).toContain( + `eql_v3.${fn}("matrix_users"."${slug(eqlType)}")`, + ) + expect(ascq.sql.toLowerCase()).toContain('asc') - const descq = render(ops.desc(matrixColumn(eqlType))) - expect(descq.sql).toContain( - `eql_v3.${fn}("matrix_users"."${slug(eqlType)}")`, - ) - expect(descq.sql.toLowerCase()).toContain('desc') - }) + const descq = render(ops.desc(matrixColumn(eqlType))) + expect(descq.sql).toContain( + `eql_v3.${fn}("matrix_users"."${slug(eqlType)}")`, + ) + expect(descq.sql.toLowerCase()).toContain('desc') + }, + ) }) describe('createEncryptionOperators - free-text match', () => { - it.each( - matchDomains, - )('%s matches emits latest eql_v3.matches with a query-term operand', async (eqlType, spec) => { - const { ops, encryptQuery, render } = setup() - const q = render(await ops.matches(matrixColumn(eqlType), needleFor(spec))) + it.each(matchDomains)( + '%s matches emits latest eql_v3.matches with a query-term operand', + async (eqlType, spec) => { + const { ops, encryptQuery, render } = setup() + const q = render( + await ops.matches(matrixColumn(eqlType), needleFor(spec)), + ) - expect(q.sql).toContain( - `eql_v3.matches("matrix_users"."${slug(eqlType)}", $1::${qcast(eqlType)})`, - ) - expect(q.params).toEqual([TERM_JSON]) - expect(encryptQuery.mock.calls[0]?.[1]?.column.getName()).toBe( - slug(eqlType), - ) - expect(encryptQuery.mock.calls[0]?.[1]?.queryType).toBe('freeTextSearch') - }) + expect(q.sql).toContain( + `eql_v3.matches("matrix_users"."${slug(eqlType)}", $1::${qcast(eqlType)})`, + ) + expect(q.params).toEqual([TERM_JSON]) + expect(encryptQuery.mock.calls[0]?.[1]?.column.getName()).toBe( + slug(eqlType), + ) + expect(encryptQuery.mock.calls[0]?.[1]?.queryType).toBe('freeTextSearch') + }, + ) // A needle shorter than the tokenizer's `token_length` produces an empty // bloom filter, and `stored_bf @> '{}'` is true for every row — so this must // throw rather than silently return the whole table. - it.each( - matchDomains, - )('%s matches rejects a needle shorter than token_length before encrypting', async (eqlType) => { - const { ops, encryptQuery } = setup() - await expect(ops.matches(matrixColumn(eqlType), 'ad')).rejects.toThrow( - /at least 3 characters/, - ) - await expect(ops.matches(matrixColumn(eqlType), '')).rejects.toThrow( - EncryptionOperatorError, - ) - expect(encryptQuery).not.toHaveBeenCalled() - }) + it.each(matchDomains)( + '%s matches rejects a needle shorter than token_length before encrypting', + async (eqlType) => { + const { ops, encryptQuery } = setup() + await expect(ops.matches(matrixColumn(eqlType), 'ad')).rejects.toThrow( + /at least 3 characters/, + ) + await expect(ops.matches(matrixColumn(eqlType), '')).rejects.toThrow( + EncryptionOperatorError, + ) + expect(encryptQuery).not.toHaveBeenCalled() + }, + ) it('matches accepts a needle exactly at token_length', async () => { const { ops, render } = setup() diff --git a/packages/stack-drizzle/__tests__/sql-dialect.test.ts b/packages/stack-drizzle/__tests__/sql-dialect.test.ts index bd67750b3..d9f473c9c 100644 --- a/packages/stack-drizzle/__tests__/sql-dialect.test.ts +++ b/packages/stack-drizzle/__tests__/sql-dialect.test.ts @@ -81,15 +81,18 @@ describe('v3Dialect', () => { ], // `$1` in a title is consumed by vitest as an index into the case tuple, // so it cannot be used to name the SQL placeholder here. - ])('%s binds a hostile operand as a positional parameter', (_name, build) => { - const query = renderFull(build()) - - expect(query.params).toEqual([hostile]) - expect(query.sql).toContain('$1') - // The raw value must appear nowhere in the SQL text. - expect(query.sql).not.toContain('OR 1=1') - expect(query.sql).not.toContain('back\\slash') - }) + ])( + '%s binds a hostile operand as a positional parameter', + (_name, build) => { + const query = renderFull(build()) + + expect(query.params).toEqual([hostile]) + expect(query.sql).toContain('$1') + // The raw value must appear nowhere in the SQL text. + expect(query.sql).not.toContain('OR 1=1') + expect(query.sql).not.toContain('back\\slash') + }, + ) it('range binds both bounds positionally, min first', () => { const query = renderFull( diff --git a/packages/stack-drizzle/__tests__/types.test.ts b/packages/stack-drizzle/__tests__/types.test.ts index b82251a9a..c9b0e0ee9 100644 --- a/packages/stack-drizzle/__tests__/types.test.ts +++ b/packages/stack-drizzle/__tests__/types.test.ts @@ -8,15 +8,16 @@ describe('v3 drizzle types namespace', () => { expect(Object.keys(types).sort()).toEqual(Object.keys(v3Types).sort()) }) - it.each( - Object.entries(types), - )('%s mirrors the authoring DSL and recovers the concrete eql type', (factoryName, factory) => { - const drizzleColumn = factory(factoryName) - const authoredColumn = - v3Types[factoryName as keyof typeof v3Types](factoryName) + it.each(Object.entries(types))( + '%s mirrors the authoring DSL and recovers the concrete eql type', + (factoryName, factory) => { + const drizzleColumn = factory(factoryName) + const authoredColumn = + v3Types[factoryName as keyof typeof v3Types](factoryName) - expect(getEqlV3Column(factoryName, drizzleColumn)?.getEqlType()).toBe( - authoredColumn.getEqlType(), - ) - }) + expect(getEqlV3Column(factoryName, drizzleColumn)?.getEqlType()).toBe( + authoredColumn.getEqlType(), + ) + }, + ) }) diff --git a/packages/stack-drizzle/integration/null-persistence.integration.test.ts b/packages/stack-drizzle/integration/null-persistence.integration.test.ts index 76c2bbc46..098b792ac 100644 --- a/packages/stack-drizzle/integration/null-persistence.integration.test.ts +++ b/packages/stack-drizzle/integration/null-persistence.integration.test.ts @@ -141,42 +141,54 @@ afterAll(async () => { }, 30000) describe('v3 drizzle NULL persistence across tiers (live pg)', () => { - it.each(TIERS)('$domain isNull selects the NULL row', async (tier) => { - expect(await selectRowKeys(ops.isNull(columnFor(tier.key)))).toEqual([ - ROW_A, - ]) - }, 30000) - - it.each(TIERS)('$domain isNotNull selects the present row', async (tier) => { - expect(await selectRowKeys(ops.isNotNull(columnFor(tier.key)))).toEqual([ - ROW_B, - ]) - }, 30000) - - it.each( - TIERS, - )('$domain stores a real NULL for the null row', async (tier) => { - const [row] = await sqlClient.unsafe>( - `SELECT "${tier.db}"::jsonb AS value FROM ${TABLE_NAME} + it.each(TIERS)( + '$domain isNull selects the NULL row', + async (tier) => { + expect(await selectRowKeys(ops.isNull(columnFor(tier.key)))).toEqual([ + ROW_A, + ]) + }, + 30000, + ) + + it.each(TIERS)( + '$domain isNotNull selects the present row', + async (tier) => { + expect(await selectRowKeys(ops.isNotNull(columnFor(tier.key)))).toEqual([ + ROW_B, + ]) + }, + 30000, + ) + + it.each(TIERS)( + '$domain stores a real NULL for the null row', + async (tier) => { + const [row] = await sqlClient.unsafe>( + `SELECT "${tier.db}"::jsonb AS value FROM ${TABLE_NAME} WHERE test_run_id = $1 AND row_key = $2`, - [RUN, ROW_A], - ) - // Without this, a missing fixture makes `row.value` raise a TypeError and - // the failure reads as a null-handling bug rather than an absent row. - expect(row).toBeDefined() - expect(row.value).toBeNull() - }, 30000) - - it.each( - TIERS, - )('$domain present cell decrypts to its plaintext', async (tier) => { - const [row] = await sqlClient.unsafe>( - `SELECT "${tier.db}"::jsonb AS value FROM ${TABLE_NAME} + [RUN, ROW_A], + ) + // Without this, a missing fixture makes `row.value` raise a TypeError and + // the failure reads as a null-handling bug rather than an absent row. + expect(row).toBeDefined() + expect(row.value).toBeNull() + }, + 30000, + ) + + it.each(TIERS)( + '$domain present cell decrypts to its plaintext', + async (tier) => { + const [row] = await sqlClient.unsafe>( + `SELECT "${tier.db}"::jsonb AS value FROM ${TABLE_NAME} WHERE test_run_id = $1 AND row_key = $2`, - [RUN, ROW_B], - ) - expect(row.value).toHaveProperty('c') - const decrypted = unwrap(await client.decrypt(row.value as never)) - expect(decrypted).toBe(tier.sample) - }, 30000) + [RUN, ROW_B], + ) + expect(row.value).toHaveProperty('c') + const decrypted = unwrap(await client.decrypt(row.value as never)) + expect(decrypted).toBe(tier.sample) + }, + 30000, + ) }) diff --git a/packages/stack-prisma/test/v3/operator-lowering-v3-order-range.test.ts b/packages/stack-prisma/test/v3/operator-lowering-v3-order-range.test.ts index 09b53b15f..82394449c 100644 --- a/packages/stack-prisma/test/v3/operator-lowering-v3-order-range.test.ts +++ b/packages/stack-prisma/test/v3/operator-lowering-v3-order-range.test.ts @@ -41,22 +41,27 @@ describe('cipherstash v3 operator lowering — comparisons', () => { ['eqlGte', 'gte'], ['eqlLt', 'lt'], ['eqlLte', 'lte'], - ] as const)('%s lowers to eql_v3.%s(col, $1::eql_v3.query_integer_ord)', (method, fn) => { - const predicate = callOperator( - getOperator(method), - columnAccessorV3(TABLE, 'score', INTEGER_ORD_CODEC_ID), - 10, - ) - const lowered = makeV3Adapter().lower(selectWithWhere(predicate), { - contract: contractV3, - }) - expect(lowered.sql).toBe( - `SELECT "user"."id" AS "id" FROM "user" WHERE eql_v3.${fn}("user"."score", $1::eql_v3.query_integer_ord)`, - ) - const envelope = literalParamValue(lowered.params[0]) - expect(envelope).toBeInstanceOf(EncryptedNumber) - expect(v3QueryTermTypeOf(envelope as EncryptedNumber)).toBe('orderAndRange') - }) + ] as const)( + '%s lowers to eql_v3.%s(col, $1::eql_v3.query_integer_ord)', + (method, fn) => { + const predicate = callOperator( + getOperator(method), + columnAccessorV3(TABLE, 'score', INTEGER_ORD_CODEC_ID), + 10, + ) + const lowered = makeV3Adapter().lower(selectWithWhere(predicate), { + contract: contractV3, + }) + expect(lowered.sql).toBe( + `SELECT "user"."id" AS "id" FROM "user" WHERE eql_v3.${fn}("user"."score", $1::eql_v3.query_integer_ord)`, + ) + const envelope = literalParamValue(lowered.params[0]) + expect(envelope).toBeInstanceOf(EncryptedNumber) + expect(v3QueryTermTypeOf(envelope as EncryptedNumber)).toBe( + 'orderAndRange', + ) + }, + ) it('casts to the ord_ore query domain on a block-ORE column (bigint envelope dispatch)', () => { const predicate = callOperator( diff --git a/packages/stack-supabase/__tests__/supabase-helpers.test.ts b/packages/stack-supabase/__tests__/supabase-helpers.test.ts index 52aa1a73e..08db16385 100644 --- a/packages/stack-supabase/__tests__/supabase-helpers.test.ts +++ b/packages/stack-supabase/__tests__/supabase-helpers.test.ts @@ -245,21 +245,19 @@ describe('parseOrStringWithSpans / rebuildOrString round-trip', () => { // The emit side must never produce a string its own parser mis-reads. A scalar // brace was the one character that escaped quoting, so rebuild → parse dropped // the condition behind it. - it.each([ - 'a{b', - 'a}b', - 'a(b', - 'a)b', - ])('round-trips a scalar value containing %s', (value) => { - const conditions = [ - { column: 'note', op: 'eq', negate: false, value }, - { column: 'id', op: 'eq', negate: false, value: '7' }, - ] - const s = rebuildOrString( - conditions.map((c) => cond(c.column, c.op, c.value)), - ) - expect(parseConditions(s)).toEqual(conditions) - }) + it.each(['a{b', 'a}b', 'a(b', 'a)b'])( + 'round-trips a scalar value containing %s', + (value) => { + const conditions = [ + { column: 'note', op: 'eq', negate: false, value }, + { column: 'id', op: 'eq', negate: false, value: '7' }, + ] + const s = rebuildOrString( + conditions.map((c) => cond(c.column, c.op, c.value)), + ) + expect(parseConditions(s)).toEqual(conditions) + }, + ) }) // --------------------------------------------------------------------------- @@ -329,20 +327,18 @@ describe('parseOrStringWithSpans structural characters inside values', () => { // column ahead of the literal and a plaintext condition behind it — the shape // that actually reaches `rebuildOrString`, since the encrypted `email` is what // forces the group to be rebuilt rather than forwarded verbatim. - it.each([ - '}', - '{', - ')', - '(', - ])('keeps a quoted %s inside a jsonb literal out of the depth count', (char) => { - expect( - parseConditions(`email.eq.x,meta.cs.{"a":"${char}"},note.eq.y`), - ).toEqual([ - { column: 'email', op: 'eq', negate: false, value: 'x' }, - { column: 'meta', op: 'cs', negate: false, value: `{"a":"${char}"}` }, - { column: 'note', op: 'eq', negate: false, value: 'y' }, - ]) - }) + it.each(['}', '{', ')', '('])( + 'keeps a quoted %s inside a jsonb literal out of the depth count', + (char) => { + expect( + parseConditions(`email.eq.x,meta.cs.{"a":"${char}"},note.eq.y`), + ).toEqual([ + { column: 'email', op: 'eq', negate: false, value: 'x' }, + { column: 'meta', op: 'cs', negate: false, value: `{"a":"${char}"}` }, + { column: 'note', op: 'eq', negate: false, value: 'y' }, + ]) + }, + ) it('keeps an escaped quote inside a jsonb value opaque', () => { // `\"` must not close the element, or the `}` behind it decrements depth. @@ -718,22 +714,18 @@ describe('parseOrStringWithSpans in-list elements', () => { // The range operators take a paren-delimited operand too. Excluding them would // re-emit `period.ov.(1,10)` as a quoted scalar — a wire-format change on a // plaintext column that merely shares an `.or()` with an encrypted one. - it.each([ - 'ov', - 'sl', - 'sr', - 'nxr', - 'nxl', - 'adj', - ])('round-trips a paren-delimited %s operand', (op) => { - const s = `period.${op}.(1,10)` - expect(parseConditions(s)).toEqual([ - { column: 'period', op, negate: false, value: ['1', '10'] }, - ]) - expect( - rebuildOrString(parseOrStringWithSpans(s) as DbPendingOrCondition[]), - ).toBe(s) - }) + it.each(['ov', 'sl', 'sr', 'nxr', 'nxl', 'adj'])( + 'round-trips a paren-delimited %s operand', + (op) => { + const s = `period.${op}.(1,10)` + expect(parseConditions(s)).toEqual([ + { column: 'period', op, negate: false, value: ['1', '10'] }, + ]) + expect( + rebuildOrString(parseOrStringWithSpans(s) as DbPendingOrCondition[]), + ).toBe(s) + }, + ) }) // PostgREST reads a bare `null` / `true` / `false` operand as the SQL value, not diff --git a/packages/stack/__tests__/cjs-require.test.ts b/packages/stack/__tests__/cjs-require.test.ts index 0b530fd58..23be34a17 100644 --- a/packages/stack/__tests__/cjs-require.test.ts +++ b/packages/stack/__tests__/cjs-require.test.ts @@ -106,62 +106,64 @@ describe('CJS consumers can require the built bundles', () => { ).not.toThrow() }) - it.each( - cjsEntries, - )('dist bundle does not externalize an ESM-only module: %s', (entry) => { - const bundlePath = path.join(packageRoot, entry) - const source = readFileSync(bundlePath, 'utf8') + it.each(cjsEntries)( + 'dist bundle does not externalize an ESM-only module: %s', + (entry) => { + const bundlePath = path.join(packageRoot, entry) + const source = readFileSync(bundlePath, 'utf8') - // Match `require("foo")` or `require('foo')` where `foo` is a bare - // specifier (not a relative path or `node:` builtin) — those are the - // cases that hit Node's CJS module resolver at runtime. - const requireRegex = /\brequire\(\s*['"]([^'".\\/][^'"]*)['"]\s*\)/g - const externalized = new Set() - for (const match of source.matchAll(requireRegex)) { - externalized.add(match[1]) - } + // Match `require("foo")` or `require('foo')` where `foo` is a bare + // specifier (not a relative path or `node:` builtin) — those are the + // cases that hit Node's CJS module resolver at runtime. + const requireRegex = /\brequire\(\s*['"]([^'".\\/][^'"]*)['"]\s*\)/g + const externalized = new Set() + for (const match of source.matchAll(requireRegex)) { + externalized.add(match[1]) + } - for (const dep of ESM_ONLY_DEPENDENCIES) { - expect( - externalized.has(dep), - `${entry} externalizes "${dep}" via require(), which is ESM-only and will crash CJS consumers with ERR_REQUIRE_ESM. Add it to noExternal in packages/stack/tsup.config.ts.`, - ).toBe(false) - } - }) + for (const dep of ESM_ONLY_DEPENDENCIES) { + expect( + externalized.has(dep), + `${entry} externalizes "${dep}" via require(), which is ESM-only and will crash CJS consumers with ERR_REQUIRE_ESM. Add it to noExternal in packages/stack/tsup.config.ts.`, + ).toBe(false) + } + }, + ) - it.each( - cjsEntries, - )('CJS bundle loads in a real Node CJS process: %s', (entry) => { - const bundlePath = path.join(packageRoot, entry) - // Spawn a fresh Node process so this is a true CJS load — vitest's - // own module graph and any test-time aliasing don't apply. - // `--no-experimental-require-module` forces the legacy CJS-cannot- - // require-ESM behavior, reproducing the exact ERR_REQUIRE_ESM that - // downstream apps hit in production runtimes that don't enable - // require(esm) (Node <22.12, Bun, Deno-as-Node, locked-down CI). - try { - execFileSync( - process.execPath, - [ - '--no-experimental-require-module', - '-e', - `require(${JSON.stringify(bundlePath)})`, - ], - { - cwd: packageRoot, - stdio: 'pipe', - encoding: 'utf8', - }, - ) - } catch (err) { - const e = err as NodeJS.ErrnoException & { - stderr?: string - stdout?: string + it.each(cjsEntries)( + 'CJS bundle loads in a real Node CJS process: %s', + (entry) => { + const bundlePath = path.join(packageRoot, entry) + // Spawn a fresh Node process so this is a true CJS load — vitest's + // own module graph and any test-time aliasing don't apply. + // `--no-experimental-require-module` forces the legacy CJS-cannot- + // require-ESM behavior, reproducing the exact ERR_REQUIRE_ESM that + // downstream apps hit in production runtimes that don't enable + // require(esm) (Node <22.12, Bun, Deno-as-Node, locked-down CI). + try { + execFileSync( + process.execPath, + [ + '--no-experimental-require-module', + '-e', + `require(${JSON.stringify(bundlePath)})`, + ], + { + cwd: packageRoot, + stdio: 'pipe', + encoding: 'utf8', + }, + ) + } catch (err) { + const e = err as NodeJS.ErrnoException & { + stderr?: string + stdout?: string + } + throw new Error( + `require("${entry}") failed in a Node CJS process:\n` + + `${e.stderr ?? ''}\n${e.stdout ?? ''}`, + ) } - throw new Error( - `require("${entry}") failed in a Node CJS process:\n` + - `${e.stderr ?? ''}\n${e.stdout ?? ''}`, - ) - } - }) + }, + ) }) diff --git a/packages/stack/__tests__/diagnostics-entry.test.ts b/packages/stack/__tests__/diagnostics-entry.test.ts index b9d5886c7..22089701f 100644 --- a/packages/stack/__tests__/diagnostics-entry.test.ts +++ b/packages/stack/__tests__/diagnostics-entry.test.ts @@ -256,22 +256,25 @@ describe('@cipherstash/stack/diagnostics', () => { it.each([ ['require', false], ['import', true], - ])('probes through an export the published protect-ffi has, so %s survives it', (_label, esm) => { - const call = `let raised = 'none' + ])( + 'probes through an export the published protect-ffi has, so %s survives it', + (_label, esm) => { + const call = `let raised = 'none' try { d.assertNativeBindingAvailable() } catch (e) { raised = e.message } process.stdout.write('imported ' + raised)` - const script = esm - ? `const d = await import('@cipherstash/stack/diagnostics')\n${call}` - : `const d = require('@cipherstash/stack/diagnostics')\n${call}` + const script = esm + ? `const d = await import('@cipherstash/stack/diagnostics')\n${call}` + : `const d = require('@cipherstash/stack/diagnostics')\n${call}` - const output = runNode(script, { esm, publishedFfi: true }) + const output = runNode(script, { esm, publishedFfi: true }) - // Reaching stdout at all means the entry loaded against that surface. - expect(output).toMatch(/^imported /) - // And that the probe ran, rather than resolving to `undefined` and - // throwing a TypeError the CLI would report as an unknown failure. - expect(output).toContain(PUBLISHED_SURFACE_MARKER) - }) + // Reaching stdout at all means the entry loaded against that surface. + expect(output).toMatch(/^imported /) + // And that the probe ran, rather than resolving to `undefined` and + // throwing a TypeError the CLI would report as an unknown failure. + expect(output).toContain(PUBLISHED_SURFACE_MARKER) + }, + ) it('is importable from both module systems', () => { expect( @@ -298,28 +301,31 @@ describe('@cipherstash/stack/diagnostics', () => { it.each([ ['require', false], ['import', true], - ])('with no binding installed, %s succeeds and only the call throws', (_label, esm) => { - const script = esm - ? `const d = await import('@cipherstash/stack/diagnostics') + ])( + 'with no binding installed, %s succeeds and only the call throws', + (_label, esm) => { + const script = esm + ? `const d = await import('@cipherstash/stack/diagnostics') let raised = 'none' try { d.assertNativeBindingAvailable() } catch (e) { raised = e.code + ' ' + e.message.split('\\n')[0] } process.stdout.write('imported ' + raised)` - : `const d = require('@cipherstash/stack/diagnostics') + : `const d = require('@cipherstash/stack/diagnostics') let raised = 'none' try { d.assertNativeBindingAvailable() } catch (e) { raised = e.code + ' ' + e.message.split('\\n')[0] } process.stdout.write('imported ' + raised)` - const output = runNode(script, { esm, hideBinding: true }) + const output = runNode(script, { esm, hideBinding: true }) - // "imported" at all means the import did not throw — the property under - // test. A child that died during import produces no stdout and fails the - // execFileSync above. - expect(output).toMatch(/^imported /) - // Unwrapped: same `code` and the platform package by name, which is what - // `packages/cli/src/native.ts` classifies on. - expect(output).toContain('MODULE_NOT_FOUND') - expect(output).toMatch( - /Cannot find module '@cipherstash\/protect-ffi-(darwin|linux|win32)-/, - ) - }) + // "imported" at all means the import did not throw — the property under + // test. A child that died during import produces no stdout and fails the + // execFileSync above. + expect(output).toMatch(/^imported /) + // Unwrapped: same `code` and the platform package by name, which is what + // `packages/cli/src/native.ts` classifies on. + expect(output).toContain('MODULE_NOT_FOUND') + expect(output).toMatch( + /Cannot find module '@cipherstash\/protect-ffi-(darwin|linux|win32)-/, + ) + }, + ) }) diff --git a/packages/stack/__tests__/dynamodb/v2-type-coverage.test.ts b/packages/stack/__tests__/dynamodb/v2-type-coverage.test.ts index 0d4f0bd6b..73e5e5a52 100644 --- a/packages/stack/__tests__/dynamodb/v2-type-coverage.test.ts +++ b/packages/stack/__tests__/dynamodb/v2-type-coverage.test.ts @@ -326,21 +326,22 @@ describe('a legacy DynamoDB read reconstructs every plaintext axis', () => { } }) - it.each( - matrixCases, - )('%s: rebuilds a v2 envelope and reconstructs the plaintext', (label, slug, castAs, sample, row) => { - // THE guard for this file. Rebuilt as v2 — `k: 'ct'` and `v: 2` — around - // the CURRENT v3 descriptor's identifier, carrying the stored ciphertext - // untouched. Drop the version branch and this goes red per domain. - expect(handedToFfi[row]?.[slug]).toEqual({ - i: { c: slug, t: matrix.tableName }, - v: 2, - k: 'ct', - c: ciphertext({ slug, row }), - }) + it.each(matrixCases)( + '%s: rebuilds a v2 envelope and reconstructs the plaintext', + (label, slug, castAs, sample, row) => { + // THE guard for this file. Rebuilt as v2 — `k: 'ct'` and `v: 2` — around + // the CURRENT v3 descriptor's identifier, carrying the stored ciphertext + // untouched. Drop the version branch and this goes red per domain. + expect(handedToFfi[row]?.[slug]).toEqual({ + i: { c: slug, t: matrix.tableName }, + v: 2, + k: 'ct', + c: ciphertext({ slug, row }), + }) - expectReconstructed(decrypted[row]?.[slug], castAs, sample, label) - }) + expectReconstructed(decrypted[row]?.[slug], castAs, sample, label) + }, + ) /** * `decryptModel` and `bulkDecryptModels` are separate wrappers over the same diff --git a/packages/stack/__tests__/encrypt-lock-context-guards.test.ts b/packages/stack/__tests__/encrypt-lock-context-guards.test.ts index ec8dd92df..0df450196 100644 --- a/packages/stack/__tests__/encrypt-lock-context-guards.test.ts +++ b/packages/stack/__tests__/encrypt-lock-context-guards.test.ts @@ -80,50 +80,57 @@ const clientFor = (variant: 'v2' | 'v3'): EncryptionClient => describe.each([ ['v2', { column: users.score, table: users }], ['v3', { column: usersV3.score, table: usersV3 }], -] as const)('encrypt with lock context rejects non-finite numbers (%s domain)', (variant, target) => { - it('rejects NaN and never reaches the FFI', async () => { - const result = await clientFor(variant) - .encrypt(Number.NaN, target) - .withLockContext(new LockContext()) - - expect(failure(result)).toBeDefined() - expect(failure(result)?.message).toContain('Cannot encrypt NaN value') - expect(vi.mocked(ffi.encrypt)).not.toHaveBeenCalled() - }) - - it('rejects +Infinity and never reaches the FFI', async () => { - const result = await clientFor(variant) - .encrypt(Number.POSITIVE_INFINITY, target) - .withLockContext(new LockContext()) - - expect(failure(result)).toBeDefined() - expect(failure(result)?.message).toContain('Cannot encrypt Infinity value') - expect(vi.mocked(ffi.encrypt)).not.toHaveBeenCalled() - }) - - it('rejects -Infinity and never reaches the FFI', async () => { - const result = await clientFor(variant) - .encrypt(Number.NEGATIVE_INFINITY, target) - .withLockContext(new LockContext()) - - expect(failure(result)).toBeDefined() - expect(failure(result)?.message).toContain('Cannot encrypt Infinity value') - expect(vi.mocked(ffi.encrypt)).not.toHaveBeenCalled() - }) - - it('accepts a finite number and forwards it to the FFI', async () => { - // Positive control: proves the guards above reject *because* of the value, - // not because the lock-context path is broken for all numbers. - const result = await clientFor(variant) - .encrypt(42, target) - .withLockContext(new LockContext()) - - expect(failure(result)).toBeUndefined() - expect(vi.mocked(ffi.encrypt)).toHaveBeenCalledTimes(1) - const opts = vi.mocked(ffi.encrypt).mock.calls[0][1] - expect(opts.plaintext).toBe(42) - }) -}) +] as const)( + 'encrypt with lock context rejects non-finite numbers (%s domain)', + (variant, target) => { + it('rejects NaN and never reaches the FFI', async () => { + const result = await clientFor(variant) + .encrypt(Number.NaN, target) + .withLockContext(new LockContext()) + + expect(failure(result)).toBeDefined() + expect(failure(result)?.message).toContain('Cannot encrypt NaN value') + expect(vi.mocked(ffi.encrypt)).not.toHaveBeenCalled() + }) + + it('rejects +Infinity and never reaches the FFI', async () => { + const result = await clientFor(variant) + .encrypt(Number.POSITIVE_INFINITY, target) + .withLockContext(new LockContext()) + + expect(failure(result)).toBeDefined() + expect(failure(result)?.message).toContain( + 'Cannot encrypt Infinity value', + ) + expect(vi.mocked(ffi.encrypt)).not.toHaveBeenCalled() + }) + + it('rejects -Infinity and never reaches the FFI', async () => { + const result = await clientFor(variant) + .encrypt(Number.NEGATIVE_INFINITY, target) + .withLockContext(new LockContext()) + + expect(failure(result)).toBeDefined() + expect(failure(result)?.message).toContain( + 'Cannot encrypt Infinity value', + ) + expect(vi.mocked(ffi.encrypt)).not.toHaveBeenCalled() + }) + + it('accepts a finite number and forwards it to the FFI', async () => { + // Positive control: proves the guards above reject *because* of the value, + // not because the lock-context path is broken for all numbers. + const result = await clientFor(variant) + .encrypt(42, target) + .withLockContext(new LockContext()) + + expect(failure(result)).toBeUndefined() + expect(vi.mocked(ffi.encrypt)).toHaveBeenCalledTimes(1) + const opts = vi.mocked(ffi.encrypt).mock.calls[0][1] + expect(opts.plaintext).toBe(42) + }) + }, +) // The `bigint` analog of the NaN/±Infinity guard above. `bigint` domains map to // Postgres `int8`, so a plaintext outside the signed 64-bit range cannot be @@ -144,26 +151,32 @@ describe('encrypt rejects out-of-range bigint (i64 bounds)', () => { ['i64::MIN', INT64_MIN], ['zero', 0n], ['a negative', -42n], - ] as const)('accepts in-range bigint %s and forwards it to the FFI', async (_label, value) => { - const result = await clientV3.encrypt(value, target) + ] as const)( + 'accepts in-range bigint %s and forwards it to the FFI', + async (_label, value) => { + const result = await clientV3.encrypt(value, target) - expect(failure(result)).toBeUndefined() - expect(vi.mocked(ffi.encrypt)).toHaveBeenCalledTimes(1) - expect(vi.mocked(ffi.encrypt).mock.calls[0][1].plaintext).toBe(value) - }) + expect(failure(result)).toBeUndefined() + expect(vi.mocked(ffi.encrypt)).toHaveBeenCalledTimes(1) + expect(vi.mocked(ffi.encrypt).mock.calls[0][1].plaintext).toBe(value) + }, + ) it.each([ ['2^63 (i64::MAX + 1)', 9223372036854775808n], ['-(2^63) - 1 (i64::MIN - 1)', -9223372036854775809n], - ] as const)('rejects out-of-range bigint %s and never reaches the FFI', async (_label, value) => { - const result = await clientV3.encrypt(value, target) - - expect(failure(result)).toBeDefined() - expect(failure(result)?.message).toContain( - 'Cannot encrypt bigint value out of int64 range', - ) - expect(vi.mocked(ffi.encrypt)).not.toHaveBeenCalled() - }) + ] as const)( + 'rejects out-of-range bigint %s and never reaches the FFI', + async (_label, value) => { + const result = await clientV3.encrypt(value, target) + + expect(failure(result)).toBeDefined() + expect(failure(result)?.message).toContain( + 'Cannot encrypt bigint value out of int64 range', + ) + expect(vi.mocked(ffi.encrypt)).not.toHaveBeenCalled() + }, + ) it('rejects out-of-range bigint on the lock-context arm too', async () => { const result = await clientV3 diff --git a/packages/stack/__tests__/encrypt-query-searchable-json.test.ts b/packages/stack/__tests__/encrypt-query-searchable-json.test.ts index d65e791ed..51074cd53 100644 --- a/packages/stack/__tests__/encrypt-query-searchable-json.test.ts +++ b/packages/stack/__tests__/encrypt-query-searchable-json.test.ts @@ -48,26 +48,31 @@ describe('encryptQuery with searchableJson', () => { { role: 'admin' }, { user: { profile: { role: 'admin' } } }, ['admin', 'user'], - ])('uses default structural containment for %j', async (value) => { - const result = await client.encryptQuery(value, { - column: documents.metadata, - table: documents, - queryType: 'searchableJson', - }) - expectContainment(unwrapResult(result)) - }, 30000) + ])( + 'uses default structural containment for %j', + async (value) => { + const result = await client.encryptQuery(value, { + column: documents.metadata, + table: documents, + queryType: 'searchableJson', + }) + expectContainment(unwrapResult(result)) + }, + 30000, + ) - it.each([ - 42, - true, - ])('rejects a top-level JSON scalar containment needle (%j)', async (value) => { - const result = await client.encryptQuery(value, { - column: documents.metadata, - table: documents, - queryType: 'searchableJson', - }) - expectFailure(result) - }, 30000) + it.each([42, true])( + 'rejects a top-level JSON scalar containment needle (%j)', + async (value) => { + const result = await client.encryptQuery(value, { + column: documents.metadata, + table: documents, + queryType: 'searchableJson', + }) + expectFailure(result) + }, + 30000, + ) it('infers the same operation when queryType is omitted', async () => { const explicit = await client.encryptQuery( @@ -108,21 +113,22 @@ describe('encryptQuery with searchableJson', () => { expectContainment(data[1]) }, 30000) - it.each([ - 'composite-literal', - 'escaped-composite-literal', - ] as const)('supports %s formatting for containment needles', async (returnType) => { - const result = await client.encryptQuery( - { role: 'admin' }, - { - column: documents.metadata, - table: documents, - queryType: 'searchableJson', - returnType, - }, - ) - expect(typeof unwrapResult(result)).toBe('string') - }, 30000) + it.each(['composite-literal', 'escaped-composite-literal'] as const)( + 'supports %s formatting for containment needles', + async (returnType) => { + const result = await client.encryptQuery( + { role: 'admin' }, + { + column: documents.metadata, + table: documents, + queryType: 'searchableJson', + returnType, + }, + ) + expect(typeof unwrapResult(result)).toBe('string') + }, + 30000, + ) it('supports lock-context chaining for JSON query terms', async () => { const operation = client.encryptQuery( diff --git a/packages/stack/__tests__/encrypt-query-stevec.test.ts b/packages/stack/__tests__/encrypt-query-stevec.test.ts index 894592878..7695ddc5c 100644 --- a/packages/stack/__tests__/encrypt-query-stevec.test.ts +++ b/packages/stack/__tests__/encrypt-query-stevec.test.ts @@ -63,29 +63,31 @@ describe('encryptQuery with protect-ffi 0.30 SteVec operations', () => { expectQueryJson(unwrapResult(result)) }, 30000) - it.each([ - 'zoe', - 42, - ])('returns a ciphertext-free selector ordering term for %j', async (value) => { - const result = await client.encryptQuery(value, { - column: documents.metadata, - table: documents, - queryType: 'steVecTerm', - }) - expectOrderingTerm(unwrapResult(result)) - }, 30000) + it.each(['zoe', 42])( + 'returns a ciphertext-free selector ordering term for %j', + async (value) => { + const result = await client.encryptQuery(value, { + column: documents.metadata, + table: documents, + queryType: 'steVecTerm', + }) + expectOrderingTerm(unwrapResult(result)) + }, + 30000, + ) - it.each([ - { role: 'admin' }, - ['admin', 'user'], - ])('uses default structural containment for %j', async (value) => { - const result = await client.encryptQuery(value, { - column: documents.metadata, - table: documents, - queryType: 'searchableJson', - }) - expectQueryJson(unwrapResult(result)) - }, 30000) + it.each([{ role: 'admin' }, ['admin', 'user']])( + 'uses default structural containment for %j', + async (value) => { + const result = await client.encryptQuery(value, { + column: documents.metadata, + table: documents, + queryType: 'searchableJson', + }) + expectQueryJson(unwrapResult(result)) + }, + 30000, + ) it('infers selector versus containment when queryType is omitted', async () => { const selector = await client.encryptQuery('$.user.email', { diff --git a/packages/stack/__tests__/match-bloom-live.test.ts b/packages/stack/__tests__/match-bloom-live.test.ts index e139b0e94..79369c5de 100644 --- a/packages/stack/__tests__/match-bloom-live.test.ts +++ b/packages/stack/__tests__/match-bloom-live.test.ts @@ -89,19 +89,19 @@ describe('protect-ffi match bloom', () => { // config and ignored: the bloom is the tokenizer's trigrams, nothing more. // If a future ffi starts honouring the flag, THIS fails — and the v3 domains // already emit `false` (see `eql/v3/columns.ts`), so `contains` keeps working. - it.each([ - 'Ada Lovelace', - 'ada@example.com', - ])('emits the same bloom for %j whether include_original is on or off', async (plaintext) => { - const on = await bloomOf(withOriginal, plaintext) - const off = await bloomOf(withoutOriginal, plaintext) - - expect(sorted(on)).toEqual(sorted(off)) - // A trigram-only bloom, not one carrying an extra whole-value token: 6 - // bits (k) per DISTINCT trigram, so never more than 6 × the trigram count. - expect(on.length).toBeLessThanOrEqual(6 * (plaintext.length - 2)) - expect(on.length).toBeGreaterThan(0) - }) + it.each(['Ada Lovelace', 'ada@example.com'])( + 'emits the same bloom for %j whether include_original is on or off', + async (plaintext) => { + const on = await bloomOf(withOriginal, plaintext) + const off = await bloomOf(withoutOriginal, plaintext) + + expect(sorted(on)).toEqual(sorted(off)) + // A trigram-only bloom, not one carrying an extra whole-value token: 6 + // bits (k) per DISTINCT trigram, so never more than 6 × the trigram count. + expect(on.length).toBeLessThanOrEqual(6 * (plaintext.length - 2)) + expect(on.length).toBeGreaterThan(0) + }, + ) // The corollary, and the reason `matchNeedleError` must reject short needles // instead of trusting `include_original` to make them matchable: below @@ -110,11 +110,14 @@ describe('protect-ffi match bloom', () => { it.each([ ['with include_original', true], ['without include_original', false], - ] as const)('blooms a sub-trigram value to nothing, %s', async (_label, on) => { - const bloom = await bloomOf(on ? withOriginal : withoutOriginal, 'ad') + ] as const)( + 'blooms a sub-trigram value to nothing, %s', + async (_label, on) => { + const bloom = await bloomOf(on ? withOriginal : withoutOriginal, 'ad') - expect(bloom).toEqual([]) - }) + expect(bloom).toEqual([]) + }, + ) // protect-ffi 0.30 closes the core fail-open path: query encryption itself // rejects empty-token needles, so callers that bypass every ORM guard still @@ -122,11 +125,14 @@ describe('protect-ffi match bloom', () => { it.each([ [1, 'a'], [2, 'ad'], - ] as const)('rejects the %i-character query needle in the core FFI', async (_length, needle) => { - await expect(queryBloomOf(withOriginal, needle)).rejects.toThrow( - /short|token|3|ngram/i, - ) - }) + ] as const)( + 'rejects the %i-character query needle in the core FFI', + async (_length, needle) => { + await expect(queryBloomOf(withOriginal, needle)).rejects.toThrow( + /short|token|3|ngram/i, + ) + }, + ) // The query needle's bloom is a subset of the STORED value's bloom — which is // exactly what `eql_v3.matches` (`match_term(stored) @> query_term(needle)`) diff --git a/packages/stack/__tests__/schema-v3.test.ts b/packages/stack/__tests__/schema-v3.test.ts index 56f66f5e8..3c4d46192 100644 --- a/packages/stack/__tests__/schema-v3.test.ts +++ b/packages/stack/__tests__/schema-v3.test.ts @@ -185,13 +185,16 @@ describe('eql_v3 encryptedTable', () => { 'toString', 'valueOf', 'hasOwnProperty', - ])('throws when a column name (%s) collides with a reserved property', (reserved) => { - expect(() => - encryptedTable('users', { - [reserved]: types.TextSearch(reserved), - }), - ).toThrow(/reserved EncryptedTable property/) - }) + ])( + 'throws when a column name (%s) collides with a reserved property', + (reserved) => { + expect(() => + encryptedTable('users', { + [reserved]: types.TextSearch(reserved), + }), + ).toThrow(/reserved EncryptedTable property/) + }, + ) it('build() assembles { tableName, columns } with built column configs', () => { const users = encryptedTable('users', { @@ -364,28 +367,34 @@ describe('eql_v3 catalog-driven query capability sweep', () => { (queryType) => [eqlType, spec, queryType] as const, ), ), - )('%s + queryType=%s: gating matches configured indexes', (_eqlType, spec, queryType) => { - const col = spec.builder('value') - if (queryTypeAllowed(spec.indexes, queryType)) { - expect(() => resolveIndexType(col as never, queryType)).not.toThrow() - } else { - // Broad message match: for a blocked equality the resolver reports the - // missing `unique`; for orderAndRange/freeTextSearch the missing ore/match. - expect(() => resolveIndexType(col as never, queryType)).toThrow( - /not configured/, - ) - } - }) + )( + '%s + queryType=%s: gating matches configured indexes', + (_eqlType, spec, queryType) => { + const col = spec.builder('value') + if (queryTypeAllowed(spec.indexes, queryType)) { + expect(() => resolveIndexType(col as never, queryType)).not.toThrow() + } else { + // Broad message match: for a blocked equality the resolver reports the + // missing `unique`; for orderAndRange/freeTextSearch the missing ore/match. + expect(() => resolveIndexType(col as never, queryType)).toThrow( + /not configured/, + ) + } + }, + ) it.each( typedEntries(V3_MATRIX).filter( ([, spec]) => Object.keys(spec.indexes ?? {}).length === 0, ), - )('%s: querying a storage-only column with no queryType throws', (_eqlType, spec) => { - expect(() => resolveIndexType(spec.builder('value') as never)).toThrow( - /no indexes configured/, - ) - }) + )( + '%s: querying a storage-only column with no queryType throws', + (_eqlType, spec) => { + expect(() => resolveIndexType(spec.builder('value') as never)).toThrow( + /no indexes configured/, + ) + }, + ) // Spot-check the exact messages for a queryable-but-misused column, so the // broad regex above doesn't let a message regression slip through. @@ -415,11 +424,14 @@ describe('eql_v3 equality via the ordering index on order-capable columns (regre ['date_ord', types.DateOrd, 'ope'], ['numeric_ord', types.NumericOrd, 'ope'], ['IntegerOrdOre', types.IntegerOrdOre, 'ore'], - ] as const)('%s resolves equality to its ordering index (%s)', (_name, builder, ordering) => { - expect(resolveIndexType(builder('value'), 'equality')).toEqual({ - indexType: ordering, - }) - }) + ] as const)( + '%s resolves equality to its ordering index (%s)', + (_name, builder, ordering) => { + expect(resolveIndexType(builder('value'), 'equality')).toEqual({ + indexType: ordering, + }) + }, + ) it('preserves v2: an orderAndRange-only column still throws on equality (no-v2-change)', () => { // v2 EncryptedColumn has no getQueryCapabilities, so the equality-via-ORE @@ -445,29 +457,38 @@ describe('eql_v3 text order domains carry the hm (unique) index (regression)', ( { unique: { token_filters: [] }, ore: {} }, ], ['text_ord', types.TextOrd, { unique: { token_filters: [] }, ope: {} }], - ] as const)('%s emits both unique (hm) and its ordering index', (_name, builder, expected) => { - expect(builder('c').build().indexes).toStrictEqual(expected) - }) + ] as const)( + '%s emits both unique (hm) and its ordering index', + (_name, builder, expected) => { + expect(builder('c').build().indexes).toStrictEqual(expected) + }, + ) it.each([ ['IntegerOrdOre', types.IntegerOrdOre, { ore: {} }], ['IntegerOrd', types.IntegerOrd, { ope: {} }], ['date_ord_ore', types.DateOrdOre, { ore: {} }], ['numeric_ord', types.NumericOrd, { ope: {} }], - ] as const)('%s (numeric/date order) emits its ordering index only — no unique', (_name, builder, expected) => { - expect(builder('c').build().indexes).toStrictEqual(expected) - }) + ] as const)( + '%s (numeric/date order) emits its ordering index only — no unique', + (_name, builder, expected) => { + expect(builder('c').build().indexes).toStrictEqual(expected) + }, + ) // With `unique` present, text order equality resolves to the hm index (not // ORE): `resolvesEqualityViaOre` only fires when `unique` is ABSENT. it.each([ ['text_ord_ore', types.TextOrdOre], ['text_ord', types.TextOrd], - ] as const)('%s resolves equality to the unique (hm) index', (_name, builder) => { - expect(resolveIndexType(builder('value'), 'equality')).toEqual({ - indexType: 'unique', - }) - }) + ] as const)( + '%s resolves equality to the unique (hm) index', + (_name, builder) => { + expect(resolveIndexType(builder('value'), 'equality')).toEqual({ + indexType: 'unique', + }) + }, + ) }) describe('eql_v3 timestamp domains emit cast_as "timestamp" (time-of-day preserved)', () => { diff --git a/packages/stack/__tests__/selector-path.test.ts b/packages/stack/__tests__/selector-path.test.ts index b32a9fc8f..8c9ed808d 100644 --- a/packages/stack/__tests__/selector-path.test.ts +++ b/packages/stack/__tests__/selector-path.test.ts @@ -2,20 +2,19 @@ import { describe, expect, it } from 'vitest' import { unsupportedLeafReason } from '@/eql/v3/selector-path' describe('unsupportedLeafReason', () => { - it.each([ - 'value', - 42, - true, - ])('accepts the JSON scalar %j for equality', (value) => { - expect(unsupportedLeafReason(value, false)).toBeNull() - }) + it.each(['value', 42, true])( + 'accepts the JSON scalar %j for equality', + (value) => { + expect(unsupportedLeafReason(value, false)).toBeNull() + }, + ) - it.each([ - 'value', - 42, - ])('accepts the orderable JSON scalar %j for ordering', (value) => { - expect(unsupportedLeafReason(value, true)).toBeNull() - }) + it.each(['value', 42])( + 'accepts the orderable JSON scalar %j for ordering', + (value) => { + expect(unsupportedLeafReason(value, true)).toBeNull() + }, + ) it.each([ ['null', null, /non-null scalar leaf/], diff --git a/packages/stack/__tests__/v3-matrix/matrix.test.ts b/packages/stack/__tests__/v3-matrix/matrix.test.ts index 8b985fa23..c0c1a30b1 100644 --- a/packages/stack/__tests__/v3-matrix/matrix.test.ts +++ b/packages/stack/__tests__/v3-matrix/matrix.test.ts @@ -13,34 +13,38 @@ import { typedEntries, V3_MATRIX } from './catalog' describe('eql_v3 type-driven domain matrix (runtime)', () => { // `typedEntries` keeps `eqlType` as `EqlV3TypeName` rather than widening to // `string`, so the key stays precisely typed through the callback. - it.each( - typedEntries(V3_MATRIX), - )('%s: builder, eqlType, capabilities and build() are consistent', (eqlType, spec) => { - const col = spec.builder('value') + it.each(typedEntries(V3_MATRIX))( + '%s: builder, eqlType, capabilities and build() are consistent', + (eqlType, spec) => { + const col = spec.builder('value') - expect(col).toBeInstanceOf(spec.ColumnClass) - expect(col.getName()).toBe('value') - expect(col.getEqlType()).toBe(eqlType) - expect(col.getQueryCapabilities()).toStrictEqual(spec.capabilities) - expect(col.isQueryable()).toBe( - Object.values(spec.capabilities).some(Boolean), - ) + expect(col).toBeInstanceOf(spec.ColumnClass) + expect(col.getName()).toBe('value') + expect(col.getEqlType()).toBe(eqlType) + expect(col.getQueryCapabilities()).toStrictEqual(spec.capabilities) + expect(col.isQueryable()).toBe( + Object.values(spec.capabilities).some(Boolean), + ) - // Full-fidelity `build()` check: exactly `{ cast_as, indexes }`, no extra - // keys — so SDK-facing metadata (eqlType/capabilities) can never leak. - expect(col.build()).toStrictEqual({ - cast_as: spec.castAs, - indexes: spec.indexes, - }) - }) + // Full-fidelity `build()` check: exactly `{ cast_as, indexes }`, no extra + // keys — so SDK-facing metadata (eqlType/capabilities) can never leak. + expect(col.build()).toStrictEqual({ + cast_as: spec.castAs, + indexes: spec.indexes, + }) + }, + ) it.each([ 'public.eql_v3_text_ord_ore', 'public.eql_v3_text_ord', 'public.eql_v3_text_search', - ] as const)('%s uses non-empty positive samples for live Postgres inserts', (eqlType) => { - expect(V3_MATRIX[eqlType].samples[0]).not.toBe('') - }) + ] as const)( + '%s uses non-empty positive samples for live Postgres inserts', + (eqlType) => { + expect(V3_MATRIX[eqlType].samples[0]).not.toBe('') + }, + ) // `timestamp` domains set `cast_as: 'timestamp'` (not `'date'`) precisely to // preserve the time-of-day. The live matrix can only PROVE that preservation diff --git a/packages/stack/__tests__/wasm-inline-result-contract.test.ts b/packages/stack/__tests__/wasm-inline-result-contract.test.ts index 34d24b594..23bd4429c 100644 --- a/packages/stack/__tests__/wasm-inline-result-contract.test.ts +++ b/packages/stack/__tests__/wasm-inline-result-contract.test.ts @@ -92,33 +92,36 @@ describe('wasm-inline Result contract — failure path', () => { ['bulkEncrypt', 'encryptBulk', 'EncryptionError'], ['decrypt', 'decrypt', 'DecryptionError'], ['bulkDecrypt', 'decryptBulkFallible', 'DecryptionError'], - ])('%s surfaces a %s FFI throw as { failure } typed %s', async (method, ffiName, expectedType) => { - ;(ffi as Record>)[ - ffiName - ].mockRejectedValueOnce(new Error('ffi exploded')) - - const c = await client() - const opts = { table: users, column: users.email } - const call: Record Promise> = { - encrypt: () => c.encrypt('a@b.com', opts), - encryptQuery: () => c.encryptQuery('a@b.com', opts), - bulkEncrypt: () => c.bulkEncrypt([{ plaintext: 'a@b.com', ...opts }]), - decrypt: () => c.decrypt(ct()), - bulkDecrypt: () => c.bulkDecrypt([ct()]), - } - - // Resolves — it must NOT reject. That is the regression this guards. - const result = (await call[method]()) as { - data?: unknown - failure?: { type: string; message: string } - } - - expect(result.data).toBeUndefined() - expect(result.failure).toMatchObject({ - type: expectedType, - message: 'ffi exploded', - }) - }) + ])( + '%s surfaces a %s FFI throw as { failure } typed %s', + async (method, ffiName, expectedType) => { + ;(ffi as Record>)[ + ffiName + ].mockRejectedValueOnce(new Error('ffi exploded')) + + const c = await client() + const opts = { table: users, column: users.email } + const call: Record Promise> = { + encrypt: () => c.encrypt('a@b.com', opts), + encryptQuery: () => c.encryptQuery('a@b.com', opts), + bulkEncrypt: () => c.bulkEncrypt([{ plaintext: 'a@b.com', ...opts }]), + decrypt: () => c.decrypt(ct()), + bulkDecrypt: () => c.bulkDecrypt([ct()]), + } + + // Resolves — it must NOT reject. That is the regression this guards. + const result = (await call[method]()) as { + data?: unknown + failure?: { type: string; message: string } + } + + expect(result.data).toBeUndefined() + expect(result.failure).toMatchObject({ + type: expectedType, + message: 'ffi exploded', + }) + }, + ) // Non-Error rejections must keep their detail. `withResult`'s default // `ensureError` would replace them with `new Error('Something went wrong')`, @@ -137,19 +140,22 @@ describe('wasm-inline Result contract — failure path', () => { { code: 'EQL_X', detail: 'bad domain' }, '{"code":"EQL_X","detail":"bad domain"}', ], - ])('preserves the detail of %s rejection', async (_label, thrown, expected) => { - ffi.encrypt.mockRejectedValueOnce(thrown) - - const c = await client() - const result = await c.encrypt('a@b.com', { - table: users, - column: users.email, - }) - - expect(result.failure?.type).toBe('EncryptionError') - expect(result.failure?.message).toBe(expected) - expect(result.failure?.message).not.toBe('Something went wrong') - }) + ])( + 'preserves the detail of %s rejection', + async (_label, thrown, expected) => { + ffi.encrypt.mockRejectedValueOnce(thrown) + + const c = await client() + const result = await c.encrypt('a@b.com', { + table: users, + column: users.email, + }) + + expect(result.failure?.type).toBe('EncryptionError') + expect(result.failure?.message).toBe(expected) + expect(result.failure?.message).not.toBe('Something went wrong') + }, + ) it('falls back to String() for a value JSON cannot serialize', async () => { // A cycle makes JSON.stringify throw; the catch must still yield a diff --git a/packages/stack/__tests__/wasm-inline-v3.test.ts b/packages/stack/__tests__/wasm-inline-v3.test.ts index 4e63b561f..67410fe82 100644 --- a/packages/stack/__tests__/wasm-inline-v3.test.ts +++ b/packages/stack/__tests__/wasm-inline-v3.test.ts @@ -127,18 +127,19 @@ describe('wasm-inline is EQL v3 only (#614)', () => { // had no equivalent, so a JS/JSON caller carrying `eqlVersion: 2` got a hard // error on one entry and silence on the other — the exact entry-disagreement // #815 exists to close. - it.each([ - 2, 3, - ])('rejects config.eqlVersion (%i) the way the native entry does', async (eqlVersion) => { - await expect( - Encryption({ - schemas: [users], - // A JS caller can carry this key even though the type omits it. - config: { ...config, eqlVersion } as unknown as typeof config, - }), - ).rejects.toThrow(/`config\.eqlVersion` has been removed/) - expect(vi.mocked(wasmNewClient)).not.toHaveBeenCalled() - }) + it.each([2, 3])( + 'rejects config.eqlVersion (%i) the way the native entry does', + async (eqlVersion) => { + await expect( + Encryption({ + schemas: [users], + // A JS caller can carry this key even though the type omits it. + config: { ...config, eqlVersion } as unknown as typeof config, + }), + ).rejects.toThrow(/`config\.eqlVersion` has been removed/) + expect(vi.mocked(wasmNewClient)).not.toHaveBeenCalled() + }, + ) // `eqlVersion?: never` accepts `eqlVersion: undefined` without // `exactOptionalPropertyTypes` (not enabled in this repo) and cannot be made to diff --git a/packages/stack/integration/shared/match-bloom.integration.test.ts b/packages/stack/integration/shared/match-bloom.integration.test.ts index b9c28eb7e..1efc0a6d6 100644 --- a/packages/stack/integration/shared/match-bloom.integration.test.ts +++ b/packages/stack/integration/shared/match-bloom.integration.test.ts @@ -64,21 +64,23 @@ it.each([ { label: 'interior substring, 7 chars', needle: 'a@examp' }, { label: 'trailing substring, 4 chars', needle: '.com' }, { label: 'the whole value', needle: HAYSTACK }, -])('bloom(needle) is a subset of bloom(haystack) for a $label', async ({ - needle, -}) => { - const client = await Encryption({ schemas: [docs] }) - const haystack = await bloomOf(client, HAYSTACK) - const probe = await bloomOf(client, needle) +])( + 'bloom(needle) is a subset of bloom(haystack) for a $label', + async ({ needle }) => { + const client = await Encryption({ schemas: [docs] }) + const haystack = await bloomOf(client, HAYSTACK) + const probe = await bloomOf(client, needle) - expect(HAYSTACK.includes(needle)).toBe(true) - expect( - isSubset(probe, haystack), - `bloom("${needle}") is not contained in bloom("${HAYSTACK}"). ` + - 'The operand carries a token the haystack lacks — most likely the ' + - '`include_original` whole-value token. See the file header.', - ).toBe(true) -}, 120_000) + expect(HAYSTACK.includes(needle)).toBe(true) + expect( + isSubset(probe, haystack), + `bloom("${needle}") is not contained in bloom("${HAYSTACK}"). ` + + 'The operand carries a token the haystack lacks — most likely the ' + + '`include_original` whole-value token. See the file header.', + ).toBe(true) + }, + 120_000, +) it('a needle absent from the haystack is NOT a bloom subset', async () => { // Without this, an implementation that blooms every needle to the empty set diff --git a/packages/stack/integration/shared/matrix-crypto.integration.test.ts b/packages/stack/integration/shared/matrix-crypto.integration.test.ts index 7755d6aa6..21236b1d5 100644 --- a/packages/stack/integration/shared/matrix-crypto.integration.test.ts +++ b/packages/stack/integration/shared/matrix-crypto.integration.test.ts @@ -89,29 +89,30 @@ describe('v3 matrix live round-trip (all domains × samples)', () => { ) as Array> }, 60000) - it.each( - roundTripCases, - )('%s round-trips through the model path', (_label, col, sample, i) => { - // Guard against a false pass: the field must be a real ciphertext, not a - // plaintext value that slipped through un-encrypted. Scalar domains carry it - // at a top-level `c`; a JSON (`eql_v3_json_search`) document is an ste_vec payload - // (`{ k: 'sv', sv: [...] }`) with NO top-level `c`, so it is guarded by the - // presence of its `sv` array instead. - const payload = encrypted[i][col] as { k?: unknown; sv?: unknown } - if (payload?.k === 'sv') { - expect(Array.isArray(payload.sv)).toBe(true) - } else { - expect(payload).toHaveProperty('c') - } + it.each(roundTripCases)( + '%s round-trips through the model path', + (_label, col, sample, i) => { + // Guard against a false pass: the field must be a real ciphertext, not a + // plaintext value that slipped through un-encrypted. Scalar domains carry it + // at a top-level `c`; a JSON (`eql_v3_json_search`) document is an ste_vec payload + // (`{ k: 'sv', sv: [...] }`) with NO top-level `c`, so it is guarded by the + // presence of its `sv` array instead. + const payload = encrypted[i][col] as { k?: unknown; sv?: unknown } + if (payload?.k === 'sv') { + expect(Array.isArray(payload.sv)).toBe(true) + } else { + expect(payload).toHaveProperty('c') + } - const actual = decrypted[i][col] - if (sample instanceof Date) { - expect(actual).toBeInstanceOf(Date) - expect(actual).toEqual(sample) - } else { - expect(actual).toStrictEqual(sample) - } - }) + const actual = decrypted[i][col] + if (sample instanceof Date) { + expect(actual).toBeInstanceOf(Date) + expect(actual).toEqual(sample) + } else { + expect(actual).toStrictEqual(sample) + } + }, + ) // Mirrors number-protect.test.ts: NaN/±Infinity (number domains) and // out-of-range i64 values (bigint domains) must be rejected. The guard diff --git a/packages/stack/integration/shared/matrix-sql.integration.test.ts b/packages/stack/integration/shared/matrix-sql.integration.test.ts index a4f49bb78..f95757b48 100644 --- a/packages/stack/integration/shared/matrix-sql.integration.test.ts +++ b/packages/stack/integration/shared/matrix-sql.integration.test.ts @@ -348,140 +348,146 @@ afterAll(async () => { }, 30000) describe('v3 matrix live Postgres coverage (all covered domains)', () => { - it.each( - eqDomains, - )('%s: eql_v3.eq(col, operand) selects the exact row', async (eqlType) => { - const col = slug(eqlType) - const rows = await sql.unsafe( - `SELECT id FROM ${TABLE_NAME} + it.each(eqDomains)( + '%s: eql_v3.eq(col, operand) selects the exact row', + async (eqlType) => { + const col = slug(eqlType) + const rows = await sql.unsafe( + `SELECT id FROM ${TABLE_NAME} WHERE test_run_id = $1 AND eql_v3.eq("${col}", $2::jsonb)`, - [TEST_RUN_ID, sql.json(eqTerms[col] as never)], - ) - expect(rows.map((r) => r.id)).toEqual([idA]) - }) - - it.each( - eqDomains, - )('%s: eql_v3.neq(col, operand) excludes the matching row', async (eqlType) => { - const col = slug(eqlType) - // Operand encrypts `samples[0]` (row idA). `neq` selects every row whose - // value differs, i.e. row idB (`samples[1]`) only — idA is excluded. - const rows = await sql.unsafe( - `SELECT id FROM ${TABLE_NAME} + [TEST_RUN_ID, sql.json(eqTerms[col] as never)], + ) + expect(rows.map((r) => r.id)).toEqual([idA]) + }, + ) + + it.each(eqDomains)( + '%s: eql_v3.neq(col, operand) excludes the matching row', + async (eqlType) => { + const col = slug(eqlType) + // Operand encrypts `samples[0]` (row idA). `neq` selects every row whose + // value differs, i.e. row idB (`samples[1]`) only — idA is excluded. + const rows = await sql.unsafe( + `SELECT id FROM ${TABLE_NAME} WHERE test_run_id = $1 AND eql_v3.neq("${col}", $2::jsonb)`, - [TEST_RUN_ID, sql.json(eqTerms[col] as never)], - ) - expect(rows.map((r) => r.id)).toEqual([idB]) - }) - - it.each( - ordDomains, - )('%s: equality-via-ORE selects the exact row', async (eqlType, spec) => { - const col = slug(eqlType) - // The proof must exercise the ORDER (`ob`) term, not equality. On text order - // domains (`text_ord`/`text_ord_ore`, which carry BOTH `unique` and `ore`) - // `eql_v3.eq` compares the `hm` term, so pin the ORE term with a degenerate - // range `col >= operand AND col <= operand` (both `ord_term`-based) — it - // selects the equal row via ORE. Pure-ORE numeric/date domains have no `hm`, - // so `eql_v3.eq` resolves to `ord_term`: that IS the equality-via-ORE proof. - const predicate = hasIndex(spec.indexes, 'unique') - ? `eql_v3.gte("${col}", $2::jsonb) AND eql_v3.lte("${col}", $2::jsonb)` - : `eql_v3.eq("${col}", $2::jsonb)` - const rows = await sql.unsafe( - `SELECT id FROM ${TABLE_NAME} + [TEST_RUN_ID, sql.json(eqTerms[col] as never)], + ) + expect(rows.map((r) => r.id)).toEqual([idB]) + }, + ) + + it.each(ordDomains)( + '%s: equality-via-ORE selects the exact row', + async (eqlType, spec) => { + const col = slug(eqlType) + // The proof must exercise the ORDER (`ob`) term, not equality. On text order + // domains (`text_ord`/`text_ord_ore`, which carry BOTH `unique` and `ore`) + // `eql_v3.eq` compares the `hm` term, so pin the ORE term with a degenerate + // range `col >= operand AND col <= operand` (both `ord_term`-based) — it + // selects the equal row via ORE. Pure-ORE numeric/date domains have no `hm`, + // so `eql_v3.eq` resolves to `ord_term`: that IS the equality-via-ORE proof. + const predicate = hasIndex(spec.indexes, 'unique') + ? `eql_v3.gte("${col}", $2::jsonb) AND eql_v3.lte("${col}", $2::jsonb)` + : `eql_v3.eq("${col}", $2::jsonb)` + const rows = await sql.unsafe( + `SELECT id FROM ${TABLE_NAME} WHERE test_run_id = $1 AND ${predicate}`, - [TEST_RUN_ID, sql.json(ordTerms[col] as never)], - ) - expect(rows.map((r) => r.id)).toEqual([idA]) - }) - - it.each( - textOreDomains, - )('%s: encrypted empty string is rejected by the Postgres domain', async (eqlType) => { - const col = slug(eqlType) - const column = (table as unknown as Record)[col] as never - const encrypted = unwrapResult( - // The matrix table is built from a dynamic column map, so `column` is - // already erased above and the plaintext cannot be derived from it. - await client.encrypt('' as never, { - table: table as never, - column, - }), - ) - - await expect( - sql.unsafe(`SELECT $1::${eqlType}`, [sql.json(encrypted as never)]), - ).rejects.toThrow(/violates check constraint/) - }) - - it.each( - textOpeDomains, - )('%s: encrypted empty string clears the Postgres domain (scalar op term)', async (eqlType) => { - const col = slug(eqlType) - const column = (table as unknown as Record)[col] as never - const encrypted = unwrapResult( - // The matrix table is built from a dynamic column map, so `column` is - // already erased above and the plaintext cannot be derived from it. - await client.encrypt('' as never, { - table: table as never, - column, - }), - ) - - const rows = await sql.unsafe(`SELECT $1::${eqlType} AS v`, [ - sql.json(encrypted as never), - ]) - expect(rows).toHaveLength(1) - }) - - it.each( - matchDomains, - )('%s: eql_v3.matches selects the row containing "ada"', async (eqlType, spec) => { - const col = slug(eqlType) - const expectedId = String(spec.samples[0]).includes('ada') ? idA : idB - const rows = await sql.unsafe( - `SELECT id FROM ${TABLE_NAME} + [TEST_RUN_ID, sql.json(ordTerms[col] as never)], + ) + expect(rows.map((r) => r.id)).toEqual([idA]) + }, + ) + + it.each(textOreDomains)( + '%s: encrypted empty string is rejected by the Postgres domain', + async (eqlType) => { + const col = slug(eqlType) + const column = (table as unknown as Record)[col] as never + const encrypted = unwrapResult( + // The matrix table is built from a dynamic column map, so `column` is + // already erased above and the plaintext cannot be derived from it. + await client.encrypt('' as never, { + table: table as never, + column, + }), + ) + + await expect( + sql.unsafe(`SELECT $1::${eqlType}`, [sql.json(encrypted as never)]), + ).rejects.toThrow(/violates check constraint/) + }, + ) + + it.each(textOpeDomains)( + '%s: encrypted empty string clears the Postgres domain (scalar op term)', + async (eqlType) => { + const col = slug(eqlType) + const column = (table as unknown as Record)[col] as never + const encrypted = unwrapResult( + // The matrix table is built from a dynamic column map, so `column` is + // already erased above and the plaintext cannot be derived from it. + await client.encrypt('' as never, { + table: table as never, + column, + }), + ) + + const rows = await sql.unsafe(`SELECT $1::${eqlType} AS v`, [ + sql.json(encrypted as never), + ]) + expect(rows).toHaveLength(1) + }, + ) + + it.each(matchDomains)( + '%s: eql_v3.matches selects the row containing "ada"', + async (eqlType, spec) => { + const col = slug(eqlType) + const expectedId = String(spec.samples[0]).includes('ada') ? idA : idB + const rows = await sql.unsafe( + `SELECT id FROM ${TABLE_NAME} WHERE test_run_id = $1 AND eql_v3.matches("${col}", $2::jsonb)`, - [TEST_RUN_ID, sql.json(matchTerms[col] as never)], - ) - expect(rows.map((r) => r.id)).toEqual([expectedId]) - }) - - it.each( - orderingDomains, - )('%s: ORE pairwise-lt reproduces plaintext order across all distinct samples', async (eqlType, spec) => { - // STRICT ordering proof: seed each of a domain's distinct sample values as - // its OWN row (orderIds[0..L-1] ↔ samples[0..L-1]) and prove the ORE - // comparison reproduces the FULL plaintext order (a orderIds[i]). - const expectedAscending = spec.samples - .map((value, i) => ({ value, id: ids[i] })) - .sort((x, y) => comparePlaintext(x.value, y.value)) - .map((entry) => entry.id) - - const pairs = await sql.unsafe< - Array<{ a: number; b: number; lt: boolean }> - >( - `SELECT x.id AS a, y.id AS b, eql_v3.lt(x."${col}", y."${col}") AS lt + [TEST_RUN_ID, sql.json(matchTerms[col] as never)], + ) + expect(rows.map((r) => r.id)).toEqual([expectedId]) + }, + ) + + it.each(orderingDomains)( + '%s: ORE pairwise-lt reproduces plaintext order across all distinct samples', + async (eqlType, spec) => { + // STRICT ordering proof: seed each of a domain's distinct sample values as + // its OWN row (orderIds[0..L-1] ↔ samples[0..L-1]) and prove the ORE + // comparison reproduces the FULL plaintext order (a orderIds[i]). + const expectedAscending = spec.samples + .map((value, i) => ({ value, id: ids[i] })) + .sort((x, y) => comparePlaintext(x.value, y.value)) + .map((entry) => entry.id) + + const pairs = await sql.unsafe< + Array<{ a: number; b: number; lt: boolean }> + >( + `SELECT x.id AS a, y.id AS b, eql_v3.lt(x."${col}", y."${col}") AS lt FROM ${TABLE_NAME} x CROSS JOIN ${TABLE_NAME} y WHERE x.test_run_id = $1 @@ -489,113 +495,116 @@ describe('v3 matrix live Postgres coverage (all covered domains)', () => { AND x.id = ANY($2) AND y.id = ANY($2) AND x.id <> y.id`, - [ORDER_RUN_ID, ids], - ) - - const lessThanCount = new Map(ids.map((id) => [id, 0])) - for (const pair of pairs) { - if (pair.lt) { - lessThanCount.set(pair.a, (lessThanCount.get(pair.a) ?? 0) + 1) + [ORDER_RUN_ID, ids], + ) + + const lessThanCount = new Map(ids.map((id) => [id, 0])) + for (const pair of pairs) { + if (pair.lt) { + lessThanCount.set(pair.a, (lessThanCount.get(pair.a) ?? 0) + 1) + } } - } - - // Ascending rank = strictly-less-than count, descending (the smallest value - // is < every other row, so it has the highest count and sorts first). - const derivedAscending = [...ids].sort( - (a, b) => (lessThanCount.get(b) ?? 0) - (lessThanCount.get(a) ?? 0), - ) - // Sanity: distinct samples must yield distinct ranks 0..L-1 (a strict total - // order), otherwise the ORE comparison collapsed two values together. - expect(new Set(lessThanCount.values()).size).toBe(L) - expect(derivedAscending).toEqual(expectedAscending) - }) - - it.each( - orderingDomains, - )('%s: eql_v3.gte/lte (inclusive) and gt/lt (exclusive) bound the ORE range', async (eqlType, spec) => { - // Explicit two-bound ORE range over the distinct-sample rows: `[lo, hi]` - // spans the whole set, so gte+lte must select ALL of them and strict gt+lt - // must select only the interior (excluding the lo and hi rows). Bounds are - // the plaintext-min/max samples; membership is compared to the plaintext - // order. Uses the boolean comparison operators (ORE-correct on superuser AND - // non-superuser Postgres) — never `ORDER BY ord_term`. For L=2 domains - // (date/timestamp) the interior is empty: a valid exclusive-bound boundary. - const col = slug(eqlType) - const L = spec.samples.length - const ids = orderIds.slice(0, L) - const operands = orderOperands[col] - - const sortedIdx = spec.samples - .map((value, i) => ({ value, i })) - .sort((a, b) => comparePlaintext(a.value, b.value)) - .map((entry) => entry.i) - const lo = sql.json(operands[sortedIdx[0]] as never) - const hi = sql.json(operands[sortedIdx[L - 1]] as never) - - // Inclusive [lo, hi] → every distinct-sample row. - const inclusive = await sql.unsafe( - `SELECT id FROM ${TABLE_NAME} + // Ascending rank = strictly-less-than count, descending (the smallest value + // is < every other row, so it has the highest count and sorts first). + const derivedAscending = [...ids].sort( + (a, b) => (lessThanCount.get(b) ?? 0) - (lessThanCount.get(a) ?? 0), + ) + + // Sanity: distinct samples must yield distinct ranks 0..L-1 (a strict total + // order), otherwise the ORE comparison collapsed two values together. + expect(new Set(lessThanCount.values()).size).toBe(L) + expect(derivedAscending).toEqual(expectedAscending) + }, + ) + + it.each(orderingDomains)( + '%s: eql_v3.gte/lte (inclusive) and gt/lt (exclusive) bound the ORE range', + async (eqlType, spec) => { + // Explicit two-bound ORE range over the distinct-sample rows: `[lo, hi]` + // spans the whole set, so gte+lte must select ALL of them and strict gt+lt + // must select only the interior (excluding the lo and hi rows). Bounds are + // the plaintext-min/max samples; membership is compared to the plaintext + // order. Uses the boolean comparison operators (ORE-correct on superuser AND + // non-superuser Postgres) — never `ORDER BY ord_term`. For L=2 domains + // (date/timestamp) the interior is empty: a valid exclusive-bound boundary. + const col = slug(eqlType) + const L = spec.samples.length + const ids = orderIds.slice(0, L) + const operands = orderOperands[col] + + const sortedIdx = spec.samples + .map((value, i) => ({ value, i })) + .sort((a, b) => comparePlaintext(a.value, b.value)) + .map((entry) => entry.i) + const lo = sql.json(operands[sortedIdx[0]] as never) + const hi = sql.json(operands[sortedIdx[L - 1]] as never) + + // Inclusive [lo, hi] → every distinct-sample row. + const inclusive = await sql.unsafe( + `SELECT id FROM ${TABLE_NAME} WHERE test_run_id = $1 AND id = ANY($2) AND eql_v3.gte("${col}", $3::jsonb) AND eql_v3.lte("${col}", $4::jsonb)`, - [ORDER_RUN_ID, ids, lo, hi], - ) - expect(new Set(inclusive.map((r) => r.id))).toEqual(new Set(ids)) - - // Exclusive (lo, hi) via strict gt/lt → strictly-interior rows only. - const interiorIds = sortedIdx.slice(1, L - 1).map((i) => ids[i]) - const exclusive = await sql.unsafe( - `SELECT id FROM ${TABLE_NAME} + [ORDER_RUN_ID, ids, lo, hi], + ) + expect(new Set(inclusive.map((r) => r.id))).toEqual(new Set(ids)) + + // Exclusive (lo, hi) via strict gt/lt → strictly-interior rows only. + const interiorIds = sortedIdx.slice(1, L - 1).map((i) => ids[i]) + const exclusive = await sql.unsafe( + `SELECT id FROM ${TABLE_NAME} WHERE test_run_id = $1 AND id = ANY($2) AND eql_v3.gt("${col}", $3::jsonb) AND eql_v3.lt("${col}", $4::jsonb)`, - [ORDER_RUN_ID, ids, lo, hi], - ) - expect(new Set(exclusive.map((r) => r.id))).toEqual(new Set(interiorIds)) - - // The controls. Domains with only two distinct samples (date, timestamp) - // have an empty interior, so the assertion above is satisfied by a - // constant-false `gt`/`lt` as well as by a correct one. One-sided strict - // bounds are non-empty for every domain: `gt(lo)` must return everything - // above the minimum, `lt(hi)` everything below the maximum. Both die on a - // constant-false operator, and on a constant-true one (which would drag in - // the excluded endpoint). - const aboveLo = await sql.unsafe( - `SELECT id FROM ${TABLE_NAME} + [ORDER_RUN_ID, ids, lo, hi], + ) + expect(new Set(exclusive.map((r) => r.id))).toEqual(new Set(interiorIds)) + + // The controls. Domains with only two distinct samples (date, timestamp) + // have an empty interior, so the assertion above is satisfied by a + // constant-false `gt`/`lt` as well as by a correct one. One-sided strict + // bounds are non-empty for every domain: `gt(lo)` must return everything + // above the minimum, `lt(hi)` everything below the maximum. Both die on a + // constant-false operator, and on a constant-true one (which would drag in + // the excluded endpoint). + const aboveLo = await sql.unsafe( + `SELECT id FROM ${TABLE_NAME} WHERE test_run_id = $1 AND id = ANY($2) AND eql_v3.gt("${col}", $3::jsonb)`, - [ORDER_RUN_ID, ids, lo], - ) - expect(new Set(aboveLo.map((r) => r.id))).toEqual( - new Set(sortedIdx.slice(1).map((i) => ids[i])), - ) - - const belowHi = await sql.unsafe( - `SELECT id FROM ${TABLE_NAME} + [ORDER_RUN_ID, ids, lo], + ) + expect(new Set(aboveLo.map((r) => r.id))).toEqual( + new Set(sortedIdx.slice(1).map((i) => ids[i])), + ) + + const belowHi = await sql.unsafe( + `SELECT id FROM ${TABLE_NAME} WHERE test_run_id = $1 AND id = ANY($2) AND eql_v3.lt("${col}", $3::jsonb)`, - [ORDER_RUN_ID, ids, hi], - ) - expect(new Set(belowHi.map((r) => r.id))).toEqual( - new Set(sortedIdx.slice(0, L - 1).map((i) => ids[i])), - ) - }) - - it.each( - storageDomains, - )('%s: ciphertext survives a real INSERT/SELECT and still decrypts', async (eqlType, spec) => { - const col = slug(eqlType) - const [row] = await sql.unsafe>( - `SELECT "${col}"::jsonb AS value FROM ${TABLE_NAME} WHERE id = $1`, - [idA], - ) - const decrypted = unwrapResult(await client.decrypt(row.value as never)) - const expected = spec.samples[0] - expectDecryptedStorageValue(decrypted, expected) - }) + [ORDER_RUN_ID, ids, hi], + ) + expect(new Set(belowHi.map((r) => r.id))).toEqual( + new Set(sortedIdx.slice(0, L - 1).map((i) => ids[i])), + ) + }, + ) + + it.each(storageDomains)( + '%s: ciphertext survives a real INSERT/SELECT and still decrypts', + async (eqlType, spec) => { + const col = slug(eqlType) + const [row] = await sql.unsafe>( + `SELECT "${col}"::jsonb AS value FROM ${TABLE_NAME} WHERE id = $1`, + [idA], + ) + const decrypted = unwrapResult(await client.decrypt(row.value as never)) + const expected = spec.samples[0] + expectDecryptedStorageValue(decrypted, expected) + }, + ) }) diff --git a/packages/stack/integration/shared/v2-decrypt-compat.integration.test.ts b/packages/stack/integration/shared/v2-decrypt-compat.integration.test.ts index 505e94814..858b05286 100644 --- a/packages/stack/integration/shared/v2-decrypt-compat.integration.test.ts +++ b/packages/stack/integration/shared/v2-decrypt-compat.integration.test.ts @@ -491,24 +491,25 @@ describe('a v3 client reconstructs every plaintext axis from stored EQL v2 paylo ).toEqual(['bigint', 'boolean', 'date', 'number', 'string', 'timestamp']) }) - it.each( - matrixCases, - )('%s decrypts from a stored EQL v2 payload through the model path', (label, slug, castAs, sample, row) => { - const payload = encrypted[row][slug] - - // THE guard for this file. Without it the whole matrix would pass just as - // happily against v3 fixtures and prove nothing about legacy reads. The - // identifier is checked too: `encryptBulk` correlates positionally, so a - // mis-zipped batch could otherwise assert one domain's sample against - // another domain's ciphertext and go green by coincidence. - expect(payload).toMatchObject({ - v: 2, - i: { t: matrix.tableName, c: slug }, - }) - expect(payload).toHaveProperty('c') - - expectReconstructed(decrypted[row][slug], castAs, sample, label) - }) + it.each(matrixCases)( + '%s decrypts from a stored EQL v2 payload through the model path', + (label, slug, castAs, sample, row) => { + const payload = encrypted[row][slug] + + // THE guard for this file. Without it the whole matrix would pass just as + // happily against v3 fixtures and prove nothing about legacy reads. The + // identifier is checked too: `encryptBulk` correlates positionally, so a + // mis-zipped batch could otherwise assert one domain's sample against + // another domain's ciphertext and go green by coincidence. + expect(payload).toMatchObject({ + v: 2, + i: { t: matrix.tableName, c: slug }, + }) + expect(payload).toHaveProperty('c') + + expectReconstructed(decrypted[row][slug], castAs, sample, label) + }, + ) /** * `decryptModel` and `bulkDecryptModels` are separate wrappers over the same diff --git a/packages/stack/integration/wasm/v2-decrypt-compat.integration.test.ts b/packages/stack/integration/wasm/v2-decrypt-compat.integration.test.ts index 757f0cd51..5873381f9 100644 --- a/packages/stack/integration/wasm/v2-decrypt-compat.integration.test.ts +++ b/packages/stack/integration/wasm/v2-decrypt-compat.integration.test.ts @@ -328,24 +328,25 @@ describe('a wasm-inline client reconstructs every plaintext axis from stored EQL ) }, 60000) - it.each( - matrixCases, - )('%s decrypts from a stored EQL v2 payload through the wasm model path', (label, slug, castAs, sample, row) => { - const payload = encrypted[row][slug] + it.each(matrixCases)( + '%s decrypts from a stored EQL v2 payload through the wasm model path', + (label, slug, castAs, sample, row) => { + const payload = encrypted[row][slug] - // THE guard for this block. Without it the whole matrix would pass just as - // happily against v3 fixtures and prove nothing about legacy reads. The - // identifier is checked too: `encryptBulk` correlates positionally, so a - // mis-zipped batch could otherwise assert one domain's sample against - // another domain's ciphertext and go green by coincidence. - expect(payload).toMatchObject({ - v: 2, - i: { t: matrix.tableName, c: slug }, - }) - expect(payload).toHaveProperty('c') + // THE guard for this block. Without it the whole matrix would pass just as + // happily against v3 fixtures and prove nothing about legacy reads. The + // identifier is checked too: `encryptBulk` correlates positionally, so a + // mis-zipped batch could otherwise assert one domain's sample against + // another domain's ciphertext and go green by coincidence. + expect(payload).toMatchObject({ + v: 2, + i: { t: matrix.tableName, c: slug }, + }) + expect(payload).toHaveProperty('c') - expectReconstructed(decrypted[row][slug], castAs, sample, label) - }) + expectReconstructed(decrypted[row][slug], castAs, sample, label) + }, + ) /** * `decryptModel` and `bulkDecryptModels` are separate wrappers over diff --git a/packages/test-kit/src/run-family-suite.ts b/packages/test-kit/src/run-family-suite.ts index 3f24b6238..4dff2b833 100644 --- a/packages/test-kit/src/run-family-suite.ts +++ b/packages/test-kit/src/run-family-suite.ts @@ -272,21 +272,21 @@ export function runFamilySuite( for (const kind of ['gt', 'gte', 'lt', 'lte'] as const) { if (!positive.has(kind)) continue - it.each([ - min, - max, - ])(`${kind}(%s) matches the oracle`, async (bound) => { - const cmp = { - gt: (c: number) => c > 0, - gte: (c: number) => c >= 0, - lt: (c: number) => c < 0, - lte: (c: number) => c <= 0, - }[kind] - await expectRows( - { kind, column: slug, value: bound }, - keysWhere((v) => cmp(comparePlain(v, bound))), - ) - }) + it.each([min, max])( + `${kind}(%s) matches the oracle`, + async (bound) => { + const cmp = { + gt: (c: number) => c > 0, + gte: (c: number) => c >= 0, + lt: (c: number) => c < 0, + lte: (c: number) => c <= 0, + }[kind] + await expectRows( + { kind, column: slug, value: bound }, + keysWhere((v) => cmp(comparePlain(v, bound))), + ) + }, + ) } if (positive.has('between')) { @@ -315,30 +315,31 @@ export function runFamilySuite( } if (positive.has('matches')) { - it.each(needlesFrom(values))('matches: $label', async ({ - needle, - }) => { - await expectRows( - { kind: 'matches', column: slug, needle }, - keysWhere((v) => matchesPlain(v, needle)), - ) - }) + it.each(needlesFrom(values))( + 'matches: $label', + async ({ needle }) => { + await expectRows( + { kind: 'matches', column: slug, needle }, + keysWhere((v) => matchesPlain(v, needle)), + ) + }, + ) } if (positive.has('order')) { - it.each([ - 'asc', - 'desc', - ] as const)('order(%s) returns rows in plaintext order', async (direction) => { - // Order is the one operation whose ROW ORDER is the assertion, so - // it does not go through `expectRows` (which sorts both sides). - const rows = await adapter.run(table, { - kind: 'order', - column: slug, - direction, - }) - expect(rows).toEqual(sortedKeysFor(spec, rowKeys, direction)) - }) + it.each(['asc', 'desc'] as const)( + 'order(%s) returns rows in plaintext order', + async (direction) => { + // Order is the one operation whose ROW ORDER is the assertion, so + // it does not go through `expectRows` (which sorts both sides). + const rows = await adapter.run(table, { + kind: 'order', + column: slug, + direction, + }) + expect(rows).toEqual(sortedKeysFor(spec, rowKeys, direction)) + }, + ) } // Structural, never capability-gated: a NULL plaintext is a SQL NULL on diff --git a/packages/test-kit/src/run-json-suite.ts b/packages/test-kit/src/run-json-suite.ts index 888780a29..6e9dfe079 100644 --- a/packages/test-kit/src/run-json-suite.ts +++ b/packages/test-kit/src/run-json-suite.ts @@ -109,12 +109,15 @@ export function runJsonSuite(makeAdapter: () => JsonIntegrationAdapter): void { ['lt', 35, ['ada', 'grace']], ['gte', 30, ['ada', 'zoe']], ['gt', 100, []], - ] as const)('%s(%s) matches the plaintext oracle', async (comparison, value, expected) => { - await expectRows( - { kind: 'selector', comparison, path: '$.age', value }, - [...expected], - ) - }) + ] as const)( + '%s(%s) matches the plaintext oracle', + async (comparison, value, expected) => { + await expectRows( + { kind: 'selector', comparison, path: '$.age', value }, + [...expected], + ) + }, + ) it('uses explicit absent-path semantics', async () => { await expectRows( diff --git a/packages/wizard/src/__tests__/interface.test.ts b/packages/wizard/src/__tests__/interface.test.ts index 9368eccf1..247c2a406 100644 --- a/packages/wizard/src/__tests__/interface.test.ts +++ b/packages/wizard/src/__tests__/interface.test.ts @@ -76,22 +76,21 @@ describe('wizardCanUseTool', () => { // The doctrine tells the agent to add placeholder keys to `.env.example`. // The guard covers Edit and Write as well as Read, so a blanket `.env.` // rule made the file the agent is told to write unreachable. - it.each([ - '.env.example', - '.env.sample', - '.env.template', - ])('allows Read/Edit/Write/Glob on %s', (name) => { - expect(wizardCanUseTool('Read', { file_path: `/project/${name}` })).toBe( - true, - ) - expect(wizardCanUseTool('Edit', { file_path: `/project/${name}` })).toBe( - true, - ) - expect(wizardCanUseTool('Write', { file_path: `/project/${name}` })).toBe( - true, - ) - expect(wizardCanUseTool('Glob', { pattern: name })).toBe(true) - }) + it.each(['.env.example', '.env.sample', '.env.template'])( + 'allows Read/Edit/Write/Glob on %s', + (name) => { + expect( + wizardCanUseTool('Read', { file_path: `/project/${name}` }), + ).toBe(true) + expect( + wizardCanUseTool('Edit', { file_path: `/project/${name}` }), + ).toBe(true) + expect( + wizardCanUseTool('Write', { file_path: `/project/${name}` }), + ).toBe(true) + expect(wizardCanUseTool('Glob', { pattern: name })).toBe(true) + }, + ) it('still blocks value-bearing files that only start with a template name', () => { expect( diff --git a/packages/wizard/src/__tests__/rewrite-migrations.test.ts b/packages/wizard/src/__tests__/rewrite-migrations.test.ts index 6a31db3a6..8e2e16933 100644 --- a/packages/wizard/src/__tests__/rewrite-migrations.test.ts +++ b/packages/wizard/src/__tests__/rewrite-migrations.test.ts @@ -258,26 +258,26 @@ describe('rewriteEncryptedAlterColumns', () => { // DOMAIN_RE is derived from ENCRYPTED_DOMAIN, so drift between the two can't // silently leave a domain unrewritten. Prove every domain the alternation // recognises is actually extracted into the emitted ADD COLUMN. - it.each([ - ...V3_DOMAINS, - 'eql_v2_encrypted', - ])('extracts the bare domain %s from a mangled ALTER', async (domain) => { - declarePlaintext('"t"', 'c') - const filePath = path.join(tmpDir, '0015_drift.sql') - fs.writeFileSync( - filePath, - `ALTER TABLE "t" ALTER COLUMN "c" SET DATA TYPE "undefined"."${domain}";\n`, - ) + it.each([...V3_DOMAINS, 'eql_v2_encrypted'])( + 'extracts the bare domain %s from a mangled ALTER', + async (domain) => { + declarePlaintext('"t"', 'c') + const filePath = path.join(tmpDir, '0015_drift.sql') + fs.writeFileSync( + filePath, + `ALTER TABLE "t" ALTER COLUMN "c" SET DATA TYPE "undefined"."${domain}";\n`, + ) - const { rewritten } = await rewriteEncryptedAlterColumns(tmpDir) + const { rewritten } = await rewriteEncryptedAlterColumns(tmpDir) - expect(rewritten).toEqual([filePath]) - const updated = fs.readFileSync(filePath, 'utf-8') - expect(updated).toContain( - `ALTER TABLE "t" ADD COLUMN "c_encrypted" "public"."${domain}";`, - ) - expect(updated).not.toContain('SET DATA TYPE') - }) + expect(rewritten).toEqual([filePath]) + const updated = fs.readFileSync(filePath, 'utf-8') + expect(updated).toContain( + `ALTER TABLE "t" ADD COLUMN "c_encrypted" "public"."${domain}";`, + ) + expect(updated).not.toContain('SET DATA TYPE') + }, + ) // The mangled forms are the cross product of what `dataType()` returns and // which drizzle-kit era renders it (see the file's comment table). @@ -865,26 +865,30 @@ describe('rewriteEncryptedAlterColumns', () => { it.each([ ['tagged', 'price$usd$cents'], ['untagged', 'price$$cents'], - ])('does not treat a %s dollar delimiter inside an unquoted identifier as a dollar-quoted body', async (_kind, identifier) => { - declarePlaintext('"users"', 'email') - const filePath = path.join(tmpDir, `0033_${_kind}-identifier.sql`) - fs.writeFileSync( - filePath, - [ - `SELECT ${identifier} FROM "prices";`, - 'ALTER TABLE "users" ALTER COLUMN "email" SET DATA TYPE eql_v3_text_search;', - '', - ].join('\n'), - ) - - const { rewritten, skipped } = await rewriteEncryptedAlterColumns(tmpDir) - - expect(rewritten).toEqual([filePath]) - expect(skipped).toEqual([]) - const updated = fs.readFileSync(filePath, 'utf-8') - expect(updated).toContain('ADD COLUMN "email_encrypted"') - expect(updated).not.toContain('SET DATA TYPE') - }) + ])( + 'does not treat a %s dollar delimiter inside an unquoted identifier as a dollar-quoted body', + async (_kind, identifier) => { + declarePlaintext('"users"', 'email') + const filePath = path.join(tmpDir, `0033_${_kind}-identifier.sql`) + fs.writeFileSync( + filePath, + [ + `SELECT ${identifier} FROM "prices";`, + 'ALTER TABLE "users" ALTER COLUMN "email" SET DATA TYPE eql_v3_text_search;', + '', + ].join('\n'), + ) + + const { rewritten, skipped } = + await rewriteEncryptedAlterColumns(tmpDir) + + expect(rewritten).toEqual([filePath]) + expect(skipped).toEqual([]) + const updated = fs.readFileSync(filePath, 'utf-8') + expect(updated).toContain('ADD COLUMN "email_encrypted"') + expect(updated).not.toContain('SET DATA TYPE') + }, + ) }) /** diff --git a/scripts/__tests__/bench-index-expressions.test.mjs b/scripts/__tests__/bench-index-expressions.test.mjs index a92578a4c..ea50f41c3 100644 --- a/scripts/__tests__/bench-index-expressions.test.mjs +++ b/scripts/__tests__/bench-index-expressions.test.mjs @@ -121,29 +121,26 @@ describe('bench index expressions match the EQL v3 operator lowering', () => { ) }) - it.each( - INDEX_CONTRACTS, - )('$index is built on the function $operator inlines to', ({ - index, - operator, - inlinesTo, - }) => { - const body = functionBody(sql, operator) - - // The operator really does lower to this function — if the bundle - // changes shape, this fails here rather than silently passing the - // schema check below against a stale assumption. - expect( - eqlCalls(body), - `${operator} no longer calls eql_v3.${inlinesTo}`, - ).toContain(inlinesTo) - - expect( - eqlCalls(indexes.get(index)), - `${index} must index eql_v3.${inlinesTo}(...) — the expression the ` + - 'operator inlines to — or the planner can never match it', - ).toEqual([inlinesTo]) - }) + it.each(INDEX_CONTRACTS)( + '$index is built on the function $operator inlines to', + ({ index, operator, inlinesTo }) => { + const body = functionBody(sql, operator) + + // The operator really does lower to this function — if the bundle + // changes shape, this fails here rather than silently passing the + // schema check below against a stale assumption. + expect( + eqlCalls(body), + `${operator} no longer calls eql_v3.${inlinesTo}`, + ).toContain(inlinesTo) + + expect( + eqlCalls(indexes.get(index)), + `${index} must index eql_v3.${inlinesTo}(...) — the expression the ` + + 'operator inlines to — or the planner can never match it', + ).toEqual([inlinesTo]) + }, + ) it('the operator wrappers are inlinable (LANGUAGE sql IMMUTABLE STRICT)', () => { for (const { operator } of INDEX_CONTRACTS) { diff --git a/scripts/__tests__/cargo-publish-opt-out.test.mjs b/scripts/__tests__/cargo-publish-opt-out.test.mjs index 7df256be5..77a8b446d 100644 --- a/scripts/__tests__/cargo-publish-opt-out.test.mjs +++ b/scripts/__tests__/cargo-publish-opt-out.test.mjs @@ -78,37 +78,36 @@ function workspaceMembers(WORKSPACE) { .sort() } -describe.each(WORKSPACES)('cargo publish opt-out ($root)', ({ - root, - publishable, - expects, -}) => { - const workspace = join(REPO_ROOT, root) - const members = workspaceMembers(workspace) +describe.each(WORKSPACES)( + 'cargo publish opt-out ($root)', + ({ root, publishable, expects }) => { + const workspace = join(REPO_ROOT, root) + const members = workspaceMembers(workspace) - // The guard on the scan: a discovery test that enumerates nothing passes - // while checking nothing. - it('finds the workspace members it means to check', () => { - expect(members).toContain(expects) - }) - - // The guard on the allowlist: an entry naming a member that no longer - // exists is an exemption excusing nothing, and it would go on reading as a - // deliberate decision. - it('allowlists only real members', () => { - expect([...publishable].filter((name) => !members.includes(name))).toEqual( - [], - ) - }) + // The guard on the scan: a discovery test that enumerates nothing passes + // while checking nothing. + it('finds the workspace members it means to check', () => { + expect(members).toContain(expects) + }) - for (const member of members) { - it(`${member} declares publish = false unless allowlisted`, () => { - if (publishable.has(member)) return - const manifest = readFileSync( - join(workspace, member, 'Cargo.toml'), - 'utf8', - ) - expect(manifest).toMatch(/^publish = false$/m) + // The guard on the allowlist: an entry naming a member that no longer + // exists is an exemption excusing nothing, and it would go on reading as a + // deliberate decision. + it('allowlists only real members', () => { + expect( + [...publishable].filter((name) => !members.includes(name)), + ).toEqual([]) }) - } -}) + + for (const member of members) { + it(`${member} declares publish = false unless allowlisted`, () => { + if (publishable.has(member)) return + const manifest = readFileSync( + join(workspace, member, 'Cargo.toml'), + 'utf8', + ) + expect(manifest).toMatch(/^publish = false$/m) + }) + } + }, +) diff --git a/scripts/__tests__/cli-vitest-alias.test.mjs b/scripts/__tests__/cli-vitest-alias.test.mjs index 5d93db5e7..0c12c0055 100644 --- a/scripts/__tests__/cli-vitest-alias.test.mjs +++ b/scripts/__tests__/cli-vitest-alias.test.mjs @@ -57,9 +57,10 @@ describe('packages/cli vitest alias map', () => { ) }) - it.each( - targets, - )('%s resolves to a file that exists', (_specifier, target) => { - expect(existsSync(target), `missing alias target: ${target}`).toBe(true) - }) + it.each(targets)( + '%s resolves to a file that exists', + (_specifier, target) => { + expect(existsSync(target), `missing alias target: ${target}`).toBe(true) + }, + ) }) diff --git a/scripts/__tests__/frozen-publisher-docs.test.mjs b/scripts/__tests__/frozen-publisher-docs.test.mjs index 556701cdc..7d9c59d28 100644 --- a/scripts/__tests__/frozen-publisher-docs.test.mjs +++ b/scripts/__tests__/frozen-publisher-docs.test.mjs @@ -186,19 +186,19 @@ describe('frozen-publisher docs track the map', () => { expect(FROZEN_PUBLISHERS.size).toBeGreaterThan(0) }) - it.each(DOCS)('$file documents the eql freeze iff the map carries it', ({ - file, - instruction, - }) => { - expect( - satisfies(instruction, read(file)), - FROZEN_PUBLISHERS.has(EQL) - ? `${file} no longer tells an agent about the ${EQL} freeze, but ` + - 'FROZEN_PUBLISHERS still carries it.' - : `${EQL} has left FROZEN_PUBLISHERS (Phase-5 cutover), so ${file} ` + - 'must stop instructing agents about the freeze.', - ).toBe(FROZEN_PUBLISHERS.has(EQL)) - }) + it.each(DOCS)( + '$file documents the eql freeze iff the map carries it', + ({ file, instruction }) => { + expect( + satisfies(instruction, read(file)), + FROZEN_PUBLISHERS.has(EQL) + ? `${file} no longer tells an agent about the ${EQL} freeze, but ` + + 'FROZEN_PUBLISHERS still carries it.' + : `${EQL} has left FROZEN_PUBLISHERS (Phase-5 cutover), so ${file} ` + + 'must stop instructing agents about the freeze.', + ).toBe(FROZEN_PUBLISHERS.has(EQL)) + }, + ) it.each(DOCS)('$file freezes only what the map freezes', ({ file }) => { const claimed = [...new Set(foreignPublishClaims(read(file)))] diff --git a/scripts/__tests__/lint-no-eql-registry-pins.test.mjs b/scripts/__tests__/lint-no-eql-registry-pins.test.mjs index 1e0abd7aa..e59ee499c 100644 --- a/scripts/__tests__/lint-no-eql-registry-pins.test.mjs +++ b/scripts/__tests__/lint-no-eql-registry-pins.test.mjs @@ -376,15 +376,14 @@ describe('npm: what counts as in-tree', () => { ).toMatchObject({ inTree: false, form: 'version' }) }) - it.each([ - 'devDependencies', - 'peerDependencies', - 'optionalDependencies', - ])('reads %s', (table) => { - expect(pkg({ [table]: { '@cipherstash/eql': '3.0.2' } })[0].table).toBe( - table, - ) - }) + it.each(['devDependencies', 'peerDependencies', 'optionalDependencies'])( + 'reads %s', + (table) => { + expect(pkg({ [table]: { '@cipherstash/eql': '3.0.2' } })[0].table).toBe( + table, + ) + }, + ) it('reads resolutions and both overrides spellings', () => { // The quietest way back to a registry: every `workspace:^` in the tree diff --git a/scripts/__tests__/neon-dist-crate-name.test.mjs b/scripts/__tests__/neon-dist-crate-name.test.mjs index f544c25d5..b944ebdbe 100644 --- a/scripts/__tests__/neon-dist-crate-name.test.mjs +++ b/scripts/__tests__/neon-dist-crate-name.test.mjs @@ -115,16 +115,15 @@ describe('neon dist through pnpm exec', () => { expect(invocations.length).toBeGreaterThan(0) }) - it.each( - invocations, - )('$file › $jobId › $name › call $nth passes the crate name', ({ - command, - }) => { - expect( - crateNameFlag(command), - 'neon dist needs -n; $npm_package_name is unset under pnpm exec', - ).toBe(crateName()) - }) + it.each(invocations)( + '$file › $jobId › $name › call $nth passes the crate name', + ({ command }) => { + expect( + crateNameFlag(command), + 'neon dist needs -n; $npm_package_name is unset under pnpm exec', + ).toBe(crateName()) + }, + ) // The scanner's own coverage. Both of these describe a workflow that does not // exist yet, so nothing above would notice if the splitting regressed to a diff --git a/scripts/__tests__/turbo-skills-inputs.test.mjs b/scripts/__tests__/turbo-skills-inputs.test.mjs index a69d2edea..a7c0998e1 100644 --- a/scripts/__tests__/turbo-skills-inputs.test.mjs +++ b/scripts/__tests__/turbo-skills-inputs.test.mjs @@ -52,25 +52,26 @@ describe('turbo build inputs cover the skills directory', () => { ) }) - it.each( - packagesCopyingSkills().map((name) => [name]), - )('%s#build declares skills as an input', (name) => { - const task = turbo.tasks[`${name}#build`] + it.each(packagesCopyingSkills().map((name) => [name]))( + '%s#build declares skills as an input', + (name) => { + const task = turbo.tasks[`${name}#build`] - expect( - task, - `turbo.json has no "${name}#build" task, so it inherits the generic ` + - '`build` inputs, which do not include the repo-root skills/ directory', - ).toBeDefined() + expect( + task, + `turbo.json has no "${name}#build" task, so it inherits the generic ` + + '`build` inputs, which do not include the repo-root skills/ directory', + ).toBeDefined() - const inputs = task.inputs ?? [] - expect( - inputs.some((i) => i.includes('skills')), - `"${name}#build".inputs must name the repo-root skills/ directory ` + - '(e.g. "$TURBO_ROOT$/skills/**") or a skills-only edit will not ' + - 'invalidate the build, and `outputs: ["dist/**"]` will restore a stale copy', - ).toBe(true) - }) + const inputs = task.inputs ?? [] + expect( + inputs.some((i) => i.includes('skills')), + `"${name}#build".inputs must name the repo-root skills/ directory ` + + '(e.g. "$TURBO_ROOT$/skills/**") or a skills-only edit will not ' + + 'invalidate the build, and `outputs: ["dist/**"]` will restore a stale copy', + ).toBe(true) + }, + ) it('keeps the generic build outputs on the overrides', () => { // An override replaces the generic task wholesale — dropping `outputs` diff --git a/scripts/__tests__/vitest-shared-alias.test.mjs b/scripts/__tests__/vitest-shared-alias.test.mjs index 771d68f8a..17892e25b 100644 --- a/scripts/__tests__/vitest-shared-alias.test.mjs +++ b/scripts/__tests__/vitest-shared-alias.test.mjs @@ -26,9 +26,10 @@ describe('vitest.shared.ts alias targets exist on disk', () => { expect(existsSync(target), `missing alias target: ${target}`).toBe(true) }) - it.each( - targets(stackSourceAlias), - )('stackSourceAlias %s', (_specifier, target) => { - expect(existsSync(target), `missing alias target: ${target}`).toBe(true) - }) + it.each(targets(stackSourceAlias))( + 'stackSourceAlias %s', + (_specifier, target) => { + expect(existsSync(target), `missing alias target: ${target}`).toBe(true) + }, + ) })