@seclai/sdk 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,25 +33,66 @@ console.log(result);
33
33
 
34
34
  | Option | Environment variable | Default |
35
35
  | --- | --- | --- |
36
- | `apiKey` | `SECLAI_API_KEY` | *required* |
36
+ | `apiKey` | `SECLAI_API_KEY` | |
37
+ | `accessToken` | — | — |
38
+ | `profile` | `SECLAI_PROFILE` | `"default"` |
39
+ | `configDir` | `SECLAI_CONFIG_DIR` | `~/.seclai` |
40
+ | `autoRefresh` | — | `true` |
41
+ | `accountId` | — | — |
37
42
  | `baseUrl` | `SECLAI_API_URL` | `https://api.seclai.com` |
38
43
  | `apiKeyHeader` | — | `x-api-key` |
39
44
  | `defaultHeaders` | — | `{}` |
40
45
  | `fetch` | — | `globalThis.fetch` |
41
46
 
47
+ ### Authentication
48
+
49
+ Credentials are resolved via a chain (first match wins):
50
+
51
+ 1. Explicit `apiKey` option
52
+ 2. Explicit `accessToken` option (string or `() => string | Promise<string>`)
53
+ 3. `SECLAI_API_KEY` environment variable
54
+ 4. SSO profile from `~/.seclai/config` with cached tokens in `~/.seclai/sso/cache/`
55
+
56
+ ```ts
57
+ // API key
58
+ const client = new Seclai({ apiKey: "sk-..." });
59
+ ```
60
+
42
61
  ```ts
62
+ // Static bearer token
63
+ const client = new Seclai({ accessToken: "eyJhbGciOi..." });
64
+ ```
65
+
66
+ ```ts
67
+ // Dynamic bearer token provider (called per request)
43
68
  const client = new Seclai({
44
- apiKey: "sk-...",
45
- baseUrl: "https://staging-api.seclai.com",
46
- defaultHeaders: { "X-Custom": "value" },
69
+ accessToken: async () => fetchTokenFromVault(),
47
70
  });
48
71
  ```
49
72
 
73
+ ```ts
74
+ // SSO profile (uses cached tokens, auto-refreshes)
75
+ const client = new Seclai({ profile: "my-profile" });
76
+ ```
77
+
78
+ ```ts
79
+ // Environment variable (no options needed)
80
+ // export SECLAI_API_KEY="sk-..."
81
+ const client = new Seclai();
82
+ ```
83
+
84
+ To set up SSO authentication, install the [Seclai CLI](https://www.npmjs.com/package/seclai) and run:
85
+
86
+ ```bash
87
+ seclai configure sso # set up an SSO profile
88
+ seclai auth login # authenticate via browser
89
+ ```
90
+
50
91
  ## API documentation
51
92
 
52
93
  Online API documentation (latest):
53
94
 
54
- https://seclai.github.io/seclai-javascript/1.1.0/
95
+ https://seclai.github.io/seclai-javascript/1.1.1/
55
96
 
56
97
  ## Resources
57
98
 
package/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -80,9 +90,283 @@ var SeclaiStreamingError = class extends SeclaiError {
80
90
  }
81
91
  };
82
92
 
