@praxium/sdk 0.4.85 → 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.
package/dist/index.d.ts CHANGED
@@ -674,7 +674,7 @@ type ClientLocale = SupportedLocale | '*';
674
674
  /**
675
675
  * Extract the tenant slug from a profile-scoped API key.
676
676
  *
677
- * Key format: praxium_v1_{tenantSlug}_{profileSlug}_{timestamp}_{signature}
677
+ * Key format: `${API_KEY_PREFIX}_{tenantSlug}_{profileSlug}_{timestamp}_{signature}`
678
678
  * Parts: [0]praxium [1]v1 [2]tenantSlug [3]profileSlug [4]timestamp [5]signature
679
679
  *
680
680
  * @throws Error if key format is invalid (wrong prefix or insufficient parts)
@@ -719,6 +719,20 @@ declare function createPraxiumClient(config: PraxiumClientConfig & {
719
719
  /** Locale-specific mode: returns PublicTeamMember[] with resolved labels/values */
720
720
  declare function createPraxiumClient(config: PraxiumClientConfig): PraxiumClient;
721
721
 
722
+ /** Version-prefixed identifier at the start of every Praxium API key. */
723
+ declare const API_KEY_PREFIX: "praxium_v1";
724
+ /** Separator used between all key segments. */
725
+ declare const API_KEY_SEPARATOR: "_";
726
+ /**
727
+ * Convenience composition: `praxium_v1_` (prefix + separator).
728
+ *
729
+ * Use this when validating that a string starts with the full Praxium API
730
+ * key prefix. Avoids re-composing `${API_KEY_PREFIX}${API_KEY_SEPARATOR}`
731
+ * at every call site. Compile-time `as const` so consumer Zod schemas can
732
+ * pass it to `.startsWith()` without widening to `string`.
733
+ */
734
+ declare const API_KEY_PREFIX_WITH_SEPARATOR: "praxium_v1_";
735
+
722
736
  /**
723
737
  * Typed error hierarchy for the Praxium SDK.
724
738
  *
@@ -849,4 +863,4 @@ declare function getCustomField(member: MultilingualTeamMember, identifier: stri
849
863
  */
850
864
  declare function localizeText(text: MultilingualText | null | undefined, locale: SupportedLocale): string;
851
865
 
852
- export { type ApiError, type BilingualText, type BookableService, type BookableServices, type BookableVariantInfo, type BusinessName, type ClientLocale, type ContactDetails, type ContactFormResult, type CustomField, type FaqCategory, type FaqContent, type FaqGroup, type FaqItem, type FeatureItem, type FeatureList, type InsuranceInfo, type InsuranceList, type Location, type MultilingualCustomField, type MultilingualPraxiumClient, type MultilingualTeamMember, type MultilingualText, type OpeningHours, type PaymentMethod, type PaymentMethodList, type PolicyInfo, type PolicyList, PraxiumAuthError, type PraxiumClient, type PraxiumClientConfig, PraxiumError, PraxiumForbiddenError, PraxiumNotFoundError, PraxiumRateLimitError, PraxiumValidationError, type PricingVariant, type PricingVariants, type PublicDaySchedule, type PublicTeamMember, type ServiceCategoryInfo, type SocialLinks, type SupportedLocale, type TeamMembers, type ValidationDetail, createPraxiumClient, extractTenantSlugFromApiKey, getCustomField, getCustomFieldValue, localizeText };
866
+ export { API_KEY_PREFIX, API_KEY_PREFIX_WITH_SEPARATOR, API_KEY_SEPARATOR, type ApiError, type BilingualText, type BookableService, type BookableServices, type BookableVariantInfo, type BusinessName, type ClientLocale, type ContactDetails, type ContactFormResult, type CustomField, type FaqCategory, type FaqContent, type FaqGroup, type FaqItem, type FeatureItem, type FeatureList, type InsuranceInfo, type InsuranceList, type Location, type MultilingualCustomField, type MultilingualPraxiumClient, type MultilingualTeamMember, type MultilingualText, type OpeningHours, type PaymentMethod, type PaymentMethodList, type PolicyInfo, type PolicyList, PraxiumAuthError, type PraxiumClient, type PraxiumClientConfig, PraxiumError, PraxiumForbiddenError, PraxiumNotFoundError, PraxiumRateLimitError, PraxiumValidationError, type PricingVariant, type PricingVariants, type PublicDaySchedule, type PublicTeamMember, type ServiceCategoryInfo, type SocialLinks, type SupportedLocale, type TeamMembers, type ValidationDetail, createPraxiumClient, extractTenantSlugFromApiKey, getCustomField, getCustomFieldValue, localizeText };
package/dist/index.js CHANGED
@@ -928,16 +928,19 @@ var STATUS_ERROR_MAP = {
928
928
  429: PraxiumRateLimitError
929
929
  };
