@praxium/sdk 0.3.61 → 0.3.68
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 -15
- package/dist/webhooks.d.ts +160 -0
- package/dist/{revalidation.js → webhooks.js} +47 -43
- package/package.json +6 -6
- package/dist/revalidation.d.ts +0 -95
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Official TypeScript SDK for the [Praxium](https://praxium.nl) platform API. Buil
|
|
|
6
6
|
- [Configuration](#configuration) — Environment variables and tenant routing
|
|
7
7
|
- [Available Methods](#available-methods) — All API endpoints
|
|
8
8
|
- [Error Handling](#error-handling) — Typed error classes
|
|
9
|
-
- [
|
|
9
|
+
- [Webhooks](#webhooks) — React to entity changes (ISR revalidation, cache busting, etc.)
|
|
10
10
|
- [Contributing](#contributing) — Development commands
|
|
11
11
|
|
|
12
12
|
## Quick Start
|
|
@@ -85,20 +85,20 @@ Your website needs two environment variables to connect to the Praxium platform,
|
|
|
85
85
|
|
|
86
86
|
| Variable | Purpose | Example |
|
|
87
87
|
|----------|---------|---------|
|
|
88
|
-
| `PRAXIUM_API_URL` | Your tenant's admin portal URL. The SDK sends API requests to this host. | `https://
|
|
89
|
-
| `PRAXIUM_API_KEY` | HMAC API key for authentication. Generated in the admin portal under API Profiles. The tenant slug is embedded in the key — no need to configure it separately. | `
|
|
88
|
+
| `PRAXIUM_API_URL` | Your tenant's admin portal URL. The SDK sends API requests to this host. | `https://mypractice.admin.praxium.nl` |
|
|
89
|
+
| `PRAXIUM_API_KEY` | HMAC API key for authentication. Generated in the admin portal under API Profiles. The tenant slug is embedded in the key — no need to configure it separately. | `hmac_v1_mypractice_default_17..._abc...` |
|
|
90
90
|
|
|
91
91
|
**Optional (only if using ISR revalidation webhooks):**
|
|
92
92
|
|
|
93
93
|
| Variable | Purpose | Example |
|
|
94
94
|
|----------|---------|---------|
|
|
95
|
-
| `
|
|
95
|
+
| `PRAXIUM_WEBHOOK_SECRET` | Shared secret for webhook signature verification. Only needed if you want the platform to notify your site when data changes (team, FAQ, opening hours). See [Webhooks](#webhooks). | (32+ character random string) |
|
|
96
96
|
|
|
97
97
|
```bash
|
|
98
98
|
# .env.local
|
|
99
99
|
PRAXIUM_API_URL="https://mypractice.admin.praxium.nl"
|
|
100
100
|
PRAXIUM_API_KEY="hmac_v1_mypractice_default_1234567890_abcdef..."
|
|
101
|
-
|
|
101
|
+
PRAXIUM_WEBHOOK_SECRET="your-webhook-secret-from-admin-portal" # optional
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
> **How tenant routing works:** Each tenant has its own admin subdomain (`{slug}.admin.praxium.nl`). The platform identifies your tenant from both the hostname AND the API key's embedded slug, cross-validating them for security. You don't need to configure the tenant slug separately — it's derived from your API key automatically.
|
|
@@ -149,21 +149,39 @@ try {
|
|
|
149
149
|
| `PraxiumRateLimitError` | 429 | Too many requests |
|
|
150
150
|
| `PraxiumError` | Other | Base class for all errors |
|
|
151
151
|
|
|
152
|
-
##
|
|
152
|
+
## Webhooks
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
When an admin updates data (team, FAQ, opening hours), the platform sends an HMAC-signed webhook to your registered endpoint. You can use this to react to entity changes in any way your application needs.
|
|
155
|
+
|
|
156
|
+
**Common use cases:**
|
|
157
|
+
|
|
158
|
+
| Use Case | What you do on entity change |
|
|
159
|
+
|----------|------------------------------|
|
|
160
|
+
| **ISR revalidation** | Call `revalidatePath()` to refresh cached pages |
|
|
161
|
+
| **Search index** | Re-index changed entities in your search engine |
|
|
162
|
+
| **CDN cache** | Purge cached API responses or assets |
|
|
163
|
+
|
|
164
|
+
**Prerequisites:**
|
|
165
|
+
|
|
166
|
+
1. Register your webhook endpoint URL in the admin portal (Settings → Webhooks)
|
|
167
|
+
2. Set `PRAXIUM_WEBHOOK_SECRET` to the shared secret from the admin portal
|
|
168
|
+
|
|
169
|
+
All webhook handlers include HMAC-SHA256 signature verification, timestamp-based replay protection (5-minute window), and timing-safe comparison.
|
|
170
|
+
|
|
171
|
+
### Next.js ISR Revalidation
|
|
172
|
+
|
|
173
|
+
The SDK provides a ready-made handler for Next.js [ISR](https://nextjs.org/docs/app/building-your-application/data-fetching/incremental-static-regeneration) revalidation. It verifies the webhook signature and calls `revalidatePath()` for the affected pages.
|
|
155
174
|
|
|
156
175
|
The `pathMap` is fully customizable — you define which pages to revalidate for each entity based on your website's routing structure:
|
|
157
176
|
|
|
158
177
|
```typescript
|
|
159
178
|
// app/api/revalidate/route.ts
|
|
160
|
-
import { createRevalidationHandler } from '@praxium/sdk/
|
|
179
|
+
import { createRevalidationHandler } from '@praxium/sdk/webhooks'
|
|
161
180
|
|
|
162
181
|
export const POST = createRevalidationHandler({
|
|
163
|
-
secret: process.env.
|
|
182
|
+
secret: process.env.PRAXIUM_WEBHOOK_SECRET!,
|
|
164
183
|
// Map platform entities to YOUR website's page paths.
|
|
165
|
-
//
|
|
166
|
-
// adjust the paths to match your pages.
|
|
184
|
+
// Adjust the paths to match your project's routing structure.
|
|
167
185
|
pathMap: {
|
|
168
186
|
'team': ['/nl/team', '/en/team'],
|
|
169
187
|
'rates': ['/nl/tarieven', '/en/rates'],
|
|
@@ -174,12 +192,35 @@ export const POST = createRevalidationHandler({
|
|
|
174
192
|
})
|
|
175
193
|
```
|
|
176
194
|
|
|
177
|
-
|
|
195
|
+
Requires `next` as a peer dependency.
|
|
196
|
+
|
|
197
|
+
### Custom Webhook Handler
|
|
178
198
|
|
|
179
|
-
|
|
180
|
-
2. Set the `PRAXIUM_REVALIDATION_SECRET` env var to match the webhook secret from the admin portal
|
|
199
|
+
For non-Next.js use cases or custom logic, use `processWebhook()` to verify the signature and extract the changed entity:
|
|
181
200
|
|
|
182
|
-
|
|
201
|
+
```typescript
|
|
202
|
+
// Example: invalidate a Redis cache on entity change
|
|
203
|
+
import { processWebhook, WEBHOOK_SIGNATURE_HEADER } from '@praxium/sdk/webhooks'
|
|
204
|
+
|
|
205
|
+
export async function POST(request: Request) {
|
|
206
|
+
const body = await request.text()
|
|
207
|
+
|
|
208
|
+
const result = await processWebhook({
|
|
209
|
+
body,
|
|
210
|
+
signature: request.headers.get(WEBHOOK_SIGNATURE_HEADER)!,
|
|
211
|
+
secret: process.env.PRAXIUM_WEBHOOK_SECRET!,
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
if (!result.valid) {
|
|
215
|
+
return new Response(result.error, { status: 401 })
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// result.entity = 'team' | 'faq' | 'rates' | ...
|
|
219
|
+
await redis.del(`cache:${result.entity}`)
|
|
220
|
+
|
|
221
|
+
return new Response('OK')
|
|
222
|
+
}
|
|
223
|
+
```
|
|
183
224
|
|
|
184
225
|
## Contributing
|
|
185
226
|
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Praxium Webhook Utilities
|
|
3
|
+
*
|
|
4
|
+
* Process and verify webhooks sent by the Praxium platform when entity
|
|
5
|
+
* data changes (team, FAQ, opening hours, etc.).
|
|
6
|
+
*
|
|
7
|
+
* Two levels of abstraction:
|
|
8
|
+
*
|
|
9
|
+
* **`processWebhook()`** — Framework-agnostic. Verifies HMAC signature,
|
|
10
|
+
* checks timestamp freshness, parses the body, and returns the entity
|
|
11
|
+
* that changed. Use this with Express, Fastify, or any custom handler.
|
|
12
|
+
*
|
|
13
|
+
* **`createRevalidationHandler()`** — Next.js-specific. Wraps processWebhook
|
|
14
|
+
* with path resolution and `revalidatePath()` calls. Returns a ready-made
|
|
15
|
+
* route handler for `app/api/revalidate/route.ts`.
|
|
16
|
+
*
|
|
17
|
+
* Signature scheme (H6):
|
|
18
|
+
* X-Praxium-Signature: t={unix_timestamp},sha256={HMAC-SHA256(secret, timestamp.body)}
|
|
19
|
+
*
|
|
20
|
+
* Security features:
|
|
21
|
+
* - HMAC-SHA256 signature verification (secret never sent in body)
|
|
22
|
+
* - Timestamp-based replay protection (default 5-minute window)
|
|
23
|
+
* - Timing-safe signature comparison (prevents timing side-channel attacks)
|
|
24
|
+
* - Web Crypto API (platform-agnostic: Edge, Node.js, browser)
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* // Framework-agnostic (Express, Fastify, etc.):
|
|
28
|
+
* import { processWebhook } from '@praxium/sdk/webhooks'
|
|
29
|
+
*
|
|
30
|
+
* const result = await processWebhook({
|
|
31
|
+
* body: rawBody,
|
|
32
|
+
* signature: req.headers['x-praxium-signature'],
|
|
33
|
+
* secret: process.env.PRAXIUM_WEBHOOK_SECRET!,
|
|
34
|
+
* })
|
|
35
|
+
* if (result.valid) console.log('Entity changed:', result.entity)
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* // Next.js ISR revalidation:
|
|
39
|
+
* import { createRevalidationHandler } from '@praxium/sdk/webhooks'
|
|
40
|
+
*
|
|
41
|
+
* export const POST = createRevalidationHandler({
|
|
42
|
+
* secret: process.env.PRAXIUM_WEBHOOK_SECRET!,
|
|
43
|
+
* pathMap: { 'team': ['/nl/team', '/en/team'] },
|
|
44
|
+
* })
|
|
45
|
+
*/
|
|
46
|
+
/** Header name for the HMAC signature */
|
|
47
|
+
declare const WEBHOOK_SIGNATURE_HEADER = "X-Praxium-Signature";
|
|
48
|
+
/** Input for processWebhook() */
|
|
49
|
+
interface ProcessWebhookInput {
|
|
50
|
+
/** Raw request body as string */
|
|
51
|
+
body: string;
|
|
52
|
+
/** Value of the X-Praxium-Signature header */
|
|
53
|
+
signature: string;
|
|
54
|
+
/** Shared secret for HMAC verification */
|
|
55
|
+
secret: string;
|
|
56
|
+
/** Maximum age of request timestamp in milliseconds (default: 5 minutes) */
|
|
57
|
+
maxTimestampAge?: number;
|
|
58
|
+
}
|
|
59
|
+
/** Successful webhook processing result */
|
|
60
|
+
interface WebhookSuccess {
|
|
61
|
+
valid: true;
|
|
62
|
+
/** Entity type that changed (e.g., 'team', 'rates', 'faq') */
|
|
63
|
+
entity: string | undefined;
|
|
64
|
+
}
|
|
65
|
+
/** Failed webhook processing result */
|
|
66
|
+
interface WebhookFailure {
|
|
67
|
+
valid: false;
|
|
68
|
+
/** Human-readable error description */
|
|
69
|
+
error: string;
|
|
70
|
+
}
|
|
71
|
+
/** Result of processWebhook() */
|
|
72
|
+
type WebhookResult = WebhookSuccess | WebhookFailure;
|
|
73
|
+
/** Configuration for createRevalidationHandler() */
|
|
74
|
+
interface RevalidationConfig {
|
|
75
|
+
/** Shared secret for HMAC signature verification */
|
|
76
|
+
secret: string;
|
|
77
|
+
/** Maximum age of request timestamp in milliseconds (default: 5 minutes) */
|
|
78
|
+
maxTimestampAge?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Entity → full paths to revalidate (including locale prefix).
|
|
81
|
+
*
|
|
82
|
+
* Paths are specific to YOUR website's routing structure.
|
|
83
|
+
* When a webhook arrives with `{ entity: 'team' }`, the handler looks up
|
|
84
|
+
* `pathMap['team']` and calls `revalidatePath()` for each path.
|
|
85
|
+
*
|
|
86
|
+
* Unknown entities fall back to revalidating ALL registered paths.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* pathMap: {
|
|
90
|
+
* 'team': ['/nl/team', '/en/team'],
|
|
91
|
+
* 'rates': ['/nl/tarieven', '/en/rates'],
|
|
92
|
+
* 'opening-hours': ['/nl', '/en'],
|
|
93
|
+
* }
|
|
94
|
+
*/
|
|
95
|
+
pathMap: Record<string, string[]>;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Process and verify a Praxium platform webhook.
|
|
99
|
+
*
|
|
100
|
+
* Verifies the HMAC-SHA256 signature, checks timestamp freshness (replay
|
|
101
|
+
* protection), and parses the webhook body to extract the changed entity.
|
|
102
|
+
*
|
|
103
|
+
* Use this for custom webhook handling in any framework (Express, Fastify,
|
|
104
|
+
* Deno, etc.). For Next.js ISR revalidation, use createRevalidationHandler()
|
|
105
|
+
* which wraps this with path resolution and revalidatePath() calls.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* import { processWebhook } from '@praxium/sdk/webhooks'
|
|
110
|
+
*
|
|
111
|
+
* const result = await processWebhook({
|
|
112
|
+
* body: rawBody,
|
|
113
|
+
* signature: req.headers['x-praxium-signature'],
|
|
114
|
+
* secret: process.env.PRAXIUM_WEBHOOK_SECRET!,
|
|
115
|
+
* })
|
|
116
|
+
*
|
|
117
|
+
* if (result.valid) {
|
|
118
|
+
* await redis.del(`cache:${result.entity}`)
|
|
119
|
+
* }
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
declare function processWebhook(input: ProcessWebhookInput): Promise<WebhookResult>;
|
|
123
|
+
/**
|
|
124
|
+
* Resolve entity to revalidation paths using the pathMap.
|
|
125
|
+
*
|
|
126
|
+
* Resolution priority:
|
|
127
|
+
* 1. pathMap[entity] → pre-computed paths for this entity
|
|
128
|
+
* 2. Unknown/missing entity → deduplicated union of ALL pathMap values
|
|
129
|
+
*/
|
|
130
|
+
declare function resolveRevalidationPaths(entity: string | undefined, pathMap: Record<string, string[]>): {
|
|
131
|
+
paths: string[];
|
|
132
|
+
fallback: boolean;
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Creates a Next.js route handler that revalidates ISR pages
|
|
136
|
+
* when triggered by a Praxium platform webhook.
|
|
137
|
+
*
|
|
138
|
+
* Wraps processWebhook() with path resolution and revalidatePath() calls.
|
|
139
|
+
* Returns a Fetch API handler `(request: Request) => Promise<Response>`
|
|
140
|
+
* compatible with Next.js App Router route exports.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* // app/api/revalidate/route.ts
|
|
145
|
+
* import { createRevalidationHandler } from '@praxium/sdk/webhooks'
|
|
146
|
+
*
|
|
147
|
+
* export const POST = createRevalidationHandler({
|
|
148
|
+
* secret: process.env.PRAXIUM_WEBHOOK_SECRET!,
|
|
149
|
+
* pathMap: {
|
|
150
|
+
* 'team': ['/nl/team', '/en/team'],
|
|
151
|
+
* 'rates': ['/nl/tarieven', '/en/rates'],
|
|
152
|
+
* 'faq': ['/nl/faq', '/en/faq'],
|
|
153
|
+
* 'opening-hours': ['/nl', '/en'],
|
|
154
|
+
* },
|
|
155
|
+
* })
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
declare function createRevalidationHandler(config: RevalidationConfig): (request: Request) => Promise<Response>;
|
|
159
|
+
|
|
160
|
+
export { type ProcessWebhookInput, type RevalidationConfig, WEBHOOK_SIGNATURE_HEADER, type WebhookFailure, type WebhookResult, type WebhookSuccess, createRevalidationHandler, processWebhook, resolveRevalidationPaths };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
var
|
|
1
|
+
// src/webhooks.ts
|
|
2
|
+
var WEBHOOK_SIGNATURE_HEADER = "X-Praxium-Signature";
|
|
3
3
|
var DEFAULT_MAX_TIMESTAMP_AGE_MS = 5 * 60 * 1e3;
|
|
4
4
|
var CLOCK_SKEW_TOLERANCE_MS = 5 * 60 * 1e3;
|
|
5
5
|
async function computeHmacSignature(secret, payload) {
|
|
@@ -46,6 +46,36 @@ function parseSignatureHeader(headerValue) {
|
|
|
46
46
|
signature: signatureMatch[1]
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
+
async function processWebhook(input) {
|
|
50
|
+
const {
|
|
51
|
+
body,
|
|
52
|
+
signature: signatureHeader,
|
|
53
|
+
secret,
|
|
54
|
+
maxTimestampAge = DEFAULT_MAX_TIMESTAMP_AGE_MS
|
|
55
|
+
} = input;
|
|
56
|
+
const parsed = parseSignatureHeader(signatureHeader);
|
|
57
|
+
if (!parsed) {
|
|
58
|
+
return { valid: false, error: "Invalid signature format" };
|
|
59
|
+
}
|
|
60
|
+
const requestAgeMs = Date.now() - parsed.timestamp * 1e3;
|
|
61
|
+
if (requestAgeMs < -CLOCK_SKEW_TOLERANCE_MS) {
|
|
62
|
+
return { valid: false, error: "Request timestamp is in the future" };
|
|
63
|
+
}
|
|
64
|
+
if (requestAgeMs > maxTimestampAge) {
|
|
65
|
+
return { valid: false, error: "Request timestamp expired" };
|
|
66
|
+
}
|
|
67
|
+
const payload = `${parsed.timestamp}.${body}`;
|
|
68
|
+
const expectedSignature = await computeHmacSignature(secret, payload);
|
|
69
|
+
if (!isSecretValid(parsed.signature, expectedSignature)) {
|
|
70
|
+
return { valid: false, error: "Invalid signature" };
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const parsedBody = body ? JSON.parse(body) : {};
|
|
74
|
+
return { valid: true, entity: parsedBody.entity };
|
|
75
|
+
} catch {
|
|
76
|
+
return { valid: false, error: "Invalid JSON body" };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
49
79
|
function resolveRevalidationPaths(entity, pathMap) {
|
|
50
80
|
if (entity && pathMap[entity]) {
|
|
51
81
|
return { paths: pathMap[entity], fallback: false };
|
|
@@ -54,60 +84,32 @@ function resolveRevalidationPaths(entity, pathMap) {
|
|
|
54
84
|
return { paths: allPaths, fallback: true };
|
|
55
85
|
}
|
|
56
86
|
function createRevalidationHandler(config) {
|
|
57
|
-
const maxTimestampAge = config.maxTimestampAge ?? DEFAULT_MAX_TIMESTAMP_AGE_MS;
|
|
58
87
|
return async (request) => {
|
|
59
|
-
const signatureHeader = request.headers.get(
|
|
88
|
+
const signatureHeader = request.headers.get(WEBHOOK_SIGNATURE_HEADER);
|
|
60
89
|
if (!signatureHeader) {
|
|
61
90
|
return Response.json(
|
|
62
|
-
{ error: `Missing ${
|
|
63
|
-
{ status: 401 }
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
const parsed = parseSignatureHeader(signatureHeader);
|
|
67
|
-
if (!parsed) {
|
|
68
|
-
return Response.json(
|
|
69
|
-
{ error: "Invalid signature format" },
|
|
70
|
-
{ status: 401 }
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
const requestAgeMs = Date.now() - parsed.timestamp * 1e3;
|
|
74
|
-
if (requestAgeMs < -CLOCK_SKEW_TOLERANCE_MS) {
|
|
75
|
-
return Response.json(
|
|
76
|
-
{ error: "Request timestamp is in the future" },
|
|
77
|
-
{ status: 401 }
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
if (requestAgeMs > maxTimestampAge) {
|
|
81
|
-
return Response.json(
|
|
82
|
-
{ error: "Request timestamp expired" },
|
|
91
|
+
{ error: `Missing ${WEBHOOK_SIGNATURE_HEADER} header` },
|
|
83
92
|
{ status: 401 }
|
|
84
93
|
);
|
|
85
94
|
}
|
|
86
95
|
const rawBody = await request.text();
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
let body;
|
|
99
|
-
try {
|
|
100
|
-
body = rawBody ? JSON.parse(rawBody) : {};
|
|
101
|
-
} catch {
|
|
102
|
-
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
96
|
+
const result = await processWebhook({
|
|
97
|
+
body: rawBody,
|
|
98
|
+
signature: signatureHeader,
|
|
99
|
+
secret: config.secret,
|
|
100
|
+
maxTimestampAge: config.maxTimestampAge
|
|
101
|
+
});
|
|
102
|
+
if (!result.valid) {
|
|
103
|
+
const status = result.error === "Invalid JSON body" ? 400 : 401;
|
|
104
|
+
return Response.json({ error: result.error }, { status });
|
|
103
105
|
}
|
|
104
106
|
const { paths, fallback } = resolveRevalidationPaths(
|
|
105
|
-
|
|
107
|
+
result.entity,
|
|
106
108
|
config.pathMap
|
|
107
109
|
);
|
|
108
110
|
if (fallback) {
|
|
109
111
|
console.warn(
|
|
110
|
-
`[
|
|
112
|
+
`[Webhook] Unknown entity "${result.entity ?? "(none)"}" \u2014 falling back to full revalidation (${paths.length} paths)`
|
|
111
113
|
);
|
|
112
114
|
}
|
|
113
115
|
try {
|
|
@@ -125,6 +127,8 @@ function createRevalidationHandler(config) {
|
|
|
125
127
|
};
|
|
126
128
|
}
|
|
127
129
|
export {
|
|
130
|
+
WEBHOOK_SIGNATURE_HEADER,
|
|
128
131
|
createRevalidationHandler,
|
|
132
|
+
processWebhook,
|
|
129
133
|
resolveRevalidationPaths
|
|
130
134
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@praxium/sdk",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.68",
|
|
4
4
|
"description": "Official TypeScript SDK for the Praxium platform API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
"import": "./dist/index.js",
|
|
9
9
|
"types": "./dist/index.d.ts"
|
|
10
10
|
},
|
|
11
|
-
"./
|
|
12
|
-
"import": "./dist/
|
|
13
|
-
"types": "./dist/
|
|
11
|
+
"./webhooks": {
|
|
12
|
+
"import": "./dist/webhooks.js",
|
|
13
|
+
"types": "./dist/webhooks.d.ts"
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
20
|
"generate": "openapi-ts",
|
|
21
|
-
"build": "tsup
|
|
22
|
-
"typecheck": "tsc --noEmit",
|
|
21
|
+
"build": "tsup",
|
|
22
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
|
|
23
23
|
"test": "vitest run",
|
|
24
24
|
"test:watch": "vitest"
|
|
25
25
|
},
|
package/dist/revalidation.d.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ISR Revalidation Handler for Tenant Websites
|
|
3
|
-
*
|
|
4
|
-
* When admin updates data in the platform, a webhook triggers
|
|
5
|
-
* revalidation on the tenant's public website. This handler
|
|
6
|
-
* validates the HMAC signature and calls Next.js revalidatePath().
|
|
7
|
-
*
|
|
8
|
-
* Architecture (entity-driven, three-layer path agnosticism):
|
|
9
|
-
* - Monolith sends { entity } — knows WHAT changed, not WHERE
|
|
10
|
-
* - SDK looks up pathMap[entity] — pure lookup + security, path-agnostic
|
|
11
|
-
* - Tenant website provides pathMap — owns WHERE pages live (locale-prefixed)
|
|
12
|
-
*
|
|
13
|
-
* Signature scheme (H6):
|
|
14
|
-
* X-Praxium-Signature: t={unix_timestamp},sha256={HMAC-SHA256(secret, timestamp.body)}
|
|
15
|
-
*
|
|
16
|
-
* Security features:
|
|
17
|
-
* - HMAC-SHA256 signature verification (secret never sent in body)
|
|
18
|
-
* - Timestamp-based replay protection (default 5-minute window)
|
|
19
|
-
* - Timing-safe signature comparison (prevents timing side-channel attacks)
|
|
20
|
-
* - Web Crypto API (platform-agnostic: Edge, Node.js, browser)
|
|
21
|
-
*
|
|
22
|
-
* Usage in tenant website's `app/api/revalidate/route.ts`:
|
|
23
|
-
*
|
|
24
|
-
* ```ts
|
|
25
|
-
* import { createRevalidationHandler } from '@praxium/sdk/revalidation'
|
|
26
|
-
*
|
|
27
|
-
* export const POST = createRevalidationHandler({
|
|
28
|
-
* secret: process.env.PRAXIUM_REVALIDATION_SECRET!,
|
|
29
|
-
* pathMap: {
|
|
30
|
-
* 'team': ['/nl/team', '/en/team'],
|
|
31
|
-
* 'rates': ['/nl/tarieven', '/en/rates'],
|
|
32
|
-
* 'faq': ['/nl/faq', '/en/faq'],
|
|
33
|
-
* 'opening-hours': ['/nl', '/en'],
|
|
34
|
-
* 'all': ['/nl', '/en'],
|
|
35
|
-
* },
|
|
36
|
-
* })
|
|
37
|
-
* ```
|
|
38
|
-
*/
|
|
39
|
-
interface RevalidationConfig {
|
|
40
|
-
/** Shared secret for HMAC signature verification */
|
|
41
|
-
secret: string;
|
|
42
|
-
/** Maximum age of request timestamp in milliseconds (default: 5 minutes) */
|
|
43
|
-
maxTimestampAge?: number;
|
|
44
|
-
/**
|
|
45
|
-
* Entity → full paths to revalidate (including locale prefix).
|
|
46
|
-
*
|
|
47
|
-
* Paths are pre-computed by the tenant website — SDK does no path manipulation.
|
|
48
|
-
* Each tenant configures its own paths based on its URL structure and locales.
|
|
49
|
-
*
|
|
50
|
-
* When a webhook arrives with `{ entity: 'team' }`, the SDK looks up
|
|
51
|
-
* `pathMap['team']` and calls `revalidatePath()` for each path.
|
|
52
|
-
*
|
|
53
|
-
* Unknown entities fall back to revalidating ALL registered paths (union of
|
|
54
|
-
* all pathMap values) with a console warning.
|
|
55
|
-
*
|
|
56
|
-
* @example
|
|
57
|
-
* pathMap: {
|
|
58
|
-
* 'team': ['/nl/team', '/en/team'],
|
|
59
|
-
* 'rates': ['/nl/tarieven', '/en/rates'],
|
|
60
|
-
* 'opening-hours': ['/nl', '/en'],
|
|
61
|
-
* }
|
|
62
|
-
*/
|
|
63
|
-
pathMap: Record<string, string[]>;
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Resolve entity to revalidation paths using the tenant's pathMap.
|
|
67
|
-
*
|
|
68
|
-
* The SDK is path-agnostic — it performs a pure lookup against the
|
|
69
|
-
* tenant-provided pathMap. Paths are pre-computed by the tenant website
|
|
70
|
-
* including locale prefixes (e.g., '/nl/tarieven', '/en/rates').
|
|
71
|
-
*
|
|
72
|
-
* Resolution priority:
|
|
73
|
-
* 1. pathMap[entity] → pre-computed paths for this entity
|
|
74
|
-
* 2. Unknown/missing entity → deduplicated union of ALL pathMap values
|
|
75
|
-
* (full site revalidation as safe fallback)
|
|
76
|
-
*
|
|
77
|
-
* @returns paths to revalidate and whether fallback was used
|
|
78
|
-
*/
|
|
79
|
-
declare function resolveRevalidationPaths(entity: string | undefined, pathMap: Record<string, string[]>): {
|
|
80
|
-
paths: string[];
|
|
81
|
-
fallback: boolean;
|
|
82
|
-
};
|
|
83
|
-
/**
|
|
84
|
-
* Creates a Next.js route handler that revalidates ISR pages
|
|
85
|
-
* when triggered by the Praxium platform webhook.
|
|
86
|
-
*
|
|
87
|
-
* Authentication uses HMAC-SHA256 signature verification with
|
|
88
|
-
* timestamp-based replay protection (H6 security scheme).
|
|
89
|
-
*
|
|
90
|
-
* Path resolution is entity-driven: the monolith sends `{ entity }`,
|
|
91
|
-
* the SDK looks up `pathMap[entity]` for pre-computed full paths.
|
|
92
|
-
*/
|
|
93
|
-
declare function createRevalidationHandler(config: RevalidationConfig): (request: Request) => Promise<Response>;
|
|
94
|
-
|
|
95
|
-
export { createRevalidationHandler, resolveRevalidationPaths };
|