@bradford-tech/supabase-integrity-attest 0.3.2 → 0.5.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.
Files changed (74) hide show
  1. package/esm/assertion.d.ts +5 -2
  2. package/esm/assertion.d.ts.map +1 -1
  3. package/esm/assertion.js +5 -2
  4. package/esm/attestation.d.ts +6 -1
  5. package/esm/attestation.d.ts.map +1 -1
  6. package/esm/attestation.js +6 -1
  7. package/esm/mod.d.ts +84 -4
  8. package/esm/mod.d.ts.map +1 -1
  9. package/esm/mod.js +84 -4
  10. package/esm/src/attestation.d.ts +9 -1
  11. package/esm/src/attestation.d.ts.map +1 -1
  12. package/esm/src/attestation.js +14 -6
  13. package/esm/src/errors.d.ts +28 -3
  14. package/esm/src/errors.d.ts.map +1 -1
  15. package/esm/src/errors.js +25 -2
  16. package/esm/src/utils.d.ts.map +1 -1
  17. package/esm/src/utils.js +3 -5
  18. package/esm/src/with-assertion.d.ts +41 -3
  19. package/esm/src/with-assertion.d.ts.map +1 -1
  20. package/esm/src/with-assertion.js +31 -9
  21. package/esm/src/with-attestation.d.ts +79 -0
  22. package/esm/src/with-attestation.d.ts.map +1 -0
  23. package/esm/src/with-attestation.js +147 -0
  24. package/package.json +4 -7
  25. package/esm/_dnt.test_polyfills.d.ts.map +0 -1
  26. package/esm/_dnt.test_shims.d.ts.map +0 -1
  27. package/esm/deps/jsr.io/@std/assert/1.0.19/almost_equals.d.ts.map +0 -1
  28. package/esm/deps/jsr.io/@std/assert/1.0.19/array_includes.d.ts.map +0 -1
  29. package/esm/deps/jsr.io/@std/assert/1.0.19/assert.d.ts.map +0 -1
  30. package/esm/deps/jsr.io/@std/assert/1.0.19/assertion_error.d.ts.map +0 -1
  31. package/esm/deps/jsr.io/@std/assert/1.0.19/equal.d.ts.map +0 -1
  32. package/esm/deps/jsr.io/@std/assert/1.0.19/equals.d.ts.map +0 -1
  33. package/esm/deps/jsr.io/@std/assert/1.0.19/exists.d.ts.map +0 -1
  34. package/esm/deps/jsr.io/@std/assert/1.0.19/fail.d.ts.map +0 -1
  35. package/esm/deps/jsr.io/@std/assert/1.0.19/false.d.ts.map +0 -1
  36. package/esm/deps/jsr.io/@std/assert/1.0.19/greater.d.ts.map +0 -1
  37. package/esm/deps/jsr.io/@std/assert/1.0.19/greater_or_equal.d.ts.map +0 -1
  38. package/esm/deps/jsr.io/@std/assert/1.0.19/instance_of.d.ts.map +0 -1
  39. package/esm/deps/jsr.io/@std/assert/1.0.19/is_error.d.ts.map +0 -1
  40. package/esm/deps/jsr.io/@std/assert/1.0.19/less.d.ts.map +0 -1
  41. package/esm/deps/jsr.io/@std/assert/1.0.19/less_or_equal.d.ts.map +0 -1
  42. package/esm/deps/jsr.io/@std/assert/1.0.19/match.d.ts.map +0 -1
  43. package/esm/deps/jsr.io/@std/assert/1.0.19/mod.d.ts.map +0 -1
  44. package/esm/deps/jsr.io/@std/assert/1.0.19/not_equals.d.ts.map +0 -1
  45. package/esm/deps/jsr.io/@std/assert/1.0.19/not_instance_of.d.ts.map +0 -1
  46. package/esm/deps/jsr.io/@std/assert/1.0.19/not_match.d.ts.map +0 -1
  47. package/esm/deps/jsr.io/@std/assert/1.0.19/not_strict_equals.d.ts.map +0 -1
  48. package/esm/deps/jsr.io/@std/assert/1.0.19/object_match.d.ts.map +0 -1
  49. package/esm/deps/jsr.io/@std/assert/1.0.19/rejects.d.ts.map +0 -1
  50. package/esm/deps/jsr.io/@std/assert/1.0.19/strict_equals.d.ts.map +0 -1
  51. package/esm/deps/jsr.io/@std/assert/1.0.19/string_includes.d.ts.map +0 -1
  52. package/esm/deps/jsr.io/@std/assert/1.0.19/throws.d.ts.map +0 -1
  53. package/esm/deps/jsr.io/@std/assert/1.0.19/unimplemented.d.ts.map +0 -1
  54. package/esm/deps/jsr.io/@std/assert/1.0.19/unreachable.d.ts.map +0 -1
  55. package/esm/deps/jsr.io/@std/internal/1.0.12/build_message.d.ts.map +0 -1
  56. package/esm/deps/jsr.io/@std/internal/1.0.12/diff.d.ts.map +0 -1
  57. package/esm/deps/jsr.io/@std/internal/1.0.12/diff_str.d.ts.map +0 -1
  58. package/esm/deps/jsr.io/@std/internal/1.0.12/format.d.ts.map +0 -1
  59. package/esm/deps/jsr.io/@std/internal/1.0.12/styles.d.ts.map +0 -1
  60. package/esm/deps/jsr.io/@std/internal/1.0.12/types.d.ts.map +0 -1
  61. package/esm/src/cose.d.ts.map +0 -1
  62. package/esm/tests/assertion-entry.test.d.ts.map +0 -1
  63. package/esm/tests/assertion.test.d.ts.map +0 -1
  64. package/esm/tests/attestation-entry.test.d.ts.map +0 -1
  65. package/esm/tests/attestation.test.d.ts.map +0 -1
  66. package/esm/tests/authdata.test.d.ts.map +0 -1
  67. package/esm/tests/certificate.test.d.ts.map +0 -1
  68. package/esm/tests/cose.test.d.ts.map +0 -1
  69. package/esm/tests/der.test.d.ts.map +0 -1
  70. package/esm/tests/errors.test.d.ts.map +0 -1
  71. package/esm/tests/fixtures/apple-attestation.d.ts.map +0 -1
  72. package/esm/tests/fixtures/generate-assertion.d.ts.map +0 -1
  73. package/esm/tests/utils.test.d.ts.map +0 -1
  74. package/esm/tests/with-assertion.test.d.ts.map +0 -1
