@reclaimprotocol/client 0.1.29 → 0.1.32

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.
Files changed (110) hide show
  1. package/README.md +255 -234
  2. package/lib/api.d.ts +2 -0
  3. package/lib/api.js +2 -0
  4. package/lib/client.js +2 -0
  5. package/lib/enckeys.d.ts +1 -1
  6. package/lib/enckeys.js +2 -2
  7. package/lib/encoding.d.ts +4 -0
  8. package/lib/encoding.js +10 -0
  9. package/lib/feature-flags.d.ts +9 -0
  10. package/lib/feature-flags.js +19 -0
  11. package/lib/index.d.ts +4 -4
  12. package/lib/index.js +4 -4
  13. package/lib/jwks.d.ts +8 -8
  14. package/lib/jwks.js +38 -17
  15. package/lib/launch/destination.d.ts +14 -0
  16. package/lib/launch/destination.js +89 -0
  17. package/lib/launch/device.d.ts +30 -0
  18. package/lib/launch/device.js +168 -0
  19. package/lib/launch/environment.d.ts +224 -0
  20. package/lib/launch/environment.js +98 -0
  21. package/lib/launch/errors.d.ts +20 -0
  22. package/lib/launch/errors.js +25 -0
  23. package/lib/launch/extension.d.ts +42 -0
  24. package/lib/launch/extension.js +72 -0
  25. package/lib/launch/flow.d.ts +82 -0
  26. package/lib/launch/flow.js +222 -0
  27. package/lib/launch/index.d.ts +16 -0
  28. package/lib/launch/index.js +12 -0
  29. package/lib/launch/mobile.d.ts +77 -0
  30. package/lib/launch/mobile.js +124 -0
  31. package/lib/launch/modal.d.ts +50 -0
  32. package/lib/launch/modal.js +258 -0
  33. package/lib/launch/options.d.ts +74 -0
  34. package/lib/launch/options.js +20 -0
  35. package/lib/launch/portal.d.ts +28 -0
  36. package/lib/launch/portal.js +68 -0
  37. package/lib/launch/qr.d.ts +5 -0
  38. package/lib/launch/qr.js +28 -0
  39. package/lib/launch/target.d.ts +46 -0
  40. package/lib/launch/target.js +128 -0
  41. package/lib/launch/vendor/qrcodegen.d.ts +134 -0
  42. package/lib/launch/vendor/qrcodegen.js +822 -0
  43. package/lib/legacy-proof.d.ts +2 -0
  44. package/lib/legacy-proof.js +37 -7
  45. package/lib/proof-hash.d.ts +5 -5
  46. package/lib/proof-hash.js +1 -1
  47. package/lib/proof-recipe.d.ts +3 -3
  48. package/lib/proof-recipe.js +1 -1
  49. package/lib/proof-types.d.ts +35 -8
  50. package/lib/reclaim.d.ts +81 -70
  51. package/lib/reclaim.js +79 -130
  52. package/lib/request-proof.d.ts +60 -0
  53. package/lib/request-proof.js +241 -0
  54. package/lib/result.d.ts +46 -38
  55. package/lib/result.js +160 -100
  56. package/lib/routes.gen.d.ts +76 -28
  57. package/lib/routes.gen.js +24 -11
  58. package/lib/tee-binding.d.ts +1 -1
  59. package/lib/types/openapi.d.ts +1 -0
  60. package/lib/types/openapi.gen.d.ts +1598 -485
  61. package/lib/types/openapi.gen.js +5 -0
  62. package/lib/verification-clients.d.ts +28 -14
  63. package/lib/verification-clients.js +31 -11
  64. package/lib/verify-attestor-tee.d.ts +19 -2
  65. package/lib/verify-attestor-tee.js +40 -4
  66. package/lib/verify-tee.d.ts +10 -1
  67. package/lib/verify-tee.js +34 -10
  68. package/package.json +5 -1
  69. package/src/api.ts +9 -0
  70. package/src/client.ts +2 -0
  71. package/src/enckeys.ts +2 -2
  72. package/src/encoding.ts +12 -0
  73. package/src/feature-flags.ts +39 -0
  74. package/src/index.ts +13 -14
  75. package/src/jwks.ts +52 -30
  76. package/src/launch/destination.ts +111 -0
  77. package/src/launch/device.ts +242 -0
  78. package/src/launch/environment.ts +337 -0
  79. package/src/launch/errors.ts +49 -0
  80. package/src/launch/extension.ts +124 -0
  81. package/src/launch/flow.ts +369 -0
  82. package/src/launch/index.ts +42 -0
  83. package/src/launch/mobile.ts +184 -0
  84. package/src/launch/modal.ts +378 -0
  85. package/src/launch/options.ts +99 -0
  86. package/src/launch/portal.ts +104 -0
  87. package/src/launch/qr.ts +37 -0
  88. package/src/launch/target.ts +150 -0
  89. package/src/launch/vendor/qrcodegen.ts +1034 -0
  90. package/src/proof-hash.ts +6 -6
  91. package/src/proof-recipe.ts +3 -3
  92. package/src/proof-types.ts +36 -8
  93. package/src/reclaim.ts +185 -218
  94. package/src/{legacy-proof.ts → request-proof.ts} +81 -36
  95. package/src/result.ts +238 -172
  96. package/src/routes.gen.ts +24 -11
  97. package/src/tee-binding.ts +1 -1
  98. package/src/types/openapi.gen.ts +1603 -485
  99. package/src/types/openapi.ts +3 -1
  100. package/src/verification-clients.ts +33 -19
  101. package/src/verify-attestor-tee.ts +66 -8
  102. package/src/verify-tee.ts +44 -13
  103. package/lib/api-client.d.ts +0 -31
  104. package/lib/api-client.js +0 -112
  105. package/lib/api-consts.d.ts +0 -4
  106. package/lib/api-consts.js +0 -4
  107. package/lib/api-errors.d.ts +0 -13
  108. package/lib/api-errors.js +0 -30
  109. package/lib/eip712.d.ts +0 -20
  110. package/lib/eip712.js +0 -108
