Skip to content

Commit 491ddff

Browse files
authored
Merge pull request #88 from reclaimprotocol/proof-validation
Add proof validation
2 parents e722109 + ffaeb21 commit 491ddff

19 files changed

Lines changed: 10381 additions & 972 deletions

example/package-lock.json

Lines changed: 8608 additions & 70 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
},
1111
"dependencies": {
1212
"@reclaimprotocol/js-sdk": "file:..",
13-
"next": "^15.5.2",
13+
"next": "^15.5.10",
1414
"react": "^18",
1515
"react-dom": "^18"
1616
},
@@ -23,4 +23,4 @@
2323
"tailwindcss": "^3.3.0",
2424
"typescript": "^5"
2525
}
26-
}
26+
}

package-lock.json

Lines changed: 741 additions & 814 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@reclaimprotocol/js-sdk",
3-
"version": "4.15.2",
3+
"version": "5.0.0",
44
"description": "Designed to request proofs from the Reclaim protocol and manage the flow of claims and witness interactions.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
@@ -74,7 +74,7 @@
7474
"devDependencies": {
7575
"@commitlint/cli": "^17.7.1",
7676
"@commitlint/config-conventional": "^17.7.0",
77-
"@release-it/conventional-changelog": "^10.0.1",
77+
"@release-it/conventional-changelog": "10.0.6",
7878
"@types/jest": "^30.0.0",
7979
"@types/qs": "^6.9.11",
8080
"@types/qrcode": "^1.5.5",
@@ -83,7 +83,7 @@
8383
"jest": "^30.1.3",
8484
"jest-environment-jsdom": "^30.1.2",
8585
"qs": "^6.11.2",
86-
"release-it": "^19.0.4",
86+
"release-it": "^19.2.4",
8787
"ts-jest": "^29.4.1",
8888
"tsup": "^8.0.1",
8989
"typescript": "^5.3.3"

src/Reclaim.ts

Lines changed: 96 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type Proof, type Context, RECLAIM_EXTENSION_ACTIONS, ExtensionMessage } from './utils/interfaces'
1+
import { type Proof, type Context, RECLAIM_EXTENSION_ACTIONS, ExtensionMessage, ProviderVersionInfo } from './utils/interfaces'
22
import {
33
ProofRequestOptions,
44
StartSessionParams,
@@ -34,69 +34,83 @@ import {
3434
SignatureGeneratingError,
3535
SignatureNotFoundError,
3636
ErrorDuringVerificationError,
37-
CallbackUrlRequiredError
38-
} from './utils/errors'
37+
CallbackUrlRequiredError,
38+
ProofNotValidatedError
39+
} from './utils/errors';
3940
import { validateContext, validateFunctionParams, validateParameters, validateSignature, validateURL, validateModalOptions, validateFunctionParamsWithFn, validateRedirectionMethod, validateRedirectionBody } from './utils/validationUtils'
4041
import { fetchStatusUrl, initSession, updateSession } from './utils/sessionUtils'
41-
import { createLinkWithTemplateData, getAttestors, recoverSignersOfSignedClaim } from './utils/proofUtils'
42+
import { assertVerifiedProof, createLinkWithTemplateData, getAttestors } from './utils/proofUtils'
4243
import { QRCodeModal } from './utils/modalUtils'
4344
import loggerModule from './utils/logger';
4445
import { getDeviceType, getMobileDeviceType } from './utils/device'
4546
import { canonicalStringify } from './utils/strings'
47+
import { assertValidateProof, VerificationConfig } from './utils/proofValidationUtils'
48+
import { fetchProviderHashRequirementsBy, ProviderHashRequirementsConfig } from './utils/providerUtils'
4649

4750
const logger = loggerModule.logger
4851

4952
const sdkVersion = require('../package.json').version;
5053

