@centrali-io/centrali-sdk 4.4.1 → 4.4.2

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
@@ -2525,10 +2525,36 @@ class CentraliSDK {
2525
2525
  this.tokenRefreshPromise = null;
2526
2526
  this.options = options;
2527
2527
  this.token = options.token || null;
2528
+ // Validate mutually exclusive auth options
2529
+ const authPaths = [
2530
+ options.publishableKey ? 'publishableKey' : null,
2531
+ options.getToken ? 'getToken' : null,
2532
+ (options.clientId || options.clientSecret) ? 'clientId/clientSecret' : null,
2533
+ ].filter(Boolean);
2534
+ if (options.publishableKey && authPaths.length > 1) {
2535
+ throw new Error(`Cannot use publishableKey with ${authPaths.filter(p => p !== 'publishableKey').join(', ')}. Use one auth method.`);
2536
+ }
2537
+ if (options.getToken && (options.clientId || options.clientSecret)) {
2538
+ throw new Error('Cannot use getToken with clientId/clientSecret. Use one auth method.');
2539
+ }
2528
2540
  const apiUrl = getApiUrl(options.baseUrl);
2529
2541
  this.axios = axios_1.default.create(Object.assign({ baseURL: apiUrl, paramsSerializer: (params) => qs_1.default.stringify(params, { arrayFormat: "repeat" }), proxy: false }, options.axiosConfig));
2530
2542
  // Attach async interceptor to fetch token on first request if needed
2531
2543
  this.axios.interceptors.request.use((config) => __awaiter(this, void 0, void 0, function* () {
2544
+ // Auth path 1: Publishable key — send as x-api-key header, no token logic
2545
+ if (this.options.publishableKey) {
2546
+ config.headers['x-api-key'] = this.options.publishableKey;
2547
+ return config;
2548
+ }
2549
+ // Auth path 2: Dynamic token callback (getToken) — call on each request
2550
+ if (this.options.getToken) {
2551
+ this.token = yield this.options.getToken();
2552
+ if (this.token) {
2553
+ config.headers.Authorization = `Bearer ${this.token}`;
2554
+ }
2555
+ return config;
2556
+ }
2557
+ // Auth path 3: Client credentials — fetch token on first request
2532
2558
  if (!this.token && this.options.clientId && this.options.clientSecret) {
2533
2559
  this.token = yield fetchClientToken(this.options.clientId, this.options.clientSecret, this.options.baseUrl);
2534
2560
  }
@@ -2541,10 +2567,26 @@ class CentraliSDK {
2541
2567
  this.axios.interceptors.response.use((response) => response, (error) => __awaiter(this, void 0, void 0, function* () {
2542
2568
  var _a, _b;
2543
2569
  const originalRequest = error.config;
2544
- // Only attempt refresh for 401/403 errors when using client credentials
2570
+ // Publishable keys: no retry scope errors are permanent
2571
+ if (this.options.publishableKey) {
2572
+ return Promise.reject(error);
2573
+ }
2545
2574
  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
2575
  const hasNotRetried = !originalRequest._hasRetried;
2576
+ // getToken path: retry once with a fresh token on 401
2577
+ if (isAuthError && this.options.getToken && hasNotRetried) {
2578
+ originalRequest._hasRetried = true;
2579
+ try {
2580
+ this.token = yield this.options.getToken();
2581
+ originalRequest.headers.Authorization = `Bearer ${this.token}`;
2582
+ return this.axios(originalRequest);
2583
+ }
2584
+ catch (refreshError) {
2585
+ return Promise.reject(error);
2586
+ }
2587
+ }
2588
+ // Client credentials path: refresh token and retry on 401/403
2589
+ const hasClientCredentials = this.options.clientId && this.options.clientSecret;
2548
2590
  if (isAuthError && hasClientCredentials && hasNotRetried) {
2549
2591
  // Mark request as retried to prevent infinite loops
2550
2592
  originalRequest._hasRetried = true;
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
  }
@@ -4773,6 +4784,21 @@ export class CentraliSDK {
4773
4784
  constructor(options: CentraliSDKOptions) {
4774
4785
  this.options = options;
4775
4786
  this.token = options.token || null;
4787
+
4788
+ // Validate mutually exclusive auth options
4789
+ const authPaths = [
4790
+ options.publishableKey ? 'publishableKey' : null,
4791
+ options.getToken ? 'getToken' : null,
4792
+ (options.clientId || options.clientSecret) ? 'clientId/clientSecret' : null,
4793
+ ].filter(Boolean);
4794
+
4795
+ if (options.publishableKey && authPaths.length > 1) {
4796
+ throw new Error(`Cannot use publishableKey with ${authPaths.filter(p => p !== 'publishableKey').join(', ')}. Use one auth method.`);
4797
+ }
4798
+ if (options.getToken && (options.clientId || options.clientSecret)) {
4799
+ throw new Error('Cannot use getToken with clientId/clientSecret. Use one auth method.');
4800
+ }
4801
+
4776
4802
  const apiUrl = getApiUrl(options.baseUrl);
4777
4803
  this.axios = axios.create({
4778
4804
  baseURL: apiUrl,
@@ -4785,6 +4811,22 @@ export class CentraliSDK {
4785
4811
  // Attach async interceptor to fetch token on first request if needed
4786
4812
  this.axios.interceptors.request.use(
4787
4813
  async (config) => {
4814
+ // Auth path 1: Publishable key — send as x-api-key header, no token logic
4815
+ if (this.options.publishableKey) {
4816
+ config.headers['x-api-key'] = this.options.publishableKey;
4817
+ return config;
4818
+ }
4819
+
4820
+ // Auth path 2: Dynamic token callback (getToken) — call on each request
4821
+ if (this.options.getToken) {
4822
+ this.token = await this.options.getToken();
4823
+ if (this.token) {
4824
+ config.headers.Authorization = `Bearer ${this.token}`;
4825
+ }
4826
+ return config;
4827
+ }
4828
+
4829
+ // Auth path 3: Client credentials — fetch token on first request
4788
4830
  if (!this.token && this.options.clientId && this.options.clientSecret) {
4789
4831
  this.token = await fetchClientToken(
4790
4832
  this.options.clientId,
@@ -4806,11 +4848,29 @@ export class CentraliSDK {
4806
4848
  async (error) => {
4807
4849
  const originalRequest = error.config;
4808
4850
 
4809
- // Only attempt refresh for 401/403 errors when using client credentials
4851
+ // Publishable keys: no retry scope errors are permanent
4852
+ if (this.options.publishableKey) {
4853
+ return Promise.reject(error);
4854
+ }
4855
+
4810
4856
  const isAuthError = error.response?.status === 401 || error.response?.status === 403;
4811
- const hasClientCredentials = this.options.clientId && this.options.clientSecret;
4812
4857
  const hasNotRetried = !originalRequest._hasRetried;
4813
4858
 
4859
+ // getToken path: retry once with a fresh token on 401
4860
+ if (isAuthError && this.options.getToken && hasNotRetried) {
4861
+ originalRequest._hasRetried = true;
4862
+ try {
4863
+ this.token = await this.options.getToken();
4864
+ originalRequest.headers.Authorization = `Bearer ${this.token}`;
4865
+ return this.axios(originalRequest);
4866
+ } catch (refreshError) {
4867
+ return Promise.reject(error);
4868
+ }
4869
+ }
4870
+
4871
+ // Client credentials path: refresh token and retry on 401/403
4872
+ const hasClientCredentials = this.options.clientId && this.options.clientSecret;
4873
+
4814
4874
  if (isAuthError && hasClientCredentials && hasNotRetried) {
4815
4875
  // Mark request as retried to prevent infinite loops
4816
4876
  originalRequest._hasRetried = true;
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.2",
4
4
  "description": "Centrali Node SDK",
5
5
  "main": "dist/index.js",
6
6
  "type": "commonjs",