package/lib/api.d.ts CHANGED
@@ -2,6 +2,8 @@ export { ReclaimClient } from './client.ts';
2
2
  export type { ReclaimClientOptions } from './client.ts';
3
3
  export { isProblemDetails, isProblemError, readProblemDetails, throwProblemError, } from './errors.ts';
4
4
  export type { ProblemError } from './errors.ts';
5
+ export { getBooleanFeatureFlag, getFeatureFlag, getNumberFeatureFlag, getStringFeatureFlag, } from './feature-flags.ts';
5
6
  export { DEFAULT_BASE_URL, PROBLEM_CONTENT_TYPE, } from './consts.ts';
7
+ export { VerificationClient, } from './verification-clients.ts';
6
8
  export type * from './types/openapi.ts';
7
9
  export type * from './types/openapi.gen.ts';
package/lib/api.js CHANGED
@@ -1,3 +1,5 @@
1
1
  export { ReclaimClient } from "./client.js";
2
2
  export { isProblemDetails, isProblemError, readProblemDetails, throwProblemError, } from "./errors.js";
3
+ export { getBooleanFeatureFlag, getFeatureFlag, getNumberFeatureFlag, getStringFeatureFlag, } from "./feature-flags.js";
3
4
  export { DEFAULT_BASE_URL, PROBLEM_CONTENT_TYPE, } from "./consts.js";
5
+ export { VerificationClient, } from "./verification-clients.js";
package/lib/client.js CHANGED
@@ -91,6 +91,8 @@ export class ReclaimClient {
91
91
  }
92
92
  const empty = res.status === 204
93
93
  || res.headers.get('content-length') === '0';
94
+ // Fetch exposes parsed JSON as `any`; OpenAPI owns the operation-specific
95
+ // response type, and callers can inspect the original Response if needed.
94
96
  const data = (empty ? undefined : await res.json());
95
97
  return { data, response: res };
96
98
  }
package/lib/enckeys.d.ts CHANGED
@@ -11,7 +11,7 @@ export type DecryptionKey = Uint8Array;
11
11
  export declare function loadDecryptionKey(input: DecryptionKeyInput): DecryptionKey;
12
12
  /**
13
13
  * Convert an uncompressed secp256k1 public key (`0x04` + 128 hex) to the public
14
- * JWK the API stores and the Verification Client encrypts to — so callers only
14
+ * JWK the API stores and Builder encrypts to — so callers only
15
15
  * ever handle eth-native hex, never a JWK. Mirrors the `x`/`y` coordinate split
16
16
  * in `jwksFromPrivateKey` (./jwks.ts).
17
17
  */
package/lib/enckeys.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * secp256k1 (Ethereum) encryption-key helpers for ECIES result delivery.
3
3
  *
4
4
  * The org's encryption key is its **Ethereum public key** (secp256k1). The
5
- * Verification Client ECIES-encrypts the signed JWS to it (see ./result.ts) and
5
+ * Builder ECIES-encrypts the signed JWS to it (see ./result.ts) and
6
6
  * the Consumer decrypts with the matching eth **private key**. Eth-native `0x`
7
7
  * hex is used everywhere at the edges (the format MetaMask/ethers/wallets
8
8
  * emit); the wire/DB store the public key as a secp256k1 JWK
@@ -32,7 +32,7 @@ export function loadDecryptionKey(input) {
32
32
  }
33
33
  /**
34
34
  * Convert an uncompressed secp256k1 public key (`0x04` + 128 hex) to the public
35
- * JWK the API stores and the Verification Client encrypts to — so callers only
35
+ * JWK the API stores and Builder encrypts to — so callers only
36
36
  * ever handle eth-native hex, never a JWK. Mirrors the `x`/`y` coordinate split
37
37
  * in `jwksFromPrivateKey` (./jwks.ts).
38
38
  */
package/lib/encoding.d.ts CHANGED
@@ -14,3 +14,7 @@ export declare function bytesToHex(bytes: Uint8Array): string;
14
14
  export declare function base64urlToBytes(s: string): Uint8Array;
15
15
  /** Encode bytes (or a UTF-8 string) as unpadded base64url. */
16
16
  export declare function b64url(input: Uint8Array | string): string;
