@bradford-tech/supabase-integrity-attest 0.2.4 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -1
- package/esm/_dnt.test_polyfills.d.ts.map +1 -0
- package/esm/assertion.d.ts +7 -0
- package/esm/assertion.d.ts.map +1 -0
- package/esm/assertion.js +6 -0
- package/esm/attestation.d.ts +4 -0
- package/esm/attestation.d.ts.map +1 -0
- package/esm/attestation.js +3 -0
- package/esm/mod.d.ts +3 -0
- package/esm/mod.d.ts.map +1 -1
- package/esm/mod.js +3 -0
- package/esm/src/errors.d.ts +6 -2
- package/esm/src/errors.d.ts.map +1 -1
- package/esm/src/errors.js +4 -2
- package/esm/src/with-assertion.d.ts +27 -0
- package/esm/src/with-assertion.d.ts.map +1 -0
- package/esm/src/with-assertion.js +72 -0
- package/esm/tests/assertion-entry.test.d.ts.map +1 -0
- package/esm/tests/assertion.test.d.ts.map +1 -1
- package/esm/tests/attestation-entry.test.d.ts.map +1 -0
- package/esm/tests/attestation.test.d.ts.map +1 -1
- package/esm/tests/authdata.test.d.ts.map +1 -1
- package/esm/tests/certificate.test.d.ts.map +1 -1
- package/esm/tests/cose.test.d.ts.map +1 -1
- package/esm/tests/der.test.d.ts.map +1 -1
- package/esm/tests/errors.test.d.ts.map +1 -1
- package/esm/tests/utils.test.d.ts.map +1 -1
- package/esm/tests/with-assertion.test.d.ts.map +1 -0
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -13,6 +13,21 @@ deno add jsr:@bradford-tech/supabase-integrity-attest
|
|
|
13
13
|
npx jsr add @bradford-tech/supabase-integrity-attest
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
## Subpath imports
|
|
17
|
+
|
|
18
|
+
If you only need assertion verification, import from the lighter entry point:
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
// Full library (attestation + assertion)
|
|
22
|
+
import { verifyAttestation, verifyAssertion } from "@bradford-tech/supabase-integrity-attest";
|
|
23
|
+
|
|
24
|
+
// Assertion only — skips asn1js and @noble/curves
|
|
25
|
+
import { verifyAssertion } from "@bradford-tech/supabase-integrity-attest/assertion";
|
|
26
|
+
|
|
27
|
+
// Attestation only
|
|
28
|
+
import { verifyAttestation } from "@bradford-tech/supabase-integrity-attest/attestation";
|
|
29
|
+
```
|
|
30
|
+
|
|
16
31
|
## Usage
|
|
17
32
|
|
|
18
33
|
### Attestation (one-time per device)
|
|
@@ -30,7 +45,88 @@ const result = await verifyAttestation(
|
|
|
30
45
|
// Store result.publicKeyPem and signCount (0) for this device
|
|
31
46
|
```
|
|
32
47
|
|
|
33
|
-
###
|
|
48
|
+
### Protecting edge functions with `withAssertion`
|
|
49
|
+
|
|
50
|
+
`withAssertion` wraps a request handler so that assertion verification,
|
|
51
|
+
device lookup, and sign count updates happen before your business logic runs.
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { createClient } from "jsr:@supabase/supabase-js@2";
|
|
55
|
+
import { withAssertion } from "@bradford-tech/supabase-integrity-attest";
|
|
56
|
+
|
|
57
|
+
const supabase = createClient(
|
|
58
|
+
Deno.env.get("SUPABASE_URL")!,
|
|
59
|
+
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
Deno.serve(withAssertion({
|
|
63
|
+
appId: Deno.env.get("APP_ATTEST_APP_ID")!,
|
|
64
|
+
developmentEnv: Deno.env.get("APP_ATTEST_ENV") === "development",
|
|
65
|
+
getDeviceKey: async (deviceId) => {
|
|
66
|
+
const { data } = await supabase
|
|
67
|
+
.from("device_attestations")
|
|
68
|
+
.select("public_key_pem, sign_count")
|
|
69
|
+
.eq("key_id", deviceId)
|
|
70
|
+
.single();
|
|
71
|
+
return data
|
|
72
|
+
? { publicKeyPem: data.public_key_pem, signCount: data.sign_count }
|
|
73
|
+
: null;
|
|
74
|
+
},
|
|
75
|
+
updateSignCount: async (deviceId, newSignCount) => {
|
|
76
|
+
await supabase
|
|
77
|
+
.from("device_attestations")
|
|
78
|
+
.update({ sign_count: newSignCount })
|
|
79
|
+
.eq("key_id", deviceId);
|
|
80
|
+
},
|
|
81
|
+
}, async (_req, { rawBody }) => {
|
|
82
|
+
const { text, voice } = JSON.parse(new TextDecoder().decode(rawBody));
|
|
83
|
+
// business logic
|
|
84
|
+
return new Response(JSON.stringify({ audio: "..." }), {
|
|
85
|
+
headers: { "Content-Type": "application/json" },
|
|
86
|
+
});
|
|
87
|
+
}));
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The client sends the assertion and device ID in headers. The request body
|
|
91
|
+
is the client data that was signed:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
POST /functions/v1/your-endpoint
|
|
95
|
+
Headers:
|
|
96
|
+
Content-Type: application/json
|
|
97
|
+
X-App-Attest-Assertion: <base64-encoded assertion>
|
|
98
|
+
X-App-Attest-Device-Id: <base64-encoded keyId>
|
|
99
|
+
Body:
|
|
100
|
+
{"text": "Hello world", "voice": "en-US"}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Once you have multiple protected functions, extract the shared options into
|
|
104
|
+
a helper:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
// supabase/functions/_shared/attest.ts
|
|
108
|
+
import type { WithAssertionOptions } from "@bradford-tech/supabase-integrity-attest";
|
|
109
|
+
|
|
110
|
+
export const attestOptions: WithAssertionOptions = {
|
|
111
|
+
appId: Deno.env.get("APP_ATTEST_APP_ID")!,
|
|
112
|
+
// ... getDeviceKey, updateSignCount as above
|
|
113
|
+
};
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// supabase/functions/text-to-speech/index.ts
|
|
118
|
+
import { withAssertion } from "@bradford-tech/supabase-integrity-attest";
|
|
119
|
+
import { attestOptions } from "../_shared/attest.ts";
|
|
120
|
+
|
|
121
|
+
Deno.serve(withAssertion(attestOptions, async (_req, { rawBody }) => {
|
|
122
|
+
const { text, voice } = JSON.parse(new TextDecoder().decode(rawBody));
|
|
123
|
+
return new Response(JSON.stringify({ audio: "..." }));
|
|
124
|
+
}));
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Assertion (low-level)
|
|
128
|
+
|
|
129
|
+
For full control over the verification flow, use `verifyAssertion` directly:
|
|
34
130
|
|
|
35
131
|
```typescript
|
|
36
132
|
import { verifyAssertion } from "@bradford-tech/supabase-integrity-attest";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_dnt.test_polyfills.d.ts","sourceRoot":"","sources":["../src/_dnt.test_polyfills.ts"],"names":[],"mappings":"AAAA,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,KAAK;QACb,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB;CACF;AAED,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { verifyAssertion } from "./src/assertion.js";
|
|
2
|
+
export type { AppInfo, AssertionResult } from "./src/assertion.js";
|
|
3
|
+
export { AssertionError, AssertionErrorCode } from "./src/errors.js";
|
|
4
|
+
export { withAssertion } from "./src/with-assertion.js";
|
|
5
|
+
export { DEFAULT_ASSERTION_HEADER, DEFAULT_DEVICE_ID_HEADER, } from "./src/with-assertion.js";
|
|
6
|
+
export type { AssertionContext, DeviceKey, ExtractAssertionFn, WithAssertionOptions, } from "./src/with-assertion.js";
|
|
7
|
+
//# sourceMappingURL=assertion.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assertion.d.ts","sourceRoot":"","sources":["../src/assertion.ts"],"names":[],"mappings":"AAEA,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"}
|
package/esm/assertion.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// assertion.ts — lightweight entry point (no asn1js / @noble/curves)
|
|
2
|
+
export { verifyAssertion } from "./src/assertion.js";
|
|
3
|
+
export { AssertionError, AssertionErrorCode } from "./src/errors.js";
|
|
4
|
+
// withAssertion wrapper
|
|
5
|
+
export { withAssertion } from "./src/with-assertion.js";
|
|
6
|
+
export { DEFAULT_ASSERTION_HEADER, DEFAULT_DEVICE_ID_HEADER, } from "./src/with-assertion.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { verifyAttestation } from "./src/attestation.js";
|
|
2
|
+
export type { AppInfo, AttestationResult, VerifyAttestationOptions, } from "./src/attestation.js";
|
|
3
|
+
export { AttestationError, AttestationErrorCode } from "./src/errors.js";
|
|
4
|
+
//# sourceMappingURL=attestation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attestation.d.ts","sourceRoot":"","sources":["../src/attestation.ts"],"names":[],"mappings":"AAEA,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"}
|
package/esm/mod.d.ts
CHANGED
|
@@ -3,4 +3,7 @@ export type { AssertionResult } from "./src/assertion.js";
|
|
|
3
3
|
export { verifyAttestation } from "./src/attestation.js";
|
|
4
4
|
export { verifyAssertion } from "./src/assertion.js";
|
|
5
5
|
export { AssertionError, AssertionErrorCode, AttestationError, AttestationErrorCode, } from "./src/errors.js";
|
|
6
|
+
export { withAssertion } from "./src/with-assertion.js";
|
|
7
|
+
export { DEFAULT_ASSERTION_HEADER, DEFAULT_DEVICE_ID_HEADER, } from "./src/with-assertion.js";
|
|
8
|
+
export type { AssertionContext, DeviceKey, ExtractAssertionFn, WithAssertionOptions, } from "./src/with-assertion.js";
|
|
6
9
|
//# 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":"AAEA,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"}
|
|
1
|
+
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAEA,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"}
|
package/esm/mod.js
CHANGED
|
@@ -2,3 +2,6 @@
|
|
|
2
2
|
export { verifyAttestation } from "./src/attestation.js";
|
|
3
3
|
export { verifyAssertion } from "./src/assertion.js";
|
|
4
4
|
export { AssertionError, AssertionErrorCode, AttestationError, AttestationErrorCode, } from "./src/errors.js";
|
|
5
|
+
// withAssertion wrapper
|
|
6
|
+
export { withAssertion } from "./src/with-assertion.js";
|
|
7
|
+
export { DEFAULT_ASSERTION_HEADER, DEFAULT_DEVICE_ID_HEADER, } from "./src/with-assertion.js";
|
package/esm/src/errors.d.ts
CHANGED
|
@@ -16,11 +16,15 @@ export declare enum AssertionErrorCode {
|
|
|
16
16
|
INVALID_FORMAT = "INVALID_FORMAT",
|
|
17
17
|
RP_ID_MISMATCH = "RP_ID_MISMATCH",
|
|
18
18
|
COUNTER_NOT_INCREMENTED = "COUNTER_NOT_INCREMENTED",
|
|
19
|
-
SIGNATURE_INVALID = "SIGNATURE_INVALID"
|
|
19
|
+
SIGNATURE_INVALID = "SIGNATURE_INVALID",
|
|
20
|
+
DEVICE_NOT_FOUND = "DEVICE_NOT_FOUND",
|
|
21
|
+
INTERNAL_ERROR = "INTERNAL_ERROR"
|
|
20
22
|
}
|
|
21
23
|
export declare class AssertionError extends Error {
|
|
22
24
|
readonly code: AssertionErrorCode;
|
|
23
25
|
readonly name = "AssertionError";
|
|
24
|
-
constructor(code: AssertionErrorCode, message: string
|
|
26
|
+
constructor(code: AssertionErrorCode, message: string, options?: {
|
|
27
|
+
cause?: unknown;
|
|
28
|
+
});
|
|
25
29
|
}
|
|
26
30
|
//# sourceMappingURL=errors.d.ts.map
|
package/esm/src/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/src/errors.ts"],"names":[],"mappings":"AAEA,oBAAY,oBAAoB;IAC9B,cAAc,mBAAmB;IACjC,yBAAyB,8BAA8B;IACvD,cAAc,mBAAmB;IACjC,cAAc,mBAAmB;IACjC,eAAe,oBAAoB;IACnC,eAAe,oBAAoB;IACnC,cAAc,mBAAmB;CAClC;AAED,qBAAa,gBAAiB,SAAQ,KAAK;aAGvB,IAAI,EAAE,oBAAoB;IAF5C,SAAkB,IAAI,sBAAsB;gBAE1B,IAAI,EAAE,oBAAoB,EAC1C,OAAO,EAAE,MAAM;CAIlB;AAED,oBAAY,kBAAkB;IAC5B,cAAc,mBAAmB;IACjC,cAAc,mBAAmB;IACjC,uBAAuB,4BAA4B;IACnD,iBAAiB,sBAAsB;
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/src/errors.ts"],"names":[],"mappings":"AAEA,oBAAY,oBAAoB;IAC9B,cAAc,mBAAmB;IACjC,yBAAyB,8BAA8B;IACvD,cAAc,mBAAmB;IACjC,cAAc,mBAAmB;IACjC,eAAe,oBAAoB;IACnC,eAAe,oBAAoB;IACnC,cAAc,mBAAmB;CAClC;AAED,qBAAa,gBAAiB,SAAQ,KAAK;aAGvB,IAAI,EAAE,oBAAoB;IAF5C,SAAkB,IAAI,sBAAsB;gBAE1B,IAAI,EAAE,oBAAoB,EAC1C,OAAO,EAAE,MAAM;CAIlB;AAED,oBAAY,kBAAkB;IAC5B,cAAc,mBAAmB;IACjC,cAAc,mBAAmB;IACjC,uBAAuB,4BAA4B;IACnD,iBAAiB,sBAAsB;IACvC,gBAAgB,qBAAqB;IACrC,cAAc,mBAAmB;CAClC;AAED,qBAAa,cAAe,SAAQ,KAAK;aAGrB,IAAI,EAAE,kBAAkB;IAF1C,SAAkB,IAAI,oBAAoB;gBAExB,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
|
@@ -32,10 +32,12 @@ export var AssertionErrorCode;
|
|
|
32
32
|
AssertionErrorCode["RP_ID_MISMATCH"] = "RP_ID_MISMATCH";
|
|
33
33
|
AssertionErrorCode["COUNTER_NOT_INCREMENTED"] = "COUNTER_NOT_INCREMENTED";
|
|
34
34
|
AssertionErrorCode["SIGNATURE_INVALID"] = "SIGNATURE_INVALID";
|
|
35
|
+
AssertionErrorCode["DEVICE_NOT_FOUND"] = "DEVICE_NOT_FOUND";
|
|
36
|
+
AssertionErrorCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
|
|
35
37
|
})(AssertionErrorCode || (AssertionErrorCode = {}));
|
|
36
38
|
export class AssertionError extends Error {
|
|
37
|
-
constructor(code, message) {
|
|
38
|
-
super(message);
|
|
39
|
+
constructor(code, message, options) {
|
|
40
|
+
super(message, options);
|
|
39
41
|
Object.defineProperty(this, "code", {
|
|
40
42
|
enumerable: true,
|
|
41
43
|
configurable: true,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { AssertionError } from "./errors.js";
|
|
2
|
+
export declare const DEFAULT_ASSERTION_HEADER = "X-App-Attest-Assertion";
|
|
3
|
+
export declare const DEFAULT_DEVICE_ID_HEADER = "X-App-Attest-Device-Id";
|
|
4
|
+
export type DeviceKey = {
|
|
5
|
+
publicKeyPem: string;
|
|
6
|
+
signCount: number;
|
|
7
|
+
};
|
|
8
|
+
export type AssertionContext = {
|
|
9
|
+
deviceId: string;
|
|
10
|
+
signCount: number;
|
|
11
|
+
rawBody: Uint8Array;
|
|
12
|
+
};
|
|
13
|
+
export type ExtractAssertionFn = (req: Request) => Promise<{
|
|
14
|
+
assertion: string;
|
|
15
|
+
deviceId: string;
|
|
16
|
+
clientData: Uint8Array;
|
|
17
|
+
}>;
|
|
18
|
+
export type WithAssertionOptions = {
|
|
19
|
+
appId: string;
|
|
20
|
+
developmentEnv?: boolean;
|
|
21
|
+
getDeviceKey: (deviceId: string) => Promise<DeviceKey | null>;
|
|
22
|
+
updateSignCount: (deviceId: string, newSignCount: number) => Promise<void>;
|
|
23
|
+
extractAssertion?: ExtractAssertionFn;
|
|
24
|
+
onError?: (error: AssertionError, req: Request) => Response | Promise<Response>;
|
|
25
|
+
};
|
|
26
|
+
export declare function withAssertion(options: WithAssertionOptions, handler: (req: Request, context: AssertionContext) => Response | Promise<Response>): (req: Request) => Promise<Response>;
|
|
27
|
+
//# sourceMappingURL=with-assertion.d.ts.map
|
|
@@ -0,0 +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,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AACjE,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AAEjE,MAAM,MAAM,SAAS,GAAG;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,UAAU,CAAC;CACrB,CAAC;AAEF,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,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAC9D,eAAe,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3E,gBAAgB,CAAC,EAAE,kBAAkB,CAAC;IACtC,OAAO,CAAC,EAAE,CACR,KAAK,EAAE,cAAc,EACrB,GAAG,EAAE,OAAO,KACT,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACnC,CAAC;AAmCF,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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/with-assertion.ts
|
|
2
|
+
import { verifyAssertion } from "./assertion.js";
|
|
3
|
+
import { AssertionError, AssertionErrorCode } from "./errors.js";
|
|
4
|
+
export const DEFAULT_ASSERTION_HEADER = "X-App-Attest-Assertion";
|
|
5
|
+
export const DEFAULT_DEVICE_ID_HEADER = "X-App-Attest-Device-Id";
|
|
6
|
+
async function defaultExtractAssertion(req) {
|
|
7
|
+
const assertion = req.headers.get(DEFAULT_ASSERTION_HEADER);
|
|
8
|
+
const deviceId = req.headers.get(DEFAULT_DEVICE_ID_HEADER);
|
|
9
|
+
if (!assertion || !deviceId) {
|
|
10
|
+
throw new AssertionError(AssertionErrorCode.INVALID_FORMAT, `Missing ${DEFAULT_ASSERTION_HEADER} or ${DEFAULT_DEVICE_ID_HEADER} header`);
|
|
11
|
+
}
|
|
12
|
+
const clientData = new Uint8Array(await req.arrayBuffer());
|
|
13
|
+
return { assertion, deviceId, clientData };
|
|
14
|
+
}
|
|
15
|
+
function defaultErrorResponse(error) {
|
|
16
|
+
const status = error.code === AssertionErrorCode.INTERNAL_ERROR
|
|
17
|
+
? 500
|
|
18
|
+
: error.code === AssertionErrorCode.INVALID_FORMAT
|
|
19
|
+
? 400
|
|
20
|
+
: 401;
|
|
21
|
+
return new Response(JSON.stringify({ error: error.message, code: error.code }), { status, headers: { "Content-Type": "application/json" } });
|
|
22
|
+
}
|
|
23
|
+
export function withAssertion(options, handler) {
|
|
24
|
+
const appInfo = {
|
|
25
|
+
appId: options.appId,
|
|
26
|
+
developmentEnv: options.developmentEnv ?? false,
|
|
27
|
+
};
|
|
28
|
+
const extract = options.extractAssertion ?? defaultExtractAssertion;
|
|
29
|
+
return async (req) => {
|
|
30
|
+
let deviceId;
|
|
31
|
+
let clientData;
|
|
32
|
+
let newSignCount;
|
|
33
|
+
// Steps 1-4: extract, verify, update sign count
|
|
34
|
+
try {
|
|
35
|
+
const extracted = await extract(req);
|
|
36
|
+
deviceId = extracted.deviceId;
|
|
37
|
+
clientData = extracted.clientData;
|
|
38
|
+
let deviceKey;
|
|
39
|
+
try {
|
|
40
|
+
deviceKey = await options.getDeviceKey(deviceId);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
throw new AssertionError(AssertionErrorCode.INTERNAL_ERROR, "Storage callback failed", { cause: err });
|
|
44
|
+
}
|
|
45
|
+
if (!deviceKey) {
|
|
46
|
+
throw new AssertionError(AssertionErrorCode.DEVICE_NOT_FOUND, "Device not found");
|
|
47
|
+
}
|
|
48
|
+
const result = await verifyAssertion(appInfo, extracted.assertion, clientData, deviceKey.publicKeyPem, deviceKey.signCount);
|
|
49
|
+
try {
|
|
50
|
+
await options.updateSignCount(deviceId, result.signCount);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
throw new AssertionError(AssertionErrorCode.INTERNAL_ERROR, "Failed to update sign count", { cause: err });
|
|
54
|
+
}
|
|
55
|
+
newSignCount = result.signCount;
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
const error = err instanceof AssertionError
|
|
59
|
+
? err
|
|
60
|
+
: new AssertionError(AssertionErrorCode.INTERNAL_ERROR, String(err), {
|
|
61
|
+
cause: err,
|
|
62
|
+
});
|
|
63
|
+
return options.onError?.(error, req) ?? defaultErrorResponse(error);
|
|
64
|
+
}
|
|
65
|
+
// Step 5: handler — outside try/catch, errors bubble up
|
|
66
|
+
return await handler(req, {
|
|
67
|
+
deviceId,
|
|
68
|
+
signCount: newSignCount,
|
|
69
|
+
rawBody: clientData,
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assertion-entry.test.d.ts","sourceRoot":"","sources":["../../src/tests/assertion-entry.test.ts"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assertion.test.d.ts","sourceRoot":"","sources":["../../src/tests/assertion.test.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"assertion.test.d.ts","sourceRoot":"","sources":["../../src/tests/assertion.test.ts"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attestation-entry.test.d.ts","sourceRoot":"","sources":["../../src/tests/attestation-entry.test.ts"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attestation.test.d.ts","sourceRoot":"","sources":["../../src/tests/attestation.test.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"attestation.test.d.ts","sourceRoot":"","sources":["../../src/tests/attestation.test.ts"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authdata.test.d.ts","sourceRoot":"","sources":["../../src/tests/authdata.test.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"authdata.test.d.ts","sourceRoot":"","sources":["../../src/tests/authdata.test.ts"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"certificate.test.d.ts","sourceRoot":"","sources":["../../src/tests/certificate.test.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"certificate.test.d.ts","sourceRoot":"","sources":["../../src/tests/certificate.test.ts"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cose.test.d.ts","sourceRoot":"","sources":["../../src/tests/cose.test.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"cose.test.d.ts","sourceRoot":"","sources":["../../src/tests/cose.test.ts"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"der.test.d.ts","sourceRoot":"","sources":["../../src/tests/der.test.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"der.test.d.ts","sourceRoot":"","sources":["../../src/tests/der.test.ts"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.test.d.ts","sourceRoot":"","sources":["../../src/tests/errors.test.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"errors.test.d.ts","sourceRoot":"","sources":["../../src/tests/errors.test.ts"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.test.d.ts","sourceRoot":"","sources":["../../src/tests/utils.test.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"utils.test.d.ts","sourceRoot":"","sources":["../../src/tests/utils.test.ts"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"with-assertion.test.d.ts","sourceRoot":"","sources":["../../src/tests/with-assertion.test.ts"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bradford-tech/supabase-integrity-attest",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Verify Apple App Attest attestations and assertions using WebCrypto.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
"exports": {
|
|
15
15
|
".": {
|
|
16
16
|
"import": "./esm/mod.js"
|
|
17
|
+
},
|
|
18
|
+
"./assertion": {
|
|
19
|
+
"import": "./esm/assertion.js"
|
|
20
|
+
},
|
|
21
|
+
"./attestation": {
|
|
22
|
+
"import": "./esm/attestation.js"
|
|
17
23
|
}
|
|
18
24
|
},
|
|
19
25
|
"scripts": {
|