@centrali-io/centrali-sdk 4.4.1 → 4.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,20 +40,36 @@ console.log('Found:', results.data.totalHits, 'results');
40
40
 
41
41
  ## Authentication
42
42
 
43
- The SDK supports two authentication methods:
43
+ The SDK supports three authentication methods. See the [full authentication guide](../docs/sdk/authentication.md) for details.
44
44
 
45
- ### Bearer Token (User Authentication)
45
+ ### Publishable Key (Frontend Apps)
46
+
47
+ Safe to use in browser code. Scoped to specific resources.
48
+
49
+ ```typescript
50
+ const centrali = new CentraliSDK({
51
+ baseUrl: 'https://centrali.io',
52
+ workspaceId: 'your-workspace',
53
+ publishableKey: 'pk_live_your_key_here'
54
+ });
55
+ ```
56
+
57
+ ### External Token / BYOT (Clerk, Auth0, etc.)
58
+
59
+ Dynamic token callback for apps with their own auth.
46
60
 
47
61
  ```typescript
48
62
  const centrali = new CentraliSDK({
49
63
  baseUrl: 'https://centrali.io',
50
64
  workspaceId: 'your-workspace',
51
- token: 'your-jwt-token'
65
+ getToken: async () => await clerk.session.getToken()
52
66
  });
53
67
  ```
54
68
 
55
69
  ### Service Account (Server-to-Server)
56
70
 
71
+ Never use in browser code. Client secret must stay on the server.
72
+
57
73
  ```typescript
58
74
  const centrali = new CentraliSDK({
59
75
  baseUrl: 'https://centrali.io',
@@ -61,8 +77,6 @@ const centrali = new CentraliSDK({
61
77
  clientId: process.env.CENTRALI_CLIENT_ID,
62
78
  clientSecret: process.env.CENTRALI_CLIENT_SECRET
63
79
  });
64
-
65
- // The SDK automatically fetches and manages tokens
66
80
  ```
67
81
 
68
82
  ## Features