17
+ /** `value` without trailing `/`s. A loop, not `/\/+$/`: that regex backtracks
18
+ * quadratically on a long run of slashes followed by anything else, and
19
+ * some callers get attacker-supplied input (a JWS header's `iss`). */
20
+ export declare function stripTrailingSlashes(value: string): string;
package/lib/encoding.js CHANGED
@@ -48,3 +48,13 @@ export function b64url(input) {
48
48
  .replace(/\//g, '_')
49
49
  .replace(/=+$/, '');
50
50
  }
51
+ /** `value` without trailing `/`s. A loop, not `/\/+$/`: that regex backtracks
52
+ * quadratically on a long run of slashes followed by anything else, and
53
+ * some callers get attacker-supplied input (a JWS header's `iss`). */
54
+ export function stripTrailingSlashes(value) {
55
+ let end = value.length;
56
+ while (end > 0 && value.charCodeAt(end - 1) === 0x2f) {
57
+ end--;
58
+ }
59
+ return value.slice(0, end);
60
+ }
@@ -0,0 +1,9 @@
1
+ import type { VerificationRuntimeConfig } from './types/openapi.ts';
2
+ /** Read a resolved session flag without assuming its runtime type. */
3
+ export declare function getFeatureFlag(config: VerificationRuntimeConfig | null | undefined, key: string): unknown;
4
+ /** Read a boolean flag, returning `fallback` for absent or mistyped values. */
5
+ export declare function getBooleanFeatureFlag(config: VerificationRuntimeConfig | null | undefined, key: string, fallback?: boolean): boolean;
6
+ /** Read a string flag, returning `fallback` for absent or mistyped values. */
7
+ export declare function getStringFeatureFlag(config: VerificationRuntimeConfig | null | undefined, key: string, fallback: string): string;
8
+ /** Read a numeric flag, returning `fallback` for absent or mistyped values. */
9
+ export declare function getNumberFeatureFlag(config: VerificationRuntimeConfig | null | undefined, key: string, fallback: number): number;
@@ -0,0 +1,19 @@
1
+ /** Read a resolved session flag without assuming its runtime type. */
2
+ export function getFeatureFlag(config, key) {
3
+ return config?.values[key];
4
+ }
5
+ /** Read a boolean flag, returning `fallback` for absent or mistyped values. */
6
+ export function getBooleanFeatureFlag(config, key, fallback = false) {
7
+ const value = getFeatureFlag(config, key);
8
+ return typeof value === 'boolean' ? value : fallback;
9
+ }
10
+ /** Read a string flag, returning `fallback` for absent or mistyped values. */
11
+ export function getStringFeatureFlag(config, key, fallback) {
12
+ const value = getFeatureFlag(config, key);
13
+ return typeof value === 'string' ? value : fallback;
14
+ }
15
+ /** Read a numeric flag, returning `fallback` for absent or mistyped values. */
16
+ export function getNumberFeatureFlag(config, key, fallback) {
17
+ const value = getFeatureFlag(config, key);
18
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
19
+ }
package/lib/index.d.ts CHANGED
@@ -7,11 +7,11 @@ export { addressFromUncompressedPubkey, secp, } from './secp.ts';
7
7
  export { eip191Digest, signEip191 } from './eip191.ts';
8
8
  export { type CallbackBody, decodeJwsPayload, decryptCallback, type DeliveryEnvelope, encryptForEthPublicKey, type Jws, ResultVerificationError, type ResultProofOutcome, signResultJws, verifyJwsSignature, verifyResult, verifyResultFull, type VerifyResultFullOptions, type VerifyResultFullOutcome, type VerifyResultOptions, type VerifyResultOutcome, } from './result.ts';
9
9
  export { type DecryptionKey, type DecryptionKeyInput, generateEthKeypair, type GeneratedEthKeypair, loadDecryptionKey, pubkeyJwkToEthAddress, uncompressedPubkeyToJwk, } from './enckeys.ts';
10
- export { createReclaim, type CreateSessionInput, type CreateSessionTeeAttestation, type Reclaim, type ReclaimConfig, type ReceiveOptions, type ReceivedDelivery, type RegisterCallbackInput, type SetKeypairInput, TERMINAL_EVENTS, } from './reclaim.ts';
11
- export { fetchJwks, fetchJwksUrl, fetchReclaimJwks, type Jwk, jwksFromPrivateKey, jwkToUncompressedPublicKey, type ReclaimJwks, selectKey, selectVerificationResultKey, trustedAttestorAddresses, validateAttestorAddresses, } from './jwks.ts';
10
+ export { type BindSessionTeeNonceInput, type CreateSessionInput, type ImportReclaimVerificationConfig, type ReclaimConfig, type ReclaimFlowFactory, ReclaimVerification, type ReclaimVerificationState, type ReceiveOptions, type ReceivedDelivery, type SessionFlow, } from './reclaim.ts';
11
+ export { fetchJwks, fetchJwksUrl, fetchReclaimJwks, type Jwk, jwksFromPrivateKey, jwkToUncompressedPublicKey, type ReclaimJwks, selectKey, selectVerificationResultKey, trustedAttestorAddresses, validateAttestorAddresses, validateAttestorImageDigests, } from './jwks.ts';
12
12
  export { type NestedProofOutcome, verifyNestedProof, } from './attestor-proof.ts';
13
- export { type LegacyProof, type LegacyProofOutcome, type LegacyVerifyProofOptions, type ProofHashRequirement, proofHashes, verifyProof, } from './legacy-proof.ts';
13
+ export { type ProofHashRequirement, type ReclaimRequestProof, type ReclaimRequestProofOutcome, type VerifyRequestProofOptions, proofHashes, verifyRequestProof, } from './request-proof.ts';
14
14
  export { type BuilderProofRecipe, proofHashesFromBuilderRecipe, } from './proof-recipe.ts';
15
15
  export { getHashFromProof, hashProofClaimParams, hashRequestSpec, type HttpProofParams, type ProofRequestSpec, } from './proof-hash.ts';
16
16
  export { builderTeeBindingPayload, BUILDER_TEE_BINDING_DOMAIN, createBuilderTeeBinding, createLegacyTeeNonceContext, deriveLegacySignatureTeeNonce, deriveLegacyTeeNonce, LEGACY_TEE_ATTESTATION_VERSION, LEGACY_TEE_NONCE_DOMAIN, legacyApplicationIdFromPrivateKey, signBuilderTeeBinding, type BuilderTeeBinding, type BuilderTeeBindingPayload, type LegacyTeeNonceContext, type LegacyTeeNonceData, type LegacyTeeNonceMode, } from './tee-binding.ts';
17
- export { KNOWN_VERIFICATION_CLIENTS, type KnownVerificationClientName, type KnownVerificationClientUrlMap, } from './verification-clients.ts';
17
+ export { VerificationClient, } from './verification-clients.ts';
package/lib/index.js CHANGED
@@ -7,11 +7,11 @@ export { addressFromUncompressedPubkey, secp, } from "./secp.js";
7
7
  export { eip191Digest, signEip191 } from "./eip191.js";
8
8
  export { decodeJwsPayload, decryptCallback, encryptForEthPublicKey, ResultVerificationError, signResultJws, verifyJwsSignature, verifyResult, verifyResultFull, } from "./result.js";
9
9
  export { generateEthKeypair, loadDecryptionKey, pubkeyJwkToEthAddress, uncompressedPubkeyToJwk, } from "./enckeys.js";
10
- export { createReclaim, TERMINAL_EVENTS, } from "./reclaim.js";
11
- export { fetchJwks, fetchJwksUrl, fetchReclaimJwks, jwksFromPrivateKey, jwkToUncompressedPublicKey, selectKey, selectVerificationResultKey, trustedAttestorAddresses, validateAttestorAddresses, } from "./jwks.js";
10
+ export { ReclaimVerification, } from "./reclaim.js";
11
+ export { fetchJwks, fetchJwksUrl, fetchReclaimJwks, jwksFromPrivateKey, jwkToUncompressedPublicKey, selectKey, selectVerificationResultKey, trustedAttestorAddresses, validateAttestorAddresses, validateAttestorImageDigests, } from "./jwks.js";
12
12
  export { verifyNestedProof, } from "./attestor-proof.js";
13
- export { proofHashes, verifyProof, } from "./legacy-proof.js";
13
+ export { proofHashes, verifyRequestProof, } from "./request-proof.js";
14
14
  export { proofHashesFromBuilderRecipe, } from "./proof-recipe.js";
15
15
  export { getHashFromProof, hashProofClaimParams, hashRequestSpec, } from "./proof-hash.js";
16
16
  export { builderTeeBindingPayload, BUILDER_TEE_BINDING_DOMAIN, createBuilderTeeBinding, createLegacyTeeNonceContext, deriveLegacySignatureTeeNonce, deriveLegacyTeeNonce, LEGACY_TEE_ATTESTATION_VERSION, LEGACY_TEE_NONCE_DOMAIN, legacyApplicationIdFromPrivateKey, signBuilderTeeBinding, } from "./tee-binding.js";
17
- export { KNOWN_VERIFICATION_CLIENTS, } from "./verification-clients.js";
17
+ export { VerificationClient, } from "./verification-clients.js";
package/lib/jwks.d.ts CHANGED
@@ -9,10 +9,6 @@ export interface Jwk {
9
9
  key_ops?: string[];
10
10
  /** Restricts how Builder consumers may use this key. */
11
11
  'x-reclaim-purpose'?: 'verification-result' | 'attestor-proof';
12
- /** Verification Client that owns a result-signing key. */
13
- 'x-reclaim-verification-client-id'?: string;
14
- /** Verification Clients that share a built-in result-signing key. */
15
- 'x-reclaim-verification-client-ids'?: string[];
16
12
  /** Attestor proofs use EIP-191 rather than JOSE ES256K. */
17
13
  'x-reclaim-signature-scheme'?: 'EIP-191';
18
14
  }
@@ -20,13 +16,15 @@ export interface ReclaimJwks {
20
16
  keys: Jwk[];
21
17
  /** Trusted EIP-191 attestor signer addresses (public-key hashes). */
22
18
  'x-reclaim-attestors'?: string[];
19
+ /** Trusted sha256 digests for attestor Confidential Space images. */
20
+ 'x-reclaim-attestor-image-digests'?: string[];
23
21
  }
24
22
  /**
25
- * Build the JWK set for an ES256K signing key (the Verification Client's
23
+ * Build the JWK set for an ES256K signing key (Builder's managed
26
24
  * result-signing key). `kid` is the key's ETH address; `x`/`y` are the
27
25
  * uncompressed public-key coordinates. Empty set when no key is configured.
28
26
  */
29
- export declare function jwksFromPrivateKey(privateKeyHex?: string, metadata?: Pick<Jwk, 'x-reclaim-purpose' | 'x-reclaim-verification-client-id' | 'x-reclaim-verification-client-ids'>): {
27
+ export declare function jwksFromPrivateKey(privateKeyHex?: string, metadata?: Pick<Jwk, 'x-reclaim-purpose'>): {
30
28
  keys: Jwk[];
31
29
  };
32
30
  /** Fetch the JWK set published at `<iss>/.well-known/jwks.json`. */
@@ -37,12 +35,14 @@ export declare function fetchJwksUrl(url: string, fetchImpl?: typeof fetch): Pro
37
35
  export declare function fetchReclaimJwks(url: string, fetchImpl?: typeof fetch): Promise<ReclaimJwks>;
38
36
  /** Find the key matching `kid` (case-insensitive — kids are ETH addresses). */
39
37
  export declare function selectKey(keys: Jwk[], kid: string): Jwk | undefined;
40
- /** Select a result key and enforce its Verification Client ownership. */
41
- export declare function selectVerificationResultKey(keys: Jwk[], kid: string, verificationClientId: string): Jwk | undefined;
38
+ /** Select Builder's result-signing key and validate its JOSE metadata. */
39
+ export declare function selectVerificationResultKey(keys: Jwk[], kid: string): Jwk | undefined;
42
40
  /** Return ETH addresses from keys explicitly registered for attestor proofs. */
43
41
  export declare function trustedAttestorAddresses(keys: Jwk[]): string[];
44
42
  /** Validate and normalize the JWKS address-only trust extension. */
45
43
  export declare function validateAttestorAddresses(addresses: string[]): string[];
44
+ /** Validate and normalize sha256 image digests from a JWKS trust extension. */
45
+ export declare function validateAttestorImageDigests(digests: string[]): string[];
46
46
  /**
47
47
  * Reconstruct the uncompressed secp256k1 public key (`0x04` + X + Y) from an
48
48
  * EC JWK's base64url `x`/`y` coordinates, for signature verification.
package/lib/jwks.js CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * JWKS (RFC 7517) discovery for verifying result/proof signatures. Signers
3
- * (the Verification Client, the Attestor) publish their secp256k1 public keys
3
+ * (Builder, the Attestor) publish their secp256k1 public keys
4
4
  * at `<iss>/.well-known/jwks.json` with `kid` = the signer's ETH address.
5
5
  */
6
- import { b64url, base64urlToBytes, bytesToHex } from "./encoding.js";
6
+ import { b64url, base64urlToBytes, bytesToHex, stripTrailingSlashes, } from "./encoding.js";
7
7
  import { addressFromUncompressedPubkey, secp } from "./secp.js";
8
8
  /**
9
- * Build the JWK set for an ES256K signing key (the Verification Client's
9
+ * Build the JWK set for an ES256K signing key (Builder's managed
10
10
  * result-signing key). `kid` is the key's ETH address; `x`/`y` are the
11
11
  * uncompressed public-key coordinates. Empty set when no key is configured.
12
12
  */
@@ -34,7 +34,7 @@ export function jwksFromPrivateKey(privateKeyHex, metadata = {}) {
34
34
  }
35
35
  /** Fetch the JWK set published at `<iss>/.well-known/jwks.json`. */
36
36
  export async function fetchJwks(iss, fetchImpl = fetch) {
37
- const base = iss.replace(/\/+$/, '');
37
+ const base = stripTrailingSlashes(iss);
38
38
  return fetchJwksUrl(`${base}/.well-known/jwks.json`, fetchImpl);
39
39
  }
40
40
  /** Fetch a JWK set from an explicitly trusted registry URL. */
@@ -44,6 +44,7 @@ export async function fetchJwksUrl(url, fetchImpl = fetch) {
44
44
  }
45
45
  /** Fetch Builder's JWKS plus its namespaced attestor trust registry. */
46
46
  export async function fetchReclaimJwks(url, fetchImpl = fetch) {
47
+ assertSecureJwksUrl(url);
47
48
  const res = await fetchImpl(url);
48
49
  if (!res.ok) {
49
50
  throw new Error(`JWKS fetch failed for ${url}: HTTP ${res.status}`);
@@ -52,6 +53,7 @@ export async function fetchReclaimJwks(url, fetchImpl = fetch) {
52
53
  return {
53
54
  keys: body.keys ?? [],
54
55
  'x-reclaim-attestors': validateAttestorAddresses(body['x-reclaim-attestors'] ?? []),
56
+ 'x-reclaim-attestor-image-digests': validateAttestorImageDigests(body['x-reclaim-attestor-image-digests'] ?? []),
55
57
  };
56
58
  }
57
59
  /** Find the key matching `kid` (case-insensitive — kids are ETH addresses). */
@@ -59,11 +61,16 @@ export function selectKey(keys, kid) {
59
61
  const want = kid.toLowerCase();
60
62
  return keys.find((k) => (k.kid ?? '').toLowerCase() === want);
61
63
  }
62
- /** Select a result key and enforce its Verification Client ownership. */
63
- export function selectVerificationResultKey(keys, kid, verificationClientId) {
64
+ /** Select Builder's result-signing key and validate its JOSE metadata. */
65
+ export function selectVerificationResultKey(keys, kid) {
64
66
  return keys.find((key) => ((key.kid ?? '').toLowerCase() === kid.toLowerCase()
67
+ && key.kty === 'EC'
68
+ && key.crv === 'secp256k1'
69
+ && (!key.alg || key.alg === 'ES256K')
70
+ && (!key.use || key.use === 'sig')
71
+ && (!key.key_ops || key.key_ops.includes('verify'))
65
72
  && key['x-reclaim-purpose'] !== 'attestor-proof'
66
- && belongsToVerificationClient(key, verificationClientId)));
73
+ && jwkMatchesKid(key)));
67
74
  }
68
75
  /** Return ETH addresses from keys explicitly registered for attestor proofs. */
69
76
  export function trustedAttestorAddresses(keys) {
@@ -94,6 +101,16 @@ export function validateAttestorAddresses(addresses) {
94
101
  return normalized;
95
102
  }))];
96
103
  }