930
930
 
931
- // src/tenant-client.ts
931
+ // src/api-key.constants.ts
932
932
  var API_KEY_PREFIX = "praxium_v1";
933
933
  var API_KEY_SEPARATOR = "_";
934
+ var API_KEY_PREFIX_WITH_SEPARATOR = `${API_KEY_PREFIX}${API_KEY_SEPARATOR}`;
935
+
936
+ // src/tenant-client.ts
934
937
  function extractTenantSlugFromApiKey(apiKey) {
935
938
  const parts = apiKey.split(API_KEY_SEPARATOR);
936
939
  if (parts.length < 6 || `${parts[0]}${API_KEY_SEPARATOR}${parts[1]}` !== API_KEY_PREFIX) {
937
940
  throw new PraxiumError(
938
941
  400,
939
942
  "INVALID_API_KEY_FORMAT",
940
- `API key must start with '${API_KEY_PREFIX}' and contain at least 6 segments. Format: praxium_v1_{tenantSlug}_{profileSlug}_{timestamp}_{signature}`
943
+ `API key must start with '${API_KEY_PREFIX}' and contain at least 6 segments. Format: ${API_KEY_PREFIX}${API_KEY_SEPARATOR}{tenantSlug}${API_KEY_SEPARATOR}{profileSlug}${API_KEY_SEPARATOR}{timestamp}${API_KEY_SEPARATOR}{signature}`
941
944
  );
942
945
  }
943
946
  return parts[2];
@@ -1013,6 +1016,9 @@ function localizeText(text, locale) {
1013
1016
  return text[locale] || text["en"] || Object.values(text)[0] || "";
1014
1017
  }
1015
1018
  export {
1019
+ API_KEY_PREFIX,
1020
+ API_KEY_PREFIX_WITH_SEPARATOR,
1021
+ API_KEY_SEPARATOR,
1016
1022
  PraxiumAuthError,
1017
1023
  PraxiumError,
1018
1024
  PraxiumForbiddenError,
@@ -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.85",
3
+ "version": "0.5.89",
4
4
  "description": "Official TypeScript SDK for the Praxium platform API",
5
5
  "type": "module",
6
6
  "exports": {
@@ -19,14 +19,18 @@
19
19
  "scripts": {
20
20
  "generate": "openapi-ts",
21
21
  "build": "tsup",
22
+ "lint": "eslint src/ tests/",
22
23
  "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
23
24
  "test": "vitest run",
24
25
  "test:watch": "vitest"
25
26
  },
26
27
  "devDependencies": {
28
+ "@eslint/js": "^9.0.0",
27
29
  "@hey-api/openapi-ts": "^0.92.4",
30
+ "eslint": "^9.0.0",
28
31
  "tsup": "^8.0.0",
29
32
  "typescript": "^5.7.0",
33
+ "typescript-eslint": "^8.0.0",
30
34
  "vitest": "^4.0.18"
31
35
  },
32
36
  "peerDependencies": {