@palbase/core 0.7.0 → 1.0.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/index.cjs +26 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +26 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -71,14 +71,19 @@ var PalbaseError = class extends Error {
|
|
|
71
71
|
};
|
|
72
72
|
|
|
73
73
|
// src/http.ts
|
|
74
|
-
var
|
|
74
|
+
var PALBASE_DEFAULT_HOST = "api.palbase.studio";
|
|
75
|
+
var REF_LEN = 8;
|
|
76
|
+
var BASE62_RE = /^[0-9A-Za-z]+$/;
|
|
75
77
|
function parseProjectRef(apiKey) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return null;
|
|
78
|
+
if (apiKey.length < 13) return null;
|
|
79
|
+
if (!apiKey.startsWith("pb_")) return null;
|
|
80
|
+
if (apiKey[11] !== "_") return null;
|
|
81
|
+
const ref = apiKey.slice(3, 11);
|
|
82
|
+
if (ref.length !== REF_LEN) return null;
|
|
83
|
+
if (!BASE62_RE.test(ref)) return null;
|
|
84
|
+
const scope = apiKey[12];
|
|
85
|
+
if (scope !== "c" && scope !== "s") return null;
|
|
86
|
+
return ref;
|
|
82
87
|
}
|
|
83
88
|
var MAX_RETRIES = 3;
|
|
84
89
|
var INITIAL_BACKOFF_MS = 200;
|
|
@@ -141,30 +146,31 @@ var HttpClient = class _HttpClient {
|
|
|
141
146
|
if (this.options?.url) {
|
|
142
147
|
return this.options.url;
|
|
143
148
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
if (this.apiKey && parseProjectRef(this.apiKey) === null) {
|
|
150
|
+
throw new PalbaseError(
|
|
151
|
+
"invalid_api_key",
|
|
152
|
+
'Invalid API key format. Expected pb_{ref}_{scope}{random}. For dev/staging pass `url: "https://api.dev.palbase.studio"` via options.',
|
|
153
|
+
0
|
|
154
|
+
);
|
|
147
155
|
}
|
|
148
|
-
|
|
149
|
-
"invalid_api_key",
|
|
150
|
-
"Invalid API key format. Expected: pb_{ref}_{random}. Provide an explicit url option for custom endpoints.",
|
|
151
|
-
0
|
|
152
|
-
);
|
|
156
|
+
return `https://${PALBASE_DEFAULT_HOST}`;
|
|
153
157
|
}
|
|
154
158
|
buildHeaders(options) {
|
|
155
159
|
const headers = {
|
|
156
160
|
"Content-Type": "application/json"
|
|
157
161
|
};
|
|
158
|
-
|
|
159
|
-
|
|
162
|
+
const effectiveKey = this.options?.serviceRoleKey ?? this.apiKey;
|
|
163
|
+
if (effectiveKey) {
|
|
164
|
+
headers["apikey"] = effectiveKey;
|
|
165
|
+
const ref = parseProjectRef(effectiveKey);
|
|
166
|
+
if (ref) {
|
|
167
|
+
headers["X-Project-Ref"] = ref;
|
|
168
|
+
}
|
|
160
169
|
}
|
|
161
170
|
const token = this.tokenManager?.getAccessToken();
|
|
162
171
|
if (token) {
|
|
163
172
|
headers["Authorization"] = `Bearer ${token}`;
|
|
164
173
|
}
|
|
165
|
-
if (this.options?.serviceRoleKey) {
|
|
166
|
-
headers["Authorization"] = `Bearer ${this.options.serviceRoleKey}`;
|
|
167
|
-
}
|
|
168
174
|
if (this.adminToken) {
|
|
169
175
|
headers["Authorization"] = `Bearer ${this.adminToken}`;
|
|
170
176
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/config.ts","../src/errors.ts","../src/http.ts","../src/platform.ts","../src/token.ts"],"sourcesContent":["export { ConfigFetcher } from './config.js';\nexport { PalbaseError } from './errors.js';\nexport { HttpClient } from './http.js';\nexport type { RequestInterceptor } from './http.js';\nexport type { Platform } from './platform.js';\nexport { detectPlatform } from './platform.js';\nexport { TokenManager } from './token.js';\nexport type {\n AuthStateCallback,\n AuthStateEvent,\n HttpClientOptions,\n PalbaseConfig,\n PalbaseResponse,\n ProjectConfig,\n RequestOptions,\n Session,\n Unsubscribe,\n} from './types.js';\n","import type { HttpClient } from './http.js';\nimport type { ProjectConfig } from './types.js';\n\nconst CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes\n\nexport class ConfigFetcher {\n protected readonly httpClient: HttpClient;\n private cachedConfig: ProjectConfig | null = null;\n private cacheTimestamp = 0;\n\n constructor(httpClient: HttpClient) {\n this.httpClient = httpClient;\n }\n\n async getConfig(): Promise<ProjectConfig | null> {\n const now = Date.now();\n\n if (this.cachedConfig && now - this.cacheTimestamp < CACHE_TTL_MS) {\n return this.cachedConfig;\n }\n\n try {\n const response = await this.httpClient.request<ProjectConfig>('GET', '/v1/config');\n\n if (response.error || !response.data) {\n return null;\n }\n\n this.cachedConfig = response.data;\n this.cacheTimestamp = now;\n\n return this.cachedConfig;\n } catch {\n return null;\n }\n }\n}\n","export class PalbaseError extends Error {\n readonly code: string;\n readonly status: number;\n readonly details?: unknown;\n\n constructor(code: string, message: string, status: number, details?: unknown) {\n super(message);\n this.name = 'PalbaseError';\n this.code = code;\n this.status = status;\n this.details = details;\n }\n}\n","import { PalbaseError } from './errors.js';\nimport type { TokenManager } from './token.js';\nimport type { HttpClientOptions, PalbaseResponse, RequestOptions } from './types.js';\n\nconst PALBASE_DOMAIN = 'palbase.studio';\n\n/**\n * Parse project ref from an API key.\n * Format: pb_{ref}_{random}\n * Example: pb_abc12345_xxxxxxxxxxxxxxxxxxxxxxxx\n * Returns the ref segment or null if the key doesn't match.\n */\nfunction parseProjectRef(apiKey: string): string | null {\n const parts = apiKey.split('_');\n // Format: [\"pb\", ref, random]\n const ref = parts[1];\n if (parts.length >= 3 && parts[0] === 'pb' && ref) {\n return ref;\n }\n return null;\n}\nconst MAX_RETRIES = 3;\nconst INITIAL_BACKOFF_MS = 200;\n\n/**\n * Request interceptor. Runs before every HTTP request.\n * Can modify headers, body, or reject the request.\n */\nexport interface RequestInterceptor {\n (request: { headers: Record<string, string>; method: string; path: string }): void | Promise<void>;\n}\n\nexport class HttpClient {\n protected readonly apiKey: string;\n protected readonly options?: HttpClientOptions;\n\n tokenManager: TokenManager | null = null;\n\n /**\n * Admin JWT used for platform admin endpoints (/admin/*).\n * When set, takes precedence over serviceRoleKey and tokenManager access\n * token in the Authorization header.\n */\n adminToken: string | null = null;\n\n private readonly interceptors: RequestInterceptor[] = [];\n\n constructor(apiKey: string, options?: HttpClientOptions) {\n this.apiKey = apiKey;\n this.options = options;\n }\n\n /** Set (or clear) the admin JWT used on admin endpoints. */\n setAdminToken(token: string | null): void {\n this.adminToken = token;\n }\n\n /**\n * Create a scoped HttpClient that adds the given extra headers to every\n * request. The returned client shares the admin token and token manager\n * with the parent at runtime — later changes on the parent propagate to\n * the scope and vice versa.\n *\n * Typical use: tagging admin calls with `x-palbase-project: <ref>` so the\n * gateway can route them to the correct project's data plane.\n */\n withHeaders(extra: Record<string, string>): HttpClient {\n const mergedHeaders = { ...(this.options?.headers ?? {}), ...extra };\n const parent = this;\n const scoped: HttpClient = new HttpClient(this.apiKey, {\n ...this.options,\n headers: mergedHeaders,\n });\n scoped.tokenManager = this.tokenManager;\n // Delegate adminToken reads + writes to the parent so the scope always\n // sees the latest token, and setAdminToken on the scope affects the parent.\n Object.defineProperty(scoped, 'adminToken', {\n get: () => parent.adminToken,\n set: (v: string | null) => {\n parent.adminToken = v;\n },\n configurable: true,\n });\n return scoped;\n }\n\n /** Add a request interceptor. Runs before every request. */\n addInterceptor(interceptor: RequestInterceptor): void {\n this.interceptors.push(interceptor);\n }\n\n async request<T>(\n method: string,\n path: string,\n options?: RequestOptions,\n ): Promise<PalbaseResponse<T>> {\n // If token is expired and refresh is available, refresh before making the request\n if (this.tokenManager?.isExpired() && this.tokenManager.getRefreshToken() && this.tokenManager.refreshFunction) {\n await this.tokenManager.refreshSession();\n }\n\n return this.executeWithRetry<T>(method, path, options, 0);\n }\n\n private getBaseUrl(): string {\n // Explicit URL always wins (local dev, testing)\n if (this.options?.url) {\n return this.options.url;\n }\n\n // Resolve from API key (format: pb_{ref}_{random})\n const ref = parseProjectRef(this.apiKey);\n if (ref) {\n return `https://${ref}.${PALBASE_DOMAIN}`;\n }\n\n throw new PalbaseError(\n 'invalid_api_key',\n 'Invalid API key format. Expected: pb_{ref}_{random}. Provide an explicit url option for custom endpoints.',\n 0,\n );\n }\n\n private buildHeaders(options?: RequestOptions): Record<string, string> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n\n // apikey header only when an API key is configured (platform admin flows\n // may omit it — e.g. setup/login before any project exists).\n if (this.apiKey) {\n headers['apikey'] = this.apiKey;\n }\n\n // Add token if available\n const token = this.tokenManager?.getAccessToken();\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n\n // serviceRoleKey overrides token-based Authorization\n if (this.options?.serviceRoleKey) {\n headers['Authorization'] = `Bearer ${this.options.serviceRoleKey}`;\n }\n\n // adminToken (platform admin JWT) takes precedence over everything else\n if (this.adminToken) {\n headers['Authorization'] = `Bearer ${this.adminToken}`;\n }\n\n // Merge global custom headers\n if (this.options?.headers) {\n Object.assign(headers, this.options.headers);\n }\n\n // Merge per-request headers\n if (options?.headers) {\n Object.assign(headers, options.headers);\n }\n\n return headers;\n }\n\n private async executeWithRetry<T>(\n method: string,\n path: string,\n options: RequestOptions | undefined,\n attempt: number,\n ): Promise<PalbaseResponse<T>> {\n const url = `${this.getBaseUrl()}${path}`;\n const headers = this.buildHeaders(options);\n\n // Run interceptors\n for (const interceptor of this.interceptors) {\n await interceptor({ headers, method, path });\n }\n\n const fetchOptions: RequestInit = {\n method,\n headers,\n signal: options?.signal,\n };\n\n if (options?.body !== undefined) {\n fetchOptions.body = JSON.stringify(options.body);\n }\n\n let response: Response;\n try {\n response = await fetch(url, fetchOptions);\n } catch (error) {\n // Network error — retry with backoff\n if (attempt < MAX_RETRIES - 1) {\n const backoff = INITIAL_BACKOFF_MS * 2 ** attempt;\n await this.delay(backoff);\n return this.executeWithRetry<T>(method, path, options, attempt + 1);\n }\n\n // All retries exhausted — throw PalbaseError\n throw new PalbaseError(\n 'network_error',\n error instanceof Error ? error.message : 'Network request failed',\n 0,\n );\n }\n\n // Handle 429 Too Many Requests — retry with Retry-After or backoff;\n // if retries exhausted, fall through to normal error response handling below\n if (response.status === 429) {\n if (attempt < MAX_RETRIES - 1) {\n const retryAfter = response.headers.get('Retry-After');\n const parsed = retryAfter ? Number.parseInt(retryAfter, 10) : Number.NaN;\n const delayMs = Number.isNaN(parsed) ? INITIAL_BACKOFF_MS * 2 ** attempt : parsed * 1000;\n await this.delay(delayMs);\n return this.executeWithRetry<T>(method, path, options, attempt + 1);\n }\n }\n\n // Parse response body\n let data: T | null = null;\n let errorBody: { error?: string; error_description?: string; status?: number } | undefined;\n\n // HEAD responses have no body by spec — skip parsing.\n const contentType = response.headers.get('Content-Type');\n if (method !== 'HEAD' && contentType?.includes('json')) {\n const body = await response.json() as Record<string, unknown>;\n if (response.ok) {\n data = body as T;\n } else {\n errorBody = body as typeof errorBody;\n }\n }\n\n if (!response.ok) {\n return {\n data: null,\n error: new PalbaseError(\n errorBody?.error ?? 'unknown_error',\n errorBody?.error_description ?? response.statusText,\n response.status,\n errorBody,\n ),\n status: response.status,\n };\n }\n\n // Parse PostgREST Content-Range for count queries (e.g. \"0-9/42\" or \"*/42\").\n const contentRange = response.headers.get('Content-Range');\n let count: number | undefined;\n if (contentRange) {\n const slash = contentRange.lastIndexOf('/');\n if (slash >= 0) {\n const totalPart = contentRange.slice(slash + 1);\n if (totalPart !== '*') {\n const parsed = Number.parseInt(totalPart, 10);\n if (!Number.isNaN(parsed)) {\n count = parsed;\n }\n }\n }\n }\n\n return {\n data,\n error: null,\n status: response.status,\n ...(count !== undefined ? { count } : {}),\n };\n }\n\n private delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n","export type Platform = 'browser' | 'node' | 'react-native' | 'deno' | 'bun';\n\ndeclare const Deno: unknown;\ndeclare const process: { versions: Record<string, string> } | undefined;\n\nexport function detectPlatform(): Platform {\n if (typeof Deno !== 'undefined') {\n return 'deno';\n }\n\n if (process?.versions) {\n if ('bun' in process.versions) {\n return 'bun';\n }\n if ('node' in process.versions) {\n return 'node';\n }\n }\n\n if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {\n return 'react-native';\n }\n\n return 'browser';\n}\n","import type { AuthStateCallback, Session, Unsubscribe } from './types.js';\n\nexport class TokenManager {\n private session: Session | null = null;\n private listeners: Set<AuthStateCallback> = new Set();\n private refreshPromise: Promise<void> | null = null;\n\n refreshFunction: ((refreshToken: string) => Promise<Session>) | null = null;\n\n setSession(session: Session): void {\n this.session = session;\n this.notify('SESSION_SET', session);\n }\n\n getAccessToken(): string | null {\n return this.session?.accessToken ?? null;\n }\n\n getRefreshToken(): string | null {\n return this.session?.refreshToken ?? null;\n }\n\n clearSession(): void {\n this.session = null;\n this.notify('SESSION_CLEARED', null);\n }\n\n isExpired(): boolean {\n if (!this.session) return true;\n return Date.now() >= this.session.expiresAt;\n }\n\n async refreshSession(): Promise<void> {\n if (!this.session?.refreshToken || !this.refreshFunction) {\n return;\n }\n\n // Collapse concurrent refresh calls into a single request\n if (this.refreshPromise) {\n return this.refreshPromise;\n }\n\n this.refreshPromise = this.executeRefresh(this.session.refreshToken);\n\n try {\n await this.refreshPromise;\n } finally {\n this.refreshPromise = null;\n }\n }\n\n onAuthStateChange(callback: AuthStateCallback): Unsubscribe {\n this.listeners.add(callback);\n return () => {\n this.listeners.delete(callback);\n };\n }\n\n private async executeRefresh(refreshToken: string): Promise<void> {\n if (!this.refreshFunction) return;\n const newSession = await this.refreshFunction(refreshToken);\n this.setSession(newSession);\n }\n\n private notify(event: 'SESSION_SET' | 'SESSION_CLEARED', session: Session | null): void {\n for (const listener of this.listeners) {\n listener(event, session);\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/config.ts","../src/errors.ts","../src/http.ts","../src/platform.ts","../src/token.ts"],"sourcesContent":["export { ConfigFetcher } from './config.js';\nexport { PalbaseError } from './errors.js';\nexport { HttpClient } from './http.js';\nexport type { RequestInterceptor } from './http.js';\nexport type { Platform } from './platform.js';\nexport { detectPlatform } from './platform.js';\nexport { TokenManager } from './token.js';\nexport type {\n AuthStateCallback,\n AuthStateEvent,\n HttpClientOptions,\n PalbaseConfig,\n PalbaseResponse,\n ProjectConfig,\n RequestOptions,\n Session,\n Unsubscribe,\n} from './types.js';\n","import type { HttpClient } from './http.js';\nimport type { ProjectConfig } from './types.js';\n\nconst CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes\n\nexport class ConfigFetcher {\n protected readonly httpClient: HttpClient;\n private cachedConfig: ProjectConfig | null = null;\n private cacheTimestamp = 0;\n\n constructor(httpClient: HttpClient) {\n this.httpClient = httpClient;\n }\n\n async getConfig(): Promise<ProjectConfig | null> {\n const now = Date.now();\n\n if (this.cachedConfig && now - this.cacheTimestamp < CACHE_TTL_MS) {\n return this.cachedConfig;\n }\n\n try {\n const response = await this.httpClient.request<ProjectConfig>('GET', '/v1/config');\n\n if (response.error || !response.data) {\n return null;\n }\n\n this.cachedConfig = response.data;\n this.cacheTimestamp = now;\n\n return this.cachedConfig;\n } catch {\n return null;\n }\n }\n}\n","export class PalbaseError extends Error {\n readonly code: string;\n readonly status: number;\n readonly details?: unknown;\n\n constructor(code: string, message: string, status: number, details?: unknown) {\n super(message);\n this.name = 'PalbaseError';\n this.code = code;\n this.status = status;\n this.details = details;\n }\n}\n","import { PalbaseError } from './errors.js';\nimport type { TokenManager } from './token.js';\nimport type { HttpClientOptions, PalbaseResponse, RequestOptions } from './types.js';\n\n/**\n * Default production host. Dev / staging / local callers override via\n * `options.url`. Apex-style routing (all traffic lands on one host and\n * Kong stamps `X-Project-Ref` from the key-auth consumer) is the only\n * supported production path — subdomain-per-tenant routing was\n * considered but retired in favour of a single gateway.\n */\nconst PALBASE_DEFAULT_HOST = 'api.palbase.studio';\n\n/**\n * Parse project ref from a palbase API key.\n *\n * Canonical shape: `pb_{ref}_{scope}{random}` where `ref` is exactly\n * 8 base62 chars and `scope` is `c` (client) or `s` (server). See\n * docs/MODULE_HEADER_CONTRACT.md §\"API key format\" (palbase repo) for\n * the full spec.\n *\n * Returns the 8-char ref on match; `null` otherwise — callers surface\n * that as an `invalid_api_key` error.\n */\nconst REF_LEN = 8;\nconst BASE62_RE = /^[0-9A-Za-z]+$/;\n\nfunction parseProjectRef(apiKey: string): string | null {\n // pb_ (3) + ref (8) + _ (1) + scope (1) = 13 min chars\n if (apiKey.length < 13) return null;\n if (!apiKey.startsWith('pb_')) return null;\n if (apiKey[11] !== '_') return null;\n const ref = apiKey.slice(3, 11);\n if (ref.length !== REF_LEN) return null;\n if (!BASE62_RE.test(ref)) return null;\n const scope = apiKey[12];\n if (scope !== 'c' && scope !== 's') return null;\n return ref;\n}\nconst MAX_RETRIES = 3;\nconst INITIAL_BACKOFF_MS = 200;\n\n/**\n * Request interceptor. Runs before every HTTP request.\n * Can modify headers, body, or reject the request.\n */\nexport interface RequestInterceptor {\n (request: { headers: Record<string, string>; method: string; path: string }): void | Promise<void>;\n}\n\nexport class HttpClient {\n protected readonly apiKey: string;\n protected readonly options?: HttpClientOptions;\n\n tokenManager: TokenManager | null = null;\n\n /**\n * Admin JWT used for platform admin endpoints (/admin/*).\n * When set, takes precedence over serviceRoleKey and tokenManager access\n * token in the Authorization header.\n */\n adminToken: string | null = null;\n\n private readonly interceptors: RequestInterceptor[] = [];\n\n constructor(apiKey: string, options?: HttpClientOptions) {\n this.apiKey = apiKey;\n this.options = options;\n }\n\n /** Set (or clear) the admin JWT used on admin endpoints. */\n setAdminToken(token: string | null): void {\n this.adminToken = token;\n }\n\n /**\n * Create a scoped HttpClient that adds the given extra headers to every\n * request. The returned client shares the admin token and token manager\n * with the parent at runtime — later changes on the parent propagate to\n * the scope and vice versa.\n *\n * Typical use: tagging admin calls with `x-palbase-project: <ref>` so the\n * gateway can route them to the correct project's data plane.\n */\n withHeaders(extra: Record<string, string>): HttpClient {\n const mergedHeaders = { ...(this.options?.headers ?? {}), ...extra };\n const parent = this;\n const scoped: HttpClient = new HttpClient(this.apiKey, {\n ...this.options,\n headers: mergedHeaders,\n });\n scoped.tokenManager = this.tokenManager;\n // Delegate adminToken reads + writes to the parent so the scope always\n // sees the latest token, and setAdminToken on the scope affects the parent.\n Object.defineProperty(scoped, 'adminToken', {\n get: () => parent.adminToken,\n set: (v: string | null) => {\n parent.adminToken = v;\n },\n configurable: true,\n });\n return scoped;\n }\n\n /** Add a request interceptor. Runs before every request. */\n addInterceptor(interceptor: RequestInterceptor): void {\n this.interceptors.push(interceptor);\n }\n\n async request<T>(\n method: string,\n path: string,\n options?: RequestOptions,\n ): Promise<PalbaseResponse<T>> {\n // If token is expired and refresh is available, refresh before making the request\n if (this.tokenManager?.isExpired() && this.tokenManager.getRefreshToken() && this.tokenManager.refreshFunction) {\n await this.tokenManager.refreshSession();\n }\n\n return this.executeWithRetry<T>(method, path, options, 0);\n }\n\n private getBaseUrl(): string {\n // Explicit URL always wins (local dev, staging, test rigs).\n if (this.options?.url) {\n return this.options.url;\n }\n\n // Validate the key shape up front so apex-routed callers still\n // fail loud on a malformed key instead of hitting the gateway\n // with bad credentials.\n if (this.apiKey && parseProjectRef(this.apiKey) === null) {\n throw new PalbaseError(\n 'invalid_api_key',\n 'Invalid API key format. Expected pb_{ref}_{scope}{random}. For dev/staging pass `url: \"https://api.dev.palbase.studio\"` via options.',\n 0,\n );\n }\n\n return `https://${PALBASE_DEFAULT_HOST}`;\n }\n\n private buildHeaders(options?: RequestOptions): Record<string, string> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n\n // The effective key is either the explicit serviceRoleKey\n // (server SDK's `serviceRole` option) or the constructor-time\n // apiKey. Palbase keys live in the `apikey` header — never in\n // `Authorization` — because Kong's key-auth resolves them on\n // that header and the gateway's pre-function plugin reads the\n // scope tag at byte offset 12 to decide service_role vs anon.\n // Putting a server key on `Authorization: Bearer …` would land\n // it on PostgREST as a JWT (PostgREST then 500s with PGRST300)\n // and Kong wouldn't see it at all on the apikey path.\n const effectiveKey = this.options?.serviceRoleKey ?? this.apiKey;\n if (effectiveKey) {\n headers['apikey'] = effectiveKey;\n // Defense-in-depth X-Project-Ref. Kong's pre-function plugin\n // always overrides this with the consumer's `custom_id` (see\n // docs/MODULE_HEADER_CONTRACT.md §\"pre-function\") so any\n // client-sent value is discarded before it reaches the\n // upstream module — but emitting one here keeps non-Kong\n // test rigs and apex-routed admin paths correct.\n const ref = parseProjectRef(effectiveKey);\n if (ref) {\n headers['X-Project-Ref'] = ref;\n }\n }\n\n // User session token, if any. Kong's pre-function plugin strips\n // Authorization on /v1/* routes anyway (PostgREST has no JWT\n // secret and would crash on a Bearer it can't decode), but\n // sending it preserves the contract for /auth/* endpoints that\n // do consume the bearer (e.g. session refresh).\n const token = this.tokenManager?.getAccessToken();\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n\n // adminToken (platform admin JWT) takes precedence — used by the\n // @palbase/admin internal flows that hit /admin/* routes; those\n // routes verify the bearer themselves and aren't subject to the\n // /v1/* Authorization-strip rule.\n if (this.adminToken) {\n headers['Authorization'] = `Bearer ${this.adminToken}`;\n }\n\n // Merge global custom headers\n if (this.options?.headers) {\n Object.assign(headers, this.options.headers);\n }\n\n // Merge per-request headers\n if (options?.headers) {\n Object.assign(headers, options.headers);\n }\n\n return headers;\n }\n\n private async executeWithRetry<T>(\n method: string,\n path: string,\n options: RequestOptions | undefined,\n attempt: number,\n ): Promise<PalbaseResponse<T>> {\n const url = `${this.getBaseUrl()}${path}`;\n const headers = this.buildHeaders(options);\n\n // Run interceptors\n for (const interceptor of this.interceptors) {\n await interceptor({ headers, method, path });\n }\n\n const fetchOptions: RequestInit = {\n method,\n headers,\n signal: options?.signal,\n };\n\n if (options?.body !== undefined) {\n fetchOptions.body = JSON.stringify(options.body);\n }\n\n let response: Response;\n try {\n response = await fetch(url, fetchOptions);\n } catch (error) {\n // Network error — retry with backoff\n if (attempt < MAX_RETRIES - 1) {\n const backoff = INITIAL_BACKOFF_MS * 2 ** attempt;\n await this.delay(backoff);\n return this.executeWithRetry<T>(method, path, options, attempt + 1);\n }\n\n // All retries exhausted — throw PalbaseError\n throw new PalbaseError(\n 'network_error',\n error instanceof Error ? error.message : 'Network request failed',\n 0,\n );\n }\n\n // Handle 429 Too Many Requests — retry with Retry-After or backoff;\n // if retries exhausted, fall through to normal error response handling below\n if (response.status === 429) {\n if (attempt < MAX_RETRIES - 1) {\n const retryAfter = response.headers.get('Retry-After');\n const parsed = retryAfter ? Number.parseInt(retryAfter, 10) : Number.NaN;\n const delayMs = Number.isNaN(parsed) ? INITIAL_BACKOFF_MS * 2 ** attempt : parsed * 1000;\n await this.delay(delayMs);\n return this.executeWithRetry<T>(method, path, options, attempt + 1);\n }\n }\n\n // Parse response body\n let data: T | null = null;\n let errorBody: { error?: string; error_description?: string; status?: number } | undefined;\n\n // HEAD responses have no body by spec — skip parsing.\n const contentType = response.headers.get('Content-Type');\n if (method !== 'HEAD' && contentType?.includes('json')) {\n const body = await response.json() as Record<string, unknown>;\n if (response.ok) {\n data = body as T;\n } else {\n errorBody = body as typeof errorBody;\n }\n }\n\n if (!response.ok) {\n return {\n data: null,\n error: new PalbaseError(\n errorBody?.error ?? 'unknown_error',\n errorBody?.error_description ?? response.statusText,\n response.status,\n errorBody,\n ),\n status: response.status,\n };\n }\n\n // Parse PostgREST Content-Range for count queries (e.g. \"0-9/42\" or \"*/42\").\n const contentRange = response.headers.get('Content-Range');\n let count: number | undefined;\n if (contentRange) {\n const slash = contentRange.lastIndexOf('/');\n if (slash >= 0) {\n const totalPart = contentRange.slice(slash + 1);\n if (totalPart !== '*') {\n const parsed = Number.parseInt(totalPart, 10);\n if (!Number.isNaN(parsed)) {\n count = parsed;\n }\n }\n }\n }\n\n return {\n data,\n error: null,\n status: response.status,\n ...(count !== undefined ? { count } : {}),\n };\n }\n\n private delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n","export type Platform = 'browser' | 'node' | 'react-native' | 'deno' | 'bun';\n\ndeclare const Deno: unknown;\ndeclare const process: { versions: Record<string, string> } | undefined;\n\nexport function detectPlatform(): Platform {\n if (typeof Deno !== 'undefined') {\n return 'deno';\n }\n\n if (process?.versions) {\n if ('bun' in process.versions) {\n return 'bun';\n }\n if ('node' in process.versions) {\n return 'node';\n }\n }\n\n if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {\n return 'react-native';\n }\n\n return 'browser';\n}\n","import type { AuthStateCallback, Session, Unsubscribe } from './types.js';\n\nexport class TokenManager {\n private session: Session | null = null;\n private listeners: Set<AuthStateCallback> = new Set();\n private refreshPromise: Promise<void> | null = null;\n\n refreshFunction: ((refreshToken: string) => Promise<Session>) | null = null;\n\n setSession(session: Session): void {\n this.session = session;\n this.notify('SESSION_SET', session);\n }\n\n getAccessToken(): string | null {\n return this.session?.accessToken ?? null;\n }\n\n getRefreshToken(): string | null {\n return this.session?.refreshToken ?? null;\n }\n\n clearSession(): void {\n this.session = null;\n this.notify('SESSION_CLEARED', null);\n }\n\n isExpired(): boolean {\n if (!this.session) return true;\n return Date.now() >= this.session.expiresAt;\n }\n\n async refreshSession(): Promise<void> {\n if (!this.session?.refreshToken || !this.refreshFunction) {\n return;\n }\n\n // Collapse concurrent refresh calls into a single request\n if (this.refreshPromise) {\n return this.refreshPromise;\n }\n\n this.refreshPromise = this.executeRefresh(this.session.refreshToken);\n\n try {\n await this.refreshPromise;\n } finally {\n this.refreshPromise = null;\n }\n }\n\n onAuthStateChange(callback: AuthStateCallback): Unsubscribe {\n this.listeners.add(callback);\n return () => {\n this.listeners.delete(callback);\n };\n }\n\n private async executeRefresh(refreshToken: string): Promise<void> {\n if (!this.refreshFunction) return;\n const newSession = await this.refreshFunction(refreshToken);\n this.setSession(newSession);\n }\n\n private notify(event: 'SESSION_SET' | 'SESSION_CLEARED', session: Session | null): void {\n for (const listener of this.listeners) {\n listener(event, session);\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -41,14 +41,19 @@ var PalbaseError = class extends Error {
|
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
// src/http.ts
|
|
44
|
-
var
|
|
44
|
+
var PALBASE_DEFAULT_HOST = "api.palbase.studio";
|
|
45
|
+
var REF_LEN = 8;
|
|
46
|
+
var BASE62_RE = /^[0-9A-Za-z]+$/;
|
|
45
47
|
function parseProjectRef(apiKey) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return null;
|
|
48
|
+
if (apiKey.length < 13) return null;
|
|
49
|
+
if (!apiKey.startsWith("pb_")) return null;
|
|
50
|
+
if (apiKey[11] !== "_") return null;
|
|
51
|
+
const ref = apiKey.slice(3, 11);
|
|
52
|
+
if (ref.length !== REF_LEN) return null;
|
|
53
|
+
if (!BASE62_RE.test(ref)) return null;
|
|
54
|
+
const scope = apiKey[12];
|
|
55
|
+
if (scope !== "c" && scope !== "s") return null;
|
|
56
|
+
return ref;
|
|
52
57
|
}
|
|
53
58
|
var MAX_RETRIES = 3;
|
|
54
59
|
var INITIAL_BACKOFF_MS = 200;
|
|
@@ -111,30 +116,31 @@ var HttpClient = class _HttpClient {
|
|
|
111
116
|
if (this.options?.url) {
|
|
112
117
|
return this.options.url;
|
|
113
118
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
if (this.apiKey && parseProjectRef(this.apiKey) === null) {
|
|
120
|
+
throw new PalbaseError(
|
|
121
|
+
"invalid_api_key",
|
|
122
|
+
'Invalid API key format. Expected pb_{ref}_{scope}{random}. For dev/staging pass `url: "https://api.dev.palbase.studio"` via options.',
|
|
123
|
+
0
|
|
124
|
+
);
|
|
117
125
|
}
|
|
118
|
-
|
|
119
|
-
"invalid_api_key",
|
|
120
|
-
"Invalid API key format. Expected: pb_{ref}_{random}. Provide an explicit url option for custom endpoints.",
|
|
121
|
-
0
|
|
122
|
-
);
|
|
126
|
+
return `https://${PALBASE_DEFAULT_HOST}`;
|
|
123
127
|
}
|
|
124
128
|
buildHeaders(options) {
|
|
125
129
|
const headers = {
|
|
126
130
|
"Content-Type": "application/json"
|
|
127
131
|
};
|
|
128
|
-
|
|
129
|
-
|
|
132
|
+
const effectiveKey = this.options?.serviceRoleKey ?? this.apiKey;
|
|
133
|
+
if (effectiveKey) {
|
|
134
|
+
headers["apikey"] = effectiveKey;
|
|
135
|
+
const ref = parseProjectRef(effectiveKey);
|
|
136
|
+
if (ref) {
|
|
137
|
+
headers["X-Project-Ref"] = ref;
|
|
138
|
+
}
|
|
130
139
|
}
|
|
131
140
|
const token = this.tokenManager?.getAccessToken();
|
|
132
141
|
if (token) {
|
|
133
142
|
headers["Authorization"] = `Bearer ${token}`;
|
|
134
143
|
}
|
|
135
|
-
if (this.options?.serviceRoleKey) {
|
|
136
|
-
headers["Authorization"] = `Bearer ${this.options.serviceRoleKey}`;
|
|
137
|
-
}
|
|
138
144
|
if (this.adminToken) {
|
|
139
145
|
headers["Authorization"] = `Bearer ${this.adminToken}`;
|
|
140
146
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/config.ts","../src/errors.ts","../src/http.ts","../src/platform.ts","../src/token.ts"],"sourcesContent":["import type { HttpClient } from './http.js';\nimport type { ProjectConfig } from './types.js';\n\nconst CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes\n\nexport class ConfigFetcher {\n protected readonly httpClient: HttpClient;\n private cachedConfig: ProjectConfig | null = null;\n private cacheTimestamp = 0;\n\n constructor(httpClient: HttpClient) {\n this.httpClient = httpClient;\n }\n\n async getConfig(): Promise<ProjectConfig | null> {\n const now = Date.now();\n\n if (this.cachedConfig && now - this.cacheTimestamp < CACHE_TTL_MS) {\n return this.cachedConfig;\n }\n\n try {\n const response = await this.httpClient.request<ProjectConfig>('GET', '/v1/config');\n\n if (response.error || !response.data) {\n return null;\n }\n\n this.cachedConfig = response.data;\n this.cacheTimestamp = now;\n\n return this.cachedConfig;\n } catch {\n return null;\n }\n }\n}\n","export class PalbaseError extends Error {\n readonly code: string;\n readonly status: number;\n readonly details?: unknown;\n\n constructor(code: string, message: string, status: number, details?: unknown) {\n super(message);\n this.name = 'PalbaseError';\n this.code = code;\n this.status = status;\n this.details = details;\n }\n}\n","import { PalbaseError } from './errors.js';\nimport type { TokenManager } from './token.js';\nimport type { HttpClientOptions, PalbaseResponse, RequestOptions } from './types.js';\n\nconst PALBASE_DOMAIN = 'palbase.studio';\n\n/**\n * Parse project ref from an API key.\n * Format: pb_{ref}_{random}\n * Example: pb_abc12345_xxxxxxxxxxxxxxxxxxxxxxxx\n * Returns the ref segment or null if the key doesn't match.\n */\nfunction parseProjectRef(apiKey: string): string | null {\n const parts = apiKey.split('_');\n // Format: [\"pb\", ref, random]\n const ref = parts[1];\n if (parts.length >= 3 && parts[0] === 'pb' && ref) {\n return ref;\n }\n return null;\n}\nconst MAX_RETRIES = 3;\nconst INITIAL_BACKOFF_MS = 200;\n\n/**\n * Request interceptor. Runs before every HTTP request.\n * Can modify headers, body, or reject the request.\n */\nexport interface RequestInterceptor {\n (request: { headers: Record<string, string>; method: string; path: string }): void | Promise<void>;\n}\n\nexport class HttpClient {\n protected readonly apiKey: string;\n protected readonly options?: HttpClientOptions;\n\n tokenManager: TokenManager | null = null;\n\n /**\n * Admin JWT used for platform admin endpoints (/admin/*).\n * When set, takes precedence over serviceRoleKey and tokenManager access\n * token in the Authorization header.\n */\n adminToken: string | null = null;\n\n private readonly interceptors: RequestInterceptor[] = [];\n\n constructor(apiKey: string, options?: HttpClientOptions) {\n this.apiKey = apiKey;\n this.options = options;\n }\n\n /** Set (or clear) the admin JWT used on admin endpoints. */\n setAdminToken(token: string | null): void {\n this.adminToken = token;\n }\n\n /**\n * Create a scoped HttpClient that adds the given extra headers to every\n * request. The returned client shares the admin token and token manager\n * with the parent at runtime — later changes on the parent propagate to\n * the scope and vice versa.\n *\n * Typical use: tagging admin calls with `x-palbase-project: <ref>` so the\n * gateway can route them to the correct project's data plane.\n */\n withHeaders(extra: Record<string, string>): HttpClient {\n const mergedHeaders = { ...(this.options?.headers ?? {}), ...extra };\n const parent = this;\n const scoped: HttpClient = new HttpClient(this.apiKey, {\n ...this.options,\n headers: mergedHeaders,\n });\n scoped.tokenManager = this.tokenManager;\n // Delegate adminToken reads + writes to the parent so the scope always\n // sees the latest token, and setAdminToken on the scope affects the parent.\n Object.defineProperty(scoped, 'adminToken', {\n get: () => parent.adminToken,\n set: (v: string | null) => {\n parent.adminToken = v;\n },\n configurable: true,\n });\n return scoped;\n }\n\n /** Add a request interceptor. Runs before every request. */\n addInterceptor(interceptor: RequestInterceptor): void {\n this.interceptors.push(interceptor);\n }\n\n async request<T>(\n method: string,\n path: string,\n options?: RequestOptions,\n ): Promise<PalbaseResponse<T>> {\n // If token is expired and refresh is available, refresh before making the request\n if (this.tokenManager?.isExpired() && this.tokenManager.getRefreshToken() && this.tokenManager.refreshFunction) {\n await this.tokenManager.refreshSession();\n }\n\n return this.executeWithRetry<T>(method, path, options, 0);\n }\n\n private getBaseUrl(): string {\n // Explicit URL always wins (local dev, testing)\n if (this.options?.url) {\n return this.options.url;\n }\n\n // Resolve from API key (format: pb_{ref}_{random})\n const ref = parseProjectRef(this.apiKey);\n if (ref) {\n return `https://${ref}.${PALBASE_DOMAIN}`;\n }\n\n throw new PalbaseError(\n 'invalid_api_key',\n 'Invalid API key format. Expected: pb_{ref}_{random}. Provide an explicit url option for custom endpoints.',\n 0,\n );\n }\n\n private buildHeaders(options?: RequestOptions): Record<string, string> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n\n // apikey header only when an API key is configured (platform admin flows\n // may omit it — e.g. setup/login before any project exists).\n if (this.apiKey) {\n headers['apikey'] = this.apiKey;\n }\n\n // Add token if available\n const token = this.tokenManager?.getAccessToken();\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n\n // serviceRoleKey overrides token-based Authorization\n if (this.options?.serviceRoleKey) {\n headers['Authorization'] = `Bearer ${this.options.serviceRoleKey}`;\n }\n\n // adminToken (platform admin JWT) takes precedence over everything else\n if (this.adminToken) {\n headers['Authorization'] = `Bearer ${this.adminToken}`;\n }\n\n // Merge global custom headers\n if (this.options?.headers) {\n Object.assign(headers, this.options.headers);\n }\n\n // Merge per-request headers\n if (options?.headers) {\n Object.assign(headers, options.headers);\n }\n\n return headers;\n }\n\n private async executeWithRetry<T>(\n method: string,\n path: string,\n options: RequestOptions | undefined,\n attempt: number,\n ): Promise<PalbaseResponse<T>> {\n const url = `${this.getBaseUrl()}${path}`;\n const headers = this.buildHeaders(options);\n\n // Run interceptors\n for (const interceptor of this.interceptors) {\n await interceptor({ headers, method, path });\n }\n\n const fetchOptions: RequestInit = {\n method,\n headers,\n signal: options?.signal,\n };\n\n if (options?.body !== undefined) {\n fetchOptions.body = JSON.stringify(options.body);\n }\n\n let response: Response;\n try {\n response = await fetch(url, fetchOptions);\n } catch (error) {\n // Network error — retry with backoff\n if (attempt < MAX_RETRIES - 1) {\n const backoff = INITIAL_BACKOFF_MS * 2 ** attempt;\n await this.delay(backoff);\n return this.executeWithRetry<T>(method, path, options, attempt + 1);\n }\n\n // All retries exhausted — throw PalbaseError\n throw new PalbaseError(\n 'network_error',\n error instanceof Error ? error.message : 'Network request failed',\n 0,\n );\n }\n\n // Handle 429 Too Many Requests — retry with Retry-After or backoff;\n // if retries exhausted, fall through to normal error response handling below\n if (response.status === 429) {\n if (attempt < MAX_RETRIES - 1) {\n const retryAfter = response.headers.get('Retry-After');\n const parsed = retryAfter ? Number.parseInt(retryAfter, 10) : Number.NaN;\n const delayMs = Number.isNaN(parsed) ? INITIAL_BACKOFF_MS * 2 ** attempt : parsed * 1000;\n await this.delay(delayMs);\n return this.executeWithRetry<T>(method, path, options, attempt + 1);\n }\n }\n\n // Parse response body\n let data: T | null = null;\n let errorBody: { error?: string; error_description?: string; status?: number } | undefined;\n\n // HEAD responses have no body by spec — skip parsing.\n const contentType = response.headers.get('Content-Type');\n if (method !== 'HEAD' && contentType?.includes('json')) {\n const body = await response.json() as Record<string, unknown>;\n if (response.ok) {\n data = body as T;\n } else {\n errorBody = body as typeof errorBody;\n }\n }\n\n if (!response.ok) {\n return {\n data: null,\n error: new PalbaseError(\n errorBody?.error ?? 'unknown_error',\n errorBody?.error_description ?? response.statusText,\n response.status,\n errorBody,\n ),\n status: response.status,\n };\n }\n\n // Parse PostgREST Content-Range for count queries (e.g. \"0-9/42\" or \"*/42\").\n const contentRange = response.headers.get('Content-Range');\n let count: number | undefined;\n if (contentRange) {\n const slash = contentRange.lastIndexOf('/');\n if (slash >= 0) {\n const totalPart = contentRange.slice(slash + 1);\n if (totalPart !== '*') {\n const parsed = Number.parseInt(totalPart, 10);\n if (!Number.isNaN(parsed)) {\n count = parsed;\n }\n }\n }\n }\n\n return {\n data,\n error: null,\n status: response.status,\n ...(count !== undefined ? { count } : {}),\n };\n }\n\n private delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n","export type Platform = 'browser' | 'node' | 'react-native' | 'deno' | 'bun';\n\ndeclare const Deno: unknown;\ndeclare const process: { versions: Record<string, string> } | undefined;\n\nexport function detectPlatform(): Platform {\n if (typeof Deno !== 'undefined') {\n return 'deno';\n }\n\n if (process?.versions) {\n if ('bun' in process.versions) {\n return 'bun';\n }\n if ('node' in process.versions) {\n return 'node';\n }\n }\n\n if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {\n return 'react-native';\n }\n\n return 'browser';\n}\n","import type { AuthStateCallback, Session, Unsubscribe } from './types.js';\n\nexport class TokenManager {\n private session: Session | null = null;\n private listeners: Set<AuthStateCallback> = new Set();\n private refreshPromise: Promise<void> | null = null;\n\n refreshFunction: ((refreshToken: string) => Promise<Session>) | null = null;\n\n setSession(session: Session): void {\n this.session = session;\n this.notify('SESSION_SET', session);\n }\n\n getAccessToken(): string | null {\n return this.session?.accessToken ?? null;\n }\n\n getRefreshToken(): string | null {\n return this.session?.refreshToken ?? null;\n }\n\n clearSession(): void {\n this.session = null;\n this.notify('SESSION_CLEARED', null);\n }\n\n isExpired(): boolean {\n if (!this.session) return true;\n return Date.now() >= this.session.expiresAt;\n }\n\n async refreshSession(): Promise<void> {\n if (!this.session?.refreshToken || !this.refreshFunction) {\n return;\n }\n\n // Collapse concurrent refresh calls into a single request\n if (this.refreshPromise) {\n return this.refreshPromise;\n }\n\n this.refreshPromise = this.executeRefresh(this.session.refreshToken);\n\n try {\n await this.refreshPromise;\n } finally {\n this.refreshPromise = null;\n }\n }\n\n onAuthStateChange(callback: AuthStateCallback): Unsubscribe {\n this.listeners.add(callback);\n return () => {\n this.listeners.delete(callback);\n };\n }\n\n private async executeRefresh(refreshToken: string): Promise<void> {\n if (!this.refreshFunction) return;\n const newSession = await this.refreshFunction(refreshToken);\n this.setSession(newSession);\n }\n\n private notify(event: 'SESSION_SET' | 'SESSION_CLEARED', session: Session | null): void {\n for (const listener of this.listeners) {\n listener(event, session);\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/config.ts","../src/errors.ts","../src/http.ts","../src/platform.ts","../src/token.ts"],"sourcesContent":["import type { HttpClient } from './http.js';\nimport type { ProjectConfig } from './types.js';\n\nconst CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes\n\nexport class ConfigFetcher {\n protected readonly httpClient: HttpClient;\n private cachedConfig: ProjectConfig | null = null;\n private cacheTimestamp = 0;\n\n constructor(httpClient: HttpClient) {\n this.httpClient = httpClient;\n }\n\n async getConfig(): Promise<ProjectConfig | null> {\n const now = Date.now();\n\n if (this.cachedConfig && now - this.cacheTimestamp < CACHE_TTL_MS) {\n return this.cachedConfig;\n }\n\n try {\n const response = await this.httpClient.request<ProjectConfig>('GET', '/v1/config');\n\n if (response.error || !response.data) {\n return null;\n }\n\n this.cachedConfig = response.data;\n this.cacheTimestamp = now;\n\n return this.cachedConfig;\n } catch {\n return null;\n }\n }\n}\n","export class PalbaseError extends Error {\n readonly code: string;\n readonly status: number;\n readonly details?: unknown;\n\n constructor(code: string, message: string, status: number, details?: unknown) {\n super(message);\n this.name = 'PalbaseError';\n this.code = code;\n this.status = status;\n this.details = details;\n }\n}\n","import { PalbaseError } from './errors.js';\nimport type { TokenManager } from './token.js';\nimport type { HttpClientOptions, PalbaseResponse, RequestOptions } from './types.js';\n\n/**\n * Default production host. Dev / staging / local callers override via\n * `options.url`. Apex-style routing (all traffic lands on one host and\n * Kong stamps `X-Project-Ref` from the key-auth consumer) is the only\n * supported production path — subdomain-per-tenant routing was\n * considered but retired in favour of a single gateway.\n */\nconst PALBASE_DEFAULT_HOST = 'api.palbase.studio';\n\n/**\n * Parse project ref from a palbase API key.\n *\n * Canonical shape: `pb_{ref}_{scope}{random}` where `ref` is exactly\n * 8 base62 chars and `scope` is `c` (client) or `s` (server). See\n * docs/MODULE_HEADER_CONTRACT.md §\"API key format\" (palbase repo) for\n * the full spec.\n *\n * Returns the 8-char ref on match; `null` otherwise — callers surface\n * that as an `invalid_api_key` error.\n */\nconst REF_LEN = 8;\nconst BASE62_RE = /^[0-9A-Za-z]+$/;\n\nfunction parseProjectRef(apiKey: string): string | null {\n // pb_ (3) + ref (8) + _ (1) + scope (1) = 13 min chars\n if (apiKey.length < 13) return null;\n if (!apiKey.startsWith('pb_')) return null;\n if (apiKey[11] !== '_') return null;\n const ref = apiKey.slice(3, 11);\n if (ref.length !== REF_LEN) return null;\n if (!BASE62_RE.test(ref)) return null;\n const scope = apiKey[12];\n if (scope !== 'c' && scope !== 's') return null;\n return ref;\n}\nconst MAX_RETRIES = 3;\nconst INITIAL_BACKOFF_MS = 200;\n\n/**\n * Request interceptor. Runs before every HTTP request.\n * Can modify headers, body, or reject the request.\n */\nexport interface RequestInterceptor {\n (request: { headers: Record<string, string>; method: string; path: string }): void | Promise<void>;\n}\n\nexport class HttpClient {\n protected readonly apiKey: string;\n protected readonly options?: HttpClientOptions;\n\n tokenManager: TokenManager | null = null;\n\n /**\n * Admin JWT used for platform admin endpoints (/admin/*).\n * When set, takes precedence over serviceRoleKey and tokenManager access\n * token in the Authorization header.\n */\n adminToken: string | null = null;\n\n private readonly interceptors: RequestInterceptor[] = [];\n\n constructor(apiKey: string, options?: HttpClientOptions) {\n this.apiKey = apiKey;\n this.options = options;\n }\n\n /** Set (or clear) the admin JWT used on admin endpoints. */\n setAdminToken(token: string | null): void {\n this.adminToken = token;\n }\n\n /**\n * Create a scoped HttpClient that adds the given extra headers to every\n * request. The returned client shares the admin token and token manager\n * with the parent at runtime — later changes on the parent propagate to\n * the scope and vice versa.\n *\n * Typical use: tagging admin calls with `x-palbase-project: <ref>` so the\n * gateway can route them to the correct project's data plane.\n */\n withHeaders(extra: Record<string, string>): HttpClient {\n const mergedHeaders = { ...(this.options?.headers ?? {}), ...extra };\n const parent = this;\n const scoped: HttpClient = new HttpClient(this.apiKey, {\n ...this.options,\n headers: mergedHeaders,\n });\n scoped.tokenManager = this.tokenManager;\n // Delegate adminToken reads + writes to the parent so the scope always\n // sees the latest token, and setAdminToken on the scope affects the parent.\n Object.defineProperty(scoped, 'adminToken', {\n get: () => parent.adminToken,\n set: (v: string | null) => {\n parent.adminToken = v;\n },\n configurable: true,\n });\n return scoped;\n }\n\n /** Add a request interceptor. Runs before every request. */\n addInterceptor(interceptor: RequestInterceptor): void {\n this.interceptors.push(interceptor);\n }\n\n async request<T>(\n method: string,\n path: string,\n options?: RequestOptions,\n ): Promise<PalbaseResponse<T>> {\n // If token is expired and refresh is available, refresh before making the request\n if (this.tokenManager?.isExpired() && this.tokenManager.getRefreshToken() && this.tokenManager.refreshFunction) {\n await this.tokenManager.refreshSession();\n }\n\n return this.executeWithRetry<T>(method, path, options, 0);\n }\n\n private getBaseUrl(): string {\n // Explicit URL always wins (local dev, staging, test rigs).\n if (this.options?.url) {\n return this.options.url;\n }\n\n // Validate the key shape up front so apex-routed callers still\n // fail loud on a malformed key instead of hitting the gateway\n // with bad credentials.\n if (this.apiKey && parseProjectRef(this.apiKey) === null) {\n throw new PalbaseError(\n 'invalid_api_key',\n 'Invalid API key format. Expected pb_{ref}_{scope}{random}. For dev/staging pass `url: \"https://api.dev.palbase.studio\"` via options.',\n 0,\n );\n }\n\n return `https://${PALBASE_DEFAULT_HOST}`;\n }\n\n private buildHeaders(options?: RequestOptions): Record<string, string> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n\n // The effective key is either the explicit serviceRoleKey\n // (server SDK's `serviceRole` option) or the constructor-time\n // apiKey. Palbase keys live in the `apikey` header — never in\n // `Authorization` — because Kong's key-auth resolves them on\n // that header and the gateway's pre-function plugin reads the\n // scope tag at byte offset 12 to decide service_role vs anon.\n // Putting a server key on `Authorization: Bearer …` would land\n // it on PostgREST as a JWT (PostgREST then 500s with PGRST300)\n // and Kong wouldn't see it at all on the apikey path.\n const effectiveKey = this.options?.serviceRoleKey ?? this.apiKey;\n if (effectiveKey) {\n headers['apikey'] = effectiveKey;\n // Defense-in-depth X-Project-Ref. Kong's pre-function plugin\n // always overrides this with the consumer's `custom_id` (see\n // docs/MODULE_HEADER_CONTRACT.md §\"pre-function\") so any\n // client-sent value is discarded before it reaches the\n // upstream module — but emitting one here keeps non-Kong\n // test rigs and apex-routed admin paths correct.\n const ref = parseProjectRef(effectiveKey);\n if (ref) {\n headers['X-Project-Ref'] = ref;\n }\n }\n\n // User session token, if any. Kong's pre-function plugin strips\n // Authorization on /v1/* routes anyway (PostgREST has no JWT\n // secret and would crash on a Bearer it can't decode), but\n // sending it preserves the contract for /auth/* endpoints that\n // do consume the bearer (e.g. session refresh).\n const token = this.tokenManager?.getAccessToken();\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n\n // adminToken (platform admin JWT) takes precedence — used by the\n // @palbase/admin internal flows that hit /admin/* routes; those\n // routes verify the bearer themselves and aren't subject to the\n // /v1/* Authorization-strip rule.\n if (this.adminToken) {\n headers['Authorization'] = `Bearer ${this.adminToken}`;\n }\n\n // Merge global custom headers\n if (this.options?.headers) {\n Object.assign(headers, this.options.headers);\n }\n\n // Merge per-request headers\n if (options?.headers) {\n Object.assign(headers, options.headers);\n }\n\n return headers;\n }\n\n private async executeWithRetry<T>(\n method: string,\n path: string,\n options: RequestOptions | undefined,\n attempt: number,\n ): Promise<PalbaseResponse<T>> {\n const url = `${this.getBaseUrl()}${path}`;\n const headers = this.buildHeaders(options);\n\n // Run interceptors\n for (const interceptor of this.interceptors) {\n await interceptor({ headers, method, path });\n }\n\n const fetchOptions: RequestInit = {\n method,\n headers,\n signal: options?.signal,\n };\n\n if (options?.body !== undefined) {\n fetchOptions.body = JSON.stringify(options.body);\n }\n\n let response: Response;\n try {\n response = await fetch(url, fetchOptions);\n } catch (error) {\n // Network error — retry with backoff\n if (attempt < MAX_RETRIES - 1) {\n const backoff = INITIAL_BACKOFF_MS * 2 ** attempt;\n await this.delay(backoff);\n return this.executeWithRetry<T>(method, path, options, attempt + 1);\n }\n\n // All retries exhausted — throw PalbaseError\n throw new PalbaseError(\n 'network_error',\n error instanceof Error ? error.message : 'Network request failed',\n 0,\n );\n }\n\n // Handle 429 Too Many Requests — retry with Retry-After or backoff;\n // if retries exhausted, fall through to normal error response handling below\n if (response.status === 429) {\n if (attempt < MAX_RETRIES - 1) {\n const retryAfter = response.headers.get('Retry-After');\n const parsed = retryAfter ? Number.parseInt(retryAfter, 10) : Number.NaN;\n const delayMs = Number.isNaN(parsed) ? INITIAL_BACKOFF_MS * 2 ** attempt : parsed * 1000;\n await this.delay(delayMs);\n return this.executeWithRetry<T>(method, path, options, attempt + 1);\n }\n }\n\n // Parse response body\n let data: T | null = null;\n let errorBody: { error?: string; error_description?: string; status?: number } | undefined;\n\n // HEAD responses have no body by spec — skip parsing.\n const contentType = response.headers.get('Content-Type');\n if (method !== 'HEAD' && contentType?.includes('json')) {\n const body = await response.json() as Record<string, unknown>;\n if (response.ok) {\n data = body as T;\n } else {\n errorBody = body as typeof errorBody;\n }\n }\n\n if (!response.ok) {\n return {\n data: null,\n error: new PalbaseError(\n errorBody?.error ?? 'unknown_error',\n errorBody?.error_description ?? response.statusText,\n response.status,\n errorBody,\n ),\n status: response.status,\n };\n }\n\n // Parse PostgREST Content-Range for count queries (e.g. \"0-9/42\" or \"*/42\").\n const contentRange = response.headers.get('Content-Range');\n let count: number | undefined;\n if (contentRange) {\n const slash = contentRange.lastIndexOf('/');\n if (slash >= 0) {\n const totalPart = contentRange.slice(slash + 1);\n if (totalPart !== '*') {\n const parsed = Number.parseInt(totalPart, 10);\n if (!Number.isNaN(parsed)) {\n count = parsed;\n }\n }\n }\n }\n\n return {\n data,\n error: null,\n status: response.status,\n ...(count !== undefined ? { count } : {}),\n };\n }\n\n private delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n","export type Platform = 'browser' | 'node' | 'react-native' | 'deno' | 'bun';\n\ndeclare const Deno: unknown;\ndeclare const process: { versions: Record<string, string> } | undefined;\n\nexport function detectPlatform(): Platform {\n if (typeof Deno !== 'undefined') {\n return 'deno';\n }\n\n if (process?.versions) {\n if ('bun' in process.versions) {\n return 'bun';\n }\n if ('node' in process.versions) {\n return 'node';\n }\n }\n\n if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {\n return 'react-native';\n }\n\n return 'browser';\n}\n","import type { AuthStateCallback, Session, Unsubscribe } from './types.js';\n\nexport class TokenManager {\n private session: Session | null = null;\n private listeners: Set<AuthStateCallback> = new Set();\n private refreshPromise: Promise<void> | null = null;\n\n refreshFunction: ((refreshToken: string) => Promise<Session>) | null = null;\n\n setSession(session: Session): void {\n this.session = session;\n this.notify('SESSION_SET', session);\n }\n\n getAccessToken(): string | null {\n return this.session?.accessToken ?? null;\n }\n\n getRefreshToken(): string | null {\n return this.session?.refreshToken ?? null;\n }\n\n clearSession(): void {\n this.session = null;\n this.notify('SESSION_CLEARED', null);\n }\n\n isExpired(): boolean {\n if (!this.session) return true;\n return Date.now() >= this.session.expiresAt;\n }\n\n async refreshSession(): Promise<void> {\n if (!this.session?.refreshToken || !this.refreshFunction) {\n return;\n }\n\n // Collapse concurrent refresh calls into a single request\n if (this.refreshPromise) {\n return this.refreshPromise;\n }\n\n this.refreshPromise = this.executeRefresh(this.session.refreshToken);\n\n try {\n await this.refreshPromise;\n } finally {\n this.refreshPromise = null;\n }\n }\n\n onAuthStateChange(callback: AuthStateCallback): Unsubscribe {\n this.listeners.add(callback);\n return () => {\n this.listeners.delete(callback);\n };\n }\n\n private async executeRefresh(refreshToken: string): Promise<void> {\n if (!this.refreshFunction) return;\n const newSession = await this.refreshFunction(refreshToken);\n this.setSession(newSession);\n }\n\n private notify(event: 'SESSION_SET' | 'SESSION_CLEARED', session: Session | null): void {\n for (const listener of this.listeners) {\n listener(event, session);\n }\n }\n}\n"],"mappings":"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","names":[]}
|