@validpay/node-sdk 0.6.0 → 0.7.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 CHANGED
@@ -62,6 +62,62 @@ const verifyUrl = `https://validpay.com/verify/${retrievalId}#key=${toBase64Url(
62
62
 
63
63
  `toBase64Url` matters because phone QR scanners + browser share-sheets mangle `+`, `/`, and `=` in URL fragments. The `/verify` page accepts both standard base64 and base64url for backward compatibility, but new links should always emit base64url.
64
64
 
65
+ ### Placing the QR on a document (`embedQr`)
66
+
67
+ For a PDF, `embedQr` builds the verify QR and stamps it onto the page for you — so you don't have to wire up a QR library, base64url the key, or wrestle with PDF coordinates (PDFs use a bottom-left origin; everything else uses top-left).
68
+
69
+ `embedQr` needs two **optional** peer dependencies — the core client stays dependency-free, so install them only if you use it:
70
+
71
+ ```bash
72
+ npm i pdf-lib qrcode
73
+ ```
74
+
75
+ ```ts
76
+ import { ValidPayClient, embedQr } from "@validpay/node-sdk";
77
+ import { readFile, writeFile } from "node:fs/promises";
78
+
79
+ const client = new ValidPayClient({ apiKey: process.env.VALIDPAY_API_KEY! });
80
+
81
+ const original = await readFile("invoice.pdf");
82
+ const { retrievalId, key } = await client.createFileIntent({
83
+ documentType: "invoice",
84
+ file: original,
85
+ fileContentType: "application/pdf",
86
+ });
87
+
88
+ const sealed = await embedQr(original, {
89
+ retrievalId,
90
+ key,
91
+ // 90pt (1.25in) QR, 36pt in from the bottom-right corner.
92
+ placement: { anchor: "bottom-right", x: 36, y: 36, width: 90 },
93
+ });
94
+ await writeFile("invoice-sealed.pdf", sealed);
95
+ ```
96
+
97
+ #### The placement contract
98
+
99
+ Coordinates read the way you think about a page, and are identical to what the **"Try it" placement tool** in the developer console emits — so position once in the UI, copy the call, and it lands in the same spot.
100
+
101
+ | field | meaning | default |
102
+ | -------- | ------- | ------- |
103
+ | `anchor` | which page **corner** the insets are measured from (`top-left` \| `top-right` \| `bottom-left` \| `bottom-right`) | `top-left` |
104
+ | `x` | horizontal inset from that corner's vertical edge | — |
105
+ | `y` | vertical inset from that corner's horizontal edge | — |
106
+ | `width` | QR side length (it's square) | — |
107
+ | `units` | `pt` (1/72in) \| `mm` \| `in` | `pt` |
108
+ | `page` | 1-based page number | `1` |
109
+
110
+ `{ anchor: "bottom-right", x: 36, y: 36, width: 90 }` sits 36pt in from the bottom and right edges — and stays bottom-right on any page size. Keep the QR **≥ ~72pt (1in)** so it scans reliably once printed; `embedQr` warns below that and throws if the placement runs off the page.
111
+
112
+ If you render PDFs with a different library, the two pure helpers are exported too:
113
+
114
+ ```ts
115
+ import { buildVerifyUrl, resolveQrRect } from "@validpay/node-sdk";
116
+
117
+ const url = buildVerifyUrl(retrievalId, key); // base64url key in the fragment
118
+ const rect = resolveQrRect(placement, pageWidthPt, pageHeightPt); // → { x, y, size } in pdf bottom-left points
119
+ ```
120
+
65
121
  ## How it works
66
122
 
67
123
  1. `createIntent` generates a fresh 256-bit key, encrypts your payload locally with AES-256-GCM, computes a SHA-256 commitment hash of the plaintext, and POSTs only the ciphertext + hash to `POST /v1/intent`.
package/dist/index.d.ts CHANGED
@@ -2,4 +2,5 @@ export { ValidPayClient } from "./client.js";
2
2
  export { generateKey, encrypt, encryptBytes, decrypt, decryptBytes, commitmentHash, splitKey, combineKeyShares, encryptFields, buildKeyMap, decryptFields, } from "./crypto.js";
3
3
  export { ValidPayError, type ValidPayClientOptions, type CreateIntentParams, type CreateFileIntentParams, type BatchIntentItem, type SelectiveIntentParams, type CreateIntentResult, type VerifyIntentResult, type TimeLockStatus, type RevocationResult, type RevocationEvent, type RawIntentResponse, type RawCreateIntentResponse, } from "./types.js";
4
4
  export { verifyWebhookSignature, DEFAULT_WEBHOOK_TOLERANCE_SECONDS, type VerifyWebhookOptions, type WebhookVerifyResult, type WebhookVerifyFailureReason, } from "./webhookSignature.js";
5
+ export { buildVerifyUrl, resolveQrRect, embedQr, MIN_RECOMMENDED_QR_PT, type QrAnchor, type QrUnit, type QrPlacement, type VerifyUrlOptions, type ResolvedQrRect, type QrRenderOptions, type EmbedQrOptions, } from "./pdf.js";
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EACL,WAAW,EACX,OAAO,EACP,YAAY,EACZ,OAAO,EACP,YAAY,EACZ,cAAc,EACd,QAAQ,EACR,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,aAAa,EACb,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,GAC7B,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,sBAAsB,EACtB,iCAAiC,EACjC,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,KAAK,0BAA0B,GAChC,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EACL,WAAW,EACX,OAAO,EACP,YAAY,EACZ,OAAO,EACP,YAAY,EACZ,cAAc,EACd,QAAQ,EACR,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,aAAa,EACb,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,GAC7B,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,sBAAsB,EACtB,iCAAiC,EACjC,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,KAAK,0BAA0B,GAChC,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,cAAc,EACd,aAAa,EACb,OAAO,EACP,qBAAqB,EACrB,KAAK,QAAQ,EACb,KAAK,MAAM,EACX,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,cAAc,GACpB,MAAM,UAAU,CAAC"}
package/dist/index.js CHANGED
@@ -2,4 +2,5 @@ export { ValidPayClient } from "./client.js";
2
2
  export { generateKey, encrypt, encryptBytes, decrypt, decryptBytes, commitmentHash, splitKey, combineKeyShares, encryptFields, buildKeyMap, decryptFields, } from "./crypto.js";
3
3
  export { ValidPayError, } from "./types.js";
4
4
  export { verifyWebhookSignature, DEFAULT_WEBHOOK_TOLERANCE_SECONDS, } from "./webhookSignature.js";
5
+ export { buildVerifyUrl, resolveQrRect, embedQr, MIN_RECOMMENDED_QR_PT, } from "./pdf.js";
5
6
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EACL,WAAW,EACX,OAAO,EACP,YAAY,EACZ,OAAO,EACP,YAAY,EACZ,cAAc,EACd,QAAQ,EACR,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,aAAa,GAad,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,sBAAsB,EACtB,iCAAiC,GAIlC,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EACL,WAAW,EACX,OAAO,EACP,YAAY,EACZ,OAAO,EACP,YAAY,EACZ,cAAc,EACd,QAAQ,EACR,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,aAAa,GAad,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,sBAAsB,EACtB,iCAAiC,GAIlC,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,cAAc,EACd,aAAa,EACb,OAAO,EACP,qBAAqB,GAQtB,MAAM,UAAU,CAAC"}
package/dist/pdf.d.ts ADDED
@@ -0,0 +1,116 @@
1
+ /** Which page corner the (x, y) inset is measured from. */
2
+ export type QrAnchor = "top-left" | "top-right" | "bottom-left" | "bottom-right";
3
+ /** Units for placement values. 1pt = 1/72 inch (PDF's native unit). */
4
+ export type QrUnit = "pt" | "mm" | "in";
5
+ /**
6
+ * Where to place the QR on a page, in a coordinate system that matches how
7
+ * people actually think about documents:
8
+ *
9
+ * - `anchor` names a page CORNER.
10
+ * - `x` is the horizontal inset from that corner's vertical edge.
11
+ * - `y` is the vertical inset from that corner's horizontal edge.
12
+ * - The QR's matching corner is pinned at that inset.
13
+ *
14
+ * So `{ anchor: "bottom-right", x: 36, y: 36, width: 90 }` is a 90pt QR sitting
15
+ * 36pt in from the bottom and right edges — and it stays bottom-right on any
16
+ * page size. The default `top-left` anchor reads like screen coordinates.
17
+ */
18
+ export interface QrPlacement {
19
+ /** 1-based page number. Default `1`. */
20
+ page?: number;
21
+ /** Page corner the insets are measured from. Default `"top-left"`. */
22
+ anchor?: QrAnchor;
23
+ /** Horizontal inset from the anchor's vertical edge, in `units`. */
24
+ x: number;
25
+ /** Vertical inset from the anchor's horizontal edge, in `units`. */
26
+ y: number;
27
+ /** QR side length (it is square), in `units`. */
28
+ width: number;
29
+ /** Units for `x` / `y` / `width`. Default `"pt"`. */
30
+ units?: QrUnit;
31
+ }
32
+ /**
33
+ * Smallest QR side we consider reliably scannable from a printed page at
34
+ * arm's length (~72pt ≈ 1in ≈ 2.54cm). Below this, {@link embedQr} emits a
35
+ * one-time console warning. Advisory only — not enforced.
36
+ */
37
+ export declare const MIN_RECOMMENDED_QR_PT = 72;
38
+ export interface VerifyUrlOptions {
39
+ /** Web origin that serves `/verify`. Default `"https://validpay.com"`. */
40
+ baseUrl?: string;
41
+ }
42
+ /**
43
+ * Build the canonical verify URL the QR encodes:
44
+ *
45
+ * <baseUrl>/verify/<retrievalId>#key=<base64url(key)>
46
+ *
47
+ * The key is placed in the URL FRAGMENT (`#key=`), which browsers never send
48
+ * to any server — so the decryption share rides along with the scan without
49
+ * ever touching ValidPay's logs. The key is converted to base64url so phone
50
+ * scanners don't mangle it.
51
+ */
52
+ export declare function buildVerifyUrl(retrievalId: string, key: string, opts?: VerifyUrlOptions): string;
53
+ /** A QR rectangle in pdf-lib's bottom-left-origin point space. */
54
+ export interface ResolvedQrRect {
55
+ /** Distance from the page's LEFT edge to the QR's left edge, in points. */
56
+ x: number;
57
+ /** Distance from the page's BOTTOM edge to the QR's bottom edge, in points. */
58
+ y: number;
59
+ /** QR side length, in points. */
60
+ size: number;
61
+ }
62
+ /**
63
+ * Convert a canonical {@link QrPlacement} (top-left-friendly, anchor-relative
64
+ * insets, arbitrary units) into pdf-lib's bottom-left-origin point rectangle
65
+ * for a page of the given size.
66
+ *
67
+ * This is the EXACT conversion the website "Try it" tool uses, so coordinates
68
+ * you copy from the tool land in the same place here. Pure and
69
+ * dependency-free.
70
+ */
71
+ export declare function resolveQrRect(placement: QrPlacement, pageWidthPt: number, pageHeightPt: number): ResolvedQrRect;
72
+ export interface QrRenderOptions {
73
+ /**
74
+ * Error-correction level. Higher tolerates more print damage/smudging at
75
+ * the cost of density. Default `"M"` (15%); use `"Q"` (25%) for documents
76
+ * that get printed and re-scanned in the wild.
77
+ */
78
+ errorCorrectionLevel?: "L" | "M" | "Q" | "H";
79
+ /** Quiet-zone width in modules. Default `2`. Don't go below 1 or scanners struggle. */
80
+ margin?: number;
81
+ /** Foreground (module) color. Default `"#0A0F1E"`. */
82
+ darkColor?: string;
83
+ /** Background color. Default `"#FFFFFF"` — keep it opaque for contrast. */
84
+ lightColor?: string;
85
+ /** Raster resolution in px for the embedded image. Default `1024`. */
86
+ renderPx?: number;
87
+ }
88
+ export interface EmbedQrOptions {
89
+ /** From `createIntent` / `createFileIntent`. */
90
+ retrievalId: string;
91
+ /** Share A from `createIntent` / `createFileIntent`. */
92
+ key: string;
93
+ /** Where to stamp the QR. */
94
+ placement: QrPlacement;
95
+ /** Verify URL base. Default `"https://validpay.com"`. */
96
+ baseUrl?: string;
97
+ /** QR rendering tweaks. */
98
+ qr?: QrRenderOptions;
99
+ }
100
+ /**
101
+ * Stamp a scannable verify QR onto an existing PDF and return the new PDF
102
+ * bytes. The input is not mutated.
103
+ *
104
+ * Requires the optional peer deps `pdf-lib` and `qrcode` (`npm i pdf-lib
105
+ * qrcode`); it throws a `missing_dependency` ValidPayError if either is
106
+ * absent. Works in Node and the browser (both libs are isomorphic).
107
+ *
108
+ * @example
109
+ * const { retrievalId, key } = await client.createFileIntent({ documentType: "invoice", file });
110
+ * const sealed = await embedQr(originalPdfBytes, {
111
+ * retrievalId, key,
112
+ * placement: { anchor: "bottom-right", x: 36, y: 36, width: 90 },
113
+ * });
114
+ */
115
+ export declare function embedQr(pdf: Uint8Array, opts: EmbedQrOptions): Promise<Uint8Array>;
116
+ //# sourceMappingURL=pdf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pdf.d.ts","sourceRoot":"","sources":["../src/pdf.ts"],"names":[],"mappings":"AA4BA,2DAA2D;AAC3D,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,WAAW,GAAG,aAAa,GAAG,cAAc,CAAC;AAEjF,uEAAuE;AACvE,MAAM,MAAM,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAExC;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,WAAW;IAC1B,wCAAwC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,oEAAoE;IACpE,CAAC,EAAE,MAAM,CAAC;IACV,oEAAoE;IACpE,CAAC,EAAE,MAAM,CAAC;IACV,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAID;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAIxC,MAAM,WAAW,gBAAgB;IAC/B,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AASD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,gBAAqB,GAC1B,MAAM,CASR;AAID,kEAAkE;AAClE,MAAM,WAAW,cAAc;IAC7B,2EAA2E;IAC3E,CAAC,EAAE,MAAM,CAAC;IACV,+EAA+E;IAC/E,CAAC,EAAE,MAAM,CAAC;IACV,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,WAAW,EACtB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GACnB,cAAc,CAiBhB;AAID,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IAC7C,uFAAuF;IACvF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,WAAW,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,GAAG,EAAE,MAAM,CAAC;IACZ,6BAA6B;IAC7B,SAAS,EAAE,WAAW,CAAC;IACvB,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2BAA2B;IAC3B,EAAE,CAAC,EAAE,eAAe,CAAC;CACtB;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,OAAO,CAC3B,GAAG,EAAE,UAAU,EACf,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,UAAU,CAAC,CAuDrB"}
package/dist/pdf.js ADDED
@@ -0,0 +1,172 @@
1
+ /**
2
+ * QR placement helpers (file mode add-on).
3
+ *
4
+ * `createFileIntent` / `createIntent` seal a document and return
5
+ * `{ retrievalId, key }`. To actually verify it, a scannable QR encoding the
6
+ * verify URL must appear ON the document. WHERE that QR goes is the
7
+ * integrator's call — but historically they were on their own to render it
8
+ * and to guess coordinates, which is fiddly and error-prone (PDFs use a
9
+ * bottom-left origin; every screen uses top-left).
10
+ *
11
+ * This module fixes that with one canonical placement contract used by the
12
+ * SDK, the website "Try it" tool, and the docs, so a position you pick once
13
+ * (e.g. in the tool) maps to the exact same spot here.
14
+ *
15
+ * `pdf-lib` and `qrcode` are OPTIONAL peer dependencies — the core
16
+ * `ValidPayClient` stays zero-dependency. They're loaded lazily, so you only
17
+ * need them installed if you call {@link embedQr}:
18
+ *
19
+ * npm i pdf-lib qrcode
20
+ *
21
+ * The coordinate math ({@link resolveQrRect}) and {@link buildVerifyUrl} are
22
+ * pure and dependency-free — use them directly if you render PDFs with a
23
+ * different library.
24
+ */
25
+ import { ValidPayError } from "./types.js";
26
+ const UNIT_TO_PT = { pt: 1, mm: 72 / 25.4, in: 72 };
27
+ /**
28
+ * Smallest QR side we consider reliably scannable from a printed page at
29
+ * arm's length (~72pt ≈ 1in ≈ 2.54cm). Below this, {@link embedQr} emits a
30
+ * one-time console warning. Advisory only — not enforced.
31
+ */
32
+ export const MIN_RECOMMENDED_QR_PT = 72;
33
+ /** base64 → base64url. Phone QR scanners and share-sheets mangle `+`, `/`,
34
+ * and `=` inside URL fragments, so keys must be base64url in a QR. Idempotent
35
+ * on already-base64url input; the `/verify` page accepts both. */
36
+ function toBase64Url(b64) {
37
+ return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
38
+ }
39
+ /**
40
+ * Build the canonical verify URL the QR encodes:
41
+ *
42
+ * <baseUrl>/verify/<retrievalId>#key=<base64url(key)>
43
+ *
44
+ * The key is placed in the URL FRAGMENT (`#key=`), which browsers never send
45
+ * to any server — so the decryption share rides along with the scan without
46
+ * ever touching ValidPay's logs. The key is converted to base64url so phone
47
+ * scanners don't mangle it.
48
+ */
49
+ export function buildVerifyUrl(retrievalId, key, opts = {}) {
50
+ if (!retrievalId) {
51
+ throw new ValidPayError("invalid_argument", "retrievalId is required");
52
+ }
53
+ if (!key) {
54
+ throw new ValidPayError("invalid_argument", "key is required");
55
+ }
56
+ const base = (opts.baseUrl ?? "https://validpay.com").replace(/\/+$/, "");
57
+ return `${base}/verify/${encodeURIComponent(retrievalId)}#key=${toBase64Url(key)}`;
58
+ }
59
+ /**
60
+ * Convert a canonical {@link QrPlacement} (top-left-friendly, anchor-relative
61
+ * insets, arbitrary units) into pdf-lib's bottom-left-origin point rectangle
62
+ * for a page of the given size.
63
+ *
64
+ * This is the EXACT conversion the website "Try it" tool uses, so coordinates
65
+ * you copy from the tool land in the same place here. Pure and
66
+ * dependency-free.
67
+ */
68
+ export function resolveQrRect(placement, pageWidthPt, pageHeightPt) {
69
+ const unit = UNIT_TO_PT[placement.units ?? "pt"];
70
+ const size = placement.width * unit;
71
+ const insetX = placement.x * unit;
72
+ const insetY = placement.y * unit;
73
+ const anchor = placement.anchor ?? "top-left";
74
+ const leftAnchored = anchor === "top-left" || anchor === "bottom-left";
75
+ const topAnchored = anchor === "top-left" || anchor === "top-right";
76
+ // Horizontal: inset measured from the left or right edge to the QR's left.
77
+ const x = leftAnchored ? insetX : pageWidthPt - insetX - size;
78
+ // Vertical: pdf-lib y is the QR's BOTTOM edge from the page bottom. A top
79
+ // inset measures from the page top down to the QR's top edge.
80
+ const y = topAnchored ? pageHeightPt - insetY - size : insetY;
81
+ return { x, y, size };
82
+ }
83
+ let smallQrWarned = false;
84
+ /**
85
+ * Stamp a scannable verify QR onto an existing PDF and return the new PDF
86
+ * bytes. The input is not mutated.
87
+ *
88
+ * Requires the optional peer deps `pdf-lib` and `qrcode` (`npm i pdf-lib
89
+ * qrcode`); it throws a `missing_dependency` ValidPayError if either is
90
+ * absent. Works in Node and the browser (both libs are isomorphic).
91
+ *
92
+ * @example
93
+ * const { retrievalId, key } = await client.createFileIntent({ documentType: "invoice", file });
94
+ * const sealed = await embedQr(originalPdfBytes, {
95
+ * retrievalId, key,
96
+ * placement: { anchor: "bottom-right", x: 36, y: 36, width: 90 },
97
+ * });
98
+ */
99
+ export async function embedQr(pdf, opts) {
100
+ if (!(pdf instanceof Uint8Array) || pdf.length === 0) {
101
+ throw new ValidPayError("invalid_argument", "pdf must be non-empty Uint8Array/Buffer bytes");
102
+ }
103
+ if (!opts?.placement) {
104
+ throw new ValidPayError("invalid_argument", "placement is required");
105
+ }
106
+ if (!(opts.placement.width > 0)) {
107
+ throw new ValidPayError("invalid_argument", "placement.width must be > 0");
108
+ }
109
+ const { PDFDocument } = await loadPdfLib();
110
+ const qrcode = await loadQrcode();
111
+ const url = buildVerifyUrl(opts.retrievalId, opts.key, { baseUrl: opts.baseUrl });
112
+ const q = opts.qr ?? {};
113
+ const dataUrl = await qrcode.toDataURL(url, {
114
+ errorCorrectionLevel: q.errorCorrectionLevel ?? "M",
115
+ margin: q.margin ?? 2,
116
+ width: q.renderPx ?? 1024,
117
+ color: { dark: q.darkColor ?? "#0A0F1E", light: q.lightColor ?? "#FFFFFF" },
118
+ });
119
+ const pngBytes = dataUrlToBytes(dataUrl);
120
+ const doc = await PDFDocument.load(pdf);
121
+ const pages = doc.getPages();
122
+ const pageIndex = (opts.placement.page ?? 1) - 1;
123
+ if (pageIndex < 0 || pageIndex >= pages.length) {
124
+ throw new ValidPayError("invalid_argument", `placement.page ${opts.placement.page ?? 1} is out of range (document has ${pages.length} page(s))`);
125
+ }
126
+ const page = pages[pageIndex];
127
+ const { width, height } = page.getSize();
128
+ const rect = resolveQrRect(opts.placement, width, height);
129
+ if (rect.size < MIN_RECOMMENDED_QR_PT && !smallQrWarned) {
130
+ smallQrWarned = true;
131
+ // eslint-disable-next-line no-console
132
+ console.warn(`[validpay] QR is ${rect.size.toFixed(0)}pt wide — below the ~${MIN_RECOMMENDED_QR_PT}pt ` +
133
+ "(1in) recommended minimum; it may be hard to scan once printed.");
134
+ }
135
+ if (rect.x < 0 || rect.y < 0 || rect.x + rect.size > width || rect.y + rect.size > height) {
136
+ throw new ValidPayError("invalid_argument", "placement puts the QR (partly) off the page — check x/y/width against the page size");
137
+ }
138
+ const png = await doc.embedPng(pngBytes);
139
+ page.drawImage(png, { x: rect.x, y: rect.y, width: rect.size, height: rect.size });
140
+ return doc.save();
141
+ }
142
+ // ── internals ───────────────────────────────────────────────────────────────
143
+ /** Decode a `data:image/png;base64,...` URL to raw bytes (Node or browser). */
144
+ function dataUrlToBytes(dataUrl) {
145
+ const b64 = dataUrl.replace(/^data:[^,]*,/, "");
146
+ if (typeof Buffer !== "undefined") {
147
+ return new Uint8Array(Buffer.from(b64, "base64"));
148
+ }
149
+ const bin = atob(b64);
150
+ const out = new Uint8Array(bin.length);
151
+ for (let i = 0; i < bin.length; i++)
152
+ out[i] = bin.charCodeAt(i);
153
+ return out;
154
+ }
155
+ async function loadPdfLib() {
156
+ try {
157
+ return (await import("pdf-lib"));
158
+ }
159
+ catch {
160
+ throw new ValidPayError("missing_dependency", "embedQr requires the optional peer dependency 'pdf-lib'. Install it: npm i pdf-lib");
161
+ }
162
+ }
163
+ async function loadQrcode() {
164
+ try {
165
+ const mod = (await import("qrcode"));
166
+ return (mod.default ?? mod);
167
+ }
168
+ catch {
169
+ throw new ValidPayError("missing_dependency", "embedQr requires the optional peer dependency 'qrcode'. Install it: npm i qrcode");
170
+ }
171
+ }
172
+ //# sourceMappingURL=pdf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pdf.js","sourceRoot":"","sources":["../src/pdf.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@validpay/node-sdk",
3
- "version": "0.6.0",
4
- "description": "Official ValidPay Node.js SDK — client-side AES-256-GCM encryption + commitment hashing + split-key + selective disclosure + revocation. Zero production dependencies.",
3
+ "version": "0.7.0",
4
+ "description": "Official ValidPay Node.js SDK — client-side AES-256-GCM encryption + commitment hashing + split-key + selective disclosure + revocation + QR placement. Zero required production dependencies.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -47,8 +47,19 @@
47
47
  },
48
48
  "license": "MIT",
49
49
  "author": "ValidPay",
50
+ "peerDependencies": {
51
+ "pdf-lib": ">=1.17.0",
52
+ "qrcode": ">=1.5.0"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "pdf-lib": { "optional": true },
56
+ "qrcode": { "optional": true }
57
+ },
50
58
  "devDependencies": {
51
59
  "@types/node": "^20.11.0",
60
+ "@types/qrcode": "^1.5.5",
61
+ "pdf-lib": "^1.17.1",
62
+ "qrcode": "^1.5.4",
52
63
  "typescript": "^5.4.0",
53
64
  "vitest": "^1.6.0"
54
65
  }