5154
/**
52-
* Verifies one or more Reclaim proofs by validating signatures and witness information
55+
* Verifies one or more Reclaim proofs by validating signatures, verifying witness information,
56+
* and performing content validation against the expected configuration.
57+
*
58+
* See also:
59+
*
60+
* * `ReclaimProofRequest.getProviderHashRequirements()` - To get the expected proof hash requirements for a proof request.
61+
* * `fetchProviderHashRequirementsBy()` - To get the expected proof hash requirements for a provider version by providing providerId and exactProviderVersionString.
62+
* * `getProviderHashRequirementsFromSpec()` - To get the expected proof hash requirements from a provider spec.
63+
* * All 3 functions above are alternatives of each other and result from these functions can be directly used as `config` parameter in this function for proof validation.
5364
*
54-
* @param proofOrProofs - A single proof object or an array of proof objects to verify
55-
* @param allowAiWitness - Optional flag to allow AI witness verification. Defaults to false
56-
* @returns Promise<boolean> - Returns true if all proofs are valid, false otherwise
57-
* @throws {SignatureNotFoundError} When proof has no signatures
58-
* @throws {ProofNotVerifiedError} When identifier mismatch occurs
65+
* @param proofOrProofs - A single proof object or an array of proof objects to be verified.
66+
* @param config - Verification configuration that specifies required hashes, allowed extra hashes, or disables content validation.
67+
* @returns Promise<boolean> - Returns `true` if all proofs are successfully verified and validated, `false` otherwise.
68+
* @throws {ProofNotVerifiedError} When signature validation or identifier mismatch occurs.
69+
* @throws {ProofNotValidatedError} When no proofs are provided, when the configuration is missing, or when proof content does not match the expectations set in the config.
5970
*
6071
* @example
6172
* ```typescript
62-
* const isValid = await verifyProof(proof);
63-
* const areAllValid = await verifyProof([proof1, proof2, proof3]);
64-
* const isValidWithAI = await verifyProof(proof, true);
73+
* // Validate a single proof against expected hash
74+
* const isValid = await verifyProof(proof, { hashes: ['0xAbC...'] });
75+
*
76+
* // Validate multiple proofs
77+
* const areAllValid = await verifyProof([proof1, proof2], {
78+
* hashes: ['0xAbC...', '0xF22..'],
79+
* });
80+
*
81+
* // Validate 1 required proofs, any number of multiple with same hash, and one optional
82+
* const areAllValid = await verifyProof([proof1, proof2, sameAsProof2], {
83+
* hashes: ['0xAbC...', { value: '0xF22..', multiple: true }, { value: '0xE33..', required: false }],
84+
* });
6585
* ```
6686
*/
6787
export async function verifyProof(
68-
proofOrProofs: Proof | Proof[],
69-
allowAiWitness = false
70-
) {
71-
try {
72-
await assertValidProof(proofOrProofs, allowAiWitness)
73-
return true
74-
} catch (error) {
75-
logger.error('error in validating proof', error)
76-
return false
77-
}
78-
}
88+
proofOrProofs: Proof | Proof[],
89+
config: VerificationConfig
90+
): Promise<boolean> {
91+
try {
92+
const proofs = Array.isArray(proofOrProofs) ? proofOrProofs : [proofOrProofs];
93+
94+
if (proofs.length === 0) {
95+
throw new ProofNotValidatedError('No proofs provided');
96+
}
7997

80-
export async function assertValidProof(
81-
proofOrProofs: Proof | Proof[],
82-
allowAiWitness = false
83-
) {
84-
const attestors = await getAttestors()
85-
proofOrProofs = Array.isArray(proofOrProofs) ? proofOrProofs : [proofOrProofs]
86-
for (const proof of proofOrProofs) {
87-
const signers = recoverSignersOfSignedClaim({
88-
claim: proof.claimData,
89-
signatures: proof.signatures
90-
.map(signature => ethers.getBytes(signature))
91-
})
92-
// ensure at least one signer is an attestor
93-
if (
94-
!attestors
95-
.some(attestor => signers.includes(attestor.id.toLowerCase()))
96-
) {
97-
throw new ProofNotVerifiedError('Identifier mismatch')
98-
}
99-
}
98+
if (!config) {
99+
throw new ProofNotValidatedError('Verification configuration is required for `verifyProof(proof, config)`');
100+
}
101+
102+
const attestors = await getAttestors()
103+
for (const proof of proofs) {
104+
await assertVerifiedProof(proof, attestors)
105+
}
106+
107+
await assertValidateProof(proofs, config);
108+
109+
return true;
110+
} catch (error) {
111+
logger.error('Error in validating proof:', error);
112+
return false;
113+
}
100114
}
101115

102116
/**
@@ -159,7 +173,7 @@ export class ReclaimProofRequest {
159173
private appCallbackUrl?: string;
160174
private sessionId: string;
161175
private options?: ProofRequestOptions;
162-
private context: Context = { contextAddress: '0x0', contextMessage: 'sample context' };
176+
private context: Context = { contextAddress: '0x0', contextMessage: 'sample context', reclaimSessionId: '' };
163177
private claimCreationType?: ClaimCreationType = ClaimCreationType.STANDALONE;
164178
private providerId: string;
165179
private resolvedProviderVersion?: string;
@@ -337,6 +351,7 @@ export class ReclaimProofRequest {
337351
const data: InitSessionResponse = await initSession(providerId, applicationId, proofRequestInstance.timeStamp, signature, options?.providerVersion);
338352
proofRequestInstance.sessionId = data.sessionId
339353
proofRequestInstance.resolvedProviderVersion = data.resolvedProviderVersion
354+
proofRequestInstance.context.reclaimSessionId = data.sessionId
340355

341356
return proofRequestInstance
342357
} catch (error) {
@@ -699,7 +714,7 @@ export class ReclaimProofRequest {
699714
{ input: context, paramName: 'context', isString: false }
700715
], 'setJsonContext');
701716
// ensure context is canonically json serializable
702-
this.context = JSON.parse(canonicalStringify(context));
717+
this.context = JSON.parse(canonicalStringify({ ...context, reclaimSessionId: this.sessionId }));
703718
} catch (error) {
704719
logger.info("Error setting context", error)
705720
throw new SetContextError("Error setting context", error as Error)
@@ -731,7 +746,7 @@ export class ReclaimProofRequest {
731746
{ input: address, paramName: 'address', isString: true },
732747
{ input: message, paramName: 'message', isString: true }
733748
], 'setContext');
734-
this.context = { contextAddress: address, contextMessage: message };
749+
this.context = { contextAddress: address, contextMessage: message, reclaimSessionId: this.sessionId };
735750
} catch (error) {
736751
logger.info("Error setting context", error)
737752
throw new SetContextError("Error setting context", error as Error)
@@ -1336,6 +1351,39 @@ export class ReclaimProofRequest {
13361351
}
13371352
}
13381353

1354+
/**
1355+
* Returns the provider id and exact version of the provider that was used in the verification session of this request.
1356+
*
1357+
* This can be provided as a config parameter to the `verifyProof` function to verify the proof.
1358+
*
1359+
* See also:
1360+
* * `verifyProof()` - Verifies a proof against the expected provider configuration.
1361+
* * `getProviderHashRequirements()` - An alternative of this function to get the expected hashes for a provider version by providing providerId and exactProviderVersionString. The result can be provided in verifyProof function's `config` parameter for proof validation.
1362+
* * `getProviderHashRequirementsFromSpec()` - An alternative of this function to get the expected hashes from a provider spec. The result can be provided in verifyProof function's `config` parameter for proof validation.
1363+
*/
1364+
public getProviderVersion(): ProviderVersionInfo {
1365+
// This should be exact version and not a version constraint/expression. This cannot be blank.
1366+
const exactProviderVersionString = this.resolvedProviderVersion ?? '';
1367+
return {
1368+
providerId: this.providerId,
1369+
providerVersion: exactProviderVersionString,
1370+
}
1371+
}
1372+
1373+
/**
1374+
* Fetches the provider config that was used for this session and returns the hash requirements
1375+
*
1376+
* See also:
1377+
* * `verifyProof()` - Verifies a proof against the expected provider configuration.
1378+
* * `fetchProviderHashRequirementsBy()` - An alternative of this function to get the expected hashes for a provider version by providing providerId and exactProviderVersionString. The result can be provided in verifyProof function's `config` parameter for proof validation.
1379+
* * `getProviderHashRequirementsFromSpec()` - An alternative of this function to get the expected hashes from a provider spec. The result can be provided in verifyProof function's `config` parameter for proof validation.
1380+
*
1381+
* @returns A promise that resolves to a ProviderHashRequirementsConfig
1382+
*/
1383+
getProviderHashRequirements(proofs: Proof[]): Promise<ProviderHashRequirementsConfig> {
1384+
return fetchProviderHashRequirementsBy(this.providerId, this.resolvedProviderVersion ?? '', proofs);
1385+
}
1386+
13391387
/**
13401388
* Starts the proof request session and monitors for proof submission
13411389
*
@@ -1418,7 +1466,7 @@ export class ReclaimProofRequest {
14181466
if (statusUrlResponse.session.proofs && statusUrlResponse.session.proofs.length > 0) {
14191467
const proofs = statusUrlResponse.session.proofs;
14201468
if (this.claimCreationType === ClaimCreationType.STANDALONE) {
1421-
const verified = await verifyProof(proofs, this.options?.acceptAiProviders);
1469+
const verified = await verifyProof(proofs, this.getProviderVersion());
14221470
if (!verified) {
14231471
logger.info(`Proofs not verified: count=${proofs?.length}`);
14241472
throw new ProofNotVerifiedError();

src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
export * from './Reclaim';
22
export type * from './utils/interfaces';
33
export type * from './utils/types';
4+
export * from './utils/proofUtils';
5+
export type * from './utils/proofUtils';
6+
export * from './utils/proofValidationUtils';
7+
export type * from './utils/proofValidationUtils';
8+
export * from './utils/providerUtils';
9+
export type * from './utils/providerUtils';
10+
export * from './utils/sessionUtils';
11+
export type * from './utils/sessionUtils';
412
// Export device detection utilities for debugging (optional)
513
export {
614
getDeviceType,

src/utils/__tests__/mock-fetch.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,13 @@ export function mockFetch(data: any) {
55
json: () => data,
66
}),
77
);
8+
}
9+
10+
export function mockFetchBy(cb: (fetchUrl: string) => any) {
11+
return jest.fn().mockImplementation((fetchUrl: string) => {
12+
return Promise.resolve({
13+
ok: true,
14+
json: () => cb(fetchUrl),
15+
});
16+
});
817
}

0 commit comments

Comments
 (0)