@@ -3,7 +3,10 @@
3
3
  * `@noble/curves`, keeping the bundle minimal for assertion-only use cases.
4
4
  *
5
5
  * ```ts
6
- * import { verifyAssertion } from "@bradford-tech/supabase-integrity-attest/assertion";
6
+ * import {
7
+ * verifyAssertion,
8
+ * withAssertion,
9
+ * } from "@bradford-tech/supabase-integrity-attest/assertion";
7
10
  *
8
11
  * const { signCount } = await verifyAssertion(
9
12
  * { appId: "TEAMID.com.example.app" },
@@ -21,5 +24,5 @@ export type { AppInfo, AssertionResult } from "./src/assertion.js";
21
24
  export { AssertionError, AssertionErrorCode } from "./src/errors.js";
22
25
  export { withAssertion } from "./src/with-assertion.js";
23
26
  export { DEFAULT_ASSERTION_HEADER, DEFAULT_DEVICE_ID_HEADER, } from "./src/with-assertion.js";
24
- export type { AssertionContext, DeviceKey, ExtractAssertionFn, WithAssertionOptions, } from "./src/with-assertion.js";
27
+ export type { AssertionContext, AssertionTimings, DeviceKey, ExtractAssertionFn, WithAssertionOptions, } from "./src/with-assertion.js";
25
28
  //# sourceMappingURL=assertion.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"assertion.d.ts","sourceRoot":"","sources":["../src/assertion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAGrE,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EACL,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,gBAAgB,EAChB,SAAS,EACT,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"assertion.d.ts","sourceRoot":"","sources":["../src/assertion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAGrE,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EACL,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,SAAS,EACT,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,yBAAyB,CAAC"}
package/esm/assertion.js CHANGED
@@ -3,7 +3,10 @@
3
3
  * `@noble/curves`, keeping the bundle minimal for assertion-only use cases.
4
4
  *
5
5
  * ```ts
6
- * import { verifyAssertion } from "@bradford-tech/supabase-integrity-attest/assertion";
6
+ * import {
7
+ * verifyAssertion,
8
+ * withAssertion,
9
+ * } from "@bradford-tech/supabase-integrity-attest/assertion";
7
10
  *
