@cutoptim/engine 0.0.1 → 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/LICENSE +21 -0
- package/README.md +231 -4
- package/dist/index.d.ts +78 -0
- package/dist/index.js +127 -0
- package/dist/types.gen.d.ts +1154 -0
- package/dist/types.gen.js +3 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/dist/webhooks.d.ts +13 -0
- package/dist/webhooks.js +74 -0
- package/package.json +46 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const VERSION = "1.0.0";
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const SIGNATURE_HEADER = "CutOptim-Signature";
|
|
2
|
+
export declare const DEFAULT_TOLERANCE_S = 300;
|
|
3
|
+
export interface VerifyOptions {
|
|
4
|
+
/** Maximum clock difference in seconds (default 300). */
|
|
5
|
+
toleranceS?: number;
|
|
6
|
+
/** Override "now" (unix seconds) — for tests. */
|
|
7
|
+
nowS?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* True when `header` is a valid CutOptim signature of `rawBody` under one of `secrets`, inside the time window.
|
|
11
|
+
* `rawBody` must be the exact bytes received (string or Uint8Array).
|
|
12
|
+
*/
|
|
13
|
+
export declare function verifyWebhook(rawBody: string | Uint8Array, header: string | null | undefined, secrets: string | string[], opts?: VerifyOptions): Promise<boolean>;
|
package/dist/webhooks.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Verify a CutOptim webhook delivery (the CutOptim-Signature header) — Web Crypto, so it runs on Node 18+, Deno, Bun,
|
|
2
|
+
// Cloudflare Workers and in the browser alike.
|
|
3
|
+
//
|
|
4
|
+
// CutOptim-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>[,v1=…]
|
|
5
|
+
//
|
|
6
|
+
// Pass the RAW request body (the exact bytes received) — a body that was parsed and serialised again does not match.
|
|
7
|
+
// During a secret rotation the header carries two v1 values; either matching your secret is enough.
|
|
8
|
+
export const SIGNATURE_HEADER = 'CutOptim-Signature';
|
|
9
|
+
export const DEFAULT_TOLERANCE_S = 300;
|
|
10
|
+
const enc = new TextEncoder();
|
|
11
|
+
async function subtle() {
|
|
12
|
+
if (globalThis.crypto?.subtle)
|
|
13
|
+
return globalThis.crypto.subtle;
|
|
14
|
+
// Node 18 without the global: the same API from node:crypto.
|
|
15
|
+
const nodeCrypto = await import('node:crypto');
|
|
16
|
+
return nodeCrypto.webcrypto.subtle;
|
|
17
|
+
}
|
|
18
|
+
function hexToBytes(hex) {
|
|
19
|
+
const out = new Uint8Array(hex.length / 2);
|
|
20
|
+
for (let i = 0; i < out.length; i++)
|
|
21
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
function timingSafeEqual(a, b) {
|
|
25
|
+
if (a.length !== b.length)
|
|
26
|
+
return false;
|
|
27
|
+
let diff = 0;
|
|
28
|
+
for (let i = 0; i < a.length; i++)
|
|
29
|
+
diff |= a[i] ^ b[i];
|
|
30
|
+
return diff === 0;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* True when `header` is a valid CutOptim signature of `rawBody` under one of `secrets`, inside the time window.
|
|
34
|
+
* `rawBody` must be the exact bytes received (string or Uint8Array).
|
|
35
|
+
*/
|
|
36
|
+
export async function verifyWebhook(rawBody, header, secrets, opts = {}) {
|
|
37
|
+
if (typeof header !== 'string' || header.length > 4096)
|
|
38
|
+
return false;
|
|
39
|
+
let t = null;
|
|
40
|
+
const v1 = [];
|
|
41
|
+
for (const part of header.split(',')) {
|
|
42
|
+
const i = part.indexOf('=');
|
|
43
|
+
if (i <= 0)
|
|
44
|
+
return false;
|
|
45
|
+
const k = part.slice(0, i).trim(), v = part.slice(i + 1).trim();
|
|
46
|
+
if (k === 't') {
|
|
47
|
+
if (t !== null || !/^\d{1,12}$/.test(v))
|
|
48
|
+
return false;
|
|
49
|
+
t = Number(v);
|
|
50
|
+
}
|
|
51
|
+
else if (k === 'v1') {
|
|
52
|
+
if (!/^[0-9a-f]{64}$/.test(v))
|
|
53
|
+
return false;
|
|
54
|
+
v1.push(v);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (t === null || !v1.length)
|
|
58
|
+
return false;
|
|
59
|
+
const now = opts.nowS ?? Math.floor(Date.now() / 1000);
|
|
60
|
+
if (Math.abs(now - t) > (opts.toleranceS ?? DEFAULT_TOLERANCE_S))
|
|
61
|
+
return false;
|
|
62
|
+
const body = typeof rawBody === 'string' ? enc.encode(rawBody) : rawBody;
|
|
63
|
+
const signed = new Uint8Array(enc.encode(`${t}.`).length + body.length);
|
|
64
|
+
signed.set(enc.encode(`${t}.`), 0);
|
|
65
|
+
signed.set(body, enc.encode(`${t}.`).length);
|
|
66
|
+
const s = await subtle();
|
|
67
|
+
for (const secret of Array.isArray(secrets) ? secrets : [secrets]) {
|
|
68
|
+
const key = await s.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
69
|
+
const mac = new Uint8Array(await s.sign('HMAC', key, signed));
|
|
70
|
+
if (v1.some((h) => timingSafeEqual(mac, hexToBytes(h))))
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cutoptim/engine",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Official client for the CutOptim Engine API — cutting optimization (2D panels, 1D linear, timber, true-shape nesting) over HTTPS.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "CutOptim <support@cutoptim.com>",
|
|
7
7
|
"homepage": "https://cutoptim.com/engine/reference",
|
|
8
|
-
"repository": {
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/cutoptim/engine-js.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/cutoptim/engine-js/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"cutoptim",
|
|
17
|
+
"cutting optimization",
|
|
18
|
+
"cut list",
|
|
19
|
+
"nesting",
|
|
20
|
+
"panel saw",
|
|
21
|
+
"api client"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"sideEffects": false,
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc -p tsconfig.json",
|
|
43
|
+
"prepublishOnly": "npm run build"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"typescript": "^5.6.0"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public",
|
|
50
|
+
"provenance": true
|
|
51
|
+
}
|
|
11
52
|
}
|