@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.
@@ -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 secret and calls Next.js revalidatePath().
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 webhook authentication */
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
 
@@ -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
- let body;
18
- try {
19
- body = await request.json();
20
- } catch {
21
- return Response.json({ error: "Invalid JSON body" }, { status: 400 });
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
- if (!isSecretValid(body.secret, config.secret)) {
58
+ const parsed = parseSignatureHeader(signatureHeader);
59
+ if (!parsed) {
24
60
  return Response.json(
25
- { error: "Invalid revalidation secret" },
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxium/sdk",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "description": "Official TypeScript SDK for the Praxium platform API",
5
5
  "type": "module",
6
6
  "exports": {