8
11
  * const { signCount } = await verifyAssertion(
9
12
  * { appId: "TEAMID.com.example.app" },
@@ -18,6 +21,6 @@
18
21
  */
19
22
  export { verifyAssertion } from "./src/assertion.js";
20
23
  export { AssertionError, AssertionErrorCode } from "./src/errors.js";
21
- // withAssertion wrapper
24
+ // withAssertion middleware
22
25
  export { withAssertion } from "./src/with-assertion.js";
23
26
  export { DEFAULT_ASSERTION_HEADER, DEFAULT_DEVICE_ID_HEADER, } from "./src/with-assertion.js";
@@ -3,7 +3,10 @@
3
3
  * dependencies (`asn1js`, `@noble/curves`).
4
4
  *
5
5
  * ```ts
6
- * import { verifyAttestation } from "@bradford-tech/supabase-integrity-attest/attestation";
6
+ * import {
7
+ * verifyAttestation,
8
+ * withAttestation,
9
+ * } from "@bradford-tech/supabase-integrity-attest/attestation";
7
10
  *
8
11
  * const { publicKeyPem } = await verifyAttestation(
9
12
  * { appId: "TEAMID.com.example.app" },
@@ -18,4 +21,6 @@
18
21
  export { verifyAttestation } from "./src/attestation.js";
19
22
  export type { AppInfo, AttestationResult, VerifyAttestationOptions, } from "./src/attestation.js";
20
23
  export { AttestationError, AttestationErrorCode } from "./src/errors.js";
24
+ export { withAttestation } from "./src/with-attestation.js";
25
+ export type { AttestationContext, AttestationTimings, ExtractAttestationFn, WithAttestationOptions, } from "./src/with-attestation.js";
21
26
  //# sourceMappingURL=attestation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"attestation.d.ts","sourceRoot":"","sources":["../src/attestation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,YAAY,EACV,OAAO,EACP,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"attestation.d.ts","sourceRoot":"","sources":["../src/attestation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,YAAY,EACV,OAAO,EACP,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAGzE,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,YAAY,EACV,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,2BAA2B,CAAC"}
@@ -3,7 +3,10 @@
3
3
  * dependencies (`asn1js`, `@noble/curves`).
4
4
  *
5
5
  * ```ts
6
- * import { verifyAttestation } from "@bradford-tech/supabase-integrity-attest/attestation";
6
+ * import {
7
+ * verifyAttestation,
8
+ * withAttestation,
9
+ * } from "@bradford-tech/supabase-integrity-attest/attestation";
7
10
  *
8
11
  * const { publicKeyPem } = await verifyAttestation(
9
12
  * { appId: "TEAMID.com.example.app" },
@@ -17,3 +20,5 @@
17
20
  */
18
21
  export { verifyAttestation } from "./src/attestation.js";
19
22
  export { AttestationError, AttestationErrorCode } from "./src/errors.js";
23
+ // withAttestation middleware
24
+ export { withAttestation } from "./src/with-attestation.js";
package/esm/mod.d.ts CHANGED
@@ -1,17 +1,95 @@
1
1
  /**
2
2
  * Verify Apple App Attest attestations and assertions using WebCrypto.
3
3
  *
4
+ * This library implements Apple's full
5
+ * [App Attest](https://developer.apple.com/documentation/devicecheck/establishing-your-app-s-integrity)
6
+ * server-side verification — CBOR decoding, X.509 certificate chain
7
+ * validation, nonce verification, ECDSA signature checks — using only
8
+ * WebCrypto APIs so it runs in Supabase Edge Functions, Deno Deploy,
9
+ * and other edge runtimes with no native dependencies.
10
+ *
11
+ * ## Attestation
12
+ *
13
+ * Verify a new device and extract its public key:
14
+ *
4
15
  * ```ts
5
- * import { verifyAttestation, verifyAssertion } from "@bradford-tech/supabase-integrity-attest";
16
+ * import { verifyAttestation } from "@bradford-tech/supabase-integrity-attest";
6
17
  *
7
- * const { publicKeyPem } = await verifyAttestation(
18
+ * // clientDataHash = SHA-256(challenge) most client SDKs hash internally
19
+ * const clientDataHash = new Uint8Array(
20
+ * await crypto.subtle.digest("SHA-256", new TextEncoder().encode(challenge)),
21
+ * );
22
+ * const { publicKeyPem, receipt, signCount } = await verifyAttestation(
8
23
  * { appId: "TEAMID.com.example.app" },
9
24
  * keyId,
10
- * challenge,
25
+ * clientDataHash,
11
26
  * attestation,
12
27
  * );
28
+ * // Store publicKeyPem and signCount for future assertion verification
13
29
  * ```
14
30
  *
31
+ * ## Assertion
32
+ *
33
+ * Verify ongoing requests from an already-attested device:
34
+ *
35
+ * ```ts
36
+ * import { verifyAssertion } from "@bradford-tech/supabase-integrity-attest";
37
+ *
38
+ * const { signCount } = await verifyAssertion(
39
+ * { appId: "TEAMID.com.example.app" },
40
+ * assertion,
41
+ * clientData,
42
+ * publicKeyPem,
43
+ * previousSignCount,
44
+ * );
45
+ * // Persist the updated signCount
46
+ * ```
47
+ *
48
+ * ## Middleware
49
+ *
50
+ * Use the {@linkcode withAttestation} and {@linkcode withAssertion}
51
+ * wrappers for automatic verification, callback-driven storage, and
52
+ * typed error handling:
53
+ *
54
+ * ```ts
55
+ * import {
56
+ * withAttestation,
57
+ * withAssertion,
58
+ * } from "@bradford-tech/supabase-integrity-attest";
59
+ *
60
+ * const attestHandler = withAttestation(
61
+ * {
62
+ * appId: "TEAMID.com.example.app",
63
+ * consumeChallenge,
64
+ * storeDeviceKey,
65
+ * },
66
+ * (_req, ctx) =>
67
+ * Response.json({ ok: true, deviceId: ctx.deviceId }),
68
+ * );
69
+ *
70
+ * const protectedHandler = withAssertion(
71
+ * {
72
+ * appId: "TEAMID.com.example.app",
73
+ * getDeviceKey,
74
+ * commitSignCount,
75
+ * },
76
+ * (_req, ctx) =>
77
+ * Response.json({ hello: ctx.deviceId, counter: ctx.signCount }),
78
+ * );
79
+ * ```
80
+ *
81
+ * ## Subpath imports
82
+ *
83
+ * For smaller bundles, import only what you need:
84
+ *
85
+ * - `@bradford-tech/supabase-integrity-attest/attestation` — attestation
86
+ * (`verifyAttestation`, `withAttestation`). Full crypto deps.
87
+ * - `@bradford-tech/supabase-integrity-attest/assertion` — assertion
88
+ * (`verifyAssertion`, `withAssertion`). Excludes `asn1js` and
89
+ * `@noble/curves` to keep the bundle minimal.
90
+ *
91
+ * Full documentation: {@link https://integrity-attest.bradford.tech}
92
+ *
15
93
  * @module
16
94
  */
17
95
  export type { AppInfo, AttestationResult, VerifyAttestationOptions, } from "./src/attestation.js";
@@ -21,5 +99,7 @@ export { verifyAssertion } from "./src/assertion.js";
21
99
  export { AssertionError, AssertionErrorCode, AttestationError, AttestationErrorCode, } from "./src/errors.js";
22
100
  export { withAssertion } from "./src/with-assertion.js";
23
101
  export { DEFAULT_ASSERTION_HEADER, DEFAULT_DEVICE_ID_HEADER, } from "./src/with-assertion.js";
24
- export type { AssertionContext, DeviceKey, ExtractAssertionFn, WithAssertionOptions, } from "./src/with-assertion.js";
102
+ export type { AssertionContext, AssertionTimings, DeviceKey, ExtractAssertionFn, WithAssertionOptions, } from "./src/with-assertion.js";
103
+ export { withAttestation } from "./src/with-attestation.js";
104
+ export type { AttestationContext, AttestationTimings, ExtractAttestationFn, WithAttestationOptions, } from "./src/with-attestation.js";
25
105
  //# sourceMappingURL=mod.d.ts.map
package/esm/mod.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,YAAY,EACV,OAAO,EACP,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EACL,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,gBAAgB,EAChB,SAAS,EACT,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6FG;AAEH,YAAY,EACV,OAAO,EACP,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EACL,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,SAAS,EACT,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,YAAY,EACV,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,2BAA2B,CAAC"}
package/esm/mod.js CHANGED
@@ -1,22 +1,102 @@
1
1
  /**
2
2
  * Verify Apple App Attest attestations and assertions using WebCrypto.
3
3
  *
4
+ * This library implements Apple's full
5
+ * [App Attest](https://developer.apple.com/documentation/devicecheck/establishing-your-app-s-integrity)
6
+ * server-side verification — CBOR decoding, X.509 certificate chain
7
+ * validation, nonce verification, ECDSA signature checks — using only
8
+ * WebCrypto APIs so it runs in Supabase Edge Functions, Deno Deploy,
9
+ * and other edge runtimes with no native dependencies.
10
+ *
11
+ * ## Attestation
12
+ *
13
+ * Verify a new device and extract its public key:
14
+ *
4
15
  * ```ts
5
- * import { verifyAttestation, verifyAssertion } from "@bradford-tech/supabase-integrity-attest";
16
+ * import { verifyAttestation } from "@bradford-tech/supabase-integrity-attest";
6
17
  *
7
- * const { publicKeyPem } = await verifyAttestation(
18
+ * // clientDataHash = SHA-256(challenge) most client SDKs hash internally
19
+ * const clientDataHash = new Uint8Array(
20
+ * await crypto.subtle.digest("SHA-256", new TextEncoder().encode(challenge)),
21
+ * );
22
+ * const { publicKeyPem, receipt, signCount } = await verifyAttestation(
8
23
  * { appId: "TEAMID.com.example.app" },
9
24
  * keyId,
10
- * challenge,
25
+ * clientDataHash,
11
26
  * attestation,
12
27
  * );
28
+ * // Store publicKeyPem and signCount for future assertion verification
13
29
  * ```
14
30
  *
31
+ * ## Assertion
32
+ *
33
+ * Verify ongoing requests from an already-attested device:
34
+ *
35
+ * ```ts
36
+ * import { verifyAssertion } from "@bradford-tech/supabase-integrity-attest";
37
+ *
38
+ * const { signCount } = await verifyAssertion(
39
+ * { appId: "TEAMID.com.example.app" },
40
+ * assertion,
41
+ * clientData,
42
+ * publicKeyPem,
43
+ * previousSignCount,
44
+ * );
45
+ * // Persist the updated signCount
46
+ * ```
47
+ *
48
+ * ## Middleware
49
+ *
50
+ * Use the {@linkcode withAttestation} and {@linkcode withAssertion}
51
+ * wrappers for automatic verification, callback-driven storage, and
52
+ * typed error handling:
53
+ *
54
+ * ```ts
55
+ * import {
56
+ * withAttestation,
57
+ * withAssertion,
58
+ * } from "@bradford-tech/supabase-integrity-attest";
59
+ *
60
+ * const attestHandler = withAttestation(
61
+ * {
62
+ * appId: "TEAMID.com.example.app",
63
+ * consumeChallenge,
64
+ * storeDeviceKey,
65
+ * },
66
+ * (_req, ctx) =>
67
+ * Response.json({ ok: true, deviceId: ctx.deviceId }),
68
+ * );
69
+ *
70
+ * const protectedHandler = withAssertion(
71
+ * {
72
+ * appId: "TEAMID.com.example.app",
73
+ * getDeviceKey,
74
+ * commitSignCount,
75
+ * },
76
+ * (_req, ctx) =>
77
+ * Response.json({ hello: ctx.deviceId, counter: ctx.signCount }),
78
+ * );
79
+ * ```
80
+ *
81
+ * ## Subpath imports
82
+ *
83
+ * For smaller bundles, import only what you need:
84
+ *
85
+ * - `@bradford-tech/supabase-integrity-attest/attestation` — attestation
86
+ * (`verifyAttestation`, `withAttestation`). Full crypto deps.
87
+ * - `@bradford-tech/supabase-integrity-attest/assertion` — assertion
88
+ * (`verifyAssertion`, `withAssertion`). Excludes `asn1js` and
89
+ * `@noble/curves` to keep the bundle minimal.
90
+ *
91
+ * Full documentation: {@link https://integrity-attest.bradford.tech}
92
+ *
15
93
  * @module
16
94
  */
17
95
  export { verifyAttestation } from "./src/attestation.js";
18
96
  export { verifyAssertion } from "./src/assertion.js";
19
97
  export { AssertionError, AssertionErrorCode, AttestationError, AttestationErrorCode, } from "./src/errors.js";
20
- // withAssertion wrapper
98
+ // withAssertion middleware
21
99
  export { withAssertion } from "./src/with-assertion.js";
22
100
  export { DEFAULT_ASSERTION_HEADER, DEFAULT_DEVICE_ID_HEADER, } from "./src/with-assertion.js";
101
+ // withAttestation middleware
102
+ export { withAttestation } from "./src/with-attestation.js";
@@ -50,7 +50,15 @@ export declare function decodeAttestationCbor(data: Uint8Array): AttestationCbor
50
50
  * CBOR decode, certificate chain validation, nonce check, key extraction,
51
51
  * AAGUID check, and credential ID verification.
52
52
  *
53
+ * **Important:** The `clientDataHash` parameter corresponds to Apple's
54
+ * `clientDataHash` argument on `DCAppAttestService.attestKey(_:clientDataHash:)`.
55
+ * Most client SDKs (Expo's `attestKeyAsync`, native wrappers) hash the
56
+ * caller's challenge with SHA-256 before passing to Apple. If you are using
57
+ * the {@linkcode withAttestation} middleware, this hashing is handled
58
+ * automatically. If calling `verifyAttestation` directly, you must pass
59
+ * `SHA-256(challenge)` — not the raw challenge — as `clientDataHash`.
60
+ *
53
61
  * @throws {AttestationError} If any verification step fails.
54
62
  */
55
- export declare function verifyAttestation(appInfo: AppInfo, keyId: string, challenge: Uint8Array | string, attestation: Uint8Array | string, options?: VerifyAttestationOptions): Promise<AttestationResult>;
63
+ export declare function verifyAttestation(appInfo: AppInfo, keyId: string, clientDataHash: Uint8Array | string, attestation: Uint8Array | string, options?: VerifyAttestationOptions): Promise<AttestationResult>;
56
64
  //# sourceMappingURL=attestation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"attestation.d.ts","sourceRoot":"","sources":["../../src/src/attestation.ts"],"names":[],"mappings":"AAaA,yCAAyC;AACzC,MAAM,WAAW,OAAO;IACtB,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,kFAAkF;IAClF,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,kDAAkD;AAClD,MAAM,WAAW,iBAAiB;IAChC,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB,4DAA4D;IAC5D,OAAO,EAAE,UAAU,CAAC;IACpB,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,iDAAiD;AACjD,MAAM,WAAW,wBAAwB;IACvC,uFAAuF;IACvF,SAAS,CAAC,EAAE,IAAI,CAAC;CAClB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE;QACP,GAAG,EAAE,UAAU,EAAE,CAAC;QAClB,OAAO,EAAE,UAAU,CAAC;KACrB,CAAC;IACF,QAAQ,EAAE,UAAU,CAAC;CACtB;AAsGD,yEAAyE;AACzE,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,UAAU,GAAG,eAAe,CAyEvE;AAED;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,UAAU,GAAG,MAAM,EAC9B,WAAW,EAAE,UAAU,GAAG,MAAM,EAChC,OAAO,CAAC,EAAE,wBAAwB,GACjC,OAAO,CAAC,iBAAiB,CAAC,CAkJ5B"}
1
+ {"version":3,"file":"attestation.d.ts","sourceRoot":"","sources":["../../src/src/attestation.ts"],"names":[],"mappings":"AAaA,yCAAyC;AACzC,MAAM,WAAW,OAAO;IACtB,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,kFAAkF;IAClF,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,kDAAkD;AAClD,MAAM,WAAW,iBAAiB;IAChC,yEAAyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB,4DAA4D;IAC5D,OAAO,EAAE,UAAU,CAAC;IACpB,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,iDAAiD;AACjD,MAAM,WAAW,wBAAwB;IACvC,uFAAuF;IACvF,SAAS,CAAC,EAAE,IAAI,CAAC;CAClB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE;QACP,GAAG,EAAE,UAAU,EAAE,CAAC;QAClB,OAAO,EAAE,UAAU,CAAC;KACrB,CAAC;IACF,QAAQ,EAAE,UAAU,CAAC;CACtB;AAsGD,yEAAyE;AACzE,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,UAAU,GAAG,eAAe,CAyEvE;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,EACb,cAAc,EAAE,UAAU,GAAG,MAAM,EACnC,WAAW,EAAE,UAAU,GAAG,MAAM,EAChC,OAAO,CAAC,EAAE,wBAAwB,GACjC,OAAO,CAAC,iBAAiB,CAAC,CAkJ5B"}
@@ -163,9 +163,17 @@ export function decodeAttestationCbor(data) {
163
163
  * CBOR decode, certificate chain validation, nonce check, key extraction,
164
164
  * AAGUID check, and credential ID verification.
165
165
  *
166
+ * **Important:** The `clientDataHash` parameter corresponds to Apple's
167
+ * `clientDataHash` argument on `DCAppAttestService.attestKey(_:clientDataHash:)`.
168
+ * Most client SDKs (Expo's `attestKeyAsync`, native wrappers) hash the
169
+ * caller's challenge with SHA-256 before passing to Apple. If you are using
170
+ * the {@linkcode withAttestation} middleware, this hashing is handled
171
+ * automatically. If calling `verifyAttestation` directly, you must pass
172
+ * `SHA-256(challenge)` — not the raw challenge — as `clientDataHash`.
173
+ *
166
174
  * @throws {AttestationError} If any verification step fails.
167
175
  */
168
- export async function verifyAttestation(appInfo, keyId, challenge, attestation, options) {
176
+ export async function verifyAttestation(appInfo, keyId, clientDataHash, attestation, options) {
169
177
  // Decode attestation bytes from base64 if string
170
178
  let attestationBytes;
171
179
  if (typeof attestation === "string") {
@@ -199,11 +207,11 @@ export async function verifyAttestation(appInfo, keyId, challenge, attestation,
199
207
  }
200
208
  // Step 4: Verify certificate chain
201
209
  await verifyCertificateChain(x5c, options?.checkDate);
202
- // Step 5-6: Compute nonce = SHA-256(authData || challenge)
203
- // Apple's nonce verification uses the raw challenge bytes concatenated with authData,
204
- // NOT a hash of the challenge.
205
- const challengeBytes = toBytes(challenge);
206
- const nonceInput = concat(authData, challengeBytes);
210
+ // Step 5-6: Compute nonce = SHA-256(authData || clientDataHash)
211
+ // The clientDataHash is typically SHA-256(challenge) see the JSDoc above.
212
+ // The withAttestation middleware handles this hashing automatically.
213
+ const clientDataHashBytes = toBytes(clientDataHash);
214
+ const nonceInput = concat(authData, clientDataHashBytes);
207
215
  const computedNonce = new Uint8Array(await crypto.subtle.digest("SHA-256", nonceInput));
208
216
  // Step 7: Extract nonce from leaf cert
209
217
  const certNonce = extractNonceFromCert(x5c[0]);
@@ -13,16 +13,32 @@ export declare enum AttestationErrorCode {
13
13
  /** Sign count is not zero (required for attestation). */
14
14
  INVALID_COUNTER = "INVALID_COUNTER",
15
15
  /** AAGUID does not match the expected environment (production/development). */
16
- INVALID_AAGUID = "INVALID_AAGUID"
16
+ INVALID_AAGUID = "INVALID_AAGUID",
17
+ /**
18
+ * The `consumeChallenge` callback returned `false`, meaning the challenge
19
+ * was missing, expired, or already consumed. Used by {@linkcode withAttestation}.
20
+ */
21
+ CHALLENGE_INVALID = "CHALLENGE_INVALID",
22
+ /**
23
+ * An internal or storage callback error occurred (used by
24
+ * {@linkcode withAttestation}). The original error is attached via
25
+ * `Error.cause` and is deliberately NOT reflected in the HTTP response
26
+ * body to avoid leaking database schema or driver diagnostics.
27
+ */
28
+ INTERNAL_ERROR = "INTERNAL_ERROR"
17
29
  }
18
30
  /** Thrown when attestation verification fails. */
19
31
  export declare class AttestationError extends Error {
20
32
  /** Machine-readable error code. */
21
33
  readonly code: AttestationErrorCode;
34
+ /** Discriminant for `instanceof` checks in catch blocks. Always `"AttestationError"`. */
22
35
  readonly name = "AttestationError";
36
+ /** Create an AttestationError with a machine-readable code and human-readable message. */
23
37
  constructor(
24
38
  /** Machine-readable error code. */
25
- code: AttestationErrorCode, message: string);
39
+ code: AttestationErrorCode, message: string, options?: {
40
+ cause?: unknown;
41
+ });
26
42
  }
27
43
  /** Error codes returned by {@linkcode AssertionError}. */
28
44
  export declare enum AssertionErrorCode {
@@ -37,13 +53,22 @@ export declare enum AssertionErrorCode {
37
53
  /** No device key found for the given device ID (used by {@linkcode withAssertion}). */
38
54
  DEVICE_NOT_FOUND = "DEVICE_NOT_FOUND",
39
55
  /** An internal or storage callback error occurred (used by {@linkcode withAssertion}). */
40
- INTERNAL_ERROR = "INTERNAL_ERROR"
56
+ INTERNAL_ERROR = "INTERNAL_ERROR",
57
+ /**
58
+ * The `commitSignCount` callback returned `false`, meaning another
59
+ * concurrent request already advanced the stored counter past this value.
60
+ * Indicates an expected race under concurrent load, not a client bug.
61
+ * Used by {@linkcode withAssertion}.
62
+ */
63
+ SIGN_COUNT_STALE = "SIGN_COUNT_STALE"
41
64
  }
42
65
  /** Thrown when assertion verification fails. */
43
66
  export declare class AssertionError extends Error {
44
67
  /** Machine-readable error code. */
45
68
  readonly code: AssertionErrorCode;
69
+ /** Discriminant for `instanceof` checks in catch blocks. Always `"AssertionError"`. */
46
70
  readonly name = "AssertionError";
71
+ /** Create an AssertionError with a machine-readable code and human-readable message. */
47
72
  constructor(
48
73
  /** Machine-readable error code. */
49
74
  code: AssertionErrorCode, message: string, options?: {
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/src/errors.ts"],"names":[],"mappings":"AAEA,4DAA4D;AAC5D,oBAAY,oBAAoB;IAC9B,qDAAqD;IACrD,cAAc,mBAAmB;IACjC,mDAAmD;IACnD,yBAAyB,8BAA8B;IACvD,uEAAuE;IACvE,cAAc,mBAAmB;IACjC,uDAAuD;IACvD,cAAc,mBAAmB;IACjC,0DAA0D;IAC1D,eAAe,oBAAoB;IACnC,yDAAyD;IACzD,eAAe,oBAAoB;IACnC,+EAA+E;IAC/E,cAAc,mBAAmB;CAClC;AAED,kDAAkD;AAClD,qBAAa,gBAAiB,SAAQ,KAAK;IAGvC,mCAAmC;aACnB,IAAI,EAAE,oBAAoB;IAH5C,SAAkB,IAAI,sBAAsB;;IAE1C,mCAAmC;IACnB,IAAI,EAAE,oBAAoB,EAC1C,OAAO,EAAE,MAAM;CAIlB;AAED,0DAA0D;AAC1D,oBAAY,kBAAkB;IAC5B,qDAAqD;IACrD,cAAc,mBAAmB;IACjC,uDAAuD;IACvD,cAAc,mBAAmB;IACjC,mEAAmE;IACnE,uBAAuB,4BAA4B;IACnD,2CAA2C;IAC3C,iBAAiB,sBAAsB;IACvC,uFAAuF;IACvF,gBAAgB,qBAAqB;IACrC,0FAA0F;IAC1F,cAAc,mBAAmB;CAClC;AAED,gDAAgD;AAChD,qBAAa,cAAe,SAAQ,KAAK;IAGrC,mCAAmC;aACnB,IAAI,EAAE,kBAAkB;IAH1C,SAAkB,IAAI,oBAAoB;;IAExC,mCAAmC;IACnB,IAAI,EAAE,kBAAkB,EACxC,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAIhC"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/src/errors.ts"],"names":[],"mappings":"AAEA,4DAA4D;AAC5D,oBAAY,oBAAoB;IAC9B,qDAAqD;IACrD,cAAc,mBAAmB;IACjC,mDAAmD;IACnD,yBAAyB,8BAA8B;IACvD,uEAAuE;IACvE,cAAc,mBAAmB;IACjC,uDAAuD;IACvD,cAAc,mBAAmB;IACjC,0DAA0D;IAC1D,eAAe,oBAAoB;IACnC,yDAAyD;IACzD,eAAe,oBAAoB;IACnC,+EAA+E;IAC/E,cAAc,mBAAmB;IACjC;;;OAGG;IACH,iBAAiB,sBAAsB;IACvC;;;;;OAKG;IACH,cAAc,mBAAmB;CAClC;AAED,kDAAkD;AAClD,qBAAa,gBAAiB,SAAQ,KAAK;IAKvC,mCAAmC;aACnB,IAAI,EAAE,oBAAoB;IAL5C,yFAAyF;IACzF,SAAkB,IAAI,sBAAsB;IAC5C,0FAA0F;;IAExF,mCAAmC;IACnB,IAAI,EAAE,oBAAoB,EAC1C,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAIhC;AAED,0DAA0D;AAC1D,oBAAY,kBAAkB;IAC5B,qDAAqD;IACrD,cAAc,mBAAmB;IACjC,uDAAuD;IACvD,cAAc,mBAAmB;IACjC,mEAAmE;IACnE,uBAAuB,4BAA4B;IACnD,2CAA2C;IAC3C,iBAAiB,sBAAsB;IACvC,uFAAuF;IACvF,gBAAgB,qBAAqB;IACrC,0FAA0F;IAC1F,cAAc,mBAAmB;IACjC;;;;;OAKG;IACH,gBAAgB,qBAAqB;CACtC;AAED,gDAAgD;AAChD,qBAAa,cAAe,SAAQ,KAAK;IAKrC,mCAAmC;aACnB,IAAI,EAAE,kBAAkB;IAL1C,uFAAuF;IACvF,SAAkB,IAAI,oBAAoB;IAC1C,wFAAwF;;IAEtF,mCAAmC;IACnB,IAAI,EAAE,kBAAkB,EACxC,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAIhC"}
package/esm/src/errors.js CHANGED
@@ -16,19 +16,33 @@ export var AttestationErrorCode;
16
16
  AttestationErrorCode["INVALID_COUNTER"] = "INVALID_COUNTER";
17
17
  /** AAGUID does not match the expected environment (production/development). */
18
18
  AttestationErrorCode["INVALID_AAGUID"] = "INVALID_AAGUID";
19
+ /**
20
+ * The `consumeChallenge` callback returned `false`, meaning the challenge
21
+ * was missing, expired, or already consumed. Used by {@linkcode withAttestation}.
22
+ */
23
+ AttestationErrorCode["CHALLENGE_INVALID"] = "CHALLENGE_INVALID";
24
+ /**
25
+ * An internal or storage callback error occurred (used by
26
+ * {@linkcode withAttestation}). The original error is attached via
27
+ * `Error.cause` and is deliberately NOT reflected in the HTTP response
28
+ * body to avoid leaking database schema or driver diagnostics.
29
+ */
30
+ AttestationErrorCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
19
31
  })(AttestationErrorCode || (AttestationErrorCode = {}));
20
32
  /** Thrown when attestation verification fails. */
21
33
  export class AttestationError extends Error {
34
+ /** Create an AttestationError with a machine-readable code and human-readable message. */
22
35
  constructor(
23
36
  /** Machine-readable error code. */
24
- code, message) {
25
- super(message);
37
+ code, message, options) {
38
+ super(message, options);
26
39
  Object.defineProperty(this, "code", {
27
40
  enumerable: true,
28
41
  configurable: true,
29
42
  writable: true,
30
43
  value: code
31
44
  });
45
+ /** Discriminant for `instanceof` checks in catch blocks. Always `"AttestationError"`. */
32
46
  Object.defineProperty(this, "name", {
33
47
  enumerable: true,
34
48
  configurable: true,
@@ -52,9 +66,17 @@ export var AssertionErrorCode;
52
66
  AssertionErrorCode["DEVICE_NOT_FOUND"] = "DEVICE_NOT_FOUND";
53
67
  /** An internal or storage callback error occurred (used by {@linkcode withAssertion}). */
54
68
  AssertionErrorCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
69
+ /**
70
+ * The `commitSignCount` callback returned `false`, meaning another
71
+ * concurrent request already advanced the stored counter past this value.
72
+ * Indicates an expected race under concurrent load, not a client bug.
73
+ * Used by {@linkcode withAssertion}.
74
+ */
75
+ AssertionErrorCode["SIGN_COUNT_STALE"] = "SIGN_COUNT_STALE";
55
76
  })(AssertionErrorCode || (AssertionErrorCode = {}));
56
77
  /** Thrown when assertion verification fails. */
57
78
  export class AssertionError extends Error {
79
+ /** Create an AssertionError with a machine-readable code and human-readable message. */
58
80
  constructor(
59
81
  /** Machine-readable error code. */
60
82
  code, message, options) {
@@ -65,6 +87,7 @@ export class AssertionError extends Error {
65
87
  writable: true,
66
88
  value: code
67
89
  });
90
+ /** Discriminant for `instanceof` checks in catch blocks. Always `"AssertionError"`. */
68
91
  Object.defineProperty(this, "name", {
69
92
  enumerable: true,
70
93
  configurable: true,
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/src/utils.ts"],"names":[],"mappings":"AAGA,iDAAiD;AACjD,wBAAgB,MAAM,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,GAAG,UAAU,CAU1D;AAED,mDAAmD;AACnD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,OAAO,CAOvE;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,UAAU,CAGxE;AAED,6EAA6E;AAC7E,wBAAgB,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,UAAU,CAG9D;AAED,wFAAwF;AACxF,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAaxE;AAED,qDAAqD;AACrD,wBAAsB,cAAc,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAIpE"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/src/utils.ts"],"names":[],"mappings":"AAGA,iDAAiD;AACjD,wBAAgB,MAAM,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,GAAG,UAAU,CAU1D;AAED,mDAAmD;AACnD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,OAAO,CAMvE;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,UAAU,CAGxE;AAED,6EAA6E;AAC7E,wBAAgB,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,UAAU,CAG9D;AAED,wFAAwF;AACxF,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAaxE;AAED,qDAAqD;AACrD,wBAAsB,cAAc,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAIpE"}
package/esm/src/utils.js CHANGED
@@ -15,10 +15,8 @@ export function concat(...arrays) {
15
15
  }
16
16
  /** Constant-time comparison of two byte arrays. */
17
17
  export function constantTimeEqual(a, b) {
18
- if (a.length !== b.length)
19
- return false;
20
- let diff = 0;
21
- for (let i = 0; i < a.length; i++) {
18
+ let diff = a.length ^ b.length;
19
+ for (let i = 0; i < a.length && i < b.length; i++) {
22
20
  diff |= a[i] ^ b[i];
23
21
  }
24
22
  return diff === 0;
@@ -47,6 +45,6 @@ export async function importPemPublicKey(pem) {
47
45
  /** Export a CryptoKey to PEM-encoded SPKI format. */
48
46
  export async function exportKeyToPem(key) {
49
47
  const spki = await crypto.subtle.exportKey("spki", key);
50
- const base64 = encodeBase64(spki);
48
+ const base64 = encodeBase64(spki).match(/.{1,64}/g).join("\n");
51
49
  return `-----BEGIN PUBLIC KEY-----\n${base64}\n-----END PUBLIC KEY-----`;
52
50
  }
@@ -10,6 +10,21 @@ export type DeviceKey = {
10
10
  /** Last verified sign count for this device. */
11
11
  signCount: number;
12
12
  };
13
+ /**
14
+ * Library-internal timing spans for an assertion verification, in
15
+ * milliseconds. Exposed on {@linkcode AssertionContext.timings} so the
16
+ * wrapped handler can emit them as part of its own Server-Timing response.
17
+ */
18
+ export type AssertionTimings = {
19
+ /** Parse request headers and read raw body bytes. */
20
+ extractMs: number;
21
+ /** `getDeviceKey` callback wall-clock duration. */
22
+ getDeviceKeyMs: number;
23
+ /** Cryptographic verification (CBOR decode, ECDSA verify, counter check). */
24
+ verifyMs: number;
25
+ /** `commitSignCount` callback wall-clock duration. */
26
+ commitMs: number;
27
+ };
13
28
  /** Context passed to the inner handler after successful assertion verification. */
14
29
  export type AssertionContext = {
15
30
  /** Device identifier from the request. */
@@ -18,6 +33,8 @@ export type AssertionContext = {
18
33
  signCount: number;
19
34
  /** Raw request body bytes (the client data that was signed). */
20
35
  rawBody: Uint8Array;
36
+ /** Library-internal timings, ready to merge into Server-Timing. */
37
+ timings: AssertionTimings;
21
38
  };
22
39
  /** Custom function to extract assertion data from an incoming request. */
23
40
  export type ExtractAssertionFn = (req: Request) => Promise<{
@@ -33,8 +50,24 @@ export type WithAssertionOptions = {
33
50
  developmentEnv?: boolean;
34
51
  /** Retrieve the stored device key for a given device ID. Return `null` if not found. */
35
52
  getDeviceKey: (deviceId: string) => Promise<DeviceKey | null>;
36
- /** Persist the new sign count after successful verification. */
37
- updateSignCount: (deviceId: string, newSignCount: number) => Promise<void>;
53
+ /**
54
+ * Atomically advance the stored sign count. MUST be implemented as a
55
+ * compare-and-swap: only update if the currently stored value is strictly
56
+ * less than `newSignCount`. Returns `true` if the row was advanced,
57
+ * `false` if another concurrent request already advanced past this value.
58
+ *
59
+ * Recommended SQL pattern against Postgres:
60
+ *
61
+ * ```sql
62
+ * UPDATE app_attest_devices
63
+ * SET sign_count = $1, last_seen_at = now()
64
+ * WHERE device_id = $2 AND sign_count < $1
65
+ * ```
66
+ *
67
+ * Return `rowCount > 0`. The library converts `false` into
68
+ * `AssertionError(SIGN_COUNT_STALE)`.
69
+ */
70
+ commitSignCount: (deviceId: string, newSignCount: number) => Promise<boolean>;
38
71
  /** Override the default header-based assertion extraction. */
39
72
  extractAssertion?: ExtractAssertionFn;
40
73
  /** Custom error response handler. Defaults to JSON error responses. */
@@ -44,8 +77,13 @@ export type WithAssertionOptions = {
44
77
  * Request handler middleware that verifies App Attest assertions.
45
78
  *
46
79
  * Wraps a handler function with automatic assertion verification,
47
- * device key lookup, and sign count management. Returns a new handler
80
+ * device key lookup, and atomic sign-count commit. Returns a new handler
48
81
  * that rejects unauthenticated requests with appropriate HTTP error responses.
82
+ *
83
+ * The `commitSignCount` callback MUST implement compare-and-swap semantics
84
+ * (see {@linkcode WithAssertionOptions.commitSignCount}) — a non-atomic
85
+ * unconditional write will silently corrupt replay protection under
86
+ * concurrent load.
49
87
  */
50
88
  export declare function withAssertion(options: WithAssertionOptions, handler: (req: Request, context: AssertionContext) => Response | Promise<Response>): (req: Request) => Promise<Response>;
51
89
  //# sourceMappingURL=with-assertion.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"with-assertion.d.ts","sourceRoot":"","sources":["../../src/src/with-assertion.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAsB,MAAM,aAAa,CAAC;AAEjE,iEAAiE;AACjE,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AACjE,0DAA0D;AAC1D,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AAEjE,0EAA0E;AAC1E,MAAM,MAAM,SAAS,GAAG;IACtB,2DAA2D;IAC3D,YAAY,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,mFAAmF;AACnF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,OAAO,EAAE,UAAU,CAAC;CACrB,CAAC;AAEF,0EAA0E;AAC1E,MAAM,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,UAAU,CAAC;CACxB,CAAC,CAAC;AAEH,kEAAkE;AAClE,MAAM,MAAM,oBAAoB,GAAG;IACjC,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,wFAAwF;IACxF,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAC9D,gEAAgE;IAChE,eAAe,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3E,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,kBAAkB,CAAC;IACtC,uEAAuE;IACvE,OAAO,CAAC,EAAE,CACR,KAAK,EAAE,cAAc,EACrB,GAAG,EAAE,OAAO,KACT,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACnC,CAAC;AAmCF;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,oBAAoB,EAC7B,OAAO,EAAE,CACP,GAAG,EAAE,OAAO,EACZ,OAAO,EAAE,gBAAgB,KACtB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAChC,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAuErC"}
1
+ {"version":3,"file":"with-assertion.d.ts","sourceRoot":"","sources":["../../src/src/with-assertion.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAsB,MAAM,aAAa,CAAC;AAEjE,iEAAiE;AACjE,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AACjE,0DAA0D;AAC1D,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AAEjE,0EAA0E;AAC1E,MAAM,MAAM,SAAS,GAAG;IACtB,2DAA2D;IAC3D,YAAY,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,qDAAqD;IACrD,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,cAAc,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,mFAAmF;AACnF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,OAAO,EAAE,UAAU,CAAC;IACpB,mEAAmE;IACnE,OAAO,EAAE,gBAAgB,CAAC;CAC3B,CAAC;AAEF,0EAA0E;AAC1E,MAAM,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,UAAU,CAAC;CACxB,CAAC,CAAC;AAEH,kEAAkE;AAClE,MAAM,MAAM,oBAAoB,GAAG;IACjC,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,wFAAwF;IACxF,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAC9D;;;;;;;;;;;;;;;;OAgBG;IACH,eAAe,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9E,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,kBAAkB,CAAC;IACtC,uEAAuE;IACvE,OAAO,CAAC,EAAE,CACR,KAAK,EAAE,cAAc,EACrB,GAAG,EAAE,OAAO,KACT,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACnC,CAAC;AAmCF;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,oBAAoB,EAC7B,OAAO,EAAE,CACP,GAAG,EAAE,OAAO,EACZ,OAAO,EAAE,gBAAgB,KACtB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAChC,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAiGrC"}