@lemmaoracle/seal 0.1.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,23 +3,27 @@
3
3
  ZK auth circuit for **Proof-based sign-in** to the Lemma developer dashboard.
4
4
 
5
5
  `seal` lets a developer prove they hold a valid Lemma API key **without
6
- revealing the key**. It is a [zero-knowledge proof](https://lemma.frame00.com)
7
- of the pre-image of the key's SHA-256 hash.
6
+ revealing the key or even which key it is**. It produces a per-session
7
+ Poseidon nullifier that is unique to the (secret, nonce) pair but reveals
8
+ nothing about the underlying secret or its SHA-256 hash.
8
9
 
9
10
  ## How it works
10
11
 
11
12
  The dashboard's sign-in flow:
12
13
 
13
14
  1. The dashboard BFF issues a challenge `nonce`.
14
- 2. The developer generates a `seal` proof: it proves knowledge of the
15
- API key whose SHA-256 hash equals the `key_hash` stored in the
16
- workers `api_keys` table, bound to that `nonce`.
17
- 3. The BFF verifies the proof, reads the attested `key_hash`, looks it
18
- up in `api_keys`, and resolves the caller's `scope_id`.
19
- 4. The BFF issues a session token tied to the scope.
20
-
21
- The proof reveals only the `key_hash` (already public, since it is what
22
- the API stores) and the `nonce` — never the API key itself.
15
+ 2. The developer generates a `seal` proof: it proves knowledge of *a*
16
+ secret whose SHA-256 hash is registered in the workers `api_keys`
17
+ table, bound to that `nonce`. The public output is a **nullifier** —
18
+ `Poseidon(keyHash_hi, keyHash_lo, nonce)` not the key hash itself.
19
+ 3. The BFF verifies the proof, then iterates registered `key_hash`
20
+ values in D1, computing the expected nullifier for each until it
21
+ finds a match (O(N), sub-millisecond per check).
22
+ 4. The BFF issues a session token tied to the resolved scope.
23
+
24
+ The proof reveals neither the secret nor its hash — only the nullifier
25
+ and the nonce. Because `nonce` changes every session, nullifiers cannot
26
+ be correlated across sign-ins, even for the same secret.
23
27
 
24
28
  > Proof-based sign-in requires an existing API key. First-time users
25
29
  > onboard via GitHub OAuth, which issues their first key.
@@ -29,24 +33,20 @@ the API stores) and the `nonce` — never the API key itself.
29
33
  ```
30
34
  seal/
31
35
  ├── circuits/
32
- │ ├── src/seal-identity.circom Circuit: SHA-256 pre-image proof
36
+ │ ├── src/seal-identity.circom Circuit: SHA-256 pre-image proof + Poseidon nullifier
33
37
  │ ├── src/seal-identity.test.ts Circuit witness tests (needs a build)
34
38
  │ └── scripts/build.sh Compile circom → wasm + groth16 setup
35
39
  ├── scripts/
36
40
  │ ├── register-circuit.ts Pin artifacts to IPFS, register via SDK
37
41
  │ └── setup-toolchain.sh Install Rust + circom
38
42
  ├── src/ TypeScript proof helpers (published)
39
- │ ├── bits.ts API key ↔ circuit signal conversions
40
- │ ├── proof.ts generateSealProof / verifySealProof
43
+ │ ├── bits.ts Secret ↔ circuit signal conversions
44
+ │ ├── proof.ts prove / verify (delegates to @lemmaoracle/sdk)
45
+ │ ├── vkey.ts Bundled verification key + named export
41
46
  │ └── index.ts Public API
42
47
  └── .env.example Credentials for register-circuit.ts
43
48
  ```
44
49
 
45
- There is no `normalize/` step: the circuit's witness input is the raw
46
- API key string (already canonical), so seal uses the existing
47
- [`passthrough-v1`](../passthrough) schema rather than a bespoke
48
- normalizer.
49
-
50
50
  `seal` is a **reference definition**. It is published to npm so
51
51
  developers can generate proofs; it is *not* imported by `workers` or the
52
52
  dashboard at runtime — the circuit reaches them through the normal
@@ -54,18 +54,21 @@ Lemma circuit-registration path.
54
54
 
55
55
  ## The circuit
56
56
 
57
- `seal-identity.circom` proves `SHA-256(apiKey) == keyHash`:
57
+ `seal-identity.circom` (v2) proves knowledge of a registered secret and
58
+ outputs a per-session nullifier:
58
59
 
59
- - **Private input** — `keyBits[512]`: the 64-byte ASCII API key as bits.
60
+ - **Private input** — `keyBits[512]`: the 64-byte ASCII secret as bits.
60
61
  Lemma keys are 32 random bytes rendered as 64 hex characters (see the
61
62
  workers `generate_api_key.js`).
62
63
  - **Public input** — `nonce`: the dashboard challenge, bound into the
63
64
  constraint system for replay protection.