93
+ // src/auth.ts
94
+ var DEFAULT_CONFIG_DIR = ".seclai";
95
+ var SSO_CACHE_DIR = "sso/cache";
96
+ var CONFIG_FILE = "config";
97
+ var EXPIRY_BUFFER_MS = 3e4;
98
+ var DEFAULT_API_KEY_HEADER = "x-api-key";
99
+ function getEnv(name) {
100
+ const p = globalThis?.process;
101
+ return p?.env?.[name];
102
+ }
103
+ function getHomeDir() {
104
+ const p = globalThis?.process;
105
+ return p?.env?.HOME ?? p?.env?.USERPROFILE;
106
+ }
107
+ async function sha1Hex(input) {
108
+ try {
109
+ const { createHash } = await import("crypto");
110
+ return createHash("sha1").update(input).digest("hex");
111
+ } catch {
112
+ const encoded = new TextEncoder().encode(input);
113
+ const buffer = await crypto.subtle.digest("SHA-1", encoded);
114
+ return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
115
+ }
116
+ }
117
+ async function cacheFileName(domain, clientId) {
118
+ return sha1Hex(`${domain}|${clientId}`);
119
+ }
120
+ function parseIni(content) {
121
+ const sections = {};
122
+ let currentSection = null;
123
+ for (const rawLine of content.split(/\r?\n/)) {
124
+ const line = rawLine.trim();
125
+ if (!line || line.startsWith("#") || line.startsWith(";")) continue;
126
+ const sectionMatch = line.match(/^\[(.+)\]$/);
127
+ if (sectionMatch) {
128
+ const raw = sectionMatch[1].trim();
129
+ currentSection = raw.startsWith("profile ") ? raw.slice("profile ".length).trim() : raw;
130
+ sections[currentSection] ??= {};
131
+ continue;
132
+ }
133
+ if (currentSection !== null) {
134
+ const eqIdx = line.indexOf("=");
135
+ if (eqIdx > 0) {
136
+ const key = line.slice(0, eqIdx).trim();
137
+ const value = line.slice(eqIdx + 1).trim();
138
+ sections[currentSection][key] = value;
139
+ }
140
+ }
141
+ }
142
+ return sections;
143
+ }
144
+ var _fs = null;
145
+ var _path = null;
146
+ async function getFs() {
147
+ if (!_fs) {
148
+ _fs = await import("fs");
149
+ }
150
+ return _fs;
151
+ }
152
+ async function getPath() {
153
+ if (!_path) {
154
+ _path = await import("path");
155
+ }
156
+ return _path;
157
+ }
158
+ async function resolveConfigDir(override) {
159
+ if (override) return override;
160
+ const envDir = getEnv("SECLAI_CONFIG_DIR");
161
+ if (envDir) return envDir;
162
+ const home = getHomeDir();
163
+ if (!home) {
164
+ throw new Error("Cannot determine home directory. Set SECLAI_CONFIG_DIR.");
165
+ }
166
+ const pathMod = await getPath();
167
+ return pathMod.join(home, DEFAULT_CONFIG_DIR);
168
+ }
169
+ async function loadSsoProfile(configDir, profileName) {
170
+ const fs = await getFs();
171
+ const pathMod = await getPath();
172
+ const configPath = pathMod.join(configDir, CONFIG_FILE);
173
+ if (!fs.existsSync(configPath)) return null;
174
+ const content = fs.readFileSync(configPath, "utf-8");
175
+ const sections = parseIni(content);
176
+ const defaultSection = sections["default"] ?? {};
177
+ const profileSection = profileName === "default" ? defaultSection : sections[profileName];
178
+ if (!profileSection) return null;
179
+ const merged = profileName === "default" ? profileSection : { ...defaultSection, ...profileSection };
180
+ const ssoAccountId = merged["sso_account_id"];
181
+ const ssoRegion = merged["sso_region"];
182
+ const ssoClientId = merged["sso_client_id"];
183
+ const ssoDomain = merged["sso_domain"];
184
+ if (!ssoAccountId || !ssoRegion || !ssoClientId || !ssoDomain) return null;
185
+ return { ssoAccountId, ssoRegion, ssoClientId, ssoDomain };
186
+ }
187
+ async function resolveCachePath(configDir, profile) {
188
+ const pathMod = await getPath();
189
+ const hash = await cacheFileName(profile.ssoDomain, profile.ssoClientId);
190
+ return pathMod.join(configDir, SSO_CACHE_DIR, `${hash}.json`);
191
+ }
192
+ async function readSsoCache(configDir, profile) {
193
+ const fs = await getFs();
194
+ const cachePath = await resolveCachePath(configDir, profile);
195
+ if (!fs.existsSync(cachePath)) return null;
196
+ try {
197
+ const raw = fs.readFileSync(cachePath, "utf-8");
198
+ return JSON.parse(raw);
199
+ } catch {
200
+ return null;
201
+ }
202
+ }
203
+ async function writeSsoCache(configDir, profile, entry) {
204
+ const fs = await getFs();
205
+ const pathMod = await getPath();
206
+ const cacheDir = pathMod.join(configDir, SSO_CACHE_DIR);
207
+ fs.mkdirSync(cacheDir, { recursive: true, mode: 448 });
208
+ const cachePath = await resolveCachePath(configDir, profile);
209
+ const tmpPath = `${cachePath}.tmp`;
210
+ fs.writeFileSync(tmpPath, JSON.stringify(entry, null, 2), { mode: 384 });
211
+ if (fs.existsSync(cachePath)) {
212
+ try {
213
+ fs.unlinkSync(cachePath);
214
+ } catch {
215
+ }
216
+ }
217
+ fs.renameSync(tmpPath, cachePath);
218
+ }
219
+ function isTokenValid(entry) {
220
+ const expiresAt = new Date(entry.expiresAt).getTime();
221
+ return Date.now() + EXPIRY_BUFFER_MS < expiresAt;
222
+ }
223
+ async function refreshToken(profile, refreshTokenValue, fetcher) {
224
+ const tokenUrl = `https://${profile.ssoDomain}/oauth2/token`;
225
+ const body = new URLSearchParams({
226
+ grant_type: "refresh_token",
227
+ client_id: profile.ssoClientId,
228
+ refresh_token: refreshTokenValue
229
+ });
230
+ const response = await fetcher(tokenUrl, {
231
+ method: "POST",
232
+ headers: { "content-type": "application/x-www-form-urlencoded" },
233
+ body: body.toString()
234
+ });
235
+ if (!response.ok) {
236
+ const text = await response.text().catch(() => "");
237
+ throw new Error(`Token refresh failed (HTTP ${response.status}): ${text}`);
238
+ }
239
+ const data = await response.json();
240
+ const expiresAt = new Date(Date.now() + data.expires_in * 1e3).toISOString();
241
+ return {
242
+ accessToken: data.access_token,
243
+ refreshToken: data.refresh_token ?? refreshTokenValue,
244
+ idToken: data.id_token ?? void 0,
245
+ expiresAt,
246
+ clientId: profile.ssoClientId,
247
+ region: profile.ssoRegion,
248
+ cognitoDomain: profile.ssoDomain
249
+ };
250
+ }
251
+ async function resolveCredentialChain(opts) {
252
+ const apiKeyHeader = opts.apiKeyHeader ?? DEFAULT_API_KEY_HEADER;
253
+ if (opts.apiKey) {
254
+ return {
255
+ mode: "apiKey",
256
+ apiKey: opts.apiKey,
257
+ apiKeyHeader,
258
+ accountId: opts.accountId,
259
+ autoRefresh: false
260
+ };
261
+ }
262
+ if (opts.accessToken) {
263
+ return {
264
+ mode: "bearerStatic",
265
+ accessToken: opts.accessToken,
266
+ apiKeyHeader,
267
+ accountId: opts.accountId,
268
+ autoRefresh: false
269
+ };
270
+ }
271
+ if (opts.accessTokenProvider) {
272
+ return {
273
+ mode: "bearerProvider",
274
+ accessTokenProvider: opts.accessTokenProvider,
275
+ apiKeyHeader,
276
+ accountId: opts.accountId,
277
+ autoRefresh: false
278
+ };
279
+ }
280
+ const envApiKey = getEnv("SECLAI_API_KEY");
281
+ if (envApiKey) {
282
+ return {
283
+ mode: "apiKey",
284
+ apiKey: envApiKey,
285
+ apiKeyHeader,
286
+ accountId: opts.accountId,
287
+ autoRefresh: false
288
+ };
289
+ }
290
+ try {
291
+ const configDir = await resolveConfigDir(opts.configDir);
292
+ const profileName = opts.profile ?? getEnv("SECLAI_PROFILE") ?? "default";
293
+ const ssoProfile = await loadSsoProfile(configDir, profileName);
294
+ if (ssoProfile) {
295
+ return {
296
+ mode: "sso",
297
+ apiKeyHeader,
298
+ accountId: opts.accountId ?? ssoProfile.ssoAccountId,
299
+ ssoProfile,
300
+ configDir,
301
+ autoRefresh: opts.autoRefresh !== false,
302
+ fetcher: opts.fetch
303
+ };
304
+ }
305
+ } catch {
306
+ }
307
+ throw new Error(
308
+ "Missing credentials. Provide apiKey, accessToken, set SECLAI_API_KEY, or run `seclai auth login`."
309
+ );
310
+ }
311
+ async function resolveAuthHeaders(state) {
312
+ const headers = {};
313
+ switch (state.mode) {
314
+ case "apiKey":
315
+ headers[state.apiKeyHeader] = state.apiKey;
316
+ break;
317
+ case "bearerStatic":
318
+ headers["authorization"] = `Bearer ${state.accessToken}`;
319
+ break;
320
+ case "bearerProvider": {
321
+ const token = await Promise.resolve(state.accessTokenProvider());
322
+ headers["authorization"] = `Bearer ${token}`;
323
+ break;
324
+ }
325
+ case "sso": {
326
+ const token = await resolveSsoToken(state);
327
+ headers["authorization"] = `Bearer ${token}`;
328
+ break;
329
+ }
330
+ }
331
+ if (state.accountId) {
332
+ headers["x-account-id"] = state.accountId;
333
+ }
334
+ return headers;
335
+ }
336
+ async function resolveSsoToken(state) {
337
+ const profile = state.ssoProfile;
338
+ const configDir = state.configDir;
339
+ const cached = await readSsoCache(configDir, profile);
340
+ if (cached && isTokenValid(cached)) {
341
+ return cached.accessToken;
342
+ }
343
+ if (cached?.refreshToken && state.autoRefresh) {
344
+ if (state._refreshPromise) {
345
+ return state._refreshPromise;
346
+ }
347
+ const fetcher = state.fetcher ?? globalThis.fetch;
348
+ if (!fetcher) {
349
+ throw new Error("No fetch implementation available for token refresh.");
350
+ }
351
+ state._refreshPromise = (async () => {
352
+ try {
353
+ const refreshed = await refreshToken(profile, cached.refreshToken, fetcher);
354
+ await writeSsoCache(configDir, profile, refreshed);
355
+ return refreshed.accessToken;
356
+ } finally {
357
+ state._refreshPromise = void 0;
358
+ }
359
+ })();
360
+ return state._refreshPromise;
361
+ }
362
+ throw new Error(
363
+ `SSO token expired. Run \`seclai auth login\` to re-authenticate.`
364
+ );
365
+ }
366
+
83
367
  // src/client.ts
