@reclaimprotocol/client 0.1.27 → 0.1.28
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 +634 -42
- package/lib/api-client.d.ts +31 -0
- package/lib/api-client.js +112 -0
- package/lib/api-consts.d.ts +4 -0
- package/lib/api-consts.js +4 -0
- package/lib/api-errors.d.ts +13 -0
- package/lib/api-errors.js +30 -0
- package/lib/api.d.ts +7 -0
- package/lib/api.js +3 -0
- package/lib/attestor-proof.d.ts +6 -0
- package/lib/attestor-proof.js +45 -18
- package/lib/client.d.ts +1 -6
- package/lib/client.js +12 -33
- package/lib/consts.d.ts +1 -1
- package/lib/consts.js +1 -1
- package/lib/eip191.d.ts +4 -2
- package/lib/eip191.js +17 -1
- package/lib/errors.d.ts +38 -4
- package/lib/errors.js +99 -16
- package/lib/index.d.ts +9 -7
- package/lib/index.js +8 -4
- package/lib/jwks.d.ts +25 -1
- package/lib/jwks.js +69 -4
- package/lib/legacy-proof.d.ts +57 -0
- package/lib/legacy-proof.js +211 -0
- package/lib/proof-hash.d.ts +35 -0
- package/lib/proof-hash.js +216 -0
- package/lib/proof-recipe.d.ts +18 -0
- package/lib/proof-recipe.js +37 -0
- package/lib/proof-trust-constants.d.ts +3 -0
- package/lib/proof-trust-constants.js +43 -0
- package/lib/proof-types.d.ts +62 -0
- package/lib/proof-types.js +1 -0
- package/lib/proof-verification-error.d.ts +13 -0
- package/lib/proof-verification-error.js +11 -0
- package/lib/reclaim.d.ts +27 -5
- package/lib/reclaim.js +140 -14
- package/lib/result.d.ts +58 -6
- package/lib/result.js +252 -61
- package/lib/routes.gen.d.ts +54 -6
- package/lib/routes.gen.js +15 -3
- package/lib/tee-binding.d.ts +82 -0
- package/lib/tee-binding.js +112 -0
- package/lib/types/openapi.gen.d.ts +1343 -206
- package/lib/verification-clients.d.ts +17 -0
- package/lib/verification-clients.js +14 -0
- package/lib/verify-attestor-tee.d.ts +76 -0
- package/lib/verify-attestor-tee.js +243 -0
- package/lib/verify-tee.d.ts +32 -0
- package/lib/verify-tee.js +347 -0
- package/package.json +5 -1
- package/src/api.ts +15 -0
- package/src/attestor-proof.ts +57 -20
- package/src/client.ts +15 -38
- package/src/consts.ts +1 -2
- package/src/eip191.ts +31 -3
- package/src/errors.ts +132 -18
- package/src/index.ts +50 -5
- package/src/jwks.ts +128 -5
- package/src/legacy-proof.ts +367 -0
- package/src/proof-hash.ts +316 -0
- package/src/proof-recipe.ts +75 -0
- package/src/proof-trust-constants.ts +45 -0
- package/src/proof-types.ts +57 -0
- package/src/proof-verification-error.ts +13 -0
- package/src/reclaim.ts +206 -19
- package/src/result.ts +403 -68
- package/src/routes.gen.ts +15 -3
- package/src/tee-binding.ts +192 -0
- package/src/types/openapi.gen.ts +1343 -206
- package/src/verification-clients.ts +22 -0
- package/src/verify-attestor-tee.ts +365 -0
- package/src/verify-tee.ts +507 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { createSignDataForClaim } from '@reclaimprotocol/attestor-core'
|
|
2
|
+
import { recoverEip191Address } from './eip191.ts'
|
|
3
|
+
import { fetchReclaimJwks, trustedAttestorAddresses } from './jwks.ts'
|
|
4
|
+
import { assertValidProofHashes, getHashFromProof } from './proof-hash.ts'
|
|
5
|
+
import type {
|
|
6
|
+
HashRequirement,
|
|
7
|
+
LegacyProof,
|
|
8
|
+
TrustedData,
|
|
9
|
+
} from './proof-types.ts'
|
|
10
|
+
import {
|
|
11
|
+
type AttestorTeeAttestationConfig,
|
|
12
|
+
runAttestorTeeVerification,
|
|
13
|
+
} from './verify-attestor-tee.ts'
|
|
14
|
+
import { runTeeVerification, type TeeAttestationConfig } from './verify-tee.ts'
|
|
15
|
+
|
|
16
|
+
export type { LegacyProof } from './proof-types.ts'
|
|
17
|
+
|
|
18
|
+
/** A proof hash requirement accepted by legacy `verifyProof`. */
|
|
19
|
+
export type ProofHashRequirement = string | HashRequirement
|
|
20
|
+
|
|
21
|
+
export interface ResolvedSessionProofValidation {
|
|
22
|
+
hashes?: ProofHashRequirement[]
|
|
23
|
+
providerId?: string
|
|
24
|
+
providerVersion?: string
|
|
25
|
+
allowedTags?: string[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Exact provider requirements returned by a caller-controlled resolver. */
|
|
29
|
+
export interface ResolvedProviderProofValidation {
|
|
30
|
+
hashes: ProofHashRequirement[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Options intentionally preserve legacy `verifyProof` vocabulary. */
|
|
34
|
+
export interface LegacyVerifyProofOptions {
|
|
35
|
+
hashes?: ProofHashRequirement[]
|
|
36
|
+
providerId?: string
|
|
37
|
+
providerVersion?: string
|
|
38
|
+
allowedTags?: string[]
|
|
39
|
+
dangerouslyDisableContentValidation?: true
|
|
40
|
+
/** Read `providerHash` from context, while still verifying signatures. */
|
|
41
|
+
hasNoPii?: true
|
|
42
|
+
teeAttestation?: TeeAttestationConfig
|
|
43
|
+
attestorTeeAttestation?: AttestorTeeAttestationConfig
|
|
44
|
+
/** Explicit trusted attestor ETH addresses. */
|
|
45
|
+
attestorAddresses?: string[]
|
|
46
|
+
/** Trusted Builder JWKS registry containing attestor-purpose keys. */
|
|
47
|
+
attestorJwksUrl?: string
|
|
48
|
+
fetchImpl?: typeof fetch
|
|
49
|
+
/** Builder session id. Requires a resolver without hashes or a provider. */
|
|
50
|
+
sessionId?: string
|
|
51
|
+
resolveSession?: (
|
|
52
|
+
sessionId: string,
|
|
53
|
+
proofs: LegacyProof[],
|
|
54
|
+
) => Promise<ResolvedSessionProofValidation>
|
|
55
|
+
| ResolvedSessionProofValidation
|
|
56
|
+
/** Resolve an exact provider without coupling this verifier to HTTP. */
|
|
57
|
+
resolveProvider?: (
|
|
58
|
+
providerId: string,
|
|
59
|
+
providerVersion: string | undefined,
|
|
60
|
+
allowedTags: string[] | undefined,
|
|
61
|
+
proofs: LegacyProof[],
|
|
62
|
+
) => Promise<ResolvedProviderProofValidation[]>
|
|
63
|
+
| ResolvedProviderProofValidation[]
|
|
64
|
+
expectedSessionId?: string
|
|
65
|
+
requireSessionBinding?: boolean
|
|
66
|
+
expectedContext?: Record<string, unknown>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface LegacyProofOutcome {
|
|
70
|
+
valid: boolean
|
|
71
|
+
reason?: string
|
|
72
|
+
proofHashes?: string[]
|
|
73
|
+
isTeeAttestationVerified?: boolean
|
|
74
|
+
isAttestorTeeAttestationVerified?: boolean
|
|
75
|
+
data?: TrustedData[]
|
|
76
|
+
publicData?: unknown[]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Return the provider hashes encoded in an exact legacy proof. */
|
|
80
|
+
export function proofHashes(proof: LegacyProof): string[] {
|
|
81
|
+
return getHashFromProof(proof)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Verify exact legacy proofs without depending on `@reclaimprotocol/js-sdk`.
|
|
86
|
+
* The signed claim is never renamed or reconstructed.
|
|
87
|
+
*/
|
|
88
|
+
export async function verifyProof(
|
|
89
|
+
proofOrProofs: LegacyProof | LegacyProof[],
|
|
90
|
+
config: LegacyVerifyProofOptions,
|
|
91
|
+
): Promise<LegacyProofOutcome> {
|
|
92
|
+
const proofs = Array.isArray(proofOrProofs) ? proofOrProofs : [proofOrProofs]
|
|
93
|
+
if(!proofs.length) {
|
|
94
|
+
return { valid: false, reason: 'no-proofs' }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const effective = await resolveValidation(config, proofs)
|
|
99
|
+
assertSessionAndContext(proofs, effective)
|
|
100
|
+
const attestors = await resolveTrustedAttestors(effective)
|
|
101
|
+
assertVerifiedSignatures(proofs, attestors)
|
|
102
|
+
await assertContent(proofs, effective)
|
|
103
|
+
|
|
104
|
+
let isTeeAttestationVerified: boolean | undefined
|
|
105
|
+
if(effective.teeAttestation) {
|
|
106
|
+
await runTeeVerification(proofs, effective.teeAttestation)
|
|
107
|
+
isTeeAttestationVerified = true
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let isAttestorTeeAttestationVerified: boolean | undefined
|
|
111
|
+
if(effective.attestorTeeAttestation) {
|
|
112
|
+
await runAttestorTeeVerification(
|
|
113
|
+
proofs,
|
|
114
|
+
effective.attestorTeeAttestation,
|
|
115
|
+
)
|
|
116
|
+
isAttestorTeeAttestationVerified = true
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
valid: true,
|
|
121
|
+
proofHashes: collectProofHashes(proofs, effective.hasNoPii),
|
|
122
|
+
isTeeAttestationVerified,
|
|
123
|
+
isAttestorTeeAttestationVerified,
|
|
124
|
+
data: proofs.map(trustedData),
|
|
125
|
+
publicData: proofs.map((proof) => proof.publicData ?? {}),
|
|
126
|
+
}
|
|
127
|
+
} catch(error) {
|
|
128
|
+
return {
|
|
129
|
+
valid: false,
|
|
130
|
+
reason: failureReason(error),
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function resolveValidation(
|
|
136
|
+
config: LegacyVerifyProofOptions,
|
|
137
|
+
proofs: LegacyProof[],
|
|
138
|
+
) {
|
|
139
|
+
let effective = config
|
|
140
|
+
if(config.sessionId && !hasContentValidation(config)) {
|
|
141
|
+
if(!config.resolveSession) {
|
|
142
|
+
throw new Error('session-resolver-required')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
effective = {
|
|
146
|
+
...config,
|
|
147
|
+
...await config.resolveSession(config.sessionId, proofs),
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return effective
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function resolveTrustedAttestors(config: LegacyVerifyProofOptions) {
|
|
155
|
+
const addresses = config.attestorAddresses?.map(normalized) ?? []
|
|
156
|
+
if(config.attestorJwksUrl) {
|
|
157
|
+
const registry = await fetchReclaimJwks(
|
|
158
|
+
config.attestorJwksUrl,
|
|
159
|
+
config.fetchImpl,
|
|
160
|
+
)
|
|
161
|
+
addresses.push(
|
|
162
|
+
...(registry['x-reclaim-attestors'] ?? []),
|
|
163
|
+
...trustedAttestorAddresses(registry.keys),
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const unique = [...new Set(addresses)]
|
|
168
|
+
if(!unique.length) {
|
|
169
|
+
throw new Error('attestor-trust-required')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return unique
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function assertVerifiedSignatures(
|
|
176
|
+
proofs: LegacyProof[],
|
|
177
|
+
trustedAttestors: string[],
|
|
178
|
+
) {
|
|
179
|
+
for(const proof of proofs) {
|
|
180
|
+
if(!proof.signatures?.length || !proof.witnesses?.length) {
|
|
181
|
+
throw new Error('proof-signature-missing')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const signData = createSignDataForClaim(proof.claimData)
|
|
185
|
+
const signers = proof.signatures.map((signature) => (
|
|
186
|
+
normalized(recoverEip191Address(signData, signature))
|
|
187
|
+
))
|
|
188
|
+
const witnesses = proof.witnesses.map(({ id }) => normalized(id))
|
|
189
|
+
if(witnesses.some((witness) => !trustedAttestors.includes(witness))) {
|
|
190
|
+
throw new Error('attestor-not-allowed')
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if(!signers.some((signer) => trustedAttestors.includes(signer))) {
|
|
194
|
+
throw new Error('attestor-not-allowed')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if(!signers.every((signer) => witnesses.includes(signer))) {
|
|
198
|
+
throw new Error('witness-signature-mismatch')
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function assertContent(
|
|
204
|
+
proofs: LegacyProof[],
|
|
205
|
+
config: LegacyVerifyProofOptions,
|
|
206
|
+
) {
|
|
207
|
+
if(config.dangerouslyDisableContentValidation) {
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if(config.hashes) {
|
|
212
|
+
assertValidProofHashes(proofs, config.hashes, config.hasNoPii)
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if(config.providerId) {
|
|
217
|
+
if(!config.resolveProvider) {
|
|
218
|
+
throw new Error('provider-resolver-required')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const candidates = await config.resolveProvider(
|
|
222
|
+
config.providerId,
|
|
223
|
+
config.providerVersion,
|
|
224
|
+
config.allowedTags,
|
|
225
|
+
proofs,
|
|
226
|
+
)
|
|
227
|
+
if(!candidates.length) {
|
|
228
|
+
throw new Error('provider-not-found')
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let lastError: unknown
|
|
232
|
+
for(const candidate of candidates) {
|
|
233
|
+
try {
|
|
234
|
+
assertValidProofHashes(proofs, candidate.hashes, config.hasNoPii)
|
|
235
|
+
return
|
|
236
|
+
} catch(error) {
|
|
237
|
+
lastError = error
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
throw lastError
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
throw new Error('proof-validation-required')
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function assertSessionAndContext(
|
|
248
|
+
proofs: LegacyProof[],
|
|
249
|
+
config: LegacyVerifyProofOptions,
|
|
250
|
+
) {
|
|
251
|
+
const expectedSessionId = config.sessionId ?? config.expectedSessionId
|
|
252
|
+
for(const proof of proofs) {
|
|
253
|
+
if(expectedSessionId) {
|
|
254
|
+
const ids = sessionIds(proof)
|
|
255
|
+
if(ids.some((id) => id !== expectedSessionId)) {
|
|
256
|
+
throw new Error('session-mismatch')
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if((config.sessionId || config.requireSessionBinding) && !ids.length) {
|
|
260
|
+
throw new Error('session-missing')
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if(!contextMatches(proof, config.expectedContext)) {
|
|
265
|
+
throw new Error('context-mismatch')
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function trustedData(proof: LegacyProof): TrustedData {
|
|
271
|
+
const context = contextFrom(proof)
|
|
272
|
+
const extractedParameters = isRecord(context.extractedParameters)
|
|
273
|
+
? Object.fromEntries(Object.entries(context.extractedParameters).map(
|
|
274
|
+
([key, value]) => [key, String(value)],
|
|
275
|
+
))
|
|
276
|
+
: {}
|
|
277
|
+
delete context.extractedParameters
|
|
278
|
+
return { context, extractedParameters }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function collectProofHashes(
|
|
282
|
+
proofs: LegacyProof[],
|
|
283
|
+
hasNoPii: true | undefined,
|
|
284
|
+
): string[] | undefined {
|
|
285
|
+
try {
|
|
286
|
+
return proofs.flatMap((proof) => getHashFromProof(proof, hasNoPii))
|
|
287
|
+
} catch{
|
|
288
|
+
return undefined
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function hasContentValidation(config: LegacyVerifyProofOptions): boolean {
|
|
293
|
+
return Boolean(
|
|
294
|
+
config.hashes
|
|
295
|
+
|| config.providerId
|
|
296
|
+
|| config.dangerouslyDisableContentValidation,
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function failureReason(error: unknown): string {
|
|
301
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
302
|
+
const lower = message.toLowerCase()
|
|
303
|
+
if(
|
|
304
|
+
lower.includes('multiple')
|
|
305
|
+
|| lower.includes('duplicate')
|
|
306
|
+
|| lower.includes('more than once')
|
|
307
|
+
) {
|
|
308
|
+
return 'duplicate-proof-hash'
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return message || 'proof-not-verified'
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function normalized(value: string): string {
|
|
315
|
+
return value.trim().toLowerCase()
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function contextFrom(proof: LegacyProof): Record<string, unknown> {
|
|
319
|
+
const raw = proof.claimData.context
|
|
320
|
+
if(!raw) {
|
|
321
|
+
return {}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const context: unknown = JSON.parse(raw)
|
|
326
|
+
return isRecord(context) ? { ...context } : {}
|
|
327
|
+
} catch{
|
|
328
|
+
return {}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function sessionIds(proof: LegacyProof): string[] {
|
|
333
|
+
const context = contextFrom(proof)
|
|
334
|
+
const ids = [context.reclaimSessionId]
|
|
335
|
+
try {
|
|
336
|
+
const parameters = JSON.parse(
|
|
337
|
+
proof.claimData.parameters,
|
|
338
|
+
) as Record<string, unknown>
|
|
339
|
+
ids.push(parameters.proxySessionId, parameters.sessionId)
|
|
340
|
+
} catch{
|
|
341
|
+
// A non-HTTP claim is valid when session binding wasn't requested.
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return ids.filter(
|
|
345
|
+
(value): value is string => (
|
|
346
|
+
typeof value === 'string' && value.length > 0
|
|
347
|
+
),
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function contextMatches(
|
|
352
|
+
proof: LegacyProof,
|
|
353
|
+
expected?: Record<string, unknown>,
|
|
354
|
+
): boolean {
|
|
355
|
+
if(!expected) {
|
|
356
|
+
return true
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const context = contextFrom(proof)
|
|
360
|
+
return Object.entries(expected).every(([key, value]) => (
|
|
361
|
+
JSON.stringify(context[key]) === JSON.stringify(value)
|
|
362
|
+
))
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
366
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value))
|
|
367
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { keccak_256 } from '@noble/hashes/sha3.js'
|
|
2
|
+
import { canonicalJson as canonical } from './canonical.ts'
|
|
3
|
+
import { bytesToHex } from './encoding.ts'
|
|
4
|
+
import type { HashRequirement, LegacyProof } from './proof-types.ts'
|
|
5
|
+
|
|
6
|
+
export interface HttpProofParams {
|
|
7
|
+
url: string
|
|
8
|
+
method: string
|
|
9
|
+
body?: string
|
|
10
|
+
responseMatches: Array<{
|
|
11
|
+
type?: string
|
|
12
|
+
value?: string
|
|
13
|
+
invert?: boolean
|
|
14
|
+
isOptional?: boolean
|
|
15
|
+
}>
|
|
16
|
+
responseRedactions: Array<{
|
|
17
|
+
xPath?: string
|
|
18
|
+
jsonPath?: string
|
|
19
|
+
regex?: string
|
|
20
|
+
hash?: string
|
|
21
|
+
}>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ProofRequestSpec extends HttpProofParams {
|
|
25
|
+
urlType: string
|
|
26
|
+
bodySniff: { enabled: boolean, template: string }
|
|
27
|
+
required?: boolean
|
|
28
|
+
multiple?: boolean
|
|
29
|
+
templateParams?: string[]
|
|
30
|
+
templateParamsMode?: 'separate' | 'merge'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Exact legacy canonical HTTP-provider hashes, including optional matches. */
|
|
34
|
+
export function hashProofClaimParams(params: HttpProofParams): string[] {
|
|
35
|
+
return canonicalProofParams(params).map((serialized) => (
|
|
36
|
+
'0x' + bytesToHex(keccak_256(new TextEncoder().encode(serialized)))
|
|
37
|
+
))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getHashFromProof(
|
|
41
|
+
proof: LegacyProof,
|
|
42
|
+
hasNoPii = false,
|
|
43
|
+
): string[] {
|
|
44
|
+
if(hasNoPii) {
|
|
45
|
+
const context = JSON.parse(
|
|
46
|
+
proof.claimData.context,
|
|
47
|
+
) as Record<string, unknown>
|
|
48
|
+
if(typeof context.providerHash !== 'string' || !context.providerHash) {
|
|
49
|
+
throw new Error('Provider hash must be present in proof context')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return [context.providerHash]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const parsed: unknown = JSON.parse(proof.claimData.parameters)
|
|
56
|
+
if(!isHttpProofParams(parsed)) {
|
|
57
|
+
throw new Error('Proof has no HTTP provider params to hash')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return hashProofClaimParams(parsed)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function hashRequestSpec(request: ProofRequestSpec): HashRequirement {
|
|
64
|
+
return {
|
|
65
|
+
value: hashProofClaimParams({
|
|
66
|
+
...request,
|
|
67
|
+
body: request.bodySniff.enabled ? request.bodySniff.template : '',
|
|
68
|
+
}),
|
|
69
|
+
required: request.required,
|
|
70
|
+
multiple: request.multiple,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function hashRequirementsFromSpecs(
|
|
75
|
+
requestData: ProofRequestSpec[],
|
|
76
|
+
allowedInjectedRequestData: ProofRequestSpec[],
|
|
77
|
+
proofs: LegacyProof[],
|
|
78
|
+
): HashRequirement[] {
|
|
79
|
+
const templates = expandTemplates(
|
|
80
|
+
allowedInjectedRequestData,
|
|
81
|
+
templateParametersFromProofs(proofs),
|
|
82
|
+
)
|
|
83
|
+
return [...requestData, ...templates].map(hashRequestSpec)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function assertValidProofHashes(
|
|
87
|
+
proofs: LegacyProof[],
|
|
88
|
+
requirements: Array<string | HashRequirement>,
|
|
89
|
+
hasNoPii = false,
|
|
90
|
+
) {
|
|
91
|
+
const unmatched = new Map(proofs.map((proof, index) => (
|
|
92
|
+
[index, getHashFromProof(proof, hasNoPii)]
|
|
93
|
+
)))
|
|
94
|
+
for(const raw of requirements) {
|
|
95
|
+
const requirement = typeof raw === 'string' ? { value: raw } : raw
|
|
96
|
+
const expected = (Array.isArray(requirement.value)
|
|
97
|
+
? requirement.value
|
|
98
|
+
: [requirement.value]).map(normalized)
|
|
99
|
+
let found = false
|
|
100
|
+
for(const [index, hashes] of unmatched) {
|
|
101
|
+
if(!hashes.map(normalized).some((hash) => expected.includes(hash))) {
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
unmatched.delete(index)
|
|
106
|
+
if(found && requirement.multiple === false) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Proof by hash '${expected.join(',')}' is not allowed `
|
|
109
|
+
+ 'to appear more than once',
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
found = true
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if(!found && requirement.required !== false) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Proof by required hash '${expected.join(',')}' was not found`,
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if(unmatched.size) {
|
|
124
|
+
throw new Error(`${unmatched.size} extra proof(s) could not be validated`)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function canonicalProofParams(params: HttpProofParams): string[] {
|
|
129
|
+
const count = params.responseMatches.length
|
|
130
|
+
const values: string[] = []
|
|
131
|
+
for(let mask = 0; mask < 2 ** count; mask++) {
|
|
132
|
+
let valid = true
|
|
133
|
+
const matches: Array<Record<string, unknown>> = []
|
|
134
|
+
const redactions: Array<Record<string, unknown>> = []
|
|
135
|
+
for(let index = 0; index < count; index++) {
|
|
136
|
+
const match = params.responseMatches[index]
|
|
137
|
+
const redaction = params.responseRedactions[index]
|
|
138
|
+
if((mask & (1 << index)) === 0) {
|
|
139
|
+
if(match && !match.isOptional) {
|
|
140
|
+
valid = false
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if(match) {
|
|
147
|
+
matches.push({
|
|
148
|
+
value: match.value ?? '',
|
|
149
|
+
type: match.type ?? 'contains',
|
|
150
|
+
invert: match.invert || undefined,
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if(redaction) {
|
|
155
|
+
redactions.push({
|
|
156
|
+
xPath: redaction.xPath ?? '',
|
|
157
|
+
jsonPath: redaction.jsonPath ?? '',
|
|
158
|
+
regex: redaction.regex ?? '',
|
|
159
|
+
hash: normalizedRedactionHash(redaction.hash),
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if(valid && matches.length) {
|
|
165
|
+
values.push(canonical({
|
|
166
|
+
url: params.url ?? '',
|
|
167
|
+
method: params.method ?? 'GET',
|
|
168
|
+
body: params.body ?? '',
|
|
169
|
+
responseMatches: matches,
|
|
170
|
+
responseRedactions: redactions,
|
|
171
|
+
}))
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return values.length ? values : [canonical({
|
|
176
|
+
url: params.url ?? '',
|
|
177
|
+
method: params.method ?? 'GET',
|
|
178
|
+
body: params.body ?? '',
|
|
179
|
+
responseMatches: [],
|
|
180
|
+
responseRedactions: [],
|
|
181
|
+
})]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function expandTemplates(
|
|
185
|
+
templates: ProofRequestSpec[],
|
|
186
|
+
parameters: Record<string, string[]>,
|
|
187
|
+
): ProofRequestSpec[] {
|
|
188
|
+
const result: ProofRequestSpec[] = []
|
|
189
|
+
for(const template of templates) {
|
|
190
|
+
const names = template.templateParams ?? []
|
|
191
|
+
if(!names.length) {
|
|
192
|
+
result.push(template)
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const present = names.filter((name) => parameters[name]?.length)
|
|
197
|
+
if(present.length !== names.length) {
|
|
198
|
+
if(template.required === false && !present.length) {
|
|
199
|
+
continue
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
throw new Error('Not all template variables are present')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const length = parameters[names[0]].length
|
|
206
|
+
if(!names.every((name) => parameters[name].length === length)) {
|
|
207
|
+
throw new Error('Not all template variables have same length')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const expanded = Array.from({ length }, (_, index) => substitute(
|
|
211
|
+
template,
|
|
212
|
+
Object.fromEntries(names.map((name) => [name, parameters[name][index]])),
|
|
213
|
+
))
|
|
214
|
+
if(template.templateParamsMode !== 'merge') {
|
|
215
|
+
result.push(...expanded)
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
result.push({
|
|
220
|
+
...template,
|
|
221
|
+
responseMatches: expanded.flatMap((item) => item.responseMatches),
|
|
222
|
+
responseRedactions: expanded.flatMap((item) => item.responseRedactions),
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return result
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function substitute(
|
|
230
|
+
template: ProofRequestSpec,
|
|
231
|
+
parameters: Record<string, string>,
|
|
232
|
+
): ProofRequestSpec {
|
|
233
|
+
const replace = (value: string) => Object.entries(parameters).reduce(
|
|
234
|
+
(result, [name, replacement]) => (
|
|
235
|
+
result.split(`\${${name}}`).join(replacement)
|
|
236
|
+
),
|
|
237
|
+
value,
|
|
238
|
+
)
|
|
239
|
+
return {
|
|
240
|
+
...template,
|
|
241
|
+
responseMatches: template.responseMatches.map((match) => ({
|
|
242
|
+
...match,
|
|
243
|
+
value: replace(match.value ?? ''),
|
|
244
|
+
})),
|
|
245
|
+
responseRedactions: template.responseRedactions.map((redaction) => ({
|
|
246
|
+
...redaction,
|
|
247
|
+
xPath: replace(redaction.xPath ?? ''),
|
|
248
|
+
jsonPath: replace(redaction.jsonPath ?? ''),
|
|
249
|
+
regex: replace(redaction.regex ?? ''),
|
|
250
|
+
})),
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function templateParametersFromProofs(proofs: LegacyProof[]) {
|
|
255
|
+
const combined: Record<string, unknown> = {}
|
|
256
|
+
for(const proof of proofs) {
|
|
257
|
+
for(const raw of [proof.claimData.parameters, proof.claimData.context]) {
|
|
258
|
+
try {
|
|
259
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>
|
|
260
|
+
Object.assign(combined, parsed.paramValues, parsed.extractedParameters)
|
|
261
|
+
} catch{
|
|
262
|
+
// A proof without template parameters is valid.
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const result: Record<string, string[]> = {}
|
|
268
|
+
for(const [name, raw] of Object.entries(combined)) {
|
|
269
|
+
let value = raw
|
|
270
|
+
if(typeof value === 'string') {
|
|
271
|
+
try {
|
|
272
|
+
value = JSON.parse(value)
|
|
273
|
+
} catch{ /* plain string */ }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if(Array.isArray(value) && value.length) {
|
|
277
|
+
result[name] = value.map(String)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return result
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function isHttpProofParams(value: unknown): value is HttpProofParams {
|
|
285
|
+
if(!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
286
|
+
return false
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const params = value as Record<string, unknown>
|
|
290
|
+
return typeof params.url === 'string'
|
|
291
|
+
&& typeof params.method === 'string'
|
|
292
|
+
&& ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(params.method)
|
|
293
|
+
&& (
|
|
294
|
+
params.body === null
|
|
295
|
+
|| params.body === undefined
|
|
296
|
+
|| typeof params.body === 'string'
|
|
297
|
+
)
|
|
298
|
+
&& Array.isArray(params.responseMatches)
|
|
299
|
+
&& params.responseMatches.length > 0
|
|
300
|
+
&& Array.isArray(params.responseRedactions)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function normalized(value: string) {
|
|
304
|
+
return value.toLowerCase().trim()
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* All OPRF transports prove the same provider-level intent: the selected
|
|
309
|
+
* value is hidden behind an OPRF. Verification Clients may use the transport
|
|
310
|
+
* they implement, so transport spelling must not change the provider hash.
|
|
311
|
+
*/
|
|
312
|
+
function normalizedRedactionHash(value: string | undefined) {
|
|
313
|
+
return value === 'oprf' || value === 'oprf-mpc' || value === 'oprf-raw'
|
|
314
|
+
? '_oprf'
|
|
315
|
+
: value || undefined
|
|
316
|
+
}
|