64
- - **Public output** — `keyHash[256]`: the SHA-256 digest bits.
65
+ - **Public output** — `nullifier`: `Poseidon(keyHash_hi, keyHash_lo, nonce)`.
66
+ A single BN254 field element; unique per (secret, nonce) pair. Add ~300
67
+ constraints on top of the SHA-256 (~60k total).
65
68
 
66
- The hashing matches the workers `middleware/auth.ts`
67
- (`SHA-256(utf8_bytes(apiKey))`), so a proof's `keyHash` is directly
68
- comparable to `api_keys.key_hash`.
69
+ The SHA-256 hashing matches the workers `middleware/auth.ts`
70
+ (`SHA-256(utf8_bytes(secret))`), but `keyHash` is now an intermediate
71
+ signal it never appears in public signals.
69
72
 
70
73
  ## Build the circuit
71
74
 
@@ -81,7 +84,8 @@ cd circuits && npm install && npm run build
81
84
  ```
82
85
 
83
86
  `build.sh` downloads the 2^17 Hermez powers-of-tau file (~290 MB) on
84
- first run SHA-256 over a 512-bit pre-image is ~60k constraints.
87
+ first run. The v2 circuit adds Poseidon (~300 constraints) to the SHA-256
88
+ base (~60k), staying well within the 2^17 budget.
85
89
 
86
90
  ## Register the circuit
87
91
 
@@ -99,15 +103,66 @@ dashboard BFF then fetches verification params at runtime via
99
103
  ## Generate a proof (developer usage)
100
104
 
101
105
  ```ts
102
- import { generateSealProof } from "@lemmaoracle/seal";
106
+ import * as seal from "@lemmaoracle/seal";
107
+
108
+ const { proof, publicSignals, nullifier } = await seal.prove({
109
+ secret: process.env.LEMMA_API_KEY!,
110
+ nonce: challengeNonce,
111
+ });
112
+ // POST { proof, publicSignals, token } to the dashboard sign-in endpoint.
113
+ ```
114
+
115
+ Circuit artifacts (wasm, zkey) are resolved automatically via the
116
+ `@lemmaoracle/sdk` from the registered circuit metadata — no local
117
+ artifact paths are required.
118
+
119
+ ## Verify a proof
120
+
121
+ ```ts
122
+ import * as seal from "@lemmaoracle/seal";
103
123
 