@@ -79,6 +93,8 @@ const centrali = new CentraliSDK({
79
93
  - ✅ **Data Validation** - AI-powered typo detection, format validation, duplicate detection
80
94
  - ✅ **Anomaly Insights** - AI-powered anomaly detection and data quality insights
81
95
  - ✅ **Orchestrations** - Multi-step workflows with compute functions, decision logic, and delays
96
+ - ✅ **Publishable keys** - Scoped, browser-safe keys for frontend apps
97
+ - ✅ **External auth (BYOT)** - Dynamic token callback for Clerk, Auth0, etc.
82
98
  - ✅ **Service accounts** - Automatic token refresh for server-to-server auth
83
99
 
84
100
  ## Core Operations
package/dist/index.js CHANGED
@@ -51,7 +51,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
51
51
  return (mod && mod.__esModule) ? mod : { "default": mod };
52
52
  };
53
53
  Object.defineProperty(exports, "__esModule", { value: true });
54
- exports.CentraliSDK = exports.ComputeFunctionsManager = exports.CollectionsManager = exports.StructuresManager = exports.AllowedDomainsManager = exports.ValidationManager = exports.AnomalyInsightsManager = exports.SmartQueriesManager = exports.TriggersManager = exports.OrchestrationsManager = exports.RealtimeManager = exports.CentraliError = void 0;
54
+ exports.CentraliSDK = exports.FunctionRunsManager = exports.ComputeFunctionsManager = exports.CollectionsManager = exports.StructuresManager = exports.AllowedDomainsManager = exports.ValidationManager = exports.AnomalyInsightsManager = exports.SmartQueriesManager = exports.TriggersManager = exports.OrchestrationsManager = exports.RealtimeManager = exports.CentraliError = void 0;
55
55
  exports.isCentraliError = isCentraliError;
56
56
  exports.getApiUrl = getApiUrl;
57
57
  exports.getAuthUrl = getAuthUrl;
@@ -84,6 +84,9 @@ exports.getCollectionBySlugApiPath = getCollectionBySlugApiPath;
84
84
  exports.getCollectionValidateApiPath = getCollectionValidateApiPath;
85
85
  exports.getComputeFunctionsApiPath = getComputeFunctionsApiPath;
86
86
  exports.getComputeFunctionTestApiPath = getComputeFunctionTestApiPath;
87
+ exports.getFunctionRunsApiPath = getFunctionRunsApiPath;
88
+ exports.getFunctionRunsByTriggerApiPath = getFunctionRunsByTriggerApiPath;
89
+ exports.getFunctionRunsByFunctionApiPath = getFunctionRunsByFunctionApiPath;
87
90
  exports.getSmartQueryTestApiPath = getSmartQueryTestApiPath;
88
91
  exports.getValidationSuggestionsApiPath = getValidationSuggestionsApiPath;
89
92
  exports.getValidationSuggestionAcceptApiPath = getValidationSuggestionAcceptApiPath;
@@ -656,6 +659,28 @@ function getComputeFunctionTestApiPath(workspaceId) {
656
659
  return `data/workspace/${workspaceId}/api/v1/compute-functions/test`;
657
660
  }
658
661
  // =====================================================
662
+ // Function Runs API Path Helpers
663
+ // =====================================================
664
+ /**
665
+ * Generate Function Runs base API URL PATH.
666
+ */
667
+ function getFunctionRunsApiPath(workspaceId, runId) {
668
+ const basePath = `data/workspace/${workspaceId}/api/v1/function-runs`;
669
+ return runId ? `${basePath}/${runId}` : basePath;
670
+ }
671
+ /**
672
+ * Generate Function Runs by trigger API URL PATH.
673
+ */
674
+ function getFunctionRunsByTriggerApiPath(workspaceId, triggerId) {
675
+ return `data/workspace/${workspaceId}/api/v1/function-runs/trigger/${triggerId}`;
676
+ }
677
+ /**
678
+ * Generate Function Runs by function API URL PATH.
679
+ */
680
+ function getFunctionRunsByFunctionApiPath(workspaceId, functionId) {
681
+ return `data/workspace/${workspaceId}/api/v1/function-runs/function/${functionId}`;
682
+ }
683
+ // =====================================================
659
684
  // Smart Query Test API Path Helper (Configuration-as-Code)
660
685
  // =====================================================
661
686
  /**
@@ -2505,6 +2530,97 @@ class ComputeFunctionsManager {
2505
2530
  }
2506
2531
  }
2507
2532
  exports.ComputeFunctionsManager = ComputeFunctionsManager;
2533
+ /**
2534
+ * Manager for querying function execution runs.
2535
+ *
2536
+ * Provides read access to function run history — useful for checking
2537
+ * whether jobs completed, inspecting outputs, and monitoring trigger activity.
2538
+ *
2539
+ * Access via `client.runs`.
2540
+ */
2541
+ class FunctionRunsManager {
2542
+ constructor(workspaceId, requestFn) {
2543
+ this.workspaceId = workspaceId;
2544
+ this.requestFn = requestFn;
2545
+ }
2546
+ /**
2547
+ * Get a function run by ID.
2548
+ *
2549
+ * @param runId - The function run UUID
2550
+ * @returns The function run details
2551
+ *
2552
+ * @example
2553
+ * ```ts
2554
+ * const run = await client.runs.get('run-uuid');
2555
+ * console.log('Status:', run.data.status);
2556
+ * console.log('Duration:', run.data.endedAt ? Date.parse(run.data.endedAt) - Date.parse(run.data.startedAt) : 'still running');
2557
+ * ```
2558
+ */
2559
+ get(runId) {
2560
+ const path = getFunctionRunsApiPath(this.workspaceId, runId);
2561
+ return this.requestFn('GET', path);
2562
+ }
2563
+ /**
2564
+ * List runs for a specific trigger.
2565
+ *
2566
+ * @param triggerId - The trigger UUID
2567
+ * @param options - Optional pagination and status filter
2568
+ * @returns Paginated list of function runs
2569
+ *
2570
+ * @example
2571
+ * ```ts
2572
+ * // List recent runs for a trigger
2573
+ * const runs = await client.runs.listByTrigger('trigger-uuid');
2574
+ *
2575
+ * // Filter to only failed runs
2576
+ * const failed = await client.runs.listByTrigger('trigger-uuid', {
2577
+ * status: 'failure'
2578
+ * });
2579
+ * ```
2580
+ */
2581
+ listByTrigger(triggerId, options) {
2582
+ const path = getFunctionRunsByTriggerApiPath(this.workspaceId, triggerId);
2583
+ const queryParams = {};
2584
+ if (options === null || options === void 0 ? void 0 : options.page)
2585
+ queryParams.page = options.page;
2586
+ if (options === null || options === void 0 ? void 0 : options.limit)
2587
+ queryParams.limit = options.limit;
2588
+ if (options === null || options === void 0 ? void 0 : options.status)
2589
+ queryParams.status = options.status;
2590
+ return this.requestFn('GET', path, null, queryParams);
2591
+ }
2592
+ /**
2593
+ * List runs for a specific compute function.
2594
+ *
2595
+ * @param functionId - The compute function UUID
2596
+ * @param options - Optional pagination and status filter
2597
+ * @returns Paginated list of function runs
2598
+ *
2599
+ * @example
2600
+ * ```ts
2601
+ * // List recent runs for a function
2602
+ * const runs = await client.runs.listByFunction('function-uuid');
2603
+ *
2604
+ * // Only completed runs, page 2
2605
+ * const completed = await client.runs.listByFunction('function-uuid', {
2606
+ * status: 'completed',
2607
+ * page: 2
2608
+ * });
2609
+ * ```
2610
+ */
2611
+ listByFunction(functionId, options) {
2612
+ const path = getFunctionRunsByFunctionApiPath(this.workspaceId, functionId);
2613
+ const queryParams = {};
2614
+ if (options === null || options === void 0 ? void 0 : options.page)
2615
+ queryParams.page = options.page;
2616
+ if (options === null || options === void 0 ? void 0 : options.limit)
2617
+ queryParams.limit = options.limit;
2618
+ if (options === null || options === void 0 ? void 0 : options.status)
2619
+ queryParams.status = options.status;
2620
+ return this.requestFn('GET', path, null, queryParams);
2621
+ }
2622
+ }
2623
+ exports.FunctionRunsManager = FunctionRunsManager;
2508
2624
  /**
2509
2625
  * Main Centrali SDK client.
2510
2626
  */
@@ -2521,14 +2637,41 @@ class CentraliSDK {
2521
2637
  this._structures = null;
2522
2638
  this._collections = null;
2523
2639
  this._functions = null;
2640
+ this._runs = null;
2524
2641
  this.isRefreshingToken = false;
2525
2642
  this.tokenRefreshPromise = null;
2526
2643
  this.options = options;
2527
2644
  this.token = options.token || null;
2645
+ // Validate mutually exclusive auth options
2646
+ const authPaths = [
2647
+ options.publishableKey ? 'publishableKey' : null,
2648
+ options.getToken ? 'getToken' : null,
2649
+ (options.clientId || options.clientSecret) ? 'clientId/clientSecret' : null,
2650
+ ].filter(Boolean);
2651
+ if (options.publishableKey && authPaths.length > 1) {
2652
+ throw new Error(`Cannot use publishableKey with ${authPaths.filter(p => p !== 'publishableKey').join(', ')}. Use one auth method.`);
2653
+ }
2654
+ if (options.getToken && (options.clientId || options.clientSecret)) {
2655
+ throw new Error('Cannot use getToken with clientId/clientSecret. Use one auth method.');
2656
+ }
2528
2657
  const apiUrl = getApiUrl(options.baseUrl);
2529
2658
  this.axios = axios_1.default.create(Object.assign({ baseURL: apiUrl, paramsSerializer: (params) => qs_1.default.stringify(params, { arrayFormat: "repeat" }), proxy: false }, options.axiosConfig));
2530
2659
  // Attach async interceptor to fetch token on first request if needed
2531
2660
  this.axios.interceptors.request.use((config) => __awaiter(this, void 0, void 0, function* () {
2661
+ // Auth path 1: Publishable key — send as x-api-key header, no token logic
2662
+ if (this.options.publishableKey) {
2663
+ config.headers['x-api-key'] = this.options.publishableKey;
2664
+ return config;
2665
+ }
2666
+ // Auth path 2: Dynamic token callback (getToken) — call on each request
2667
+ if (this.options.getToken) {
2668
+ this.token = yield this.options.getToken();
2669
+ if (this.token) {
2670
+ config.headers.Authorization = `Bearer ${this.token}`;
2671
+ }
2672
+ return config;
2673
+ }
2674
+ // Auth path 3: Client credentials — fetch token on first request
2532
2675
  if (!this.token && this.options.clientId && this.options.clientSecret) {
2533
2676
  this.token = yield fetchClientToken(this.options.clientId, this.options.clientSecret, this.options.baseUrl);
2534
2677
  }
@@ -2541,10 +2684,26 @@ class CentraliSDK {
2541
2684
  this.axios.interceptors.response.use((response) => response, (error) => __awaiter(this, void 0, void 0, function* () {
2542
2685
  var _a, _b;
2543
2686
  const originalRequest = error.config;
2544
- // Only attempt refresh for 401/403 errors when using client credentials
2687
+ // Publishable keys: no retry scope errors are permanent
2688
+ if (this.options.publishableKey) {
2689
+ return Promise.reject(error);
2690
+ }
2545
2691
  const isAuthError = ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 401 || ((_b = error.response) === null || _b === void 0 ? void 0 : _b.status) === 403;
2546
- const hasClientCredentials = this.options.clientId && this.options.clientSecret;
2547
2692
  const hasNotRetried = !originalRequest._hasRetried;
2693
+ // getToken path: retry once with a fresh token on 401
2694
+ if (isAuthError && this.options.getToken && hasNotRetried) {
2695
+ originalRequest._hasRetried = true;
2696
+ try {
2697
+ this.token = yield this.options.getToken();
2698
+ originalRequest.headers.Authorization = `Bearer ${this.token}`;
2699
+ return this.axios(originalRequest);
2700
+ }
2701
+ catch (refreshError) {
2702
+ return Promise.reject(error);
2703
+ }
2704
+ }
2705
+ // Client credentials path: refresh token and retry on 401/403
2706
+ const hasClientCredentials = this.options.clientId && this.options.clientSecret;
2548
2707
  if (isAuthError && hasClientCredentials && hasNotRetried) {
2549
2708
  // Mark request as retried to prevent infinite loops
2550
2709
  originalRequest._hasRetried = true;
@@ -2871,6 +3030,29 @@ class CentraliSDK {
2871
3030
  }
2872
3031
  return this._functions;
2873
3032
  }
3033
+ /**
3034
+ * Runs namespace for querying execution history.
3035
+ *
3036
+ * Usage:
3037
+ * ```ts
3038
+ * // Get a specific run
3039
+ * const run = await client.runs.get('run-id');
3040
+ *
3041
+ * // List runs for a trigger
3042
+ * const runs = await client.runs.listByTrigger('trigger-id');
3043
+ *
3044
+ * // List failed runs for a compute definition
3045
+ * const failed = await client.runs.listByFunction('fn-id', {
3046
+ * status: 'failure'
3047
+ * });
3048
+ * ```
3049
+ */
3050
+ get runs() {
3051
+ if (!this._runs) {
3052
+ this._runs = new FunctionRunsManager(this.options.workspaceId, this.request.bind(this));
3053
+ }
3054
+ return this._runs;
3055
+ }
2874
3056
  /**
2875
3057
  * Manually set or update the bearer token for subsequent requests.
2876
3058
  */
package/index.ts CHANGED
@@ -542,11 +542,22 @@ export interface CentraliSDKOptions {
542
542
  /** Base URL of Centrali (e.g. https://centrali.io). The SDK automatically uses api.centrali.io for API calls. */
543
543
  baseUrl: string;
544
544
  workspaceId: string;
545
+
546
+ // Auth path 1: Publishable key (frontend apps — safe to expose in browser code)
547
+ /** Publishable key for frontend access. Sent as x-api-key header. No token refresh needed. */
548
+ publishableKey?: string;
549
+
550
+ // Auth path 2: Bearer token (existing) + dynamic token callback (new)
545
551
  /** Optional initial bearer token for authentication */
546
552
  token?: string;
553
+ /** Optional callback to dynamically fetch a fresh token before each request (e.g., for Clerk, Auth0) */
554
+ getToken?: () => Promise<string>;
555
+
556
+ // Auth path 3: Service account (server-side only — never use in browser)
547
557
  /** Optional OAuth2 client credentials */
548
558
  clientId?: string;
549
559
  clientSecret?: string;
560
+
550
561
  /** Optional custom axios config */
551
562
  axiosConfig?: AxiosRequestConfig;
552
563
  }
@@ -1769,6 +1780,66 @@ export interface TestComputeFunctionResult {
1769
1780
  error?: string;
1770
1781
  }
1771
1782
 
1783
+ // =====================================================
1784
+ // Function Run Types
1785
+ // =====================================================
1786
+
1787
+ /**
1788
+ * Execution source for a function run.
1789
+ */
1790
+ export type FunctionRunExecutionSource = 'trigger' | 'rerun' | 'manual' | 'scheduled' | 'http-trigger' | 'orchestration' | 'endpoint';
1791
+
1792
+ /**
1793
+ * Status of a function run.
1794
+ */
1795
+ export type FunctionRunStatus = 'pending' | 'running' | 'completed' | 'failure' | 'timeout';
1796
+
1797
+ /**
1798
+ * A function run record representing a single execution of a compute function.
1799
+ */
1800
+ export interface FunctionRun {
1801
+ id: string;
1802
+ createdBy: string;
1803
+ functionId: string;
1804
+ workspaceSlug: string;
1805
+ jobId: string;
1806
+ status: FunctionRunStatus;
1807
+ runData: any | null;
1808
+ startedAt: string;
1809
+ endedAt?: string | null;
1810
+ createdAt?: string;
1811
+ updatedAt?: string;
1812
+ executionId: string;
1813
+ triggerId?: string | null;
1814
+ triggerType?: string | null;
1815
+ orchestrationRunId?: string | null;
1816
+ orchestrationStepId?: string | null;
1817
+ originalRunId?: string | null;
1818
+ isRerun: boolean;
1819
+ rerunCount: number;
1820
+ rerunBy?: string | null;
1821
+ rerunReason?: string | null;
1822
+ functionCodeHash?: string | null;
1823
+ functionVersion?: string | null;
1824
+ executionSource: FunctionRunExecutionSource;
1825
+ memoryUsageBytes?: number | null;
1826
+ cpuUsageSeconds?: number | null;
1827
+ errorCode?: string | null;
1828
+ errorMessage?: string | null;
1829
+ dataStrippedAt?: string | null;
1830
+ archiveStorageAddress?: string | null;
1831
+ archivedAt?: string | null;
1832
+ }
1833
+
1834
+ /**
1835
+ * Options for listing function runs by trigger or function.
1836
+ */
1837
+ export interface ListFunctionRunsOptions {
1838
+ page?: number;
1839
+ limit?: number;
1840
+ status?: FunctionRunStatus;
1841
+ }
1842
+
1772
1843
  // =====================================================
1773
1844
  // Trigger CRUD Types (Configuration-as-Code)
1774
1845
  // =====================================================
@@ -2723,6 +2794,32 @@ export function getComputeFunctionTestApiPath(workspaceId: string): string {
2723
2794
  return `data/workspace/${workspaceId}/api/v1/compute-functions/test`;
2724
2795
  }
2725
2796
 
2797
+ // =====================================================
2798
+ // Function Runs API Path Helpers
2799
+ // =====================================================
2800
+
2801
+ /**
2802
+ * Generate Function Runs base API URL PATH.
2803
+ */
2804
+ export function getFunctionRunsApiPath(workspaceId: string, runId?: string): string {
2805
+ const basePath = `data/workspace/${workspaceId}/api/v1/function-runs`;
2806
+ return runId ? `${basePath}/${runId}` : basePath;
2807
+ }
2808
+
2809
+ /**
2810
+ * Generate Function Runs by trigger API URL PATH.
2811
+ */
2812
+ export function getFunctionRunsByTriggerApiPath(workspaceId: string, triggerId: string): string {
2813
+ return `data/workspace/${workspaceId}/api/v1/function-runs/trigger/${triggerId}`;
2814
+ }
2815
+
2816
+ /**
2817
+ * Generate Function Runs by function API URL PATH.
2818
+ */
2819
+ export function getFunctionRunsByFunctionApiPath(workspaceId: string, functionId: string): string {
2820
+ return `data/workspace/${workspaceId}/api/v1/function-runs/function/${functionId}`;
2821
+ }
2822
+
2726
2823
  // =====================================================
2727
2824
  // Smart Query Test API Path Helper (Configuration-as-Code)
2728
2825
  // =====================================================
@@ -4750,6 +4847,100 @@ export class ComputeFunctionsManager {
4750
4847
  }
4751
4848
  }
4752
4849
 
4850
+ /**
4851
+ * Manager for querying function execution runs.
4852
+ *
4853
+ * Provides read access to function run history — useful for checking
4854
+ * whether jobs completed, inspecting outputs, and monitoring trigger activity.
4855
+ *
4856
+ * Access via `client.runs`.
4857
+ */
4858
+ export class FunctionRunsManager {
4859
+ private requestFn: <T>(method: Method, path: string, data?: any, queryParams?: Record<string, any>) => Promise<ApiResponse<T>>;
4860
+ private workspaceId: string;
4861
+
4862
+ constructor(
4863
+ workspaceId: string,
4864
+ requestFn: <T>(method: Method, path: string, data?: any, queryParams?: Record<string, any>) => Promise<ApiResponse<T>>
4865
+ ) {
4866
+ this.workspaceId = workspaceId;
4867
+ this.requestFn = requestFn;
4868
+ }
4869
+
4870
+ /**
4871
+ * Get a function run by ID.
4872
+ *
4873
+ * @param runId - The function run UUID
4874
+ * @returns The function run details
4875
+ *
4876
+ * @example
4877
+ * ```ts
4878
+ * const run = await client.runs.get('run-uuid');
4879
+ * console.log('Status:', run.data.status);
4880
+ * console.log('Duration:', run.data.endedAt ? Date.parse(run.data.endedAt) - Date.parse(run.data.startedAt) : 'still running');
4881
+ * ```
4882
+ */
4883
+ public get(runId: string): Promise<ApiResponse<FunctionRun>> {
4884
+ const path = getFunctionRunsApiPath(this.workspaceId, runId);
4885
+ return this.requestFn<FunctionRun>('GET', path);
4886
+ }
4887
+
4888
+ /**
4889
+ * List runs for a specific trigger.
4890
+ *
4891
+ * @param triggerId - The trigger UUID
4892
+ * @param options - Optional pagination and status filter
4893
+ * @returns Paginated list of function runs
4894
+ *
4895
+ * @example
4896
+ * ```ts
4897
+ * // List recent runs for a trigger
4898
+ * const runs = await client.runs.listByTrigger('trigger-uuid');
4899
+ *
4900
+ * // Filter to only failed runs
4901
+ * const failed = await client.runs.listByTrigger('trigger-uuid', {
4902
+ * status: 'failure'
4903
+ * });
4904
+ * ```
4905
+ */
4906
+ public listByTrigger(triggerId: string, options?: ListFunctionRunsOptions): Promise<ApiResponse<PaginatedResponse<FunctionRun>>> {
4907
+ const path = getFunctionRunsByTriggerApiPath(this.workspaceId, triggerId);
4908
+ const queryParams: Record<string, any> = {};
4909
+ if (options?.page) queryParams.page = options.page;
4910
+ if (options?.limit) queryParams.limit = options.limit;
4911
+ if (options?.status) queryParams.status = options.status;
4912
+ return this.requestFn<PaginatedResponse<FunctionRun>>('GET', path, null, queryParams);
4913
+ }
4914
+
4915
+ /**
4916
+ * List runs for a specific compute function.
4917
+ *
4918
+ * @param functionId - The compute function UUID
4919
+ * @param options - Optional pagination and status filter
4920
+ * @returns Paginated list of function runs
4921
+ *
4922
+ * @example
4923
+ * ```ts
4924
+ * // List recent runs for a function
4925
+ * const runs = await client.runs.listByFunction('function-uuid');
4926
+ *
4927
+ * // Only completed runs, page 2
4928
+ * const completed = await client.runs.listByFunction('function-uuid', {
4929
+ * status: 'completed',
4930
+ * page: 2
4931
+ * });
4932
+ * ```
4933
+ */
4934
+ public listByFunction(functionId: string, options?: ListFunctionRunsOptions): Promise<ApiResponse<PaginatedResponse<FunctionRun>>> {
4935
+ const path = getFunctionRunsByFunctionApiPath(this.workspaceId, functionId);
4936
+ const queryParams: Record<string, any> = {};
4937
+ if (options?.page) queryParams.page = options.page;
4938
+ if (options?.limit) queryParams.limit = options.limit;
4939
+ if (options?.status) queryParams.status = options.status;
4940
+ return this.requestFn<PaginatedResponse<FunctionRun>>('GET', path, null, queryParams);
4941
+ }
4942
+ }
4943
+
4753
4944
  /**
4754
4945
  * Main Centrali SDK client.
4755
4946
  */
@@ -4767,12 +4958,28 @@ export class CentraliSDK {
4767
4958
  private _structures: StructuresManager | null = null;
4768
4959
  private _collections: CollectionsManager | null = null;
4769
4960
  private _functions: ComputeFunctionsManager | null = null;
4961
+ private _runs: FunctionRunsManager | null = null;
4770
4962
  private isRefreshingToken: boolean = false;
4771
4963
  private tokenRefreshPromise: Promise<string> | null = null;
4772
4964
 
4773
4965
  constructor(options: CentraliSDKOptions) {
4774
4966
  this.options = options;
4775
4967
  this.token = options.token || null;
4968
+
4969
+ // Validate mutually exclusive auth options
4970
+ const authPaths = [
4971
+ options.publishableKey ? 'publishableKey' : null,
4972
+ options.getToken ? 'getToken' : null,
4973
+ (options.clientId || options.clientSecret) ? 'clientId/clientSecret' : null,
4974
+ ].filter(Boolean);
4975
+
4976
+ if (options.publishableKey && authPaths.length > 1) {
4977
+ throw new Error(`Cannot use publishableKey with ${authPaths.filter(p => p !== 'publishableKey').join(', ')}. Use one auth method.`);
4978
+ }
4979
+ if (options.getToken && (options.clientId || options.clientSecret)) {
4980
+ throw new Error('Cannot use getToken with clientId/clientSecret. Use one auth method.');
4981
+ }
4982
+
4776
4983
  const apiUrl = getApiUrl(options.baseUrl);
4777
4984
  this.axios = axios.create({
4778
4985
  baseURL: apiUrl,
@@ -4785,6 +4992,22 @@ export class CentraliSDK {
4785
4992
  // Attach async interceptor to fetch token on first request if needed
4786
4993
  this.axios.interceptors.request.use(
4787
4994
  async (config) => {
4995
+ // Auth path 1: Publishable key — send as x-api-key header, no token logic
4996
+ if (this.options.publishableKey) {
4997
+ config.headers['x-api-key'] = this.options.publishableKey;
4998
+ return config;
4999
+ }
5000
+
5001
+ // Auth path 2: Dynamic token callback (getToken) — call on each request
5002
+ if (this.options.getToken) {
5003
+ this.token = await this.options.getToken();
5004
+ if (this.token) {
5005
+ config.headers.Authorization = `Bearer ${this.token}`;
5006
+ }
5007
+ return config;
5008
+ }
5009
+
5010
+ // Auth path 3: Client credentials — fetch token on first request
4788
5011
  if (!this.token && this.options.clientId && this.options.clientSecret) {
4789
5012
  this.token = await fetchClientToken(
4790
5013
  this.options.clientId,
@@ -4806,11 +5029,29 @@ export class CentraliSDK {
4806
5029
  async (error) => {
4807
5030
  const originalRequest = error.config;
4808
5031
 
4809
- // Only attempt refresh for 401/403 errors when using client credentials
5032
+ // Publishable keys: no retry scope errors are permanent
5033
+ if (this.options.publishableKey) {
5034
+ return Promise.reject(error);
5035
+ }
5036
+
4810
5037
  const isAuthError = error.response?.status === 401 || error.response?.status === 403;
4811
- const hasClientCredentials = this.options.clientId && this.options.clientSecret;
4812
5038
  const hasNotRetried = !originalRequest._hasRetried;
4813
5039
 
5040
+ // getToken path: retry once with a fresh token on 401
5041
+ if (isAuthError && this.options.getToken && hasNotRetried) {
5042
+ originalRequest._hasRetried = true;
5043
+ try {
5044
+ this.token = await this.options.getToken();
5045
+ originalRequest.headers.Authorization = `Bearer ${this.token}`;
5046
+ return this.axios(originalRequest);
5047
+ } catch (refreshError) {
5048
+ return Promise.reject(error);
5049
+ }
5050
+ }
5051
+
5052
+ // Client credentials path: refresh token and retry on 401/403
5053
+ const hasClientCredentials = this.options.clientId && this.options.clientSecret;
5054
+
4814
5055
  if (isAuthError && hasClientCredentials && hasNotRetried) {
4815
5056
  // Mark request as retried to prevent infinite loops
4816
5057
  originalRequest._hasRetried = true;
@@ -5188,6 +5429,33 @@ export class CentraliSDK {
5188
5429
  return this._functions;
5189
5430
  }
5190
5431
 
5432
+ /**
5433
+ * Runs namespace for querying execution history.
5434
+ *
5435
+ * Usage:
5436
+ * ```ts
5437
+ * // Get a specific run
5438
+ * const run = await client.runs.get('run-id');
5439
+ *
5440
+ * // List runs for a trigger
5441
+ * const runs = await client.runs.listByTrigger('trigger-id');
5442
+ *
5443
+ * // List failed runs for a compute definition
5444
+ * const failed = await client.runs.listByFunction('fn-id', {
5445
+ * status: 'failure'
5446
+ * });
5447
+ * ```
5448
+ */
5449
+ public get runs(): FunctionRunsManager {
5450
+ if (!this._runs) {
5451
+ this._runs = new FunctionRunsManager(
5452
+ this.options.workspaceId,
5453
+ this.request.bind(this)
5454
+ );
5455
+ }
5456
+ return this._runs;
5457
+ }
5458
+
5191
5459
  /**
5192
5460
  * Manually set or update the bearer token for subsequent requests.
5193
5461
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centrali-io/centrali-sdk",
3
- "version": "4.4.1",
3
+ "version": "4.4.4",
4
4
  "description": "Centrali Node SDK",
5
5
  "main": "dist/index.js",
6
6
  "type": "commonjs",