-
Notifications
You must be signed in to change notification settings - Fork 3
feat(stack,cli): EQL v3 Supabase adapter (encryptedSupabaseV3) + v3 install path #547
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
freshtonic
wants to merge
67
commits into
main
Choose a base branch
from
james/cip-3300-spike-integrate-eql-v3-into-supabase-orm
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
67 commits
Select commit
Hold shift + click to select a range
a3f8fcf
docs(stack): design spec for eql_v3 text_search schema DSL (increment 1)
tobyhede 2808524
docs(stack): implementation plan for eql_v3 text_search schema DSL
tobyhede 047163b
docs(stack): incorporate plan review feedback for eql_v3 text_search
tobyhede e4bb5e3
docs(stack): scope v3 to working client integration + apply batch-2 r…
tobyhede adb3ca9
docs(stack): apply batch-3 plan review (widen internal consumers, sco…
tobyhede 04a0681
docs(stack): split query column contract so encryptQuery rejects non-…
tobyhede af41a95
docs(stack): pin bulk-encrypt widen-site + note WASM v3 boundary
tobyhede fd6ddb2
docs(stack): pin concrete BuiltMatchIndexOpts type for v3 match opts
tobyhede ed32da3
docs(stack): fix @ts-expect-error placement in negative type tests
tobyhede 6566fee
feat(stack): add eql_v3 text_search column builder
tobyhede 1c72fa2
feat(stack): add eql_v3 encryptedTable and buildEncryptConfig
tobyhede adacd24
feat(stack): wire @cipherstash/stack/schema/v3 export subpath
tobyhede 482afe4
test(stack): type-level tests for eql_v3 schema DSL + scoped CI typec…
tobyhede ca11c2b
feat(stack): widen public client types so v3 builders work with the c…
tobyhede 5e4f354
chore(stack): changeset for eql_v3 text_search DSL (minor)
tobyhede 14492d9
style(stack): biome format v3 tests + drop now-unused EncryptedTable …
tobyhede 5a633c4
fix(stack): guard v3 encryptedTable against reserved column names
tobyhede 21ee03c
fix(stack): resolve column name structurally in wasm-inline encrypt e…
tobyhede eb3d7de
feat(stack): guard v3 buildEncryptConfig against duplicate table names
tobyhede ccfe6d9
feat(stack): extend v3 reserved-column guard to inherited prototype keys
tobyhede f956d7e
docs(stack): note text_search defaults to equality, needs queryType:'…
tobyhede 3ce855e
test(stack): cover eql v3 typed schema domains
tobyhede 5a64f04
feat(stack): add eql v3 domain builders for all generated SQL domains
tobyhede 2103e43
feat(stack): support v3 tables and Date/bigint in client encryption
tobyhede a3cf310
test(stack): stabilize v3 client, pg, and wasm-inline coverage
tobyhede cb34d71
chore(stack): add changeset for eql v3 typed schema
tobyhede 5c9771e
docs: add eql v3 typed schema plan and query API walkthrough
tobyhede 20f5b94
fix(stack): address PR review feedback for eql v3 typed schema
tobyhede 4ceefed
feat(stack): strongly-typed EQL v3 client surface (@cipherstash/stack…
tobyhede 298dfc6
fix(stack): use string plaintext for eql v3 int8 domains
tobyhede 34d5d01
fix(stack): address code review findings for eql v3 typed client
tobyhede b6496df
fix(stack): key v3 encrypt config by DB column name, not JS property
tobyhede 5fc710f
fix(stack): match v3 model fields by JS property, encrypt by DB name
tobyhede d41fa99
ci(stack): add blocking FTA complexity gate for EQL v3
tobyhede 1cde5e3
test(stack): type-driven v3 domain test matrix
tobyhede d0fdf90
feat(stack): equality-via-ORE fix + live v3 domain coverage
tobyhede a50e512
test(stack): live Postgres SQL coverage for all 35 v3 domains
tobyhede 67dffc3
fix(stack): address CodeRabbit review findings on eql v3 typed client
tobyhede ea1c1c7
test(stack): harden v3 CJS export check and preserve run() transcript…
tobyhede da75be9
test(stack): disable timestamptz and matrix-live-pg tests failing on CI
tobyhede 0b6fafe
fix(stack): wrap ciphertext params with sql.json() in matrix-live-pg
tobyhede 0e60bd8
docs(stack): design for Stryker v3 blocking CI gate
tobyhede f032ee3
docs(stack): confirm lean, single-workflow Stryker v3 gate
tobyhede 6bcd670
refactor(stack): extract EQL v3 DSL into eql/v3 types module
tobyhede 92d3e8e
fix(stack): guard against duplicate v3 column DB names in build()
tobyhede a03d2cc
fix(stack): export v3 Buildable* contracts from public types entrypoint
tobyhede 0f4b37e
test(stack): migrate v3-matrix suite to eql/v3 types namespace
tobyhede 748b949
test(stack): cover lock-context encrypt guards and wasm newClient shape
tobyhede fce4619
test(stack): re-enable matrix-live-pg live SQL coverage (stale skip)
tobyhede 476c1df
fix(stack): emit hm index for text order domains (eql_v3 SQL domain r…
tobyhede 17b0971
test(stack): v3 lock-context coverage + text_match/no-op/empty-config…
tobyhede b44eb55
chore(stack): EQL v3 maintainability follow-ups from the #547 review
freshtonic ecd3f38
feat(stack): encryptedSupabaseV3 — EQL v3 dialect of the Supabase ada…
freshtonic d15414a
feat(cli): EQL v3 install path — vendored bundles + --eql-version flag
freshtonic 542095d
test(stack): live Supabase suites for v3 + the missing v2 range baseline
freshtonic cc62407
docs(stack): EQL v3 Supabase docs + changeset
freshtonic 4a7d060
fix(stack,cli): address #547 review — operand/typing guards, v3-aware…
freshtonic b9a5f20
chore: retrigger CI after rebase onto main
freshtonic 36a9b45
fix(stack): matrix-live-pg — ob-carrying text domains cannot store ''
freshtonic 6374804
fix(stack): matrix-live-pg storage proof — parse lone-decrypt output …
freshtonic 21f8f3e
docs(stack,cli): follow the stash db install → stash eql install rename
freshtonic a580c48
chore(stack,cli): re-vendor EQL v3 bundle from upstream eql-3.0.0-alp…
freshtonic d28070b
docs(stack,cli): re-baseline EQL v3 Supabase docs on eql-3.0.0-alpha.2
freshtonic e626025
refactor(stack)!: rename EQL v3 scalar domains to SQL-standard names …
freshtonic ba91f7c
feat(stack): adopt protect-ffi 0.27 and wire eqlVersion through the c…
freshtonic fe80490
fix(stack,cli): integrate the re-baseline — v3 wire in live suites, h…
freshtonic f3e4c3e
test(stack): bulkDecrypt malformed ciphertexts are per-item errors un…
freshtonic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| --- | ||
| '@cipherstash/stack': minor | ||
| 'stash': minor | ||
| --- | ||
|
|
||
| Add EQL v3 Supabase support, baselined on the `eql-3.0.0-alpha.2` release. | ||
|
|
||
| `@cipherstash/stack/supabase` gains `encryptedSupabaseV3` — the EQL v3 | ||
| counterpart of `encryptedSupabase` for schemas authored with | ||
| `@cipherstash/stack/eql/v3`. The public surface and call shape are identical | ||
| to v2 (same filter methods, `withLockContext`, `audit`); only the schema type | ||
| and wire encoding differ. | ||
|
|
||
| **The v3 surface** is the `eql-3.0.0-alpha.2` release artifact: domains use | ||
| SQL-standard type names (`eql_v3.integer_ord`, `eql_v3.timestamp_ord`, | ||
| `eql_v3.boolean`, … mirrored by `types.IntegerOrd`, `types.TimestampOrd`, | ||
| `types.Boolean`, …), SEM internals live in a separate `eql_v3_internal` | ||
| schema (grant it roles, never expose it — only `eql_v3` goes in Supabase's | ||
| Exposed schemas), and envelopes are versioned `v: 3`. Envelope production | ||
| rides on `@cipherstash/protect-ffi` 0.27, which takes an `eqlVersion` so the | ||
| same client emits v2 or v3 payloads per schema. | ||
|
|
||
| **Adapter behaviour:** | ||
|
|
||
| - columns are stored in their native `eql_v3.*` domains (raw jsonb payloads, | ||
| no composite wrap), with JS property → DB column name resolution and `Date` | ||
| reconstruction from `cast_as` on decrypted rows; | ||
| - **INTERIM:** filter operands are full storage envelopes — every `eql_v3.*` | ||
| domain CHECK requires the storage keys, and the SQL operators coerce their | ||
| operand into the domain, so a term-only operand is rejected today. This is | ||
| a tracked workaround (Linear CIP-3402), not the design: a full-envelope | ||
| operand carries a real decryptable ciphertext plus all of the column's | ||
| index terms, and PostgREST filters travel in GET query strings, so operands | ||
| can land in URL logs, proxies, and Supabase request logs (query terms are | ||
| index-terms-only by design). The fix is an EQL-side term-only scalar query | ||
| envelope (the scalar analog of `eql_v3.jsonb_query`); | ||
| - `like`/`ilike` on encrypted columns are emitted as PostgREST `cs` | ||
| (bloom-filter `@>`) — the v3 domains define no LIKE operator. Substring | ||
| search currently also requires `include_original: false` on the match | ||
| index; that requirement is a symptom of the same interim full-envelope | ||
| operand and goes away with CIP-3402; | ||
| - filters on storage-only columns (e.g. `types.Boolean`) and null filter | ||
| values are rejected at the type level and at runtime. | ||
|
|
||
| The v3 builder's default row type is exactly the table's inferred plaintext | ||
| shape (no index-signature widening — widening would disable the storage-only | ||
| filter guard). Filtering or inserting plaintext passthrough columns requires | ||
| an explicit row type: `es.from<typeof users, UserRow>('users', users)`. | ||
|
|
||
| The CLI gains an EQL v3 path: `stash eql install --eql-version 3` installs the | ||
| vendored `eql-3.0.0-alpha.2` bundle (`--supabase` selects the opclass-stripped | ||
| variant and applies the role grants for both `eql_v3` and `eql_v3_internal`); | ||
| `stash db upgrade` also accepts `--eql-version`, and `stash db status` reports | ||
| v2 and v3 installs independently. The v2 `SUPABASE_PERMISSIONS_SQL` block is | ||
| now generated from a shared `supabasePermissionsSql(schemaName)` helper, with | ||
| `SUPABASE_PERMISSIONS_SQL_V3` covering the v3 schemas. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| --- | ||
| "@cipherstash/stack": minor | ||
| --- | ||
|
|
||
| Add the EQL v3 `text_search` authoring DSL on a new `@cipherstash/stack/eql/v3` | ||
| subpath (`types.TextSearch`, v3 `encryptedTable` / `buildEncryptConfig`). The v3 | ||
| builders emit the existing `EncryptConfig` shape, so encryption, payloads, and | ||
| query paths are unchanged at runtime. | ||
|
|
||
| Also widens the public client types (`EncryptionClientConfig.schemas`, | ||
| `EncryptOptions`, `SearchTerm`/`EncryptQueryOptions`) to a structural contract so | ||
| both v2 and v3 builders are accepted by `Encryption` / `encrypt` / `decrypt` / | ||
| `encryptQuery`. This is a backward-compatible widening — existing v2 usage is | ||
| unaffected. The structural contracts themselves (`BuildableColumn`, | ||
| `BuildableQueryColumn`, `BuildableV3QueryableColumn`, `BuildableTable`, | ||
| `BuildableTableColumns`) and the `encryptModel` return-type mapper | ||
| (`EncryptedFromBuildableTable`) are exported from `@cipherstash/stack/types` so | ||
| consumers can name them. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| --- | ||
| "@cipherstash/stack": minor | ||
| --- | ||
|
|
||
| Add a strongly-typed EQL v3 client surface on a new `@cipherstash/stack/v3` | ||
| subpath (`EncryptionV3`, `typedClient`, `TypedEncryptionClient`). It re-exports | ||
| the v3 `types` namespace and table API (from `@cipherstash/stack/eql/v3`), so a | ||
| single import provides everything needed to author and use a v3 schema. | ||
|
|
||
| Every method derives its types from the concrete `table` / `column` builder | ||
| arguments: | ||
|
|
||
| - `encrypt` / `encryptQuery` pin the plaintext to the column's domain type | ||
| (`text → string`, `int8 → bigint`, `timestamptz → Date`, …). | ||
| - `encryptQuery` constrains `queryType` to the column's capabilities and rejects | ||
| storage-only columns at compile time. | ||
| - `encryptModel` / `bulkEncryptModels` validate schema-column fields against their | ||
| inferred plaintext type (passthrough fields are untouched) and return a precise | ||
| encrypted model. | ||
| - `decryptModel` / `bulkDecryptModels` return the precise plaintext model, | ||
| reconstructing `Date` / `bigint` values from the encrypt-config `cast_as`. | ||
|
|
||
| Because the typed methods bind to the concrete branded v3 classes, a hand-rolled | ||
| structural table/column is rejected — closing the soundness gap where a non-branded | ||
| table could be encrypted at runtime while typed as plaintext. | ||
|
|
||
| Runtime behaviour is unchanged: the encrypt/query paths return the same operations | ||
| as the base client; only the model-decrypt paths add a per-column `Date` / `bigint` | ||
| reconstruction step. The v2 client surface (`Encryption`) is untouched. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| '@cipherstash/stack': minor | ||
| --- | ||
|
|
||
| Add EQL v3 schema builders for all generated SQL domains under `@cipherstash/stack/eql/v3`, exposed as the `types` namespace (one member per EQL v3 domain, e.g. `types.TextEq` / `types.Int4Ord` / `types.Timestamptz`), including explicit query capability metadata (`getQueryCapabilities()` / `isQueryable()`) and v3 table support in model encryption helpers (`encryptModel` / `bulkEncryptModels`). | ||
|
|
||
| Also widen the accepted plaintext input type for `encrypt` / `encryptQuery` to include `Date` and `bigint` (via the new `Plaintext` type), so v3 `date` / `timestamptz` / `int8` domains can be encrypted and queried with their natural JavaScript values. | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| name: "FTA Complexity (EQL v3)" | ||
|
|
||
| # Blocking complexity gate for the EQL v3 text-search schema. Runs the Fast | ||
| # TypeScript Analyzer (fta-cli) against the v3 source directory only and fails | ||
| # the check when any file exceeds the score cap (`--score-cap` in the | ||
| # `analyze:complexity` script). FTA is pure static source analysis, so this job | ||
| # needs no build step, database, or credentials. | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - 'main' | ||
| paths: | ||
| - 'packages/stack/src/eql/v3/**' | ||
| - 'packages/stack/package.json' | ||
| - '.github/workflows/fta-v3.yml' | ||
| pull_request: | ||
| branches: | ||
| - "**" | ||
| paths: | ||
| - 'packages/stack/src/eql/v3/**' | ||
| - 'packages/stack/package.json' | ||
| - '.github/workflows/fta-v3.yml' | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| fta: | ||
| name: Analyze v3 complexity | ||
| runs-on: blacksmith-4vcpu-ubuntu-2404 | ||
|
|
||
| steps: | ||
| - name: Checkout Repo | ||
| uses: actions/checkout@v6 | ||
|
|
||
| - uses: pnpm/action-setup@v6.0.8 | ||
| name: Install pnpm | ||
| with: | ||
| run_install: false | ||
|
|
||
| - name: Install Node.js | ||
| uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: 22 | ||
| cache: 'pnpm' | ||
|
|
||
| # node-pty's install hook falls back to `node-gyp rebuild` when no | ||
| # linux-x64 prebuild matches. pnpm/action-setup v6 no longer ships | ||
| # node-gyp on PATH, so install it explicitly. | ||
| - name: Install node-gyp | ||
| run: npm install -g node-gyp | ||
|
|
||
| - name: Install dependencies | ||
| run: pnpm install --frozen-lockfile | ||
|
|
||
| # Non-zero exit (score above the cap) fails the check — this is the | ||
| # blocking gate. No `continue-on-error`. | ||
| - name: Analyze v3 complexity | ||
| run: pnpm --filter @cipherstash/stack run analyze:complexity |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| # Query API Walkthrough — API → FFI → CipherStash Client | ||
|
|
||
| How a query value travels from the public API down to the Rust SDK across the FFI boundary. Terse by design. | ||
|
|
||
| ## Flow | ||
|
|
||
| ```mermaid | ||
| flowchart TD | ||
| subgraph JS["@cipherstash/stack (TypeScript)"] | ||
| A["User query builder<br/>ops.eq / Supabase filter / client.encryptQuery()"] | ||
| B["EncryptionClient.encryptQuery(value | terms[])<br/>encryption/index.ts:259"] | ||
| C["EncryptQueryOperation.execute()<br/>BatchEncryptQueryOperation.execute()"] | ||
| D["resolveIndexType() + queryTypeToFfi/QueryOp<br/>build QueryPayload{plaintext,column,table,indexType,queryOp}"] | ||
| E["validate: validateNumericValue<br/>assertValueIndexCompatibility"] | ||
| end | ||
|
|
||
| subgraph FFI["@cipherstash/protect-ffi (Neon bindings)"] | ||
| F["JS wrapper encryptQuery / encryptQueryBulk<br/>lib/index.cjs:155"] | ||
| G["native handle via @neon-rs/load<br/>lib/load.cjs:9"] | ||
| H["platform .node addon<br/>protect-ffi-darwin-arm64/index.node"] | ||
| end | ||
|
|
||
| subgraph RUST["CipherStash Client (Rust SDK)"] | ||
| I["EQL term generation<br/>ORE / match / unique / ste_vec"] | ||
| J["ZeroKMS key ops"] | ||
| end | ||
|
|
||
| A --> B --> C --> E --> D --> F --> G --> H --> I | ||
| I --> J | ||
| I -- "Encrypted | EncryptedQuery" --> F | ||
| F -- "formatEncryptedResult()" --> C | ||
| C -- "SQL/PostgREST WHERE clause" --> A | ||
| ``` | ||
|
|
||
| ## Layers | ||
|
|
||
| | # | Layer | Entry point | Role | | ||
| |---|-------|-------------|------| | ||
| | 1 | Public API | `encryption/index.ts:259/270` `encryptQuery()` | Overloaded: single value → `EncryptQueryOperation`; `ScalarQueryTerm[]` → `BatchEncryptQueryOperation`. | | ||
| | 1a | Query builders | `drizzle/operators.ts:976`, `supabase/query-builder.ts:44` | `eq/gt/...` operators & deferred builders that batch-encrypt RHS values, then emit a WHERE clause. | | ||
| | 2 | Operations | `operations/encrypt-query.ts:41`, `operations/batch-encrypt-query.ts:115` | `execute()`: validate → resolve index → call FFI. `*WithLockContext` resolves `LockContextInput` via `resolveLockContext` before the FFI call. | | ||
| | 3 | EQL resolution | `helpers/infer-index-type.ts:89`, `types.ts:292` | `resolveIndexType` + `queryTypeToFfi`/`queryTypeToQueryOp` map public `QueryTypeName` → FFI `indexType`/`queryOp`. | | ||
| | 4 | FFI JS wrapper | `protect-ffi/lib/index.cjs:155` | `encryptQuery`/`encryptQueryBulk` → `wrapAsync(native.*)`. | | ||
| | 5 | Native loader | `protect-ffi/lib/load.cjs:9` | `@neon-rs/load` proxies to the platform prebuilt `.node`. | | ||
| | 6 | Rust SDK | compiled into `.node` | CipherStash Client: encryption, EQL/ORE/STE-vec term gen, ZeroKMS. Not a JS dep — shipped inside the addon. | | ||
|
|
||
| ## Query-type mapping (Layer 3) | ||
|
|
||
| ```mermaid | ||
| flowchart LR | ||
| subgraph Public["QueryTypeName"] | ||
| eq[equality] | ||
| ord[orderAndRange] | ||
| txt[freeTextSearch] | ||
| sel[steVecSelector] | ||
| trm[steVecTerm] | ||
| end | ||
| subgraph FFI["indexType / queryOp"] | ||
| u[unique] | ||
| o[ore] | ||
| m[match] | ||
| sv[ste_vec] | ||
| end | ||
| eq --> u | ||
| ord --> o | ||
| txt --> m | ||
| sel --> sv | ||
| trm --> sv | ||
| ``` | ||
|
|
||
| ## Notes | ||
|
|
||
| - **Client init:** `EncryptionClient.init()` (`encryption/index.ts:81`) calls FFI `newClient()` once; the returned `Client` handle is passed into every `encryptQuery` call. | ||
| - **`cipherstashclient`** = the CipherStash Client **Rust SDK**, compiled via Neon into the platform `.node` binary inside `@cipherstash/protect-ffi`. It performs the actual crypto and talks to ZeroKMS. | ||
| - **Result shape:** `EncryptedQueryResult` (`types.ts:175`); shaped by `formatEncryptedResult(..., returnType)` (`eql` vs raw). | ||
| - **Version:** `package.json` pins `@cipherstash/protect-ffi@0.24.0` (installed tree observed at `0.23.0` — confirm before relying on it). | ||
| - `packages/protect/src/ffi/*` mirrors this flow under the older `protect` package name. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.