@praxium/sdk 0.2.13 → 0.2.15
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/dist/revalidation.d.ts +16 -2
- package/dist/revalidation.js +68 -7
- package/package.json +1 -1
package/dist/revalidation.d.ts
CHANGED
|
@@ -3,7 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* When admin updates data in the platform, a webhook triggers
|
|
5
5
|
* revalidation on the tenant's public website. This handler
|
|
6
|
-
* validates the
|
|
6
|
+
* validates the HMAC signature and calls Next.js revalidatePath().
|
|
7
|
+
*
|
|
8
|
+
* Signature scheme (H6):
|
|
9
|
+
* X-Praxium-Signature: t={unix_timestamp},sha256={HMAC-SHA256(secret, timestamp.body)}
|
|
10
|
+
*
|
|
11
|
+
* Security features:
|
|
12
|
+
* - HMAC-SHA256 signature verification (secret never sent in body)
|
|
13
|
+
* - Timestamp-based replay protection (default 5-minute window)
|
|
14
|
+
* - Timing-safe signature comparison (prevents timing side-channel attacks)
|
|
15
|
+
* - Web Crypto API (platform-agnostic: Edge, Node.js, browser)
|
|
7
16
|
*
|
|
8
17
|
* Usage in tenant website's `app/api/revalidate/route.ts`:
|
|
9
18
|
*
|
|
@@ -16,12 +25,17 @@
|
|
|
16
25
|
* ```
|
|
17
26
|
*/
|
|
18
27
|
interface RevalidationConfig {
|
|
19
|
-
/** Shared secret for
|
|
28
|
+
/** Shared secret for HMAC signature verification */
|
|
20
29
|
secret: string;
|
|
30
|
+
/** Maximum age of request timestamp in milliseconds (default: 5 minutes) */
|
|
31
|
+
maxTimestampAge?: number;
|
|
21
32
|
}
|
|
22
33
|
/**
|
|
23
34
|
* Creates a Next.js route handler that revalidates ISR pages
|
|
24
35
|
* when triggered by the Praxium platform webhook.
|
|
36
|
+
*
|
|
37
|
+
* Authentication uses HMAC-SHA256 signature verification with
|
|
38
|
+
* timestamp-based replay protection (H6 security scheme).
|
|
25
39
|
*/
|
|
26
40
|
declare function createRevalidationHandler(config: RevalidationConfig): (request: Request) => Promise<Response>;
|
|
27
41
|
|
package/dist/revalidation.js
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
// src/revalidation.ts
|
|
2
|
+
var SIGNATURE_HEADER = "X-Praxium-Signature";
|
|
3
|
+
var DEFAULT_MAX_TIMESTAMP_AGE_MS = 5 * 60 * 1e3;
|
|
4
|
+
async function computeHmacSignature(secret, payload) {
|
|
5
|
+
const encoder = new TextEncoder();
|
|
6
|
+
const key = await crypto.subtle.importKey(
|
|
7
|
+
"raw",
|
|
8
|
+
encoder.encode(secret),
|
|
9
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
10
|
+
false,
|
|
11
|
+
["sign"]
|
|
12
|
+
);
|
|
13
|
+
const signature = await crypto.subtle.sign(
|
|
14
|
+
"HMAC",
|
|
15
|
+
key,
|
|
16
|
+
encoder.encode(payload)
|
|
17
|
+
);
|
|
18
|
+
return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
19
|
+
}
|
|
2
20
|
function isSecretValid(provided, expected) {
|
|
3
21
|
const encoder = new TextEncoder();
|
|
4
22
|
const a = encoder.encode(provided);
|
|
@@ -12,20 +30,63 @@ function isSecretValid(provided, expected) {
|
|
|
12
30
|
}
|
|
13
31
|
return mismatch === 0;
|
|
14
32
|
}
|
|
33
|
+
function parseSignatureHeader(headerValue) {
|
|
34
|
+
const timestampMatch = headerValue.match(/t=(\d+)/);
|
|
35
|
+
const signatureMatch = headerValue.match(/sha256=([a-f0-9]+)/);
|
|
36
|
+
if (!timestampMatch || !signatureMatch) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const timestamp = parseInt(timestampMatch[1], 10);
|
|
40
|
+
if (isNaN(timestamp) || timestamp <= 0) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
timestamp,
|
|
45
|
+
signature: signatureMatch[1]
|
|
46
|
+
};
|
|
47
|
+
}
|
|
15
48
|
function createRevalidationHandler(config) {
|
|
49
|
+
const maxTimestampAge = config.maxTimestampAge ?? DEFAULT_MAX_TIMESTAMP_AGE_MS;
|
|
16
50
|
return async (request) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
51
|
+
const signatureHeader = request.headers.get(SIGNATURE_HEADER);
|
|
52
|
+
if (!signatureHeader) {
|
|
53
|
+
return Response.json(
|
|
54
|
+
{ error: `Missing ${SIGNATURE_HEADER} header` },
|
|
55
|
+
{ status: 401 }
|
|
56
|
+
);
|
|
22
57
|
}
|
|
23
|
-
|
|
58
|
+
const parsed = parseSignatureHeader(signatureHeader);
|
|
59
|
+
if (!parsed) {
|
|
24
60
|
return Response.json(
|
|
25
|
-
{ error: "Invalid
|
|
61
|
+
{ error: "Invalid signature format" },
|
|
26
62
|
{ status: 401 }
|
|
27
63
|
);
|
|
28
64
|
}
|
|
65
|
+
const requestAgeMs = Date.now() - parsed.timestamp * 1e3;
|
|
66
|
+
if (requestAgeMs > maxTimestampAge) {
|
|
67
|
+
return Response.json(
|
|
68
|
+
{ error: "Request timestamp expired" },
|
|
69
|
+
{ status: 401 }
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const rawBody = await request.text();
|
|
73
|
+
const payload = `${parsed.timestamp}.${rawBody}`;
|
|
74
|
+
const expectedSignature = await computeHmacSignature(
|
|
75
|
+
config.secret,
|
|
76
|
+
payload
|
|
77
|
+
);
|
|
78
|
+
if (!isSecretValid(parsed.signature, expectedSignature)) {
|
|
79
|
+
return Response.json(
|
|
80
|
+
{ error: "Invalid signature" },
|
|
81
|
+
{ status: 401 }
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
let body;
|
|
85
|
+
try {
|
|
86
|
+
body = rawBody ? JSON.parse(rawBody) : {};
|
|
87
|
+
} catch {
|
|
88
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
89
|
+
}
|
|
29
90
|
const paths = body.paths ?? ["/"];
|
|
30
91
|
try {
|
|
31
92
|
const { revalidatePath } = await import("next/cache");
|