104
- const { proof, publicSignals, keyHash } = await generateSealProof(
105
- { apiKey: process.env.LEMMA_API_KEY!, nonce: challengeNonce },
106
- { wasm: "seal-identity.wasm", zkey: "seal-identity_final.zkey" },
107
- );
108
- // POST { proof, publicSignals } to the dashboard sign-in endpoint.
124
+ const result = await seal.verify({ proof, publicSignals, nullifier });
125
+ // result: { nullifier, nonce } | null
109
126
  ```
110
127
 
128
+ The verification key is bundled internally — no additional arguments are
129
+ needed. Verification delegates to `@lemmaoracle/sdk` verifier.
130
+
131
+ ## Access the verification key
132
+
133
+ ```ts
134
+ import { sealVkey } from "@lemmaoracle/seal";
135
+ // Or via the dedicated sub-export:
136
+ import vkey from "@lemmaoracle/seal/vkey";
137
+ ```
138
+
139
+ ## v2 migration notes
140
+
141
+ v1 exposed `keyHash[256]` as a public output, making the key hash
142
+ readable by any observer of the proof transcript. v2 replaces this with
143
+ a Poseidon nullifier that is uncorrelatable across sessions.
144
+
145
+ Breaking changes:
146
+ - `SEAL_CIRCUIT_ID` is now `"seal-identity-v1"` (requires re-registration).
147
+ - `SealProof.keyHash` removed; replaced by `SealProof.nullifier`.
148
+ - Server-side: `scopeIdForKeyHash()` replaced by `scopeIdForNullifier()`
149
+ (full D1 scan + `poseidon-lite` computation).
150
+ - Circuit artifacts (wasm, zkey, vkey) must be regenerated.
151
+
152
+ v3 migration notes:
153
+ - `generateSealProof` renamed to `prove`; `verifySealProof` renamed to `verify`.
154
+ - `SealProofInput.apiKey` renamed to `SealProofInput.secret`.
155
+ - `apiKeyToBits` renamed to `secretToBits`; `SEAL_KEY_BYTES`/`SEAL_KEY_BITS`
156
+ renamed to `SEAL_SECRET_BYTES`/`SEAL_SECRET_BITS`.
157
+ - `SealArtifacts` type removed — circuit artifacts are resolved
158
+ automatically via `@lemmaoracle/sdk`.
159
+ - `prove` no longer requires a second `artifacts` argument.
160
+ - `verify` no longer requires a `verificationKey` argument — the vkey is
161
+ bundled internally.
162
+ - `sealVkey` named export added for direct vkey access.
163
+ - `snarkjs` is no longer a direct dependency — it is used via
164
+ `@lemmaoracle/sdk`.
165
+
111
166
  ## Scripts
112
167
 
113
168
  | Command | Description |
@@ -1,20 +1,36 @@
1
1
  pragma circom 2.1.0;
2
2
 
3
3
  include "circomlib/circuits/sha256/sha256.circom";
4
+ include "circomlib/circuits/poseidon.circom";
5
+ include "circomlib/circuits/bitify.circom";
4
6
 
5
7
  /**
6
8
  * SealIdentity — Proof-based sign-in for the Lemma developer dashboard.
7
9
  *
8
- * Proves knowledge of the pre-image of an API key hash without revealing
9
- * the key. The pre-image is the raw API key string; the hash is the
10
- * SHA-256 `key_hash` stored in the workers `api_keys` D1 table.
10
+ * **v2 (nullifier)** Replaces the v1 `keyHash` public output with a
11
+ * per-session Poseidon nullifier. The `keyHash` is now an intermediate
12
+ * signal that never appears in public signals, so neither the verifier
13
+ * nor any network observer can link proofs to a specific API key or
14
+ * correlate a user across sessions.
11
15
  *
12
- * This matches the hashing in workers `middleware/auth.ts`:
13
- * key_hash = SHA-256( utf8_bytes(apiKey) )
16
+ * Proves: "I hold the API key whose SHA-256 hash is registered in
17
+ * `api_keys`," without revealing which key.
18
+ *
19
+ * The nullifier is derived as:
20
+ * nullifier = Poseidon(keyHash_hi, keyHash_lo, nonce)
21
+ *
22
+ * where `keyHash_hi` and `keyHash_lo` are the upper and lower 128 bits
23
+ * of the SHA-256 digest, interpreted as BN254 field elements. Because
24
+ * `nonce` is unique per session, the nullifier is unique per session —
25
+ * even if the same API key generates two proofs in a row, the nullifiers
26
+ * are unrelated.
27
+ *
28
+ * The dashboard BFF verifies the proof, then iterates over registered
29
+ * `key_hash` values in D1, computing the expected nullifier for each
30
+ * until it finds a match (O(N), Poseidon is ~300 constraints but
31
+ * sub-millisecond in JS). This preserves the "key holder proves
32
+ * identity" invariant while removing `keyHash` from the wire entirely.
14
33
  *
15
- * Lemma API keys are 32 random bytes rendered as a 64-character
16
- * lowercase hex string (see workers `generate_api_key.js`), so the
17
- * SHA-256 pre-image is exactly 64 ASCII bytes = 512 bits.
18
34
  *
19
35
  * Private input:
20
36
  * keyBits[512] Bit decomposition of the 64-byte ASCII API key,
@@ -26,16 +42,16 @@ include "circomlib/circuits/sha256/sha256.circom";
26
42
  * against a different challenge.
27
43
  *
28
44
  * Public output:
29
- * keyHash[256] SHA-256 digest bits. The dashboard BFF reassembles
30
- * these into the hex `key_hash` and looks it up in
31
- * `api_keys` to resolve the caller's `scope_id`.
45
+ * nullifier Poseidon-keyed session nullifier. Unique per
46
+ * (key, nonce) pair, reveals nothing about the key
47
+ * or its hash.
32
48
  */
33
49
  template SealIdentity(keyBytes) {
34
50
  var keyBitLen = keyBytes * 8;
35
51
 
36
52
  signal input keyBits[keyBitLen];
37
53
  signal input nonce;
38
- signal output keyHash[256];
54
+ signal output nullifier;
39
55
 
40
56
  // Constrain every pre-image bit to be boolean. Without this a
41
57
  // malicious prover could feed non-binary field values into the
@@ -50,10 +66,32 @@ template SealIdentity(keyBytes) {
50
66
  for (var i = 0; i < keyBitLen; i++) {
51
67
  sha.in[i] <== keyBits[i];
52
68
  }
69
+
70
+ // keyHash is an intermediate signal — never exposed publicly.
71
+ signal keyHash[256];
53
72
  for (var i = 0; i < 256; i++) {
54
73
  keyHash[i] <== sha.out[i];
55
74
  }
56
75
 
76
+ // Split the 256-bit keyHash into two 128-bit field elements.
77
+ // Bits2Num expects LSB-first input; keyHash is MSB-first (SHA-256
78
+ // convention), so we reverse the bit order on connection.
79
+ component hiBits = Bits2Num(128);
80
+ component loBits = Bits2Num(128);
81
+ for (var i = 0; i < 128; i++) {
82
+ hiBits.in[i] <== keyHash[127 - i];
83
+ loBits.in[i] <== keyHash[255 - i];
84
+ }
85
+
86
+ // Per-session nullifier: Poseidon(keyHash_hi, keyHash_lo, nonce).
87
+ // Adds ~300 constraints (vs ~30,000 for an extra SHA-256).
88
+ component hash = Poseidon(3);
89
+ hash.inputs[0] <== hiBits.out;
90
+ hash.inputs[1] <== loBits.out;
91
+ hash.inputs[2] <== nonce;
92
+
93
+ nullifier <== hash.out;
94
+
57
95
  // Bind the challenge nonce into the constraint system. The squaring
58
96
  // is otherwise meaningless — its only purpose is to make `nonce` a
59
97
  // constrained public signal, so the verifier knows the proof was
package/dist/bits.d.ts CHANGED
@@ -1,20 +1,20 @@
1
1
  /**
2
- * Bit-level conversions between API key strings, circuit witness
2
+ * Bit-level conversions between secret strings, circuit witness
3
3
  * signals, and SHA-256 hashes. Pure and dependency-free.
4
4
  */
5
5
  import type { Bit } from "./types.js";
6
- /** The seal circuit's fixed pre-image length: a 64-byte ASCII API key. */
7
- export declare const SEAL_KEY_BYTES = 64;
6
+ /** The seal circuit's fixed pre-image length: a 64-byte ASCII secret. */
7
+ export declare const SEAL_SECRET_BYTES = 64;
8
8
  /** The seal circuit's pre-image bit length (512). */
9
- export declare const SEAL_KEY_BITS: number;
9
+ export declare const SEAL_SECRET_BITS: number;
10
10
  /**
11
- * Convert a raw API key string into the `keyBits` witness signal — the
12
- * UTF-8 bytes of the key, MSB-first per byte.
11
+ * Convert a raw secret string into the `keyBits` witness signal — the
12
+ * UTF-8 bytes of the secret, MSB-first per byte.
13
13
  *
14
- * Throws if the key is not exactly {@link SEAL_KEY_BYTES} ASCII bytes,
14
+ * Throws if the secret is not exactly {@link SEAL_SECRET_BYTES} ASCII bytes,
15
15
  * since the circuit's pre-image length is fixed.
16
16
  */
17
- export declare const apiKeyToBits: (apiKey: string) => ReadonlyArray<Bit>;
17
+ export declare const secretToBits: (secret: string) => ReadonlyArray<Bit>;
18
18
  /**
19
19
  * Reassemble the circuit's 256-bit `keyHash` public output into the
20
20
  * lowercase hex `key_hash` string stored in the `api_keys` table.
@@ -1 +1 @@
1
- {"version":3,"file":"bits.d.ts","sourceRoot":"","sources":["../src/bits.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAEtC,0EAA0E;AAC1E,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC,qDAAqD;AACrD,eAAO,MAAM,aAAa,QAAqB,CAAC;AAShD;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,MAAM,KAAG,aAAa,CAAC,GAAG,CAS9D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,GACxB,MAAM,aAAa,CAAC,MAAM,GAAG,MAAM,CAAC,KACnC,MAKO,CAAC"}
1
+ {"version":3,"file":"bits.d.ts","sourceRoot":"","sources":["../src/bits.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAEtC,yEAAyE;AACzE,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC,qDAAqD;AACrD,eAAO,MAAM,gBAAgB,QAAwB,CAAC;AAStD;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,MAAM,KAAG,aAAa,CAAC,GAAG,CAS9D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,GACxB,MAAM,aAAa,CAAC,MAAM,GAAG,MAAM,CAAC,KACnC,MAKO,CAAC"}
package/dist/bits.js CHANGED
@@ -1,29 +1,29 @@
1
1
  /**
2
- * Bit-level conversions between API key strings, circuit witness
2
+ * Bit-level conversions between secret strings, circuit witness
3
3
  * signals, and SHA-256 hashes. Pure and dependency-free.
4
4
  */
5
- /** The seal circuit's fixed pre-image length: a 64-byte ASCII API key. */
6
- export const SEAL_KEY_BYTES = 64;
5
+ /** The seal circuit's fixed pre-image length: a 64-byte ASCII secret. */
6
+ export const SEAL_SECRET_BYTES = 64;
7
7
  /** The seal circuit's pre-image bit length (512). */
8
- export const SEAL_KEY_BITS = SEAL_KEY_BYTES * 8;
8
+ export const SEAL_SECRET_BITS = SEAL_SECRET_BYTES * 8;
9
9
  /**
10
10
  * Decompose a byte into 8 bits, most-significant-bit first — the bit
11
11
  * order circomlib's SHA-256 gadget expects.
12
12
  */
13
13
  const byteToBits = (byte) => Array.from({ length: 8 }, (_, i) => ((byte >> (7 - i)) & 1));
14
14
  /**
15
- * Convert a raw API key string into the `keyBits` witness signal — the
16
- * UTF-8 bytes of the key, MSB-first per byte.
15
+ * Convert a raw secret string into the `keyBits` witness signal — the
16
+ * UTF-8 bytes of the secret, MSB-first per byte.
17
17
  *
18
- * Throws if the key is not exactly {@link SEAL_KEY_BYTES} ASCII bytes,
18
+ * Throws if the secret is not exactly {@link SEAL_SECRET_BYTES} ASCII bytes,
19
19
  * since the circuit's pre-image length is fixed.
20
20
  */
21
- export const apiKeyToBits = (apiKey) => {
22
- const bytes = new TextEncoder().encode(apiKey);
23
- return bytes.length === SEAL_KEY_BYTES
21
+ export const secretToBits = (secret) => {
22
+ const bytes = new TextEncoder().encode(secret);
23
+ return bytes.length === SEAL_SECRET_BYTES
24
24
  ? [...bytes].flatMap(byteToBits)
25
25
  : (() => {
26
- throw new Error(`seal: API key must be ${SEAL_KEY_BYTES} bytes, got ${bytes.length}`);
26
+ throw new Error(`seal: secret must be ${SEAL_SECRET_BYTES} bytes, got ${bytes.length}`);
27
27
  })();
28
28
  };
29
29
  /**
package/dist/bits.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"bits.js","sourceRoot":"","sources":["../src/bits.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,0EAA0E;AAC1E,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC;AAEjC,qDAAqD;AACrD,MAAM,CAAC,MAAM,aAAa,GAAG,cAAc,GAAG,CAAC,CAAC;AAEhD;;;GAGG;AACH,MAAM,UAAU,GAAG,CAAC,IAAY,EAAsB,EAAE,CACtD,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAO,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAQ,CAAC,CAAC;AAE3E;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAAc,EAAsB,EAAE;IACjE,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC/C,OAAO,KAAK,CAAC,MAAM,KAAK,cAAc;QACpC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC;QAChC,CAAC,CAAC,CAAC,GAAG,EAAE;YACJ,MAAM,IAAI,KAAK,CACb,yBAAyB,cAAc,eAAe,KAAK,CAAC,MAAM,EAAE,CACrE,CAAC;QACJ,CAAC,CAAC,EAAE,CAAC;AACX,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,IAAoC,EAC5B,EAAE,CACV,IAAI,CAAC,MAAM,KAAK,GAAG;IACjB,CAAC,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAC;IACzE,CAAC,CAAC,CAAC,GAAG,EAAE;QACJ,MAAM,IAAI,KAAK,CAAC,uCAAuC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IACxE,CAAC,CAAC,EAAE,CAAC"}
1
+ {"version":3,"file":"bits.js","sourceRoot":"","sources":["../src/bits.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,yEAAyE;AACzE,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAEpC,qDAAqD;AACrD,MAAM,CAAC,MAAM,gBAAgB,GAAG,iBAAiB,GAAG,CAAC,CAAC;AAEtD;;;GAGG;AACH,MAAM,UAAU,GAAG,CAAC,IAAY,EAAsB,EAAE,CACtD,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAO,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAQ,CAAC,CAAC;AAE3E;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAAc,EAAsB,EAAE;IACjE,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC/C,OAAO,KAAK,CAAC,MAAM,KAAK,iBAAiB;QACvC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC;QAChC,CAAC,CAAC,CAAC,GAAG,EAAE;YACJ,MAAM,IAAI,KAAK,CACb,wBAAwB,iBAAiB,eAAe,KAAK,CAAC,MAAM,EAAE,CACvE,CAAC;QACJ,CAAC,CAAC,EAAE,CAAC;AACX,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,IAAoC,EAC5B,EAAE,CACV,IAAI,CAAC,MAAM,KAAK,GAAG;IACjB,CAAC,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAC;IACzE,CAAC,CAAC,CAAC,GAAG,EAAE;QACJ,MAAM,IAAI,KAAK,CAAC,uCAAuC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IACxE,CAAC,CAAC,EAAE,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,21 +1,26 @@
1
1
  /**
2
2
  * @lemmaoracle/seal — ZK auth circuit for Lemma dashboard sign-in.
3
3
  *
4
- * `seal` lets a developer prove knowledge of their Lemma API key
5
- * without revealing it. The dashboard BFF issues a challenge nonce, the
6
- * developer generates a proof, and the BFF resolves the attested
7
- * SHA-256 `key_hash` to a `scope_id`.
4
+ * `seal` lets a developer prove they hold a valid Lemma API key
5
+ * without revealing it — or even *which* key. The dashboard BFF issues
6
+ * a challenge nonce; the developer generates a proof whose public
7
+ * output is a per-session Poseidon nullifier. The nullifier is unique
8
+ * to the (secret, nonce) pair but reveals nothing about the secret or
9
+ * its SHA-256 hash, so proofs cannot be correlated across sessions.
8
10
  *
9
- * The circuit (`circuits/src/seal-identity.circom`) is registered with
10
- * the Lemma workers API as a normal circuit resource. This package is a
11
- * reference definition: it is published for developers to generate
12
- * proofs and is not imported by workers or the dashboard at runtime.
11
+ * The dashboard resolves identity by iterating registered `key_hash`
12
+ * values and computing the expected nullifier until a match is found.
13
+ *
14
+ * The circuit (`circuits/src/seal-identity.circom`, v2) is registered
15
+ * with the Lemma workers API as a normal circuit resource. This
16
+ * package is a reference definition: it is published for developers
17
+ * to generate proofs and is not imported by workers or the dashboard
18
+ * at runtime.
13
19
  */
14
- export type { Bit, SealArtifacts, SealProof, SealProofInput, } from "./types.js";
15
- export { SEAL_KEY_BYTES, SEAL_KEY_BITS, apiKeyToBits, hashBitsToHex, } from "./bits.js";
16
- export { generateSealProof, verifySealProof } from "./proof.js";
17
- /** The circuit id under which seal is registered with the Lemma API. */
18
- export declare const SEAL_CIRCUIT_ID = "seal-identity-v1";
20
+ export type { Bit, SealProof, SealProofInput } from "./types.js";
21
+ export { SEAL_SECRET_BYTES, SEAL_SECRET_BITS, secretToBits, hashBitsToHex, } from "./bits.js";
22
+ export { prove, verify, SEAL_CIRCUIT_ID } from "./proof.js";
23
+ export { sealVkey } from "./vkey.js";
19
24
  /** The schema the seal circuit is registered against. */
20
25
  export declare const SEAL_SCHEMA = "passthrough-v1";
21
26
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,YAAY,EACV,GAAG,EACH,aAAa,EACb,SAAS,EACT,cAAc,GACf,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,cAAc,EACd,aAAa,EACb,YAAY,EACZ,aAAa,GACd,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEhE,wEAAwE;AACxE,eAAO,MAAM,eAAe,qBAAqB,CAAC;AAElD,yDAAyD;AACzD,eAAO,MAAM,WAAW,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,YAAY,EAAE,GAAG,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjE,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,EACZ,aAAa,GACd,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE5D,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC,yDAAyD;AACzD,eAAO,MAAM,WAAW,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -1,20 +1,25 @@
1
1
  /**
2
2
  * @lemmaoracle/seal — ZK auth circuit for Lemma dashboard sign-in.
3
3
  *
4
- * `seal` lets a developer prove knowledge of their Lemma API key
5
- * without revealing it. The dashboard BFF issues a challenge nonce, the
6
- * developer generates a proof, and the BFF resolves the attested
7
- * SHA-256 `key_hash` to a `scope_id`.
4
+ * `seal` lets a developer prove they hold a valid Lemma API key
5
+ * without revealing it — or even *which* key. The dashboard BFF issues
6
+ * a challenge nonce; the developer generates a proof whose public
7
+ * output is a per-session Poseidon nullifier. The nullifier is unique
8
+ * to the (secret, nonce) pair but reveals nothing about the secret or
9
+ * its SHA-256 hash, so proofs cannot be correlated across sessions.
8
10
  *
9
- * The circuit (`circuits/src/seal-identity.circom`) is registered with
10
- * the Lemma workers API as a normal circuit resource. This package is a
11
- * reference definition: it is published for developers to generate
12
- * proofs and is not imported by workers or the dashboard at runtime.
11
+ * The dashboard resolves identity by iterating registered `key_hash`
12
+ * values and computing the expected nullifier until a match is found.
13
+ *
14
+ * The circuit (`circuits/src/seal-identity.circom`, v2) is registered
15
+ * with the Lemma workers API as a normal circuit resource. This
16
+ * package is a reference definition: it is published for developers
17
+ * to generate proofs and is not imported by workers or the dashboard
18
+ * at runtime.
13
19
  */
14
- export { SEAL_KEY_BYTES, SEAL_KEY_BITS, apiKeyToBits, hashBitsToHex, } from "./bits.js";
15
- export { generateSealProof, verifySealProof } from "./proof.js";
16
- /** The circuit id under which seal is registered with the Lemma API. */
17
- export const SEAL_CIRCUIT_ID = "seal-identity-v1";
20
+ export { SEAL_SECRET_BYTES, SEAL_SECRET_BITS, secretToBits, hashBitsToHex, } from "./bits.js";
21
+ export { prove, verify, SEAL_CIRCUIT_ID } from "./proof.js";
22
+ export { sealVkey } from "./vkey.js";
18
23
  /** The schema the seal circuit is registered against. */
19
24
  export const SEAL_SCHEMA = "passthrough-v1";
20
25
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,OAAO,EACL,cAAc,EACd,aAAa,EACb,YAAY,EACZ,aAAa,GACd,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEhE,wEAAwE;AACxE,MAAM,CAAC,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAElD,yDAAyD;AACzD,MAAM,CAAC,MAAM,WAAW,GAAG,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,EACZ,aAAa,GACd,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE5D,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC,yDAAyD;AACzD,MAAM,CAAC,MAAM,WAAW,GAAG,gBAAgB,CAAC"}
package/dist/proof.d.ts CHANGED
@@ -1,30 +1,43 @@
1
1
  /**
2
- * Seal proof generation and verification.
2
+ * Seal proof generation and verification (v2: nullifier-based).
3
3
  *
4
4
  * Dashboard sign-in flow:
5
5
  * 1. The BFF issues a challenge `nonce`.
6
- * 2. The developer runs {@link generateSealProof} with their API key.
7
- * 3. The BFF runs {@link verifySealProof} and reads the attested
8
- * `keyHash` to resolve the caller's scope.
6
+ * 2. The developer runs {@link prove} with their secret.
7
+ * 3. The BFF verifies the proof, reads the `nullifier`, and resolves
8
+ * the caller's scope by matching it against registered key hashes.
9
9
  *
10
- * snarkjs is imported lazily so that consumers who only need the pure
11
- * bit helpers ({@link apiKeyToBits} etc.) do not pay for it.
10
+ * Both prove and verify delegate to `@lemmaoracle/sdk`, which handles
11
+ * circuit artifact resolution and snarkjs orchestration. The vkey for
12
+ * verification is resolved internally from the bundled JSON.
12
13
  */
13
- import type { SealArtifacts, SealProof, SealProofInput } from "./types.js";
14
+ import type { SealProof, SealProofInput } from "./types.js";
15
+ /** The circuit id under which seal v2 is registered with the Lemma API. */
16
+ export declare const SEAL_CIRCUIT_ID = "seal-identity-v1";
17
+ type VerifyResult = Readonly<{
18
+ nullifier: string;
19
+ nonce: string;
20
+ }>;
14
21
  /**
15
- * Generate a groth16 proof that the caller knows the API key whose
16
- * SHA-256 hash is `keyHash`, bound to the given challenge `nonce`.
22
+ * Generate a groth16 proof that the caller holds the secret behind a
23
+ * registered `key_hash`, bound to the given challenge `nonce`.
24
+ *
25
+ * The returned `nullifier` is a per-session Poseidon fingerprint that
26
+ * reveals neither the secret nor its SHA-256 hash. The server resolves
27
+ * identity by iterating registered key hashes and computing the
28
+ * expected nullifier until a match is found.
17
29
  *
18
- * Requires the compiled circuit artifacts build them with
19
- * `npm run build` in `packages/seal/circuits` (see the package README).
30
+ * Circuit artifacts (wasm, zkey) are resolved automatically via the
31
+ * Lemma SDK from `circuitId: "seal-identity-v1"`. No local artifact
32
+ * paths are required.
20
33
  */
21
- export declare const generateSealProof: (input: SealProofInput, artifacts: SealArtifacts) => Promise<SealProof>;
34
+ export declare const prove: (input: SealProofInput) => Promise<SealProof>;
22
35
  /**
23
- * Verify a seal proof against the circuit verification key. Resolves to
24
- * the attested `keyHash` and `nonce` on success, or `null` if invalid.
36
+ * Verify a seal proof against the bundled circuit verification key.
37
+ *
38
+ * Resolves to the attested `nullifier` and `nonce` on success, or
39
+ * `null` if invalid. Delegates to `@lemmaoracle/sdk` verifier.
25
40
  */
26
- export declare const verifySealProof: (proof: SealProof, verificationKey: Readonly<Record<string, unknown>>) => Promise<Readonly<{
27
- keyHash: string;
28
- nonce: string;
29
- }> | null>;
41
+ export declare const verify: (proof: SealProof) => Promise<VerifyResult | null>;
42
+ export {};
30
43
  //# sourceMappingURL=proof.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"proof.d.ts","sourceRoot":"","sources":["../src/proof.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAK3E;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAC5B,OAAO,cAAc,EACrB,WAAW,aAAa,KACvB,OAAO,CAAC,SAAS,CAgBnB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAO,SAAS,EAChB,iBAAiB,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,KACjD,OAAO,CAAC,QAAQ,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,CAa7D,CAAC"}
1
+ {"version":3,"file":"proof.d.ts","sourceRoot":"","sources":["../src/proof.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5D,2EAA2E;AAC3E,eAAO,MAAM,eAAe,qBAAqB,CAAC;AAElD,KAAK,YAAY,GAAG,QAAQ,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEnE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,KAAK,GAAU,OAAO,cAAc,KAAG,OAAO,CAAC,SAAS,CAepE,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,MAAM,GACjB,OAAO,SAAS,KACf,OAAO,CAAC,YAAY,GAAG,IAAI,CAe7B,CAAC"}
package/dist/proof.js CHANGED
@@ -1,49 +1,69 @@
1
1
  /**
2
- * Seal proof generation and verification.
2
+ * Seal proof generation and verification (v2: nullifier-based).
3
3
  *
4
4
  * Dashboard sign-in flow:
5
5
  * 1. The BFF issues a challenge `nonce`.
6
- * 2. The developer runs {@link generateSealProof} with their API key.
7
- * 3. The BFF runs {@link verifySealProof} and reads the attested
8
- * `keyHash` to resolve the caller's scope.
6
+ * 2. The developer runs {@link prove} with their secret.
7
+ * 3. The BFF verifies the proof, reads the `nullifier`, and resolves
8
+ * the caller's scope by matching it against registered key hashes.
9
9
  *
10
- * snarkjs is imported lazily so that consumers who only need the pure
11
- * bit helpers ({@link apiKeyToBits} etc.) do not pay for it.
10
+ * Both prove and verify delegate to `@lemmaoracle/sdk`, which handles
11
+ * circuit artifact resolution and snarkjs orchestration. The vkey for
12
+ * verification is resolved internally from the bundled JSON.
12
13
  */
13
- import { apiKeyToBits, hashBitsToHex } from "./bits.js";
14
- /** Number of `keyHash` bits at the head of the public signal vector. */
15
- const KEY_HASH_BITS = 256;
14
+ import { create, prover, verifier } from "@lemmaoracle/sdk";
15
+ import { secretToBits } from "./bits.js";
16
+ import vkey from "./vkey.js";
17
+ /** The circuit id under which seal v2 is registered with the Lemma API. */
18
+ export const SEAL_CIRCUIT_ID = "seal-identity-v1";
16
19
  /**
17
- * Generate a groth16 proof that the caller knows the API key whose
18
- * SHA-256 hash is `keyHash`, bound to the given challenge `nonce`.
20
+ * Generate a groth16 proof that the caller holds the secret behind a
21
+ * registered `key_hash`, bound to the given challenge `nonce`.
19
22
  *
20
- * Requires the compiled circuit artifacts build them with
21
- * `npm run build` in `packages/seal/circuits` (see the package README).
23
+ * The returned `nullifier` is a per-session Poseidon fingerprint that
24
+ * reveals neither the secret nor its SHA-256 hash. The server resolves
25
+ * identity by iterating registered key hashes and computing the
26
+ * expected nullifier until a match is found.
27
+ *
28
+ * Circuit artifacts (wasm, zkey) are resolved automatically via the
29
+ * Lemma SDK from `circuitId: "seal-identity-v1"`. No local artifact
30
+ * paths are required.
22
31
  */
23
- export const generateSealProof = async (input, artifacts) => {
24
- const { groth16 } = await import("snarkjs");
32
+ export const prove = async (input) => {
33
+ const client = create({});
25
34
  const witness = {
26
- keyBits: apiKeyToBits(input.apiKey).map(Number),
35
+ keyBits: secretToBits(input.secret).map(Number),
27
36
  nonce: input.nonce,
28
37
  };
29
- const { proof, publicSignals } = await groth16.fullProve(witness, artifacts.wasm, artifacts.zkey);
38
+ const { proof, inputs } = await prover.prove(client, {
39
+ circuitId: SEAL_CIRCUIT_ID,
40
+ witness,
41
+ });
30
42
  return {
31
- proof,
32
- publicSignals,
33
- keyHash: hashBitsToHex(publicSignals.slice(0, KEY_HASH_BITS)),
43
+ proof: JSON.parse(atob(proof)),
44
+ publicSignals: inputs,
45
+ nullifier: inputs[0] ?? "",
34
46
  };
35
47
  };
36
48
  /**
37
- * Verify a seal proof against the circuit verification key. Resolves to
38
- * the attested `keyHash` and `nonce` on success, or `null` if invalid.
49
+ * Verify a seal proof against the bundled circuit verification key.
50
+ *
51
+ * Resolves to the attested `nullifier` and `nonce` on success, or
52
+ * `null` if invalid. Delegates to `@lemmaoracle/sdk` verifier.
39
53
  */
40
- export const verifySealProof = async (proof, verificationKey) => {
41
- const { groth16 } = await import("snarkjs");
42
- const ok = await groth16.verify(verificationKey, [...proof.publicSignals], proof.proof);
54
+ export const verify = async (proof) => {
55
+ const { ok } = await verifier.verify({
56
+ alg: "groth16-bn254-snarkjs",
57
+ inputs: {
58
+ vkey,
59
+ proof: proof.proof,
60
+ publicSignals: proof.publicSignals,
61
+ },
62
+ });
43
63
  return ok
44
64
  ? {
45
- keyHash: hashBitsToHex(proof.publicSignals.slice(0, KEY_HASH_BITS)),
46
- nonce: proof.publicSignals[KEY_HASH_BITS] ?? "",
65
+ nullifier: proof.publicSignals[0] ?? "",
66
+ nonce: proof.publicSignals[1] ?? "",
47
67
  }
48
68
  : null;
49
69
  };