@vaam-apps/vpay-sdk 0.2.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/LICENSE +21 -0
- package/README.md +732 -0
- package/dist/auth.d.ts +180 -0
- package/dist/auth.js +297 -0
- package/dist/client.d.ts +75 -0
- package/dist/client.js +79 -0
- package/dist/errors.d.ts +91 -0
- package/dist/errors.js +112 -0
- package/dist/form.d.ts +47 -0
- package/dist/form.js +127 -0
- package/dist/http.d.ts +33 -0
- package/dist/http.js +115 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +14 -0
- package/dist/resources/account-holders.d.ts +36 -0
- package/dist/resources/account-holders.js +46 -0
- package/dist/resources/balance.d.ts +8 -0
- package/dist/resources/balance.js +10 -0
- package/dist/resources/checkout-sessions.d.ts +55 -0
- package/dist/resources/checkout-sessions.js +127 -0
- package/dist/resources/customers.d.ts +97 -0
- package/dist/resources/customers.js +157 -0
- package/dist/resources/events.d.ts +8 -0
- package/dist/resources/events.js +10 -0
- package/dist/resources/invoices.d.ts +193 -0
- package/dist/resources/invoices.js +275 -0
- package/dist/resources/payment-intents.d.ts +12 -0
- package/dist/resources/payment-intents.js +44 -0
- package/dist/resources/refunds.d.ts +92 -0
- package/dist/resources/refunds.js +106 -0
- package/dist/stripe-auth.d.ts +177 -0
- package/dist/stripe-auth.js +192 -0
- package/dist/types.d.ts +1268 -0
- package/dist/types.js +48 -0
- package/dist/validate.d.ts +16 -0
- package/dist/validate.js +20 -0
- package/dist/version.d.ts +12 -0
- package/dist/version.js +12 -0
- package/dist/webhooks.d.ts +22 -0
- package/dist/webhooks.js +94 -0
- package/package.json +63 -0
- package/scripts/mint-assertion.mjs +73 -0
package/dist/types.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire types for vpay's `/v1` resource contract
|
|
3
|
+
* (docs/api/README.md, docs/flows/merchant-auth.md's "Objects" table).
|
|
4
|
+
*/
|
|
5
|
+
/** Narrows a {@link PaymentIntent}-shaped value out of `event.data.object`. */
|
|
6
|
+
export function isPaymentIntentEvent(event) {
|
|
7
|
+
return event.type.startsWith("payment_intent.");
|
|
8
|
+
}
|
|
9
|
+
/** Narrows a {@link Refund}-shaped value out of `event.data.object`. */
|
|
10
|
+
export function isRefundEvent(event) {
|
|
11
|
+
return event.type.startsWith("charge.refund");
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Narrows a {@link CheckoutSession}-shaped value out of `event.data.object`.
|
|
15
|
+
*
|
|
16
|
+
* Matched on the `checkout.session.` prefix rather than on the one literal
|
|
17
|
+
* type vpay emits today, exactly as the two guards above are: a later
|
|
18
|
+
* `checkout.session.*` type carries the same object, and a guard that had to
|
|
19
|
+
* be edited for each one is a guard that will be out of date before the
|
|
20
|
+
* `switch` that calls it is.
|
|
21
|
+
*
|
|
22
|
+
* The session on an event **never** carries `client_secret`, and its `url` is
|
|
23
|
+
* always `null`: both would put a live payer credential in a body that is
|
|
24
|
+
* stored, delivered at-least-once and replayed
|
|
25
|
+
* (`vpay_api::model::CheckoutSessionObject::expired_snapshot`). So
|
|
26
|
+
* {@link CheckoutSession.url} being `null` here does not mean the session was
|
|
27
|
+
* embedded — read {@link CheckoutSession.ui_mode} for that.
|
|
28
|
+
*/
|
|
29
|
+
export function isCheckoutSessionEvent(event) {
|
|
30
|
+
return event.type.startsWith("checkout.session.");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Narrows an {@link Invoice}-shaped value out of `event.data.object`, for one
|
|
34
|
+
* of the four `invoice.*` events (S4b).
|
|
35
|
+
*
|
|
36
|
+
* Matched on the `invoice.` prefix rather than on the four literals, as every
|
|
37
|
+
* guard above is.
|
|
38
|
+
*
|
|
39
|
+
* **The invoice on an event carries no lines.** `data.object.lines.data` is
|
|
40
|
+
* empty on every `invoice.*` body — the event is rendered inside the
|
|
41
|
+
* transition's own transaction, where the lines are not read — so
|
|
42
|
+
* {@link Invoice.lines} here is an empty {@link List}, and that is not the
|
|
43
|
+
* same statement as "this invoice has no lines". A handler that needs them
|
|
44
|
+
* calls `client.invoices.retrieve`, whose response always carries them.
|
|
45
|
+
*/
|
|
46
|
+
export function isInvoiceEvent(event) {
|
|
47
|
+
return event.type.startsWith("invoice.");
|
|
48
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared request-shape validation, run before anything reaches the wire.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Mirrors `@vpay/api-client`'s `formatAmount` integer check
|
|
6
|
+
* (frontends/packages/api-client/src/index.ts) and
|
|
7
|
+
* docs/flows/money.md's rule that money is an integer count of a currency's
|
|
8
|
+
* minor unit — no floating point in the money path, ever.
|
|
9
|
+
*
|
|
10
|
+
* `Number.isInteger` alone is not that rule: it accepts `-1`, `1e21` (which
|
|
11
|
+
* `String()` renders as `1e+21`, not a decimal integer) and integers past
|
|
12
|
+
* `Number.MAX_SAFE_INTEGER`, where the value the caller wrote and the value
|
|
13
|
+
* JavaScript holds have already diverged. An amount must be a non-negative
|
|
14
|
+
* **safe** integer or nothing goes to the wire.
|
|
15
|
+
*/
|
|
16
|
+
export declare function assertIntegerAmount(amount: number, field?: string): void;
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared request-shape validation, run before anything reaches the wire.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Mirrors `@vpay/api-client`'s `formatAmount` integer check
|
|
6
|
+
* (frontends/packages/api-client/src/index.ts) and
|
|
7
|
+
* docs/flows/money.md's rule that money is an integer count of a currency's
|
|
8
|
+
* minor unit — no floating point in the money path, ever.
|
|
9
|
+
*
|
|
10
|
+
* `Number.isInteger` alone is not that rule: it accepts `-1`, `1e21` (which
|
|
11
|
+
* `String()` renders as `1e+21`, not a decimal integer) and integers past
|
|
12
|
+
* `Number.MAX_SAFE_INTEGER`, where the value the caller wrote and the value
|
|
13
|
+
* JavaScript holds have already diverged. An amount must be a non-negative
|
|
14
|
+
* **safe** integer or nothing goes to the wire.
|
|
15
|
+
*/
|
|
16
|
+
export function assertIntegerAmount(amount, field = "amount") {
|
|
17
|
+
if (!Number.isSafeInteger(amount) || amount < 0) {
|
|
18
|
+
throw new TypeError(`${field} must be a non-negative safe integer in minor units, got ${amount}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SDK's own version, sent as the `vpay-sdk-node/<version>` User-Agent
|
|
3
|
+
* suffix (docs/flows/merchant-auth.md, "Headers" table).
|
|
4
|
+
*
|
|
5
|
+
* Kept as a hand-written constant rather than read from `package.json` at
|
|
6
|
+
* runtime: this package builds to plain Node ESM via `tsc`
|
|
7
|
+
* (`moduleResolution: "NodeNext"`), and importing JSON across that boundary
|
|
8
|
+
* needs an import attribute whose support has shifted between Node
|
|
9
|
+
* releases. A hand-written constant has no such landmine — it just needs to
|
|
10
|
+
* be bumped alongside `package.json`'s own `version` field.
|
|
11
|
+
*/
|
|
12
|
+
export declare const SDK_VERSION = "0.2.1";
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SDK's own version, sent as the `vpay-sdk-node/<version>` User-Agent
|
|
3
|
+
* suffix (docs/flows/merchant-auth.md, "Headers" table).
|
|
4
|
+
*
|
|
5
|
+
* Kept as a hand-written constant rather than read from `package.json` at
|
|
6
|
+
* runtime: this package builds to plain Node ESM via `tsc`
|
|
7
|
+
* (`moduleResolution: "NodeNext"`), and importing JSON across that boundary
|
|
8
|
+
* needs an import attribute whose support has shifted between Node
|
|
9
|
+
* releases. A hand-written constant has no such landmine — it just needs to
|
|
10
|
+
* be bumped alongside `package.json`'s own `version` field.
|
|
11
|
+
*/
|
|
12
|
+
export const SDK_VERSION = "0.2.1"; // x-release-please-version
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Event } from "./types.js";
|
|
2
|
+
export interface VerifyWebhookOptions {
|
|
3
|
+
/** The exact bytes vpay sent. A parsed-and-reserialised body breaks the HMAC. */
|
|
4
|
+
rawBody: string | Buffer;
|
|
5
|
+
/** The `Vpay-Signature` header value: `t=<unix seconds>,v1=<hex>[,v1=<hex>...]`. */
|
|
6
|
+
signatureHeader: string;
|
|
7
|
+
/** The webhook endpoint's signing secret. */
|
|
8
|
+
secret: string;
|
|
9
|
+
/** Default 300 (5 minutes), per docs/flows/webhooks.md. */
|
|
10
|
+
toleranceSeconds?: number | undefined;
|
|
11
|
+
/** Unix seconds. Defaults to `Date.now() / 1000`, injectable for tests. */
|
|
12
|
+
now?: number | undefined;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Verifies a `Vpay-Signature` header against `rawBody` and returns the
|
|
16
|
+
* parsed {@link Event} only once verification succeeds.
|
|
17
|
+
*
|
|
18
|
+
* @throws {WebhookSignatureError} on a malformed header, a timestamp outside
|
|
19
|
+
* `toleranceSeconds`, no matching signature, or a body that is not valid
|
|
20
|
+
* JSON once the signature checks out.
|
|
21
|
+
*/
|
|
22
|
+
export declare function verifyWebhook(options: VerifyWebhookOptions): Event;
|
package/dist/webhooks.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifier for vpay's outbound webhook signature scheme — Stripe's own,
|
|
3
|
+
* copied exactly (docs/flows/webhooks.md, docs/flows/merchant-auth.md's
|
|
4
|
+
* "Webhook verification" section). Productises the hand-rolled version in
|
|
5
|
+
* `examples/webhook-receiver/index.mjs`.
|
|
6
|
+
*/
|
|
7
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
8
|
+
import { WebhookSignatureError } from "./errors.js";
|
|
9
|
+
const DEFAULT_TOLERANCE_SECONDS = 300;
|
|
10
|
+
/**
|
|
11
|
+
* The only `t` this verifier accepts: one or more decimal digits, nothing
|
|
12
|
+
* else. `Number()` is far more permissive — it reads `0x65566CC0` as
|
|
13
|
+
* 1700000000, `1700000000.0` as 1700000000 and `""` as 0 — which would turn
|
|
14
|
+
* a malformed header into either a *tolerance* failure or a signature that is
|
|
15
|
+
* computed over bytes the sender never signed.
|
|
16
|
+
*/
|
|
17
|
+
const TIMESTAMP_PATTERN = /^\d+$/;
|
|
18
|
+
function parseSignatureHeader(header) {
|
|
19
|
+
let timestampText;
|
|
20
|
+
const signatures = [];
|
|
21
|
+
for (const part of header.split(",")) {
|
|
22
|
+
const separatorIndex = part.indexOf("=");
|
|
23
|
+
if (separatorIndex === -1) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const key = part.slice(0, separatorIndex).trim();
|
|
27
|
+
const value = part.slice(separatorIndex + 1).trim();
|
|
28
|
+
if (key === "t") {
|
|
29
|
+
timestampText = value;
|
|
30
|
+
}
|
|
31
|
+
else if (key === "v1" && value.length > 0) {
|
|
32
|
+
signatures.push(value);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (timestampText === undefined || !TIMESTAMP_PATTERN.test(timestampText)) {
|
|
36
|
+
throw new WebhookSignatureError('malformed Vpay-Signature header: missing or invalid "t"');
|
|
37
|
+
}
|
|
38
|
+
const timestamp = Number(timestampText);
|
|
39
|
+
if (!Number.isFinite(timestamp)) {
|
|
40
|
+
throw new WebhookSignatureError('malformed Vpay-Signature header: missing or invalid "t"');
|
|
41
|
+
}
|
|
42
|
+
if (signatures.length === 0) {
|
|
43
|
+
throw new WebhookSignatureError('malformed Vpay-Signature header: no "v1" signature present');
|
|
44
|
+
}
|
|
45
|
+
return { timestampText, timestamp, signatures };
|
|
46
|
+
}
|
|
47
|
+
function constantTimeHexEquals(a, b) {
|
|
48
|
+
const bufferA = Buffer.from(a, "utf8");
|
|
49
|
+
const bufferB = Buffer.from(b, "utf8");
|
|
50
|
+
// Length check first: `timingSafeEqual` throws on a length mismatch
|
|
51
|
+
// rather than returning false.
|
|
52
|
+
return bufferA.length === bufferB.length && timingSafeEqual(bufferA, bufferB);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Verifies a `Vpay-Signature` header against `rawBody` and returns the
|
|
56
|
+
* parsed {@link Event} only once verification succeeds.
|
|
57
|
+
*
|
|
58
|
+
* @throws {WebhookSignatureError} on a malformed header, a timestamp outside
|
|
59
|
+
* `toleranceSeconds`, no matching signature, or a body that is not valid
|
|
60
|
+
* JSON once the signature checks out.
|
|
61
|
+
*/
|
|
62
|
+
export function verifyWebhook(options) {
|
|
63
|
+
const tolerance = options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
|
|
64
|
+
const now = options.now ?? Math.floor(Date.now() / 1000);
|
|
65
|
+
const { timestampText, timestamp, signatures } = parseSignatureHeader(options.signatureHeader);
|
|
66
|
+
if (Math.abs(now - timestamp) > tolerance) {
|
|
67
|
+
throw new WebhookSignatureError(`webhook timestamp ${timestamp} is outside the ${tolerance}s tolerance (now=${now})`);
|
|
68
|
+
}
|
|
69
|
+
const rawBodyBuffer = typeof options.rawBody === "string"
|
|
70
|
+
? Buffer.from(options.rawBody, "utf8")
|
|
71
|
+
: options.rawBody;
|
|
72
|
+
// The signed payload is the literal bytes `"<t>.<raw body>"`
|
|
73
|
+
// (docs/flows/merchant-auth.md). `timestampText`, not `timestamp`: the
|
|
74
|
+
// number is a lossy re-rendering of the header text, and any difference
|
|
75
|
+
// between the two — a leading zero, say — computes an HMAC over bytes the
|
|
76
|
+
// sender never signed, rejecting a genuine delivery.
|
|
77
|
+
const signedPayload = Buffer.concat([
|
|
78
|
+
Buffer.from(`${timestampText}.`, "utf8"),
|
|
79
|
+
rawBodyBuffer,
|
|
80
|
+
]);
|
|
81
|
+
const expectedSignature = createHmac("sha256", options.secret)
|
|
82
|
+
.update(signedPayload)
|
|
83
|
+
.digest("hex");
|
|
84
|
+
const matched = signatures.some((candidate) => constantTimeHexEquals(candidate, expectedSignature));
|
|
85
|
+
if (!matched) {
|
|
86
|
+
throw new WebhookSignatureError('no "v1" signature in the header matches the computed HMAC');
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(rawBodyBuffer.toString("utf8"));
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
throw new WebhookSignatureError("signature verified but the body is not valid JSON", { cause: err });
|
|
93
|
+
}
|
|
94
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vaam-apps/vpay-sdk",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "The Node.js merchant SDK for vpay's /v1 API: OAuth2 private_key_jwt, payment intents, checkout sessions, refunds, events and webhook verification.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/vaam-apps/vpay.git",
|
|
12
|
+
"directory": "sdks/nodejs"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/vaam-apps/vpay/tree/master/sdks/nodejs#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/vaam-apps/vpay/issues"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=22.11.0"
|
|
21
|
+
},
|
|
22
|
+
"main": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./stripe": {
|
|
30
|
+
"types": "./dist/stripe-auth.d.ts",
|
|
31
|
+
"default": "./dist/stripe-auth.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"scripts",
|
|
38
|
+
"README.md"
|
|
39
|
+
],
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"stripe": "^22.6.1"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"stripe": {
|
|
45
|
+
"optional": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^22.10.0",
|
|
50
|
+
"eslint": "9.39.5",
|
|
51
|
+
"stripe": "^22.6.1",
|
|
52
|
+
"typescript": "^5.7.0",
|
|
53
|
+
"vitest": "^4.1.11",
|
|
54
|
+
"@vpay/config": "0.0.0"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsc -p tsconfig.build.json",
|
|
58
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"test:live": "vitest run -c vitest.live.config.ts",
|
|
61
|
+
"lint": "eslint . --max-warnings 0"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Mints one `private_key_jwt` client assertion and prints it, alongside the
|
|
4
|
+
* public JWK derived from the same private key, as JSON on stdout:
|
|
5
|
+
*
|
|
6
|
+
* { "assertion": "<jwt>", "jwks": { "keys": [<public JWK>] } }
|
|
7
|
+
*
|
|
8
|
+
* Intended for a Rust example to feed straight into the real
|
|
9
|
+
* `authkestra_op::client_assertion::verify_client_assertion` — see
|
|
10
|
+
* docs/flows/merchant-auth.md and docs/status.md's "Node SDK" row.
|
|
11
|
+
*
|
|
12
|
+
* Reads from the environment:
|
|
13
|
+
* VPAY_CLIENT_ID required
|
|
14
|
+
* VPAY_PRIVATE_KEY_FILE required — path to a PEM private key
|
|
15
|
+
* VPAY_AUDIENCE required — the assertion's `aud` (token endpoint URL)
|
|
16
|
+
* VPAY_KID optional
|
|
17
|
+
*
|
|
18
|
+
* Run after `pnpm --filter @vaam-apps/vpay-sdk build` (imports from ../dist).
|
|
19
|
+
*/
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
21
|
+
import { createPrivateKey, createPublicKey } from "node:crypto";
|
|
22
|
+
import { mintClientAssertion } from "../dist/index.js";
|
|
23
|
+
|
|
24
|
+
function requireEnv(name) {
|
|
25
|
+
const value = process.env[name];
|
|
26
|
+
if (!value) {
|
|
27
|
+
console.error(`missing required environment variable: ${name}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const clientId = requireEnv("VPAY_CLIENT_ID");
|
|
34
|
+
const privateKeyFile = requireEnv("VPAY_PRIVATE_KEY_FILE");
|
|
35
|
+
const audience = requireEnv("VPAY_AUDIENCE");
|
|
36
|
+
const kid = process.env.VPAY_KID || undefined;
|
|
37
|
+
|
|
38
|
+
let privateKeyPem;
|
|
39
|
+
try {
|
|
40
|
+
privateKeyPem = readFileSync(privateKeyFile, "utf8");
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error(
|
|
43
|
+
`cannot read VPAY_PRIVATE_KEY_FILE (${privateKeyFile}): ${err.message}`,
|
|
44
|
+
);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let privateKey;
|
|
49
|
+
try {
|
|
50
|
+
privateKey = createPrivateKey(privateKeyPem);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error(
|
|
53
|
+
`VPAY_PRIVATE_KEY_FILE (${privateKeyFile}) is not a readable PEM private key: ${err.message}`,
|
|
54
|
+
);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
const publicKey = createPublicKey(privateKey);
|
|
58
|
+
|
|
59
|
+
const assertion = mintClientAssertion({
|
|
60
|
+
clientId,
|
|
61
|
+
privateKey,
|
|
62
|
+
audience,
|
|
63
|
+
...(kid ? { kid } : {}),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const jwk = publicKey.export({ format: "jwk" });
|
|
67
|
+
if (kid) {
|
|
68
|
+
jwk.kid = kid;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
process.stdout.write(
|
|
72
|
+
`${JSON.stringify({ assertion, jwks: { keys: [jwk] } })}\n`,
|
|
73
|
+
);
|