@centrali-io/centrali-sdk 4.4.0 → 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 +21 -5
- package/dist/index.js +44 -2
- package/index.ts +62 -2
- package/package.json +1 -1
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
|
|
43
|
+
The SDK supports three authentication methods. See the [full authentication guide](../docs/sdk/authentication.md) for details.
|
|
44
44
|
|
|
45
|
-
###
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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;
|