@orbinum/sdk 0.25.0 → 1.0.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 +355 -23
- package/dist/adapters/indexeddb/index.d.mts +152 -0
- package/dist/adapters/indexeddb/index.d.ts +152 -0
- package/dist/adapters/indexeddb/index.js +381 -0
- package/dist/adapters/indexeddb/index.mjs +326 -0
- package/dist/chunk-JMYU5QAK.mjs +40 -0
- package/dist/chunk-Y6LNYJAJ.mjs +1242 -0
- package/dist/index-JYVjYJtf.d.mts +353 -0
- package/dist/index-JYVjYJtf.d.ts +353 -0
- package/dist/index.d.mts +4745 -2971
- package/dist/index.d.ts +4745 -2971
- package/dist/index.js +6319 -3431
- package/dist/index.mjs +4884 -3259
- package/dist/secretStore-CF6Nse__.d.mts +292 -0
- package/dist/secretStore-CF6Nse__.d.ts +292 -0
- package/dist/wallet/worker/index.d.mts +1 -0
- package/dist/wallet/worker/index.d.ts +1 -0
- package/dist/wallet/worker/index.js +859 -0
- package/dist/wallet/worker/index.mjs +28 -0
- package/package.json +24 -7
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape a note takes at rest.
|
|
3
|
+
*
|
|
4
|
+
* Any backend — IndexedDB, SQLite, remote — produces and consumes exactly this,
|
|
5
|
+
* so `encryptNote` / `decryptNoteRecord` work against all of them unmodified.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* One encrypted note as stored.
|
|
9
|
+
*
|
|
10
|
+
* Identifiers are BLINDED: stored as HMAC tags under the vault blind key, never
|
|
11
|
+
* the raw on-chain hex. A storage dump therefore reveals no commitment,
|
|
12
|
+
* nullifier or asset that could be linked to chain activity, while equality
|
|
13
|
+
* lookups still work by comparing tags.
|
|
14
|
+
*
|
|
15
|
+
* `spent` / `spentAt` stay plaintext deliberately — they are local flags with
|
|
16
|
+
* no on-chain linkage, and keeping them readable lets a backend filter spent
|
|
17
|
+
* notes without unlocking the vault.
|
|
18
|
+
*/
|
|
19
|
+
interface EncryptedNoteRecord {
|
|
20
|
+
/** Primary key — blinded commitment tag: HMAC(blindKey, commitmentHex). */
|
|
21
|
+
commitmentTag: string;
|
|
22
|
+
/** AES-GCM IV for this record — base64 */
|
|
23
|
+
iv: string;
|
|
24
|
+
/** AES-GCM ciphertext of the full ZkNote JSON — base64 */
|
|
25
|
+
ciphertext: string;
|
|
26
|
+
/** Blinded nullifier tag: HMAC(blindKey, nullifierHex). Quick spent-check. */
|
|
27
|
+
nullifierTag: string;
|
|
28
|
+
/** Blinded asset tag: HMAC(blindKey, assetId). Filter by asset without unlock. */
|
|
29
|
+
assetTag: string;
|
|
30
|
+
/** Whether the note has already been spent/nullified on-chain. */
|
|
31
|
+
spent?: boolean;
|
|
32
|
+
/** When the app marked the note as spent locally, if known. */
|
|
33
|
+
spentAt?: number | null;
|
|
34
|
+
updatedAt: number;
|
|
35
|
+
}
|
|
36
|
+
/** Partial update applied to a note's spent status without re-encrypting the full payload. */
|
|
37
|
+
interface NoteStatusUpdate {
|
|
38
|
+
spent?: boolean;
|
|
39
|
+
spentAt?: number | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The persistence contract a wallet's vault runs on.
|
|
44
|
+
*
|
|
45
|
+
* Split by responsibility rather than exposed as one interface: a consumer that
|
|
46
|
+
* only decrypts notes never has to supply a nullifier cache, and an
|
|
47
|
+
* implementation can grow one concern at a time. `VaultStorage` composes the
|
|
48
|
+
* three for the common case.
|
|
49
|
+
*
|
|
50
|
+
* Every method is async because the reference backend is IndexedDB. Records
|
|
51
|
+
* cross this boundary already encrypted and with identifiers blinded (see
|
|
52
|
+
* `EncryptedNoteRecord`), so a backend never sees a commitment, a nullifier, or
|
|
53
|
+
* an amount — which is what makes an untrusted or remote backend viable.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/** Wallet-level state that is not a note: cursors and ephemeral-key counters. */
|
|
57
|
+
interface VaultConfigRecord {
|
|
58
|
+
id: 'main';
|
|
59
|
+
/** Schema version of this record's shape. */
|
|
60
|
+
v: number;
|
|
61
|
+
/** Genesis hash the vault was last reconciled against. */
|
|
62
|
+
chainFingerprint?: string;
|
|
63
|
+
/** Highest leafIndex a completed scan processed; the next one resumes after it. */
|
|
64
|
+
lastScannedLeafIndex?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Next self-note ephemeral index.
|
|
67
|
+
*
|
|
68
|
+
* Monotonic, and that is a protocol requirement rather than bookkeeping:
|
|
69
|
+
* reusing an index republishes the same ephPk and publicly links the two
|
|
70
|
+
* notes as sharing a creator.
|
|
71
|
+
*/
|
|
72
|
+
selfEphCounter?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Counterparties whose payments can be recognised without a trial ECDH,
|
|
75
|
+
* keyed by their packed viewing public key (lowercase hex). Each carries its
|
|
76
|
+
* own counter, monotonic for the same reason as `selfEphCounter`.
|
|
77
|
+
*/
|
|
78
|
+
pairwiseCounterparties?: Record<string, {
|
|
79
|
+
nextIndex: number;
|
|
80
|
+
addedAt: number;
|
|
81
|
+
}>;
|
|
82
|
+
createdAt: number;
|
|
83
|
+
updatedAt: number;
|
|
84
|
+
}
|
|
85
|
+
/** An encrypted entry in the local transaction history. */
|
|
86
|
+
interface EncryptedTxRecord {
|
|
87
|
+
/** Primary key — the transaction hash, the one field left in plaintext. */
|
|
88
|
+
id: string;
|
|
89
|
+
/** AES-GCM IV, base64. */
|
|
90
|
+
iv: string;
|
|
91
|
+
/** AES-GCM ciphertext of the record body, base64. */
|
|
92
|
+
ciphertext: string;
|
|
93
|
+
updatedAt: number;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* What is known locally about a spend, once a nullifier turns up in the set.
|
|
97
|
+
*
|
|
98
|
+
* Both fields are public chain data recovered from a set that always transfers
|
|
99
|
+
* whole — neither implies a per-nullifier query.
|
|
100
|
+
*/
|
|
101
|
+
interface SpendDetails {
|
|
102
|
+
/** Spend block timestamp (ms); null when the source carried none. */
|
|
103
|
+
spentAt: number | null;
|
|
104
|
+
/** Hash of the spending transaction; null when the extrinsic was unresolvable. */
|
|
105
|
+
txHash: string | null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* One entry of the spent-nullifier set, stored as it arrives.
|
|
109
|
+
*
|
|
110
|
+
* Field names are terse because there is one record per spent note on the
|
|
111
|
+
* chain: at millions of notes the key strings are a material share of the
|
|
112
|
+
* cache's size on disk.
|
|
113
|
+
*/
|
|
114
|
+
interface CachedNullifier {
|
|
115
|
+
/** The nullifier hex, as the source publishes it. Primary key. */
|
|
116
|
+
h: string;
|
|
117
|
+
/** Spend block timestamp (ms); null when the source carried none. */
|
|
118
|
+
ts: number | null;
|
|
119
|
+
/** Hash of the spending transaction; null when unresolvable. */
|
|
120
|
+
tx?: string | null;
|
|
121
|
+
}
|
|
122
|
+
/** Progress of the nullifier-set download, so a sync resumes instead of restarting. */
|
|
123
|
+
interface NullifierSyncMeta {
|
|
124
|
+
id: 'main';
|
|
125
|
+
/** Sealed chunks already stored. */
|
|
126
|
+
chunksDone: number;
|
|
127
|
+
/** Generation of the set the stored chunks belong to; a change invalidates them. */
|
|
128
|
+
generation: string;
|
|
129
|
+
totalStored: number;
|
|
130
|
+
updatedAt: number;
|
|
131
|
+
}
|
|
132
|
+
/** Config and note persistence — the minimum a wallet needs to hold funds. */
|
|
133
|
+
interface NoteStorage {
|
|
134
|
+
getConfig(): Promise<VaultConfigRecord | null>;
|
|
135
|
+
putConfig(config: VaultConfigRecord): Promise<void>;
|
|
136
|
+
/**
|
|
137
|
+
* Applies `mutate` to the stored config as ONE atomic read-modify-write.
|
|
138
|
+
*
|
|
139
|
+
* Part of the contract, not sugar over get + put. Two concurrent callers
|
|
140
|
+
* doing that separately both read the same `selfEphCounter`, so both derive
|
|
141
|
+
* the same ephemeral index and publish the same ephPk — a privacy leak, not
|
|
142
|
+
* a lost update. A backend that cannot make this atomic cannot host a vault.
|
|
143
|
+
*
|
|
144
|
+
* Resolves to null when no config exists; `mutate` is not called.
|
|
145
|
+
*/
|
|
146
|
+
updateConfig(mutate: (config: VaultConfigRecord) => VaultConfigRecord): Promise<VaultConfigRecord | null>;
|
|
147
|
+
getAllNoteRecords(): Promise<EncryptedNoteRecord[]>;
|
|
148
|
+
putNote(record: EncryptedNoteRecord): Promise<void>;
|
|
149
|
+
putNotes(records: EncryptedNoteRecord[]): Promise<void>;
|
|
150
|
+
deleteNote(commitmentTag: string): Promise<void>;
|
|
151
|
+
deleteNotes(commitmentTags: string[]): Promise<void>;
|
|
152
|
+
clearNotes(): Promise<void>;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Local mirror of the spent-nullifier set.
|
|
156
|
+
*
|
|
157
|
+
* The whole set is downloaded and intersected locally, deliberately: asking a
|
|
158
|
+
* server whether one specific nullifier is spent would tell it which notes the
|
|
159
|
+
* wallet holds. That is why this is a cache to fill, not a lookup to call.
|
|
160
|
+
*/
|
|
161
|
+
interface NullifierCache {
|
|
162
|
+
/**
|
|
163
|
+
* Stores one sealed chunk together with the sync progress it produced.
|
|
164
|
+
*
|
|
165
|
+
* Both must land in the same transaction: progress ahead of the data would
|
|
166
|
+
* make the next sync resume past chunks the cache never stored, leaving
|
|
167
|
+
* spent notes looking unspent.
|
|
168
|
+
*/
|
|
169
|
+
putNullifierChunk(entries: CachedNullifier[], meta: NullifierSyncMeta): Promise<void>;
|
|
170
|
+
getNullifierSyncMeta(): Promise<NullifierSyncMeta | null>;
|
|
171
|
+
/**
|
|
172
|
+
* Which of `hexes` the cache holds. Absent keys are unspent as far as it
|
|
173
|
+
* knows.
|
|
174
|
+
*
|
|
175
|
+
* Matching is EXACT: a backend must not lowercase, trim or otherwise repair
|
|
176
|
+
* a key. Normalising happens once at ingestion, where the feed's rows are
|
|
177
|
+
* lowercased on the way into `putNullifierChunk` — so everything stored is
|
|
178
|
+
* already in one form, and a backend that "helpfully" normalised again
|
|
179
|
+
* would only hide a caller passing the wrong case.
|
|
180
|
+
*
|
|
181
|
+
* Both sides must agree, and the cost of disagreeing is not a missing row:
|
|
182
|
+
* a lookup that misses reports a SPENT note as unspent, the wallet offers
|
|
183
|
+
* it, and the spend dies on a duplicate nullifier.
|
|
184
|
+
*/
|
|
185
|
+
getSpentNullifiers(hexes: string[]): Promise<Map<string, SpendDetails>>;
|
|
186
|
+
countNullifiers(): Promise<number>;
|
|
187
|
+
clearNullifierCache(): Promise<void>;
|
|
188
|
+
}
|
|
189
|
+
/** Encrypted local transaction history — outgoing sends the chain cannot reveal. */
|
|
190
|
+
interface TxHistoryStore {
|
|
191
|
+
addTxRecord(record: EncryptedTxRecord): Promise<void>;
|
|
192
|
+
getAllTxRecords(): Promise<EncryptedTxRecord[]>;
|
|
193
|
+
}
|
|
194
|
+
/** Everything a full wallet needs from persistence. */
|
|
195
|
+
interface VaultStorage extends NoteStorage, NullifierCache, TxHistoryStore {
|
|
196
|
+
/** Whether a vault already exists for this identity. */
|
|
197
|
+
hasVault(): Promise<boolean>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The key that protects secrets at rest on one device.
|
|
202
|
+
*
|
|
203
|
+
* A single AES-GCM-256 key, generated once and kept for the life of the install.
|
|
204
|
+
* Where the platform allows it the key must be NON-EXTRACTABLE — created with
|
|
205
|
+
* `extractable: false` and persisted as a key handle rather than as bytes — so
|
|
206
|
+
* a storage dump, disk image or synced profile yields nothing usable.
|
|
207
|
+
*
|
|
208
|
+
* ## Why not derive it from the wallet signature
|
|
209
|
+
*
|
|
210
|
+
* Circular. The thing being protected is the cached identity, and the reason it
|
|
211
|
+
* is cached is to avoid asking for a signature again. A key derived from that
|
|
212
|
+
* signature would require the signature to read the cache that exists to avoid
|
|
213
|
+
* the signature.
|
|
214
|
+
*
|
|
215
|
+
* ## Platform notes
|
|
216
|
+
*
|
|
217
|
+
* Browsers and extensions: `@orbinum/sdk/storage/indexeddb` exports
|
|
218
|
+
* `getOrCreateIndexedDbDeviceKey`, which stores a non-extractable `CryptoKey`
|
|
219
|
+
* via structured clone — the key material never becomes visible to JavaScript.
|
|
220
|
+
*
|
|
221
|
+
* React Native and Node: no non-extractable handle survives a restart, so store
|
|
222
|
+
* raw key bytes in the platform's secure enclave (Keychain, Keystore) and import
|
|
223
|
+
* them with `importDeviceKey`. The isolation then comes from the enclave rather
|
|
224
|
+
* than from the key being unexportable.
|
|
225
|
+
*/
|
|
226
|
+
/**
|
|
227
|
+
* A fresh AES-GCM-256 device key.
|
|
228
|
+
*
|
|
229
|
+
* `extractable` defaults to false and should stay that way. Passing true is for
|
|
230
|
+
* the platforms described above — the ones that cannot persist a key handle and
|
|
231
|
+
* must export raw bytes into a secure enclave. Anywhere a handle survives a
|
|
232
|
+
* restart, an extractable key is strictly worse: the material becomes readable
|
|
233
|
+
* by any code in the context.
|
|
234
|
+
*/
|
|
235
|
+
declare function generateDeviceKey(extractable?: boolean): Promise<CryptoKey>;
|
|
236
|
+
/**
|
|
237
|
+
* Imports raw device-key bytes, for platforms that persist bytes in a secure
|
|
238
|
+
* enclave rather than a key handle. The imported key is non-extractable, so the
|
|
239
|
+
* bytes cannot be read back out through WebCrypto.
|
|
240
|
+
*/
|
|
241
|
+
declare function importDeviceKey(raw: Uint8Array): Promise<CryptoKey>;
|
|
242
|
+
/**
|
|
243
|
+
* Where a device key is persisted between launches.
|
|
244
|
+
*
|
|
245
|
+
* `load` returning null means "no key yet", not "failed" — the provider takes
|
|
246
|
+
* that as permission to generate one. An adapter that cannot reach its backend
|
|
247
|
+
* should throw instead, or the first transient failure silently mints a second
|
|
248
|
+
* key and orphans every secret encrypted under the first.
|
|
249
|
+
*/
|
|
250
|
+
interface DeviceKeyStore {
|
|
251
|
+
load(): Promise<CryptoKey | null>;
|
|
252
|
+
save(key: CryptoKey): Promise<void>;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* The device key, generating and persisting it on first use.
|
|
256
|
+
*
|
|
257
|
+
* Cached in memory per process: generating a second key would silently orphan
|
|
258
|
+
* every secret encrypted under the first.
|
|
259
|
+
*/
|
|
260
|
+
declare function createDeviceKeyProvider(store: DeviceKeyStore): () => Promise<CryptoKey>;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Where a host keeps small secrets between sessions.
|
|
264
|
+
*
|
|
265
|
+
* Three methods, because that is all the identity cache needs and every platform
|
|
266
|
+
* has them: `localStorage` in a page, `chrome.storage.local` in an extension,
|
|
267
|
+
* Keychain or SharedPreferences on mobile, a file on a server. Keeping the
|
|
268
|
+
* surface this small is what makes those adapters a few lines each.
|
|
269
|
+
*
|
|
270
|
+
* Values handed to `set` are already ENCRYPTED — see `sessionCache`. A store is
|
|
271
|
+
* not trusted with plaintext secrets, so a backend with weak isolation is still
|
|
272
|
+
* a safe place to put one.
|
|
273
|
+
*/
|
|
274
|
+
interface SecretStore {
|
|
275
|
+
get(key: string): Promise<string | null>;
|
|
276
|
+
set(key: string, value: string): Promise<void>;
|
|
277
|
+
remove(key: string): Promise<void>;
|
|
278
|
+
/**
|
|
279
|
+
* Keys currently held. Needed to clear every network's cache for one account
|
|
280
|
+
* on disconnect, which cannot be done by constructing keys — the caller does
|
|
281
|
+
* not know which chains the user has visited.
|
|
282
|
+
*/
|
|
283
|
+
keys(): Promise<string[]>;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* An in-memory `SecretStore`. Loses everything on restart, which is the whole
|
|
287
|
+
* point of the real ones — useful for tests and for a host that deliberately
|
|
288
|
+
* wants re-signing on every launch.
|
|
289
|
+
*/
|
|
290
|
+
declare function createMemorySecretStore(): SecretStore;
|
|
291
|
+
|
|
292
|
+
export { type CachedNullifier as C, type DeviceKeyStore as D, type EncryptedNoteRecord as E, type NullifierSyncMeta as N, type SpendDetails as S, type TxHistoryStore as T, type VaultStorage as V, type VaultConfigRecord as a, type EncryptedTxRecord as b, type SecretStore as c, type NoteStorage as d, type NoteStatusUpdate as e, type NullifierCache as f, createDeviceKeyProvider as g, createMemorySecretStore as h, generateDeviceKey as i, importDeviceKey as j };
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape a note takes at rest.
|
|
3
|
+
*
|
|
4
|
+
* Any backend — IndexedDB, SQLite, remote — produces and consumes exactly this,
|
|
5
|
+
* so `encryptNote` / `decryptNoteRecord` work against all of them unmodified.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* One encrypted note as stored.
|
|
9
|
+
*
|
|
10
|
+
* Identifiers are BLINDED: stored as HMAC tags under the vault blind key, never
|
|
11
|
+
* the raw on-chain hex. A storage dump therefore reveals no commitment,
|
|
12
|
+
* nullifier or asset that could be linked to chain activity, while equality
|
|
13
|
+
* lookups still work by comparing tags.
|
|
14
|
+
*
|
|
15
|
+
* `spent` / `spentAt` stay plaintext deliberately — they are local flags with
|
|
16
|
+
* no on-chain linkage, and keeping them readable lets a backend filter spent
|
|
17
|
+
* notes without unlocking the vault.
|
|
18
|
+
*/
|
|
19
|
+
interface EncryptedNoteRecord {
|
|
20
|
+
/** Primary key — blinded commitment tag: HMAC(blindKey, commitmentHex). */
|
|
21
|
+
commitmentTag: string;
|
|
22
|
+
/** AES-GCM IV for this record — base64 */
|
|
23
|
+
iv: string;
|
|
24
|
+
/** AES-GCM ciphertext of the full ZkNote JSON — base64 */
|
|
25
|
+
ciphertext: string;
|
|
26
|
+
/** Blinded nullifier tag: HMAC(blindKey, nullifierHex). Quick spent-check. */
|
|
27
|
+
nullifierTag: string;
|
|
28
|
+
/** Blinded asset tag: HMAC(blindKey, assetId). Filter by asset without unlock. */
|
|
29
|
+
assetTag: string;
|
|
30
|
+
/** Whether the note has already been spent/nullified on-chain. */
|
|
31
|
+
spent?: boolean;
|
|
32
|
+
/** When the app marked the note as spent locally, if known. */
|
|
33
|
+
spentAt?: number | null;
|
|
34
|
+
updatedAt: number;
|
|
35
|
+
}
|
|
36
|
+
/** Partial update applied to a note's spent status without re-encrypting the full payload. */
|
|
37
|
+
interface NoteStatusUpdate {
|
|
38
|
+
spent?: boolean;
|
|
39
|
+
spentAt?: number | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The persistence contract a wallet's vault runs on.
|
|
44
|
+
*
|
|
45
|
+
* Split by responsibility rather than exposed as one interface: a consumer that
|
|
46
|
+
* only decrypts notes never has to supply a nullifier cache, and an
|
|
47
|
+
* implementation can grow one concern at a time. `VaultStorage` composes the
|
|
48
|
+
* three for the common case.
|
|
49
|
+
*
|
|
50
|
+
* Every method is async because the reference backend is IndexedDB. Records
|
|
51
|
+
* cross this boundary already encrypted and with identifiers blinded (see
|
|
52
|
+
* `EncryptedNoteRecord`), so a backend never sees a commitment, a nullifier, or
|
|
53
|
+
* an amount — which is what makes an untrusted or remote backend viable.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/** Wallet-level state that is not a note: cursors and ephemeral-key counters. */
|
|
57
|
+
interface VaultConfigRecord {
|
|
58
|
+
id: 'main';
|
|
59
|
+
/** Schema version of this record's shape. */
|
|
60
|
+
v: number;
|
|
61
|
+
/** Genesis hash the vault was last reconciled against. */
|
|
62
|
+
chainFingerprint?: string;
|
|
63
|
+
/** Highest leafIndex a completed scan processed; the next one resumes after it. */
|
|
64
|
+
lastScannedLeafIndex?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Next self-note ephemeral index.
|
|
67
|
+
*
|
|
68
|
+
* Monotonic, and that is a protocol requirement rather than bookkeeping:
|
|
69
|
+
* reusing an index republishes the same ephPk and publicly links the two
|
|
70
|
+
* notes as sharing a creator.
|
|
71
|
+
*/
|
|
72
|
+
selfEphCounter?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Counterparties whose payments can be recognised without a trial ECDH,
|
|
75
|
+
* keyed by their packed viewing public key (lowercase hex). Each carries its
|
|
76
|
+
* own counter, monotonic for the same reason as `selfEphCounter`.
|
|
77
|
+
*/
|
|
78
|
+
pairwiseCounterparties?: Record<string, {
|
|
79
|
+
nextIndex: number;
|
|
80
|
+
addedAt: number;
|
|
81
|
+
}>;
|
|
82
|
+
createdAt: number;
|
|
83
|
+
updatedAt: number;
|
|
84
|
+
}
|
|
85
|
+
/** An encrypted entry in the local transaction history. */
|
|
86
|
+
interface EncryptedTxRecord {
|
|
87
|
+
/** Primary key — the transaction hash, the one field left in plaintext. */
|
|
88
|
+
id: string;
|
|
89
|
+
/** AES-GCM IV, base64. */
|
|
90
|
+
iv: string;
|
|
91
|
+
/** AES-GCM ciphertext of the record body, base64. */
|
|
92
|
+
ciphertext: string;
|
|
93
|
+
updatedAt: number;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* What is known locally about a spend, once a nullifier turns up in the set.
|
|
97
|
+
*
|
|
98
|
+
* Both fields are public chain data recovered from a set that always transfers
|
|
99
|
+
* whole — neither implies a per-nullifier query.
|
|
100
|
+
*/
|
|
101
|
+
interface SpendDetails {
|
|
102
|
+
/** Spend block timestamp (ms); null when the source carried none. */
|
|
103
|
+
spentAt: number | null;
|
|
104
|
+
/** Hash of the spending transaction; null when the extrinsic was unresolvable. */
|
|
105
|
+
txHash: string | null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* One entry of the spent-nullifier set, stored as it arrives.
|
|
109
|
+
*
|
|
110
|
+
* Field names are terse because there is one record per spent note on the
|
|
111
|
+
* chain: at millions of notes the key strings are a material share of the
|
|
112
|
+
* cache's size on disk.
|
|
113
|
+
*/
|
|
114
|
+
interface CachedNullifier {
|
|
115
|
+
/** The nullifier hex, as the source publishes it. Primary key. */
|
|
116
|
+
h: string;
|
|
117
|
+
/** Spend block timestamp (ms); null when the source carried none. */
|
|
118
|
+
ts: number | null;
|
|
119
|
+
/** Hash of the spending transaction; null when unresolvable. */
|
|
120
|
+
tx?: string | null;
|
|
121
|
+
}
|
|
122
|
+
/** Progress of the nullifier-set download, so a sync resumes instead of restarting. */
|
|
123
|
+
interface NullifierSyncMeta {
|
|
124
|
+
id: 'main';
|
|
125
|
+
/** Sealed chunks already stored. */
|
|
126
|
+
chunksDone: number;
|
|
127
|
+
/** Generation of the set the stored chunks belong to; a change invalidates them. */
|
|
128
|
+
generation: string;
|
|
129
|
+
totalStored: number;
|
|
130
|
+
updatedAt: number;
|
|
131
|
+
}
|
|
132
|
+
/** Config and note persistence — the minimum a wallet needs to hold funds. */
|
|
133
|
+
interface NoteStorage {
|
|
134
|
+
getConfig(): Promise<VaultConfigRecord | null>;
|
|
135
|
+
putConfig(config: VaultConfigRecord): Promise<void>;
|
|
136
|
+
/**
|
|
137
|
+
* Applies `mutate` to the stored config as ONE atomic read-modify-write.
|
|
138
|
+
*
|
|
139
|
+
* Part of the contract, not sugar over get + put. Two concurrent callers
|
|
140
|
+
* doing that separately both read the same `selfEphCounter`, so both derive
|
|
141
|
+
* the same ephemeral index and publish the same ephPk — a privacy leak, not
|
|
142
|
+
* a lost update. A backend that cannot make this atomic cannot host a vault.
|
|
143
|
+
*
|
|
144
|
+
* Resolves to null when no config exists; `mutate` is not called.
|
|
145
|
+
*/
|
|
146
|
+
updateConfig(mutate: (config: VaultConfigRecord) => VaultConfigRecord): Promise<VaultConfigRecord | null>;
|
|
147
|
+
getAllNoteRecords(): Promise<EncryptedNoteRecord[]>;
|
|
148
|
+
putNote(record: EncryptedNoteRecord): Promise<void>;
|
|
149
|
+
putNotes(records: EncryptedNoteRecord[]): Promise<void>;
|
|
150
|
+
deleteNote(commitmentTag: string): Promise<void>;
|
|
151
|
+
deleteNotes(commitmentTags: string[]): Promise<void>;
|
|
152
|
+
clearNotes(): Promise<void>;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Local mirror of the spent-nullifier set.
|
|
156
|
+
*
|
|
157
|
+
* The whole set is downloaded and intersected locally, deliberately: asking a
|
|
158
|
+
* server whether one specific nullifier is spent would tell it which notes the
|
|
159
|
+
* wallet holds. That is why this is a cache to fill, not a lookup to call.
|
|
160
|
+
*/
|
|
161
|
+
interface NullifierCache {
|
|
162
|
+
/**
|
|
163
|
+
* Stores one sealed chunk together with the sync progress it produced.
|
|
164
|
+
*
|
|
165
|
+
* Both must land in the same transaction: progress ahead of the data would
|
|
166
|
+
* make the next sync resume past chunks the cache never stored, leaving
|
|
167
|
+
* spent notes looking unspent.
|
|
168
|
+
*/
|
|
169
|
+
putNullifierChunk(entries: CachedNullifier[], meta: NullifierSyncMeta): Promise<void>;
|
|
170
|
+
getNullifierSyncMeta(): Promise<NullifierSyncMeta | null>;
|
|
171
|
+
/**
|
|
172
|
+
* Which of `hexes` the cache holds. Absent keys are unspent as far as it
|
|
173
|
+
* knows.
|
|
174
|
+
*
|
|
175
|
+
* Matching is EXACT: a backend must not lowercase, trim or otherwise repair
|
|
176
|
+
* a key. Normalising happens once at ingestion, where the feed's rows are
|
|
177
|
+
* lowercased on the way into `putNullifierChunk` — so everything stored is
|
|
178
|
+
* already in one form, and a backend that "helpfully" normalised again
|
|
179
|
+
* would only hide a caller passing the wrong case.
|
|
180
|
+
*
|
|
181
|
+
* Both sides must agree, and the cost of disagreeing is not a missing row:
|
|
182
|
+
* a lookup that misses reports a SPENT note as unspent, the wallet offers
|
|
183
|
+
* it, and the spend dies on a duplicate nullifier.
|
|
184
|
+
*/
|
|
185
|
+
getSpentNullifiers(hexes: string[]): Promise<Map<string, SpendDetails>>;
|
|
186
|
+
countNullifiers(): Promise<number>;
|
|
187
|
+
clearNullifierCache(): Promise<void>;
|
|
188
|
+
}
|
|
189
|
+
/** Encrypted local transaction history — outgoing sends the chain cannot reveal. */
|
|
190
|
+
interface TxHistoryStore {
|
|
191
|
+
addTxRecord(record: EncryptedTxRecord): Promise<void>;
|
|
192
|
+
getAllTxRecords(): Promise<EncryptedTxRecord[]>;
|
|
193
|
+
}
|
|
194
|
+
/** Everything a full wallet needs from persistence. */
|
|
195
|
+
interface VaultStorage extends NoteStorage, NullifierCache, TxHistoryStore {
|
|
196
|
+
/** Whether a vault already exists for this identity. */
|
|
197
|
+
hasVault(): Promise<boolean>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The key that protects secrets at rest on one device.
|
|
202
|
+
*
|
|
203
|
+
* A single AES-GCM-256 key, generated once and kept for the life of the install.
|
|
204
|
+
* Where the platform allows it the key must be NON-EXTRACTABLE — created with
|
|
205
|
+
* `extractable: false` and persisted as a key handle rather than as bytes — so
|
|
206
|
+
* a storage dump, disk image or synced profile yields nothing usable.
|
|
207
|
+
*
|
|
208
|
+
* ## Why not derive it from the wallet signature
|
|
209
|
+
*
|
|
210
|
+
* Circular. The thing being protected is the cached identity, and the reason it
|
|
211
|
+
* is cached is to avoid asking for a signature again. A key derived from that
|
|
212
|
+
* signature would require the signature to read the cache that exists to avoid
|
|
213
|
+
* the signature.
|
|
214
|
+
*
|
|
215
|
+
* ## Platform notes
|
|
216
|
+
*
|
|
217
|
+
* Browsers and extensions: `@orbinum/sdk/storage/indexeddb` exports
|
|
218
|
+
* `getOrCreateIndexedDbDeviceKey`, which stores a non-extractable `CryptoKey`
|
|
219
|
+
* via structured clone — the key material never becomes visible to JavaScript.
|
|
220
|
+
*
|
|
221
|
+
* React Native and Node: no non-extractable handle survives a restart, so store
|
|
222
|
+
* raw key bytes in the platform's secure enclave (Keychain, Keystore) and import
|
|
223
|
+
* them with `importDeviceKey`. The isolation then comes from the enclave rather
|
|
224
|
+
* than from the key being unexportable.
|
|
225
|
+
*/
|
|
226
|
+
/**
|
|
227
|
+
* A fresh AES-GCM-256 device key.
|
|
228
|
+
*
|
|
229
|
+
* `extractable` defaults to false and should stay that way. Passing true is for
|
|
230
|
+
* the platforms described above — the ones that cannot persist a key handle and
|
|
231
|
+
* must export raw bytes into a secure enclave. Anywhere a handle survives a
|
|
232
|
+
* restart, an extractable key is strictly worse: the material becomes readable
|
|
233
|
+
* by any code in the context.
|
|
234
|
+
*/
|
|
235
|
+
declare function generateDeviceKey(extractable?: boolean): Promise<CryptoKey>;
|
|
236
|
+
/**
|
|
237
|
+
* Imports raw device-key bytes, for platforms that persist bytes in a secure
|
|
238
|
+
* enclave rather than a key handle. The imported key is non-extractable, so the
|
|
239
|
+
* bytes cannot be read back out through WebCrypto.
|
|
240
|
+
*/
|
|
241
|
+
declare function importDeviceKey(raw: Uint8Array): Promise<CryptoKey>;
|
|
242
|
+
/**
|
|
243
|
+
* Where a device key is persisted between launches.
|
|
244
|
+
*
|
|
245
|
+
* `load` returning null means "no key yet", not "failed" — the provider takes
|
|
246
|
+
* that as permission to generate one. An adapter that cannot reach its backend
|
|
247
|
+
* should throw instead, or the first transient failure silently mints a second
|
|
248
|
+
* key and orphans every secret encrypted under the first.
|
|
249
|
+
*/
|
|
250
|
+
interface DeviceKeyStore {
|
|
251
|
+
load(): Promise<CryptoKey | null>;
|
|
252
|
+
save(key: CryptoKey): Promise<void>;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* The device key, generating and persisting it on first use.
|
|
256
|
+
*
|
|
257
|
+
* Cached in memory per process: generating a second key would silently orphan
|
|
258
|
+
* every secret encrypted under the first.
|
|
259
|
+
*/
|
|
260
|
+
declare function createDeviceKeyProvider(store: DeviceKeyStore): () => Promise<CryptoKey>;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Where a host keeps small secrets between sessions.
|
|
264
|
+
*
|
|
265
|
+
* Three methods, because that is all the identity cache needs and every platform
|
|
266
|
+
* has them: `localStorage` in a page, `chrome.storage.local` in an extension,
|
|
267
|
+
* Keychain or SharedPreferences on mobile, a file on a server. Keeping the
|
|
268
|
+
* surface this small is what makes those adapters a few lines each.
|
|
269
|
+
*
|
|
270
|
+
* Values handed to `set` are already ENCRYPTED — see `sessionCache`. A store is
|
|
271
|
+
* not trusted with plaintext secrets, so a backend with weak isolation is still
|
|
272
|
+
* a safe place to put one.
|
|
273
|
+
*/
|
|
274
|
+
interface SecretStore {
|
|
275
|
+
get(key: string): Promise<string | null>;
|
|
276
|
+
set(key: string, value: string): Promise<void>;
|
|
277
|
+
remove(key: string): Promise<void>;
|
|
278
|
+
/**
|
|
279
|
+
* Keys currently held. Needed to clear every network's cache for one account
|
|
280
|
+
* on disconnect, which cannot be done by constructing keys — the caller does
|
|
281
|
+
* not know which chains the user has visited.
|
|
282
|
+
*/
|
|
283
|
+
keys(): Promise<string[]>;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* An in-memory `SecretStore`. Loses everything on restart, which is the whole
|
|
287
|
+
* point of the real ones — useful for tests and for a host that deliberately
|
|
288
|
+
* wants re-signing on every launch.
|
|
289
|
+
*/
|
|
290
|
+
declare function createMemorySecretStore(): SecretStore;
|
|
291
|
+
|
|
292
|
+
export { type CachedNullifier as C, type DeviceKeyStore as D, type EncryptedNoteRecord as E, type NullifierSyncMeta as N, type SpendDetails as S, type TxHistoryStore as T, type VaultStorage as V, type VaultConfigRecord as a, type EncryptedTxRecord as b, type SecretStore as c, type NoteStorage as d, type NoteStatusUpdate as e, type NullifierCache as f, createDeviceKeyProvider as g, createMemorySecretStore as h, generateDeviceKey as i, importDeviceKey as j };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { c as DECRYPT_YIELD_EVERY, d as DecryptBatchResult, a as DecryptPool, e as DecryptRequest, E as EMPTY_BATCH_RESULT, K as KnownEphEntry, f as KnownEphWindow, M as MAX_WORKERS, g as MatchSource, P as PAIRWISE_EPH_WINDOW, i as SELF_EPH_WINDOW, b as ScanKeys, W as WORKER_CRASHED, j as WorkerFactory, k as WorkerLike, l as WorkerMessage, m as clearKnownEphWindow, n as createDecryptPool, o as createMainThreadPool, p as createWorkerPool, q as decryptHintBatch, r as getKnownEphWindow } from '../../index-JYVjYJtf.mjs';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { c as DECRYPT_YIELD_EVERY, d as DecryptBatchResult, a as DecryptPool, e as DecryptRequest, E as EMPTY_BATCH_RESULT, K as KnownEphEntry, f as KnownEphWindow, M as MAX_WORKERS, g as MatchSource, P as PAIRWISE_EPH_WINDOW, i as SELF_EPH_WINDOW, b as ScanKeys, W as WORKER_CRASHED, j as WorkerFactory, k as WorkerLike, l as WorkerMessage, m as clearKnownEphWindow, n as createDecryptPool, o as createMainThreadPool, p as createWorkerPool, q as decryptHintBatch, r as getKnownEphWindow } from '../../index-JYVjYJtf.js';
|