@centrali-io/centrali-sdk 5.5.1 → 6.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.
Files changed (49) hide show
  1. package/README.md +164 -14
  2. package/dist/index.d.ts +1807 -878
  3. package/dist/index.js +9153 -4076
  4. package/index.ts +61 -7152
  5. package/package.json +10 -3
  6. package/query-types.ts +83 -2
  7. package/scripts/smoke-types.ts +145 -5
  8. package/src/client.ts +1507 -0
  9. package/src/internal/auth.ts +35 -0
  10. package/src/internal/deprecation.ts +11 -0
  11. package/src/internal/error.ts +90 -0
  12. package/src/internal/paths.ts +456 -0
  13. package/src/internal/queryGuard.ts +21 -0
  14. package/src/managers/allowedDomains.ts +90 -0
  15. package/src/managers/anomalyInsights.ts +215 -0
  16. package/src/managers/auditLog.ts +105 -0
  17. package/src/managers/collections.ts +197 -0
  18. package/src/managers/files.ts +182 -0
  19. package/src/managers/functionRuns.ts +229 -0
  20. package/src/managers/functions.ts +171 -0
  21. package/src/managers/orchestrationRuns.ts +122 -0
  22. package/src/managers/orchestrations.ts +297 -0
  23. package/src/managers/query.ts +199 -0
  24. package/src/managers/records.ts +186 -0
  25. package/src/managers/smartQueries.ts +374 -0
  26. package/src/managers/structures.ts +205 -0
  27. package/src/managers/triggers.ts +349 -0
  28. package/src/managers/validation.ts +303 -0
  29. package/src/managers/webhookSubscriptions.ts +206 -0
  30. package/src/realtime/manager.ts +292 -0
  31. package/src/types/allowedDomains.ts +29 -0
  32. package/src/types/auth.ts +83 -0
  33. package/src/types/common.ts +57 -0
  34. package/src/types/compute.ts +145 -0
  35. package/src/types/insights.ts +113 -0
  36. package/src/types/orchestrations.ts +460 -0
  37. package/src/types/realtime.ts +403 -0
  38. package/src/types/records.ts +261 -0
  39. package/src/types/search.ts +44 -0
  40. package/src/types/smartQueries.ts +303 -0
  41. package/src/types/structures.ts +203 -0
  42. package/src/types/triggers.ts +122 -0
  43. package/src/types/validation.ts +167 -0
  44. package/src/types/webhooks.ts +114 -0
  45. package/src/urls.ts +33 -0
  46. package/dist/query-types.d.ts +0 -187
  47. package/dist/query-types.js +0 -137
  48. package/dist/scripts/smoke-types.d.ts +0 -12
  49. package/dist/scripts/smoke-types.js +0 -102
