Initial Commit
This commit is contained in:
166
openspec/archive/2026-05-10-enterprise-srp/proposal.md
Normal file
166
openspec/archive/2026-05-10-enterprise-srp/proposal.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# `@suaveplan/srp` — SRP-6a zero-knowledge password proof (RFC 5054 / RFC 2945)
|
||||
|
||||
**Package**: `@suaveplan/srp` (NEW)
|
||||
**Status**: PROPOSED
|
||||
**Type**: new-package (greenfield migration into genesis; vaulted plugin, universal-tier)
|
||||
**Date**: 2026-05-10
|
||||
**Path**: `packages/vault/srp/`
|
||||
**Source audit**: `../suaveplan/enterprise/srp.md`
|
||||
|
||||
## Why
|
||||
|
||||
Vaulted's account-unlock flow today derives keys via 2SKD inside `@suaveplan/crypto-vault` (HMAC-SHA-256 → Argon2id → AUK), but the legacy server-authentication path that protects the verifier database in transit is SRP-6a (Stanford SRP, RFC 5054). The server stores `{ I, salt, v }` where `v = g^x mod N` and `x = H(salt | H(I | ":" | P))`; the password never leaves the client, and an attacker who steals the verifier file cannot replay it against the server or impersonate the user without solving a discrete-log problem on a 2048-bit safe prime. This is a zero-knowledge contract — server learns nothing about the password beyond the verifier — and it is the one piece of crypto that `crypto-vault` does NOT provide because SRP-6a is a network protocol, not a local KDF.
|
||||
|
||||
`@suaveplan/srp` is the missing piece. It supplies both client and server roles in a single universal-tier package so the same package powers a Bun-server vaulted backend and a browser vaulted client, runs the test-vector suite end-to-end against itself, and exposes the augmented-PAKE seam (`PakeProvider`) that lets a future migration to OPAQUE (draft-irtf-cfrg-opaque) drop in without changing the `@suaveplan/vault` consumer surface. The package coexists with `crypto-vault`'s 2SKD path: 2SKD derives the master key locally; SRP-6a authenticates the user to the server during sync. They are orthogonal.
|
||||
|
||||
The package is greenfield in genesis (no prior implementation in `packages/vault/`). Source mapping is therefore against the canonical RFCs and the original Wu paper, not against an upstream `packages/<plugin>/srp/` directory.
|
||||
|
||||
## What changes
|
||||
|
||||
### New package: `@suaveplan/srp`
|
||||
|
||||
- ADDED: `packages/vault/srp/` package scaffold (`package.json`, `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts`, `README.md`, `src/__benchmarks__/`).
|
||||
- ADDED: `src/groups/groups-2048.ts` — RFC 5054 Appendix A 2048-bit safe prime constants `N_2048: Uint8Array` (256 bytes), `g_2048 = 2`, `BIT_LENGTH = 2048`. Bundled by default — every consumer gets this group with no extra import.
|
||||
- ADDED: `src/groups/1024/groups-1024.ts` — RFC 5054 Appendix A 1024-bit group. Reachable only via `@suaveplan/srp/groups/1024` subpath; tree-shakeable.
|
||||
- ADDED: `src/groups/1536/groups-1536.ts` — RFC 5054 Appendix A 1536-bit group. Subpath `@suaveplan/srp/groups/1536`.
|
||||
- ADDED: `src/groups/3072/groups-3072.ts` — RFC 5054 Appendix A 3072-bit group. Subpath `@suaveplan/srp/groups/3072`.
|
||||
- ADDED: `src/groups/4096/groups-4096.ts` — RFC 5054 Appendix A 4096-bit group. Subpath `@suaveplan/srp/groups/4096`.
|
||||
- ADDED: `src/groups/6144/groups-6144.ts` — RFC 5054 Appendix A 6144-bit group. Subpath `@suaveplan/srp/groups/6144`.
|
||||
- ADDED: `src/groups/8192/groups-8192.ts` — RFC 5054 Appendix A 8192-bit group. Subpath `@suaveplan/srp/groups/8192`.
|
||||
- ADDED: `src/params/params.ts` — `SrpParams` type (`{ N: Uint8Array; g: Uint8Array; bitLength: number; hash: HashLike; algorithm: "SHA256" | "SHA1" }`), `SrpParamsSchema` Zod boundary, `params2048Sha256(hash)` constructor, validation rejecting groups whose `byteLength(N)` is not a multiple of 4 or whose `g` exceeds `N`.
|
||||
- ADDED: `src/math/pad.ts` — `PAD(x, N)` per RFC 5054 §2.5.3 — left-zero-extends `x` to `byteLength(N)`. Throws `SRP.INVALID_PARAMS` if `x.byteLength > N.byteLength`.
|
||||
- ADDED: `src/math/modpow.ts` — `modPow(base, exp, mod)` thin wrapper over `@noble/curves` big-integer modpow. Constant-time-ish (Montgomery ladder inside noble); never `BigInt ** BigInt % N`.
|
||||
- ADDED: `src/math/k.ts` — `computeK(params): Uint8Array` — `k = H(N | PAD(g))` per RFC 5054 §2.5.3.
|
||||
- ADDED: `src/math/u.ts` — `computeU(A, B, params): Uint8Array` — `u = H(PAD(A) | PAD(B))` per RFC 5054 §2.5.4. Throws `SRP.PUBLIC_KEY_INVALID` if `u ≡ 0 mod N`.
|
||||
- ADDED: `src/math/x.ts` — `computeX(I, P, salt, params): Uint8Array` — `x = H(salt | H(I | ":" | P))` per RFC 5054 §2.6.
|
||||
- ADDED: `src/verifier/verifier.ts` — `computeVerifier(I, P, salt, params): { x: Uint8Array; v: Uint8Array }` — `v = g^x mod N`. Server stores `{ I, salt, v }`.
|
||||
- ADDED: `src/client/client-start.ts` — `clientStartHandshake(rng, params): { a: Uint8Array; A: Uint8Array }`. `a` is `≥ 256` random bits drawn from injectable `rng: Rng` (`@suaveplan/types`). `A = g^a mod N`. Caller must send `A` to the server; if server signals `A mod N == 0`, client rebuilds with fresh `a`.
|
||||
- ADDED: `src/client/client-finish.ts` — `clientFinishHandshake(I, P, salt, A, B, a, params): { K: Uint8Array; M1: Uint8Array; S: Uint8Array }` — full `S = (B - k * g^x)^(a + u * x) mod N`, `K = H(S)`, `M1 = H(H(N) XOR H(g) | H(I) | salt | A | B | K)`. Throws `SRP.PUBLIC_KEY_INVALID` when `B mod N == 0` per RFC 5054 §2.5.4.
|
||||
- ADDED: `src/client/client-verify.ts` — `clientVerifyServerProof(M1, M2Server, K, A, params): void` — recomputes expected `M2 = H(A | M1 | K)`, constant-time-compares against `M2Server`, throws `SRP.MUTUAL_AUTH_FAILED` on mismatch.
|
||||
- ADDED: `src/server/server-start.ts` — `serverStartHandshake(v, rng, params): { b: Uint8Array; B: Uint8Array }` — `b ≥ 256` random bits, `B = (k * v + g^b) mod N`. Re-rolls automatically if `B mod N == 0`.
|
||||
- ADDED: `src/server/server-finish.ts` — `serverFinishHandshake(I, salt, A, B, b, v, M1Client, params): { K: Uint8Array; M2: Uint8Array; S: Uint8Array }` — verifies `A mod N != 0` (throws `SRP.PUBLIC_KEY_INVALID`), computes `S = (A * v^u)^b mod N`, derives expected `M1`, constant-time compares against `M1Client` (throws `SRP.AUTH_FAILED` on mismatch), emits `M2 = H(A | M1 | K)`.
|
||||
- ADDED: `src/server/server-m2.ts` — `serverComputeM2(A, M1, K): Uint8Array` — split-roundtrip helper for transports that ack `M1` over one HTTP request and emit `M2` from another worker / route.
|
||||
- ADDED: `src/pake/pake-provider.ts` — `PakeProvider` interface (`registerCredential`, `clientHandshake`, `serverHandshake`) + `srpPakeProvider(params): PakeProvider` SRP-6a implementation. Augmented PAKE bridge for future OPAQUE migration.
|
||||
- ADDED: `src/pake/pake-provider.types.ts` — `PakeProvider`, `PakeRegistration`, `PakeClientHandshake`, `PakeServerHandshake`, `PakeOutcome`.
|
||||
- ADDED: `src/hash/hash-bridge.ts` — `HashLike` re-export from `@suaveplan/types`, `nobleHashBridge(algorithm)` sync-only `@noble/hashes`-backed implementation, `subtleHashBridge(algorithm, signal?)` async `crypto.subtle.digest`-backed implementation, `defaultHashBridge(algorithm)` factory that picks subtle on browsers and noble in Node/Bun.
|
||||
- ADDED: `src/errors/errors.ts` — `defineCodes("SRP", ["INVALID_PARAMS", "PUBLIC_KEY_INVALID", "AUTH_FAILED", "MUTUAL_AUTH_FAILED", "GROUP_UNSUPPORTED", "HASH_UNSUPPORTED"])` registry + `SrpError extends SuaveplanError<SrpErrorCode>`.
|
||||
- ADDED: `src/observability/metrics.ts` — `srpMetrics` factory: `srp.handshake_duration_ms{role,phase}` histogram (`role ∈ "client" | "server"`, `phase ∈ "start" | "finish"`), `srp.outcome_total{role,outcome}` counter (`outcome ∈ "ok" | "auth_failed" | "mutual_auth_failed" | "public_key_invalid"`), `srp.errors_total{code}` counter.
|
||||
- ADDED: `src/schemas/schemas.ts` — `SrpParamsSchema`, `SrpHandshakeStateSchema`, `VerifierSchema`. Boundary-validated; mismatch → `SRP.INVALID_PARAMS`.
|
||||
- ADDED: `src/index.ts` — explicit named re-exports of every public surface (no `export *`).
|
||||
- ADDED: `scripts/generate-srp-vectors.ts` — derives reproducible `(I, P, salt, params) → { x, v, A, B, K, M1, M2 }` test vectors for any `(group, hash)` pair. Used to ship reference data for the 2048/SHA-256 default that RFC 5054 omits.
|
||||
- ADDED: `src/__benchmarks__/srp.bench.ts` — mitata benchmarks: `client_handshake_2048_sha256`, `server_handshake_2048_sha256`, `compute_verifier_2048_sha256`.
|
||||
|
||||
### Subpath exports
|
||||
|
||||
- `.` — root barrel
|
||||
- `./groups` — default 2048-bit group constants
|
||||
- `./groups/1024`, `./groups/1536`, `./groups/3072`, `./groups/4096`, `./groups/6144`, `./groups/8192` — optional groups, tree-shakeable
|
||||
- `./params` — `SrpParams` + factory + Zod schema
|
||||
- `./client` — `clientStartHandshake`, `clientFinishHandshake`, `clientVerifyServerProof`
|
||||
- `./server` — `serverStartHandshake`, `serverFinishHandshake`, `serverComputeM2`
|
||||
- `./verifier` — `computeVerifier`
|
||||
- `./pake` — `PakeProvider` interface + `srpPakeProvider`
|
||||
- `./hash` — `nobleHashBridge`, `subtleHashBridge`, `defaultHashBridge`
|
||||
- `./errors` — `SRP_CODES`, `SrpError`, `SrpErrorCode`
|
||||
|
||||
### Dependencies
|
||||
|
||||
- `@suaveplan/error workspace:^` — `defineCodes`, `SuaveplanError`
|
||||
- `@suaveplan/types workspace:^` — `Rng`, `Clock`, `HashLike`
|
||||
- `@suaveplan/crypto-utils workspace:^` — `constantTimeEqual`, `randomBytes`, `concat`, `xor`, hex helpers, `zeroize`
|
||||
- `@suaveplan/crypto-hash workspace:^` — SHA-256 (default), SHA-1 (legacy compat)
|
||||
- `@noble/curves catalog:crypto` — big-integer modpow (Montgomery ladder, hardened)
|
||||
- `@noble/hashes catalog:crypto` — sync streaming hash backend
|
||||
- `@opentelemetry/api catalog:observability` — span + metric emission
|
||||
- `tslib catalog:build`
|
||||
- `zod catalog:forms` — `SrpParamsSchema`, `SrpHandshakeStateSchema`
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **OPAQUE implementation** — only the `PakeProvider` seam ships. A real OPAQUE provider lives in a future `@suaveplan/opaque` package; no work in this change.
|
||||
- **TLS-SRP integration** (RFC 5054 §2 cipher-suite handshake) — genesis does not run a TLS stack; SRP-6a here is application-layer.
|
||||
- **CLDR plural / locale handling for error messages** — error codes are stable identifiers; localized presentation happens upstream via `@suaveplan/i18n`.
|
||||
- **SRP-3 / SRP-6 (non-`a`) compatibility** — RFC 5054 deprecates earlier SRP variants; only SRP-6a ships.
|
||||
- **CLI front-end / `srp-cli` binary** — application code, not library.
|
||||
- **Browser-tier `crypto.subtle` polyfill for Node < 20** — Node ≥ 20 ships `globalThis.crypto.subtle`; older runtimes use `nobleHashBridge` directly.
|
||||
- **Persistent verifier storage** — server-side `{ I, salt, v }` storage is the consumer's responsibility (`@suaveplan/db-sqlite`, `@suaveplan/storage`); this package emits the verifier and does not own its lifecycle.
|
||||
|
||||
## Source mapping
|
||||
|
||||
This change is greenfield in genesis. The source-of-truth audit is `../suaveplan/enterprise/srp.md`; the canonical math sources are RFC 5054, RFC 2945, and Wu's original SRP paper. Every requirement maps to a precise section.
|
||||
|
||||
| Requirement | Source | Section / lines | Notes |
|
||||
|---|---|---|---|
|
||||
| 2048-bit safe prime + `g = 2` bundled by default | RFC 5054 | Appendix A, "1024-bit Group through 8192-bit Group" — 2048-bit block | `N` 256 bytes, `g = 2` |
|
||||
| Optional 1024 / 1536 / 3072 / 4096 / 6144 / 8192 groups under subpaths | RFC 5054 | Appendix A | Tree-shakeable per genesis convention §6 |
|
||||
| `PAD(x, N)` zero-extend to `byteLength(N)` | RFC 5054 | §2.5.3 | Mandatory before every hash mixing group elements |
|
||||
| `k = H(N \| PAD(g))` | RFC 5054 | §2.5.3 | Multiplier parameter |
|
||||
| Hash function — SHA-256 default, SHA-1 legacy opt-in | RFC 5054 | §3 (TLS context mandates SHA-1); modern deployments — `enterprise/srp.md` L22 | RFC mandates SHA-1 for TLS only; modern SRP defaults to SHA-256 |
|
||||
| `computeVerifier(I, P, salt, params)` — `x = H(salt \| H(I \| ":" \| P))`, `v = g^x mod N` | RFC 5054 | §2.6 (verifier definition) | `enterprise/srp.md` L38 |
|
||||
| `clientStartHandshake` — `a ≥ 256` random bits, `A = g^a mod N` | RFC 5054 | §2.5.4 paragraph 2 | RFC requires `a` of "at least 256 bits" |
|
||||
| Reject `A mod N == 0` and `B mod N == 0` | RFC 5054 | §2.5.4 paragraph 4 ("MUST abort") | Both client and server abort |
|
||||
| `clientFinishHandshake` — `S = (B - k * g^x)^(a + u * x) mod N`, `K = H(S)` | RFC 5054 | §2.6 + §2.5.4 | `enterprise/srp.md` L40 |
|
||||
| `M1 = H(H(N) XOR H(g) \| H(I) \| s \| A \| B \| K)` | RFC 2945 | §3 ("M = H(H(N) XOR H(g), H(U), s, A, B, K)") + Wu §3 | `enterprise/srp.md` L24 |
|
||||
| `M2 = H(A \| M1 \| K)` | RFC 2945 | §3 (server response proof) | `enterprise/srp.md` L25 |
|
||||
| `serverStartHandshake` — `B = (k * v + g^b) mod N` | RFC 5054 | §2.5.3 | Augmented form distinguishing SRP-6a from SRP-6 |
|
||||
| `serverFinishHandshake` — `S = (A * v^u)^b mod N` | RFC 5054 | §2.6 | Server-side shared-secret derivation |
|
||||
| `serverComputeM2` split-roundtrip helper | Wu | §3 ("once the server has verified the client's evidence, it computes its own evidence") | Splittable per consumer transport |
|
||||
| Constant-time compare on every `M1` / `M2` / `S` | Wu | §6 (security considerations) + RFC 5054 §2.5.4 | `enterprise/srp.md` L26 |
|
||||
| Modpow via `@noble/curves` (never `BigInt ** BigInt % N`) | RFC 5054 | §2.5.4 (timing-attack guidance) + `enterprise/srp.md` L33 | TypeScript `BigInt` not constant-time |
|
||||
| RFC 5054 Appendix B test vectors (1024-bit) | RFC 5054 | Appendix B in full | Reference correctness check |
|
||||
| Local 2048-bit + SHA-256 vectors via `scripts/generate-srp-vectors.ts` | `enterprise/srp.md` | L67 | RFC has no SHA-256 vectors |
|
||||
| Property test — `clientFinishHandshake` and `serverFinishHandshake` produce the same `K` | Wu | §3 ("both endpoints derive the same K") | `enterprise/srp.md` L59 |
|
||||
| Augmented PAKE bridge — `PakeProvider` interface | `enterprise/srp.md` | L65 | Future OPAQUE drop-in |
|
||||
| `HashLike` injectable + `nobleHashBridge` / `subtleHashBridge` | `enterprise/srp.md` | L66 | Browser-friendly streaming hash |
|
||||
| AbortSignal threaded through async paths | `enterprise/srp.md` | L55 | `subtleHashBridge` is async |
|
||||
| OTel — `srp.handshake_duration_ms{role,phase}`, `srp.outcome_total`, `srp.errors_total` | `enterprise/srp.md` | L54 | Standard genesis observability surface |
|
||||
| Zod boundary schemas — `SrpParamsSchema`, `SrpHandshakeStateSchema` | `enterprise/srp.md` | L52 | Standard genesis Zod-at-boundaries pattern |
|
||||
| 6 error codes (`INVALID_PARAMS`, `PUBLIC_KEY_INVALID`, `AUTH_FAILED`, `MUTUAL_AUTH_FAILED`, `GROUP_UNSUPPORTED`, `HASH_UNSUPPORTED`) | `enterprise/srp.md` | L53 | Every code thrown by ≥1 production path |
|
||||
| Mitata benchmarks — `client_handshake_2048_sha256`, `server_handshake_2048_sha256`, `compute_verifier_2048_sha256` | `enterprise/srp.md` | L68 | Standard genesis benchmark surface |
|
||||
| `bun scripts/generate-srp-vectors.ts` | `enterprise/srp.md` | L67 | Reproducible test-vector generator |
|
||||
|
||||
### Requirements with no source line
|
||||
|
||||
| Requirement | Justification |
|
||||
|---|---|
|
||||
| Subpath exports for groups (`./groups/1024` etc.) | Genesis convention §6 — tree-shakeable subpath per feature module. Source doc says "under `./groups/<size>` subpaths"; no specific line numbering needed. |
|
||||
| `defaultHashBridge(algorithm)` factory selecting subtle vs noble | Genesis convention — universal-tier packages must work in browser AND server with no runtime branching cost; the factory captures the platform decision once. |
|
||||
| `srpPakeProvider(params): PakeProvider` concrete factory | The `PakeProvider` interface alone is unimplementable until SRP-6a wires into it; both the seam and its first implementation ship in this change. |
|
||||
|
||||
## Verification approach
|
||||
|
||||
Each requirement carries a `**Verified by:**` line in the spec delta. Test files:
|
||||
|
||||
- `packages/vault/srp/src/groups/groups-2048.test.ts` — RFC 5054 Appendix A 2048-bit constants byte-exact; `g = 2`; `bitLength = 2048`
|
||||
- `packages/vault/srp/src/groups/1024/groups-1024.test.ts` — RFC 5054 Appendix A 1024-bit constants byte-exact (used by RFC Appendix B vectors)
|
||||
- `packages/vault/srp/src/groups/groups.spec-conformance.test.ts` — every documented subpath resolves; banned `BigInt ** BigInt %` pattern absent from any group constant file
|
||||
- `packages/vault/srp/src/params/params.test.ts` — `params2048Sha256` builds, `SrpParamsSchema` rejects non-multiple-of-4 N, rejects `g >= N`
|
||||
- `packages/vault/srp/src/math/pad.test.ts` — `PAD` left-zero-extends, throws `SRP.INVALID_PARAMS` on overflow
|
||||
- `packages/vault/srp/src/math/k.test.ts` — `k` matches RFC 5054 Appendix B 1024-bit reference value
|
||||
- `packages/vault/srp/src/math/u.test.ts` — `u` matches RFC 5054 Appendix B; throws `SRP.PUBLIC_KEY_INVALID` when `u ≡ 0`
|
||||
- `packages/vault/srp/src/math/x.test.ts` — `x` matches RFC 5054 Appendix B reference value
|
||||
- `packages/vault/srp/src/math/modpow.test.ts` — agrees with naive `BigInt` reference on small inputs (banned in production but allowed in tests)
|
||||
- `packages/vault/srp/src/verifier/verifier.test.ts` — RFC 5054 Appendix B `v` matches; locally-derived 2048/SHA-256 vector matches
|
||||
- `packages/vault/srp/src/client/client-start.test.ts` — `a` is ≥ 256 bits; `A` reproducible from injected RNG
|
||||
- `packages/vault/srp/src/client/client-finish.test.ts` — RFC 5054 Appendix B `K` matches; throws `SRP.PUBLIC_KEY_INVALID` on `B ≡ 0 mod N`
|
||||
- `packages/vault/srp/src/client/client-verify.test.ts` — accepts correct `M2`; throws `SRP.MUTUAL_AUTH_FAILED` on bit-flipped `M2`; constant-time compare path exercised
|
||||
- `packages/vault/srp/src/server/server-start.test.ts` — `B` reproducible from injected RNG; re-rolls when synthetic RNG returns `b` such that `B ≡ 0 mod N`
|
||||
- `packages/vault/srp/src/server/server-finish.test.ts` — accepts correct `M1`; throws `SRP.AUTH_FAILED` on mismatched `M1`; throws `SRP.PUBLIC_KEY_INVALID` on `A ≡ 0`
|
||||
- `packages/vault/srp/src/server/server-m2.test.ts` — split-roundtrip `M2` matches inline `serverFinishHandshake` output
|
||||
- `packages/vault/srp/src/pake/pake-provider.test.ts` — `srpPakeProvider` round-trips through `register → clientHandshake → serverHandshake`; outcome `K` matches between client and server
|
||||
- `packages/vault/srp/src/hash/hash-bridge.test.ts` — `nobleHashBridge("SHA256")` and `subtleHashBridge("SHA256")` produce byte-identical digests on the RFC 5054 Appendix B input; SHA-1 path exercised; AbortSignal pre-aborted on `subtleHashBridge` throws `SRP.HASH_UNSUPPORTED` (or wraps the `AbortError`)
|
||||
- `packages/vault/srp/src/errors/errors.test.ts` — every code in the registry is thrown by at least one test path
|
||||
- `packages/vault/srp/src/observability/metrics.test.ts` — every histogram and counter emitted with expected attributes during a full handshake
|
||||
- `packages/vault/srp/src/schemas/schemas.test.ts` — `SrpParamsSchema` rejects malformed shapes; `SrpHandshakeStateSchema` enforces invariants
|
||||
- `packages/vault/srp/src/srp.spec-conformance.test.ts` — RFC 5054 Appendix B end-to-end + locally-generated 2048/SHA-256 end-to-end + property test (1000 random `(I, P, salt)` triples → `K_client === K_server`) + every "MUST abort" condition (`A ≡ 0`, `B ≡ 0`, `u ≡ 0`) triggers the right error code
|
||||
- `packages/vault/srp/src/index.test.ts` — every documented subpath export resolves
|
||||
|
||||
## Migration impact
|
||||
|
||||
None — additive only. No existing genesis package imports `@suaveplan/srp`. `@suaveplan/vault` will adopt the legacy SRP path in a follow-up change once this package ships; this change does NOT modify `@suaveplan/vault`.
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
`bunx turbo ls --filter=...@suaveplan/srp` returns nothing — this is a new package with zero existing consumers in genesis. The eventual consumer (`@suaveplan/vault` legacy server-auth path) lands in a separate change. No dependents (verified by enumeration of `packages/*/*/package.json` for `@suaveplan/srp` in dependency lists).
|
||||
Reference in New Issue
Block a user