@praxium/sdk 0.4.87 → 0.5.89

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.
@@ -70,6 +70,33 @@ interface WebhookFailure {
70
70
  }
71
71
  /** Result of processWebhook() */
72
72
  type WebhookResult = WebhookSuccess | WebhookFailure;
73
+ /**
74
+ * Pluggable structured logger for createRevalidationHandler.
75
+ *
76
+ * Each method receives a string message + an optional structured context
77
+ * object. The handler NEVER logs the secret, signature value, or request
78
+ * body — only structural facts (HTTP status, entity name, path counts,
79
+ * error class names, duration).
80
+ *
81
+ * Default: routes to `console.{log,warn,error}` — the correct sink for
82
+ * Vercel/Next.js where function logs surface in the Vercel dashboard.
83
+ * Override to wire Pino / OpenTelemetry, or a no-op logger for tests.
84
+ */
85
+ interface RevalidationLogger {
86
+ info: (message: string, context?: Record<string, unknown>) => void;
87
+ warn: (message: string, context?: Record<string, unknown>) => void;
88
+ error: (message: string, context?: Record<string, unknown>) => void;
89
+ }
90
+ /**
91
+ * Per-path revalidation failure. Returned in the response body for both
92
+ * 200 partial-success and 500 total-failure cases. `errorName` is the
93
+ * thrown error's class name only — never the message (which could leak
94
+ * PII or implementation details).
95
+ */
96
+ interface PathFailure {
97
+ path: string;
98
+ errorName: string;
99
+ }
73
100
  /** Configuration for createRevalidationHandler() */
74
101
  interface RevalidationConfig {
75
102
  /** Shared secret for HMAC signature verification */
@@ -93,6 +120,13 @@ interface RevalidationConfig {
93
120
  * }
94
121
  */
95
122
  pathMap: Record<string, string[]>;
123
+ /**
124
+ * Pluggable structured logger. Defaults to `console.{log,warn,error}`,
125
+ * which is the right sink for Vercel/Next.js (function logs surface in
126
+ * the Vercel dashboard). Pass a no-op logger for tests, or wire in
127
+ * Pino/OpenTelemetry for custom observability stacks.
128
+ */
129
+ logger?: RevalidationLogger;
96
130
  }
97
131
  /**
98
132
  * Process and verify a Praxium platform webhook.
@@ -132,11 +166,34 @@ declare function resolveRevalidationPaths(entity: string | undefined, pathMap: R
132
166
  fallback: boolean;
133
167
  };
134
168
  /**
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>`
169
+ * Production-grade Next.js route handler for Praxium revalidation webhooks.
170
+ *
171
+ * Wraps `processWebhook()` + `resolveRevalidationPaths()` with three
172
+ * tenant-grade concerns the lower-level primitives don't own:
173
+ *
174
+ * 1. **PII-safe structural logging at every branch** — missing header,
175
+ * bad signature format, expired timestamp, signature mismatch,
176
+ * unknown entity (fallback), per-path failures, total success. Logs
177
+ * NEVER include the secret, signature value, or request body. They
178
+ * DO include: HTTP status, entity name, path counts, error class
179
+ * names, duration. Sink defaults to `console.*` (Vercel/Next.js);
180
+ * override via `config.logger` for tests / Pino / OpenTelemetry.
181
+ *
182
+ * 2. **Per-path try/catch + partial-success semantics** — one failing
183
+ * `revalidatePath()` call does NOT abort the rest of the batch:
184
+ * - all paths succeed → 200, info log, no warnings
185
+ * - partial (N/M failed) → 200 with `failures[]` in body, warn log;
186
+ * the dispatcher sees OK and does NOT retry, because retrying
187
+ * would re-revalidate the paths that already succeeded
188
+ * - all paths fail → 500 with `failures[]`, error log; dispatcher
189
+ * retries the whole webhook
190
+ *
191
+ * 3. **Unknown-entity fallback warning** — `resolveRevalidationPaths`
192
+ * falls back to revalidating every registered path when it doesn't
193
+ * recognise the entity; this emits a warn so drift between the
194
+ * monolith's entity enum and the tenant's `pathMap` is visible.
195
+ *
196
+ * Returns a Fetch-API handler `(request: Request) => Promise<Response>`
140
197
  * compatible with Next.js App Router route exports.
141
198
  *
142
199
  * @example
@@ -157,4 +214,4 @@ declare function resolveRevalidationPaths(entity: string | undefined, pathMap: R
157
214
  */
158
215
  declare function createRevalidationHandler(config: RevalidationConfig): (request: Request) => Promise<Response>;
159
216
 
160
- export { type ProcessWebhookInput, type RevalidationConfig, WEBHOOK_SIGNATURE_HEADER, type WebhookFailure, type WebhookResult, type WebhookSuccess, createRevalidationHandler, processWebhook, resolveRevalidationPaths };
217
+ export { type PathFailure, type ProcessWebhookInput, type RevalidationConfig, type RevalidationLogger, WEBHOOK_SIGNATURE_HEADER, type WebhookFailure, type WebhookResult, type WebhookSuccess, createRevalidationHandler, processWebhook, resolveRevalidationPaths };
package/dist/webhooks.js CHANGED
@@ -83,10 +83,20 @@ function resolveRevalidationPaths(entity, pathMap) {
83
83
  const allPaths = [...new Set(Object.values(pathMap).flat())];
84
84
  return { paths: allPaths, fallback: true };
85
85
  }
