@formatjet/sdk 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +100 -2
- package/dist/client.d.ts +175 -0
- package/dist/client.js +406 -0
- package/dist/errors.d.ts +47 -0
- package/dist/errors.js +87 -0
- package/dist/generated/openapi.d.ts +2757 -0
- package/dist/generated/openapi.js +2 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/webhooks.d.ts +55 -0
- package/dist/webhooks.js +77 -0
- package/package.json +45 -4
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { DEFAULT_BASE_URL, filenameFrom, FormatJet, Jobs, webhooks, type ApiClient, type ConversionOptions, type ConvertOptions, type ConvertResult, type DeletionReceipt, type FileData, type FileInput, type FormatId, type FormatJetOptions, type Job, type JobOutput, type JobRequest, type JobStatus, type JobStep, type JobSummary, type OutputFile, type RequestOptions, type Upload, type UploadAndConvertOptions, type WaitOptions, } from './client.js';
|
|
2
|
+
export { FormatJetError, type ErrorCode, type FormatJetErrorCode, type FormatJetErrorInit, type Problem, } from './errors.js';
|
|
3
|
+
export type { components, operations, paths } from './generated/openapi.js';
|
|
4
|
+
export { DEFAULT_TOLERANCE_SECONDS, parseWebhook, SIGNATURE_HEADER, verifyWebhook, WebhookSignatureError, type VerifyWebhookInput, type WebhookEvent, type WebhookEventType, type WebhookVerification, } from './webhooks.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { components } from './generated/openapi.js';
|
|
2
|
+
/** The header each delivery carries: `t=<unix seconds>,v1=<hex HMAC-SHA256>[,v1=…]`. */
|
|
3
|
+
export declare const SIGNATURE_HEADER = "FormatJet-Signature";
|
|
4
|
+
/** Deliveries signed longer ago than this (5 minutes) are refused, so a copy can't be replayed. */
|
|
5
|
+
export declare const DEFAULT_TOLERANCE_SECONDS = 300;
|
|
6
|
+
export type WebhookEventType = 'job.completed' | 'job.failed' | 'job.canceled';
|
|
7
|
+
/** The body of a delivery. */
|
|
8
|
+
export interface WebhookEvent {
|
|
9
|
+
/** `evt_…`: the same on every retry of this event, so use it to skip repeats. */
|
|
10
|
+
id: string;
|
|
11
|
+
type: WebhookEventType;
|
|
12
|
+
created_at: string;
|
|
13
|
+
mode: 'live' | 'test';
|
|
14
|
+
data: {
|
|
15
|
+
job: components['schemas']['Job'];
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export interface VerifyWebhookInput {
|
|
19
|
+
/** The raw request body exactly as it arrived: read it before parsing the JSON. */
|
|
20
|
+
body: string | Uint8Array | ArrayBuffer;
|
|
21
|
+
/** The `FormatJet-Signature` header. */
|
|
22
|
+
header: string | null | undefined;
|
|
23
|
+
/** The endpoint's secret (`whsec_…`). */
|
|
24
|
+
secret: string;
|
|
25
|
+
toleranceSeconds?: number;
|
|
26
|
+
/** Now, in milliseconds (tests). */
|
|
27
|
+
now?: number;
|
|
28
|
+
}
|
|
29
|
+
export type WebhookVerification = {
|
|
30
|
+
ok: true;
|
|
31
|
+
timestamp: number;
|
|
32
|
+
} | {
|
|
33
|
+
ok: false;
|
|
34
|
+
reason: 'missing_header' | 'malformed_header' | 'timestamp_out_of_range' | 'no_match';
|
|
35
|
+
};
|
|
36
|
+
/** A delivery whose signature didn't check out. */
|
|
37
|
+
export declare class WebhookSignatureError extends Error {
|
|
38
|
+
readonly reason: Exclude<WebhookVerification, {
|
|
39
|
+
ok: true;
|
|
40
|
+
}>['reason'];
|
|
41
|
+
readonly name = "WebhookSignatureError";
|
|
42
|
+
constructor(reason: Exclude<WebhookVerification, {
|
|
43
|
+
ok: true;
|
|
44
|
+
}>['reason']);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Checks a delivery's signature: any `v1` made with the secret is enough (a rotation sends
|
|
48
|
+
* two), and a timestamp more than `toleranceSeconds` from now is refused.
|
|
49
|
+
*/
|
|
50
|
+
export declare function verifyWebhook(input: VerifyWebhookInput): Promise<WebhookVerification>;
|
|
51
|
+
/**
|
|
52
|
+
* Checks a delivery and returns its event, or throws `WebhookSignatureError`. Answer 2xx
|
|
53
|
+
* quickly once it's checked: a delivery that takes over 10 seconds is retried.
|
|
54
|
+
*/
|
|
55
|
+
export declare function parseWebhook(input: VerifyWebhookInput): Promise<WebhookEvent>;
|
package/dist/webhooks.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** The header each delivery carries: `t=<unix seconds>,v1=<hex HMAC-SHA256>[,v1=…]`. */
|
|
2
|
+
export const SIGNATURE_HEADER = 'FormatJet-Signature';
|
|
3
|
+
/** Deliveries signed longer ago than this (5 minutes) are refused, so a copy can't be replayed. */
|
|
4
|
+
export const DEFAULT_TOLERANCE_SECONDS = 300;
|
|
5
|
+
/** A delivery whose signature didn't check out. */
|
|
6
|
+
export class WebhookSignatureError extends Error {
|
|
7
|
+
reason;
|
|
8
|
+
name = 'WebhookSignatureError';
|
|
9
|
+
constructor(reason) {
|
|
10
|
+
super(`The webhook signature isn't valid: ${reason}`);
|
|
11
|
+
this.reason = reason;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const encoder = new TextEncoder();
|
|
15
|
+
function bytesOf(body) {
|
|
16
|
+
if (typeof body === 'string')
|
|
17
|
+
return encoder.encode(body);
|
|
18
|
+
return body instanceof Uint8Array ? body : new Uint8Array(body);
|
|
19
|
+
}
|
|
20
|
+
async function hmacHex(secret, message) {
|
|
21
|
+
const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
22
|
+
const signature = new Uint8Array(await crypto.subtle.sign('HMAC', key, message));
|
|
23
|
+
return Array.from(signature, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
24
|
+
}
|
|
25
|
+
/** Compares two strings in time that depends only on their length. */
|
|
26
|
+
function sameText(a, b) {
|
|
27
|
+
if (a.length !== b.length)
|
|
28
|
+
return false;
|
|
29
|
+
let diff = 0;
|
|
30
|
+
for (let i = 0; i < a.length; i++)
|
|
31
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
32
|
+
return diff === 0;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Checks a delivery's signature: any `v1` made with the secret is enough (a rotation sends
|
|
36
|
+
* two), and a timestamp more than `toleranceSeconds` from now is refused.
|
|
37
|
+
*/
|
|
38
|
+
export async function verifyWebhook(input) {
|
|
39
|
+
if (!input.header)
|
|
40
|
+
return { ok: false, reason: 'missing_header' };
|
|
41
|
+
let timestamp;
|
|
42
|
+
const signatures = [];
|
|
43
|
+
for (const part of input.header.split(',')) {
|
|
44
|
+
const [name, value] = part.trim().split('=', 2);
|
|
45
|
+
if (name === 't' && value && /^\d{1,12}$/.test(value))
|
|
46
|
+
timestamp = Number(value);
|
|
47
|
+
else if (name === 'v1' && value && /^[0-9a-f]{64}$/.test(value))
|
|
48
|
+
signatures.push(value);
|
|
49
|
+
}
|
|
50
|
+
if (timestamp === undefined || signatures.length === 0) {
|
|
51
|
+
return { ok: false, reason: 'malformed_header' };
|
|
52
|
+
}
|
|
53
|
+
const now = Math.floor((input.now ?? Date.now()) / 1000);
|
|
54
|
+
if (Math.abs(now - timestamp) > (input.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS)) {
|
|
55
|
+
return { ok: false, reason: 'timestamp_out_of_range' };
|
|
56
|
+
}
|
|
57
|
+
const body = bytesOf(input.body);
|
|
58
|
+
const prefix = encoder.encode(`${timestamp}.`);
|
|
59
|
+
const message = new Uint8Array(prefix.length + body.length);
|
|
60
|
+
message.set(prefix);
|
|
61
|
+
message.set(body, prefix.length);
|
|
62
|
+
const expected = await hmacHex(input.secret, message);
|
|
63
|
+
return signatures.some((signature) => sameText(signature, expected))
|
|
64
|
+
? { ok: true, timestamp }
|
|
65
|
+
: { ok: false, reason: 'no_match' };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Checks a delivery and returns its event, or throws `WebhookSignatureError`. Answer 2xx
|
|
69
|
+
* quickly once it's checked: a delivery that takes over 10 seconds is retried.
|
|
70
|
+
*/
|
|
71
|
+
export async function parseWebhook(input) {
|
|
72
|
+
const result = await verifyWebhook(input);
|
|
73
|
+
if (!result.ok)
|
|
74
|
+
throw new WebhookSignatureError(result.reason);
|
|
75
|
+
const text = typeof input.body === 'string' ? input.body : new TextDecoder().decode(bytesOf(input.body));
|
|
76
|
+
return JSON.parse(text);
|
|
77
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formatjet/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Convert documents, images, data and PDFs with the FormatJet API, from TypeScript and JavaScript.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Taprobane Labs",
|
|
7
|
+
"homepage": "https://formatjet.com/docs",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/TaprobaneLabs/formatjet.git",
|
|
11
|
+
"directory": "packages/sdk-js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"formatjet",
|
|
15
|
+
"file conversion",
|
|
16
|
+
"pdf",
|
|
17
|
+
"docx",
|
|
18
|
+
"api"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc -p tsconfig.build.json",
|
|
38
|
+
"prepublishOnly": "bun run build",
|
|
39
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"openapi-fetch": "0.17.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"openapi-typescript": "7.13.0"
|
|
46
|
+
}
|
|
47
|
+
}
|