104
+ /** Validate and normalize sha256 image digests from a JWKS trust extension. */
105
+ export function validateAttestorImageDigests(digests) {
106
+ return [...new Set(digests.map((digest) => {
107
+ const normalized = digest.trim().toLowerCase();
108
+ if (!/^sha256:[0-9a-f]{64}$/.test(normalized)) {
109
+ throw new Error('invalid attestor image digest in JWKS trust registry');
110
+ }
111
+ return normalized;
112
+ }))];
113
+ }
97
114
  /**
98
115
  * Reconstruct the uncompressed secp256k1 public key (`0x04` + X + Y) from an
99
116
  * EC JWK's base64url `x`/`y` coordinates, for signature verification.
@@ -110,18 +127,22 @@ export function jwkToUncompressedPublicKey(jwk) {
110
127
  out.set(y, 33);
111
128
  return '0x' + bytesToHex(out);
112
129
  }
113
- function belongsToVerificationClient(key, verificationClientId) {
114
- const one = key['x-reclaim-verification-client-id'];
115
- const many = key['x-reclaim-verification-client-ids'];
116
- if (one) {
117
- return one === verificationClientId;
130
+ function jwkMatchesKid(key) {
131
+ try {
132
+ const address = addressFromUncompressedPubkey(hexPublicKeyBytes(jwkToUncompressedPublicKey(key)));
133
+ return address.toLowerCase() === key.kid?.toLowerCase();
134
+ }
135
+ catch {
136
+ return false;
118
137
  }
119
- if (many) {
120
- return many.includes(verificationClientId);
138
+ }
139
+ function assertSecureJwksUrl(value) {
140
+ const url = new URL(value);
141
+ if (url.protocol === 'https:' || (url.protocol === 'http:'
142
+ && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))) {
143
+ return;
121
144
  }
122
- // Untagged self-hosted JWKS remain compatible because their URL is itself
123
- // selected from the registered Verification Client record.
124
- return key['x-reclaim-purpose'] !== 'verification-result';
145
+ throw new Error('JWKS URL must use HTTPS except on loopback');
125
146
  }
126
147
  function hexPublicKeyBytes(value) {
127
148
  return Uint8Array.from(Buffer.from(value.slice(2), 'hex'));
@@ -0,0 +1,14 @@
1
+ import type { ParsedFlowUrl, VerificationTarget } from './target.ts';
2
+ export interface DestinationOptions {
3
+ portalUrl?: string;
4
+ appLinkBaseUrl?: string;
5
+ }
6
+ export declare const DEFAULT_PORTAL_URL = "https://portal.reclaimprotocol.org";
7
+ /** The legacy share page. No longer a default — `verifier-app` sessions now
8
+ * carry a real Builder share URL — but kept as the value a deployment still
9
+ * pointing at `share.reclaimprotocol.org` would pass as `appLinkBaseUrl`. */
10
+ export declare const LEGACY_APP_LINK_BASE_URL = "https://share.reclaimprotocol.org/verify";
11
+ /** Copies `api`/`sessionId`/`diag` onto `base`, preserving any query
12
+ * parameters `base` already carries (e.g. an App Clip's `?p=`). */
13
+ export declare function appendSessionQuery(base: string, parsed: Pick<ParsedFlowUrl, 'sessionId' | 'diag'>): string;
14
+ export declare function resolveDestinationUrl(target: VerificationTarget | undefined, parsed: ParsedFlowUrl, options: DestinationOptions): string;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Rebuilds the real, openable destination URL for a parsed flow.
3
+ *
4
+ * Since migration `20260917130000000.share-routes-uris.sql`, every built-in
5
+ * client's registered uri is itself a real, openable Builder page (`/share/
6
+ * {name}` — see `builtin-clients.ts` and `SharePage.tsx`), including
7
+ * `portals`, which used to be an attribution-only label Builder served
8
+ * nothing at. That does NOT mean this file's job shrinks to "pass every
9
+ * target through unchanged": which side rewrites is decided per target by
10
+ * who is able to honour an override and who has to keep working against a
11
+ * pre-migration deployment, not by whether Builder happens to have a page
12
+ * there today.
13
+ *
14
+ * - `builder`: registered uri (`/v/{sessionId}` pre-migration, `/share/
15
+ * builder` post-migration) is always real and there is no override to
16
+ * express — Builder's own route owns any redirecting (`/share/builder`
17
+ * 302s to `/v/{sessionId}` server-side). This file never rewrites it.
18
+ * - `portals`: this file keeps rewriting to `options.portalUrl ??
19
+ * DEFAULT_PORTAL_URL`, unconditionally, even though `/share/portals` now
20
+ * redirects there too. Two reasons it can't defer to Builder's hop: (1) a
21
+ * pre-migration `/portals` uri is still a bare label Builder serves
22
+ * nothing at, so passing it through would 404; (2) `options.portalUrl` is
23
+ * an override Builder's fixed server-side redirect has no way to honour.
24
+ * `DEFAULT_PORTAL_URL` here and `SharePage.tsx`'s redirect default are
25
+ * deliberately kept identical, so the common (no-override) case reaches
26
+ * the same destination either hop — Builder's `/share/portals` hop is
27
+ * what makes a *pasted* link (opened without this SDK) still work.
28
+ * - `verifier-app`: registered uri (`/s` or `/share/verifier-app`, both
29
+ * real) is the share link itself, so this file passes it through unless
30
+ * `options.appLinkBaseUrl` overrides it — same shape of reasoning as
31
+ * `portals`, just already correct before this migration.
32
+ * - `reclaim-browser-extension`: has no destination URL at all — the
33
+ * mechanism is a `postMessage` handoff, not a navigation (see
34
+ * ./extension.ts) — so `getUrl()` on that target, like an unrecognised
35
+ * path, returns the original `verificationUrl` unchanged regardless of
36
+ * which shape registered it.
37
+ */
38
+ import { stripTrailingSlashes } from "../encoding.js";
39
+ export const DEFAULT_PORTAL_URL = 'https://portal.reclaimprotocol.org';
40
+ /** The legacy share page. No longer a default — `verifier-app` sessions now
41
+ * carry a real Builder share URL — but kept as the value a deployment still
42
+ * pointing at `share.reclaimprotocol.org` would pass as `appLinkBaseUrl`. */
43
+ export const LEGACY_APP_LINK_BASE_URL = 'https://share.reclaimprotocol.org/verify';
44
+ /** Copies `api`/`sessionId`/`diag` onto `base`, preserving any query
45
+ * parameters `base` already carries (e.g. an App Clip's `?p=`). */
46
+ export function appendSessionQuery(base, parsed) {
47
+ const url = new URL(base);
48
+ url.searchParams.set('api', '2');
49
+ if (parsed.sessionId) {
50
+ url.searchParams.set('sessionId', parsed.sessionId);
51
+ }
52
+ if (parsed.diag) {
53
+ url.searchParams.set('diag', '1');
54
+ }
55
+ return url.toString();
56
+ }
57
+ function withoutTrailingSlash(value) {
58
+ return stripTrailingSlashes(value);
59
+ }
60
+ export function resolveDestinationUrl(target, parsed, options) {
61
+ switch (target) {
62
+ case 'builder':
63
+ return parsed.url.toString();
64
+ case 'portals': {
65
+ // Always rewritten here, even now that `/share/portals` also
66
+ // redirects server-side — see the file header for why this file,
67
+ // not Builder's route, has to own it.
68
+ const base = options.portalUrl ?? DEFAULT_PORTAL_URL;
69
+ return appendSessionQuery(`${withoutTrailingSlash(base)}/verify`, parsed);
70
+ }
71
+ case 'verifier-app': {
72
+ // The registered uri for `verifier-app` is Builder's own share page
73
+ // (`GET /s`), a real openable URL — so the verificationUrl already
74
+ // IS the share link, and rewriting it onto another origin would
75
+ // throw away the app-link association that makes a scan open the
76
+ // app (`build.reclaimprotocol.org` is what the app's entitlements
77
+ // and assetlinks now claim).
78
+ //
79
+ // `appLinkBaseUrl` stays as the override for a deployment that
80
+ // hosts its own share page, or one still pointing at the legacy
81
+ // `share.reclaimprotocol.org/verify`.
82
+ const base = options.appLinkBaseUrl;
83
+ return base ? appendSessionQuery(base, parsed) : parsed.url.toString();
84
+ }
85
+ case 'reclaim-browser-extension':
86
+ case undefined:
87
+ return parsed.url.toString();
88
+ }
89
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Device detection, ported from the reference JS SDK's `src/utils/device.ts`
3
+ * (`getDeviceType`/`getMobileDeviceType`). Two differences from that source,
4
+ * both required by the launch-API plan:
5
+ *
6
+ * - Every signal is read through a `LaunchEnvironment` (see ./environment.ts)
7
+ * instead of module-scope `navigator`/`window`, so this is testable with
8
+ * plain fakes and safe to import under SSR.
9
+ * - The result carries a `confidence` ('high' | 'low') instead of silently
10
+ * committing to a guess, so a caller can render a chooser instead of
11
+ * assuming. `navigator.userAgentData` (Chromium's low-entropy User-Agent
12
+ * Client Hints) is read as an extra signal where present — it is
13
+ * Chromium-only and absent on every iOS browser, so it augments the
14
+ * heuristics below rather than replacing them, and `getHighEntropyValues`
15
+ * is never called (it's async and equally absent on WebKit).
16
+ *
17
+ * The mobile-vs-desktop scoring mirrors the reference SDK's weights exactly
18
+ * (`CONFIDENCE_THRESHOLD = 3`); the iOS-vs-Android cascade mirrors its rule
19
+ * order, including the iPad-reporting-as-macOS special case.
20
+ */
21
+ import type { LaunchEnvironment } from './environment.ts';
22
+ export type DeviceKind = 'desktop' | 'ios' | 'android';
23
+ export type DetectionConfidence = 'high' | 'low';
24
+ export interface DeviceDetection {
25
+ device: DeviceKind;
26
+ confidence: DetectionConfidence;
27
+ }
28
+ /** Resolves the device this flow is running on, with a confidence a caller
29
+ * can act on instead of a silent guess. */
30
+ export declare function resolveDevice(env: LaunchEnvironment): DeviceDetection;
@@ -0,0 +1,168 @@
1
+ /** Matches the reference SDK's mobile/desktop scoring cutoff. */
2
+ const MOBILE_SCORE_THRESHOLD = 3;
3
+ /** A mobile/desktop score this close to the cutoff is a coin flip, not a
4
+ * confident read — surfaced as `confidence: 'low'`. */
5
+ const MOBILE_SCORE_MARGIN = 2;
6
+ function safeMatchMedia(win, query) {
7
+ try {
8
+ return win?.matchMedia?.(query)?.matches ?? false;
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ function safeCssSupports(win, property, value) {
15
+ try {
16
+ return win?.CSS?.supports?.(property, value) ?? false;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ function safeQuerySelector(env, selector) {
23
+ try {
24
+ return !!env.document?.querySelector(selector);
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ /** Ported scoring pass. Returns the raw mobile score — positive leans
31
+ * mobile, negative leans desktop. */
32
+ function scoreMobile(env) {
33
+ const win = env.window;
34
+ const nav = env.navigator;
35
+ const userAgent = (nav?.userAgent ?? '').toLowerCase();
36
+ if (!win || !nav) {
37
+ // SSR / no environment: the reference SDK assumes desktop.
38
+ return -MOBILE_SCORE_THRESHOLD;
39
+ }
40
+ let score = 0;
41
+ const screenWidth = win.innerWidth || win.screen?.width || 0;
42
+ const screenHeight = win.innerHeight || win.screen?.height || 0;
43
+ const hasSmallScreen = screenWidth <= 480 || screenHeight <= 480;
44
+ const hasLargeScreen = screenWidth > 1024 && screenHeight > 768;
45
+ const hasTouch = 'ontouchstart' in win || (nav.maxTouchPoints ?? 0) > 0;
46
+ const hasPreciseMouse = safeMatchMedia(win, '(pointer: fine)');
47
+ const canHover = safeMatchMedia(win, '(hover: hover)');
48
+ const hasMouseAndTouch = hasTouch && hasPreciseMouse;
49
+ const isWindowsTouchLaptop = /windows/i.test(userAgent)
50
+ && hasPreciseMouse && hasTouch;
51
+ if (hasTouch && !hasMouseAndTouch) {
52
+ score += 2;
53
+ }
54
+ else if (hasMouseAndTouch) {
55
+ score += 1;
56
+ }
57
+ if (hasSmallScreen && !isWindowsTouchLaptop) {
58
+ score += 2;
59
+ }
60
+ const mobileUaPattern = new RegExp('android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini|mobile', 'i');
61
+ const hasMobileUserAgent = mobileUaPattern.test(userAgent);
62
+ if (hasMobileUserAgent) {
63
+ score += 3;
64
+ }
65
+ const hasMobileApis = 'orientation' in win
66
+ || 'DeviceMotionEvent' in win
67
+ || 'DeviceOrientationEvent' in win;
68
+ const isDesktopSafari = /safari/i.test(userAgent)
69
+ && !/mobile/i.test(userAgent)
70
+ && /mac|intel/i.test(userAgent);
71
+ const mobileApisImplyMobile = hasMobileApis
72
+ && (hasSmallScreen || hasMobileUserAgent)
73
+ && !isDesktopSafari
74
+ && !isWindowsTouchLaptop;
75
+ if (mobileApisImplyMobile) {
76
+ score += 2;
77
+ }
78
+ const hasHighDpi = (win.devicePixelRatio ?? 0) > 1.5;
79
+ if (hasHighDpi && hasSmallScreen) {
80
+ score += 1;
81
+ }
82
+ const hasViewportMeta = safeQuerySelector(env, 'meta[name="viewport"]');
83
+ if (hasViewportMeta && hasSmallScreen) {
84
+ score += 1;
85
+ }
86
+ const isPadProInDesktopMode = userAgent.includes('macintosh') && hasTouch;
87
+ if (isPadProInDesktopMode) {
88
+ score += 2;
89
+ }
90
+ if (hasLargeScreen && hasPreciseMouse) {
91
+ score -= 3;
92
+ }
93
+ else if (hasLargeScreen && !hasTouch) {
94
+ score -= 2;
95
+ }
96
+ if (hasPreciseMouse && canHover) {
97
+ score -= 2;
98
+ }
99
+ const isWindowsDesktop = /windows/i.test(userAgent) && !hasMobileUserAgent;
100
+ if (isWindowsDesktop) {
101
+ score -= 3;
102
+ }
103
+ // Chromium's low-entropy UA-CH `mobile` hint: an explicit, high-confidence
104
+ // signal where present, applied on top of the heuristic score above rather
105
+ // than replacing it (it's absent on WebKit/Firefox entirely).
106
+ if (nav.userAgentData?.mobile === true) {
107
+ score += 2;
108
+ }
109
+ else if (nav.userAgentData?.mobile === false) {
110
+ score -= 2;
111
+ }
112
+ return score;
113
+ }
114
+ /** Ported iOS/Android cascade. Only called once `scoreMobile` has decided the
115
+ * device is mobile. */
116
+ function guessMobileOs(env) {
117
+ const win = env.window;
118
+ const nav = env.navigator;
119
+ const userAgent = nav?.userAgent ?? '';
120
+ const hasTouch = !!win
121
+ && ('ontouchstart' in win || (nav?.maxTouchPoints ?? 0) > 0);
122
+ if (/iPad|iPhone|iPod/i.test(userAgent)) {
123
+ return { os: 'ios', explicit: true };
124
+ }
125
+ // iPad Pro reports as a Mac (both in the UA string and in UA-CH's
126
+ // `platform` hint) once it gains a keyboard/trackpad, but keeps touch.
127
+ const isMacWithTouch = /Macintosh|MacIntel/i.test(userAgent) && hasTouch;
128
+ const isMacOsWithTouch = nav?.userAgentData?.platform === 'macOS' && hasTouch;
129
+ if (isMacWithTouch || isMacOsWithTouch) {
130
+ return { os: 'ios', explicit: true };
131
+ }
132
+ const hasIosPermissionApi = typeof win?.DeviceMotionEvent?.requestPermission
133
+ === 'function';
134
+ const hasIosTouchCallout = safeCssSupports(win, '-webkit-touch-callout', 'none');
135
+ if (hasIosPermissionApi || hasIosTouchCallout) {
136
+ return { os: 'ios', explicit: true };
137
+ }
138
+ const isIosWebKit = /WebKit/i.test(userAgent)
139
+ && !/Chrome|CriOS|Android/i.test(userAgent)
140
+ && !/Macintosh|MacIntel/i.test(userAgent);
141
+ if (isIosWebKit) {
142
+ return { os: 'ios', explicit: true };
143
+ }
144
+ if (/Android/i.test(userAgent)) {
145
+ return { os: 'android', explicit: true };
146
+ }
147
+ if (nav?.userAgentData?.platform === 'Android') {
148
+ return { os: 'android', explicit: true };
149
+ }
150
+ // Final fallback: no rule matched. Android is the more common global
151
+ // install base, but this is a guess — callers see `confidence: 'low'`.
152
+ return { os: 'android', explicit: false };
153
+ }
154
+ /** Resolves the device this flow is running on, with a confidence a caller
155
+ * can act on instead of a silent guess. */
156
+ export function resolveDevice(env) {
157
+ const score = scoreMobile(env);
158
+ const isMobile = score >= MOBILE_SCORE_THRESHOLD;
159
+ const borderline = Math.abs(score - MOBILE_SCORE_THRESHOLD) < MOBILE_SCORE_MARGIN;
160
+ if (!isMobile) {
161
+ return { device: 'desktop', confidence: borderline ? 'low' : 'high' };
162
+ }
163
+ const guess = guessMobileOs(env);
164
+ return {
165
+ device: guess.os,
166
+ confidence: borderline || !guess.explicit ? 'low' : 'high',
167
+ };
168
+ }