@riverbankcms/sdk 0.15.0 → 0.16.0
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/_dts/sdk/src/version.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/client/client.js +1 -1
- package/dist/client/client.js.map +1 -1
- package/dist/client/client.mjs +1 -1
- package/dist/client/client.mjs.map +1 -1
- package/dist/server/{chunk-TXSJ2JRJ.mjs → chunk-4QZPS3BW.mjs} +2 -2
- package/dist/server/{chunk-UFPJXTZT.js → chunk-DRVSYEA2.js} +3 -3
- package/dist/server/{chunk-UFPJXTZT.js.map → chunk-DRVSYEA2.js.map} +1 -1
- package/dist/server/chunk-HBHOQOKE.mjs +7 -0
- package/dist/server/{chunk-JDYZBZKF.mjs.map → chunk-HBHOQOKE.mjs.map} +1 -1
- package/dist/server/{chunk-JUIFAUEE.js → chunk-NZQNS6AB.js} +2 -2
- package/dist/server/{chunk-JUIFAUEE.js.map → chunk-NZQNS6AB.js.map} +1 -1
- package/dist/server/{chunk-HMJM43I7.mjs → chunk-QP22XYPX.mjs} +2 -2
- package/dist/server/{chunk-ZESVLGDI.js → chunk-VD5JTAF2.js} +3 -3
- package/dist/server/{chunk-ZESVLGDI.js.map → chunk-VD5JTAF2.js.map} +1 -1
- package/dist/server/index.js +4 -4
- package/dist/server/index.mjs +3 -3
- package/dist/server/next.js +3 -3
- package/dist/server/next.mjs +2 -2
- package/dist/server/prebuild.js +3 -3
- package/dist/server/prebuild.mjs +2 -2
- package/dist/server/server.js +3 -3
- package/dist/server/server.mjs +2 -2
- package/package.json +1 -1
- package/dist/server/chunk-JDYZBZKF.mjs +0 -7
- /package/dist/server/{chunk-TXSJ2JRJ.mjs.map → chunk-4QZPS3BW.mjs.map} +0 -0
- /package/dist/server/{chunk-HMJM43I7.mjs.map → chunk-QP22XYPX.mjs.map} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SDK_VERSION
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-HBHOQOKE.mjs";
|
|
4
4
|
import {
|
|
5
5
|
ApiEnvelopeError,
|
|
6
6
|
ApiRequestError,
|
|
@@ -1114,4 +1114,4 @@ export {
|
|
|
1114
1114
|
RiverbankApiError,
|
|
1115
1115
|
createRiverbankClient
|
|
1116
1116
|
};
|
|
1117
|
-
//# sourceMappingURL=chunk-
|
|
1117
|
+
//# sourceMappingURL=chunk-4QZPS3BW.mjs.map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
var _chunkGPJB33BKjs = require('./chunk-GPJB33BK.js');
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
var
|
|
7
|
+
var _chunkNZQNS6ABjs = require('./chunk-NZQNS6AB.js');
|
|
8
8
|
|
|
9
9
|
// src/prebuild/index.ts
|
|
10
10
|
var _fs = require('fs'); var fs = _interopRequireWildcard(_fs);
|
|
@@ -312,7 +312,7 @@ async function prebuildCache(options) {
|
|
|
312
312
|
version: MANIFEST_VERSION,
|
|
313
313
|
generatedAt,
|
|
314
314
|
siteId,
|
|
315
|
-
sdkVersion:
|
|
315
|
+
sdkVersion: _chunkNZQNS6ABjs.SDK_VERSION,
|
|
316
316
|
keyToFile,
|
|
317
317
|
includedTypes: include,
|
|
318
318
|
fileCount: files.length,
|
|
@@ -336,4 +336,4 @@ async function prebuildCache(options) {
|
|
|
336
336
|
|
|
337
337
|
|
|
338
338
|
exports.prebuildCache = prebuildCache;
|
|
339
|
-
//# sourceMappingURL=chunk-
|
|
339
|
+
//# sourceMappingURL=chunk-DRVSYEA2.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/will/Projects/Business/cms/main-branch/packages/sdk/dist/server/chunk-UFPJXTZT.js","../../src/prebuild/index.ts"],"names":[],"mappings":"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","file":"/Users/will/Projects/Business/cms/main-branch/packages/sdk/dist/server/chunk-UFPJXTZT.js","sourcesContent":[null,"/**\n * Prebuild cache generation\n *\n * Exports static content at build time for use as a last-resort fallback\n * when the CMS is unavailable.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as crypto from 'crypto';\nimport type {\n PrebuildOptions,\n PrebuildResult,\n PrebuildProgress,\n PrebuildManifest,\n SiteCacheFile,\n PageCacheFile,\n PageIndexFile,\n EntriesCacheFile,\n NavigationCacheFile,\n FormsCacheFile,\n} from './types';\nimport type { RiverbankClient, SiteResponse, EntriesResponse } from '../client/types';\nimport { SDK_VERSION } from '../version';\nimport { PREBUILD_PAGE_SIZE, DEFAULT_PREBUILD_DIR } from '../constants';\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst MANIFEST_VERSION = '1.0.0';\nconst MANIFEST_FILENAME = 'manifest.json';\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Convert a URL path to a safe filename.\n * Sanitizes special characters that are invalid on some filesystems.\n * e.g., '/about/team' -> 'about-team.json'\n * e.g., '/api/users?id=1' -> 'api-users_id_1.json'\n * Note: '/' becomes '_home.json' to avoid collision with '_index.json' (page listing)\n */\nfunction pathToFilename(routePath: string): string {\n if (routePath === '/') return '_home.json';\n return (\n routePath\n .slice(1)\n // Replace special characters with underscores (?, *, :, etc.)\n .replace(/[^a-zA-Z0-9\\-\\/]/g, '_')\n // Replace slashes with dashes\n .replace(/\\//g, '-') + '.json'\n );\n}\n\n/**\n * Ensure a directory exists, creating it recursively if needed.\n */\nfunction ensureDir(dirPath: string): void {\n if (!fs.existsSync(dirPath)) {\n fs.mkdirSync(dirPath, { recursive: true });\n }\n}\n\n/**\n * Write JSON data to a file.\n */\nfunction writeJsonFile(filePath: string, data: unknown): number {\n const content = JSON.stringify(data, null, 2);\n fs.writeFileSync(filePath, content, 'utf-8');\n return Buffer.byteLength(content, 'utf-8');\n}\n\n/**\n * Calculate SHA256 checksum of object (excluding checksum field).\n */\nfunction calculateChecksum(data: Record<string, unknown>): string {\n const { checksum: _, ...rest } = data;\n const content = JSON.stringify(rest);\n return crypto.createHash('sha256').update(content).digest('hex');\n}\n\n// ============================================================================\n// Entry Fetching\n// ============================================================================\n\n/**\n * Fetch all entries for a content type using pagination.\n * Loops until all entries are retrieved.\n */\nasync function fetchAllEntries(\n client: RiverbankClient,\n siteId: string,\n contentType: string,\n onProgress?: (item: string) => void\n): Promise<EntriesResponse['entries']> {\n const allEntries: EntriesResponse['entries'] = [];\n let offset = 0;\n let hasMore = true;\n\n while (hasMore) {\n onProgress?.(`${contentType} (batch ${Math.floor(offset / PREBUILD_PAGE_SIZE) + 1})`);\n\n const response = await client.getEntries({\n siteId,\n contentType,\n limit: PREBUILD_PAGE_SIZE,\n offset,\n preview: false, // Published content only\n });\n\n allEntries.push(...response.entries);\n offset += PREBUILD_PAGE_SIZE;\n hasMore = response.entries.length === PREBUILD_PAGE_SIZE;\n }\n\n return allEntries;\n}\n\n// ============================================================================\n// Content Type Prebuild Functions\n// ============================================================================\n\nasync function prebuildSite(\n client: RiverbankClient,\n siteId: string,\n outputDir: string\n): Promise<{ files: string[]; size: number; siteData: SiteResponse }> {\n const site = await client.getSite({ id: siteId });\n const filename = 'site.json';\n const filePath = path.join(outputDir, filename);\n\n const cacheFile: SiteCacheFile = {\n data: site,\n generatedAt: new Date().toISOString(),\n };\n\n const size = writeJsonFile(filePath, cacheFile);\n return { files: [filename], size, siteData: site };\n}\n\nasync function prebuildPages(\n client: RiverbankClient,\n siteId: string,\n routes: Record<string, { path: string; status: string | null }>,\n outputDir: string,\n onProgress?: (progress: PrebuildProgress) => void\n): Promise<{ files: string[]; size: number; pathMappings: Record<string, string> }> {\n const pagesDir = path.join(outputDir, 'pages');\n ensureDir(pagesDir);\n\n const files: string[] = [];\n const pathMappings: Record<string, string> = {}; // path -> filename\n let totalSize = 0;\n\n // Filter to published routes only\n const publishedPaths = Object.entries(routes)\n .filter(([_, route]) => route.status === 'published')\n .map(([_, route]) => route.path);\n\n // Create page index\n const pageIndex: PageIndexFile['pages'] = [];\n\n const total = publishedPaths.length;\n let current = 0;\n\n for (const pagePath of publishedPaths) {\n current++;\n onProgress?.({\n current,\n total,\n item: `Page: ${pagePath}`,\n contentType: 'pages',\n });\n\n try {\n const pageData = await client.getPage({ siteId, path: pagePath, preview: false });\n const filename = pathToFilename(pagePath);\n const filePath = path.join(pagesDir, filename);\n\n const cacheFile: PageCacheFile = {\n data: pageData,\n path: pagePath,\n generatedAt: new Date().toISOString(),\n };\n\n totalSize += writeJsonFile(filePath, cacheFile);\n files.push(`pages/${filename}`);\n pathMappings[pagePath] = `pages/${filename}`;\n\n // Add to index (only for page type responses)\n if ('page' in pageData) {\n pageIndex.push({\n path: pagePath,\n pageId: pageData.page.routeId || '',\n title: pageData.page.name || 'Untitled',\n });\n }\n } catch (error) {\n // Log but continue - partial prebuild is okay\n console.warn(`[Prebuild] Failed to fetch page ${pagePath}:`, (error as Error).message);\n }\n }\n\n // Write page index (uses _index.json to avoid collision with home page)\n const indexFile: PageIndexFile = {\n pages: pageIndex,\n totalCount: pageIndex.length,\n generatedAt: new Date().toISOString(),\n };\n const indexPath = path.join(pagesDir, '_index.json');\n totalSize += writeJsonFile(indexPath, indexFile);\n files.push('pages/_index.json');\n\n return { files, size: totalSize, pathMappings };\n}\n\nasync function prebuildEntries(\n client: RiverbankClient,\n siteId: string,\n contentTypes: string[],\n outputDir: string,\n onProgress?: (progress: PrebuildProgress) => void\n): Promise<{ files: string[]; size: number }> {\n const entriesDir = path.join(outputDir, 'entries');\n ensureDir(entriesDir);\n\n const files: string[] = [];\n let totalSize = 0;\n\n const total = contentTypes.length;\n let current = 0;\n\n for (const contentType of contentTypes) {\n current++;\n\n const entries = await fetchAllEntries(\n client,\n siteId,\n contentType,\n (item) => {\n onProgress?.({\n current,\n total,\n item: `Entries: ${item}`,\n contentType: 'entries',\n });\n }\n );\n\n // Create content type directory\n const typeDir = path.join(entriesDir, contentType);\n ensureDir(typeDir);\n\n const cacheFile: EntriesCacheFile = {\n entries,\n contentType,\n totalCount: entries.length,\n generatedAt: new Date().toISOString(),\n };\n\n const filename = `entries/${contentType}/all.json`;\n const filePath = path.join(entriesDir, contentType, 'all.json');\n totalSize += writeJsonFile(filePath, cacheFile);\n files.push(filename);\n }\n\n return { files, size: totalSize };\n}\n\nasync function prebuildNavigation(\n siteData: SiteResponse,\n outputDir: string,\n onProgress?: (progress: PrebuildProgress) => void\n): Promise<{ files: string[]; size: number }> {\n const navDir = path.join(outputDir, 'navigation');\n ensureDir(navDir);\n\n const files: string[] = [];\n let totalSize = 0;\n\n // Navigation data comes from site response\n const menus = siteData.navigation || [];\n\n onProgress?.({\n current: 1,\n total: 1,\n item: 'Navigation menus',\n contentType: 'navigation',\n });\n\n const cacheFile: NavigationCacheFile = {\n menus: menus.map((menu) => ({\n identifier: menu.identifier,\n id: menu.id,\n name: menu.name,\n items: menu.items,\n })),\n generatedAt: new Date().toISOString(),\n };\n\n const filename = 'navigation/menus.json';\n const filePath = path.join(navDir, 'menus.json');\n totalSize += writeJsonFile(filePath, cacheFile);\n files.push(filename);\n\n return { files, size: totalSize };\n}\n\nasync function prebuildForms(\n client: RiverbankClient,\n siteId: string,\n outputDir: string,\n onProgress?: (progress: PrebuildProgress) => void\n): Promise<{ files: string[]; size: number }> {\n const formsDir = path.join(outputDir, 'forms');\n ensureDir(formsDir);\n\n const files: string[] = [];\n let totalSize = 0;\n\n onProgress?.({\n current: 1,\n total: 1,\n item: 'Forms',\n contentType: 'forms',\n });\n\n try {\n // Fetch forms via the public API endpoint\n const response = await client.getForms({ siteId });\n const forms = response.forms || [];\n\n const cacheFile: FormsCacheFile = {\n forms: forms.map((form) => ({\n id: form.id,\n slug: form.slug,\n name: form.name,\n schema: form.schema,\n settings: form.settings,\n })),\n totalCount: forms.length,\n generatedAt: new Date().toISOString(),\n };\n\n const filename = 'forms/forms.json';\n const filePath = path.join(formsDir, 'forms.json');\n totalSize += writeJsonFile(filePath, cacheFile);\n files.push(filename);\n } catch (error) {\n console.warn('[Prebuild] Failed to fetch forms:', (error as Error).message);\n }\n\n return { files, size: totalSize };\n}\n\n// ============================================================================\n// Main Prebuild Function\n// ============================================================================\n\n/**\n * Generate a prebuild cache for the specified site.\n *\n * The prebuild cache contains static JSON files that can serve as a\n * last-resort fallback when the CMS is unavailable.\n *\n * @param options - Prebuild configuration\n * @returns Result with generated files and metadata\n *\n * @example\n * ```typescript\n * import { createRiverbankClient, prebuildCache } from '@riverbankcms/sdk';\n *\n * const client = createRiverbankClient({ apiKey, baseUrl });\n *\n * const result = await prebuildCache({\n * client,\n * siteId: 'your-site-id',\n * outputDir: '.riverbank-cache',\n * onProgress: (p) => console.log(`${p.current}/${p.total}: ${p.item}`),\n * });\n *\n * console.log(`Generated ${result.files.length} files (${result.totalSize} bytes)`);\n * ```\n */\nexport async function prebuildCache(options: PrebuildOptions): Promise<PrebuildResult> {\n const {\n client,\n siteId,\n outputDir = DEFAULT_PREBUILD_DIR,\n include = ['site', 'pages', 'entries', 'navigation', 'forms'],\n contentTypes,\n onProgress,\n } = options;\n\n const generatedAt = new Date().toISOString();\n const files: string[] = [];\n let totalSize = 0;\n const errors: string[] = [];\n let siteData: SiteResponse | null = null;\n let pagePathMappings: Record<string, string> = {}; // path -> filename\n\n // Ensure output directory exists\n ensureDir(outputDir);\n\n // -------------------------------------------------------------------------\n // 1. Prebuild site data (always needed for routes and navigation)\n // -------------------------------------------------------------------------\n if (include.includes('site') || include.includes('pages') || include.includes('navigation')) {\n try {\n const result = await prebuildSite(client, siteId, outputDir);\n siteData = result.siteData;\n if (include.includes('site')) {\n files.push(...result.files);\n totalSize += result.size;\n }\n onProgress?.({ current: 1, total: 4, item: 'Site data', contentType: 'site' });\n } catch (error) {\n errors.push(`Site prebuild failed: ${(error as Error).message}`);\n }\n }\n\n // -------------------------------------------------------------------------\n // 2. Prebuild pages\n // -------------------------------------------------------------------------\n if (include.includes('pages') && siteData) {\n try {\n const result = await prebuildPages(\n client,\n siteId,\n siteData.routes,\n outputDir,\n onProgress\n );\n files.push(...result.files);\n totalSize += result.size;\n pagePathMappings = result.pathMappings;\n } catch (error) {\n errors.push(`Pages prebuild failed: ${(error as Error).message}`);\n }\n }\n\n // -------------------------------------------------------------------------\n // 3. Prebuild entries\n // -------------------------------------------------------------------------\n if (include.includes('entries')) {\n try {\n // Content types must be explicitly provided via options.contentTypes\n // (The API's sdkConfig doesn't include content type definitions)\n const typesToPrebuild = contentTypes || [];\n\n if (typesToPrebuild.length > 0) {\n const result = await prebuildEntries(\n client,\n siteId,\n typesToPrebuild,\n outputDir,\n onProgress\n );\n files.push(...result.files);\n totalSize += result.size;\n } else {\n console.warn('[Prebuild] No contentTypes provided - skipping entries prebuild');\n }\n } catch (error) {\n errors.push(`Entries prebuild failed: ${(error as Error).message}`);\n }\n }\n\n // -------------------------------------------------------------------------\n // 4. Prebuild navigation\n // -------------------------------------------------------------------------\n if (include.includes('navigation') && siteData) {\n try {\n const result = await prebuildNavigation(siteData, outputDir, onProgress);\n files.push(...result.files);\n totalSize += result.size;\n } catch (error) {\n errors.push(`Navigation prebuild failed: ${(error as Error).message}`);\n }\n }\n\n // -------------------------------------------------------------------------\n // 5. Prebuild forms\n // -------------------------------------------------------------------------\n if (include.includes('forms')) {\n try {\n const result = await prebuildForms(client, siteId, outputDir, onProgress);\n files.push(...result.files);\n totalSize += result.size;\n } catch (error) {\n errors.push(`Forms prebuild failed: ${(error as Error).message}`);\n }\n }\n\n // -------------------------------------------------------------------------\n // 6. Generate manifest\n // -------------------------------------------------------------------------\n const keyToFile: Record<string, string> = {};\n\n // Map cache keys to files\n if (files.includes('site.json')) {\n keyToFile[`site:${siteId}`] = 'site.json';\n }\n\n // Use path mappings for pages (avoids lossy filename-to-path conversion)\n for (const [pagePath, file] of Object.entries(pagePathMappings)) {\n keyToFile[`page:${siteId}:${pagePath}:false`] = file;\n }\n\n // Add entries keys (entries/{contentType}/all.json)\n for (const file of files.filter((f) => f.startsWith('entries/') && f.endsWith('/all.json'))) {\n const contentType = file.split('/')[1];\n keyToFile[`entries-all:${siteId}:${contentType}`] = file;\n }\n\n // Add forms key\n if (files.includes('forms/forms.json')) {\n keyToFile[`forms:${siteId}`] = 'forms/forms.json';\n }\n\n const manifest: Omit<PrebuildManifest, 'checksum'> & { checksum?: string } = {\n version: MANIFEST_VERSION,\n generatedAt,\n siteId,\n sdkVersion: SDK_VERSION,\n keyToFile,\n includedTypes: include,\n fileCount: files.length,\n totalSize,\n };\n\n // Calculate checksum\n const checksum = calculateChecksum(manifest as Record<string, unknown>);\n (manifest as PrebuildManifest).checksum = checksum;\n\n const manifestPath = path.join(outputDir, MANIFEST_FILENAME);\n writeJsonFile(manifestPath, manifest);\n\n return {\n success: errors.length === 0,\n outputDir,\n files,\n totalSize,\n generatedAt,\n checksum,\n errors: errors.length > 0 ? errors : undefined,\n };\n}\n\n// Re-export types\nexport type {\n PrebuildOptions,\n PrebuildResult,\n PrebuildProgress,\n PrebuildManifest,\n PrebuildContentType,\n} from './types';\n"]}
|
|
1
|
+
{"version":3,"sources":["/Users/will/Projects/Business/cms/main-branch/packages/sdk/dist/server/chunk-DRVSYEA2.js","../../src/prebuild/index.ts"],"names":[],"mappings":"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","file":"/Users/will/Projects/Business/cms/main-branch/packages/sdk/dist/server/chunk-DRVSYEA2.js","sourcesContent":[null,"/**\n * Prebuild cache generation\n *\n * Exports static content at build time for use as a last-resort fallback\n * when the CMS is unavailable.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as crypto from 'crypto';\nimport type {\n PrebuildOptions,\n PrebuildResult,\n PrebuildProgress,\n PrebuildManifest,\n SiteCacheFile,\n PageCacheFile,\n PageIndexFile,\n EntriesCacheFile,\n NavigationCacheFile,\n FormsCacheFile,\n} from './types';\nimport type { RiverbankClient, SiteResponse, EntriesResponse } from '../client/types';\nimport { SDK_VERSION } from '../version';\nimport { PREBUILD_PAGE_SIZE, DEFAULT_PREBUILD_DIR } from '../constants';\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst MANIFEST_VERSION = '1.0.0';\nconst MANIFEST_FILENAME = 'manifest.json';\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Convert a URL path to a safe filename.\n * Sanitizes special characters that are invalid on some filesystems.\n * e.g., '/about/team' -> 'about-team.json'\n * e.g., '/api/users?id=1' -> 'api-users_id_1.json'\n * Note: '/' becomes '_home.json' to avoid collision with '_index.json' (page listing)\n */\nfunction pathToFilename(routePath: string): string {\n if (routePath === '/') return '_home.json';\n return (\n routePath\n .slice(1)\n // Replace special characters with underscores (?, *, :, etc.)\n .replace(/[^a-zA-Z0-9\\-\\/]/g, '_')\n // Replace slashes with dashes\n .replace(/\\//g, '-') + '.json'\n );\n}\n\n/**\n * Ensure a directory exists, creating it recursively if needed.\n */\nfunction ensureDir(dirPath: string): void {\n if (!fs.existsSync(dirPath)) {\n fs.mkdirSync(dirPath, { recursive: true });\n }\n}\n\n/**\n * Write JSON data to a file.\n */\nfunction writeJsonFile(filePath: string, data: unknown): number {\n const content = JSON.stringify(data, null, 2);\n fs.writeFileSync(filePath, content, 'utf-8');\n return Buffer.byteLength(content, 'utf-8');\n}\n\n/**\n * Calculate SHA256 checksum of object (excluding checksum field).\n */\nfunction calculateChecksum(data: Record<string, unknown>): string {\n const { checksum: _, ...rest } = data;\n const content = JSON.stringify(rest);\n return crypto.createHash('sha256').update(content).digest('hex');\n}\n\n// ============================================================================\n// Entry Fetching\n// ============================================================================\n\n/**\n * Fetch all entries for a content type using pagination.\n * Loops until all entries are retrieved.\n */\nasync function fetchAllEntries(\n client: RiverbankClient,\n siteId: string,\n contentType: string,\n onProgress?: (item: string) => void\n): Promise<EntriesResponse['entries']> {\n const allEntries: EntriesResponse['entries'] = [];\n let offset = 0;\n let hasMore = true;\n\n while (hasMore) {\n onProgress?.(`${contentType} (batch ${Math.floor(offset / PREBUILD_PAGE_SIZE) + 1})`);\n\n const response = await client.getEntries({\n siteId,\n contentType,\n limit: PREBUILD_PAGE_SIZE,\n offset,\n preview: false, // Published content only\n });\n\n allEntries.push(...response.entries);\n offset += PREBUILD_PAGE_SIZE;\n hasMore = response.entries.length === PREBUILD_PAGE_SIZE;\n }\n\n return allEntries;\n}\n\n// ============================================================================\n// Content Type Prebuild Functions\n// ============================================================================\n\nasync function prebuildSite(\n client: RiverbankClient,\n siteId: string,\n outputDir: string\n): Promise<{ files: string[]; size: number; siteData: SiteResponse }> {\n const site = await client.getSite({ id: siteId });\n const filename = 'site.json';\n const filePath = path.join(outputDir, filename);\n\n const cacheFile: SiteCacheFile = {\n data: site,\n generatedAt: new Date().toISOString(),\n };\n\n const size = writeJsonFile(filePath, cacheFile);\n return { files: [filename], size, siteData: site };\n}\n\nasync function prebuildPages(\n client: RiverbankClient,\n siteId: string,\n routes: Record<string, { path: string; status: string | null }>,\n outputDir: string,\n onProgress?: (progress: PrebuildProgress) => void\n): Promise<{ files: string[]; size: number; pathMappings: Record<string, string> }> {\n const pagesDir = path.join(outputDir, 'pages');\n ensureDir(pagesDir);\n\n const files: string[] = [];\n const pathMappings: Record<string, string> = {}; // path -> filename\n let totalSize = 0;\n\n // Filter to published routes only\n const publishedPaths = Object.entries(routes)\n .filter(([_, route]) => route.status === 'published')\n .map(([_, route]) => route.path);\n\n // Create page index\n const pageIndex: PageIndexFile['pages'] = [];\n\n const total = publishedPaths.length;\n let current = 0;\n\n for (const pagePath of publishedPaths) {\n current++;\n onProgress?.({\n current,\n total,\n item: `Page: ${pagePath}`,\n contentType: 'pages',\n });\n\n try {\n const pageData = await client.getPage({ siteId, path: pagePath, preview: false });\n const filename = pathToFilename(pagePath);\n const filePath = path.join(pagesDir, filename);\n\n const cacheFile: PageCacheFile = {\n data: pageData,\n path: pagePath,\n generatedAt: new Date().toISOString(),\n };\n\n totalSize += writeJsonFile(filePath, cacheFile);\n files.push(`pages/${filename}`);\n pathMappings[pagePath] = `pages/${filename}`;\n\n // Add to index (only for page type responses)\n if ('page' in pageData) {\n pageIndex.push({\n path: pagePath,\n pageId: pageData.page.routeId || '',\n title: pageData.page.name || 'Untitled',\n });\n }\n } catch (error) {\n // Log but continue - partial prebuild is okay\n console.warn(`[Prebuild] Failed to fetch page ${pagePath}:`, (error as Error).message);\n }\n }\n\n // Write page index (uses _index.json to avoid collision with home page)\n const indexFile: PageIndexFile = {\n pages: pageIndex,\n totalCount: pageIndex.length,\n generatedAt: new Date().toISOString(),\n };\n const indexPath = path.join(pagesDir, '_index.json');\n totalSize += writeJsonFile(indexPath, indexFile);\n files.push('pages/_index.json');\n\n return { files, size: totalSize, pathMappings };\n}\n\nasync function prebuildEntries(\n client: RiverbankClient,\n siteId: string,\n contentTypes: string[],\n outputDir: string,\n onProgress?: (progress: PrebuildProgress) => void\n): Promise<{ files: string[]; size: number }> {\n const entriesDir = path.join(outputDir, 'entries');\n ensureDir(entriesDir);\n\n const files: string[] = [];\n let totalSize = 0;\n\n const total = contentTypes.length;\n let current = 0;\n\n for (const contentType of contentTypes) {\n current++;\n\n const entries = await fetchAllEntries(\n client,\n siteId,\n contentType,\n (item) => {\n onProgress?.({\n current,\n total,\n item: `Entries: ${item}`,\n contentType: 'entries',\n });\n }\n );\n\n // Create content type directory\n const typeDir = path.join(entriesDir, contentType);\n ensureDir(typeDir);\n\n const cacheFile: EntriesCacheFile = {\n entries,\n contentType,\n totalCount: entries.length,\n generatedAt: new Date().toISOString(),\n };\n\n const filename = `entries/${contentType}/all.json`;\n const filePath = path.join(entriesDir, contentType, 'all.json');\n totalSize += writeJsonFile(filePath, cacheFile);\n files.push(filename);\n }\n\n return { files, size: totalSize };\n}\n\nasync function prebuildNavigation(\n siteData: SiteResponse,\n outputDir: string,\n onProgress?: (progress: PrebuildProgress) => void\n): Promise<{ files: string[]; size: number }> {\n const navDir = path.join(outputDir, 'navigation');\n ensureDir(navDir);\n\n const files: string[] = [];\n let totalSize = 0;\n\n // Navigation data comes from site response\n const menus = siteData.navigation || [];\n\n onProgress?.({\n current: 1,\n total: 1,\n item: 'Navigation menus',\n contentType: 'navigation',\n });\n\n const cacheFile: NavigationCacheFile = {\n menus: menus.map((menu) => ({\n identifier: menu.identifier,\n id: menu.id,\n name: menu.name,\n items: menu.items,\n })),\n generatedAt: new Date().toISOString(),\n };\n\n const filename = 'navigation/menus.json';\n const filePath = path.join(navDir, 'menus.json');\n totalSize += writeJsonFile(filePath, cacheFile);\n files.push(filename);\n\n return { files, size: totalSize };\n}\n\nasync function prebuildForms(\n client: RiverbankClient,\n siteId: string,\n outputDir: string,\n onProgress?: (progress: PrebuildProgress) => void\n): Promise<{ files: string[]; size: number }> {\n const formsDir = path.join(outputDir, 'forms');\n ensureDir(formsDir);\n\n const files: string[] = [];\n let totalSize = 0;\n\n onProgress?.({\n current: 1,\n total: 1,\n item: 'Forms',\n contentType: 'forms',\n });\n\n try {\n // Fetch forms via the public API endpoint\n const response = await client.getForms({ siteId });\n const forms = response.forms || [];\n\n const cacheFile: FormsCacheFile = {\n forms: forms.map((form) => ({\n id: form.id,\n slug: form.slug,\n name: form.name,\n schema: form.schema,\n settings: form.settings,\n })),\n totalCount: forms.length,\n generatedAt: new Date().toISOString(),\n };\n\n const filename = 'forms/forms.json';\n const filePath = path.join(formsDir, 'forms.json');\n totalSize += writeJsonFile(filePath, cacheFile);\n files.push(filename);\n } catch (error) {\n console.warn('[Prebuild] Failed to fetch forms:', (error as Error).message);\n }\n\n return { files, size: totalSize };\n}\n\n// ============================================================================\n// Main Prebuild Function\n// ============================================================================\n\n/**\n * Generate a prebuild cache for the specified site.\n *\n * The prebuild cache contains static JSON files that can serve as a\n * last-resort fallback when the CMS is unavailable.\n *\n * @param options - Prebuild configuration\n * @returns Result with generated files and metadata\n *\n * @example\n * ```typescript\n * import { createRiverbankClient, prebuildCache } from '@riverbankcms/sdk';\n *\n * const client = createRiverbankClient({ apiKey, baseUrl });\n *\n * const result = await prebuildCache({\n * client,\n * siteId: 'your-site-id',\n * outputDir: '.riverbank-cache',\n * onProgress: (p) => console.log(`${p.current}/${p.total}: ${p.item}`),\n * });\n *\n * console.log(`Generated ${result.files.length} files (${result.totalSize} bytes)`);\n * ```\n */\nexport async function prebuildCache(options: PrebuildOptions): Promise<PrebuildResult> {\n const {\n client,\n siteId,\n outputDir = DEFAULT_PREBUILD_DIR,\n include = ['site', 'pages', 'entries', 'navigation', 'forms'],\n contentTypes,\n onProgress,\n } = options;\n\n const generatedAt = new Date().toISOString();\n const files: string[] = [];\n let totalSize = 0;\n const errors: string[] = [];\n let siteData: SiteResponse | null = null;\n let pagePathMappings: Record<string, string> = {}; // path -> filename\n\n // Ensure output directory exists\n ensureDir(outputDir);\n\n // -------------------------------------------------------------------------\n // 1. Prebuild site data (always needed for routes and navigation)\n // -------------------------------------------------------------------------\n if (include.includes('site') || include.includes('pages') || include.includes('navigation')) {\n try {\n const result = await prebuildSite(client, siteId, outputDir);\n siteData = result.siteData;\n if (include.includes('site')) {\n files.push(...result.files);\n totalSize += result.size;\n }\n onProgress?.({ current: 1, total: 4, item: 'Site data', contentType: 'site' });\n } catch (error) {\n errors.push(`Site prebuild failed: ${(error as Error).message}`);\n }\n }\n\n // -------------------------------------------------------------------------\n // 2. Prebuild pages\n // -------------------------------------------------------------------------\n if (include.includes('pages') && siteData) {\n try {\n const result = await prebuildPages(\n client,\n siteId,\n siteData.routes,\n outputDir,\n onProgress\n );\n files.push(...result.files);\n totalSize += result.size;\n pagePathMappings = result.pathMappings;\n } catch (error) {\n errors.push(`Pages prebuild failed: ${(error as Error).message}`);\n }\n }\n\n // -------------------------------------------------------------------------\n // 3. Prebuild entries\n // -------------------------------------------------------------------------\n if (include.includes('entries')) {\n try {\n // Content types must be explicitly provided via options.contentTypes\n // (The API's sdkConfig doesn't include content type definitions)\n const typesToPrebuild = contentTypes || [];\n\n if (typesToPrebuild.length > 0) {\n const result = await prebuildEntries(\n client,\n siteId,\n typesToPrebuild,\n outputDir,\n onProgress\n );\n files.push(...result.files);\n totalSize += result.size;\n } else {\n console.warn('[Prebuild] No contentTypes provided - skipping entries prebuild');\n }\n } catch (error) {\n errors.push(`Entries prebuild failed: ${(error as Error).message}`);\n }\n }\n\n // -------------------------------------------------------------------------\n // 4. Prebuild navigation\n // -------------------------------------------------------------------------\n if (include.includes('navigation') && siteData) {\n try {\n const result = await prebuildNavigation(siteData, outputDir, onProgress);\n files.push(...result.files);\n totalSize += result.size;\n } catch (error) {\n errors.push(`Navigation prebuild failed: ${(error as Error).message}`);\n }\n }\n\n // -------------------------------------------------------------------------\n // 5. Prebuild forms\n // -------------------------------------------------------------------------\n if (include.includes('forms')) {\n try {\n const result = await prebuildForms(client, siteId, outputDir, onProgress);\n files.push(...result.files);\n totalSize += result.size;\n } catch (error) {\n errors.push(`Forms prebuild failed: ${(error as Error).message}`);\n }\n }\n\n // -------------------------------------------------------------------------\n // 6. Generate manifest\n // -------------------------------------------------------------------------\n const keyToFile: Record<string, string> = {};\n\n // Map cache keys to files\n if (files.includes('site.json')) {\n keyToFile[`site:${siteId}`] = 'site.json';\n }\n\n // Use path mappings for pages (avoids lossy filename-to-path conversion)\n for (const [pagePath, file] of Object.entries(pagePathMappings)) {\n keyToFile[`page:${siteId}:${pagePath}:false`] = file;\n }\n\n // Add entries keys (entries/{contentType}/all.json)\n for (const file of files.filter((f) => f.startsWith('entries/') && f.endsWith('/all.json'))) {\n const contentType = file.split('/')[1];\n keyToFile[`entries-all:${siteId}:${contentType}`] = file;\n }\n\n // Add forms key\n if (files.includes('forms/forms.json')) {\n keyToFile[`forms:${siteId}`] = 'forms/forms.json';\n }\n\n const manifest: Omit<PrebuildManifest, 'checksum'> & { checksum?: string } = {\n version: MANIFEST_VERSION,\n generatedAt,\n siteId,\n sdkVersion: SDK_VERSION,\n keyToFile,\n includedTypes: include,\n fileCount: files.length,\n totalSize,\n };\n\n // Calculate checksum\n const checksum = calculateChecksum(manifest as Record<string, unknown>);\n (manifest as PrebuildManifest).checksum = checksum;\n\n const manifestPath = path.join(outputDir, MANIFEST_FILENAME);\n writeJsonFile(manifestPath, manifest);\n\n return {\n success: errors.length === 0,\n outputDir,\n files,\n totalSize,\n generatedAt,\n checksum,\n errors: errors.length > 0 ? errors : undefined,\n };\n}\n\n// Re-export types\nexport type {\n PrebuildOptions,\n PrebuildResult,\n PrebuildProgress,\n PrebuildManifest,\n PrebuildContentType,\n} from './types';\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/version.ts"],"sourcesContent":["/**\n * SDK version - MUST match version in package.json\n *\n * This version is sent to the API in the x-sdk-version header\n * to help with debugging and compatibility tracking.\n *\n * IMPORTANT: When releasing a new SDK version, update BOTH:\n * 1. This constant\n * 2. The \"version\" field in package.json\n */\nexport const SDK_VERSION = '0.
|
|
1
|
+
{"version":3,"sources":["../../src/version.ts"],"sourcesContent":["/**\n * SDK version - MUST match version in package.json\n *\n * This version is sent to the API in the x-sdk-version header\n * to help with debugging and compatibility tracking.\n *\n * IMPORTANT: When releasing a new SDK version, update BOTH:\n * 1. This constant\n * 2. The \"version\" field in package.json\n */\nexport const SDK_VERSION = '0.16.0';\n"],"mappings":";AAUO,IAAM,cAAc;","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true});// src/version.ts
|
|
2
|
-
var SDK_VERSION = "0.
|
|
2
|
+
var SDK_VERSION = "0.16.0";
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
exports.SDK_VERSION = SDK_VERSION;
|
|
7
|
-
//# sourceMappingURL=chunk-
|
|
7
|
+
//# sourceMappingURL=chunk-NZQNS6AB.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/will/Projects/Business/cms/main-branch/packages/sdk/dist/server/chunk-
|
|
1
|
+
{"version":3,"sources":["/Users/will/Projects/Business/cms/main-branch/packages/sdk/dist/server/chunk-NZQNS6AB.js","../../src/version.ts"],"names":[],"mappings":"AAAA;ACUO,IAAM,YAAA,EAAc,QAAA;ADR3B;AACA;AACE;AACF,kCAAC","file":"/Users/will/Projects/Business/cms/main-branch/packages/sdk/dist/server/chunk-NZQNS6AB.js","sourcesContent":[null,"/**\n * SDK version - MUST match version in package.json\n *\n * This version is sent to the API in the x-sdk-version header\n * to help with debugging and compatibility tracking.\n *\n * IMPORTANT: When releasing a new SDK version, update BOTH:\n * 1. This constant\n * 2. The \"version\" field in package.json\n */\nexport const SDK_VERSION = '0.16.0';\n"]}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-3BUK2GUG.mjs";
|
|
5
5
|
import {
|
|
6
6
|
SDK_VERSION
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-HBHOQOKE.mjs";
|
|
8
8
|
|
|
9
9
|
// src/prebuild/index.ts
|
|
10
10
|
import * as fs from "fs";
|
|
@@ -336,4 +336,4 @@ async function prebuildCache(options) {
|
|
|
336
336
|
export {
|
|
337
337
|
prebuildCache
|
|
338
338
|
};
|
|
339
|
-
//# sourceMappingURL=chunk-
|
|
339
|
+
//# sourceMappingURL=chunk-QP22XYPX.mjs.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var _chunkNZQNS6ABjs = require('./chunk-NZQNS6AB.js');
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
@@ -545,7 +545,7 @@ function getPrebuildModule() {
|
|
|
545
545
|
return null;
|
|
546
546
|
}
|
|
547
547
|
}
|
|
548
|
-
_chunk3BURSKX3js.setSdkVersion.call(void 0,
|
|
548
|
+
_chunk3BURSKX3js.setSdkVersion.call(void 0, _chunkNZQNS6ABjs.SDK_VERSION);
|
|
549
549
|
var DEFAULT_BROWSER_TIMEOUT_MS = 5e3;
|
|
550
550
|
var DEFAULT_SERVER_TIMEOUT_MS = 8e3;
|
|
551
551
|
function generateRequestId() {
|
|
@@ -1114,4 +1114,4 @@ function createRiverbankClient(config) {
|
|
|
1114
1114
|
|
|
1115
1115
|
|
|
1116
1116
|
exports.RiverbankApiError = RiverbankApiError; exports.createRiverbankClient = createRiverbankClient;
|
|
1117
|
-
//# sourceMappingURL=chunk-
|
|
1117
|
+
//# sourceMappingURL=chunk-VD5JTAF2.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/will/Projects/Business/cms/main-branch/packages/sdk/dist/server/chunk-ZESVLGDI.js","../../src/client/error.ts","../../src/client/cache.ts","../../src/client/resilience.ts","../../src/client/index.ts"],"names":["errorInfo"],"mappings":"AAAA;AACE;AACF,sDAA4B;AAC5B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACF,sDAA4B;AAC5B;AACA;AC4CO,IAAM,kBAAA,EAAN,MAAM,mBAAA,QAA0B,MAAM;AAAA,EAgC3C,WAAA,CAAY,QAAA,EAAwD;AAClE,IAAA,KAAA,CAAM,QAAA,CAAS,OAAO,CAAA;AACtB,IAAA,IAAA,CAAK,KAAA,EAAO,mBAAA;AAEZ,IAAA,GAAA,CAAI,QAAA,GAAW,SAAA,GAAY,QAAA,CAAS,KAAA,EAAO;AACzC,MAAA,IAAA,CAAK,MAAA,EAAQ,QAAA,CAAS,KAAA;AAAA,IACxB;AACA,IAAA,IAAA,CAAK,KAAA,EAAO,QAAA,CAAS,IAAA;AACrB,IAAA,IAAA,CAAK,UAAA,EAAY,QAAA,CAAS,SAAA;AAC1B,IAAA,IAAA,CAAK,OAAA,EAAS,QAAA,CAAS,MAAA;AACvB,IAAA,IAAA,CAAK,YAAA,EAAc,QAAA,CAAS,WAAA;AAC5B,IAAA,IAAA,CAAK,UAAA,EAAY,QAAA,CAAS,SAAA;AAC1B,IAAA,IAAA,CAAK,aAAA,EAAe,eAAA,GAAkB,SAAA,EAAW,QAAA,CAAS,aAAA,EAAe,KAAA,CAAA;AACzE,IAAA,IAAA,CAAK,YAAA,EAAc,IAAA,CAAK,gBAAA,CAAiB,CAAA;AAGzC,IAAA,MAAA,CAAO,cAAA,CAAe,IAAA,EAAM,kBAAA,CAAkB,SAAS,CAAA;AAAA,EACzD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,gBAAA,CAAA,EAA4B;AAElC,IAAA,GAAA,CAAI,IAAA,CAAK,OAAA,IAAW,CAAA,EAAG,OAAO,IAAA;AAE9B,IAAA,GAAA,CAAI,IAAA,CAAK,OAAA,IAAW,GAAA,EAAK,OAAO,IAAA;AAEhC,IAAA,GAAA,CAAI,IAAA,CAAK,OAAA,GAAU,GAAA,EAAK,OAAO,IAAA;AAE/B,IAAA,OAAO,KAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,cAAA,CAAA,EAA0B;AACxB,IAAA,OAAO,IAAA,CAAK,IAAA,CAAK,UAAA,CAAW,UAAU,CAAA;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,EAAA,CAAG,IAAA,EAA6B;AAC9B,IAAA,OAAO,IAAA,CAAK,KAAA,IAAS,IAAA;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,WAAA,CAAA,EAAuB;AACrB,IAAA,OAAO,IAAA,CAAK,IAAA,CAAK,UAAA,CAAW,OAAO,CAAA;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBAAA,CAAA,EAA6B;AAC3B,IAAA,OAAO,IAAA,CAAK,IAAA,CAAK,UAAA,CAAW,aAAa,CAAA;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,eAAA,CAAA,EAA2B;AACzB,IAAA,OAAO,IAAA,CAAK,IAAA,CAAK,UAAA,CAAW,WAAW,CAAA;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAA,CAAA,EAA4B;AAC1B,IAAA,OAAO,IAAA,CAAK,IAAA,CAAK,UAAA,CAAW,aAAa,CAAA;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA,EAKA,cAAA,CAAA,EAA0B;AACxB,IAAA,OAAO,IAAA,CAAK,IAAA,CAAK,UAAA,CAAW,UAAU,CAAA;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA,EAKA,aAAA,CAAA,EAAyB;AACvB,IAAA,OAAO,IAAA,CAAK,IAAA,CAAK,UAAA,CAAW,SAAS,CAAA;AAAA,EACvC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASS,QAAA,CAAA,EAAmB;AAC1B,IAAA,MAAM,MAAA,EAAQ,CAAC,CAAA,mBAAA,EAAsB,IAAA,CAAK,OAAO,CAAA,CAAA;AACH,IAAA;AACI,IAAA;AACA,IAAA;AAC3B,IAAA;AACzB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOY,EAAA;AACmB,IAAA;AAC/B,EAAA;AAAA;AAAA;AAAA;AAAA;AAM2B,EAAA;AACX,IAAA;AACsB,MAAA;AACd,MAAA;AACI,MAAA;AACM,MAAA;AACA,MAAA;AAChC,IAAA;AAEsB,IAAA;AACU,MAAA;AACP,MAAA;AACwB,QAAA;AAC/C,MAAA;AACF,IAAA;AAEkD,IAAA;AACrB,MAAA;AACM,MAAA;AACc,QAAA;AAC9C,MAAA;AACH,IAAA;AAEsB,IAAA;AACxB,EAAA;AACF;AD/FsD;AACA;AEpH1B;AAMuD,EAAA;AALlC,IAAA;AAMX,IAAA;AACR,IAAA;AACU,IAAA;AACtC,EAAA;AAAA;AAAA;AAAA;AAAA;AAMgC,EAAA;AACE,IAAA;AACb,IAAA;AAEE,IAAA;AACQ,IAAA;AACd,MAAA;AACf,IAAA;AAEO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAM6C,EAAA;AACX,IAAA;AACb,IAAA;AAEE,IAAA;AAGO,IAAA;AACL,MAAA;AACd,MAAA;AACT,IAAA;AAKI,IAAA;AAEG,IAAA;AACQ,MAAA;AACb,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAK4D,EAAA;AACzB,IAAA;AACU,IAAA;AACtB,IAAA;AAM2B,IAAA;AAC7B,MAAA;AACnB,IAAA;AAEoB,IAAA;AAClB,MAAA;AACW,MAAA;AACO,MAAA;AACM,MAAA;AACzB,IAAA;AACH,EAAA;AAAA;AAAA;AAAA;AAAA;AAMoC,EAAA;AACE,IAAA;AACd,IAAA;AACc,IAAA;AACd,IAAA;AAEiB,IAAA;AACT,MAAA;AAEa,QAAA;AACb,UAAA;AACP,UAAA;AACnB,QAAA;AACK,MAAA;AAEkC,QAAA;AACb,UAAA;AACP,UAAA;AACnB,QAAA;AACF,MAAA;AACF,IAAA;AAGqC,IAAA;AACrB,IAAA;AACc,MAAA;AAC9B,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKc,EAAA;AACS,IAAA;AACkB,IAAA;AACT,MAAA;AACL,QAAA;AACvB,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKc,EAAA;AACK,IAAA;AACnB,EAAA;AAAA;AAAA;AAAA;AAK0B,EAAA;AACQ,IAAA;AACb,IAAA;AAEE,IAAA;AACO,IAAA;AACL,MAAA;AACd,MAAA;AACT,IAAA;AAEoB,IAAA;AACtB,EAAA;AACF;AFoFsD;AACA;AGhQsB;AAC7D,EAAA;AACA,EAAA;AACD,EAAA;AACJ,EAAA;AACV;AAE8E;AAC1D,EAAA;AACF,EAAA;AACK,EAAA;AACvB;AAUsC;AACpC,EAAA;AAAA;AACA,EAAA;AAAA;AACA,EAAA;AAAA;AACD;AAMqC;AACpC,EAAA;AAAA;AACA,EAAA;AAAA;AACA,EAAA;AAAA;AACA,EAAA;AAAA;AACD;AAMoC;AAAI;AAEvC,EAAA;AACA,EAAA;AACA,EAAA;AAAA;AAEA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACD;AAMsD;AAGvB,EAAA;AAChC;AAgBiE;AAE9B,EAAA;AAEjB,EAAA;AAEI,IAAA;AAC8B,IAAA;AAEF,MAAA;AACzB,QAAA;AACnB,MAAA;AACF,IAAA;AAGuB,IAAA;AACE,IAAA;AAC3B,EAAA;AAEO,EAAA;AACT;AAiBwD;AACd,EAAA;AAEO,IAAA;AACE,IAAA;AACtC,MAAA;AACT,IAAA;AAG+B,IAAA;AAGE,IAAA;AAGD,IAAA;AAGzB,IAAA;AACT,EAAA;AAGgC,EAAA;AACe,IAAA;AAE9B,IAAA;AAEkC,MAAA;AACtC,QAAA;AACT,MAAA;AAG+C,MAAA;AACtC,QAAA;AACT,MAAA;AACF,IAAA;AAIO,IAAA;AACT,EAAA;AAGO,EAAA;AACT;AAeU;AACkC,EAAA;AACF,EAAA;AACR,EAAA;AAGc,EAAA;AAGC,EAAA;AAGxB,EAAA;AACE,IAAA;AACzB,EAAA;AAEO,EAAA;AACT;AAc4B;AAQiB,EAAA;AAPM,IAAA;AAC1B,IAAA;AACA,IAAA;AACH,IAAA;AACO,IAAA;AAIX,IAAA;AACkC,MAAA;AACJ,MAAA;AACb,MAAA;AAC/B,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAMkB,EAAA;AACgC,IAAA;AACjB,MAAA;AAC/B,IAAA;AACsB,IAAA;AACxB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQsB,EAAA;AACgB,IAAA;AACF,IAAA;AAES,IAAA;AAC7C,EAAA;AAAA;AAAA;AAAA;AAKkC,EAAA;AACA,IAAA;AACzB,MAAA;AACP,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKsB,EAAA;AACY,IAAA;AACzB,MAAA;AACgC,MAAA;AACT,QAAA;AAC5B,MAAA;AACK,IAAA;AAEe,MAAA;AACtB,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAMkC,EAAA;AAEF,IAAA;AAEzB,IAAA;AAE2B,IAAA;AACN,MAAA;AACkB,IAAA;AAClB,MAAA;AAC1B,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKyB,EAAA;AAChB,IAAA;AACO,MAAA;AACO,MAAA;AACqB,MAAA;AAC1C,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKsE,EAAA;AACvD,IAAA;AAEY,IAAA;AACmB,MAAA;AACP,IAAA;AACX,MAAA;AACJ,MAAA;AACY,IAAA;AACZ,MAAA;AACA,MAAA;AACI,MAAA;AAC1B,IAAA;AACF,EAAA;AACF;AA4BE;AAE0C,EAAA;AACU,EAAA;AAEhD,EAAA;AAE0C,EAAA;AACxC,IAAA;AACqC,MAAA;AACO,MAAA;AAE1C,MAAA;AAC4C,QAAA;AACvC,QAAA;AACP,MAAA;AACsB,QAAA;AACxB,MAAA;AACc,IAAA;AACF,MAAA;AAGyC,MAAA;AAEnC,MAAA;AACV,QAAA;AACR,MAAA;AAG2B,MAAA;AAC4B,QAAA;AACpC,QAAA;AACnB,MAAA;AACF,IAAA;AACF,EAAA;AAEM,EAAA;AACR;AAK+C;AAEF,EAAA;AAClC,IAAA;AACT,EAAA;AAE6C,EAAA;AACpC,IAAA;AACT,EAAA;AACO,EAAA;AACT;AAQW;AAIgB,EAAA;AAChB,IAAA;AACT,EAAA;AAGmB,EAAA;AACmB,IAAA;AACE,IAAA;AACxC,EAAA;AAG6B,EAAA;AAC/B;AAK4F;AAE1C,EAAA;AACjC,IAAA;AACf,EAAA;AAGuC,EAAA;AACzC;AAK0C;AACY,EAAA;AACtD;AAS4C;AAGT,EAAA;AACA,IAAA;AACnB,IAAA;AACQ,IAAA;AACtB,EAAA;AACF;AHqDsD;AACA;AInfW;AAEhB;AAC3C,EAAA;AAKmB,IAAA;AAGU,IAAA;AACzB,EAAA;AACC,IAAA;AACT,EAAA;AACF;AAEyE;AACnC,EAAA;AAGW,EAAA;AACtC,IAAA;AACT,EAAA;AAEI,EAAA;AAG8C,IAAA;AACzC,IAAA;AACD,EAAA;AACC,IAAA;AACT,EAAA;AACF;AAGyB;AAGU;AACD;AAKG;AACgB,EAAA;AACrD;AAK+C;AAEO,EAAA;AAC3C,IAAA;AACT,EAAA;AAE6C,EAAA;AACpC,IAAA;AACT,EAAA;AACO,EAAA;AACT;AASiH;AAE3D,EAAA;AAC3C,IAAA;AACT,EAAA;AAGkB,EAAA;AACE,EAAA;AACM,IAAA;AACjB,MAAA;AACA,MAAA;AACI,QAAA;AACJ,MAAA;AACA,MAAA;AACI,QAAA;AACJ,MAAA;AACA,MAAA;AACA,MAAA;AACI,QAAA;AACX,IAAA;AACF,EAAA;AAG0C,EAAA;AACU,EAAA;AAC3C,IAAA;AACT,EAAA;AACgD,EAAA;AACvC,IAAA;AACT,EAAA;AAEO,EAAA;AACT;AAUoD;AAEzB,EAAA;AACjB,IAAA;AACR,EAAA;AAGuC,EAAA;AACT,IAAA;AACd,MAAA;AACG,MAAA;AACE,MAAA;AACA,MAAA;AACH,MAAA;AACK,MAAA;AACpB,IAAA;AACH,EAAA;AAG8C,EAAA;AACzB,IAAA;AACG,IAAA;AAEqB,MAAA;AACb,MAAA;AACvB,QAAA;AACiB,QAAA;AACrB,MAAA;AACH,IAAA;AACF,EAAA;AAIoD,EAAA;AAC7B,IAAA;AACO,IAAA;AACY,MAAA;AACL,MAAA;AACH,MAAA;AACR,MAAA;AACd,MAAA;AAAA;AACD,MAAA;AAAA;AACR,IAAA;AACH,EAAA;AAGM,EAAA;AACR;AAUmD;AACD,EAAA;AACvC,IAAA;AACT,EAAA;AAC0C,EAAA;AACjC,IAAA;AACT,EAAA;AACO,EAAA;AACT;AAMmE;AAEhD,EAAA;AACD,IAAA;AAChB,EAAA;AAGmB,EAAA;AAC4B,IAAA;AAC/C,EAAA;AAEU,EAAA;AACR,IAAA;AAGF,EAAA;AACF;AAesF;AAC/D,EAAA;AACT,IAAA;AACR,MAAA;AAEF,IAAA;AACF,EAAA;AAGsC,EAAA;AAC1B,IAAA;AACmC,MAAA;AAE7C,IAAA;AACF,EAAA;AAM8C,EAAA;AACA,EAAA;AACA,EAAA;AAGD,EAAA;AACL,EAAA;AACD,EAAA;AACK,EAAA;AAGxB,EAAA;AACqB,IAAA;AACA,IAAA;AACD,IAAA;AACM,IAAA;AACT,IAAA;AACrC,EAAA;AAE6B,EAAA;AACU,IAAA;AACF,IAAA;AACK,IAAA;AAC1C,EAAA;AAMqC,EAAA;AAGjC,EAAA;AACqB,EAAA;AAClB,IAAA;AACO,MAAA;AACV,MAAA;AACG,IAAA;AACmC,MAAA;AACtC,MAAA;AACG,IAAA;AAEO,MAAA;AACV,MAAA;AACJ,EAAA;AAKM,EAAA;AAUiC,EAAA;AAC5B,IAAA;AACJ,IAAA;AACK,IAAA;AACX,EAAA;AAGyC,EAAA;AAGH,EAAA;AACa,EAAA;AACD,EAAA;AACV,EAAA;AAEnC,IAAA;AACsC,IAAA;AAExC,EAAA;AAGsC,EAAA;AAGzB,EAAA;AAqBZ,EAAA;AAC8B,IAAA;AAC/B,MAAA;AACmB,MAAA;AACD,MAAA;AACH,MAAA;AACM,MAAA;AACG,MAAA;AACS,MAAA;AACd,MAAA;AACC,MAAA;AACtB,IAAA;AAEa,IAAA;AAG6B,oBAAA;AAGA,IAAA;AACV,IAAA;AACjB,MAAA;AACuB,sBAAA;AACtC,IAAA;AAEO,IAAA;AACT,EAAA;AAkBE,EAAA;AAQoC,IAAA;AACT,IAAA;AACU,IAAA;AAG6C,IAAA;AAChF,MAAA;AACA,MAAA;AACA,MAAA;AACyB,MAAA;AACtB,MAAA;AACL,IAAA;AAKoC,IAAA;AACG,MAAA;AACjB,MAAA;AACgB,QAAA;AACpC,MAAA;AACF,IAAA;AAMkD,IAAA;AAEhB,MAAA;AACO,QAAA;AAC1B,QAAA;AACoC,UAAA;AACxB,YAAA;AACqB,YAAA;AACxC,UAAA;AACJ,QAAA;AACF,MAAA;AAG4C,MAAA;AACX,QAAA;AACX,QAAA;AACY,UAAA;AACG,YAAA;AACS,YAAA;AACxC,UAAA;AACJ,QAAA;AACF,MAAA;AAG6C,MAAA;AACL,MAAA;AACE,QAAA;AACxC,MAAA;AACqC,MAAA;AACzC,IAAA;AAKI,IAAA;AACE,MAAA;AAEmB,MAAA;AAEmB,QAAA;AACG,UAAA;AAC3C,QAAA;AAGa,QAAA;AACc,UAAA;AAGnB,YAAA;AAGA,YAAA;AAC6B,cAAA;AACD,cAAA;AAChB,YAAA;AACW,cAAA;AAC3B,YAAA;AACF,UAAA;AACA,UAAA;AACK,YAAA;AACH,YAAA;AACF,UAAA;AACF,QAAA;AAE6B,QAAA;AACxB,MAAA;AAED,QAAA;AACqC,UAAA;AACT,UAAA;AAChB,QAAA;AACW,UAAA;AAC3B,QAAA;AACF,MAAA;AAGkB,MAAA;AACQ,QAAA;AAC1B,MAAA;AAE+C,MAAA;AACjC,IAAA;AAE4B,MAAA;AACN,QAAA;AACpC,MAAA;AAKgD,MAAA;AACT,QAAA;AAC1B,QAAA;AAC0B,UAAA;AAIU,UAAA;AACxB,YAAA;AACZA,YAAAA;AACP,UAAA;AACJ,QAAA;AACF,MAAA;AAK4C,MAAA;AACX,QAAA;AACX,QAAA;AACiB,UAAA;AAIL,UAAA;AACG,YAAA;AACxBA,YAAAA;AACP,UAAA;AACJ,QAAA;AACF,MAAA;AAKmC,MAAA;AAIO,MAAA;AACpC,MAAA;AACR,IAAA;AACF,EAAA;AASqE,EAAA;AAC5B,IAAA;AAET,IAAA;AACR,MAAA;AACY,QAAA;AAC9B,QAAA;AACF,MAAA;AACuC,MAAA;AACzC,IAAA;AAEkB,IAAA;AACpB,EAAA;AAa0C,EAAA;AACO,IAAA;AACF,IAAA;AACE,IAAA;AACtB,MAAA;AACX,QAAA;AACoC,QAAA;AACvB,QAAA;AACxB,MAAA;AACU,IAAA;AACf,EAAA;AAEO,EAAA;AACiB,IAAA;AACwB,MAAA;AAEf,MAAA;AACjB,QAAA;AACR,UAAA;AAEF,QAAA;AACF,MAAA;AAE6C,MAAA;AAChB,MAAA;AAEkB,MAAA;AACF,QAAA;AACF,QAAA;AACI,QAAA;AACR,QAAA;AAGR,QAAA;AACN,QAAA;AACX,UAAA;AACF,UAAA;AAAA;AAEqC,UAAA;AAC9C,QAAA;AACA,MAAA;AACD,QAAA;AACA,QAAA;AAEI,QAAA;AAEL,MAAA;AACH,IAAA;AAEsB,IAAA;AAC0B,MAAA;AACH,MAAA;AAEI,MAAA;AACV,QAAA;AAClC,MAAA;AACD,QAAA;AACA,QAAA;AAAA;AAEqC,QAAA;AAGtC,MAAA;AACH,IAAA;AAE2E,IAAA;AAC7B,MAAA;AAEE,MAAA;AACR,MAAA;AAES,MAAA;AACzC,QAAA;AACoB,QAAA;AACT,UAAA;AACgB,QAAA;AAChB,UAAA;AACe,QAAA;AACf,UAAA;AACf,QAAA;AAEkB,QAAA;AAChB,UAAA;AACM,UAAA;AACoC,UAAA;AACE,UAAA;AACV,UAAA;AACI,UAAA;AACK,UAAA;AACE,UAAA;AACrC,YAAA;AAC2B,YAAA;AACnC,UAAA;AACF,QAAA;AAEmC,QAAA;AAClC,MAAA;AACD,QAAA;AACA,QAAA;AAAA;AAEqC,QAAA;AAGtC,MAAA;AACH,IAAA;AAEuB,IAAA;AACyB,MAAA;AACC,MAAA;AAEA,MAAA;AACV,QAAA;AACxB,MAAA;AACf,IAAA;AAEgC,IAAA;AACK,MAAA;AACX,MAAA;AACN,QAAA;AAClB,MAAA;AACgD,MAAA;AACD,MAAA;AACV,QAAA;AACxB,MAAA;AACf,IAAA;AAEuB,IAAA;AACM,MAAA;AACd,MAAA;AACiC,QAAA;AAC9C,MAAA;AACgC,MAAA;AACe,MAAA;AACV,QAAA;AAClC,MAAA;AACD,QAAA;AAEU,QAAA;AACoC,UAAA;AAC5B,UAAA;AAC6B,YAAA;AACzC,UAAA;AACO,UAAA;AAET,QAAA;AACL,MAAA;AACH,IAAA;AAEuC,IAAA;AACL,MAAA;AACnB,MAAA;AACK,QAAA;AAClB,MAAA;AAC4C,MAAA;AACG,MAAA;AAC3B,QAAA;AAChB,UAAA;AACiB,UAAA;AACnB,QAAA;AACmC,QAAA;AACxB,MAAA;AACf,IAAA;AAE+B,IAAA;AACW,MAAA;AAC3B,MAAA;AACK,QAAA;AAClB,MAAA;AAC4C,MAAA;AACG,MAAA;AAC3B,QAAA;AAChB,UAAA;AAC0C,UAAA;AACvB,UAAA;AACJ,UAAA;AACM,UAAA;AACI,UAAA;AACE,UAAA;AACE,UAAA;AAC/B,QAAA;AACmC,QAAA;AACxB,MAAA;AACf,IAAA;AAEgC,IAAA;AACW,MAAA;AAC5B,MAAA;AACK,QAAA;AAClB,MAAA;AAC6C,MAAA;AACE,MAAA;AAC3B,QAAA;AAChB,UAAA;AAC0C,UAAA;AACrB,UAAA;AACvB,QAAA;AACmC,QAAA;AACxB,MAAA;AACf,IAAA;AAE+B,IAAA;AACU,MAAA;AAC1B,MAAA;AACK,QAAA;AAClB,MAAA;AAC4C,MAAA;AACG,MAAA;AAC3B,QAAA;AAChB,UAAA;AAC+B,UAAA;AACjC,QAAA;AACmC,QAAA;AACxB,MAAA;AACf,IAAA;AAEoC,IAAA;AACK,MAAA;AAC1B,MAAA;AACK,QAAA;AAClB,MAAA;AAC6C,MAAA;AACE,MAAA;AAC3B,QAAA;AAChB,UAAA;AAC+B,UAAA;AACjC,QAAA;AACmC,QAAA;AACxB,MAAA;AACf,IAAA;AAEmC,IAAA;AACc,MAAA;AACD,MAAA;AAC5B,QAAA;AAClB,MAAA;AAC8C,MAAA;AAChD,IAAA;AAEqC,IAAA;AACU,MAAA;AACR,MAAA;AACnB,QAAA;AAClB,MAAA;AACqB,MAAA;AACnB,QAAA;AACA,QAAA;AACM,QAAA;AACN,QAAA;AACA,QAAA;AACD,MAAA;AACM,MAAA;AAEuB,QAAA;AAG9B,MAAA;AACF,IAAA;AAE4B,IAAA;AACO,MAAA;AACX,MAAA;AACJ,QAAA;AAClB,MAAA;AAC2C,MAAA;AACI,MAAA;AACtB,QAAA;AACX,UAAA;AACmB,UAAA;AACN,UAAA;AACxB,QAAA;AACU,MAAA;AACf,IAAA;AAEoC,IAAA;AACP,MAAA;AACd,MAAA;AACK,QAAA;AAClB,MAAA;AAC2C,MAAA;AACI,MAAA;AACtB,QAAA;AACX,UAAA;AAC8B,UAAA;AACjB,UAAA;AACxB,QAAA;AACU,MAAA;AACf,IAAA;AAEa,IAAA;AACC,MAAA;AACd,IAAA;AAEyB,IAAA;AAChB,MAAA;AACT,IAAA;AAEgD,IAAA;AACvC,MAAA;AACT,IAAA;AAEgC,IAAA;AACC,MAAA;AACjC,IAAA;AAEqB,IAAA;AACL,MAAA;AAChB,IAAA;AACF,EAAA;AACF;AJiMsD;AACA;AACA;AACA;AACA","file":"/Users/will/Projects/Business/cms/main-branch/packages/sdk/dist/server/chunk-ZESVLGDI.js","sourcesContent":[null,"/**\n * SDK API Error Class\n *\n * Thrown when the API returns an error response with the new envelope format.\n * Provides typed access to error details including code, message, request ID, and field errors.\n */\n\nimport type { ApiErrorCode, FieldError, ApiError } from '@riverbankcms/api/public';\n\n/**\n * Error options for creating RiverbankApiError\n */\nexport interface RiverbankApiErrorOptions {\n code: ApiErrorCode;\n message: string;\n requestId: string;\n status: number;\n timestamp: string;\n fieldErrors?: FieldError[];\n /** Retry delay from Retry-After header, in milliseconds */\n retryAfterMs?: number;\n /** Original error that caused this error (for network errors) */\n cause?: Error;\n}\n\n/**\n * Error thrown by SDK when API returns an error response\n *\n * @example\n * ```ts\n * try {\n * await client.getSite({ slug: 'my-site' });\n * } catch (error) {\n * if (error instanceof RiverbankApiError) {\n * console.log('Error code:', error.code);\n * console.log('Request ID:', error.requestId);\n *\n * if (error.isAuthError()) {\n * // Handle authentication error\n * }\n *\n * if (error.isValidationError() && error.fieldErrors) {\n * // Handle field-level validation errors\n * error.fieldErrors.forEach(fe => {\n * console.log(`${fe.field}: ${fe.message}`);\n * });\n * }\n *\n * // Check if error can be retried\n * if (error.isRetryable) {\n * const delay = error.retryAfterMs ?? 1000;\n * console.log(`Retrying after ${delay}ms...`);\n * }\n * }\n * }\n * ```\n */\nexport class RiverbankApiError extends Error {\n /** Namespaced error code (e.g., 'auth:unauthenticated', 'validation:invalid_input') */\n readonly code: ApiErrorCode;\n\n /** Unique request ID for debugging/support */\n readonly requestId: string;\n\n /** HTTP status code */\n readonly status: number;\n\n /** Field-level validation errors (only present for validation errors) */\n readonly fieldErrors?: FieldError[];\n\n /** ISO timestamp when the error occurred */\n readonly timestamp: string;\n\n /** Original error cause for error classification (ECONNREFUSED, etc.) */\n declare readonly cause?: Error;\n\n /**\n * Whether this error is safe to retry.\n * True for: 429 rate limit, 5xx server errors\n * False for: 4xx client errors (except 429)\n */\n readonly isRetryable: boolean;\n\n /**\n * Suggested retry delay from Retry-After header, in milliseconds.\n * Only present when the server provides a Retry-After header.\n */\n readonly retryAfterMs?: number;\n\n constructor(apiError: ApiError['error'] | RiverbankApiErrorOptions) {\n super(apiError.message);\n this.name = 'RiverbankApiError';\n // Preserve original error cause for error classification (ECONNREFUSED, etc.)\n if ('cause' in apiError && apiError.cause) {\n this.cause = apiError.cause;\n }\n this.code = apiError.code;\n this.requestId = apiError.requestId;\n this.status = apiError.status;\n this.fieldErrors = apiError.fieldErrors;\n this.timestamp = apiError.timestamp;\n this.retryAfterMs = 'retryAfterMs' in apiError ? apiError.retryAfterMs : undefined;\n this.isRetryable = this.computeRetryable();\n\n // Maintain proper prototype chain for instanceof checks\n Object.setPrototypeOf(this, RiverbankApiError.prototype);\n }\n\n /**\n * Compute whether this error is retryable based on HTTP status code.\n * - 0 (network errors - no HTTP response): retryable\n * - 429 (rate limit): retryable\n * - 5xx (server errors): retryable\n * - 4xx (client errors, except 429): NOT retryable\n */\n private computeRetryable(): boolean {\n // Network errors (no HTTP response) are retryable\n if (this.status === 0) return true;\n // Rate limiting is retryable\n if (this.status === 429) return true;\n // Server errors are retryable\n if (this.status >= 500) return true;\n // Client errors (4xx except 429) are not retryable\n return false;\n }\n\n /**\n * Check if this is a network error (no HTTP response received)\n *\n * Matches: network:connection_error, network:timeout, network:dns_error\n */\n isNetworkError(): boolean {\n return this.code.startsWith('network:');\n }\n\n /**\n * Check if this error matches a specific error code\n *\n * @example\n * ```ts\n * if (error.is('auth:unauthenticated')) {\n * // Redirect to login\n * }\n * ```\n */\n is(code: ApiErrorCode): boolean {\n return this.code === code;\n }\n\n /**\n * Check if this is an authentication or authorization error\n *\n * Matches: auth:unauthenticated, auth:token_expired, auth:token_invalid,\n * auth:forbidden, auth:mfa_required, auth:insufficient_permissions\n */\n isAuthError(): boolean {\n return this.code.startsWith('auth:');\n }\n\n /**\n * Check if this is a validation error\n *\n * Matches: validation:invalid_input, validation:missing_field, validation:invalid_format\n */\n isValidationError(): boolean {\n return this.code.startsWith('validation:');\n }\n\n /**\n * Check if this is a resource error (not found, conflict, etc.)\n *\n * Matches: resource:not_found, resource:already_exists, resource:conflict, resource:gone\n */\n isResourceError(): boolean {\n return this.code.startsWith('resource:');\n }\n\n /**\n * Check if this is a rate limiting error\n */\n isRateLimitError(): boolean {\n return this.code.startsWith('rate_limit:');\n }\n\n /**\n * Check if this is a billing/payment error\n */\n isBillingError(): boolean {\n return this.code.startsWith('billing:');\n }\n\n /**\n * Check if this is a server error\n */\n isServerError(): boolean {\n return this.code.startsWith('server:');\n }\n\n /**\n * Returns a human-readable string representation of the error.\n * Includes all key details for debugging.\n *\n * @example\n * \"RiverbankApiError: Content keys cannot access preview content | Code: auth:forbidden | Status: 401 | RequestId: req-abc123\"\n */\n override toString(): string {\n const parts = [`RiverbankApiError: ${this.message}`];\n if (this.code) parts.push(`Code: ${this.code}`);\n if (this.status) parts.push(`Status: ${this.status}`);\n if (this.requestId) parts.push(`RequestId: ${this.requestId}`);\n return parts.join(' | ');\n }\n\n /**\n * Custom Node.js inspect output for better console.log display.\n * This ensures that console.log(error) shows all relevant details\n * instead of just \"[Object]\" for nested properties.\n */\n [Symbol.for('nodejs.util.inspect.custom')](): string {\n return this.toDetailedString();\n }\n\n /**\n * Returns a detailed multi-line string for debugging.\n * Used by the Node.js inspect symbol for console output.\n */\n toDetailedString(): string {\n const lines = [\n `RiverbankApiError: ${this.message}`,\n ` Code: ${this.code}`,\n ` Status: ${this.status}`,\n ` RequestId: ${this.requestId}`,\n ` Timestamp: ${this.timestamp}`,\n ];\n\n if (this.isRetryable) {\n lines.push(` Retryable: true`);\n if (this.retryAfterMs) {\n lines.push(` RetryAfter: ${this.retryAfterMs}ms`);\n }\n }\n\n if (this.fieldErrors && this.fieldErrors.length > 0) {\n lines.push(' FieldErrors:');\n this.fieldErrors.forEach((fe) => {\n lines.push(` - ${fe.field}: ${fe.message}`);\n });\n }\n\n return lines.join('\\n');\n }\n}\n","/**\n * Simple in-memory cache with TTL and stale support\n *\n * Supports:\n * - Fresh entries: Within TTL, returned immediately\n * - Stale entries: Past TTL but within staleTtl, returned as fallback\n * - Automatic eviction: Stale entries evicted first, then oldest fresh\n */\n\n/**\n * Cache entry with freshness and stale windows\n */\ninterface CacheEntry<T> {\n value: T;\n /** Timestamp when entry was created */\n createdAt: number;\n /** Timestamp until which entry is fresh */\n freshUntil: number;\n /** Timestamp until which entry can be served as stale */\n staleUntil: number;\n}\n\n/**\n * Result from getStale() with age information\n */\nexport interface StaleResult<T> {\n value: T;\n /** Seconds since the entry's TTL expired (0 if still fresh) */\n staleAgeSec: number;\n}\n\n/**\n * Options for cache.set()\n */\nexport interface CacheSetOptions {\n /** Time-to-live in milliseconds (overrides default) */\n ttl?: number;\n /** Additional time after TTL during which stale data can be served (overrides default) */\n staleTtl?: number;\n}\n\nexport class SimpleCache<T> {\n private cache = new Map<string, CacheEntry<T>>();\n private maxSize: number;\n private ttl: number;\n private staleTtl: number;\n\n constructor(options: { maxSize?: number; ttl?: number; staleTtl?: number } = {}) {\n this.maxSize = options.maxSize ?? 100;\n this.ttl = options.ttl ?? 300000; // 5 minutes in milliseconds\n this.staleTtl = options.staleTtl ?? 300000; // 5 minutes additional stale window\n }\n\n /**\n * Get a fresh value (within TTL)\n * @returns The value if fresh, null otherwise\n */\n getFresh(key: string): T | null {\n const entry = this.cache.get(key);\n if (!entry) return null;\n\n const now = Date.now();\n if (now <= entry.freshUntil) {\n return entry.value;\n }\n\n return null;\n }\n\n /**\n * Get a value that may be stale (past TTL but within staleTtl)\n * @returns Object with value and stale age, or null if expired\n */\n getStale(key: string): StaleResult<T> | null {\n const entry = this.cache.get(key);\n if (!entry) return null;\n\n const now = Date.now();\n\n // Completely expired\n if (now > entry.staleUntil) {\n this.cache.delete(key);\n return null;\n }\n\n // Calculate stale age (0 if still fresh)\n const staleAgeSec = now <= entry.freshUntil\n ? 0\n : Math.floor((now - entry.freshUntil) / 1000);\n\n return {\n value: entry.value,\n staleAgeSec,\n };\n }\n\n /**\n * Store a value with TTL and stale window\n */\n set(key: string, value: T, options?: CacheSetOptions): void {\n const ttl = options?.ttl ?? this.ttl;\n const staleTtl = options?.staleTtl ?? this.staleTtl;\n const now = Date.now();\n\n // Enforce max size with smart eviction.\n // Use while loop because evictOne() may not reduce size if the cache\n // contains only non-expired entries and evictOne finds nothing to remove\n // (defensive edge case - shouldn't happen in normal operation).\n while (this.cache.size >= this.maxSize && !this.cache.has(key)) {\n this.evictOne(now);\n }\n\n this.cache.set(key, {\n value,\n createdAt: now,\n freshUntil: now + ttl,\n staleUntil: now + ttl + staleTtl,\n });\n }\n\n /**\n * Evict one entry to make room for a new one\n * Priority: oldest stale entry, then oldest fresh entry\n */\n private evictOne(now: number): void {\n let oldestStaleKey: string | null = null;\n let oldestStaleTime = Infinity;\n let oldestFreshKey: string | null = null;\n let oldestFreshTime = Infinity;\n\n for (const [key, entry] of this.cache) {\n if (now > entry.freshUntil) {\n // Stale entry\n if (entry.createdAt < oldestStaleTime) {\n oldestStaleTime = entry.createdAt;\n oldestStaleKey = key;\n }\n } else {\n // Fresh entry\n if (entry.createdAt < oldestFreshTime) {\n oldestFreshTime = entry.createdAt;\n oldestFreshKey = key;\n }\n }\n }\n\n // Evict oldest stale first, then oldest fresh\n const keyToEvict = oldestStaleKey ?? oldestFreshKey;\n if (keyToEvict) {\n this.cache.delete(keyToEvict);\n }\n }\n\n /**\n * Remove all fully expired entries (past staleUntil)\n */\n prune(): void {\n const now = Date.now();\n for (const [key, entry] of this.cache) {\n if (now > entry.staleUntil) {\n this.cache.delete(key);\n }\n }\n }\n\n /**\n * Clear all entries\n */\n clear(): void {\n this.cache.clear();\n }\n\n /**\n * Check if a key exists and is not fully expired\n */\n has(key: string): boolean {\n const entry = this.cache.get(key);\n if (!entry) return false;\n\n const now = Date.now();\n if (now > entry.staleUntil) {\n this.cache.delete(key);\n return false;\n }\n\n return now <= entry.freshUntil;\n }\n}\n","/**\n * Resilience utilities for the SDK client\n *\n * Provides:\n * - Circuit breaker pattern for fail-fast behavior\n * - Retry with exponential backoff and jitter\n * - Timeout handling\n */\n\nimport type { RetryConfig, CircuitBreakerConfig, CircuitState } from './types';\nimport { RiverbankApiError } from './error';\n\n// ============================================================================\n// Default Configurations\n// ============================================================================\n\nexport const DEFAULT_RETRY_CONFIG: Required<Omit<RetryConfig, 'retryOn'>> = {\n maxAttempts: 3,\n baseDelayMs: 200,\n maxDelayMs: 2000,\n jitter: 'full',\n};\n\nexport const DEFAULT_CIRCUIT_BREAKER_CONFIG: Required<CircuitBreakerConfig> = {\n failureThreshold: 5,\n resetTimeoutMs: 30000,\n halfOpenMaxRequests: 2,\n};\n\n// ============================================================================\n// Error Classification\n// ============================================================================\n\n/**\n * Error codes that indicate permanent infrastructure failures.\n * These should NOT be retried as the server is definitively unreachable.\n */\nconst PERMANENT_NETWORK_ERROR_CODES = new Set([\n 'ECONNREFUSED', // Server is not running / port not listening\n 'ENOTFOUND', // DNS lookup failed - hostname doesn't exist\n 'EAI_AGAIN', // DNS lookup timeout (usually permanent for invalid hosts)\n]);\n\n/**\n * Error codes that indicate transient network issues.\n * These MAY succeed on retry as the server was reachable but had a hiccup.\n */\nconst TRANSIENT_NETWORK_ERROR_CODES = new Set([\n 'ECONNRESET', // Connection was reset mid-request (server dropped it)\n 'EPIPE', // Broken pipe (connection closed while writing)\n 'ETIMEDOUT', // Connection timed out (could be temporary congestion)\n 'ESOCKETTIMEDOUT', // Socket timeout\n]);\n\n/**\n * Node.js network error codes (POSIX style).\n * These are the codes we're looking for in the cause chain.\n */\nconst NODE_NETWORK_ERROR_CODES = new Set([\n // Permanent\n 'ECONNREFUSED',\n 'ENOTFOUND',\n 'EAI_AGAIN',\n // Transient\n 'ECONNRESET',\n 'EPIPE',\n 'ETIMEDOUT',\n 'ESOCKETTIMEDOUT',\n]);\n\n/**\n * Check if a string is a Node.js network error code (not an API error code).\n * API error codes look like 'network:connection_error', Node codes look like 'ECONNREFUSED'.\n */\nfunction isNodeNetworkErrorCode(code: string): boolean {\n // Node.js error codes are uppercase without colons\n // API error codes contain colons (e.g., 'network:connection_error')\n return !code.includes(':') && NODE_NETWORK_ERROR_CODES.has(code);\n}\n\n/**\n * Extract the Node.js error code from an error's cause chain.\n * Handles nested cause chains like:\n * RiverbankApiError -> TypeError -> Error (ECONNREFUSED)\n *\n * Skips API error codes (like 'network:connection_error') and only returns\n * actual Node.js error codes (like 'ECONNREFUSED', 'ENOTFOUND', etc.)\n *\n * @example\n * RiverbankApiError: Network request failed\n * [cause]: TypeError: fetch failed\n * [cause]: Error: connect ECONNREFUSED 127.0.0.1:4010\n * code: 'ECONNREFUSED'\n */\nfunction getErrorCodeFromCause(error: Error): string | undefined {\n // Walk the cause chain looking for a Node.js error code\n let current: Error | undefined = error;\n\n while (current) {\n // Check if current error has a Node.js error code\n const nodeError = current as NodeJS.ErrnoException;\n if (nodeError.code && typeof nodeError.code === 'string') {\n // Only return actual Node.js error codes, not API error codes\n if (isNodeNetworkErrorCode(nodeError.code)) {\n return nodeError.code;\n }\n }\n\n // Move to the cause\n const errorWithCause = current as Error & { cause?: Error };\n current = errorWithCause.cause;\n }\n\n return undefined;\n}\n\n/**\n * Determine if an error is transient and should trigger circuit breaker / retry\n *\n * Transient (opens circuit, triggers retry):\n * - Network errors (status 0) that are NOT permanent infrastructure failures\n * - 5xx server errors\n * - 429 rate limit\n * - Connection reset, broken pipe, timeout\n *\n * Permanent (does NOT open circuit, no retry):\n * - 4xx client errors (400, 401, 403, 404)\n * - Validation errors\n * - ECONNREFUSED (server not running)\n * - ENOTFOUND (DNS failure)\n */\nexport function isTransientError(error: Error): boolean {\n if (error instanceof RiverbankApiError) {\n // Check for permanent network errors first (from wrapped TypeErrors)\n const errorCode = getErrorCodeFromCause(error);\n if (errorCode && PERMANENT_NETWORK_ERROR_CODES.has(errorCode)) {\n return false;\n }\n\n // Network errors are transient (unless permanent code above)\n if (error.status === 0) return true;\n\n // 429 rate limit is transient\n if (error.status === 429) return true;\n\n // 5xx server errors are transient\n if (error.status >= 500) return true;\n\n // 4xx client errors are permanent\n return false;\n }\n\n // For TypeErrors (Node.js fetch failures), check the cause for error code\n if (error instanceof TypeError) {\n const errorCode = getErrorCodeFromCause(error);\n\n if (errorCode) {\n // Permanent infrastructure failures - don't retry\n if (PERMANENT_NETWORK_ERROR_CODES.has(errorCode)) {\n return false;\n }\n\n // Known transient codes - do retry\n if (TRANSIENT_NETWORK_ERROR_CODES.has(errorCode)) {\n return true;\n }\n }\n\n // Unknown TypeError without recognized code - default to transient\n // (backwards compatible, safer assumption for unexpected errors)\n return true;\n }\n\n // Other generic errors - default to transient (backwards compatible)\n return true;\n}\n\n// ============================================================================\n// Backoff Calculation\n// ============================================================================\n\n/**\n * Calculate backoff delay for retry attempt\n *\n * Uses exponential backoff with optional full jitter.\n * Formula: min(maxDelay, baseDelay * 2^(attempt-1)) * random()\n */\nexport function calculateBackoff(\n attempt: number,\n config: Partial<Pick<RetryConfig, 'baseDelayMs' | 'maxDelayMs' | 'jitter'>>\n): number {\n const baseDelayMs = config.baseDelayMs ?? DEFAULT_RETRY_CONFIG.baseDelayMs;\n const maxDelayMs = config.maxDelayMs ?? DEFAULT_RETRY_CONFIG.maxDelayMs;\n const jitter = config.jitter ?? DEFAULT_RETRY_CONFIG.jitter;\n\n // Exponential: baseDelay * 2^(attempt-1)\n const exponential = baseDelayMs * Math.pow(2, attempt - 1);\n\n // Cap at maxDelay\n const capped = Math.min(exponential, maxDelayMs);\n\n // Apply jitter\n if (jitter === 'full') {\n return Math.random() * capped;\n }\n\n return capped;\n}\n\n// ============================================================================\n// Circuit Breaker\n// ============================================================================\n\n/**\n * Circuit breaker for fail-fast behavior\n *\n * States:\n * - closed: Normal operation, requests allowed\n * - open: Too many failures, requests blocked\n * - half-open: Testing if service recovered, limited requests allowed\n */\nexport class CircuitBreaker {\n private state: 'closed' | 'open' | 'half-open' = 'closed';\n private failureCount = 0;\n private successCount = 0;\n private openUntil = 0;\n private halfOpenRequests = 0;\n private config: Required<CircuitBreakerConfig>;\n\n constructor(config?: CircuitBreakerConfig) {\n this.config = {\n failureThreshold: config?.failureThreshold ?? DEFAULT_CIRCUIT_BREAKER_CONFIG.failureThreshold,\n resetTimeoutMs: config?.resetTimeoutMs ?? DEFAULT_CIRCUIT_BREAKER_CONFIG.resetTimeoutMs,\n halfOpenMaxRequests: config?.halfOpenMaxRequests ?? DEFAULT_CIRCUIT_BREAKER_CONFIG.halfOpenMaxRequests,\n };\n }\n\n /**\n * Check if circuit is open (requests should be blocked)\n * Also handles automatic transition from open to half-open after timeout\n */\n isOpen(): boolean {\n if (this.state === 'open' && Date.now() >= this.openUntil) {\n this.transitionTo('half-open');\n }\n return this.state === 'open';\n }\n\n /**\n * Check if a request can be attempted\n * - closed: always yes\n * - open: always no\n * - half-open: limited number of probes\n */\n canAttempt(): boolean {\n if (this.state === 'closed') return true;\n if (this.state === 'open') return false;\n // half-open: allow limited probes\n return this.halfOpenRequests < this.config.halfOpenMaxRequests;\n }\n\n /**\n * Increment half-open request counter (call before making request in half-open)\n */\n incrementHalfOpenRequests(): void {\n if (this.state === 'half-open') {\n this.halfOpenRequests++;\n }\n }\n\n /**\n * Record a successful request\n */\n recordSuccess(): void {\n if (this.state === 'half-open') {\n this.successCount++;\n if (this.successCount >= this.config.halfOpenMaxRequests) {\n this.transitionTo('closed');\n }\n } else {\n // Reset consecutive failures on any success\n this.failureCount = 0;\n }\n }\n\n /**\n * Record a failed request\n * Only counts transient failures toward circuit breaker threshold\n */\n recordFailure(error: Error): void {\n // Only count transient failures\n if (!isTransientError(error)) return;\n\n this.failureCount++;\n\n if (this.state === 'half-open') {\n this.transitionTo('open');\n } else if (this.failureCount >= this.config.failureThreshold) {\n this.transitionTo('open');\n }\n }\n\n /**\n * Get current circuit state\n */\n getState(): CircuitState {\n return {\n state: this.state,\n failureCount: this.failureCount,\n openUntil: this.state === 'open' ? this.openUntil : undefined,\n };\n }\n\n /**\n * Transition to a new state\n */\n private transitionTo(newState: 'closed' | 'open' | 'half-open'): void {\n this.state = newState;\n\n if (newState === 'open') {\n this.openUntil = Date.now() + this.config.resetTimeoutMs;\n } else if (newState === 'half-open') {\n this.halfOpenRequests = 0;\n this.successCount = 0;\n } else if (newState === 'closed') {\n this.failureCount = 0;\n this.successCount = 0;\n this.halfOpenRequests = 0;\n }\n }\n}\n\n// ============================================================================\n// Retry with Timeout\n// ============================================================================\n\n/**\n * Configuration for fetchWithTimeoutAndRetry\n */\nexport interface FetchWithRetryConfig {\n maxAttempts?: number;\n baseDelayMs?: number;\n maxDelayMs?: number;\n jitter?: 'full' | 'none';\n requestTimeoutMs?: number;\n retryOn?: (error: Error, statusCode?: number) => boolean;\n}\n\n/**\n * Fetch with timeout and retry logic\n *\n * @param fetcher - Function that performs the fetch, receives AbortSignal for timeout\n * @param config - Retry configuration\n * @returns Result from successful fetch\n * @throws Last error after all retries exhausted\n */\nexport async function fetchWithTimeoutAndRetry<T>(\n fetcher: (signal: AbortSignal) => Promise<T>,\n config: FetchWithRetryConfig\n): Promise<T> {\n const maxAttempts = config.maxAttempts ?? DEFAULT_RETRY_CONFIG.maxAttempts;\n const requestTimeoutMs = config.requestTimeoutMs ?? 8000;\n\n let lastError: Error | undefined;\n\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);\n\n try {\n const result = await fetcher(controller.signal);\n return result;\n } finally {\n clearTimeout(timeoutId);\n }\n } catch (error) {\n lastError = error as Error;\n\n // Check if we should retry\n const shouldRetry = shouldRetryError(error as Error, config.retryOn);\n\n if (!shouldRetry) {\n throw error;\n }\n\n // Don't delay after last attempt\n if (attempt < maxAttempts) {\n const delay = getRetryDelay(error as Error, attempt, config);\n await sleep(delay);\n }\n }\n }\n\n throw lastError;\n}\n\n/**\n * Check if an error is an AbortError (from AbortController.abort() or timeout)\n */\nfunction isAbortError(error: unknown): boolean {\n // DOMException with name 'AbortError' (browsers)\n if (typeof DOMException !== 'undefined' && error instanceof DOMException && error.name === 'AbortError') {\n return true;\n }\n // Node.js AbortError\n if (error instanceof Error && error.name === 'AbortError') {\n return true;\n }\n return false;\n}\n\n/**\n * Check if an error should be retried\n */\nfunction shouldRetryError(\n error: Error,\n customRetryOn?: (error: Error, statusCode?: number) => boolean\n): boolean {\n // Never retry AbortErrors - these are either:\n // 1. User explicitly cancelled the request\n // 2. Timeout fired (request already took too long, retrying won't help)\n if (isAbortError(error)) {\n return false;\n }\n\n // Custom retry logic takes precedence\n if (customRetryOn) {\n const statusCode = error instanceof RiverbankApiError ? error.status : undefined;\n return customRetryOn(error, statusCode);\n }\n\n // Default: retry transient errors\n return isTransientError(error);\n}\n\n/**\n * Get delay before retrying, respecting Retry-After if present\n */\nfunction getRetryDelay(error: Error, attempt: number, config: FetchWithRetryConfig): number {\n // Check for Retry-After from rate limiting\n if (error instanceof RiverbankApiError && error.retryAfterMs) {\n return error.retryAfterMs;\n }\n\n // Use exponential backoff\n return calculateBackoff(attempt, config);\n}\n\n/**\n * Sleep for specified milliseconds\n */\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n// ============================================================================\n// Circuit Open Error\n// ============================================================================\n\n/**\n * Error thrown when circuit breaker is open\n */\nexport class CircuitOpenError extends Error {\n readonly circuitState: CircuitState;\n\n constructor(state: CircuitState) {\n super('Circuit breaker is open');\n this.name = 'CircuitOpenError';\n this.circuitState = state;\n }\n}\n","import { createBearerAPIClient, createInternalAPIClient, setSdkVersion, ApiRequestError, isApiError, unwrapResponse, ApiEnvelopeError } from '@riverbankcms/api/public';\nimport type { ApiResult, ApiError } from '@riverbankcms/api/public';\nimport type {\n RiverbankClient,\n RiverbankClientConfig,\n GetEntriesBaseParams,\n ResilienceStatus,\n ResilienceSource,\n CircuitState,\n ApiKeyType,\n SdkAuthConfig,\n ResolveEntrySubrouteResponse,\n} from './types';\nimport { SimpleCache } from './cache';\nimport { SDK_VERSION } from '../version';\nimport { RiverbankApiError } from './error';\nimport {\n CircuitBreaker,\n CircuitOpenError,\n fetchWithTimeoutAndRetry,\n DEFAULT_RETRY_CONFIG,\n DEFAULT_CIRCUIT_BREAKER_CONFIG,\n} from './resilience';\n\n// Lazy-loaded prebuild module to avoid bundling Node.js 'fs' module in client builds.\n// The prebuild loader is only used server-side for fallback content.\nlet prebuildModule: typeof import('../prebuild/loader') | null = null;\n\nfunction tryRequire(modulePath: string): unknown {\n try {\n // Hide `require()` from bundlers so Node-only modules like `fs` don't get pulled into\n // Edge/browser bundles. In Node.js CJS, this returns the real require function.\n // In ESM/Edge, it throws or returns null and we treat it as unavailable.\n \n const req = Function('return typeof require === \"function\" ? require : null')() as\n | ((id: string) => unknown)\n | null;\n return req ? req(modulePath) : null;\n } catch {\n return null;\n }\n}\n\nfunction getPrebuildModule(): typeof import('../prebuild/loader') | null {\n if (prebuildModule !== null) return prebuildModule;\n\n // Only attempt to load in Node.js environments\n if (typeof process === 'undefined' || !process.versions?.node) {\n return null;\n }\n\n try {\n // Node-only: load prebuild module synchronously when available.\n // Uses an indirect require to avoid static bundling in Edge/browser builds.\n prebuildModule = tryRequire('../prebuild/loader') as typeof import('../prebuild/loader');\n return prebuildModule;\n } catch {\n return null;\n }\n}\n\n// Set SDK version for all API requests from this SDK\nsetSdkVersion(SDK_VERSION);\n\n// Default timeout values - shorter for browsers (user is waiting), longer for servers\nconst DEFAULT_BROWSER_TIMEOUT_MS = 5000;\nconst DEFAULT_SERVER_TIMEOUT_MS = 8000;\n\n/**\n * Generate a unique request ID for tracing\n */\nfunction generateRequestId(): string {\n return `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n}\n\n/**\n * Check if an error is an AbortError (from AbortController.abort())\n */\nfunction isAbortError(error: unknown): boolean {\n // DOMException with name 'AbortError' (browsers)\n if (error instanceof DOMException && error.name === 'AbortError') {\n return true;\n }\n // Node.js AbortError\n if (error instanceof Error && error.name === 'AbortError') {\n return true;\n }\n return false;\n}\n\n/**\n * Determine the network error code based on error type, code, and message.\n * Uses multiple detection strategies for cross-runtime compatibility:\n * 1. Error name (TimeoutError, AbortError) - reliable in browsers\n * 2. Node.js error code (ETIMEDOUT, ENOTFOUND, etc.) - most reliable in Node\n * 3. Message matching - fallback for edge cases\n */\nfunction getNetworkErrorCode(error: Error): 'network:connection_error' | 'network:timeout' | 'network:dns_error' {\n // Check error name first (reliable for browser and some Node errors)\n if (error.name === 'TimeoutError' || error.name === 'AbortError') {\n return 'network:timeout';\n }\n\n // Check Node.js error code (most reliable for Node.js)\n const nodeError = error as NodeJS.ErrnoException;\n if (nodeError.code) {\n switch (nodeError.code) {\n case 'ETIMEDOUT':\n case 'ESOCKETTIMEDOUT':\n return 'network:timeout';\n case 'ENOTFOUND':\n case 'EAI_AGAIN':\n return 'network:dns_error';\n case 'ECONNREFUSED':\n case 'ECONNRESET':\n case 'EPIPE':\n return 'network:connection_error';\n }\n }\n\n // Fallback to message matching for browsers and edge cases\n const message = error.message.toLowerCase();\n if (message.includes('timeout') || message.includes('timed out')) {\n return 'network:timeout';\n }\n if (message.includes('dns') || message.includes('getaddrinfo') || message.includes('enotfound')) {\n return 'network:dns_error';\n }\n\n return 'network:connection_error';\n}\n\n/**\n * Convert API errors to RiverbankApiError for consistent SDK error handling.\n * Handles:\n * - ApiEnvelopeError (from unwrapResponse)\n * - ApiRequestError (from fetch with HTTP error)\n * - Network errors (TypeError, etc. - wrapped with status 0)\n * - AbortError (let propagate raw for consumer detection)\n */\nfunction convertToTypedError(error: unknown): never {\n // Let AbortError propagate raw - it's intentional cancellation, not an error to retry\n if (isAbortError(error)) {\n throw error;\n }\n\n // Handle ApiEnvelopeError from unwrapResponse (error envelope in successful HTTP response)\n if (error instanceof ApiEnvelopeError) {\n throw new RiverbankApiError({\n code: error.code,\n message: error.message,\n requestId: error.requestId,\n timestamp: error.timestamp,\n status: error.status,\n fieldErrors: error.fieldErrors,\n });\n }\n\n // Handle ApiRequestError with envelope body (HTTP error with envelope format)\n if (error instanceof ApiRequestError && error.body && typeof error.body === 'object') {\n const body = error.body as ApiResult<unknown>;\n if (isApiError(body)) {\n // Merge envelope error with retryAfterMs from ApiRequestError\n const envelopeError = (body as ApiError).error;\n throw new RiverbankApiError({\n ...envelopeError,\n retryAfterMs: error.retryAfterMs,\n });\n }\n }\n\n // Wrap network errors (TypeError: Failed to fetch, etc.) as RiverbankApiError\n // Preserve the original error as cause for error classification (ECONNREFUSED, etc.)\n if (error instanceof TypeError || (error instanceof Error && !('status' in error))) {\n const networkError = error as Error;\n throw new RiverbankApiError({\n code: getNetworkErrorCode(networkError),\n message: networkError.message || 'Network request failed',\n requestId: `local-${Date.now()}`,\n timestamp: new Date().toISOString(),\n status: 0, // No HTTP response received\n cause: networkError, // Preserve original error for retry/circuit breaker classification\n });\n }\n\n // Re-throw original error if not in envelope format\n throw error;\n}\n\n\n/**\n * Detect API key type from prefix.\n *\n * Key prefixes:\n * - bld_live_sk_* / bld_test_sk_* → 'content' (published content)\n * - bld_preview_sk_* → 'preview' (draft content)\n */\nfunction detectKeyType(apiKey: string): ApiKeyType {\n if (apiKey.startsWith('bld_live_sk_') || apiKey.startsWith('bld_test_sk_')) {\n return 'content';\n }\n if (apiKey.startsWith('bld_preview_sk_')) {\n return 'preview';\n }\n return 'unknown';\n}\n\n/**\n * Resolve auth configuration from config, handling backward compatibility.\n * Throws if neither `auth` nor `apiKey` is provided.\n */\nfunction resolveAuth(config: RiverbankClientConfig): SdkAuthConfig {\n // Prefer new auth config\n if (config.auth) {\n return config.auth;\n }\n\n // Fall back to deprecated apiKey\n if (config.apiKey) {\n return { type: 'api-key', key: config.apiKey };\n }\n\n throw new Error(\n 'Authentication configuration is required. ' +\n 'Provide either `auth` (recommended) or `apiKey` (deprecated). ' +\n 'For public endpoints without auth, use: { auth: { type: \"none\" } }'\n );\n}\n\n/**\n * Create a Riverbank CMS client for fetching content\n *\n * @example\n * ```ts\n * const client = createRiverbankClient({\n * apiKey: 'bld_live_sk_...',\n * baseUrl: 'https://dashboard.example.com/api',\n * });\n *\n * const site = await client.getSite({ slug: 'my-site' });\n * ```\n */\nexport function createRiverbankClient(config: RiverbankClientConfig): RiverbankClient {\n if (!config.baseUrl) {\n throw new Error(\n 'baseUrl is required when creating a Riverbank client. ' +\n 'Expected format: https://dashboard.example.com/api (must include /api path)'\n );\n }\n\n // Validate baseUrl format\n if (!config.baseUrl.endsWith('/api')) {\n throw new Error(\n `baseUrl must end with '/api'. Received: ${config.baseUrl}. ` +\n 'Expected format: https://dashboard.example.com/api'\n );\n }\n\n // ============================================================================\n // Configuration\n // ============================================================================\n\n const cacheEnabled = config.cache?.enabled ?? true;\n const cacheTTL = (config.cache?.ttl ?? 300) * 1000; // Convert seconds to milliseconds\n const cacheMaxSize = config.cache?.maxSize ?? 100;\n\n // Resilience config with defaults\n const resilienceEnabled = config.resilience?.enabled ?? true;\n const staleIfError = config.resilience?.staleIfError ?? true;\n const staleTtlMs = (config.resilience?.staleTtlSec ?? 300) * 1000;\n const requestTimeoutMs = config.resilience?.requestTimeoutMs ??\n (typeof window !== 'undefined' ? DEFAULT_BROWSER_TIMEOUT_MS : DEFAULT_SERVER_TIMEOUT_MS);\n\n const retryConfig = {\n maxAttempts: config.resilience?.retry?.maxAttempts ?? DEFAULT_RETRY_CONFIG.maxAttempts,\n baseDelayMs: config.resilience?.retry?.baseDelayMs ?? DEFAULT_RETRY_CONFIG.baseDelayMs,\n maxDelayMs: config.resilience?.retry?.maxDelayMs ?? DEFAULT_RETRY_CONFIG.maxDelayMs,\n jitter: config.resilience?.retry?.jitter ?? DEFAULT_RETRY_CONFIG.jitter,\n retryOn: config.resilience?.retry?.retryOn,\n };\n\n const circuitBreakerConfig = {\n failureThreshold: config.resilience?.circuitBreaker?.failureThreshold ?? DEFAULT_CIRCUIT_BREAKER_CONFIG.failureThreshold,\n resetTimeoutMs: config.resilience?.circuitBreaker?.resetTimeoutMs ?? DEFAULT_CIRCUIT_BREAKER_CONFIG.resetTimeoutMs,\n halfOpenMaxRequests: config.resilience?.circuitBreaker?.halfOpenMaxRequests ?? DEFAULT_CIRCUIT_BREAKER_CONFIG.halfOpenMaxRequests,\n };\n\n // ============================================================================\n // Auth Resolution\n // ============================================================================\n\n const authConfig = resolveAuth(config);\n\n // Detect key type based on auth config\n let keyType: ApiKeyType;\n switch (authConfig.type) {\n case 'none':\n keyType = 'unknown';\n break;\n case 'api-key':\n keyType = detectKeyType(authConfig.key);\n break;\n case 'bearer':\n // Bearer tokens are typically for preview mode\n keyType = 'preview';\n break;\n }\n\n // Create internal API client based on auth type\n const apiClient =\n authConfig.type === 'none'\n ? createInternalAPIClient(config.baseUrl)\n : authConfig.type === 'api-key'\n ? createBearerAPIClient(authConfig.key, config.baseUrl)\n : createBearerAPIClient(authConfig.token, config.baseUrl)\n\n // ============================================================================\n // State\n // ============================================================================\n\n // Create cache instance with stale support\n const cache = new SimpleCache<unknown>({\n maxSize: cacheMaxSize,\n ttl: cacheTTL,\n staleTtl: staleTtlMs,\n });\n\n // Create circuit breaker\n const circuitBreaker = new CircuitBreaker(circuitBreakerConfig);\n\n // Create prebuild loader if configured and available (lazy-load to avoid fs in browser)\n const prebuildDir = config.resilience?.prebuildDir;\n const prebuildMod = prebuildDir ? getPrebuildModule() : null;\n const canUsePrebuild = prebuildMod?.canUsePrebuild() ?? false;\n const prebuildLoader = canUsePrebuild && prebuildMod && prebuildDir\n ? new prebuildMod.PrebuildLoader({\n prebuildDir,\n maxPrebuildAgeSec: config.resilience?.maxPrebuildAgeSec,\n })\n : null;\n\n // Last request status (for getResilienceStatus())\n let lastStatus: ResilienceStatus | null = null;\n\n // Track degraded mode for onDegradedMode callback\n let isDegraded = false;\n\n // ============================================================================\n // Status Emission\n // ============================================================================\n\n /**\n * Emit a status update and trigger callbacks\n */\n function emitStatus<T>(\n source: ResilienceSource,\n data: T | null,\n details: {\n requestId: string;\n cacheKey: string;\n isPreview: boolean;\n durationMs: number;\n error?: { code?: string; message: string };\n staleAgeSec?: number;\n prebuildAgeSec?: number;\n }\n ): T {\n const status: ResilienceStatus = {\n source,\n isPreview: details.isPreview,\n cacheKey: details.cacheKey,\n error: details.error,\n staleAgeSec: details.staleAgeSec,\n prebuildAgeSec: details.prebuildAgeSec,\n circuit: circuitBreaker.getState(),\n requestId: details.requestId,\n durationMs: details.durationMs,\n };\n\n lastStatus = status;\n\n // Fire onStatusChange callback\n config.resilience?.onStatusChange?.(status);\n\n // Fire onDegradedMode callback on transitions\n const nowDegraded = source === 'stale' || source === 'error';\n if (nowDegraded !== isDegraded) {\n isDegraded = nowDegraded;\n config.resilience?.onDegradedMode?.(nowDegraded, status);\n }\n\n return data as T;\n }\n\n // ============================================================================\n // Resilient Fetch\n // ============================================================================\n\n /**\n * Optional prebuild fallback callback type.\n * Returns the data from prebuild cache along with its age, or null if not available.\n */\n type PrebuildFallback<T> = () => { data: T; prebuildAgeSec: number } | null;\n\n /**\n * Fetch with resilience features: cache, stale fallback, retry, circuit breaker, prebuild\n */\n async function resilientFetch<T>(\n cacheKey: string,\n fetcher: (signal: AbortSignal) => Promise<ApiResult<T>>,\n options: {\n preview?: boolean;\n force?: boolean;\n signal?: AbortSignal;\n /** Optional prebuild fallback - called when live + stale both fail */\n prebuildFallback?: PrebuildFallback<T>;\n }\n ): Promise<T> {\n const requestId = generateRequestId();\n const startTime = Date.now();\n const isPreview = options.preview ?? false;\n\n // Helper to create status details\n const statusDetails = (extra: Partial<Parameters<typeof emitStatus>[2]> = {}) => ({\n requestId,\n cacheKey,\n isPreview,\n durationMs: Date.now() - startTime,\n ...extra,\n });\n\n // -------------------------------------------------------------------------\n // 1. Check fresh cache (unless force refresh)\n // -------------------------------------------------------------------------\n if (cacheEnabled && !options.force) {\n const fresh = cache.getFresh(cacheKey) as T | null;\n if (fresh !== null) {\n return emitStatus('cache', fresh, statusDetails());\n }\n }\n\n // -------------------------------------------------------------------------\n // 2. Circuit breaker check (always check, regardless of force)\n // force=true means skip cache, not skip circuit breaker protection\n // -------------------------------------------------------------------------\n if (resilienceEnabled && circuitBreaker.isOpen()) {\n // Try stale fallback (non-preview only)\n if (!isPreview && staleIfError) {\n const stale = cache.getStale(cacheKey);\n if (stale) {\n return emitStatus('stale', stale.value as T, statusDetails({\n staleAgeSec: stale.staleAgeSec,\n error: { code: 'circuit_open', message: 'Circuit breaker is open' },\n }));\n }\n }\n\n // Try prebuild fallback (non-preview only)\n if (!isPreview && options.prebuildFallback) {\n const prebuildResult = options.prebuildFallback();\n if (prebuildResult) {\n return emitStatus('prebuild', prebuildResult.data, statusDetails({\n prebuildAgeSec: prebuildResult.prebuildAgeSec,\n error: { code: 'circuit_open', message: 'Circuit breaker is open' },\n }));\n }\n }\n\n // No fallback available - circuit is protecting the system\n const circuitState = circuitBreaker.getState();\n emitStatus('error', null, statusDetails({\n error: { code: 'circuit_open', message: 'Circuit breaker is open' },\n }));\n throw new CircuitOpenError(circuitState);\n }\n\n // -------------------------------------------------------------------------\n // 3. Attempt live fetch with retries\n // -------------------------------------------------------------------------\n try {\n let data: T;\n\n if (resilienceEnabled) {\n // Track half-open requests\n if (circuitBreaker.getState().state === 'half-open') {\n circuitBreaker.incrementHalfOpenRequests();\n }\n\n // Fetch with timeout and retry\n data = await fetchWithTimeoutAndRetry(\n async (timeoutSignal) => {\n // Combine timeout signal with user-provided signal\n const combinedSignal = options.signal\n ? combineAbortSignals(timeoutSignal, options.signal)\n : timeoutSignal;\n\n try {\n const response = await fetcher(combinedSignal);\n return unwrapResponse(response);\n } catch (error) {\n convertToTypedError(error);\n }\n },\n {\n ...retryConfig,\n requestTimeoutMs,\n }\n );\n\n circuitBreaker.recordSuccess();\n } else {\n // Non-resilient path (resilience disabled)\n try {\n const response = await fetcher(options.signal ?? new AbortController().signal);\n data = unwrapResponse(response);\n } catch (error) {\n convertToTypedError(error);\n }\n }\n\n // Store in cache\n if (cacheEnabled) {\n cache.set(cacheKey, data);\n }\n\n return emitStatus('live', data, statusDetails());\n } catch (error) {\n // Record failure for circuit breaker\n if (resilienceEnabled && error instanceof Error) {\n circuitBreaker.recordFailure(error);\n }\n\n // -----------------------------------------------------------------------\n // 4. Stale fallback (published only, not preview)\n // -----------------------------------------------------------------------\n if (!isPreview && staleIfError && cacheEnabled) {\n const stale = cache.getStale(cacheKey);\n if (stale) {\n const errorInfo = error instanceof RiverbankApiError\n ? { code: error.code, message: error.message }\n : { message: (error as Error).message };\n\n return emitStatus('stale', stale.value as T, statusDetails({\n staleAgeSec: stale.staleAgeSec,\n error: errorInfo,\n }));\n }\n }\n\n // -----------------------------------------------------------------------\n // 5. Prebuild fallback (published only, not preview)\n // -----------------------------------------------------------------------\n if (!isPreview && options.prebuildFallback) {\n const prebuildResult = options.prebuildFallback();\n if (prebuildResult) {\n const errorInfo = error instanceof RiverbankApiError\n ? { code: error.code, message: error.message }\n : { message: (error as Error).message };\n\n return emitStatus('prebuild', prebuildResult.data, statusDetails({\n prebuildAgeSec: prebuildResult.prebuildAgeSec,\n error: errorInfo,\n }));\n }\n }\n\n // -----------------------------------------------------------------------\n // 6. No fallback available - propagate error\n // -----------------------------------------------------------------------\n const errorInfo = error instanceof RiverbankApiError\n ? { code: error.code, message: error.message }\n : { message: (error as Error).message };\n\n emitStatus('error', null, statusDetails({ error: errorInfo }));\n throw error;\n }\n }\n\n // ============================================================================\n // Helper: Combine AbortSignals\n // ============================================================================\n\n /**\n * Combine multiple AbortSignals into one that aborts when any input aborts\n */\n function combineAbortSignals(...signals: AbortSignal[]): AbortSignal {\n const controller = new AbortController();\n\n for (const signal of signals) {\n if (signal.aborted) {\n controller.abort(signal.reason);\n break;\n }\n signal.addEventListener('abort', () => controller.abort(signal.reason), { once: true });\n }\n\n return controller.signal;\n }\n\n // ============================================================================\n // Client Methods\n // ============================================================================\n\n async function resolveEntrySubrouteInternal(args: {\n siteId: string;\n entryId: string;\n kind: 'event-occurrence' | 'course-run';\n segment: string;\n stage?: 'preview' | 'published';\n signal?: AbortSignal;\n }): Promise<ResolveEntrySubrouteResponse> {\n const { siteId, entryId, kind, segment, stage, signal } = args;\n const cacheKey = `entry-subroute:${siteId}:${entryId}:${kind}:${segment}:${stage ?? 'published'}`;\n return resilientFetch(cacheKey, async (sig) => {\n return await apiClient({\n endpoint: 'resolveEntrySubroute',\n params: { siteId, entryId, kind, segment, ...(stage ? { stage } : {}) },\n options: { signal: sig },\n });\n }, { signal });\n }\n\n return {\n async getSite(params) {\n const { slug, domain, id, signal, force } = params;\n\n if (!slug && !domain && !id) {\n throw new Error(\n 'getSite() requires at least one identifier: slug, domain, or id. ' +\n `Received: ${JSON.stringify(params)}`\n );\n }\n\n const cacheKey = `site:${slug || domain || id}`;\n const siteId = id || slug || domain;\n\n return resilientFetch(cacheKey, async (sig) => {\n const apiParams: Record<string, string> = {};\n if (params.slug) apiParams.slug = params.slug;\n if (params.domain) apiParams.domain = params.domain;\n if (params.id) apiParams.id = params.id;\n // The dashboard `GET /api/sites` route supports `?force=true` to bypass\n // any intermediate caching (CDN/proxy) after a config push.\n if (force) apiParams.force = 'true';\n return await apiClient({\n endpoint: 'getSite',\n params: apiParams,\n // When force=true, bypass Next.js fetch caching (revalidate) as well.\n options: { signal: sig, ...(force ? { cache: 'no-store' as const } : {}) },\n });\n }, {\n signal,\n force,\n prebuildFallback: prebuildLoader && siteId\n ? () => prebuildLoader.loadSite(siteId)\n : undefined,\n });\n },\n\n async getPage(params) {\n const { siteId, path, preview = false, signal } = params;\n const cacheKey = `page:${siteId}:${path}:${preview}`;\n\n return resilientFetch(cacheKey, async (sig) => {\n return await apiClient({ endpoint: 'getContentByPath', params: { siteId }, body: { path, preview }, options: { signal: sig } });\n }, {\n preview,\n signal,\n // Prebuild fallback only for published pages (not preview)\n prebuildFallback: prebuildLoader && !preview\n ? () => prebuildLoader.loadPage(siteId, path)\n : undefined,\n });\n },\n\n async getEntries(params: GetEntriesBaseParams & { includeMeta?: boolean }) {\n const { siteId, contentType, limit, offset, order, preview = false, mode, entryIds, includeMeta, signal } = params;\n\n const entryIdsCacheKey = mode === 'manual' && entryIds?.length ? entryIds.join(',') : '';\n const cacheKey = `entries:${siteId}:${contentType}:${limit ?? ''}:${offset ?? ''}:${order ?? ''}:${preview}:${mode ?? ''}:${entryIdsCacheKey}:${includeMeta ?? ''}`;\n\n return resilientFetch(cacheKey, async (sig) => {\n let orderParam: string | undefined;\n if (order === 'newest') {\n orderParam = 'published_at.desc';\n } else if (order === 'oldest') {\n orderParam = 'published_at.asc';\n } else if (order === 'title') {\n orderParam = 'title.asc';\n }\n\n const apiParams = {\n siteId,\n type: contentType,\n ...(typeof limit === 'number' && { limit: String(limit) }),\n ...(typeof offset === 'number' && { offset: String(offset) }),\n ...(includeMeta && { meta: 'true' }),\n ...(orderParam && { order: orderParam }),\n ...(preview && { stage: 'preview' as const }),\n ...(mode === 'manual' && entryIds?.length && {\n mode: 'manual' as const,\n entryIds: JSON.stringify(entryIds),\n }),\n };\n\n return await apiClient({ endpoint: 'listPublishedEntries', params: apiParams, options: { signal: sig } });\n }, {\n preview,\n signal,\n // Prebuild fallback only for published entries (not preview, not manual mode)\n prebuildFallback: prebuildLoader && !preview\n ? () => prebuildLoader.loadEntries(siteId, params)\n : undefined,\n });\n },\n\n async getEntry(params) {\n const { siteId, contentType, slug, signal } = params;\n const cacheKey = `entry:${siteId}:${contentType}:${slug}`;\n\n return resilientFetch(cacheKey, async (sig) => {\n return await apiClient({ endpoint: 'getPublishedEntryPreview', params: { siteId, type: contentType, slug }, options: { signal: sig } });\n }, { signal });\n },\n\n async getPublicFormById(params) {\n const { formId, siteId, signal } = params;\n if (!formId || !siteId) {\n throw new Error('getPublicFormById() requires formId and siteId');\n }\n const cacheKey = `public-form:${siteId}:${formId}`;\n return resilientFetch(cacheKey, async (sig) => {\n return await apiClient({ endpoint: 'getPublicFormById', params: { formId, siteId }, options: { signal: sig } });\n }, { signal });\n },\n\n async getForms(params) {\n const { siteId, signal } = params;\n if (!siteId) {\n throw new Error('getForms() requires siteId');\n }\n const cacheKey = `forms:${siteId}`;\n return resilientFetch(cacheKey, async (sig) => {\n return await apiClient({ endpoint: 'listPublicForms', params: { siteId }, options: { signal: sig } });\n }, {\n signal,\n prebuildFallback: prebuildLoader\n ? () => {\n const result = prebuildLoader.loadForms();\n if (result) {\n return { data: { forms: result.data }, prebuildAgeSec: result.prebuildAgeSec };\n }\n return null;\n }\n : undefined,\n });\n },\n\n async getPublicBookingServices(params) {\n const { siteId, ids, signal } = params;\n if (!siteId) {\n throw new Error('getPublicBookingServices() requires siteId');\n }\n const cacheKey = `public-booking-services:${siteId}:${ids ?? ''}`;\n return resilientFetch(cacheKey, async (sig) => {\n const apiParams = {\n siteId,\n ...(ids && { ids }),\n };\n return await apiClient({ endpoint: 'getPublicBookingServices', params: apiParams, options: { signal: sig } });\n }, { signal });\n },\n\n async listPublicEvents(params) {\n const { siteId, limit, from, to, stage, entryId, seriesId, includeOccurrenceId, signal } = params;\n if (!siteId) {\n throw new Error('listPublicEvents() requires siteId');\n }\n const cacheKey = `public-events:${siteId}:${limit ?? ''}:${from ?? ''}:${to ?? ''}:${stage ?? ''}:${entryId ?? ''}:${seriesId ?? ''}:${includeOccurrenceId ?? ''}`;\n return resilientFetch(cacheKey, async (sig) => {\n const apiParams = {\n siteId,\n ...(typeof limit === 'number' && { limit: String(limit) }),\n ...(from && { from }),\n ...(to && { to }),\n ...(stage && { stage }),\n ...(entryId && { entryId }),\n ...(seriesId && { seriesId }),\n ...(includeOccurrenceId && { includeOccurrenceId }),\n };\n return await apiClient({ endpoint: 'listPublicEvents', params: apiParams, options: { signal: sig } });\n }, { signal });\n },\n\n async listPublicCourses(params) {\n const { siteId, limit, stage, signal } = params;\n if (!siteId) {\n throw new Error('listPublicCourses() requires siteId');\n }\n const cacheKey = `public-courses:${siteId}:${limit ?? ''}:${stage ?? ''}`;\n return resilientFetch(cacheKey, async (sig) => {\n const apiParams = {\n siteId,\n ...(typeof limit === 'number' && { limit: String(limit) }),\n ...(stage && { stage }),\n };\n return await apiClient({ endpoint: 'listPublicCourses', params: apiParams, options: { signal: sig } });\n }, { signal });\n },\n\n async listPublicPasses(params) {\n const { siteId, categories, signal } = params;\n if (!siteId) {\n throw new Error('listPublicPasses() requires siteId');\n }\n const cacheKey = `public-passes:${siteId}:${categories ?? ''}`;\n return resilientFetch(cacheKey, async (sig) => {\n const apiParams = {\n siteId,\n ...(categories && { categories }),\n };\n return await apiClient({ endpoint: 'listPublicPasses', params: apiParams, options: { signal: sig } });\n }, { signal });\n },\n\n async listPublicMemberships(params) {\n const { siteId, categories, signal } = params;\n if (!siteId) {\n throw new Error('listPublicMemberships() requires siteId');\n }\n const cacheKey = `public-memberships:${siteId}:${categories ?? ''}`;\n return resilientFetch(cacheKey, async (sig) => {\n const apiParams = {\n siteId,\n ...(categories && { categories }),\n };\n return await apiClient({ endpoint: 'listPublicMemberships', params: apiParams, options: { signal: sig } });\n }, { signal });\n },\n\n async resolveEntrySubroute(params) {\n const { siteId, entryId, kind, segment, stage, signal } = params;\n if (!siteId || !entryId || !kind || !segment) {\n throw new Error('resolveEntrySubroute() requires siteId, entryId, kind, and segment');\n }\n return resolveEntrySubrouteInternal({ siteId, entryId, kind, segment, stage, signal });\n },\n\n async resolveEventOccurrence(params) {\n const { siteId, entryId, segment, signal } = params;\n if (!siteId || !entryId || !segment) {\n throw new Error('resolveEventOccurrence() requires siteId, entryId, and segment');\n }\n const result = await resolveEntrySubrouteInternal({\n siteId,\n entryId,\n kind: 'event-occurrence',\n segment,\n signal,\n });\n return {\n occurrence:\n result.subroute?.kind === 'event-occurrence'\n ? result.subroute.occurrence\n : null,\n };\n },\n\n async checkRedirect(params) {\n const { siteId, path, signal } = params;\n if (!siteId || !path) {\n throw new Error('checkRedirect() requires siteId and path');\n }\n const cacheKey = `redirect:${siteId}:${path}`;\n return resilientFetch(cacheKey, async (sig) => {\n return await apiClient({\n endpoint: 'checkRedirect',\n params: { site: siteId, path },\n options: { signal: sig },\n });\n }, { signal });\n },\n\n async getAllPublishedRoutes(params) {\n const { siteId, signal } = params;\n if (!siteId) {\n throw new Error('getAllPublishedRoutes() requires siteId');\n }\n const cacheKey = `routable-content:${siteId}:published`;\n return resilientFetch(cacheKey, async (sig) => {\n return await apiClient({\n endpoint: 'getPublicRoutableContent',\n params: { siteId, publishedOnly: 'true' },\n options: { signal: sig },\n });\n }, { signal });\n },\n\n clearCache() {\n cache.clear();\n },\n\n getKeyType(): ApiKeyType {\n return keyType;\n },\n\n getLastEmittedStatus(): ResilienceStatus | null {\n return lastStatus;\n },\n\n getCircuitState(): CircuitState {\n return circuitBreaker.getState();\n },\n\n getBaseUrl(): string {\n return config.baseUrl;\n },\n } as RiverbankClient;\n}\n\n// Re-export types\nexport type { RiverbankClient, RiverbankClientConfig, ResilienceConfig, ResilienceStatus, ApiKeyType, SdkAuthConfig } from './types';\n\n// Re-export error class\nexport { RiverbankApiError } from './error';\n\n// Re-export CircuitOpenError for consumer detection\nexport { CircuitOpenError } from './resilience';\n"]}
|
|
1
|
+
{"version":3,"sources":["/Users/will/Projects/Business/cms/main-branch/packages/sdk/dist/server/chunk-VD5JTAF2.js","../../src/client/error.ts","../../src/client/cache.ts","../../src/client/resilience.ts","../../src/client/index.ts"],"names":["errorInfo"],"mappings":"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","file":"/Users/will/Projects/Business/cms/main-branch/packages/sdk/dist/server/chunk-VD5JTAF2.js","sourcesContent":[null,"/**\n * SDK API Error Class\n *\n * Thrown when the API returns an error response with the new envelope format.\n * Provides typed access to error details including code, message, request ID, and field errors.\n */\n\nimport type { ApiErrorCode, FieldError, ApiError } from '@riverbankcms/api/public';\n\n/**\n * Error options for creating RiverbankApiError\n */\nexport interface RiverbankApiErrorOptions {\n code: ApiErrorCode;\n message: string;\n requestId: string;\n status: number;\n timestamp: string;\n fieldErrors?: FieldError[];\n /** Retry delay from Retry-After header, in milliseconds */\n retryAfterMs?: number;\n /** Original error that caused this error (for network errors) */\n cause?: Error;\n}\n\n/**\n * Error thrown by SDK when API returns an error response\n *\n * @example\n * ```ts\n * try {\n * await client.getSite({ slug: 'my-site' });\n * } catch (error) {\n * if (error instanceof RiverbankApiError) {\n * console.log('Error code:', error.code);\n * console.log('Request ID:', error.requestId);\n *\n * if (error.isAuthError()) {\n * // Handle authentication error\n * }\n *\n * if (error.isValidationError() && error.fieldErrors) {\n * // Handle field-level validation errors\n * error.fieldErrors.forEach(fe => {\n * console.log(`${fe.field}: ${fe.message}`);\n * });\n * }\n *\n * // Check if error can be retried\n * if (error.isRetryable) {\n * const delay = error.retryAfterMs ?? 1000;\n * console.log(`Retrying after ${delay}ms...`);\n * }\n * }\n * }\n * ```\n */\nexport class RiverbankApiError extends Error {\n /** Namespaced error code (e.g., 'auth:unauthenticated', 'validation:invalid_input') */\n readonly code: ApiErrorCode;\n\n /** Unique request ID for debugging/support */\n readonly requestId: string;\n\n /** HTTP status code */\n readonly status: number;\n\n /** Field-level validation errors (only present for validation errors) */\n readonly fieldErrors?: FieldError[];\n\n /** ISO timestamp when the error occurred */\n readonly timestamp: string;\n\n /** Original error cause for error classification (ECONNREFUSED, etc.) */\n declare readonly cause?: Error;\n\n /**\n * Whether this error is safe to retry.\n * True for: 429 rate limit, 5xx server errors\n * False for: 4xx client errors (except 429)\n */\n readonly isRetryable: boolean;\n\n /**\n * Suggested retry delay from Retry-After header, in milliseconds.\n * Only present when the server provides a Retry-After header.\n */\n readonly retryAfterMs?: number;\n\n constructor(apiError: ApiError['error'] | RiverbankApiErrorOptions) {\n super(apiError.message);\n this.name = 'RiverbankApiError';\n // Preserve original error cause for error classification (ECONNREFUSED, etc.)\n if ('cause' in apiError && apiError.cause) {\n this.cause = apiError.cause;\n }\n this.code = apiError.code;\n this.requestId = apiError.requestId;\n this.status = apiError.status;\n this.fieldErrors = apiError.fieldErrors;\n this.timestamp = apiError.timestamp;\n this.retryAfterMs = 'retryAfterMs' in apiError ? apiError.retryAfterMs : undefined;\n this.isRetryable = this.computeRetryable();\n\n // Maintain proper prototype chain for instanceof checks\n Object.setPrototypeOf(this, RiverbankApiError.prototype);\n }\n\n /**\n * Compute whether this error is retryable based on HTTP status code.\n * - 0 (network errors - no HTTP response): retryable\n * - 429 (rate limit): retryable\n * - 5xx (server errors): retryable\n * - 4xx (client errors, except 429): NOT retryable\n */\n private computeRetryable(): boolean {\n // Network errors (no HTTP response) are retryable\n if (this.status === 0) return true;\n // Rate limiting is retryable\n if (this.status === 429) return true;\n // Server errors are retryable\n if (this.status >= 500) return true;\n // Client errors (4xx except 429) are not retryable\n return false;\n }\n\n /**\n * Check if this is a network error (no HTTP response received)\n *\n * Matches: network:connection_error, network:timeout, network:dns_error\n */\n isNetworkError(): boolean {\n return this.code.startsWith('network:');\n }\n\n /**\n * Check if this error matches a specific error code\n *\n * @example\n * ```ts\n * if (error.is('auth:unauthenticated')) {\n * // Redirect to login\n * }\n * ```\n */\n is(code: ApiErrorCode): boolean {\n return this.code === code;\n }\n\n /**\n * Check if this is an authentication or authorization error\n *\n * Matches: auth:unauthenticated, auth:token_expired, auth:token_invalid,\n * auth:forbidden, auth:mfa_required, auth:insufficient_permissions\n */\n isAuthError(): boolean {\n return this.code.startsWith('auth:');\n }\n\n /**\n * Check if this is a validation error\n *\n * Matches: validation:invalid_input, validation:missing_field, validation:invalid_format\n */\n isValidationError(): boolean {\n return this.code.startsWith('validation:');\n }\n\n /**\n * Check if this is a resource error (not found, conflict, etc.)\n *\n * Matches: resource:not_found, resource:already_exists, resource:conflict, resource:gone\n */\n isResourceError(): boolean {\n return this.code.startsWith('resource:');\n }\n\n /**\n * Check if this is a rate limiting error\n */\n isRateLimitError(): boolean {\n return this.code.startsWith('rate_limit:');\n }\n\n /**\n * Check if this is a billing/payment error\n */\n isBillingError(): boolean {\n return this.code.startsWith('billing:');\n }\n\n /**\n * Check if this is a server error\n */\n isServerError(): boolean {\n return this.code.startsWith('server:');\n }\n\n /**\n * Returns a human-readable string representation of the error.\n * Includes all key details for debugging.\n *\n * @example\n * \"RiverbankApiError: Content keys cannot access preview content | Code: auth:forbidden | Status: 401 | RequestId: req-abc123\"\n */\n override toString(): string {\n const parts = [`RiverbankApiError: ${this.message}`];\n if (this.code) parts.push(`Code: ${this.code}`);\n if (this.status) parts.push(`Status: ${this.status}`);\n if (this.requestId) parts.push(`RequestId: ${this.requestId}`);\n return parts.join(' | ');\n }\n\n /**\n * Custom Node.js inspect output for better console.log display.\n * This ensures that console.log(error) shows all relevant details\n * instead of just \"[Object]\" for nested properties.\n */\n [Symbol.for('nodejs.util.inspect.custom')](): string {\n return this.toDetailedString();\n }\n\n /**\n * Returns a detailed multi-line string for debugging.\n * Used by the Node.js inspect symbol for console output.\n */\n toDetailedString(): string {\n const lines = [\n `RiverbankApiError: ${this.message}`,\n ` Code: ${this.code}`,\n ` Status: ${this.status}`,\n ` RequestId: ${this.requestId}`,\n ` Timestamp: ${this.timestamp}`,\n ];\n\n if (this.isRetryable) {\n lines.push(` Retryable: true`);\n if (this.retryAfterMs) {\n lines.push(` RetryAfter: ${this.retryAfterMs}ms`);\n }\n }\n\n if (this.fieldErrors && this.fieldErrors.length > 0) {\n lines.push(' FieldErrors:');\n this.fieldErrors.forEach((fe) => {\n lines.push(` - ${fe.field}: ${fe.message}`);\n });\n }\n\n return lines.join('\\n');\n }\n}\n","/**\n * Simple in-memory cache with TTL and stale support\n *\n * Supports:\n * - Fresh entries: Within TTL, returned immediately\n * - Stale entries: Past TTL but within staleTtl, returned as fallback\n * - Automatic eviction: Stale entries evicted first, then oldest fresh\n */\n\n/**\n * Cache entry with freshness and stale windows\n */\ninterface CacheEntry<T> {\n value: T;\n /** Timestamp when entry was created */\n createdAt: number;\n /** Timestamp until which entry is fresh */\n freshUntil: number;\n /** Timestamp until which entry can be served as stale */\n staleUntil: number;\n}\n\n/**\n * Result from getStale() with age information\n */\nexport interface StaleResult<T> {\n value: T;\n /** Seconds since the entry's TTL expired (0 if still fresh) */\n staleAgeSec: number;\n}\n\n/**\n * Options for cache.set()\n */\nexport interface CacheSetOptions {\n /** Time-to-live in milliseconds (overrides default) */\n ttl?: number;\n /** Additional time after TTL during which stale data can be served (overrides default) */\n staleTtl?: number;\n}\n\nexport class SimpleCache<T> {\n private cache = new Map<string, CacheEntry<T>>();\n private maxSize: number;\n private ttl: number;\n private staleTtl: number;\n\n constructor(options: { maxSize?: number; ttl?: number; staleTtl?: number } = {}) {\n this.maxSize = options.maxSize ?? 100;\n this.ttl = options.ttl ?? 300000; // 5 minutes in milliseconds\n this.staleTtl = options.staleTtl ?? 300000; // 5 minutes additional stale window\n }\n\n /**\n * Get a fresh value (within TTL)\n * @returns The value if fresh, null otherwise\n */\n getFresh(key: string): T | null {\n const entry = this.cache.get(key);\n if (!entry) return null;\n\n const now = Date.now();\n if (now <= entry.freshUntil) {\n return entry.value;\n }\n\n return null;\n }\n\n /**\n * Get a value that may be stale (past TTL but within staleTtl)\n * @returns Object with value and stale age, or null if expired\n */\n getStale(key: string): StaleResult<T> | null {\n const entry = this.cache.get(key);\n if (!entry) return null;\n\n const now = Date.now();\n\n // Completely expired\n if (now > entry.staleUntil) {\n this.cache.delete(key);\n return null;\n }\n\n // Calculate stale age (0 if still fresh)\n const staleAgeSec = now <= entry.freshUntil\n ? 0\n : Math.floor((now - entry.freshUntil) / 1000);\n\n return {\n value: entry.value,\n staleAgeSec,\n };\n }\n\n /**\n * Store a value with TTL and stale window\n */\n set(key: string, value: T, options?: CacheSetOptions): void {\n const ttl = options?.ttl ?? this.ttl;\n const staleTtl = options?.staleTtl ?? this.staleTtl;\n const now = Date.now();\n\n // Enforce max size with smart eviction.\n // Use while loop because evictOne() may not reduce size if the cache\n // contains only non-expired entries and evictOne finds nothing to remove\n // (defensive edge case - shouldn't happen in normal operation).\n while (this.cache.size >= this.maxSize && !this.cache.has(key)) {\n this.evictOne(now);\n }\n\n this.cache.set(key, {\n value,\n createdAt: now,\n freshUntil: now + ttl,\n staleUntil: now + ttl + staleTtl,\n });\n }\n\n /**\n * Evict one entry to make room for a new one\n * Priority: oldest stale entry, then oldest fresh entry\n */\n private evictOne(now: number): void {\n let oldestStaleKey: string | null = null;\n let oldestStaleTime = Infinity;\n let oldestFreshKey: string | null = null;\n let oldestFreshTime = Infinity;\n\n for (const [key, entry] of this.cache) {\n if (now > entry.freshUntil) {\n // Stale entry\n if (entry.createdAt < oldestStaleTime) {\n oldestStaleTime = entry.createdAt;\n oldestStaleKey = key;\n }\n } else {\n // Fresh entry\n if (entry.createdAt < oldestFreshTime) {\n oldestFreshTime = entry.createdAt;\n oldestFreshKey = key;\n }\n }\n }\n\n // Evict oldest stale first, then oldest fresh\n const keyToEvict = oldestStaleKey ?? oldestFreshKey;\n if (keyToEvict) {\n this.cache.delete(keyToEvict);\n }\n }\n\n /**\n * Remove all fully expired entries (past staleUntil)\n */\n prune(): void {\n const now = Date.now();\n for (const [key, entry] of this.cache) {\n if (now > entry.staleUntil) {\n this.cache.delete(key);\n }\n }\n }\n\n /**\n * Clear all entries\n */\n clear(): void {\n this.cache.clear();\n }\n\n /**\n * Check if a key exists and is not fully expired\n */\n has(key: string): boolean {\n const entry = this.cache.get(key);\n if (!entry) return false;\n\n const now = Date.now();\n if (now > entry.staleUntil) {\n this.cache.delete(key);\n return false;\n }\n\n return now <= entry.freshUntil;\n }\n}\n","/**\n * Resilience utilities for the SDK client\n *\n * Provides:\n * - Circuit breaker pattern for fail-fast behavior\n * - Retry with exponential backoff and jitter\n * - Timeout handling\n */\n\nimport type { RetryConfig, CircuitBreakerConfig, CircuitState } from './types';\nimport { RiverbankApiError } from './error';\n\n// ============================================================================\n// Default Configurations\n// ============================================================================\n\nexport const DEFAULT_RETRY_CONFIG: Required<Omit<RetryConfig, 'retryOn'>> = {\n maxAttempts: 3,\n baseDelayMs: 200,\n maxDelayMs: 2000,\n jitter: 'full',\n};\n\nexport const DEFAULT_CIRCUIT_BREAKER_CONFIG: Required<CircuitBreakerConfig> = {\n failureThreshold: 5,\n resetTimeoutMs: 30000,\n halfOpenMaxRequests: 2,\n};\n\n// ============================================================================\n// Error Classification\n// ============================================================================\n\n/**\n * Error codes that indicate permanent infrastructure failures.\n * These should NOT be retried as the server is definitively unreachable.\n */\nconst PERMANENT_NETWORK_ERROR_CODES = new Set([\n 'ECONNREFUSED', // Server is not running / port not listening\n 'ENOTFOUND', // DNS lookup failed - hostname doesn't exist\n 'EAI_AGAIN', // DNS lookup timeout (usually permanent for invalid hosts)\n]);\n\n/**\n * Error codes that indicate transient network issues.\n * These MAY succeed on retry as the server was reachable but had a hiccup.\n */\nconst TRANSIENT_NETWORK_ERROR_CODES = new Set([\n 'ECONNRESET', // Connection was reset mid-request (server dropped it)\n 'EPIPE', // Broken pipe (connection closed while writing)\n 'ETIMEDOUT', // Connection timed out (could be temporary congestion)\n 'ESOCKETTIMEDOUT', // Socket timeout\n]);\n\n/**\n * Node.js network error codes (POSIX style).\n * These are the codes we're looking for in the cause chain.\n */\nconst NODE_NETWORK_ERROR_CODES = new Set([\n // Permanent\n 'ECONNREFUSED',\n 'ENOTFOUND',\n 'EAI_AGAIN',\n // Transient\n 'ECONNRESET',\n 'EPIPE',\n 'ETIMEDOUT',\n 'ESOCKETTIMEDOUT',\n]);\n\n/**\n * Check if a string is a Node.js network error code (not an API error code).\n * API error codes look like 'network:connection_error', Node codes look like 'ECONNREFUSED'.\n */\nfunction isNodeNetworkErrorCode(code: string): boolean {\n // Node.js error codes are uppercase without colons\n // API error codes contain colons (e.g., 'network:connection_error')\n return !code.includes(':') && NODE_NETWORK_ERROR_CODES.has(code);\n}\n\n/**\n * Extract the Node.js error code from an error's cause chain.\n * Handles nested cause chains like:\n * RiverbankApiError -> TypeError -> Error (ECONNREFUSED)\n *\n * Skips API error codes (like 'network:connection_error') and only returns\n * actual Node.js error codes (like 'ECONNREFUSED', 'ENOTFOUND', etc.)\n *\n * @example\n * RiverbankApiError: Network request failed\n * [cause]: TypeError: fetch failed\n * [cause]: Error: connect ECONNREFUSED 127.0.0.1:4010\n * code: 'ECONNREFUSED'\n */\nfunction getErrorCodeFromCause(error: Error): string | undefined {\n // Walk the cause chain looking for a Node.js error code\n let current: Error | undefined = error;\n\n while (current) {\n // Check if current error has a Node.js error code\n const nodeError = current as NodeJS.ErrnoException;\n if (nodeError.code && typeof nodeError.code === 'string') {\n // Only return actual Node.js error codes, not API error codes\n if (isNodeNetworkErrorCode(nodeError.code)) {\n return nodeError.code;\n }\n }\n\n // Move to the cause\n const errorWithCause = current as Error & { cause?: Error };\n current = errorWithCause.cause;\n }\n\n return undefined;\n}\n\n/**\n * Determine if an error is transient and should trigger circuit breaker / retry\n *\n * Transient (opens circuit, triggers retry):\n * - Network errors (status 0) that are NOT permanent infrastructure failures\n * - 5xx server errors\n * - 429 rate limit\n * - Connection reset, broken pipe, timeout\n *\n * Permanent (does NOT open circuit, no retry):\n * - 4xx client errors (400, 401, 403, 404)\n * - Validation errors\n * - ECONNREFUSED (server not running)\n * - ENOTFOUND (DNS failure)\n */\nexport function isTransientError(error: Error): boolean {\n if (error instanceof RiverbankApiError) {\n // Check for permanent network errors first (from wrapped TypeErrors)\n const errorCode = getErrorCodeFromCause(error);\n if (errorCode && PERMANENT_NETWORK_ERROR_CODES.has(errorCode)) {\n return false;\n }\n\n // Network errors are transient (unless permanent code above)\n if (error.status === 0) return true;\n\n // 429 rate limit is transient\n if (error.status === 429) return true;\n\n // 5xx server errors are transient\n if (error.status >= 500) return true;\n\n // 4xx client errors are permanent\n return false;\n }\n\n // For TypeErrors (Node.js fetch failures), check the cause for error code\n if (error instanceof TypeError) {\n const errorCode = getErrorCodeFromCause(error);\n\n if (errorCode) {\n // Permanent infrastructure failures - don't retry\n if (PERMANENT_NETWORK_ERROR_CODES.has(errorCode)) {\n return false;\n }\n\n // Known transient codes - do retry\n if (TRANSIENT_NETWORK_ERROR_CODES.has(errorCode)) {\n return true;\n }\n }\n\n // Unknown TypeError without recognized code - default to transient\n // (backwards compatible, safer assumption for unexpected errors)\n return true;\n }\n\n // Other generic errors - default to transient (backwards compatible)\n return true;\n}\n\n// ============================================================================\n// Backoff Calculation\n// ============================================================================\n\n/**\n * Calculate backoff delay for retry attempt\n *\n * Uses exponential backoff with optional full jitter.\n * Formula: min(maxDelay, baseDelay * 2^(attempt-1)) * random()\n */\nexport function calculateBackoff(\n attempt: number,\n config: Partial<Pick<RetryConfig, 'baseDelayMs' | 'maxDelayMs' | 'jitter'>>\n): number {\n const baseDelayMs = config.baseDelayMs ?? DEFAULT_RETRY_CONFIG.baseDelayMs;\n const maxDelayMs = config.maxDelayMs ?? DEFAULT_RETRY_CONFIG.maxDelayMs;\n const jitter = config.jitter ?? DEFAULT_RETRY_CONFIG.jitter;\n\n // Exponential: baseDelay * 2^(attempt-1)\n const exponential = baseDelayMs * Math.pow(2, attempt - 1);\n\n // Cap at maxDelay\n const capped = Math.min(exponential, maxDelayMs);\n\n // Apply jitter\n if (jitter === 'full') {\n return Math.random() * capped;\n }\n\n return capped;\n}\n\n// ============================================================================\n// Circuit Breaker\n// ============================================================================\n\n/**\n * Circuit breaker for fail-fast behavior\n *\n * States:\n * - closed: Normal operation, requests allowed\n * - open: Too many failures, requests blocked\n * - half-open: Testing if service recovered, limited requests allowed\n */\nexport class CircuitBreaker {\n private state: 'closed' | 'open' | 'half-open' = 'closed';\n private failureCount = 0;\n private successCount = 0;\n private openUntil = 0;\n private halfOpenRequests = 0;\n private config: Required<CircuitBreakerConfig>;\n\n constructor(config?: CircuitBreakerConfig) {\n this.config = {\n failureThreshold: config?.failureThreshold ?? DEFAULT_CIRCUIT_BREAKER_CONFIG.failureThreshold,\n resetTimeoutMs: config?.resetTimeoutMs ?? DEFAULT_CIRCUIT_BREAKER_CONFIG.resetTimeoutMs,\n halfOpenMaxRequests: config?.halfOpenMaxRequests ?? DEFAULT_CIRCUIT_BREAKER_CONFIG.halfOpenMaxRequests,\n };\n }\n\n /**\n * Check if circuit is open (requests should be blocked)\n * Also handles automatic transition from open to half-open after timeout\n */\n isOpen(): boolean {\n if (this.state === 'open' && Date.now() >= this.openUntil) {\n this.transitionTo('half-open');\n }\n return this.state === 'open';\n }\n\n /**\n * Check if a request can be attempted\n * - closed: always yes\n * - open: always no\n * - half-open: limited number of probes\n */\n canAttempt(): boolean {\n if (this.state === 'closed') return true;\n if (this.state === 'open') return false;\n // half-open: allow limited probes\n return this.halfOpenRequests < this.config.halfOpenMaxRequests;\n }\n\n /**\n * Increment half-open request counter (call before making request in half-open)\n */\n incrementHalfOpenRequests(): void {\n if (this.state === 'half-open') {\n this.halfOpenRequests++;\n }\n }\n\n /**\n * Record a successful request\n */\n recordSuccess(): void {\n if (this.state === 'half-open') {\n this.successCount++;\n if (this.successCount >= this.config.halfOpenMaxRequests) {\n this.transitionTo('closed');\n }\n } else {\n // Reset consecutive failures on any success\n this.failureCount = 0;\n }\n }\n\n /**\n * Record a failed request\n * Only counts transient failures toward circuit breaker threshold\n */\n recordFailure(error: Error): void {\n // Only count transient failures\n if (!isTransientError(error)) return;\n\n this.failureCount++;\n\n if (this.state === 'half-open') {\n this.transitionTo('open');\n } else if (this.failureCount >= this.config.failureThreshold) {\n this.transitionTo('open');\n }\n }\n\n /**\n * Get current circuit state\n */\n getState(): CircuitState {\n return {\n state: this.state,\n failureCount: this.failureCount,\n openUntil: this.state === 'open' ? this.openUntil : undefined,\n };\n }\n\n /**\n * Transition to a new state\n */\n private transitionTo(newState: 'closed' | 'open' | 'half-open'): void {\n this.state = newState;\n\n if (newState === 'open') {\n this.openUntil = Date.now() + this.config.resetTimeoutMs;\n } else if (newState === 'half-open') {\n this.halfOpenRequests = 0;\n this.successCount = 0;\n } else if (newState === 'closed') {\n this.failureCount = 0;\n this.successCount = 0;\n this.halfOpenRequests = 0;\n }\n }\n}\n\n// ============================================================================\n// Retry with Timeout\n// ============================================================================\n\n/**\n * Configuration for fetchWithTimeoutAndRetry\n */\nexport interface FetchWithRetryConfig {\n maxAttempts?: number;\n baseDelayMs?: number;\n maxDelayMs?: number;\n jitter?: 'full' | 'none';\n requestTimeoutMs?: number;\n retryOn?: (error: Error, statusCode?: number) => boolean;\n}\n\n/**\n * Fetch with timeout and retry logic\n *\n * @param fetcher - Function that performs the fetch, receives AbortSignal for timeout\n * @param config - Retry configuration\n * @returns Result from successful fetch\n * @throws Last error after all retries exhausted\n */\nexport async function fetchWithTimeoutAndRetry<T>(\n fetcher: (signal: AbortSignal) => Promise<T>,\n config: FetchWithRetryConfig\n): Promise<T> {\n const maxAttempts = config.maxAttempts ?? DEFAULT_RETRY_CONFIG.maxAttempts;\n const requestTimeoutMs = config.requestTimeoutMs ?? 8000;\n\n let lastError: Error | undefined;\n\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);\n\n try {\n const result = await fetcher(controller.signal);\n return result;\n } finally {\n clearTimeout(timeoutId);\n }\n } catch (error) {\n lastError = error as Error;\n\n // Check if we should retry\n const shouldRetry = shouldRetryError(error as Error, config.retryOn);\n\n if (!shouldRetry) {\n throw error;\n }\n\n // Don't delay after last attempt\n if (attempt < maxAttempts) {\n const delay = getRetryDelay(error as Error, attempt, config);\n await sleep(delay);\n }\n }\n }\n\n throw lastError;\n}\n\n/**\n * Check if an error is an AbortError (from AbortController.abort() or timeout)\n */\nfunction isAbortError(error: unknown): boolean {\n // DOMException with name 'AbortError' (browsers)\n if (typeof DOMException !== 'undefined' && error instanceof DOMException && error.name === 'AbortError') {\n return true;\n }\n // Node.js AbortError\n if (error instanceof Error && error.name === 'AbortError') {\n return true;\n }\n return false;\n}\n\n/**\n * Check if an error should be retried\n */\nfunction shouldRetryError(\n error: Error,\n customRetryOn?: (error: Error, statusCode?: number) => boolean\n): boolean {\n // Never retry AbortErrors - these are either:\n // 1. User explicitly cancelled the request\n // 2. Timeout fired (request already took too long, retrying won't help)\n if (isAbortError(error)) {\n return false;\n }\n\n // Custom retry logic takes precedence\n if (customRetryOn) {\n const statusCode = error instanceof RiverbankApiError ? error.status : undefined;\n return customRetryOn(error, statusCode);\n }\n\n // Default: retry transient errors\n return isTransientError(error);\n}\n\n/**\n * Get delay before retrying, respecting Retry-After if present\n */\nfunction getRetryDelay(error: Error, attempt: number, config: FetchWithRetryConfig): number {\n // Check for Retry-After from rate limiting\n if (error instanceof RiverbankApiError && error.retryAfterMs) {\n return error.retryAfterMs;\n }\n\n // Use exponential backoff\n return calculateBackoff(attempt, config);\n}\n\n/**\n * Sleep for specified milliseconds\n */\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n// ============================================================================\n// Circuit Open Error\n// ============================================================================\n\n/**\n * Error thrown when circuit breaker is open\n */\nexport class CircuitOpenError extends Error {\n readonly circuitState: CircuitState;\n\n constructor(state: CircuitState) {\n super('Circuit breaker is open');\n this.name = 'CircuitOpenError';\n this.circuitState = state;\n }\n}\n","import { createBearerAPIClient, createInternalAPIClient, setSdkVersion, ApiRequestError, isApiError, unwrapResponse, ApiEnvelopeError } from '@riverbankcms/api/public';\nimport type { ApiResult, ApiError } from '@riverbankcms/api/public';\nimport type {\n RiverbankClient,\n RiverbankClientConfig,\n GetEntriesBaseParams,\n ResilienceStatus,\n ResilienceSource,\n CircuitState,\n ApiKeyType,\n SdkAuthConfig,\n ResolveEntrySubrouteResponse,\n} from './types';\nimport { SimpleCache } from './cache';\nimport { SDK_VERSION } from '../version';\nimport { RiverbankApiError } from './error';\nimport {\n CircuitBreaker,\n CircuitOpenError,\n fetchWithTimeoutAndRetry,\n DEFAULT_RETRY_CONFIG,\n DEFAULT_CIRCUIT_BREAKER_CONFIG,\n} from './resilience';\n\n// Lazy-loaded prebuild module to avoid bundling Node.js 'fs' module in client builds.\n// The prebuild loader is only used server-side for fallback content.\nlet prebuildModule: typeof import('../prebuild/loader') | null = null;\n\nfunction tryRequire(modulePath: string): unknown {\n try {\n // Hide `require()` from bundlers so Node-only modules like `fs` don't get pulled into\n // Edge/browser bundles. In Node.js CJS, this returns the real require function.\n // In ESM/Edge, it throws or returns null and we treat it as unavailable.\n \n const req = Function('return typeof require === \"function\" ? require : null')() as\n | ((id: string) => unknown)\n | null;\n return req ? req(modulePath) : null;\n } catch {\n return null;\n }\n}\n\nfunction getPrebuildModule(): typeof import('../prebuild/loader') | null {\n if (prebuildModule !== null) return prebuildModule;\n\n // Only attempt to load in Node.js environments\n if (typeof process === 'undefined' || !process.versions?.node) {\n return null;\n }\n\n try {\n // Node-only: load prebuild module synchronously when available.\n // Uses an indirect require to avoid static bundling in Edge/browser builds.\n prebuildModule = tryRequire('../prebuild/loader') as typeof import('../prebuild/loader');\n return prebuildModule;\n } catch {\n return null;\n }\n}\n\n// Set SDK version for all API requests from this SDK\nsetSdkVersion(SDK_VERSION);\n\n// Default timeout values - shorter for browsers (user is waiting), longer for servers\nconst DEFAULT_BROWSER_TIMEOUT_MS = 5000;\nconst DEFAULT_SERVER_TIMEOUT_MS = 8000;\n\n/**\n * Generate a unique request ID for tracing\n */\nfunction generateRequestId(): string {\n return `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n}\n\n/**\n * Check if an error is an AbortError (from AbortController.abort())\n */\nfunction isAbortError(error: unknown): boolean {\n // DOMException with name 'AbortError' (browsers)\n if (error instanceof DOMException && error.name === 'AbortError') {\n return true;\n }\n // Node.js AbortError\n if (error instanceof Error && error.name === 'AbortError') {\n return true;\n }\n return false;\n}\n\n/**\n * Determine the network error code based on error type, code, and message.\n * Uses multiple detection strategies for cross-runtime compatibility:\n * 1. Error name (TimeoutError, AbortError) - reliable in browsers\n * 2. Node.js error code (ETIMEDOUT, ENOTFOUND, etc.) - most reliable in Node\n * 3. Message matching - fallback for edge cases\n */\nfunction getNetworkErrorCode(error: Error): 'network:connection_error' | 'network:timeout' | 'network:dns_error' {\n // Check error name first (reliable for browser and some Node errors)\n if (error.name === 'TimeoutError' || error.name === 'AbortError') {\n return 'network:timeout';\n }\n\n // Check Node.js error code (most reliable for Node.js)\n const nodeError = error as NodeJS.ErrnoException;\n if (nodeError.code) {\n switch (nodeError.code) {\n case 'ETIMEDOUT':\n case 'ESOCKETTIMEDOUT':\n return 'network:timeout';\n case 'ENOTFOUND':\n case 'EAI_AGAIN':\n return 'network:dns_error';\n case 'ECONNREFUSED':\n case 'ECONNRESET':\n case 'EPIPE':\n return 'network:connection_error';\n }\n }\n\n // Fallback to message matching for browsers and edge cases\n const message = error.message.toLowerCase();\n if (message.includes('timeout') || message.includes('timed out')) {\n return 'network:timeout';\n }\n if (message.includes('dns') || message.includes('getaddrinfo') || message.includes('enotfound')) {\n return 'network:dns_error';\n }\n\n return 'network:connection_error';\n}\n\n/**\n * Convert API errors to RiverbankApiError for consistent SDK error handling.\n * Handles:\n * - ApiEnvelopeError (from unwrapResponse)\n * - ApiRequestError (from fetch with HTTP error)\n * - Network errors (TypeError, etc. - wrapped with status 0)\n * - AbortError (let propagate raw for consumer detection)\n */\nfunction convertToTypedError(error: unknown): never {\n // Let AbortError propagate raw - it's intentional cancellation, not an error to retry\n if (isAbortError(error)) {\n throw error;\n }\n\n // Handle ApiEnvelopeError from unwrapResponse (error envelope in successful HTTP response)\n if (error instanceof ApiEnvelopeError) {\n throw new RiverbankApiError({\n code: error.code,\n message: error.message,\n requestId: error.requestId,\n timestamp: error.timestamp,\n status: error.status,\n fieldErrors: error.fieldErrors,\n });\n }\n\n // Handle ApiRequestError with envelope body (HTTP error with envelope format)\n if (error instanceof ApiRequestError && error.body && typeof error.body === 'object') {\n const body = error.body as ApiResult<unknown>;\n if (isApiError(body)) {\n // Merge envelope error with retryAfterMs from ApiRequestError\n const envelopeError = (body as ApiError).error;\n throw new RiverbankApiError({\n ...envelopeError,\n retryAfterMs: error.retryAfterMs,\n });\n }\n }\n\n // Wrap network errors (TypeError: Failed to fetch, etc.) as RiverbankApiError\n // Preserve the original error as cause for error classification (ECONNREFUSED, etc.)\n if (error instanceof TypeError || (error instanceof Error && !('status' in error))) {\n const networkError = error as Error;\n throw new RiverbankApiError({\n code: getNetworkErrorCode(networkError),\n message: networkError.message || 'Network request failed',\n requestId: `local-${Date.now()}`,\n timestamp: new Date().toISOString(),\n status: 0, // No HTTP response received\n cause: networkError, // Preserve original error for retry/circuit breaker classification\n });\n }\n\n // Re-throw original error if not in envelope format\n throw error;\n}\n\n\n/**\n * Detect API key type from prefix.\n *\n * Key prefixes:\n * - bld_live_sk_* / bld_test_sk_* → 'content' (published content)\n * - bld_preview_sk_* → 'preview' (draft content)\n */\nfunction detectKeyType(apiKey: string): ApiKeyType {\n if (apiKey.startsWith('bld_live_sk_') || apiKey.startsWith('bld_test_sk_')) {\n return 'content';\n }\n if (apiKey.startsWith('bld_preview_sk_')) {\n return 'preview';\n }\n return 'unknown';\n}\n\n/**\n * Resolve auth configuration from config, handling backward compatibility.\n * Throws if neither `auth` nor `apiKey` is provided.\n */\nfunction resolveAuth(config: RiverbankClientConfig): SdkAuthConfig {\n // Prefer new auth config\n if (config.auth) {\n return config.auth;\n }\n\n // Fall back to deprecated apiKey\n if (config.apiKey) {\n return { type: 'api-key', key: config.apiKey };\n }\n\n throw new Error(\n 'Authentication configuration is required. ' +\n 'Provide either `auth` (recommended) or `apiKey` (deprecated). ' +\n 'For public endpoints without auth, use: { auth: { type: \"none\" } }'\n );\n}\n\n/**\n * Create a Riverbank CMS client for fetching content\n *\n * @example\n * ```ts\n * const client = createRiverbankClient({\n * apiKey: 'bld_live_sk_...',\n * baseUrl: 'https://dashboard.example.com/api',\n * });\n *\n * const site = await client.getSite({ slug: 'my-site' });\n * ```\n */\nexport function createRiverbankClient(config: RiverbankClientConfig): RiverbankClient {\n if (!config.baseUrl) {\n throw new Error(\n 'baseUrl is required when creating a Riverbank client. ' +\n 'Expected format: https://dashboard.example.com/api (must include /api path)'\n );\n }\n\n // Validate baseUrl format\n if (!config.baseUrl.endsWith('/api')) {\n throw new Error(\n `baseUrl must end with '/api'. Received: ${config.baseUrl}. ` +\n 'Expected format: https://dashboard.example.com/api'\n );\n }\n\n // ============================================================================\n // Configuration\n // ============================================================================\n\n const cacheEnabled = config.cache?.enabled ?? true;\n const cacheTTL = (config.cache?.ttl ?? 300) * 1000; // Convert seconds to milliseconds\n const cacheMaxSize = config.cache?.maxSize ?? 100;\n\n // Resilience config with defaults\n const resilienceEnabled = config.resilience?.enabled ?? true;\n const staleIfError = config.resilience?.staleIfError ?? true;\n const staleTtlMs = (config.resilience?.staleTtlSec ?? 300) * 1000;\n const requestTimeoutMs = config.resilience?.requestTimeoutMs ??\n (typeof window !== 'undefined' ? DEFAULT_BROWSER_TIMEOUT_MS : DEFAULT_SERVER_TIMEOUT_MS);\n\n const retryConfig = {\n maxAttempts: config.resilience?.retry?.maxAttempts ?? DEFAULT_RETRY_CONFIG.maxAttempts,\n baseDelayMs: config.resilience?.retry?.baseDelayMs ?? DEFAULT_RETRY_CONFIG.baseDelayMs,\n maxDelayMs: config.resilience?.retry?.maxDelayMs ?? DEFAULT_RETRY_CONFIG.maxDelayMs,\n jitter: config.resilience?.retry?.jitter ?? DEFAULT_RETRY_CONFIG.jitter,\n retryOn: config.resilience?.retry?.retryOn,\n };\n\n const circuitBreakerConfig = {\n failureThreshold: config.resilience?.circuitBreaker?.failureThreshold ?? DEFAULT_CIRCUIT_BREAKER_CONFIG.failureThreshold,\n resetTimeoutMs: config.resilience?.circuitBreaker?.resetTimeoutMs ?? DEFAULT_CIRCUIT_BREAKER_CONFIG.resetTimeoutMs,\n halfOpenMaxRequests: config.resilience?.circuitBreaker?.halfOpenMaxRequests ?? DEFAULT_CIRCUIT_BREAKER_CONFIG.halfOpenMaxRequests,\n };\n\n // ============================================================================\n // Auth Resolution\n // ============================================================================\n\n const authConfig = resolveAuth(config);\n\n // Detect key type based on auth config\n let keyType: ApiKeyType;\n switch (authConfig.type) {\n case 'none':\n keyType = 'unknown';\n break;\n case 'api-key':\n keyType = detectKeyType(authConfig.key);\n break;\n case 'bearer':\n // Bearer tokens are typically for preview mode\n keyType = 'preview';\n break;\n }\n\n // Create internal API client based on auth type\n const apiClient =\n authConfig.type === 'none'\n ? createInternalAPIClient(config.baseUrl)\n : authConfig.type === 'api-key'\n ? createBearerAPIClient(authConfig.key, config.baseUrl)\n : createBearerAPIClient(authConfig.token, config.baseUrl)\n\n // ============================================================================\n // State\n // ============================================================================\n\n // Create cache instance with stale support\n const cache = new SimpleCache<unknown>({\n maxSize: cacheMaxSize,\n ttl: cacheTTL,\n staleTtl: staleTtlMs,\n });\n\n // Create circuit breaker\n const circuitBreaker = new CircuitBreaker(circuitBreakerConfig);\n\n // Create prebuild loader if configured and available (lazy-load to avoid fs in browser)\n const prebuildDir = config.resilience?.prebuildDir;\n const prebuildMod = prebuildDir ? getPrebuildModule() : null;\n const canUsePrebuild = prebuildMod?.canUsePrebuild() ?? false;\n const prebuildLoader = canUsePrebuild && prebuildMod && prebuildDir\n ? new prebuildMod.PrebuildLoader({\n prebuildDir,\n maxPrebuildAgeSec: config.resilience?.maxPrebuildAgeSec,\n })\n : null;\n\n // Last request status (for getResilienceStatus())\n let lastStatus: ResilienceStatus | null = null;\n\n // Track degraded mode for onDegradedMode callback\n let isDegraded = false;\n\n // ============================================================================\n // Status Emission\n // ============================================================================\n\n /**\n * Emit a status update and trigger callbacks\n */\n function emitStatus<T>(\n source: ResilienceSource,\n data: T | null,\n details: {\n requestId: string;\n cacheKey: string;\n isPreview: boolean;\n durationMs: number;\n error?: { code?: string; message: string };\n staleAgeSec?: number;\n prebuildAgeSec?: number;\n }\n ): T {\n const status: ResilienceStatus = {\n source,\n isPreview: details.isPreview,\n cacheKey: details.cacheKey,\n error: details.error,\n staleAgeSec: details.staleAgeSec,\n prebuildAgeSec: details.prebuildAgeSec,\n circuit: circuitBreaker.getState(),\n requestId: details.requestId,\n durationMs: details.durationMs,\n };\n\n lastStatus = status;\n\n // Fire onStatusChange callback\n config.resilience?.onStatusChange?.(status);\n\n // Fire onDegradedMode callback on transitions\n const nowDegraded = source === 'stale' || source === 'error';\n if (nowDegraded !== isDegraded) {\n isDegraded = nowDegraded;\n config.resilience?.onDegradedMode?.(nowDegraded, status);\n }\n\n return data as T;\n }\n\n // ============================================================================\n // Resilient Fetch\n // ============================================================================\n\n /**\n * Optional prebuild fallback callback type.\n * Returns the data from prebuild cache along with its age, or null if not available.\n */\n type PrebuildFallback<T> = () => { data: T; prebuildAgeSec: number } | null;\n\n /**\n * Fetch with resilience features: cache, stale fallback, retry, circuit breaker, prebuild\n */\n async function resilientFetch<T>(\n cacheKey: string,\n fetcher: (signal: AbortSignal) => Promise<ApiResult<T>>,\n options: {\n preview?: boolean;\n force?: boolean;\n signal?: AbortSignal;\n /** Optional prebuild fallback - called when live + stale both fail */\n prebuildFallback?: PrebuildFallback<T>;\n }\n ): Promise<T> {\n const requestId = generateRequestId();\n const startTime = Date.now();\n const isPreview = options.preview ?? false;\n\n // Helper to create status details\n const statusDetails = (extra: Partial<Parameters<typeof emitStatus>[2]> = {}) => ({\n requestId,\n cacheKey,\n isPreview,\n durationMs: Date.now() - startTime,\n ...extra,\n });\n\n // -------------------------------------------------------------------------\n // 1. Check fresh cache (unless force refresh)\n // -------------------------------------------------------------------------\n if (cacheEnabled && !options.force) {\n const fresh = cache.getFresh(cacheKey) as T | null;\n if (fresh !== null) {\n return emitStatus('cache', fresh, statusDetails());\n }\n }\n\n // -------------------------------------------------------------------------\n // 2. Circuit breaker check (always check, regardless of force)\n // force=true means skip cache, not skip circuit breaker protection\n // -------------------------------------------------------------------------\n if (resilienceEnabled && circuitBreaker.isOpen()) {\n // Try stale fallback (non-preview only)\n if (!isPreview && staleIfError) {\n const stale = cache.getStale(cacheKey);\n if (stale) {\n return emitStatus('stale', stale.value as T, statusDetails({\n staleAgeSec: stale.staleAgeSec,\n error: { code: 'circuit_open', message: 'Circuit breaker is open' },\n }));\n }\n }\n\n // Try prebuild fallback (non-preview only)\n if (!isPreview && options.prebuildFallback) {\n const prebuildResult = options.prebuildFallback();\n if (prebuildResult) {\n return emitStatus('prebuild', prebuildResult.data, statusDetails({\n prebuildAgeSec: prebuildResult.prebuildAgeSec,\n error: { code: 'circuit_open', message: 'Circuit breaker is open' },\n }));\n }\n }\n\n // No fallback available - circuit is protecting the system\n const circuitState = circuitBreaker.getState();\n emitStatus('error', null, statusDetails({\n error: { code: 'circuit_open', message: 'Circuit breaker is open' },\n }));\n throw new CircuitOpenError(circuitState);\n }\n\n // -------------------------------------------------------------------------\n // 3. Attempt live fetch with retries\n // -------------------------------------------------------------------------\n try {\n let data: T;\n\n if (resilienceEnabled) {\n // Track half-open requests\n if (circuitBreaker.getState().state === 'half-open') {\n circuitBreaker.incrementHalfOpenRequests();\n }\n\n // Fetch with timeout and retry\n data = await fetchWithTimeoutAndRetry(\n async (timeoutSignal) => {\n // Combine timeout signal with user-provided signal\n const combinedSignal = options.signal\n ? combineAbortSignals(timeoutSignal, options.signal)\n : timeoutSignal;\n\n try {\n const response = await fetcher(combinedSignal);\n return unwrapResponse(response);\n } catch (error) {\n convertToTypedError(error);\n }\n },\n {\n ...retryConfig,\n requestTimeoutMs,\n }\n );\n\n circuitBreaker.recordSuccess();\n } else {\n // Non-resilient path (resilience disabled)\n try {\n const response = await fetcher(options.signal ?? new AbortController().signal);\n data = unwrapResponse(response);\n } catch (error) {\n convertToTypedError(error);\n }\n }\n\n // Store in cache\n if (cacheEnabled) {\n cache.set(cacheKey, data);\n }\n\n return emitStatus('live', data, statusDetails());\n } catch (error) {\n // Record failure for circuit breaker\n if (resilienceEnabled && error instanceof Error) {\n circuitBreaker.recordFailure(error);\n }\n\n // -----------------------------------------------------------------------\n // 4. Stale fallback (published only, not preview)\n // -----------------------------------------------------------------------\n if (!isPreview && staleIfError && cacheEnabled) {\n const stale = cache.getStale(cacheKey);\n if (stale) {\n const errorInfo = error instanceof RiverbankApiError\n ? { code: error.code, message: error.message }\n : { message: (error as Error).message };\n\n return emitStatus('stale', stale.value as T, statusDetails({\n staleAgeSec: stale.staleAgeSec,\n error: errorInfo,\n }));\n }\n }\n\n // -----------------------------------------------------------------------\n // 5. Prebuild fallback (published only, not preview)\n // -----------------------------------------------------------------------\n if (!isPreview && options.prebuildFallback) {\n const prebuildResult = options.prebuildFallback();\n if (prebuildResult) {\n const errorInfo = error instanceof RiverbankApiError\n ? { code: error.code, message: error.message }\n : { message: (error as Error).message };\n\n return emitStatus('prebuild', prebuildResult.data, statusDetails({\n prebuildAgeSec: prebuildResult.prebuildAgeSec,\n error: errorInfo,\n }));\n }\n }\n\n // -----------------------------------------------------------------------\n // 6. No fallback available - propagate error\n // -----------------------------------------------------------------------\n const errorInfo = error instanceof RiverbankApiError\n ? { code: error.code, message: error.message }\n : { message: (error as Error).message };\n\n emitStatus('error', null, statusDetails({ error: errorInfo }));\n throw error;\n }\n }\n\n // ============================================================================\n // Helper: Combine AbortSignals\n // ============================================================================\n\n /**\n * Combine multiple AbortSignals into one that aborts when any input aborts\n */\n function combineAbortSignals(...signals: AbortSignal[]): AbortSignal {\n const controller = new AbortController();\n\n for (const signal of signals) {\n if (signal.aborted) {\n controller.abort(signal.reason);\n break;\n }\n signal.addEventListener('abort', () => controller.abort(signal.reason), { once: true });\n }\n\n return controller.signal;\n }\n\n // ============================================================================\n // Client Methods\n // ============================================================================\n\n async function resolveEntrySubrouteInternal(args: {\n siteId: string;\n entryId: string;\n kind: 'event-occurrence' | 'course-run';\n segment: string;\n stage?: 'preview' | 'published';\n signal?: AbortSignal;\n }): Promise<ResolveEntrySubrouteResponse> {\n const { siteId, entryId, kind, segment, stage, signal } = args;\n const cacheKey = `entry-subroute:${siteId}:${entryId}:${kind}:${segment}:${stage ?? 'published'}`;\n return resilientFetch(cacheKey, async (sig) => {\n return await apiClient({\n endpoint: 'resolveEntrySubroute',\n params: { siteId, entryId, kind, segment, ...(stage ? { stage } : {}) },\n options: { signal: sig },\n });\n }, { signal });\n }\n\n return {\n async getSite(params) {\n const { slug, domain, id, signal, force } = params;\n\n if (!slug && !domain && !id) {\n throw new Error(\n 'getSite() requires at least one identifier: slug, domain, or id. ' +\n `Received: ${JSON.stringify(params)}`\n );\n }\n\n const cacheKey = `site:${slug || domain || id}`;\n const siteId = id || slug || domain;\n\n return resilientFetch(cacheKey, async (sig) => {\n const apiParams: Record<string, string> = {};\n if (params.slug) apiParams.slug = params.slug;\n if (params.domain) apiParams.domain = params.domain;\n if (params.id) apiParams.id = params.id;\n // The dashboard `GET /api/sites` route supports `?force=true` to bypass\n // any intermediate caching (CDN/proxy) after a config push.\n if (force) apiParams.force = 'true';\n return await apiClient({\n endpoint: 'getSite',\n params: apiParams,\n // When force=true, bypass Next.js fetch caching (revalidate) as well.\n options: { signal: sig, ...(force ? { cache: 'no-store' as const } : {}) },\n });\n }, {\n signal,\n force,\n prebuildFallback: prebuildLoader && siteId\n ? () => prebuildLoader.loadSite(siteId)\n : undefined,\n });\n },\n\n async getPage(params) {\n const { siteId, path, preview = false, signal } = params;\n const cacheKey = `page:${siteId}:${path}:${preview}`;\n\n return resilientFetch(cacheKey, async (sig) => {\n return await apiClient({ endpoint: 'getContentByPath', params: { siteId }, body: { path, preview }, options: { signal: sig } });\n }, {\n preview,\n signal,\n // Prebuild fallback only for published pages (not preview)\n prebuildFallback: prebuildLoader && !preview\n ? () => prebuildLoader.loadPage(siteId, path)\n : undefined,\n });\n },\n\n async getEntries(params: GetEntriesBaseParams & { includeMeta?: boolean }) {\n const { siteId, contentType, limit, offset, order, preview = false, mode, entryIds, includeMeta, signal } = params;\n\n const entryIdsCacheKey = mode === 'manual' && entryIds?.length ? entryIds.join(',') : '';\n const cacheKey = `entries:${siteId}:${contentType}:${limit ?? ''}:${offset ?? ''}:${order ?? ''}:${preview}:${mode ?? ''}:${entryIdsCacheKey}:${includeMeta ?? ''}`;\n\n return resilientFetch(cacheKey, async (sig) => {\n let orderParam: string | undefined;\n if (order === 'newest') {\n orderParam = 'published_at.desc';\n } else if (order === 'oldest') {\n orderParam = 'published_at.asc';\n } else if (order === 'title') {\n orderParam = 'title.asc';\n }\n\n const apiParams = {\n siteId,\n type: contentType,\n ...(typeof limit === 'number' && { limit: String(limit) }),\n ...(typeof offset === 'number' && { offset: String(offset) }),\n ...(includeMeta && { meta: 'true' }),\n ...(orderParam && { order: orderParam }),\n ...(preview && { stage: 'preview' as const }),\n ...(mode === 'manual' && entryIds?.length && {\n mode: 'manual' as const,\n entryIds: JSON.stringify(entryIds),\n }),\n };\n\n return await apiClient({ endpoint: 'listPublishedEntries', params: apiParams, options: { signal: sig } });\n }, {\n preview,\n signal,\n // Prebuild fallback only for published entries (not preview, not manual mode)\n prebuildFallback: prebuildLoader && !preview\n ? () => prebuildLoader.loadEntries(siteId, params)\n : undefined,\n });\n },\n\n async getEntry(params) {\n const { siteId, contentType, slug, signal } = params;\n const cacheKey = `entry:${siteId}:${contentType}:${slug}`;\n\n return resilientFetch(cacheKey, async (sig) => {\n return await apiClient({ endpoint: 'getPublishedEntryPreview', params: { siteId, type: contentType, slug }, options: { signal: sig } });\n }, { signal });\n },\n\n async getPublicFormById(params) {\n const { formId, siteId, signal } = params;\n if (!formId || !siteId) {\n throw new Error('getPublicFormById() requires formId and siteId');\n }\n const cacheKey = `public-form:${siteId}:${formId}`;\n return resilientFetch(cacheKey, async (sig) => {\n return await apiClient({ endpoint: 'getPublicFormById', params: { formId, siteId }, options: { signal: sig } });\n }, { signal });\n },\n\n async getForms(params) {\n const { siteId, signal } = params;\n if (!siteId) {\n throw new Error('getForms() requires siteId');\n }\n const cacheKey = `forms:${siteId}`;\n return resilientFetch(cacheKey, async (sig) => {\n return await apiClient({ endpoint: 'listPublicForms', params: { siteId }, options: { signal: sig } });\n }, {\n signal,\n prebuildFallback: prebuildLoader\n ? () => {\n const result = prebuildLoader.loadForms();\n if (result) {\n return { data: { forms: result.data }, prebuildAgeSec: result.prebuildAgeSec };\n }\n return null;\n }\n : undefined,\n });\n },\n\n async getPublicBookingServices(params) {\n const { siteId, ids, signal } = params;\n if (!siteId) {\n throw new Error('getPublicBookingServices() requires siteId');\n }\n const cacheKey = `public-booking-services:${siteId}:${ids ?? ''}`;\n return resilientFetch(cacheKey, async (sig) => {\n const apiParams = {\n siteId,\n ...(ids && { ids }),\n };\n return await apiClient({ endpoint: 'getPublicBookingServices', params: apiParams, options: { signal: sig } });\n }, { signal });\n },\n\n async listPublicEvents(params) {\n const { siteId, limit, from, to, stage, entryId, seriesId, includeOccurrenceId, signal } = params;\n if (!siteId) {\n throw new Error('listPublicEvents() requires siteId');\n }\n const cacheKey = `public-events:${siteId}:${limit ?? ''}:${from ?? ''}:${to ?? ''}:${stage ?? ''}:${entryId ?? ''}:${seriesId ?? ''}:${includeOccurrenceId ?? ''}`;\n return resilientFetch(cacheKey, async (sig) => {\n const apiParams = {\n siteId,\n ...(typeof limit === 'number' && { limit: String(limit) }),\n ...(from && { from }),\n ...(to && { to }),\n ...(stage && { stage }),\n ...(entryId && { entryId }),\n ...(seriesId && { seriesId }),\n ...(includeOccurrenceId && { includeOccurrenceId }),\n };\n return await apiClient({ endpoint: 'listPublicEvents', params: apiParams, options: { signal: sig } });\n }, { signal });\n },\n\n async listPublicCourses(params) {\n const { siteId, limit, stage, signal } = params;\n if (!siteId) {\n throw new Error('listPublicCourses() requires siteId');\n }\n const cacheKey = `public-courses:${siteId}:${limit ?? ''}:${stage ?? ''}`;\n return resilientFetch(cacheKey, async (sig) => {\n const apiParams = {\n siteId,\n ...(typeof limit === 'number' && { limit: String(limit) }),\n ...(stage && { stage }),\n };\n return await apiClient({ endpoint: 'listPublicCourses', params: apiParams, options: { signal: sig } });\n }, { signal });\n },\n\n async listPublicPasses(params) {\n const { siteId, categories, signal } = params;\n if (!siteId) {\n throw new Error('listPublicPasses() requires siteId');\n }\n const cacheKey = `public-passes:${siteId}:${categories ?? ''}`;\n return resilientFetch(cacheKey, async (sig) => {\n const apiParams = {\n siteId,\n ...(categories && { categories }),\n };\n return await apiClient({ endpoint: 'listPublicPasses', params: apiParams, options: { signal: sig } });\n }, { signal });\n },\n\n async listPublicMemberships(params) {\n const { siteId, categories, signal } = params;\n if (!siteId) {\n throw new Error('listPublicMemberships() requires siteId');\n }\n const cacheKey = `public-memberships:${siteId}:${categories ?? ''}`;\n return resilientFetch(cacheKey, async (sig) => {\n const apiParams = {\n siteId,\n ...(categories && { categories }),\n };\n return await apiClient({ endpoint: 'listPublicMemberships', params: apiParams, options: { signal: sig } });\n }, { signal });\n },\n\n async resolveEntrySubroute(params) {\n const { siteId, entryId, kind, segment, stage, signal } = params;\n if (!siteId || !entryId || !kind || !segment) {\n throw new Error('resolveEntrySubroute() requires siteId, entryId, kind, and segment');\n }\n return resolveEntrySubrouteInternal({ siteId, entryId, kind, segment, stage, signal });\n },\n\n async resolveEventOccurrence(params) {\n const { siteId, entryId, segment, signal } = params;\n if (!siteId || !entryId || !segment) {\n throw new Error('resolveEventOccurrence() requires siteId, entryId, and segment');\n }\n const result = await resolveEntrySubrouteInternal({\n siteId,\n entryId,\n kind: 'event-occurrence',\n segment,\n signal,\n });\n return {\n occurrence:\n result.subroute?.kind === 'event-occurrence'\n ? result.subroute.occurrence\n : null,\n };\n },\n\n async checkRedirect(params) {\n const { siteId, path, signal } = params;\n if (!siteId || !path) {\n throw new Error('checkRedirect() requires siteId and path');\n }\n const cacheKey = `redirect:${siteId}:${path}`;\n return resilientFetch(cacheKey, async (sig) => {\n return await apiClient({\n endpoint: 'checkRedirect',\n params: { site: siteId, path },\n options: { signal: sig },\n });\n }, { signal });\n },\n\n async getAllPublishedRoutes(params) {\n const { siteId, signal } = params;\n if (!siteId) {\n throw new Error('getAllPublishedRoutes() requires siteId');\n }\n const cacheKey = `routable-content:${siteId}:published`;\n return resilientFetch(cacheKey, async (sig) => {\n return await apiClient({\n endpoint: 'getPublicRoutableContent',\n params: { siteId, publishedOnly: 'true' },\n options: { signal: sig },\n });\n }, { signal });\n },\n\n clearCache() {\n cache.clear();\n },\n\n getKeyType(): ApiKeyType {\n return keyType;\n },\n\n getLastEmittedStatus(): ResilienceStatus | null {\n return lastStatus;\n },\n\n getCircuitState(): CircuitState {\n return circuitBreaker.getState();\n },\n\n getBaseUrl(): string {\n return config.baseUrl;\n },\n } as RiverbankClient;\n}\n\n// Re-export types\nexport type { RiverbankClient, RiverbankClientConfig, ResilienceConfig, ResilienceStatus, ApiKeyType, SdkAuthConfig } from './types';\n\n// Re-export error class\nexport { RiverbankApiError } from './error';\n\n// Re-export CircuitOpenError for consumer detection\nexport { CircuitOpenError } from './resilience';\n"]}
|