86
+ var DEFAULT_LOGGER = {
87
+ info: (message, context) => context !== void 0 ? console.log(message, context) : console.log(message),
88
+ warn: (message, context) => context !== void 0 ? console.warn(message, context) : console.warn(message),
89
+ error: (message, context) => context !== void 0 ? console.error(message, context) : console.error(message)
90
+ };
86
91
  function createRevalidationHandler(config) {
92
+ const log = config.logger ?? DEFAULT_LOGGER;
87
93
  return async (request) => {
94
+ const start = Date.now();
88
95
  const signatureHeader = request.headers.get(WEBHOOK_SIGNATURE_HEADER);
89
96
  if (!signatureHeader) {
97
+ log.warn(
98
+ `[revalidate] 401 \u2014 missing ${WEBHOOK_SIGNATURE_HEADER} header (${Date.now() - start}ms)`
99
+ );
90
100
  return Response.json(
91
101
  { error: `Missing ${WEBHOOK_SIGNATURE_HEADER} header` },
92
102
  { status: 401 }
@@ -101,6 +111,9 @@ function createRevalidationHandler(config) {
101
111
  });
102
112
  if (!result.valid) {
103
113
  const status = result.error === "Invalid JSON body" ? 400 : 401;
114
+ log.warn(
115
+ `[revalidate] ${status} \u2014 ${result.error} (bodyBytes=${rawBody.length}, ${Date.now() - start}ms)`
116
+ );
104
117
  return Response.json({ error: result.error }, { status });
105
118
  }
106
119
  const { paths, fallback } = resolveRevalidationPaths(
@@ -108,22 +121,69 @@ function createRevalidationHandler(config) {
108
121
  config.pathMap
109
122
  );
110
123
  if (fallback) {
111
- console.warn(
112
- `[Webhook] Unknown entity "${result.entity ?? "(none)"}" \u2014 falling back to full revalidation (${paths.length} paths)`
124
+ log.warn(
125
+ `[revalidate] unknown entity "${result.entity ?? "(none)"}" \u2014 falling back to full revalidation (${paths.length} paths)`
113
126
  );
114
127
  }
128
+ let revalidatePath;
115
129
  try {
116
- const { revalidatePath } = await import("next/cache");
117
- for (const path of paths) {
118
- revalidatePath(path);
119
- }
130
+ const mod = await import("next/cache");
131
+ revalidatePath = mod.revalidatePath;
120
132
  } catch {
133
+ log.error(
134
+ `[revalidate] 500 \u2014 next/cache module not available (${Date.now() - start}ms)`
135
+ );
121
136
  return Response.json(
122
137
  { error: "Revalidation failed \u2014 next/cache not available" },
123
138
  { status: 500 }
124
139
  );
125
140
  }
126
- return Response.json({ revalidated: true, paths });
141
+ const failures = [];
142
+ for (const path of paths) {
143
+ try {
144
+ revalidatePath(path);
145
+ } catch (error) {
146
+ failures.push({
147
+ path,
148
+ errorName: error instanceof Error ? error.name : "unknown"
149
+ });
150
+ }
151
+ }
152
+ const succeeded = paths.length - failures.length;
153
+ const durationMs = Date.now() - start;
154
+ if (failures.length === paths.length && paths.length > 0) {
155
+ log.error("[revalidate] 500 \u2014 all revalidatePath calls threw", {
156
+ entity: result.entity,
157
+ pathCount: paths.length,
158
+ firstErrorName: failures[0].errorName,
159
+ durationMs
160
+ });
161
+ return Response.json(
162
+ { error: "Revalidation failed", failures },
163
+ { status: 500 }
164
+ );
165
+ }
166
+ if (failures.length > 0) {
167
+ log.warn(
168
+ "[revalidate] partial success \u2014 some revalidatePath calls threw",
169
+ {
170
+ entity: result.entity,
171
+ succeeded,
172
+ failed: failures.length,
173
+ totalPaths: paths.length,
174
+ failures,
175
+ durationMs
176
+ }
177
+ );
178
+ }
179
+ log.info(
180
+ `[revalidate] 200 \u2014 entity="${result.entity ?? "(none)"}" fallback=${fallback} succeeded=${succeeded}/${paths.length} (${durationMs}ms)`
181
+ );
182
+ return Response.json({
183
+ revalidated: true,
184
+ paths,
185
+ ...failures.length > 0 && { failures }
186
+ });
127
187
  };
128
188
  }
129
189
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxium/sdk",
3
- "version": "0.4.87",
3
+ "version": "0.5.89",
4
4
  "description": "Official TypeScript SDK for the Praxium platform API",
5
5
  "type": "module",
6
6
  "exports": {