@@ -0,0 +1,292 @@
1
+ import { EventSource as EventSourcePolyfill } from 'eventsource';
2
+ import { getRealtimeUrl } from '../urls';
3
+ import { emitDeprecationWarning } from '../internal/deprecation';
4
+ import type {
5
+ RealtimeSubscribeOptions,
6
+ RealtimeSubscription,
7
+ RealtimeRecordEvent,
8
+ RealtimeCloseEvent,
9
+ } from '../types/realtime';
10
+
11
+ // Use native EventSource in browser, polyfill in Node.js
12
+ const EventSourceImpl: typeof EventSource = typeof EventSource !== 'undefined'
13
+ ? EventSource
14
+ : EventSourcePolyfill as unknown as typeof EventSource;
15
+
16
+ /**
17
+ * Generate the SSE endpoint path for a workspace.
18
+ * Matches: services/backend/realtime/internal/sse/handler.go ServeHTTP route
19
+ */
20
+ function getRealtimeEventPath(workspaceSlug: string): string {
21
+ return `/workspace/${workspaceSlug}/events`;
22
+ }
23
+
24
+ /**
25
+ * Internal configuration for realtime connections.
26
+ */
27
+ interface RealtimeConfig {
28
+ /** Maximum reconnection attempts (default: 10) */
29
+ maxReconnectAttempts: number;
30
+ /** Initial reconnect delay in ms (default: 1000) */
31
+ initialReconnectDelayMs: number;
32
+ /** Maximum reconnect delay in ms (default: 30000) */
33
+ maxReconnectDelayMs: number;
34
+ }
35
+
36
+ /**
37
+ * Default realtime configuration.
38
+ */
39
+ const DEFAULT_REALTIME_CONFIG: RealtimeConfig = {
40
+ maxReconnectAttempts: 10,
41
+ initialReconnectDelayMs: 1000,
42
+ maxReconnectDelayMs: 30000,
43
+ };
44
+
45
+ /**
46
+ * RealtimeManager handles SSE connections to the Centrali Realtime Service.
47
+ * Provides automatic reconnection with exponential backoff.
48
+ *
49
+ * Usage:
50
+ * ```ts
51
+ * const realtime = new RealtimeManager(baseUrl, workspaceSlug, () => client.getToken());
52
+ * const sub = realtime.subscribe({
53
+ * structures: ['order'],
54
+ * events: ['record_created', 'record_updated'],
55
+ * onEvent: (event) => console.log(event),
56
+ * onError: (error) => console.error(error),
57
+ * });
58
+ * // Later: sub.unsubscribe();
59
+ * ```
60
+ */
61
+ export class RealtimeManager {
62
+ private baseUrl: string;
63
+ private workspaceSlug: string;
64
+ private getToken: () => string | null | Promise<string | null>;
65
+ private config: RealtimeConfig;
66
+
67
+ constructor(
68
+ baseUrl: string,
69
+ workspaceSlug: string,
70
+ getToken: () => string | null | Promise<string | null>,
71
+ config?: Partial<RealtimeConfig>
72
+ ) {
73
+ this.baseUrl = baseUrl;
74
+ this.workspaceSlug = workspaceSlug;
75
+ this.getToken = getToken;
76
+ this.config = { ...DEFAULT_REALTIME_CONFIG, ...config };
77
+ }
78
+
79
+ /**
80
+ * Subscribe to realtime events for the workspace.
81
+ *
82
+ * IMPORTANT: Initial Sync Pattern
83
+ * Realtime delivers only new events after connection. For dashboards and lists:
84
+ * 1. Fetch current records first
85
+ * 2. Subscribe to realtime
86
+ * 3. Apply diffs while UI shows the snapshot
87
+ *
88
+ * @param options - Subscription options
89
+ * @returns Subscription handle with unsubscribe() method
90
+ */
91
+ public subscribe(options: RealtimeSubscribeOptions): RealtimeSubscription {
92
+ let eventSource: EventSource | null = null;
93
+ let unsubscribed = false;
94
+ let connected = false;
95
+ let reconnectAttempt = 0;
96
+ let reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
97
+
98
+ const connect = async () => {
99
+ // Zombie loop prevention: don't reconnect if unsubscribed
100
+ if (unsubscribed) {
101
+ return;
102
+ }
103
+
104
+ try {
105
+ // Get token (may be async for client credentials flow)
106
+ const token = await Promise.resolve(this.getToken());
107
+ if (!token) {
108
+ options.onError?.({
109
+ code: 'MISSING_TOKEN',
110
+ message: 'No authentication token available',
111
+ recoverable: false,
112
+ });
113
+ return;
114
+ }
115
+
116
+ // Build SSE URL with query params
117
+ const realtimeBaseUrl = getRealtimeUrl(this.baseUrl);
118
+ const path = getRealtimeEventPath(this.workspaceSlug);
119
+ const url = new URL(`${realtimeBaseUrl}${path}`);
120
+
121
+ // Add access token
122
+ url.searchParams.set('access_token', token);
123
+
124
+ // Add collection filter
125
+ if (options.collections?.length) {
126
+ url.searchParams.set('collections', options.collections.join(','));
127
+ } else if (options.structures?.length) {
128
+ emitDeprecationWarning("The 'structures' filter option is deprecated. Use 'collections' instead.");
129
+ url.searchParams.set('collections', options.structures.join(','));
130
+ }
131
+
132
+ // Add event type filter
133
+ if (options.events?.length) {
134
+ url.searchParams.set('events', options.events.join(','));
135
+ }
136
+
137
+ // Add CFL filter
138
+ if (options.filter) {
139
+ url.searchParams.set('filter', options.filter);
140
+ }
141
+
142
+ // Add compute function filters
143
+ if (options.jobId) {
144
+ url.searchParams.set('jobId', options.jobId);
145
+ }
146
+ if (options.triggerType) {
147
+ url.searchParams.set('triggerType', options.triggerType);
148
+ }
149
+ if (options.functionId) {
150
+ url.searchParams.set('functionId', options.functionId);
151
+ }
152
+
153
+ // Add orchestration filters
154
+ if (options.orchestrationId) {
155
+ url.searchParams.set('orchestrationId', options.orchestrationId);
156
+ }
157
+ if (options.runId) {
158
+ url.searchParams.set('runId', options.runId);
159
+ }
160
+
161
+ // Create EventSource (uses polyfill in Node.js)
162
+ eventSource = new EventSourceImpl(url.toString());
163
+
164
+ // Handle connection open
165
+ eventSource.onopen = () => {
166
+ if (unsubscribed) {
167
+ eventSource?.close();
168
+ return;
169
+ }
170
+ connected = true;
171
+ reconnectAttempt = 0;
172
+ options.onConnected?.();
173
+ };
174
+
175
+ // Handle record events - server sends all record events as 'message' type
176
+ // The event.event field inside the payload contains the actual type
177
+ // (record_created, record_updated, record_deleted)
178
+ eventSource.addEventListener('message', (e: MessageEvent) => {
179
+ if (unsubscribed) return;
180
+ try {
181
+ const event = JSON.parse(e.data) as RealtimeRecordEvent;
182
+ options.onEvent(event);
183
+ } catch (err) {
184
+ options.onError?.({
185
+ code: 'PARSE_ERROR',
186
+ message: `Failed to parse event: ${err}`,
187
+ recoverable: true,
188
+ });
189
+ }
190
+ });
191
+
192
+ // Handle close event from server
193
+ eventSource.addEventListener('close', (e: MessageEvent) => {
194
+ if (unsubscribed) return;
195
+ try {
196
+ const closeEvent = JSON.parse(e.data) as RealtimeCloseEvent;
197
+ connected = false;
198
+ options.onDisconnected?.(closeEvent.reason);
199
+
200
+ // Reconnect if server says to
201
+ if (closeEvent.reconnect && !unsubscribed) {
202
+ scheduleReconnect();
203
+ }
204
+ } catch {
205
+ // Ignore parse errors for close events
206
+ }
207
+ });
208
+
209
+ // Handle errors
210
+ eventSource.onerror = () => {
211
+ if (unsubscribed) {
212
+ eventSource?.close();
213
+ return;
214
+ }
215
+
216
+ connected = false;
217
+ eventSource?.close();
218
+ eventSource = null;
219
+
220
+ // EventSource error events don't provide much detail
221
+ // The connection will be closed, so we notify and potentially reconnect
222
+ options.onDisconnected?.('connection_error');
223
+ options.onError?.({
224
+ code: 'CONNECTION_ERROR',
225
+ message: 'Connection to realtime service failed',
226
+ recoverable: true,
227
+ });
228
+
229
+ scheduleReconnect();
230
+ };
231
+
232
+ } catch (err) {
233
+ options.onError?.({
234
+ code: 'CONNECTION_ERROR',
235
+ message: `Failed to connect: ${err}`,
236
+ recoverable: true,
237
+ });
238
+ scheduleReconnect();
239
+ }
240
+ };
241
+
242
+ const scheduleReconnect = () => {
243
+ // Zombie loop prevention
244
+ if (unsubscribed) return;
245
+
246
+ reconnectAttempt++;
247
+ if (reconnectAttempt > this.config.maxReconnectAttempts) {
248
+ options.onError?.({
249
+ code: 'CONNECTION_ERROR',
250
+ message: `Max reconnection attempts (${this.config.maxReconnectAttempts}) exceeded`,
251
+ recoverable: false,
252
+ });
253
+ return;
254
+ }
255
+
256
+ // Exponential backoff with jitter
257
+ const delay = Math.min(
258
+ this.config.initialReconnectDelayMs * Math.pow(2, reconnectAttempt - 1),
259
+ this.config.maxReconnectDelayMs
260
+ );
261
+ const jitter = Math.random() * 0.3 * delay; // 0-30% jitter
262
+
263
+ reconnectTimeout = setTimeout(() => {
264
+ if (!unsubscribed) {
265
+ connect();
266
+ }
267
+ }, delay + jitter);
268
+ };
269
+
270
+ // Start connection
271
+ connect();
272
+
273
+ // Return subscription handle
274
+ return {
275
+ unsubscribe: () => {
276
+ unsubscribed = true;
277
+ connected = false;
278
+ if (reconnectTimeout) {
279
+ clearTimeout(reconnectTimeout);
280
+ reconnectTimeout = null;
281
+ }
282
+ if (eventSource) {
283
+ eventSource.close();
284
+ eventSource = null;
285
+ }
286
+ },
287
+ get connected() {
288
+ return connected;
289
+ },
290
+ };
291
+ }
292
+ }
@@ -0,0 +1,29 @@
1
+ // =====================================================
2
+ // Allowed Domains Types
3
+ // =====================================================
4
+
5
+ /**
6
+ * An allowed domain entry for compute function external calls.
7
+ */
8
+ export interface AllowedDomain {
9
+ id: string;
10
+ domain: string;
11
+ createdAt: string;
12
+ createdBy: string;
13
+ }
14
+
15
+ /**
16
+ * Response from listing allowed domains.
17
+ */
18
+ export interface AllowedDomainsListResponse {
19
+ data: AllowedDomain[];
20
+ meta: { total: number };
21
+ }
22
+
23
+ /**
24
+ * Options for adding an allowed domain.
25
+ */
26
+ export interface AddAllowedDomainOptions {
27
+ /** The domain to allow (e.g., 'api.example.com'). No protocol prefix. */
28
+ domain: string;
29
+ }
@@ -0,0 +1,83 @@
1
+ // =====================================================
2
+ // Authorization Types (BYOT - Bring Your Own Token)
3
+ // =====================================================
4
+
5
+ /**
6
+ * Resource category for authorization.
7
+ * - 'workspace': Workspace-level resources (e.g., settings, members)
8
+ * - 'structure': Structure-level resources (e.g., specific records)
9
+ * - 'custom': Custom resources defined by the user for AuthZ-as-a-Service
10
+ */
11
+ export type ResourceCategory = 'workspace' | 'structure' | 'custom';
12
+
13
+ /**
14
+ * Options for authorization check.
15
+ * Use this when authorizing access using an external IdP token (BYOT).
16
+ */
17
+ export interface CheckAuthorizationOptions {
18
+ /**
19
+ * The JWT token from your external identity provider (e.g., Clerk, Auth0, Okta).
20
+ * The token will be validated against the configured external auth provider.
21
+ */
22
+ token: string;
23
+
24
+ /**
25
+ * The resource being accessed.
26
+ * Can be a Centrali system resource (e.g., 'records', 'files') or a custom
27
+ * resource you've defined for AuthZ-as-a-Service (e.g., 'orders', 'invoices').
28
+ */
29
+ resource: string;
30
+
31
+ /**
32
+ * The action being performed on the resource.
33
+ * Common actions: 'create', 'read', 'update', 'delete', 'admin'
34
+ * You can also define custom actions (e.g., 'approve', 'publish').
35
+ */
36
+ action: string;
37
+
38
+ /**
39
+ * Resource category for authorization evaluation.
40
+ * - 'workspace': Workspace-level resources
41
+ * - 'structure': Structure-level resources
42
+ * - 'custom': Custom resources for AuthZ-as-a-Service
43
+ * @default 'custom'
44
+ */
45
+ resourceCategory?: ResourceCategory;
46
+
47
+ /**
48
+ * Optional context data for policy evaluation.
49
+ * This data becomes available as `request_metadata` in policies.
50
+ *
51
+ * @example
52
+ * // Policy can reference: request_metadata.orderId, request_metadata.amount
53
+ * context: {
54
+ * orderId: 'order-123',
55
+ * amount: 50000,
56
+ * department: 'sales'
57
+ * }
58
+ */
59
+ context?: Record<string, unknown>;
60
+ }
61
+
62
+ /**
63
+ * Result of an authorization check.
64
+ */
65
+ export interface AuthorizationResult {
66
+ /**
67
+ * Whether the action is allowed.
68
+ */
69
+ allowed: boolean;
70
+
71
+ /**
72
+ * The decision from the policy evaluator.
73
+ * - 'allow': Access granted
74
+ * - 'deny': Access denied
75
+ * - 'not_applicable': No matching policy found
76
+ */
77
+ decision: 'allow' | 'deny' | 'not_applicable';
78
+
79
+ /**
80
+ * Human-readable message explaining the decision.
81
+ */
82
+ message?: string;
83
+ }
@@ -0,0 +1,57 @@
1
+ import type { AxiosRequestConfig } from 'axios';
2
+
3
+ /**
4
+ * Options for initializing the Centrali SDK client.
5
+ */
6
+ export interface CentraliSDKOptions {
7
+ /** Base URL of Centrali (e.g. https://centrali.io). The SDK automatically uses api.centrali.io for API calls. */
8
+ baseUrl: string;
9
+ workspaceId: string;
10
+
11
+ // Auth path 1: Publishable key (frontend apps — safe to expose in browser code)
12
+ /** Publishable key for frontend access. Sent as x-api-key header. No token refresh needed. */
13
+ publishableKey?: string;
14
+
15
+ // Auth path 2: Bearer token (existing) + dynamic token callback (new)
16
+ /** Optional initial bearer token for authentication */
17
+ token?: string;
18
+ /** Optional callback to dynamically fetch a fresh token before each request (e.g., for Clerk, Auth0) */
19
+ getToken?: () => Promise<string>;
20
+
21
+ // Auth path 3: Service account (server-side only — never use in browser)
22
+ /** Optional OAuth2 client credentials */
23
+ clientId?: string;
24
+ clientSecret?: string;
25
+
26
+ /** Optional custom axios config */
27
+ axiosConfig?: AxiosRequestConfig;
28
+ }
29
+
30
+ /**
31
+ * Generic API response wrapper.
32
+ */
33
+ export interface ApiResponse<T> {
34
+ data: T;
35
+ meta?: Record<string, any>;
36
+ error?: any
37
+ id?: string;
38
+ createdAt?: string;
39
+ updatedAt?: string;
40
+ }
41
+
42
+ /**
43
+ * Paginated API response wrapper.
44
+ */
45
+ export interface PaginatedResponse<T> {
46
+ /** Data items */
47
+ data: T[];
48
+ /** Pagination metadata */
49
+ meta: {
50
+ /** Total number of items */
51
+ total: number;
52
+ /** Current page number */
53
+ page: number;
54
+ /** Items per page */
55
+ pageSize: number;
56
+ };
57
+ }
@@ -0,0 +1,145 @@
1
+ // =====================================================
2
+ // Compute Function Types (Configuration-as-Code)
3
+ // =====================================================
4
+
5
+ /**
6
+ * Compute function definition.
7
+ */
8
+ export interface ComputeFunction {
9
+ id: string;
10
+ name: string;
11
+ code: string;
12
+ description?: string;
13
+ workspaceSlug: string;
14
+ createdBy: string;
15
+ updatedBy?: string;
16
+ timeoutMs?: number;
17
+ createdAt: string;
18
+ updatedAt: string;
19
+ }
20
+
21
+ /**
22
+ * Input for creating a new compute function.
23
+ */
24
+ export interface CreateComputeFunctionInput {
25
+ name: string;
26
+ code: string;
27
+ description?: string;
28
+ timeoutMs?: number;
29
+ }
30
+
31
+ /**
32
+ * Input for updating an existing compute function.
33
+ */
34
+ export interface UpdateComputeFunctionInput {
35
+ name?: string;
36
+ description?: string;
37
+ code?: string;
38
+ timeoutMs?: number;
39
+ }
40
+
41
+ /**
42
+ * Options for listing compute functions.
43
+ */
44
+ export interface ListComputeFunctionsOptions {
45
+ page?: number;
46
+ limit?: number;
47
+ search?: string;
48
+ searchField?: string;
49
+ }
50
+
51
+ /**
52
+ * Input for test-executing a compute function without saving.
53
+ */
54
+ export interface TestComputeFunctionInput {
55
+ code: string;
56
+ params?: Record<string, any>;
57
+ timeoutMs?: number;
58
+ }
59
+
60
+ /**
61
+ * Result from test-executing a compute function.
62
+ */
63
+ export interface TestComputeFunctionResult {
64
+ success: boolean;
65
+ output?: any;
66
+ duration_ms?: number;
67
+ logs?: string[];
68
+ error?: string;
69
+ }
70
+
71
+ // =====================================================
72
+ // Function Run Types
73
+ // =====================================================
74
+
75
+ /**
76
+ * Execution source for a function run.
77
+ */
78
+ export type FunctionRunExecutionSource = 'trigger' | 'rerun' | 'manual' | 'scheduled' | 'http-trigger' | 'orchestration' | 'endpoint';
79
+
80
+ /**
81
+ * Status of a function run.
82
+ */
83
+ export type FunctionRunStatus = 'pending' | 'running' | 'completed' | 'failure' | 'timeout';
84
+
85
+ /**
86
+ * A function run record representing a single execution of a compute function.
87
+ */
88
+ export interface FunctionRun {
89
+ id: string;
90
+ createdBy: string;
91
+ functionId: string;
92
+ workspaceSlug: string;
93
+ jobId: string;
94
+ status: FunctionRunStatus;
95
+ runData: any | null;
96
+ startedAt: string;
97
+ endedAt?: string | null;
98
+ createdAt?: string;
99
+ updatedAt?: string;
100
+ executionId: string;
101
+ triggerId?: string | null;
102
+ triggerType?: string | null;
103
+ orchestrationRunId?: string | null;
104
+ orchestrationStepId?: string | null;
105
+ originalRunId?: string | null;
106
+ isRerun: boolean;
107
+ rerunCount: number;
108
+ rerunBy?: string | null;
109
+ rerunReason?: string | null;
110
+ functionCodeHash?: string | null;
111
+ functionVersion?: string | null;
112
+ executionSource: FunctionRunExecutionSource;
113
+ memoryUsageBytes?: number | null;
114
+ cpuUsageSeconds?: number | null;
115
+ errorCode?: string | null;
116
+ errorMessage?: string | null;
117
+ dataStrippedAt?: string | null;
118
+ archiveStorageAddress?: string | null;
119
+ archivedAt?: string | null;
120
+ }
121
+
122
+ /**
123
+ * Options for listing function runs by trigger or function.
124
+ */
125
+ export interface ListFunctionRunsOptions {
126
+ page?: number;
127
+ limit?: number;
128
+ status?: FunctionRunStatus;
129
+ }
130
+
131
+ /**
132
+ * Status of a compute job in the execution pipeline.
133
+ */
134
+ export type ComputeJobStatus = 'queued' | 'running' | 'completed' | 'failed';
135
+
136
+ /**
137
+ * A compute job status response from the job status endpoint.
138
+ * Represents the state of an async compute execution.
139
+ */
140
+ export interface ComputeJobStatusResponse {
141
+ jobId: string;
142
+ status: ComputeJobStatus;
143
+ returnValue?: any;
144
+ failedReason?: string | null;
145
+ }