84
368
  var SECLAI_API_URL = "https://api.seclai.com";
85
- function getEnv(name) {
369
+ function getEnv2(name) {
86
370
  const p = globalThis?.process;
87
371
  return p?.env?.[name];
88
372
  }
@@ -209,23 +493,29 @@ function inferMimeType(fileName) {
209
493
  return ext ? MIME_TYPES[ext] : void 0;
210
494
  }
211
495
  var Seclai = class {
212
- apiKey;
213
496
  baseUrl;
214
- apiKeyHeader;
215
497
  defaultHeaders;
216
498
  fetcher;
499
+ _authState = null;
500
+ _authInitPromise = null;
501
+ _authInitError = null;
217
502
  /**
218
503
  * Create a new Seclai client.
219
504
  *
505
+ * Credentials are resolved via a chain (first match wins):
506
+ * 1. Explicit `apiKey` option
507
+ * 2. Explicit `accessToken` option (static string or provider function)
508
+ * 3. `SECLAI_API_KEY` environment variable
509
+ * 4. SSO profile from `~/.seclai/config` + cached tokens in `~/.seclai/sso/cache/`
510
+ *
220
511
  * @param opts - Client configuration.
221
- * @throws {@link SeclaiConfigurationError} If no API key is provided (and `SECLAI_API_KEY` is not set).
222
512
  * @throws {@link SeclaiConfigurationError} If no `fetch` implementation is available.
513
+ * @throws {@link SeclaiConfigurationError} If both `apiKey` and `accessToken` are provided.
223
514
  */
224
515
  constructor(opts = {}) {
225
- const apiKey = opts.apiKey ?? getEnv("SECLAI_API_KEY");
226
- if (!apiKey) {
516
+ if (opts.apiKey && opts.accessToken) {
227
517
  throw new SeclaiConfigurationError(
228
- "Missing API key. Provide apiKey or set SECLAI_API_KEY."
518
+ "Provide either apiKey or accessToken, not both."
229
519
  );
230
520
  }
231
521
  const fetcher = opts.fetch ?? globalThis.fetch;
@@ -234,11 +524,49 @@ var Seclai = class {
234
524
  "No fetch implementation available. Provide opts.fetch or run in an environment with global fetch."
235
525
  );
236
526
  }
237
- this.apiKey = apiKey;
238
- this.baseUrl = opts.baseUrl ?? getEnv("SECLAI_API_URL") ?? SECLAI_API_URL;
239
- this.apiKeyHeader = opts.apiKeyHeader ?? "x-api-key";
527
+ this.baseUrl = opts.baseUrl ?? getEnv2("SECLAI_API_URL") ?? SECLAI_API_URL;
240
528
  this.defaultHeaders = { ...opts.defaultHeaders ?? {} };
241
529
  this.fetcher = fetcher;
530
+ const accessTokenProvider = typeof opts.accessToken === "function" ? opts.accessToken : void 0;
531
+ const accessTokenStatic = typeof opts.accessToken === "string" ? opts.accessToken : void 0;
532
+ this._authInitPromise = resolveCredentialChain({
533
+ apiKey: opts.apiKey,
534
+ accessToken: accessTokenStatic,
535
+ accessTokenProvider,
536
+ profile: opts.profile,
537
+ configDir: opts.configDir,
538
+ autoRefresh: opts.autoRefresh,
539
+ accountId: opts.accountId,
540
+ apiKeyHeader: opts.apiKeyHeader,
541
+ fetch: fetcher
542
+ }).then((state) => {
543
+ this._authState = state;
544
+ }).catch((err) => {
545
+ this._authInitError = new SeclaiConfigurationError(
546
+ err instanceof Error ? err.message : String(err)
547
+ );
548
+ });
549
+ }
550
+ /** Ensure the credential chain has been resolved. */
551
+ async ensureAuth() {
552
+ if (this._authInitPromise) {
553
+ await this._authInitPromise;
554
+ this._authInitPromise = null;
555
+ }
556
+ if (this._authInitError) {
557
+ throw this._authInitError;
558
+ }
559
+ if (!this._authState) {
560
+ throw new SeclaiConfigurationError(
561
+ "Missing credentials. Provide apiKey, accessToken, set SECLAI_API_KEY, or run `seclai auth login`."
562
+ );
563
+ }
564
+ return this._authState;
565
+ }
566
+ /** Resolve auth headers for the current request. */
567
+ async authHeaders() {
568
+ const state = await this.ensureAuth();
569
+ return resolveAuthHeaders(state);
242
570
  }
243
571
  // ═══════════════════════════════════════════════════════════════════════════
244
572
  // Low-level request
@@ -257,10 +585,11 @@ var Seclai = class {
257
585
  */
258
586
  async request(method, path, opts) {
259
587
  const url = buildURL(this.baseUrl, path, opts?.query);
588
+ const authHeaders = await this.authHeaders();
260
589
  const headers = {
261
590
  ...this.defaultHeaders,
262
591
  ...opts?.headers ?? {},
263
- [this.apiKeyHeader]: this.apiKey
592
+ ...authHeaders
264
593
  };
265
594
  let body;
266
595
  if (opts?.json !== void 0) {
@@ -312,13 +641,16 @@ var Seclai = class {
312
641
  * @param path - Request path relative to `baseUrl`.
313
642
  * @param opts - Query params, JSON body, per-request headers, and optional AbortSignal.
314
643
  * @returns The raw `Response` object.
644
+ * @throws {SeclaiAPIValidationError} On HTTP 422 responses.
645
+ * @throws {SeclaiAPIStatusError} On other non-2xx responses.
315
646
  */
316
647
  async requestRaw(method, path, opts) {
317
648
  const url = buildURL(this.baseUrl, path, opts?.query);
649
+ const authHeaders = await this.authHeaders();
318
650
  const headers = {
319
651
  ...this.defaultHeaders,
320
652
  ...opts?.headers ?? {},
321
- [this.apiKeyHeader]: this.apiKey
653
+ ...authHeaders
322
654
  };
323
655
  let body;
324
656
  if (opts?.json !== void 0) {
@@ -355,9 +687,10 @@ var Seclai = class {
355
687
  /** Shared multipart upload helper. */
356
688
  async uploadFile(path, opts) {
357
689
  const url = buildURL(this.baseUrl, path);
690
+ const authHeaders = await this.authHeaders();
358
691
  const headers = {
359
692
  ...this.defaultHeaders,
360
- [this.apiKeyHeader]: this.apiKey
693
+ ...authHeaders
361
694
  };
362
695
  delete headers["content-type"];
363
696
  delete headers["Content-Type"];
@@ -545,9 +878,10 @@ var Seclai = class {
545
878
  */
546
879
  async runStreamingAgentAndWait(agentId, body, opts) {
547
880
  const url = buildURL(this.baseUrl, `/agents/${agentId}/runs/stream`);
881
+ const authHdrs = await this.authHeaders();
548
882
  const headers = {
549
883
  ...this.defaultHeaders,
550
- [this.apiKeyHeader]: this.apiKey,
884
+ ...authHdrs,
551
885
  accept: "text/event-stream",
552
886
  "content-type": "application/json"
553
887
  };
@@ -654,9 +988,10 @@ var Seclai = class {
654
988
  */
655
989
  async *runStreamingAgent(agentId, body, opts) {
656
990
  const url = buildURL(this.baseUrl, `/agents/${agentId}/runs/stream`);
991
+ const authHdrs = await this.authHeaders();
657
992
  const headers = {
658
993
  ...this.defaultHeaders,
659
- [this.apiKeyHeader]: this.apiKey,
994
+ ...authHdrs,
660
995
  accept: "text/event-stream",
661
996
  "content-type": "application/json"
662
997
  };