@mitralab.io/functions-sdk 0.1.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ ## 0.1.0
6
+
7
+ - Add environment-based Server Function client configuration.
8
+ - Add current user, entity, custom query, Server Function, and integration APIs.
9
+ - Add typed errors, request timeout, credential redaction, and package release automation.
10
+ - Compose shared API contracts and modules from `@mitralab.io/sdk-core`.
11
+ - Add commit- and digest-pinned SDK parity gates backed by the installed Core corpus.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mitra Platform
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,186 @@
1
+ # Mitra Functions SDK for JavaScript
2
+
3
+ JavaScript and TypeScript SDK for code running inside Mitra Server Functions. It exposes the application-scoped Platform APIs that a function commonly needs without bringing browser authentication into the runtime.
4
+
5
+ The SDK uses the short-lived platform access token injected into the function runtime. It does not log in users, persist credentials, refresh tokens, use API keys, or retry requests automatically.
6
+
7
+ Shared API contracts and modules come from `@mitralab.io/sdk-core`. This package owns only the Server Function runtime adapter: environment configuration, token headers, timeout, credential redaction, redirect refusal, and one-attempt request policy. The core never receives or stores the token.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @mitralab.io/functions-sdk
13
+ ```
14
+
15
+ Node.js 18 or newer is required.
16
+
17
+ ## Runtime integration status
18
+
19
+ The SDK is ready for the target runtime contract, but Functions and Sandbox do not inject it yet. Until that integration is delivered, these variables must be supplied explicitly in local or controlled execution environments:
20
+
21
+ - `MITRA_API_URL`
22
+ - `MITRA_PLATFORM_ACCESS_TOKEN`
23
+ - `MITRA_APP_ID`
24
+ - `MITRA_DATA_SOURCE_ID`, when already resolved by the runtime
25
+
26
+ Do not treat user-defined function secrets as the runtime credential channel. The pending runtime integration must inject the platform access token separately from user secrets.
27
+
28
+ ## Quick start
29
+
30
+ ```typescript
31
+ import { createClient } from "@mitralab.io/functions-sdk"
32
+
33
+ const mitra = createClient()
34
+
35
+ export async function handler(input: { orderId: string }) {
36
+ const order = await mitra.entities.Order.get(input.orderId)
37
+ return { order }
38
+ }
39
+ ```
40
+
41
+ Once the runtime integration is available, `createClient()` reads the process environment by default. For local development or explicit dependency injection, pass configuration directly:
42
+
43
+ ```typescript
44
+ const mitra = createClient({
45
+ apiUrl: "https://api.example.com",
46
+ accessToken: process.env.MITRA_PLATFORM_ACCESS_TOKEN,
47
+ appId: "app-id",
48
+ dataSourceId: "data-source-id",
49
+ })
50
+ ```
51
+
52
+ `createClientFromEnvironment(env)` is available when a function needs to supply a specific environment object, such as in tests.
53
+
54
+ ## Initialization
55
+
56
+ Entity operations use the app identity from the token and `X-App-Id`, so they are available immediately. Custom queries require a data source ID.
57
+
58
+ If `MITRA_DATA_SOURCE_ID` or `dataSourceId` is not configured, call `init()` once before executing queries. It resolves the data source from Code Studio and is safe to call concurrently.
59
+
60
+ ```typescript
61
+ const mitra = createClient()
62
+ await mitra.init()
63
+
64
+ const result = await mitra.queries.execute("5df41c69-2a74-4db6-9cca-4af2b473941f", {
65
+ status: "active",
66
+ })
67
+ ```
68
+
69
+ ## API
70
+
71
+ ### Current user
72
+
73
+ ```typescript
74
+ const user = await mitra.auth.me()
75
+ ```
76
+
77
+ The SDK only reads the current authenticated user. Login, logout, browser storage, and token refresh belong to the Platform SDK used by frontend applications.
78
+
79
+ ### Entities
80
+
81
+ ```typescript
82
+ type Task = {
83
+ id: string
84
+ title: string
85
+ status: "pending" | "done"
86
+ }
87
+
88
+ const tasks = mitra.entities.getTable<Task>("Task")
89
+ const recent = await tasks.list({ sort: "-created_at", limit: 20 })
90
+ const pending = await tasks.filter({ status: "pending" })
91
+ const created = await tasks.create({ title: "Review order" })
92
+ await tasks.update(created.id, { status: "done" })
93
+ await tasks.delete(created.id)
94
+
95
+ const dynamic = await mitra.entities.Task.list()
96
+ ```
97
+
98
+ Available methods are `list`, `filter`, `get`, `create`, `bulkCreate`, `update`, `delete`, and `deleteMany`.
99
+
100
+ ### Custom queries
101
+
102
+ ```typescript
103
+ await mitra.init()
104
+ const result = await mitra.queries.execute("5df41c69-2a74-4db6-9cca-4af2b473941f", {
105
+ customerId: "customer-id",
106
+ })
107
+ ```
108
+
109
+ ### Server Functions
110
+
111
+ `execute` waits for a terminal execution response. `executeAsync` requests asynchronous execution and returns the pending execution record.
112
+
113
+ ```typescript
114
+ const completed = await mitra.functions.execute("function-id", { orderId: "order-id" })
115
+ const pending = await mitra.functions.executeAsync("function-id", { orderId: "order-id" })
116
+ const current = await mitra.functions.getExecution(pending.id)
117
+ await mitra.functions.cancelExecution(pending.id)
118
+ ```
119
+
120
+ ### Integrations
121
+
122
+ ```typescript
123
+ const resource = await mitra.integration.executeResource("resource-id", {
124
+ description: "Notebook",
125
+ })
126
+
127
+ const proxy = await mitra.integration.execute("config-id", {
128
+ method: "GET",
129
+ endpoint: "/users",
130
+ queryParams: { limit: "10" },
131
+ })
132
+ ```
133
+
134
+ Credentials configured for an integration are injected by the Integration service. Do not pass provider credentials through function input.
135
+
136
+ ## Errors and request behavior
137
+
138
+ API, timeout, invalid response, and network failures throw `MitraApiError`. The error exposes `status`, `code`, `details`, `requestId`, and `retryable`, preserving API metadata when it is provided. Configuration failures throw `MitraConfigurationError`.
139
+
140
+ When an API error omits `retryable`, the HTTP adapter classifies 4xx responses
141
+ as `false` and 5xx responses as `true`. This field is diagnostic classification
142
+ only. The SDK still makes one attempt and never retries a request automatically.
143
+
144
+ The Python package uses idiomatic specialized names for local failures. JavaScript `REQUEST_TIMEOUT` and `NETWORK_ERROR` map to Python `MitraNetworkError`, while JavaScript `INVALID_RESPONSE` maps to Python `MitraResponseError`. API responses use `MitraApiError` in both packages.
145
+
146
+ Every request sends `Authorization: Bearer <token>` and `X-App-Id`. The default timeout is 10 seconds. Requests are never retried automatically, which avoids replaying writes with unknown idempotency.
147
+
148
+ The access token is redacted from API error messages and error details.
149
+
150
+ ## Contract parity
151
+
152
+ The test suite reads SDK-PARITY-001 directly from the installed
153
+ `@mitralab.io/sdk-core` package. The source manifest pins Core version 0.1.0,
154
+ the SHA-256 digest, and the full source commit
155
+ `b513454d0d1f7344a4656cd9c0e1e32530c5ea90`. It does not duplicate the corpus.
156
+
157
+ The JavaScript consumer requirement is exactly `httpAdapterCases: all`. Tests
158
+ execute all recorded 404, 503, and timeout cases through the public client and
159
+ the real `HttpClient`, using a mock fetch only at the network boundary. They
160
+ verify the request and the complete error semantics. Continuous integration and
161
+ release gates also download the canonical file from the public Core repository
162
+ at the pinned commit and compare it byte for byte with the installed package.
163
+
164
+ ## Scope
165
+
166
+ Version 0.1 includes application runtime operations: current user, entity CRUD, custom query execution, Server Function execution management, and integrations. Administrative control-plane methods and the legacy messenger API are intentionally excluded.
167
+
168
+ ## Development
169
+
170
+ ```bash
171
+ npm install
172
+ npm run check
173
+ ```
174
+
175
+ `@mitralab.io/sdk-core@0.1.0` is resolved from the public npm registry and
176
+ locked by integrity in `package-lock.json`. Do not replace it with a `file:`
177
+ dependency or a local tarball.
178
+
179
+ The contract source can be checked explicitly without changing the manifest or
180
+ lockfile:
181
+
182
+ ```bash
183
+ npm run check:contracts -- --canonical /path/to/sdk-parity.json
184
+ ```
185
+
186
+ The package builds ESM, CommonJS, and TypeScript declarations in `dist/`. Tests enforce at least 80 percent coverage for lines, functions, branches, and statements.
@@ -0,0 +1,15 @@
1
+ {
2
+ "contract": "SDK-PARITY-001",
3
+ "version": "0.1.0",
4
+ "dependency": {
5
+ "package": "@mitralab.io/sdk-core",
6
+ "version": "0.1.0",
7
+ "contractPath": "contracts/v0.1.0/sdk-parity.json"
8
+ },
9
+ "source": {
10
+ "repository": "https://github.com/mitralab-dev/mitra-core-sdk",
11
+ "commit": "b513454d0d1f7344a4656cd9c0e1e32530c5ea90",
12
+ "path": "contracts/v0.1.0/sdk-parity.json"
13
+ },
14
+ "sha256": "600be3688cea7cfef16a8cf347516e87949b8e4151efb2c4136a2345557dde70"
15
+ }
package/dist/index.cjs ADDED
@@ -0,0 +1,328 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ MitraApiError: () => MitraApiError,
24
+ MitraConfigurationError: () => MitraConfigurationError,
25
+ createClient: () => createClient,
26
+ createClientFromEnvironment: () => createClientFromEnvironment
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/client.ts
31
+ var import_sdk_core = require("@mitralab.io/sdk-core");
32
+
33
+ // src/errors.ts
34
+ var MitraConfigurationError = class extends Error {
35
+ constructor(message) {
36
+ super(message);
37
+ this.name = "MitraConfigurationError";
38
+ }
39
+ };
40
+ var MitraApiError = class extends Error {
41
+ status;
42
+ code;
43
+ details;
44
+ requestId;
45
+ retryable;
46
+ constructor(message, status, options = {}) {
47
+ super(message);
48
+ this.name = "MitraApiError";
49
+ this.status = status;
50
+ this.code = options.code;
51
+ this.details = options.details;
52
+ this.requestId = options.requestId;
53
+ this.retryable = options.retryable;
54
+ }
55
+ };
56
+
57
+ // src/config.ts
58
+ var DEFAULT_TIMEOUT_MS = 1e4;
59
+ function readDefaultEnvironment() {
60
+ return typeof process === "undefined" ? {} : process.env;
61
+ }
62
+ function requiredValue(value, name) {
63
+ if (!value?.trim()) {
64
+ throw new MitraConfigurationError(`${name} is required`);
65
+ }
66
+ return value.trim();
67
+ }
68
+ function normalizeApiUrl(value) {
69
+ let parsed;
70
+ try {
71
+ parsed = new URL(value);
72
+ } catch {
73
+ throw new MitraConfigurationError("apiUrl must be a valid HTTP or HTTPS URL");
74
+ }
75
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
76
+ throw new MitraConfigurationError("apiUrl must be a valid HTTP or HTTPS URL");
77
+ }
78
+ if (parsed.username || parsed.password || parsed.search || parsed.hash || value.includes("?") || value.includes("#")) {
79
+ throw new MitraConfigurationError(
80
+ "apiUrl must not include credentials, query parameters, or a fragment"
81
+ );
82
+ }
83
+ return parsed.toString().replace(/\/+$/, "");
84
+ }
85
+ function resolveConfig(config = {}, environment = readDefaultEnvironment()) {
86
+ const apiUrl = normalizeApiUrl(
87
+ requiredValue(config.apiUrl ?? environment.MITRA_API_URL, "apiUrl")
88
+ );
89
+ const accessToken = requiredValue(
90
+ config.accessToken ?? environment.MITRA_PLATFORM_ACCESS_TOKEN,
91
+ "accessToken"
92
+ );
93
+ const appId = requiredValue(config.appId ?? environment.MITRA_APP_ID, "appId");
94
+ const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
95
+ const fetchImplementation = config.fetch ?? globalThis.fetch;
96
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
97
+ throw new MitraConfigurationError("timeoutMs must be a positive number");
98
+ }
99
+ if (typeof fetchImplementation !== "function") {
100
+ throw new MitraConfigurationError("A fetch implementation is required");
101
+ }
102
+ const dataSourceId = config.dataSourceId ?? environment.MITRA_DATA_SOURCE_ID;
103
+ if (dataSourceId !== void 0 && !dataSourceId.trim()) {
104
+ throw new MitraConfigurationError("dataSourceId must not be empty when provided");
105
+ }
106
+ return {
107
+ apiUrl,
108
+ accessToken,
109
+ appId,
110
+ ...dataSourceId === void 0 ? {} : { dataSourceId: dataSourceId.trim() },
111
+ timeoutMs,
112
+ fetch: fetchImplementation
113
+ };
114
+ }
115
+
116
+ // src/http-client.ts
117
+ var bearerCredentialPattern = /(\bBearer\s+)\S+/gi;
118
+ function redactText(value, accessToken) {
119
+ return value.replace(bearerCredentialPattern, "$1[REDACTED]").split(accessToken).join("[REDACTED]");
120
+ }
121
+ function redactDetails(value, accessToken) {
122
+ if (typeof value === "string") return redactText(value, accessToken);
123
+ if (Array.isArray(value)) return value.map((item) => redactDetails(item, accessToken));
124
+ if (value && typeof value === "object") {
125
+ return Object.fromEntries(
126
+ Object.entries(value).map(([key, entry]) => {
127
+ const sensitive = /token|authorization|password|secret|api.?key/i.test(key);
128
+ return [
129
+ redactText(key, accessToken),
130
+ sensitive ? "[REDACTED]" : redactDetails(entry, accessToken)
131
+ ];
132
+ })
133
+ );
134
+ }
135
+ return value;
136
+ }
137
+ function optionalString(value) {
138
+ return typeof value === "string" ? value : void 0;
139
+ }
140
+ var HttpClient = class {
141
+ #accessToken;
142
+ baseUrl;
143
+ appId;
144
+ timeoutMs;
145
+ fetchImplementation;
146
+ constructor(config) {
147
+ this.baseUrl = config.baseUrl;
148
+ this.#accessToken = config.accessToken;
149
+ this.appId = config.appId;
150
+ this.timeoutMs = config.timeoutMs;
151
+ this.fetchImplementation = config.fetch;
152
+ }
153
+ async request(path, options = {}) {
154
+ const url = new URL(`${this.baseUrl}${path}`);
155
+ for (const [key, value] of Object.entries(options.params ?? {})) {
156
+ if (value !== void 0) url.searchParams.append(key, String(value));
157
+ }
158
+ const controller = new AbortController();
159
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
160
+ const hasBody = options.body !== void 0;
161
+ try {
162
+ const response = await this.fetchImplementation(url, {
163
+ method: options.method ?? "GET",
164
+ headers: {
165
+ Accept: "application/json",
166
+ ...hasBody ? { "Content-Type": "application/json" } : {},
167
+ Authorization: `Bearer ${this.#accessToken}`,
168
+ "X-App-Id": this.appId,
169
+ ...options.headers
170
+ },
171
+ ...hasBody ? { body: JSON.stringify(options.body) } : {},
172
+ redirect: "manual",
173
+ signal: controller.signal
174
+ });
175
+ if (response.status === 204) return void 0;
176
+ const text = await response.text();
177
+ let payload;
178
+ try {
179
+ payload = JSON.parse(text);
180
+ } catch {
181
+ if (response.ok) {
182
+ throw new MitraApiError("The Mitra API returned invalid JSON", response.status, {
183
+ code: "INVALID_RESPONSE",
184
+ retryable: false
185
+ });
186
+ }
187
+ payload = {};
188
+ }
189
+ if (!response.ok) {
190
+ const errorPayload = payload && typeof payload === "object" ? payload : {};
191
+ const rawMessage = optionalString(errorPayload.message);
192
+ const message = redactText(
193
+ rawMessage ?? `Request failed with status ${response.status}`,
194
+ this.#accessToken
195
+ );
196
+ const rawRequestId = optionalString(errorPayload.request_id) ?? optionalString(errorPayload.requestId) ?? response.headers.get("X-Request-Id") ?? void 0;
197
+ const rawCode = optionalString(errorPayload.error_code) ?? optionalString(errorPayload.code);
198
+ const requestId = rawRequestId === void 0 ? void 0 : redactText(rawRequestId, this.#accessToken);
199
+ const code = rawCode === void 0 ? void 0 : redactText(rawCode, this.#accessToken);
200
+ const retryable = typeof errorPayload.retryable === "boolean" ? errorPayload.retryable : response.status >= 500;
201
+ throw new MitraApiError(message, response.status, {
202
+ ...code === void 0 ? {} : { code },
203
+ ...errorPayload.details === void 0 ? {} : { details: redactDetails(errorPayload.details, this.#accessToken) },
204
+ ...requestId === void 0 ? {} : { requestId },
205
+ ...retryable === void 0 ? {} : { retryable }
206
+ });
207
+ }
208
+ return payload;
209
+ } catch (error) {
210
+ if (error instanceof MitraApiError) throw error;
211
+ if (controller.signal.aborted) {
212
+ throw new MitraApiError("Mitra request timed out", 0, {
213
+ code: "REQUEST_TIMEOUT",
214
+ retryable: true
215
+ });
216
+ }
217
+ throw new MitraApiError("The Mitra API request failed", 0, {
218
+ code: "NETWORK_ERROR",
219
+ retryable: true
220
+ });
221
+ } finally {
222
+ clearTimeout(timeout);
223
+ }
224
+ }
225
+ get(path, params) {
226
+ return this.request(path, { method: "GET", ...params === void 0 ? {} : { params } });
227
+ }
228
+ post(path, body, headers) {
229
+ return this.request(path, {
230
+ method: "POST",
231
+ ...body === void 0 ? {} : { body },
232
+ ...headers === void 0 ? {} : { headers }
233
+ });
234
+ }
235
+ put(path, body) {
236
+ return this.request(path, { method: "PUT", body });
237
+ }
238
+ delete(path, params) {
239
+ return this.request(path, {
240
+ method: "DELETE",
241
+ ...params === void 0 ? {} : { params }
242
+ });
243
+ }
244
+ };
245
+
246
+ // src/client.ts
247
+ var coreErrors = {
248
+ configuration: (message) => new MitraConfigurationError(message),
249
+ invalidResponse: (message) => new MitraApiError(message, 200, { code: "INVALID_RESPONSE", retryable: false })
250
+ };
251
+ var DefaultMitraClient = class {
252
+ auth;
253
+ entities;
254
+ functions;
255
+ integration;
256
+ queries;
257
+ #dataSourceId;
258
+ #initPromise;
259
+ #appId;
260
+ #codeStudioHttpClient;
261
+ constructor(config) {
262
+ this.#appId = config.appId;
263
+ this.#dataSourceId = config.dataSourceId;
264
+ const httpClient = (service) => new HttpClient({
265
+ baseUrl: `${config.apiUrl}/${service}`,
266
+ accessToken: config.accessToken,
267
+ appId: config.appId,
268
+ timeoutMs: config.timeoutMs,
269
+ fetch: config.fetch
270
+ });
271
+ const core = (0, import_sdk_core.createSdkCore)({
272
+ transports: {
273
+ auth: httpClient("iam"),
274
+ dataManager: httpClient("data-manager"),
275
+ functions: httpClient("functions"),
276
+ integration: httpClient("integration")
277
+ },
278
+ getDataSourceId: () => this.#dataSourceId,
279
+ functions: { executeInvocationType: "sync", emptyInput: "empty-object" },
280
+ errors: coreErrors
281
+ });
282
+ this.auth = core.auth;
283
+ this.entities = core.entities;
284
+ this.functions = core.functions;
285
+ this.integration = core.integration;
286
+ this.queries = core.queries;
287
+ this.#codeStudioHttpClient = httpClient("code-studio");
288
+ }
289
+ init() {
290
+ if (this.#dataSourceId) return Promise.resolve();
291
+ if (!this.#initPromise) {
292
+ this.#initPromise = this.resolveDataSourceId().catch((error) => {
293
+ this.#initPromise = void 0;
294
+ throw error;
295
+ });
296
+ }
297
+ return this.#initPromise;
298
+ }
299
+ async resolveDataSourceId() {
300
+ const appInfo = (0, import_sdk_core.expectObject)(
301
+ await this.#codeStudioHttpClient.get(
302
+ `/api/v1/apps/${(0, import_sdk_core.encodePathSegment)(this.#appId, "appId", coreErrors)}/info`
303
+ ),
304
+ "App info response",
305
+ coreErrors
306
+ );
307
+ if (!appInfo.dataSourceId?.trim()) {
308
+ throw new MitraApiError("The app info response does not include a dataSourceId", 200, {
309
+ code: "INVALID_RESPONSE",
310
+ retryable: false
311
+ });
312
+ }
313
+ this.#dataSourceId = appInfo.dataSourceId;
314
+ }
315
+ };
316
+ function createClient(config = {}) {
317
+ return new DefaultMitraClient(resolveConfig(config));
318
+ }
319
+ function createClientFromEnvironment(environment) {
320
+ return new DefaultMitraClient(resolveConfig({}, environment));
321
+ }
322
+ // Annotate the CommonJS export names for ESM import in node:
323
+ 0 && (module.exports = {
324
+ MitraApiError,
325
+ MitraConfigurationError,
326
+ createClient,
327
+ createClientFromEnvironment
328
+ });
@@ -0,0 +1,50 @@
1
+ import { AuthModule, EntitiesProxy, FunctionsModule, IntegrationModule, QueriesModule } from '@mitralab.io/sdk-core';
2
+ export { EntityListOptions, EntityTable, FunctionExecution, Plan, ProxyInput, ProxyResult, QueryResult, Tenant, User } from '@mitralab.io/sdk-core';
3
+
4
+ type Fetch = typeof globalThis.fetch;
5
+ interface MitraEnvironment {
6
+ MITRA_API_URL?: string;
7
+ MITRA_PLATFORM_ACCESS_TOKEN?: string;
8
+ MITRA_APP_ID?: string;
9
+ MITRA_DATA_SOURCE_ID?: string;
10
+ [key: string]: string | undefined;
11
+ }
12
+ interface MitraClientConfig {
13
+ apiUrl?: string;
14
+ accessToken?: string;
15
+ appId?: string;
16
+ dataSourceId?: string;
17
+ timeoutMs?: number;
18
+ fetch?: Fetch;
19
+ }
20
+
21
+ interface MitraClient {
22
+ init(): Promise<void>;
23
+ readonly auth: AuthModule;
24
+ readonly entities: EntitiesProxy;
25
+ readonly functions: FunctionsModule;
26
+ readonly integration: IntegrationModule;
27
+ readonly queries: QueriesModule;
28
+ }
29
+ declare function createClient(config?: MitraClientConfig): MitraClient;
30
+ declare function createClientFromEnvironment(environment?: MitraEnvironment): MitraClient;
31
+
32
+ declare class MitraConfigurationError extends Error {
33
+ constructor(message: string);
34
+ }
35
+ interface MitraApiErrorOptions {
36
+ code?: string;
37
+ details?: unknown;
38
+ requestId?: string;
39
+ retryable?: boolean;
40
+ }
41
+ declare class MitraApiError extends Error {
42
+ readonly status: number;
43
+ readonly code: string | undefined;
44
+ readonly details: unknown;
45
+ readonly requestId: string | undefined;
46
+ readonly retryable: boolean | undefined;
47
+ constructor(message: string, status: number, options?: MitraApiErrorOptions);
48
+ }
49
+
50
+ export { type Fetch, MitraApiError, type MitraClient, type MitraClientConfig, MitraConfigurationError, type MitraEnvironment, createClient, createClientFromEnvironment };
@@ -0,0 +1,50 @@
1
+ import { AuthModule, EntitiesProxy, FunctionsModule, IntegrationModule, QueriesModule } from '@mitralab.io/sdk-core';
2
+ export { EntityListOptions, EntityTable, FunctionExecution, Plan, ProxyInput, ProxyResult, QueryResult, Tenant, User } from '@mitralab.io/sdk-core';
3
+
4
+ type Fetch = typeof globalThis.fetch;
5
+ interface MitraEnvironment {
6
+ MITRA_API_URL?: string;
7
+ MITRA_PLATFORM_ACCESS_TOKEN?: string;
8
+ MITRA_APP_ID?: string;
9
+ MITRA_DATA_SOURCE_ID?: string;
10
+ [key: string]: string | undefined;
11
+ }
12
+ interface MitraClientConfig {
13
+ apiUrl?: string;
14
+ accessToken?: string;
15
+ appId?: string;
16
+ dataSourceId?: string;
17
+ timeoutMs?: number;
18
+ fetch?: Fetch;
19
+ }
20
+
21
+ interface MitraClient {
22
+ init(): Promise<void>;
23
+ readonly auth: AuthModule;
24
+ readonly entities: EntitiesProxy;
25
+ readonly functions: FunctionsModule;
26
+ readonly integration: IntegrationModule;
27
+ readonly queries: QueriesModule;
28
+ }
29
+ declare function createClient(config?: MitraClientConfig): MitraClient;
30
+ declare function createClientFromEnvironment(environment?: MitraEnvironment): MitraClient;
31
+
32
+ declare class MitraConfigurationError extends Error {
33
+ constructor(message: string);
34
+ }
35
+ interface MitraApiErrorOptions {
36
+ code?: string;
37
+ details?: unknown;
38
+ requestId?: string;
39
+ retryable?: boolean;
40
+ }
41
+ declare class MitraApiError extends Error {
42
+ readonly status: number;
43
+ readonly code: string | undefined;
44
+ readonly details: unknown;
45
+ readonly requestId: string | undefined;
46
+ readonly retryable: boolean | undefined;
47
+ constructor(message: string, status: number, options?: MitraApiErrorOptions);
48
+ }
49
+
50
+ export { type Fetch, MitraApiError, type MitraClient, type MitraClientConfig, MitraConfigurationError, type MitraEnvironment, createClient, createClientFromEnvironment };
package/dist/index.js ADDED
@@ -0,0 +1,302 @@
1
+ // src/client.ts
2
+ import {
3
+ createSdkCore,
4
+ encodePathSegment,
5
+ expectObject
6
+ } from "@mitralab.io/sdk-core";
7
+
8
+ // src/errors.ts
9
+ var MitraConfigurationError = class extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = "MitraConfigurationError";
13
+ }
14
+ };
15
+ var MitraApiError = class extends Error {
16
+ status;
17
+ code;
18
+ details;
19
+ requestId;
20
+ retryable;
21
+ constructor(message, status, options = {}) {
22
+ super(message);
23
+ this.name = "MitraApiError";
24
+ this.status = status;
25
+ this.code = options.code;
26
+ this.details = options.details;
27
+ this.requestId = options.requestId;
28
+ this.retryable = options.retryable;
29
+ }
30
+ };
31
+
32
+ // src/config.ts
33
+ var DEFAULT_TIMEOUT_MS = 1e4;
34
+ function readDefaultEnvironment() {
35
+ return typeof process === "undefined" ? {} : process.env;
36
+ }
37
+ function requiredValue(value, name) {
38
+ if (!value?.trim()) {
39
+ throw new MitraConfigurationError(`${name} is required`);
40
+ }
41
+ return value.trim();
42
+ }
43
+ function normalizeApiUrl(value) {
44
+ let parsed;
45
+ try {
46
+ parsed = new URL(value);
47
+ } catch {
48
+ throw new MitraConfigurationError("apiUrl must be a valid HTTP or HTTPS URL");
49
+ }
50
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
51
+ throw new MitraConfigurationError("apiUrl must be a valid HTTP or HTTPS URL");
52
+ }
53
+ if (parsed.username || parsed.password || parsed.search || parsed.hash || value.includes("?") || value.includes("#")) {
54
+ throw new MitraConfigurationError(
55
+ "apiUrl must not include credentials, query parameters, or a fragment"
56
+ );
57
+ }
58
+ return parsed.toString().replace(/\/+$/, "");
59
+ }
60
+ function resolveConfig(config = {}, environment = readDefaultEnvironment()) {
61
+ const apiUrl = normalizeApiUrl(
62
+ requiredValue(config.apiUrl ?? environment.MITRA_API_URL, "apiUrl")
63
+ );
64
+ const accessToken = requiredValue(
65
+ config.accessToken ?? environment.MITRA_PLATFORM_ACCESS_TOKEN,
66
+ "accessToken"
67
+ );
68
+ const appId = requiredValue(config.appId ?? environment.MITRA_APP_ID, "appId");
69
+ const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
70
+ const fetchImplementation = config.fetch ?? globalThis.fetch;
71
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
72
+ throw new MitraConfigurationError("timeoutMs must be a positive number");
73
+ }
74
+ if (typeof fetchImplementation !== "function") {
75
+ throw new MitraConfigurationError("A fetch implementation is required");
76
+ }
77
+ const dataSourceId = config.dataSourceId ?? environment.MITRA_DATA_SOURCE_ID;
78
+ if (dataSourceId !== void 0 && !dataSourceId.trim()) {
79
+ throw new MitraConfigurationError("dataSourceId must not be empty when provided");
80
+ }
81
+ return {
82
+ apiUrl,
83
+ accessToken,
84
+ appId,
85
+ ...dataSourceId === void 0 ? {} : { dataSourceId: dataSourceId.trim() },
86
+ timeoutMs,
87
+ fetch: fetchImplementation
88
+ };
89
+ }
90
+
91
+ // src/http-client.ts
92
+ var bearerCredentialPattern = /(\bBearer\s+)\S+/gi;
93
+ function redactText(value, accessToken) {
94
+ return value.replace(bearerCredentialPattern, "$1[REDACTED]").split(accessToken).join("[REDACTED]");
95
+ }
96
+ function redactDetails(value, accessToken) {
97
+ if (typeof value === "string") return redactText(value, accessToken);
98
+ if (Array.isArray(value)) return value.map((item) => redactDetails(item, accessToken));
99
+ if (value && typeof value === "object") {
100
+ return Object.fromEntries(
101
+ Object.entries(value).map(([key, entry]) => {
102
+ const sensitive = /token|authorization|password|secret|api.?key/i.test(key);
103
+ return [
104
+ redactText(key, accessToken),
105
+ sensitive ? "[REDACTED]" : redactDetails(entry, accessToken)
106
+ ];
107
+ })
108
+ );
109
+ }
110
+ return value;
111
+ }
112
+ function optionalString(value) {
113
+ return typeof value === "string" ? value : void 0;
114
+ }
115
+ var HttpClient = class {
116
+ #accessToken;
117
+ baseUrl;
118
+ appId;
119
+ timeoutMs;
120
+ fetchImplementation;
121
+ constructor(config) {
122
+ this.baseUrl = config.baseUrl;
123
+ this.#accessToken = config.accessToken;
124
+ this.appId = config.appId;
125
+ this.timeoutMs = config.timeoutMs;
126
+ this.fetchImplementation = config.fetch;
127
+ }
128
+ async request(path, options = {}) {
129
+ const url = new URL(`${this.baseUrl}${path}`);
130
+ for (const [key, value] of Object.entries(options.params ?? {})) {
131
+ if (value !== void 0) url.searchParams.append(key, String(value));
132
+ }
133
+ const controller = new AbortController();
134
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
135
+ const hasBody = options.body !== void 0;
136
+ try {
137
+ const response = await this.fetchImplementation(url, {
138
+ method: options.method ?? "GET",
139
+ headers: {
140
+ Accept: "application/json",
141
+ ...hasBody ? { "Content-Type": "application/json" } : {},
142
+ Authorization: `Bearer ${this.#accessToken}`,
143
+ "X-App-Id": this.appId,
144
+ ...options.headers
145
+ },
146
+ ...hasBody ? { body: JSON.stringify(options.body) } : {},
147
+ redirect: "manual",
148
+ signal: controller.signal
149
+ });
150
+ if (response.status === 204) return void 0;
151
+ const text = await response.text();
152
+ let payload;
153
+ try {
154
+ payload = JSON.parse(text);
155
+ } catch {
156
+ if (response.ok) {
157
+ throw new MitraApiError("The Mitra API returned invalid JSON", response.status, {
158
+ code: "INVALID_RESPONSE",
159
+ retryable: false
160
+ });
161
+ }
162
+ payload = {};
163
+ }
164
+ if (!response.ok) {
165
+ const errorPayload = payload && typeof payload === "object" ? payload : {};
166
+ const rawMessage = optionalString(errorPayload.message);
167
+ const message = redactText(
168
+ rawMessage ?? `Request failed with status ${response.status}`,
169
+ this.#accessToken
170
+ );
171
+ const rawRequestId = optionalString(errorPayload.request_id) ?? optionalString(errorPayload.requestId) ?? response.headers.get("X-Request-Id") ?? void 0;
172
+ const rawCode = optionalString(errorPayload.error_code) ?? optionalString(errorPayload.code);
173
+ const requestId = rawRequestId === void 0 ? void 0 : redactText(rawRequestId, this.#accessToken);
174
+ const code = rawCode === void 0 ? void 0 : redactText(rawCode, this.#accessToken);
175
+ const retryable = typeof errorPayload.retryable === "boolean" ? errorPayload.retryable : response.status >= 500;
176
+ throw new MitraApiError(message, response.status, {
177
+ ...code === void 0 ? {} : { code },
178
+ ...errorPayload.details === void 0 ? {} : { details: redactDetails(errorPayload.details, this.#accessToken) },
179
+ ...requestId === void 0 ? {} : { requestId },
180
+ ...retryable === void 0 ? {} : { retryable }
181
+ });
182
+ }
183
+ return payload;
184
+ } catch (error) {
185
+ if (error instanceof MitraApiError) throw error;
186
+ if (controller.signal.aborted) {
187
+ throw new MitraApiError("Mitra request timed out", 0, {
188
+ code: "REQUEST_TIMEOUT",
189
+ retryable: true
190
+ });
191
+ }
192
+ throw new MitraApiError("The Mitra API request failed", 0, {
193
+ code: "NETWORK_ERROR",
194
+ retryable: true
195
+ });
196
+ } finally {
197
+ clearTimeout(timeout);
198
+ }
199
+ }
200
+ get(path, params) {
201
+ return this.request(path, { method: "GET", ...params === void 0 ? {} : { params } });
202
+ }
203
+ post(path, body, headers) {
204
+ return this.request(path, {
205
+ method: "POST",
206
+ ...body === void 0 ? {} : { body },
207
+ ...headers === void 0 ? {} : { headers }
208
+ });
209
+ }
210
+ put(path, body) {
211
+ return this.request(path, { method: "PUT", body });
212
+ }
213
+ delete(path, params) {
214
+ return this.request(path, {
215
+ method: "DELETE",
216
+ ...params === void 0 ? {} : { params }
217
+ });
218
+ }
219
+ };
220
+
221
+ // src/client.ts
222
+ var coreErrors = {
223
+ configuration: (message) => new MitraConfigurationError(message),
224
+ invalidResponse: (message) => new MitraApiError(message, 200, { code: "INVALID_RESPONSE", retryable: false })
225
+ };
226
+ var DefaultMitraClient = class {
227
+ auth;
228
+ entities;
229
+ functions;
230
+ integration;
231
+ queries;
232
+ #dataSourceId;
233
+ #initPromise;
234
+ #appId;
235
+ #codeStudioHttpClient;
236
+ constructor(config) {
237
+ this.#appId = config.appId;
238
+ this.#dataSourceId = config.dataSourceId;
239
+ const httpClient = (service) => new HttpClient({
240
+ baseUrl: `${config.apiUrl}/${service}`,
241
+ accessToken: config.accessToken,
242
+ appId: config.appId,
243
+ timeoutMs: config.timeoutMs,
244
+ fetch: config.fetch
245
+ });
246
+ const core = createSdkCore({
247
+ transports: {
248
+ auth: httpClient("iam"),
249
+ dataManager: httpClient("data-manager"),
250
+ functions: httpClient("functions"),
251
+ integration: httpClient("integration")
252
+ },
253
+ getDataSourceId: () => this.#dataSourceId,
254
+ functions: { executeInvocationType: "sync", emptyInput: "empty-object" },
255
+ errors: coreErrors
256
+ });
257
+ this.auth = core.auth;
258
+ this.entities = core.entities;
259
+ this.functions = core.functions;
260
+ this.integration = core.integration;
261
+ this.queries = core.queries;
262
+ this.#codeStudioHttpClient = httpClient("code-studio");
263
+ }
264
+ init() {
265
+ if (this.#dataSourceId) return Promise.resolve();
266
+ if (!this.#initPromise) {
267
+ this.#initPromise = this.resolveDataSourceId().catch((error) => {
268
+ this.#initPromise = void 0;
269
+ throw error;
270
+ });
271
+ }
272
+ return this.#initPromise;
273
+ }
274
+ async resolveDataSourceId() {
275
+ const appInfo = expectObject(
276
+ await this.#codeStudioHttpClient.get(
277
+ `/api/v1/apps/${encodePathSegment(this.#appId, "appId", coreErrors)}/info`
278
+ ),
279
+ "App info response",
280
+ coreErrors
281
+ );
282
+ if (!appInfo.dataSourceId?.trim()) {
283
+ throw new MitraApiError("The app info response does not include a dataSourceId", 200, {
284
+ code: "INVALID_RESPONSE",
285
+ retryable: false
286
+ });
287
+ }
288
+ this.#dataSourceId = appInfo.dataSourceId;
289
+ }
290
+ };
291
+ function createClient(config = {}) {
292
+ return new DefaultMitraClient(resolveConfig(config));
293
+ }
294
+ function createClientFromEnvironment(environment) {
295
+ return new DefaultMitraClient(resolveConfig({}, environment));
296
+ }
297
+ export {
298
+ MitraApiError,
299
+ MitraConfigurationError,
300
+ createClient,
301
+ createClientFromEnvironment
302
+ };
package/package.json ADDED
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "@mitralab.io/functions-sdk",
3
+ "version": "0.1.0",
4
+ "description": "JavaScript and TypeScript SDK for Mitra Server Functions",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "require": {
17
+ "types": "./dist/index.d.cts",
18
+ "default": "./dist/index.cjs"
19
+ }
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "contracts",
25
+ "README.md",
26
+ "LICENSE",
27
+ "CHANGELOG.md"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public",
31
+ "registry": "https://registry.npmjs.org"
32
+ },
33
+ "scripts": {
34
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
35
+ "format": "prettier --write .",
36
+ "format:check": "prettier --check .",
37
+ "lint": "eslint .",
38
+ "typecheck": "tsc --noEmit",
39
+ "test": "vitest run --coverage",
40
+ "test:watch": "vitest",
41
+ "check:exports": "node scripts/check-exports.mjs",
42
+ "check:contracts": "node scripts/check-contract-corpus.mjs",
43
+ "pack:check": "npm pack --dry-run",
44
+ "smoke:package": "node scripts/smoke-package.mjs",
45
+ "check": "npm run format:check && npm run lint && npm run typecheck && npm test && npm run build && npm run check:exports && npm run check:contracts && npm run pack:check && npm run smoke:package",
46
+ "prepublishOnly": "npm run check"
47
+ },
48
+ "keywords": [
49
+ "mitra",
50
+ "sdk",
51
+ "serverless",
52
+ "functions"
53
+ ],
54
+ "author": "Mitra Platform",
55
+ "license": "MIT",
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "https://github.com/mitralab-dev/mitra-functions-sdk-js"
59
+ },
60
+ "homepage": "https://github.com/mitralab-dev/mitra-functions-sdk-js#readme",
61
+ "bugs": {
62
+ "url": "https://github.com/mitralab-dev/mitra-functions-sdk-js/issues"
63
+ },
64
+ "engines": {
65
+ "node": ">=18.0.0"
66
+ },
67
+ "dependencies": {
68
+ "@mitralab.io/sdk-core": "^0.1.0"
69
+ },
70
+ "devDependencies": {
71
+ "@eslint/js": "^9.39.1",
72
+ "@types/node": "^22.15.0",
73
+ "@vitest/coverage-v8": "^3.2.4",
74
+ "eslint": "^9.39.1",
75
+ "globals": "^16.5.0",
76
+ "prettier": "^3.8.1",
77
+ "tsup": "^8.5.1",
78
+ "typescript": "~5.9.3",
79
+ "typescript-eslint": "~8.55.0",
80
+ "vite": "~6.4.3",
81
+ "vitest": "^3.2.4"
82
+ },
83
+ "overrides": {
84
+ "esbuild": "^0.25.0",
85
+ "test-exclude": "7.0.1"
86
+ }
87
+ }