@praxium/sdk 0.3.56 → 0.3.66
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 +86 -11
- 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
|
@@ -2,14 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
Official TypeScript SDK for the [Praxium](https://praxium.nl) platform API. Build tenant websites that display practice data (team, services, FAQ, opening hours) with full locale support.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- [Quick Start](#quick-start) — Installation and usage examples
|
|
6
|
+
- [Configuration](#configuration) — Environment variables and tenant routing
|
|
7
|
+
- [Available Methods](#available-methods) — All API endpoints
|
|
8
|
+
- [Error Handling](#error-handling) — Typed error classes
|
|
9
|
+
- [Webhooks](#webhooks) — React to entity changes (ISR revalidation, cache busting, etc.)
|
|
10
|
+
- [Contributing](#contributing) — Development commands
|
|
11
|
+
|
|
12
|
+
## Quick Start
|
|
6
13
|
|
|
7
14
|
```bash
|
|
8
15
|
npm install @praxium/sdk
|
|
9
16
|
```
|
|
10
17
|
|
|
11
|
-
## Quick Start
|
|
12
|
-
|
|
13
18
|
### Locale-Specific Mode
|
|
14
19
|
|
|
15
20
|
Pass a locale to get pre-resolved labels and values in that language:
|
|
@@ -72,6 +77,32 @@ const result = await client.submitContactForm({
|
|
|
72
77
|
// → { success: true, emailStatus: 'sent' }
|
|
73
78
|
```
|
|
74
79
|
|
|
80
|
+
## Configuration
|
|
81
|
+
|
|
82
|
+
Your website needs two environment variables to connect to the Praxium platform, plus an optional third for webhook-based cache revalidation:
|
|
83
|
+
|
|
84
|
+
**Required:**
|
|
85
|
+
|
|
86
|
+
| Variable | Purpose | Example |
|
|
87
|
+
|----------|---------|---------|
|
|
88
|
+
| `PRAXIUM_API_URL` | Your tenant's admin portal URL. The SDK sends API requests to this host. | `https://ijfysio.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_ijfysio_default_17..._abc...` |
|
|
90
|
+
|
|
91
|
+
**Optional (only if using ISR revalidation webhooks):**
|
|
92
|
+
|
|
93
|
+
| Variable | Purpose | Example |
|
|
94
|
+
|----------|---------|---------|
|
|
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
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# .env.local
|
|
99
|
+
PRAXIUM_API_URL="https://mypractice.admin.praxium.nl"
|
|
100
|
+
PRAXIUM_API_KEY="hmac_v1_mypractice_default_1234567890_abcdef..."
|
|
101
|
+
PRAXIUM_WEBHOOK_SECRET="your-webhook-secret-from-admin-portal" # optional
|
|
102
|
+
```
|
|
103
|
+
|
|
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.
|
|
105
|
+
|
|
75
106
|
## Available Methods
|
|
76
107
|
|
|
77
108
|
| Method | Description |
|
|
@@ -118,21 +149,39 @@ try {
|
|
|
118
149
|
| `PraxiumRateLimitError` | 429 | Too many requests |
|
|
119
150
|
| `PraxiumError` | Other | Base class for all errors |
|
|
120
151
|
|
|
121
|
-
##
|
|
152
|
+
## Webhooks
|
|
153
|
+
|
|
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:**
|
|
122
157
|
|
|
123
|
-
|
|
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.
|
|
124
174
|
|
|
125
175
|
The `pathMap` is fully customizable — you define which pages to revalidate for each entity based on your website's routing structure:
|
|
126
176
|
|
|
127
177
|
```typescript
|
|
128
178
|
// app/api/revalidate/route.ts
|
|
129
|
-
import { createRevalidationHandler } from '@praxium/sdk/
|
|
179
|
+
import { createRevalidationHandler } from '@praxium/sdk/webhooks'
|
|
130
180
|
|
|
131
181
|
export const POST = createRevalidationHandler({
|
|
132
|
-
secret: process.env.
|
|
182
|
+
secret: process.env.PRAXIUM_WEBHOOK_SECRET!,
|
|
133
183
|
// Map platform entities to YOUR website's page paths.
|
|
134
|
-
//
|
|
135
|
-
// adjust the paths to match your pages.
|
|
184
|
+
// Adjust the paths to match your project's routing structure.
|
|
136
185
|
pathMap: {
|
|
137
186
|
'team': ['/nl/team', '/en/team'],
|
|
138
187
|
'rates': ['/nl/tarieven', '/en/rates'],
|
|
@@ -143,9 +192,35 @@ export const POST = createRevalidationHandler({
|
|
|
143
192
|
})
|
|
144
193
|
```
|
|
145
194
|
|
|
146
|
-
|
|
195
|
+
Requires `next` as a peer dependency.
|
|
196
|
+
|
|
197
|
+
### Custom Webhook Handler
|
|
198
|
+
|
|
199
|
+
For non-Next.js use cases or custom logic, use `processWebhook()` to verify the signature and extract the changed entity:
|
|
200
|
+
|
|
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}`)
|
|
147
220
|
|
|
148
|
-
|
|
221
|
+
return new Response('OK')
|
|
222
|
+
}
|
|
223
|
+
```
|
|
149
224
|
|
|
150
225
|
## Contributing
|
|
151
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.66",
|
|
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 };
|