@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 +56 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/pdf.d.ts +116 -0
- package/dist/pdf.d.ts.map +1 -0
- package/dist/pdf.js +172 -0
- package/dist/pdf.js.map +1 -0
- package/package.json +13 -2
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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
|
package/dist/pdf.js.map
ADDED
|
@@ -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.
|
|
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
|
}
|