@hathbanger/tap-core 1.0.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.
package/README.md ADDED
@@ -0,0 +1,280 @@
1
+ # @hathbanger/tap-core
2
+
3
+ RFC 9421 HTTP Message Signatures for AI agents — the crypto primitives behind [Visa's Trusted Agent Protocol (TAP)](https://developer.visa.com/pages/tap).
4
+
5
+ Generate Ed25519 keypairs, sign HTTP requests with cryptographically verifiable identity, and verify those signatures on the server side. Works in Node.js and any runtime with the Web Crypto API.
6
+
7
+ ```ts
8
+ import { generateKeyPair, signRequest, verifyAgentSignature } from "@hathbanger/tap-core"
9
+
10
+ const keyPair = await generateKeyPair()
11
+
12
+ const headers = await signRequest(keyPair, [
13
+ ["@method", "POST"],
14
+ ["@authority", "api.example.com"],
15
+ ["@path", "/v1/checkout"],
16
+ ], "agent-payer-auth")
17
+
18
+ // headers["signature-input"] → sig1=("@method" "@authority" "@path");created=...;keyid="...";...
19
+ // headers["signature"] → sig1=:base64url:
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ npm install @hathbanger/tap-core
28
+ # or
29
+ pnpm add @hathbanger/tap-core
30
+ ```
31
+
32
+ Requires Node.js 18+. No native dependencies.
33
+
34
+ ---
35
+
36
+ ## Core concepts
37
+
38
+ **Keypair** — an Ed25519 keypair with a derived key ID (JWK SHA-256 thumbprint). The public half is published to a JWKS endpoint; the private half signs requests.
39
+
40
+ **Signature-Input** — an RFC 9421 header that describes _what_ was signed: the ordered list of covered components, a timestamp window, nonce, key ID, algorithm, and an optional application tag.
41
+
42
+ **Signature** — the Ed25519 signature over the canonical signature base, base64url-encoded.
43
+
44
+ **Verification** — the recipient re-derives the signature base from the live request values and verifies the signature against the key fetched from JWKS.
45
+
46
+ ---
47
+
48
+ ## Usage
49
+
50
+ ### 1. Generate a keypair
51
+
52
+ ```ts
53
+ import { generateKeyPair, exportKeyPairToJwk } from "@hathbanger/tap-core"
54
+
55
+ const keyPair = await generateKeyPair()
56
+ // keyPair.keyId → "abc123..." (JWK thumbprint, stable key identifier)
57
+ // keyPair.privateKey → CryptoKey (sign)
58
+ // keyPair.publicKey → CryptoKey (verify)
59
+ // keyPair.jwk → TapJwk (publish to /.well-known/jwks)
60
+
61
+ // Export to JWK for storage / transport
62
+ const { privateJwk, publicJwk } = await exportKeyPairToJwk(keyPair)
63
+ ```
64
+
65
+ ### 2. Sign a request
66
+
67
+ Pass an ordered list of `[componentName, componentValue]` tuples. You control exactly which parts of the request are covered.
68
+
69
+ ```ts
70
+ import { signRequest } from "@hathbanger/tap-core"
71
+
72
+ // Minimal — authority + path only
73
+ const headers = await signRequest(keyPair, [
74
+ ["@authority", "api.example.com"],
75
+ ["@path", "/v1/browse"],
76
+ ], "agent-browser-auth")
77
+
78
+ // Full — method + authority + path + body integrity
79
+ const headers = await signRequest(keyPair, [
80
+ ["@method", "POST"],
81
+ ["@authority", "api.example.com"],
82
+ ["@path", "/v1/checkout"],
83
+ ["content-type", "application/json"],
84
+ ["content-digest", `sha-256=:${bodyDigestBase64}:`],
85
+ ], "agent-payer-auth")
86
+
87
+ // Attach to your fetch call
88
+ await fetch("https://api.example.com/v1/checkout", {
89
+ method: "POST",
90
+ headers: { ...headers, "content-type": "application/json" },
91
+ body: JSON.stringify(payload),
92
+ })
93
+ ```
94
+
95
+ **Covered component names** follow RFC 9421 conventions:
96
+ - Derived: `@method`, `@authority`, `@path`, `@target-uri`, `@scheme`, `@query`
97
+ - HTTP fields: any lowercase header name — `content-type`, `content-digest`, `authorization`, etc.
98
+
99
+ **Tags** are application-defined labels. TAP uses:
100
+ | Tag | When to use |
101
+ |-----|-------------|
102
+ | `agent-browser-auth` | Agent browsing / reading data |
103
+ | `agent-payer-auth` | Agent initiating a payment as payer |
104
+ | `agent-payee-auth` | Agent accepting a payment as payee |
105
+
106
+ ### 3. Verify a signature
107
+
108
+ The verifier reads the covered component list out of `Signature-Input` and reconstructs the base from the values you supply. Pass a map of all components that could appear.
109
+
110
+ ```ts
111
+ import { verifyAgentSignature, fetchPublicKeyFromJwks } from "@hathbanger/tap-core"
112
+
113
+ const result = await verifyAgentSignature(
114
+ {
115
+ "@method": req.method,
116
+ "@authority": req.headers.host,
117
+ "@path": new URL(req.url, "http://x").pathname,
118
+ "content-type": req.headers["content-type"] ?? "",
119
+ },
120
+ req.headers["signature-input"],
121
+ req.headers["signature"],
122
+ (kid) => fetchPublicKeyFromJwks("https://mcp.visa.com/.well-known/jwks", kid)
123
+ )
124
+
125
+ if (!result.valid) {
126
+ return res.status(401).json({ error: result.failureReason })
127
+ }
128
+
129
+ // result.agentKeyId → verified key ID
130
+ // result.tag → "agent-payer-auth" etc.
131
+ ```
132
+
133
+ ### 4. Load a keypair from stored JWK
134
+
135
+ ```ts
136
+ import { loadKeyPairFromJwk } from "@hathbanger/tap-core"
137
+
138
+ const privateJwk = JSON.parse(fs.readFileSync("agent.private.jwk.json", "utf-8"))
139
+ const keyPair = await loadKeyPairFromJwk(privateJwk)
140
+ ```
141
+
142
+ ### 5. Serve a JWKS endpoint
143
+
144
+ ```ts
145
+ import { buildJwksResponse } from "@hathbanger/tap-core"
146
+
147
+ // In your route handler:
148
+ const body = buildJwksResponse([keyPair.jwk])
149
+ res.json(body)
150
+ // → { keys: [{ kty: "OKP", crv: "Ed25519", x: "...", kid: "...", use: "sig" }] }
151
+ ```
152
+
153
+ ---
154
+
155
+ ## API Reference
156
+
157
+ ### Keys
158
+
159
+ | Function | Description |
160
+ |----------|-------------|
161
+ | `generateKeyPair()` | Generate an Ed25519 `TapKeyPair` with derived key ID |
162
+ | `exportKeyPairToJwk(pair)` | Export keypair to `{ privateJwk, publicJwk }` |
163
+ | `loadKeyPairFromJwk(privateJwk)` | Reconstruct a `TapKeyPair` from a stored private JWK |
164
+ | `importPrivateKey(jwk)` | Import a raw JWK → `CryptoKey` for signing |
165
+ | `importPublicKey(jwk)` | Import a raw JWK → `CryptoKey` for verification |
166
+
167
+ ### Signing
168
+
169
+ | Function | Description |
170
+ |----------|-------------|
171
+ | `signRequest(keyPair, components, tag)` | Sign an HTTP request — returns `{ "signature-input", "signature" }` |
172
+ | `signPayload(keyPair, payload)` | Sign an arbitrary JSON payload — returns base64url signature |
173
+ | `buildSignatureBase(components, params)` | Low-level: construct the RFC 9421 signature base string |
174
+ | `buildSignatureInputParams(componentNames, params)` | Low-level: serialize the Inner List + params string |
175
+
176
+ ### Verification
177
+
178
+ | Function | Description |
179
+ |----------|-------------|
180
+ | `verifyAgentSignature(componentValues, sigInput, sig, fetchKey)` | Verify an RFC 9421 agent signature |
181
+ | `parseSignatureInput(header)` | Parse `Signature-Input` → `{ params, components, raw }` |
182
+ | `parseSignatureHeader(header)` | Extract raw base64url signature from `Signature` header |
183
+ | `verifyConsumerRecognition(obj, nonce, fetchKey)` | Verify a TAP Consumer Recognition Object |
184
+ | `verifyPaymentContainer(obj, nonce, fetchKey)` | Verify a TAP Payment Container |
185
+ | `verifyAgentPayeeObject(payment, fetchKey)` | Verify a TAP A2A payment credential |
186
+
187
+ ### JWKS
188
+
189
+ | Function | Description |
190
+ |----------|-------------|
191
+ | `fetchPublicKeyFromJwks(url, keyId)` | Fetch a key by ID from a JWKS endpoint (5-min cache) |
192
+ | `buildJwksResponse(publicJwks)` | Build a `{ keys: [...] }` JWKS response body |
193
+
194
+ ### Nonce / Timestamps
195
+
196
+ | Function | Description |
197
+ |----------|-------------|
198
+ | `generateNonce()` | Generate a cryptographically random base64url nonce |
199
+ | `makeTimestamps()` | Return `{ created, expires }` with an 8-minute window |
200
+ | `validateTimestamps(created, expires)` | Validate timestamp window against current time |
201
+ | `nowSeconds()` | Current Unix timestamp in seconds |
202
+
203
+ ### TAP Tokens (Consumer Identity)
204
+
205
+ | Function | Description |
206
+ |----------|-------------|
207
+ | `createIdToken(keyPair, claims)` | Create an EdDSA JWT for a Consumer Recognition Object |
208
+ | `verifyIdToken(token, publicJwk, audience)` | Verify and decode a TAP ID token |
209
+ | `hashPii(value)` | SHA-256 hash a PII value (email / phone) |
210
+ | `maskEmail(email)` | Mask email to `a***@domain.com` |
211
+ | `maskPhone(phone)` | Mask phone to `***-***-1234` |
212
+
213
+ ---
214
+
215
+ ## Types
216
+
217
+ ```ts
218
+ import type {
219
+ TapKeyPair, // { keyId, privateKey, publicKey, jwk }
220
+ TapJwk, // JWK with optional kid, use, alg
221
+ TapSignedHeaders, // { "signature-input": string; signature: string }
222
+ SignatureInputParams,// { tag, keyId, alg, created, expires, nonce }
223
+ SignatureTag, // "agent-browser-auth" | "agent-payer-auth" | "agent-payee-auth"
224
+ VerificationResult, // { valid, agentKeyId?, tag?, failureReason? }
225
+ } from "@hathbanger/tap-core"
226
+ ```
227
+
228
+ ---
229
+
230
+ ## Express middleware example
231
+
232
+ ```ts
233
+ import express from "express"
234
+ import { verifyAgentSignature, fetchPublicKeyFromJwks } from "@hathbanger/tap-core"
235
+
236
+ const JWKS_URL = process.env.TAP_JWKS_URL ?? "http://localhost:4456/.well-known/jwks"
237
+
238
+ async function tapAuth(req, res, next) {
239
+ const sigInput = req.headers["signature-input"]
240
+ const sig = req.headers["signature"]
241
+
242
+ if (!sigInput || !sig) return next() // not a TAP request
243
+
244
+ const result = await verifyAgentSignature(
245
+ {
246
+ "@method": req.method,
247
+ "@authority": req.hostname,
248
+ "@path": req.path,
249
+ },
250
+ sigInput,
251
+ sig,
252
+ (kid) => fetchPublicKeyFromJwks(JWKS_URL, kid)
253
+ )
254
+
255
+ if (!result.valid) {
256
+ return res.status(401).json({ error: result.failureReason })
257
+ }
258
+
259
+ req.tapAgent = { keyId: result.agentKeyId, tag: result.tag }
260
+ next()
261
+ }
262
+ ```
263
+
264
+ ---
265
+
266
+ ## Spec compliance
267
+
268
+ This library implements the signature and verification mechanics from [RFC 9421 — HTTP Message Signatures](https://www.rfc-editor.org/rfc/rfc9421). Key points:
269
+
270
+ - **Signature base** is constructed as specified in §2.5, with each covered component on its own line followed by `"@signature-params"`.
271
+ - **Inner List** serialization (§4.1) covers any ordered set of derived components (`@method`, `@authority`, `@path`, etc.) and HTTP field names.
272
+ - **Algorithm**: Ed25519 (`alg="ed25519"`) using Web Crypto `subtle.sign/verify`.
273
+ - **Key IDs**: derived as a JWK SHA-256 thumbprint (RFC 7638).
274
+ - **Timestamp window**: 8 minutes (`created` → `expires`), enforced on both sign and verify.
275
+
276
+ ---
277
+
278
+ ## License
279
+
280
+ MIT
@@ -0,0 +1,13 @@
1
+ import type { ConsumerRecognitionObject, ContextualData, TapKeyPair } from "./types.js";
2
+ export interface ConsumerProfile {
3
+ sub: string;
4
+ email: string;
5
+ phone?: string;
6
+ audience: string;
7
+ }
8
+ /**
9
+ * Builds a ConsumerRecognitionObject for inclusion in browsing or checkout requests.
10
+ * The nonce must match the agent recognition signature nonce.
11
+ */
12
+ export declare function buildConsumerRecognition(keyPair: TapKeyPair, nonce: string, consumer: ConsumerProfile, contextualData?: ContextualData): Promise<ConsumerRecognitionObject>;
13
+ //# sourceMappingURL=agent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,yBAAyB,EACzB,cAAc,EACd,UAAU,EACX,MAAM,YAAY,CAAA;AAEnB,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,UAAU,EACnB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,eAAe,EACzB,cAAc,CAAC,EAAE,cAAc,GAC9B,OAAO,CAAC,yBAAyB,CAAC,CA4BpC"}
package/dist/agent.js ADDED
@@ -0,0 +1,32 @@
1
+ import { signPayload } from "./sign.js";
2
+ import { createIdToken, hashPii, maskEmail, maskPhone } from "./tokens.js";
3
+ /**
4
+ * Builds a ConsumerRecognitionObject for inclusion in browsing or checkout requests.
5
+ * The nonce must match the agent recognition signature nonce.
6
+ */
7
+ export async function buildConsumerRecognition(keyPair, nonce, consumer, contextualData) {
8
+ const [hashedEmail, hashedPhone] = await Promise.all([
9
+ hashPii(consumer.email),
10
+ consumer.phone ? hashPii(consumer.phone) : Promise.resolve(undefined),
11
+ ]);
12
+ const idToken = await createIdToken(keyPair, {
13
+ sub: consumer.sub,
14
+ aud: consumer.audience,
15
+ email: hashedEmail,
16
+ phone_number: hashedPhone,
17
+ email_verified: true,
18
+ phone_number_verified: !!consumer.phone,
19
+ email_mask: maskEmail(consumer.email),
20
+ phone_number_mask: consumer.phone ? maskPhone(consumer.phone) : undefined,
21
+ });
22
+ const unsigned = {
23
+ nonce,
24
+ idToken,
25
+ contextualData: contextualData ?? {},
26
+ kid: keyPair.keyId,
27
+ alg: "ed25519",
28
+ };
29
+ const signature = await signPayload(keyPair, unsigned);
30
+ return { ...unsigned, signature };
31
+ }
32
+ //# sourceMappingURL=agent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.js","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAc1E;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,OAAmB,EACnB,KAAa,EACb,QAAyB,EACzB,cAA+B;IAE/B,MAAM,CAAC,WAAW,EAAE,WAAW,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACnD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;QACvB,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC;KACtE,CAAC,CAAA;IAEF,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,OAAO,EAAE;QAC3C,GAAG,EAAE,QAAQ,CAAC,GAAG;QACjB,GAAG,EAAE,QAAQ,CAAC,QAAQ;QACtB,KAAK,EAAE,WAAW;QAClB,YAAY,EAAE,WAAW;QACzB,cAAc,EAAE,IAAI;QACpB,qBAAqB,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK;QACvC,UAAU,EAAE,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC;QACrC,iBAAiB,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS;KAC1E,CAAC,CAAA;IAEF,MAAM,QAAQ,GAAiD;QAC7D,KAAK;QACL,OAAO;QACP,cAAc,EAAE,cAAc,IAAI,EAAE;QACpC,GAAG,EAAE,OAAO,CAAC,KAAK;QAClB,GAAG,EAAE,SAAS;KACf,CAAA;IAED,MAAM,SAAS,GAAG,MAAM,WAAW,CAAC,OAAO,EAAE,QAAmC,CAAC,CAAA;IAEjF,OAAO,EAAE,GAAG,QAAQ,EAAE,SAAS,EAAE,CAAA;AACnC,CAAC"}
@@ -0,0 +1,10 @@
1
+ export * from "./types.js";
2
+ export * from "./keys.js";
3
+ export * from "./nonce.js";
4
+ export * from "./sign.js";
5
+ export * from "./verify.js";
6
+ export * from "./tokens.js";
7
+ export * from "./payment.js";
8
+ export * from "./agent.js";
9
+ export * from "./jwks.js";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export * from "./types.js";
2
+ export * from "./keys.js";
3
+ export * from "./nonce.js";
4
+ export * from "./sign.js";
5
+ export * from "./verify.js";
6
+ export * from "./tokens.js";
7
+ export * from "./payment.js";
8
+ export * from "./agent.js";
9
+ export * from "./jwks.js";
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA"}
package/dist/jwks.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { JwksResponse, TapJwk } from "./types.js";
2
+ /**
3
+ * Fetches a JWK by keyId from a JWKS endpoint.
4
+ * Used by merchants to retrieve the agent's public key for verification.
5
+ *
6
+ * In production: https://mcp.visa.com/.well-known/jwks
7
+ * In development: http://localhost:4456/.well-known/jwks (tap-cli dev server)
8
+ */
9
+ export declare function fetchPublicKeyFromJwks(jwksUrl: string, keyId: string, cacheTtlMs?: number): Promise<TapJwk | null>;
10
+ /**
11
+ * Creates a JWKS response body from an array of public JWKs.
12
+ */
13
+ export declare function buildJwksResponse(publicJwks: TapJwk[]): JwksResponse;
14
+ //# sourceMappingURL=jwks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwks.d.ts","sourceRoot":"","sources":["../src/jwks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAEtD;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,UAAU,SAAU,GACnB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAmBxB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,YAAY,CAEpE"}
package/dist/jwks.js ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Fetches a JWK by keyId from a JWKS endpoint.
3
+ * Used by merchants to retrieve the agent's public key for verification.
4
+ *
5
+ * In production: https://mcp.visa.com/.well-known/jwks
6
+ * In development: http://localhost:4456/.well-known/jwks (tap-cli dev server)
7
+ */
8
+ export async function fetchPublicKeyFromJwks(jwksUrl, keyId, cacheTtlMs = 300_000) {
9
+ const cacheKey = `${jwksUrl}:${keyId}`;
10
+ const cached = jwksCache.get(cacheKey);
11
+ if (cached && Date.now() - cached.fetchedAt < cacheTtlMs) {
12
+ return cached.jwk;
13
+ }
14
+ const res = await fetch(jwksUrl);
15
+ if (!res.ok)
16
+ throw new Error(`JWKS fetch failed: ${res.status}`);
17
+ const body = await res.json();
18
+ const match = body.keys.find((k) => k.kid === keyId) ?? null;
19
+ if (match) {
20
+ jwksCache.set(cacheKey, { jwk: match, fetchedAt: Date.now() });
21
+ }
22
+ return match;
23
+ }
24
+ /**
25
+ * Creates a JWKS response body from an array of public JWKs.
26
+ */
27
+ export function buildJwksResponse(publicJwks) {
28
+ return { keys: publicJwks };
29
+ }
30
+ const jwksCache = new Map();
31
+ //# sourceMappingURL=jwks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwks.js","sourceRoot":"","sources":["../src/jwks.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,OAAe,EACf,KAAa,EACb,UAAU,GAAG,OAAO;IAEpB,MAAM,QAAQ,GAAG,GAAG,OAAO,IAAI,KAAK,EAAE,CAAA;IACtC,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IAEtC,IAAI,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,SAAS,GAAG,UAAU,EAAE,CAAC;QACzD,OAAO,MAAM,CAAC,GAAG,CAAA;IACnB,CAAC;IAED,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,CAAA;IAChC,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,GAAG,CAAC,MAAM,EAAE,CAAC,CAAA;IAEhE,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAkB,CAAA;IAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,KAAK,CAAC,IAAI,IAAI,CAAA;IAE5D,IAAI,KAAK,EAAE,CAAC;QACV,SAAS,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;IAChE,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAAoB;IACpD,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,CAAA;AAC7B,CAAC;AAED,MAAM,SAAS,GAAG,IAAI,GAAG,EAA8C,CAAA"}
package/dist/keys.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { TapJwk, TapKeyPair } from "./types.js";
2
+ export declare function generateKeyPair(): Promise<TapKeyPair>;
3
+ export declare function importPrivateKey(jwk: TapJwk): Promise<CryptoKey>;
4
+ export declare function importPublicKey(jwk: TapJwk): Promise<CryptoKey>;
5
+ export declare function exportKeyPairToJwk(pair: TapKeyPair): Promise<{
6
+ privateJwk: TapJwk;
7
+ publicJwk: TapJwk;
8
+ }>;
9
+ export declare function loadKeyPairFromJwk(privateJwk: TapJwk): Promise<TapKeyPair>;
10
+ export declare function base64url(buffer: ArrayBuffer | Uint8Array<ArrayBufferLike>): string;
11
+ export declare function base64urlDecode(str: string): Buffer;
12
+ //# sourceMappingURL=keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAIpD,wBAAsB,eAAe,IAAI,OAAO,CAAC,UAAU,CAAC,CAiB3D;AAED,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAEtE;AAED,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAErE;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAO7G;AAED,wBAAsB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAchF;AAaD,wBAAgB,SAAS,CAAC,MAAM,EAAE,WAAW,GAAG,UAAU,CAAC,eAAe,CAAC,GAAG,MAAM,CAOnF;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAInD"}
package/dist/keys.js ADDED
@@ -0,0 +1,65 @@
1
+ import { webcrypto } from "node:crypto";
2
+ const subtle = webcrypto.subtle;
3
+ export async function generateKeyPair() {
4
+ const result = await subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"]);
5
+ const { privateKey, publicKey } = result;
6
+ const publicJwk = await subtle.exportKey("jwk", publicKey);
7
+ const keyId = await deriveKeyId(publicJwk);
8
+ return {
9
+ keyId,
10
+ privateKey,
11
+ publicKey,
12
+ jwk: { ...publicJwk, kid: keyId, use: "sig", alg: "EdDSA" },
13
+ };
14
+ }
15
+ export async function importPrivateKey(jwk) {
16
+ return subtle.importKey("jwk", jwk, { name: "Ed25519" }, false, ["sign"]);
17
+ }
18
+ export async function importPublicKey(jwk) {
19
+ return subtle.importKey("jwk", jwk, { name: "Ed25519" }, false, ["verify"]);
20
+ }
21
+ export async function exportKeyPairToJwk(pair) {
22
+ const privateJwk = await subtle.exportKey("jwk", pair.privateKey);
23
+ const publicJwk = await subtle.exportKey("jwk", pair.publicKey);
24
+ return {
25
+ privateJwk: { ...privateJwk, kid: pair.keyId },
26
+ publicJwk: { ...publicJwk, kid: pair.keyId, use: "sig" },
27
+ };
28
+ }
29
+ export async function loadKeyPairFromJwk(privateJwk) {
30
+ const privateKey = await importPrivateKey(privateJwk);
31
+ const publicJwk = {
32
+ kty: privateJwk.kty,
33
+ crv: privateJwk.crv,
34
+ x: privateJwk.x,
35
+ kid: privateJwk.kid,
36
+ use: "sig",
37
+ };
38
+ const publicKey = await importPublicKey(publicJwk);
39
+ const keyId = privateJwk.kid ?? await deriveKeyId(publicJwk);
40
+ return { keyId, privateKey, publicKey, jwk: { ...publicJwk, kid: keyId } };
41
+ }
42
+ async function deriveKeyId(publicJwk) {
43
+ const thumbprintInput = JSON.stringify({
44
+ crv: publicJwk.crv,
45
+ kty: publicJwk.kty,
46
+ x: publicJwk.x,
47
+ });
48
+ const encoded = new TextEncoder().encode(thumbprintInput);
49
+ const hash = await subtle.digest("SHA-256", encoded);
50
+ return base64url(hash);
51
+ }
52
+ export function base64url(buffer) {
53
+ const bytes = buffer instanceof ArrayBuffer ? new Uint8Array(buffer) : buffer;
54
+ return Buffer.from(bytes)
55
+ .toString("base64")
56
+ .replace(/\+/g, "-")
57
+ .replace(/\//g, "_")
58
+ .replace(/=/g, "");
59
+ }
60
+ export function base64urlDecode(str) {
61
+ const padded = str.replace(/-/g, "+").replace(/_/g, "/");
62
+ const pad = padded.length % 4;
63
+ return Buffer.from(pad ? padded + "=".repeat(4 - pad) : padded, "base64");
64
+ }
65
+ //# sourceMappingURL=keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export declare const NONCE_WINDOW_SECONDS = 480;
2
+ export declare const SIGNATURE_WINDOW_SECONDS = 480;
3
+ export declare function generateNonce(): string;
4
+ export declare function nowSeconds(): number;
5
+ export declare function makeTimestamps(): {
6
+ created: number;
7
+ expires: number;
8
+ };
9
+ export declare function validateTimestamps(created: number, expires: number): boolean;
10
+ //# sourceMappingURL=nonce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nonce.d.ts","sourceRoot":"","sources":["../src/nonce.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB,MAAM,CAAA;AACvC,eAAO,MAAM,wBAAwB,MAAM,CAAA;AAE3C,wBAAgB,aAAa,IAAI,MAAM,CAGtC;AAED,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAED,wBAAgB,cAAc,IAAI;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAGrE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAM5E"}
package/dist/nonce.js ADDED
@@ -0,0 +1,26 @@
1
+ import { webcrypto } from "node:crypto";
2
+ import { base64url } from "./keys.js";
3
+ export const NONCE_WINDOW_SECONDS = 480; // 8 minutes per spec
4
+ export const SIGNATURE_WINDOW_SECONDS = 480;
5
+ export function generateNonce() {
6
+ const bytes = webcrypto.getRandomValues(new Uint8Array(24));
7
+ return base64url(bytes.buffer);
8
+ }
9
+ export function nowSeconds() {
10
+ return Math.floor(Date.now() / 1000);
11
+ }
12
+ export function makeTimestamps() {
13
+ const created = nowSeconds();
14
+ return { created, expires: created + SIGNATURE_WINDOW_SECONDS };
15
+ }
16
+ export function validateTimestamps(created, expires) {
17
+ const now = nowSeconds();
18
+ if (created > now)
19
+ return false;
20
+ if (expires < now)
21
+ return false;
22
+ if (expires - created > SIGNATURE_WINDOW_SECONDS)
23
+ return false;
24
+ return true;
25
+ }
26
+ //# sourceMappingURL=nonce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nonce.js","sourceRoot":"","sources":["../src/nonce.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAErC,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAG,CAAA,CAAC,qBAAqB;AAC7D,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAG,CAAA;AAE3C,MAAM,UAAU,aAAa;IAC3B,MAAM,KAAK,GAAG,SAAS,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,CAAA;IAC3D,OAAO,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;AAChC,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;AACtC,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,MAAM,OAAO,GAAG,UAAU,EAAE,CAAA;IAC5B,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,wBAAwB,EAAE,CAAA;AACjE,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,OAAe,EAAE,OAAe;IACjE,MAAM,GAAG,GAAG,UAAU,EAAE,CAAA;IACxB,IAAI,OAAO,GAAG,GAAG;QAAE,OAAO,KAAK,CAAA;IAC/B,IAAI,OAAO,GAAG,GAAG;QAAE,OAAO,KAAK,CAAA;IAC/B,IAAI,OAAO,GAAG,OAAO,GAAG,wBAAwB;QAAE,OAAO,KAAK,CAAA;IAC9D,OAAO,IAAI,CAAA;AACb,CAAC"}
@@ -0,0 +1,19 @@
1
+ import type { Address, AgentPayeeObject, CardMetadata, EncryptedPaymentPayload, PaymentContainerObject, TapKeyPair, TapPaymentRequest } from "./types.js";
2
+ /**
3
+ * Builds a PaymentContainerObject for checkout requests.
4
+ *
5
+ * In production this would contain an encrypted Visa payment token.
6
+ * In demo mode it contains a mock token with realistic structure.
7
+ */
8
+ export declare function buildPaymentContainer(keyPair: TapKeyPair, nonce: string, payload: EncryptedPaymentPayload, cardMetadata: CardMetadata): Promise<PaymentContainerObject>;
9
+ /**
10
+ * Creates a mock payment token for demo purposes.
11
+ * Simulates the encrypted token Visa would issue in production.
12
+ */
13
+ export declare function createMockPaymentToken(lastFour: string, cardholderName: string, billingAddress: Address, shippingAddress: Address, email: string): Promise<{
14
+ payload: EncryptedPaymentPayload;
15
+ cardMetadata: CardMetadata;
16
+ }>;
17
+ export declare function buildTapPaymentRequest(keyPair: TapKeyPair, amount: number, currency: string, description: string): TapPaymentRequest;
18
+ export declare function buildAgentPayeeObject(keyPair: TapKeyPair, paymentRequest: TapPaymentRequest): Promise<AgentPayeeObject>;
19
+ //# sourceMappingURL=payment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment.d.ts","sourceRoot":"","sources":["../src/payment.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,OAAO,EACP,gBAAgB,EAChB,YAAY,EACZ,uBAAuB,EACvB,sBAAsB,EACtB,UAAU,EACV,iBAAiB,EAClB,MAAM,YAAY,CAAA;AAEnB;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,UAAU,EACnB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,uBAAuB,EAChC,YAAY,EAAE,YAAY,GACzB,OAAO,CAAC,sBAAsB,CAAC,CAYjC;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,OAAO,EACvB,eAAe,EAAE,OAAO,EACxB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC;IAAE,OAAO,EAAE,uBAAuB,CAAC;IAAC,YAAY,EAAE,YAAY,CAAA;CAAE,CAAC,CA2B3E;AAED,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,UAAU,EACnB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,GAClB,iBAAiB,CASnB;AAED,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,UAAU,EACnB,cAAc,EAAE,iBAAiB,GAChC,OAAO,CAAC,gBAAgB,CAAC,CAgB3B"}