@mitralab.io/platform-sdk 1.0.6 → 1.0.8

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.mjs CHANGED
@@ -1,4 +1,32 @@
1
+ // src/client.ts
2
+ import { encodePathSegment, expectObject } from "@mitralab.io/sdk-core";
3
+
1
4
  // src/utils/http-client.ts
5
+ var bearerCredentialPattern = /(Bearer\s+)\S+/gi;
6
+ function redactText(value, currentToken) {
7
+ const withoutBearerCredentials = value.replace(bearerCredentialPattern, "$1[REDACTED]");
8
+ return currentToken ? withoutBearerCredentials.split(currentToken).join("[REDACTED]") : withoutBearerCredentials;
9
+ }
10
+ function redactDetails(value, currentToken) {
11
+ if (typeof value === "string") return redactText(value, currentToken);
12
+ if (Array.isArray(value)) return value.map((item) => redactDetails(item, currentToken));
13
+ if (value && typeof value === "object") {
14
+ return Object.fromEntries(
15
+ Object.entries(value).map(([key, entry]) => [
16
+ redactText(key, currentToken),
17
+ redactDetails(entry, currentToken)
18
+ ])
19
+ );
20
+ }
21
+ return value;
22
+ }
23
+ function asErrorPayload(value) {
24
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
25
+ return value;
26
+ }
27
+ function optionalString(value) {
28
+ return typeof value === "string" ? value : void 0;
29
+ }
2
30
  var HttpClient = class {
3
31
  baseUrl;
4
32
  tokenGetter;
@@ -62,8 +90,18 @@ var HttpClient = class {
62
90
  const response = await fetch(url, {
63
91
  method,
64
92
  headers: requestHeaders,
65
- body: body ? JSON.stringify(body) : void 0
93
+ body: body ? JSON.stringify(body) : void 0,
94
+ redirect: "manual"
66
95
  });
96
+ if (response.redirected || response.type === "opaqueredirect") {
97
+ const error = new MitraApiError(
98
+ "Redirected responses are not allowed",
99
+ response.status,
100
+ "REDIRECT_NOT_ALLOWED"
101
+ );
102
+ this.onError?.(error);
103
+ throw error;
104
+ }
67
105
  if (!response.ok) {
68
106
  if (response.status === 401 && !isRetry && this.onUnauthorized) {
69
107
  const refreshed = await this.onUnauthorized();
@@ -72,11 +110,14 @@ var HttpClient = class {
72
110
  }
73
111
  }
74
112
  const errorBody = await response.json().catch(() => ({}));
113
+ const errorPayload = asErrorPayload(errorBody);
114
+ const rawMessage = optionalString(errorPayload.message);
115
+ const rawCode = optionalString(errorPayload.error_code);
75
116
  const error = new MitraApiError(
76
- errorBody.message || `Request failed with status ${response.status}`,
117
+ redactText(rawMessage || `Request failed with status ${response.status}`, token),
77
118
  response.status,
78
- errorBody.error_code,
79
- errorBody
119
+ rawCode === void 0 ? void 0 : redactText(rawCode, token),
120
+ redactDetails(errorBody, token)
80
121
  );
81
122
  this.onError?.(error);
82
123
  throw error;
@@ -157,22 +198,31 @@ var MitraApiError = class extends Error {
157
198
  }
158
199
  };
159
200
 
201
+ // src/core-errors.ts
202
+ var coreErrors = {
203
+ configuration: (message) => new MitraApiError(message, 0, "INVALID_CONFIGURATION"),
204
+ invalidResponse: (message) => new MitraApiError(message, 200, "INVALID_RESPONSE")
205
+ };
206
+
160
207
  // src/modules/auth.ts
208
+ import { createAuthModule } from "@mitralab.io/sdk-core";
161
209
  var AuthModule = class {
162
210
  appId;
163
211
  _currentUser = null;
164
- _accessToken = null;
165
- _refreshToken = null;
212
+ #accessToken = null;
213
+ #refreshToken = null;
166
214
  refreshPromise = null;
167
215
  listeners = /* @__PURE__ */ new Set();
168
216
  storageKey;
169
217
  publicClient;
170
218
  authedClient;
219
+ currentUserApi;
171
220
  constructor(appId, iamBaseUrl) {
172
221
  this.appId = appId;
173
222
  this.storageKey = `mitra_auth_${appId}`;
174
223
  this.publicClient = new HttpClient({ baseUrl: iamBaseUrl, getToken: () => null });
175
- this.authedClient = new HttpClient({ baseUrl: iamBaseUrl, getToken: () => this._accessToken });
224
+ this.authedClient = new HttpClient({ baseUrl: iamBaseUrl, getToken: () => this.#accessToken });
225
+ this.currentUserApi = createAuthModule(this.authedClient, coreErrors);
176
226
  this.loadFromStorage();
177
227
  }
178
228
  /** The currently authenticated user, or null. */
@@ -181,11 +231,11 @@ var AuthModule = class {
181
231
  }
182
232
  /** The current JWT access token, or null. */
183
233
  get accessToken() {
184
- return this._accessToken;
234
+ return this.#accessToken;
185
235
  }
186
236
  /** Whether a user is currently authenticated (local check, not server-validated). */
187
237
  get isAuthenticated() {
188
- return this._currentUser !== null && this._accessToken !== null;
238
+ return this._currentUser !== null && this.#accessToken !== null;
189
239
  }
190
240
  /**
191
241
  * Signs in a user with email and password.
@@ -210,9 +260,9 @@ var AuthModule = class {
210
260
  "/api/v1/auth/login",
211
261
  { ...credentials, appId: this.appId }
212
262
  );
213
- this._accessToken = tokenResponse.accessToken;
214
- this._refreshToken = tokenResponse.refreshToken;
215
- const user = await this.authedClient.get("/api/v1/auth/me");
263
+ this.#accessToken = tokenResponse.accessToken;
264
+ this.#refreshToken = tokenResponse.refreshToken;
265
+ const user = await this.getCurrentUser();
216
266
  this.setAuthState(user, tokenResponse.accessToken, tokenResponse.refreshToken);
217
267
  return user;
218
268
  }
@@ -272,7 +322,7 @@ var AuthModule = class {
272
322
  * ```
273
323
  */
274
324
  async refreshSession() {
275
- if (!this._refreshToken) return false;
325
+ if (!this.#refreshToken) return false;
276
326
  if (this.refreshPromise) return this.refreshPromise;
277
327
  this.refreshPromise = this.doRefresh();
278
328
  try {
@@ -296,9 +346,9 @@ var AuthModule = class {
296
346
  * ```
297
347
  */
298
348
  async me() {
299
- if (!this._accessToken) return null;
349
+ if (!this.#accessToken) return null;
300
350
  try {
301
- const user = await this.authedClient.get("/api/v1/auth/me");
351
+ const user = await this.getCurrentUser();
302
352
  this._currentUser = user;
303
353
  this.saveToStorage();
304
354
  this.notifyListeners();
@@ -339,7 +389,7 @@ var AuthModule = class {
339
389
  * ```
340
390
  */
341
391
  setToken(token, saveToStorage = true) {
342
- this._accessToken = token;
392
+ this.#accessToken = token;
343
393
  if (saveToStorage) {
344
394
  this.saveToStorage();
345
395
  }
@@ -390,11 +440,11 @@ var AuthModule = class {
390
440
  try {
391
441
  const tokenResponse = await this.publicClient.post(
392
442
  "/api/v1/auth/refresh-token",
393
- { refreshToken: this._refreshToken }
443
+ { refreshToken: this.#refreshToken }
394
444
  );
395
- this._accessToken = tokenResponse.accessToken;
396
- this._refreshToken = tokenResponse.refreshToken;
397
- const user = await this.authedClient.get("/api/v1/auth/me");
445
+ this.#accessToken = tokenResponse.accessToken;
446
+ this.#refreshToken = tokenResponse.refreshToken;
447
+ const user = await this.getCurrentUser();
398
448
  this.setAuthState(user, tokenResponse.accessToken, tokenResponse.refreshToken);
399
449
  return true;
400
450
  } catch {
@@ -404,15 +454,19 @@ var AuthModule = class {
404
454
  }
405
455
  setAuthState(user, token, refreshToken) {
406
456
  this._currentUser = user;
407
- this._accessToken = token;
408
- this._refreshToken = refreshToken;
457
+ this.#accessToken = token;
458
+ this.#refreshToken = refreshToken;
409
459
  this.saveToStorage();
410
460
  this.notifyListeners();
411
461
  }
462
+ async getCurrentUser() {
463
+ const user = await this.currentUserApi.me();
464
+ return { ...user, tenantId: user.tenant.id };
465
+ }
412
466
  clearAuthState() {
413
467
  this._currentUser = null;
414
- this._accessToken = null;
415
- this._refreshToken = null;
468
+ this.#accessToken = null;
469
+ this.#refreshToken = null;
416
470
  this.removeFromStorage();
417
471
  this.notifyListeners();
418
472
  }
@@ -432,8 +486,8 @@ var AuthModule = class {
432
486
  this.storageKey,
433
487
  JSON.stringify({
434
488
  user: this._currentUser,
435
- token: this._accessToken,
436
- refreshToken: this._refreshToken
489
+ token: this.#accessToken,
490
+ refreshToken: this.#refreshToken
437
491
  })
438
492
  );
439
493
  } catch {
@@ -446,8 +500,8 @@ var AuthModule = class {
446
500
  if (stored) {
447
501
  const { user, token, refreshToken } = JSON.parse(stored);
448
502
  this._currentUser = user;
449
- this._accessToken = token;
450
- this._refreshToken = refreshToken ?? null;
503
+ this.#accessToken = token;
504
+ this.#refreshToken = refreshToken ?? null;
451
505
  }
452
506
  } catch {
453
507
  this.removeFromStorage();
@@ -463,208 +517,123 @@ var AuthModule = class {
463
517
  };
464
518
 
465
519
  // src/modules/entities.ts
520
+ import {
521
+ createEntitiesModule
522
+ } from "@mitralab.io/sdk-core";
466
523
  var EntitiesModule = class _EntitiesModule {
467
- httpClient;
468
- dataSourceId;
469
- tableProxies = /* @__PURE__ */ new Map();
470
524
  constructor(httpClient, dataSourceId) {
471
525
  this.httpClient = httpClient;
472
- this.dataSourceId = dataSourceId;
526
+ void dataSourceId;
527
+ this.core = createEntitiesModule(httpClient, coreErrors);
473
528
  }
529
+ core;
474
530
  static createProxy(httpClient, dataSourceId) {
475
531
  const instance = new _EntitiesModule(httpClient, dataSourceId);
476
532
  return new Proxy(instance, {
477
- get: (target, prop) => {
478
- if (prop in target) {
479
- return target[prop];
533
+ get(target, property, receiver) {
534
+ if (typeof property !== "string" || property in target) {
535
+ return Reflect.get(target, property, receiver);
480
536
  }
481
- return target.getTable(prop);
537
+ return target.getTable(property);
482
538
  }
483
539
  });
484
540
  }
541
+ /**
542
+ * Preserved for Platform SDK 1.x compatibility.
543
+ * Records now resolve the app from authenticated context instead of a data source path.
544
+ */
485
545
  setDataSourceId(dataSourceId) {
486
- this.dataSourceId = dataSourceId;
487
- this.tableProxies.clear();
546
+ void dataSourceId;
547
+ this.core = createEntitiesModule(this.httpClient, coreErrors);
488
548
  }
489
549
  getTable(tableName) {
490
- if (!this.tableProxies.has(tableName)) {
491
- this.tableProxies.set(tableName, this.createTableAccessor(tableName));
492
- }
493
- return this.tableProxies.get(tableName);
494
- }
495
- createTableAccessor(tableName) {
496
- const basePath = `/api/v1/data-sources/${this.dataSourceId}/tables/${tableName}/records`;
497
- return {
498
- list: async (sortOrOptions, limit, skip, fields) => {
499
- let params;
500
- if (typeof sortOrOptions === "string" || sortOrOptions === void 0) {
501
- params = {
502
- sort: sortOrOptions,
503
- limit,
504
- skip,
505
- fields: fields?.join(",")
506
- };
507
- } else {
508
- params = {
509
- sort: sortOrOptions.sort,
510
- limit: sortOrOptions.limit,
511
- skip: sortOrOptions.skip,
512
- fields: sortOrOptions.fields?.join(",")
513
- };
514
- }
515
- const response = await this.httpClient.get(basePath, params);
516
- return response.data;
517
- },
518
- filter: async (query, sort, limit, skip, fields) => {
519
- const params = {
520
- q: JSON.stringify(query),
521
- sort,
522
- limit,
523
- skip,
524
- fields: fields?.join(",")
525
- };
526
- const response = await this.httpClient.get(basePath, params);
527
- return response.data;
528
- },
529
- get: async (id) => {
530
- return this.httpClient.get(`${basePath}/${id}`);
531
- },
532
- create: async (data) => {
533
- return this.httpClient.post(basePath, data);
534
- },
535
- update: async (id, data) => {
536
- return this.httpClient.put(`${basePath}/${id}`, data);
537
- },
538
- delete: async (id) => {
539
- return this.httpClient.delete(`${basePath}/${id}`);
540
- },
541
- deleteMany: async (query) => {
542
- return this.httpClient.delete(basePath, {
543
- q: JSON.stringify(query)
544
- });
545
- },
546
- bulkCreate: async (data) => {
547
- return this.httpClient.post(`${basePath}/bulk`, data);
548
- }
549
- };
550
+ return this.core.getTable(tableName);
550
551
  }
551
552
  };
552
553
 
553
554
  // src/modules/functions.ts
555
+ import {
556
+ createFunctionsModule
557
+ } from "@mitralab.io/sdk-core";
554
558
  var FunctionsModule = class {
555
- httpClient;
559
+ core;
556
560
  constructor(httpClient) {
557
- this.httpClient = httpClient;
561
+ this.core = createFunctionsModule(httpClient, { emptyInput: "omit-body" }, coreErrors);
558
562
  }
559
563
  /**
560
- * Executes a serverless function by ID.
561
- *
562
- * Triggers the function's current published version with the provided input.
563
- *
564
- * @param functionId - UUID of the function to execute.
565
- * @param input - Input data to pass to the function.
566
- * @returns The execution result with status, output, and metadata.
567
- * @throws {MitraApiError} On function not found (404) or unauthorized (401).
568
- *
569
- * @example
570
- * ```typescript
571
- * const execution = await mitra.functions.execute('fn-id', { key: 'value' });
572
- * console.log(execution.status, execution.output);
573
- * ```
564
+ * Executes a Function using the Platform SDK 1.x server-default invocation semantics.
565
+ * The runtime SDK uses an explicit invocation header instead.
574
566
  */
575
567
  async execute(functionId, input) {
576
- return this.httpClient.post(
577
- `/api/v1/functions/${functionId}/execute`,
578
- input ? { input } : void 0
579
- );
568
+ const execution = await this.core.execute(functionId, input);
569
+ if (execution.input === null) {
570
+ throw coreErrors.invalidResponse(
571
+ "Function execution response has an invalid input field"
572
+ );
573
+ }
574
+ return { ...execution, input: execution.input };
580
575
  }
581
576
  };
582
577
 
583
578
  // src/modules/integration.ts
579
+ import {
580
+ createIntegrationModule
581
+ } from "@mitralab.io/sdk-core";
584
582
  var IntegrationModule = class {
585
- httpClient;
583
+ core;
586
584
  constructor(httpClient) {
587
- this.httpClient = httpClient;
585
+ this.core = createIntegrationModule(httpClient, coreErrors);
588
586
  }
589
- /**
590
- * Executes a pre-defined integration resource by ID.
591
- *
592
- * The resource's endpoint, method, and body are resolved server-side
593
- * using the provided parameters. Only declared parameters can be passed.
594
- *
595
- * @param resourceId - UUID of the integration resource.
596
- * @param params - Named parameters declared in the resource's params schema.
597
- * @returns Proxy result with status, headers, body, and execution metadata.
598
- * @throws {MitraApiError} On resource not found (404) or external API failure.
599
- *
600
- * @example
601
- * ```typescript
602
- * const result = await mitra.integration.executeResource('resource-id', {
603
- * descricao: 'Notebook',
604
- * limit: 10,
605
- * });
606
- * console.log(result.body);
607
- * ```
608
- */
609
- async executeResource(resourceId, params) {
610
- return this.httpClient.post(
611
- `/api/v1/proxy/resources/${resourceId}/execute`,
612
- { params }
613
- );
587
+ executeResource(resourceId, params) {
588
+ return this.core.executeResource(resourceId, params);
614
589
  }
615
- /**
616
- * Executes a proxied HTTP request through an integration config.
617
- *
618
- * The Mitra server handles authentication and injects credentials automatically.
619
- * Note: integrations configured with RESOURCE_ONLY mode will block direct proxy access.
620
- *
621
- * @param configId - UUID of the integration config.
622
- * @param request - The HTTP request to proxy (method, endpoint, body, etc.).
623
- * @returns Proxy result with status, headers, body, and execution metadata.
624
- * @throws {MitraApiError} On config not found (404) or external API failure.
625
- */
626
- async execute(configId, request) {
627
- return this.httpClient.post(
628
- `/api/v1/proxy/template-configs/${configId}/execute`,
629
- { ...request, source: "SDK" }
630
- );
590
+ execute(configId, request) {
591
+ return this.core.execute(configId, request);
631
592
  }
632
593
  };
633
594
 
634
595
  // src/modules/queries.ts
596
+ import {
597
+ createQueriesModule
598
+ } from "@mitralab.io/sdk-core";
635
599
  var QueriesModule = class {
636
- httpClient;
637
600
  dataSourceId = "";
601
+ core;
638
602
  constructor(httpClient) {
639
- this.httpClient = httpClient;
603
+ this.core = createQueriesModule(httpClient, () => this.dataSourceId, coreErrors);
640
604
  }
641
- /** @internal Called by client.init() to set the resolved data source. */
605
+ /** Called by `client.init()` to set the app's resolved data source. */
642
606
  setDataSourceId(dataSourceId) {
643
607
  this.dataSourceId = dataSourceId;
644
608
  }
645
- /**
646
- * Executes a named query.
647
- *
648
- * @param id - UUID of the custom query.
649
- * @param parameters - Named parameters for the prepared statement.
650
- * @returns Query result with rows and affected row count.
651
- * @throws {MitraApiError} On query not found (404).
652
- *
653
- * @example
654
- * ```typescript
655
- * const result = await mitra.queries.execute('query-id', { status: 'active' });
656
- * console.log(`Found ${result.rows.length} rows`);
657
- * ```
658
- */
659
609
  async execute(id, parameters) {
660
- return this.httpClient.post(
661
- `/api/v1/custom-queries/${id}/execute`,
662
- { datasourceId: this.dataSourceId, parameters }
663
- );
610
+ const result = await this.core.execute(id, parameters);
611
+ return { ...result, affectedRows: result.affectedRows ?? null };
664
612
  }
665
613
  };
666
614
 
667
615
  // src/client.ts
616
+ function expectAppInfoResponse(value) {
617
+ const response = expectObject(
618
+ value,
619
+ "App info response",
620
+ coreErrors
621
+ );
622
+ if (typeof response.dataSourceId !== "string" || !response.dataSourceId.trim()) {
623
+ throw coreErrors.invalidResponse(
624
+ "App info response has an invalid dataSourceId field"
625
+ );
626
+ }
627
+ if (typeof response.allowSignup !== "boolean") {
628
+ throw coreErrors.invalidResponse(
629
+ "App info response has an invalid allowSignup field"
630
+ );
631
+ }
632
+ return {
633
+ dataSourceId: response.dataSourceId,
634
+ allowSignup: response.allowSignup
635
+ };
636
+ }
668
637
  function createClient(config) {
669
638
  const { appId, apiUrl, onError } = config;
670
639
  const iamUrl = `${apiUrl}/iam`;
@@ -708,8 +677,10 @@ function createClient(config) {
708
677
  baseUrl: codeStudioUrl,
709
678
  getToken: () => null
710
679
  });
711
- const appInfo = await publicClient.get(
712
- `/api/v1/apps/${appId}/info`
680
+ const appInfo = expectAppInfoResponse(
681
+ await publicClient.get(
682
+ `/api/v1/apps/${encodePathSegment(appId, "appId", coreErrors)}/info`
683
+ )
713
684
  );
714
685
  entitiesModule.setDataSourceId(appInfo.dataSourceId);
715
686
  queriesModule.setDataSourceId(appInfo.dataSourceId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mitralab.io/platform-sdk",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "JavaScript/TypeScript SDK for building apps on the Mitra Platform",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -19,9 +19,12 @@
19
19
  "build": "tsup src/index.ts --format cjs,esm --dts --clean",
20
20
  "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
21
21
  "lint": "eslint src --ext .ts",
22
- "test": "vitest run",
22
+ "typecheck": "tsc --noEmit",
23
+ "test": "vitest run --coverage",
23
24
  "test:watch": "vitest",
24
- "prepublishOnly": "npm run build"
25
+ "smoke:package": "node scripts/smoke-package.mjs",
26
+ "check": "npm run lint && npm run typecheck && npm test && npm run build && npm run smoke:package",
27
+ "prepublishOnly": "npm run check"
25
28
  },
26
29
  "keywords": [
27
30
  "mitra",
@@ -42,18 +45,26 @@
42
45
  "bugs": {
43
46
  "url": "https://github.com/mitralab-dev/mitra-platform-sdk/issues"
44
47
  },
48
+ "dependencies": {
49
+ "@mitralab.io/sdk-core": "^0.1.0"
50
+ },
45
51
  "devDependencies": {
46
- "@types/node": "^25.3.0",
47
- "@vitest/coverage-v8": "^4.1.2",
48
- "eslint": "^10.0.1",
49
- "tsup": "^8.0.1",
50
- "typescript": "^6.0.2",
51
- "vitest": "^4.1.2"
52
+ "@eslint/js": "^9.39.1",
53
+ "@types/node": "^22.15.0",
54
+ "@vitest/coverage-v8": "^3.2.4",
55
+ "eslint": "^9.39.1",
56
+ "globals": "^16.5.0",
57
+ "tsup": "^8.5.1",
58
+ "typescript": "~5.9.3",
59
+ "typescript-eslint": "~8.55.0",
60
+ "vite": "~6.4.3",
61
+ "vitest": "^3.2.4"
52
62
  },
53
63
  "engines": {
54
64
  "node": ">=18.0.0"
55
65
  },
56
66
  "overrides": {
57
- "rollup": "^4.59.0"
67
+ "rollup": "^4.59.0",
68
+ "test-exclude": "7.0.1"
58
69
  }
59
70
  }