@blaxel/core 0.2.86-preview.165 → 0.2.87-dev.173
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 +37 -2
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/authentication/credentials.js +11 -1
- package/dist/cjs/authentication/index.js +3 -1
- package/dist/cjs/client/sdk.gen.js +63 -12
- package/dist/cjs/common/credentials.test.js +71 -0
- package/dist/cjs/common/errors.js +15 -0
- package/dist/cjs/common/h2fetch.js +101 -50
- package/dist/cjs/common/h2pool.js +9 -0
- package/dist/cjs/common/pagination.js +87 -0
- package/dist/cjs/common/pagination.test.js +62 -0
- package/dist/cjs/common/settings.js +41 -3
- package/dist/cjs/common/settings.test.js +12 -3
- package/dist/cjs/drive/index.js +39 -3
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/jobs/jobs.js +42 -9
- package/dist/cjs/sandbox/client/sdk.gen.js +1 -1
- package/dist/cjs/sandbox/preview.js +8 -3
- package/dist/cjs/sandbox/sandbox.js +41 -4
- package/dist/cjs/types/authentication/credentials.d.ts +9 -0
- package/dist/cjs/types/client/sdk.gen.d.ts +34 -19
- package/dist/cjs/types/client/types.gen.d.ts +704 -22
- package/dist/cjs/types/common/credentials.test.d.ts +1 -0
- package/dist/cjs/types/common/errors.d.ts +10 -0
- package/dist/cjs/types/common/h2fetch.d.ts +9 -4
- package/dist/cjs/types/common/pagination.d.ts +35 -0
- package/dist/cjs/types/common/pagination.test.d.ts +1 -0
- package/dist/cjs/types/common/settings.d.ts +9 -0
- package/dist/cjs/types/drive/index.d.ts +35 -4
- package/dist/cjs/types/index.d.ts +1 -0
- package/dist/cjs/types/jobs/jobs.d.ts +33 -3
- package/dist/cjs/types/sandbox/client/sdk.gen.d.ts +1 -1
- package/dist/cjs/types/sandbox/client/types.gen.d.ts +24 -0
- package/dist/cjs/types/sandbox/preview.d.ts +2 -2
- package/dist/cjs/types/sandbox/sandbox.d.ts +36 -2
- package/dist/cjs/types/volume/index.d.ts +37 -4
- package/dist/cjs/volume/index.js +41 -3
- package/dist/cjs-browser/.tsbuildinfo +1 -1
- package/dist/cjs-browser/authentication/credentials.js +11 -1
- package/dist/cjs-browser/authentication/index.js +3 -1
- package/dist/cjs-browser/client/sdk.gen.js +63 -12
- package/dist/cjs-browser/common/credentials.test.js +71 -0
- package/dist/cjs-browser/common/errors.js +15 -0
- package/dist/cjs-browser/common/pagination.js +87 -0
- package/dist/cjs-browser/common/pagination.test.js +62 -0
- package/dist/cjs-browser/common/settings.js +41 -3
- package/dist/cjs-browser/common/settings.test.js +12 -3
- package/dist/cjs-browser/drive/index.js +39 -3
- package/dist/cjs-browser/index.js +1 -0
- package/dist/cjs-browser/jobs/jobs.js +42 -9
- package/dist/cjs-browser/sandbox/client/sdk.gen.js +1 -1
- package/dist/cjs-browser/sandbox/preview.js +8 -3
- package/dist/cjs-browser/sandbox/sandbox.js +41 -4
- package/dist/cjs-browser/types/authentication/credentials.d.ts +9 -0
- package/dist/cjs-browser/types/client/sdk.gen.d.ts +34 -19
- package/dist/cjs-browser/types/client/types.gen.d.ts +704 -22
- package/dist/cjs-browser/types/common/credentials.test.d.ts +1 -0
- package/dist/cjs-browser/types/common/errors.d.ts +10 -0
- package/dist/cjs-browser/types/common/h2fetch.d.ts +9 -4
- package/dist/cjs-browser/types/common/pagination.d.ts +35 -0
- package/dist/cjs-browser/types/common/pagination.test.d.ts +1 -0
- package/dist/cjs-browser/types/common/settings.d.ts +9 -0
- package/dist/cjs-browser/types/drive/index.d.ts +35 -4
- package/dist/cjs-browser/types/index.d.ts +1 -0
- package/dist/cjs-browser/types/jobs/jobs.d.ts +33 -3
- package/dist/cjs-browser/types/sandbox/client/sdk.gen.d.ts +1 -1
- package/dist/cjs-browser/types/sandbox/client/types.gen.d.ts +24 -0
- package/dist/cjs-browser/types/sandbox/preview.d.ts +2 -2
- package/dist/cjs-browser/types/sandbox/sandbox.d.ts +36 -2
- package/dist/cjs-browser/types/volume/index.d.ts +37 -4
- package/dist/cjs-browser/volume/index.js +41 -3
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/authentication/credentials.js +9 -0
- package/dist/esm/authentication/index.js +4 -2
- package/dist/esm/client/sdk.gen.js +57 -9
- package/dist/esm/common/credentials.test.js +69 -0
- package/dist/esm/common/errors.js +13 -0
- package/dist/esm/common/h2fetch.js +101 -50
- package/dist/esm/common/h2pool.js +9 -0
- package/dist/esm/common/pagination.js +83 -0
- package/dist/esm/common/pagination.test.js +60 -0
- package/dist/esm/common/settings.js +41 -3
- package/dist/esm/common/settings.test.js +12 -3
- package/dist/esm/drive/index.js +39 -3
- package/dist/esm/index.js +1 -0
- package/dist/esm/jobs/jobs.js +42 -9
- package/dist/esm/sandbox/client/sdk.gen.js +1 -1
- package/dist/esm/sandbox/preview.js +8 -3
- package/dist/esm/sandbox/sandbox.js +41 -4
- package/dist/esm/volume/index.js +41 -3
- package/dist/esm-browser/.tsbuildinfo +1 -1
- package/dist/esm-browser/authentication/credentials.js +9 -0
- package/dist/esm-browser/authentication/index.js +4 -2
- package/dist/esm-browser/client/sdk.gen.js +57 -9
- package/dist/esm-browser/common/credentials.test.js +69 -0
- package/dist/esm-browser/common/errors.js +13 -0
- package/dist/esm-browser/common/pagination.js +83 -0
- package/dist/esm-browser/common/pagination.test.js +60 -0
- package/dist/esm-browser/common/settings.js +41 -3
- package/dist/esm-browser/common/settings.test.js +12 -3
- package/dist/esm-browser/drive/index.js +39 -3
- package/dist/esm-browser/index.js +1 -0
- package/dist/esm-browser/jobs/jobs.js +42 -9
- package/dist/esm-browser/sandbox/client/sdk.gen.js +1 -1
- package/dist/esm-browser/sandbox/preview.js +8 -3
- package/dist/esm-browser/sandbox/sandbox.js +41 -4
- package/dist/esm-browser/volume/index.js +41 -3
- package/package.json +1 -1
|
@@ -11,3 +11,12 @@ export class Credentials {
|
|
|
11
11
|
return "";
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Marker returned by `authentication()` when no usable Blaxel credentials were
|
|
16
|
+
* resolved (no env vars, no matching config workspace). Construction is
|
|
17
|
+
* side-effect free so importing `@blaxel/core` never fails; the actionable
|
|
18
|
+
* error is raised when an authenticated request is actually attempted
|
|
19
|
+
* (see `Settings.assertCredentials`).
|
|
20
|
+
*/
|
|
21
|
+
export class MissingCredentials extends Credentials {
|
|
22
|
+
}
|
|
@@ -3,7 +3,7 @@ import { env } from "../common/env.js";
|
|
|
3
3
|
import { fs, os, path } from "../common/node.js";
|
|
4
4
|
import { ApiKey } from "./apikey.js";
|
|
5
5
|
import { ClientCredentials } from "./clientcredentials.js";
|
|
6
|
-
import { Credentials } from "./credentials.js";
|
|
6
|
+
import { Credentials, MissingCredentials } from "./credentials.js";
|
|
7
7
|
import { DeviceMode } from "./deviceMode.js";
|
|
8
8
|
function getCredentials() {
|
|
9
9
|
if (env.BL_API_KEY) {
|
|
@@ -45,7 +45,9 @@ function getCredentials() {
|
|
|
45
45
|
export function authentication() {
|
|
46
46
|
const credentials = getCredentials();
|
|
47
47
|
if (!credentials) {
|
|
48
|
-
|
|
48
|
+
// Never silently fall back to empty headers: a marker that triggers a
|
|
49
|
+
// clear CredentialsError when an authenticated request is attempted.
|
|
50
|
+
return new MissingCredentials();
|
|
49
51
|
}
|
|
50
52
|
if (credentials.apiKey) {
|
|
51
53
|
return new ApiKey(credentials);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { client as _heyApiClient } from "./client.gen.js";
|
|
3
3
|
/**
|
|
4
4
|
* List all agents
|
|
5
|
-
* Returns
|
|
5
|
+
* Returns AI agents deployed in the workspace. Each agent includes its deployment status, runtime configuration, and global inference endpoint URL. Starting with API version 2026-04-28 the response is wrapped in `{data, meta}` and supports cursor pagination via the `cursor` and `limit` query parameters; older versions keep returning a bare array with all agents.
|
|
6
6
|
*/
|
|
7
7
|
export const listAgents = (options) => {
|
|
8
8
|
return (options?.client ?? _heyApiClient).get({
|
|
@@ -221,7 +221,7 @@ export const verifyCustomDomain = (options) => {
|
|
|
221
221
|
};
|
|
222
222
|
/**
|
|
223
223
|
* List drives
|
|
224
|
-
* Returns all drives in the workspace. Drives provide persistent storage that can be attached to agents, functions, and sandboxes.
|
|
224
|
+
* Returns all drives in the workspace. Drives provide persistent storage that can be attached to agents, functions, and sandboxes. Starting with API version 2026-04-28, the response wraps items in `{data, meta}` and supports cursor pagination via the `cursor` and `limit` query parameters; older versions keep returning a bare array with all drives.
|
|
225
225
|
*/
|
|
226
226
|
export const listDrives = (options) => {
|
|
227
227
|
return (options?.client ?? _heyApiClient).get({
|
|
@@ -348,6 +348,22 @@ export const listAllEgressGateways = (options) => {
|
|
|
348
348
|
...options
|
|
349
349
|
});
|
|
350
350
|
};
|
|
351
|
+
/**
|
|
352
|
+
* Egress gateway sandbox attachments
|
|
353
|
+
* Returns the inverse map (gateway → sandbox names) for the workspace. Used by the egress-IPs UI to render attachment counts without fetching the sandboxes listing full client-side.
|
|
354
|
+
*/
|
|
355
|
+
export const getEgressGatewayUsage = (options) => {
|
|
356
|
+
return (options?.client ?? _heyApiClient).get({
|
|
357
|
+
security: [
|
|
358
|
+
{
|
|
359
|
+
scheme: 'bearer',
|
|
360
|
+
type: 'http'
|
|
361
|
+
}
|
|
362
|
+
],
|
|
363
|
+
url: '/egressgateways/usage',
|
|
364
|
+
...options
|
|
365
|
+
});
|
|
366
|
+
};
|
|
351
367
|
/**
|
|
352
368
|
* List all egress IPs across all VPCs and gateways in the workspace
|
|
353
369
|
*/
|
|
@@ -397,7 +413,7 @@ export const testFeatureFlag = (options) => {
|
|
|
397
413
|
};
|
|
398
414
|
/**
|
|
399
415
|
* List all MCP servers
|
|
400
|
-
* Returns
|
|
416
|
+
* Returns MCP server functions deployed in the workspace. Each function includes its deployment status, transport protocol (websocket or http-stream), and endpoint URL. Starting with API version 2026-04-28 the response is wrapped in `{data, meta}` and supports cursor pagination via the `cursor` and `limit` query parameters; older versions keep returning a bare array with all functions.
|
|
401
417
|
*/
|
|
402
418
|
export const listFunctions = (options) => {
|
|
403
419
|
return (options?.client ?? _heyApiClient).get({
|
|
@@ -805,7 +821,7 @@ export const getIntegrationConnectionModel = (options) => {
|
|
|
805
821
|
};
|
|
806
822
|
/**
|
|
807
823
|
* List batch jobs
|
|
808
|
-
* Returns
|
|
824
|
+
* Returns batch job definitions in the workspace. Each job can be triggered to run multiple parallel tasks with configurable concurrency and retry settings. Starting with API version 2026-04-28 the response is wrapped in `{data, meta}` and supports cursor pagination via the `cursor` and `limit` query parameters; older versions keep returning a bare array with all jobs.
|
|
809
825
|
*/
|
|
810
826
|
export const listJobs = (options) => {
|
|
811
827
|
return (options?.client ?? _heyApiClient).get({
|
|
@@ -893,7 +909,7 @@ export const updateJob = (options) => {
|
|
|
893
909
|
};
|
|
894
910
|
/**
|
|
895
911
|
* List job executions
|
|
896
|
-
* Returns
|
|
912
|
+
* Returns executions for a batch job. Starting with API version 2026-04-28 the response is wrapped in `{data, meta}` and supports cursor pagination via the `cursor` and `limit` query parameters; older versions keep the legacy offset/limit contract and return a bare array.
|
|
897
913
|
*/
|
|
898
914
|
export const listJobExecutions = (options) => {
|
|
899
915
|
return (options.client ?? _heyApiClient).get({
|
|
@@ -959,6 +975,22 @@ export const getJobExecution = (options) => {
|
|
|
959
975
|
...options
|
|
960
976
|
});
|
|
961
977
|
};
|
|
978
|
+
/**
|
|
979
|
+
* List execution tasks
|
|
980
|
+
* Returns one cursor-paginated page of an execution's tasks. Tasks are derived from event history each request; only the in-memory slicing is paginated, the events scan still fetches the whole event log behind the scenes. Available starting with API version 2026-04-28.
|
|
981
|
+
*/
|
|
982
|
+
export const listJobExecutionTasks = (options) => {
|
|
983
|
+
return (options.client ?? _heyApiClient).get({
|
|
984
|
+
security: [
|
|
985
|
+
{
|
|
986
|
+
scheme: 'bearer',
|
|
987
|
+
type: 'http'
|
|
988
|
+
}
|
|
989
|
+
],
|
|
990
|
+
url: '/jobs/{jobId}/executions/{executionId}/tasks',
|
|
991
|
+
...options
|
|
992
|
+
});
|
|
993
|
+
};
|
|
962
994
|
/**
|
|
963
995
|
* List job revisions
|
|
964
996
|
* Returns revisions for a job by name.
|
|
@@ -1009,7 +1041,7 @@ export const listMcpHubDefinitions = (options) => {
|
|
|
1009
1041
|
};
|
|
1010
1042
|
/**
|
|
1011
1043
|
* List model endpoints
|
|
1012
|
-
* Returns
|
|
1044
|
+
* Returns model gateway endpoints configured in the workspace. Each model represents a proxy to an external LLM provider (OpenAI, Anthropic, etc.) with unified access control. Starting with API version 2026-04-28 the response is wrapped in `{data, meta}` and supports cursor pagination via the `cursor` and `limit` query parameters; older versions keep returning a bare array with all models.
|
|
1013
1045
|
*/
|
|
1014
1046
|
export const listModels = (options) => {
|
|
1015
1047
|
return (options?.client ?? _heyApiClient).get({
|
|
@@ -1161,7 +1193,7 @@ export const declineImageShare = (options) => {
|
|
|
1161
1193
|
};
|
|
1162
1194
|
/**
|
|
1163
1195
|
* List governance policies
|
|
1164
|
-
* Returns
|
|
1196
|
+
* Returns governance policies in the workspace. Policies control deployment locations, hardware flavors, and token limits for agents, functions, and models. Starting with API version 2026-04-28 the response is wrapped in `{data, meta}` and supports cursor pagination via the `cursor` and `limit` query parameters; older versions keep returning a bare array with all policies.
|
|
1165
1197
|
*/
|
|
1166
1198
|
export const listPolicies = (options) => {
|
|
1167
1199
|
return (options?.client ?? _heyApiClient).get({
|
|
@@ -1247,6 +1279,22 @@ export const updatePolicy = (options) => {
|
|
|
1247
1279
|
}
|
|
1248
1280
|
});
|
|
1249
1281
|
};
|
|
1282
|
+
/**
|
|
1283
|
+
* List resources using a policy
|
|
1284
|
+
* Returns the names of every resource (agent, function, model, sandbox, job) currently referencing the given policy. Replaces the client-side fan-out the policies UI used to do over the listings.
|
|
1285
|
+
*/
|
|
1286
|
+
export const getPolicyUsages = (options) => {
|
|
1287
|
+
return (options.client ?? _heyApiClient).get({
|
|
1288
|
+
security: [
|
|
1289
|
+
{
|
|
1290
|
+
scheme: 'bearer',
|
|
1291
|
+
type: 'http'
|
|
1292
|
+
}
|
|
1293
|
+
],
|
|
1294
|
+
url: '/policies/{policyName}/usages',
|
|
1295
|
+
...options
|
|
1296
|
+
});
|
|
1297
|
+
};
|
|
1250
1298
|
/**
|
|
1251
1299
|
* List public ips
|
|
1252
1300
|
* Returns a list of all public ips used in Blaxel..
|
|
@@ -1281,7 +1329,7 @@ export const listSandboxHubDefinitions = (options) => {
|
|
|
1281
1329
|
};
|
|
1282
1330
|
/**
|
|
1283
1331
|
* List sandboxes
|
|
1284
|
-
* Returns
|
|
1332
|
+
* Returns sandboxes in the workspace. Each sandbox includes its configuration, status, and endpoint URL. Terminated sandboxes are hidden by default; pass `showTerminated=true` to include them. Starting with API version 2026-04-28 the response is wrapped in `{data, meta}` and supports cursor pagination via the `cursor` and `limit` query parameters; older versions keep returning a bare array of all sandboxes.
|
|
1285
1333
|
*/
|
|
1286
1334
|
export const listSandboxes = (options) => {
|
|
1287
1335
|
return (options?.client ?? _heyApiClient).get({
|
|
@@ -1841,7 +1889,7 @@ export const deleteVolumeTemplateVersion = (options) => {
|
|
|
1841
1889
|
};
|
|
1842
1890
|
/**
|
|
1843
1891
|
* List persistent volumes
|
|
1844
|
-
* Returns
|
|
1892
|
+
* Returns persistent storage volumes in the workspace. Volumes can be attached to sandboxes for durable file storage that persists across sessions and sandbox deletions. Starting with API version 2026-04-28 the response is wrapped in `{data, meta}` and supports cursor pagination via the `cursor` and `limit` query parameters; older versions keep returning a bare array of volumes.
|
|
1845
1893
|
*/
|
|
1846
1894
|
export const listVolumes = (options) => {
|
|
1847
1895
|
return (options?.client ?? _heyApiClient).get({
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { ApiKey } from '../authentication/apikey.js';
|
|
3
|
+
import { MissingCredentials } from '../authentication/credentials.js';
|
|
4
|
+
import { authentication } from '../authentication/index.js';
|
|
5
|
+
import { CredentialsError } from './errors.js';
|
|
6
|
+
import { settings } from './settings.js';
|
|
7
|
+
/**
|
|
8
|
+
* ENG-2698: missing / partial BL_WORKSPACE / BL_API_KEY must fail fast with a
|
|
9
|
+
* clear, actionable error instead of silently sending empty headers and
|
|
10
|
+
* surfacing the server's misleading "workspace is required".
|
|
11
|
+
*
|
|
12
|
+
* `env` reads through to `process.env`, so env state is controlled there.
|
|
13
|
+
*/
|
|
14
|
+
describe('credential validation', () => {
|
|
15
|
+
const AUTH_ENV = ['BL_API_KEY', 'BL_WORKSPACE', 'BL_CLIENT_CREDENTIALS'];
|
|
16
|
+
let savedEnv;
|
|
17
|
+
let savedCredentials;
|
|
18
|
+
let savedConfig;
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
savedEnv = {};
|
|
21
|
+
for (const key of AUTH_ENV) {
|
|
22
|
+
savedEnv[key] = process.env[key];
|
|
23
|
+
delete process.env[key];
|
|
24
|
+
}
|
|
25
|
+
savedCredentials = settings.credentials;
|
|
26
|
+
savedConfig = settings.config;
|
|
27
|
+
settings.config = { proxy: '', apikey: '', workspace: '' };
|
|
28
|
+
});
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
for (const key of AUTH_ENV) {
|
|
31
|
+
if (savedEnv[key] === undefined)
|
|
32
|
+
delete process.env[key];
|
|
33
|
+
else
|
|
34
|
+
process.env[key] = savedEnv[key];
|
|
35
|
+
}
|
|
36
|
+
settings.credentials = savedCredentials;
|
|
37
|
+
settings.config = savedConfig;
|
|
38
|
+
});
|
|
39
|
+
it('API key without workspace throws a clear BL_WORKSPACE error from headers', () => {
|
|
40
|
+
settings.credentials = new ApiKey({ apiKey: 'test-key' });
|
|
41
|
+
expect(() => settings.headers).toThrow(CredentialsError);
|
|
42
|
+
expect(() => settings.headers).toThrow(/BL_WORKSPACE/);
|
|
43
|
+
});
|
|
44
|
+
it('API key without workspace rejects from authenticate()', async () => {
|
|
45
|
+
settings.credentials = new ApiKey({ apiKey: 'test-key' });
|
|
46
|
+
await expect(settings.authenticate()).rejects.toThrow(/BL_WORKSPACE/);
|
|
47
|
+
});
|
|
48
|
+
it('workspace set but no API key names the missing API key', () => {
|
|
49
|
+
process.env.BL_WORKSPACE = 'my-workspace';
|
|
50
|
+
settings.credentials = new MissingCredentials();
|
|
51
|
+
expect(() => settings.headers).toThrow(/API key is missing/);
|
|
52
|
+
});
|
|
53
|
+
it('no credentials at all names both env vars', () => {
|
|
54
|
+
settings.credentials = new MissingCredentials();
|
|
55
|
+
expect(() => settings.headers).toThrow(CredentialsError);
|
|
56
|
+
expect(() => settings.headers).toThrow(/BL_API_KEY and BL_WORKSPACE/);
|
|
57
|
+
});
|
|
58
|
+
it('api key + workspace builds clean headers with no empty values', () => {
|
|
59
|
+
settings.credentials = new ApiKey({ apiKey: 'test-key', workspace: 'my-workspace' });
|
|
60
|
+
const headers = settings.headers;
|
|
61
|
+
expect(headers['x-blaxel-workspace']).toBe('my-workspace');
|
|
62
|
+
expect(headers['x-blaxel-authorization']).toBe('Bearer test-key');
|
|
63
|
+
expect(Object.values(headers)).not.toContain('');
|
|
64
|
+
});
|
|
65
|
+
it('authentication() returns a real ApiKey when BL_API_KEY is set', () => {
|
|
66
|
+
process.env.BL_API_KEY = 'test-key';
|
|
67
|
+
expect(authentication()).toBeInstanceOf(ApiKey);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when Blaxel credentials are missing or incomplete.
|
|
3
|
+
*
|
|
4
|
+
* Surfaced eagerly with an actionable message (which env var / login step is
|
|
5
|
+
* missing) instead of silently sending empty workspace/authorization headers
|
|
6
|
+
* and letting the user hit a misleading server-side "workspace is required".
|
|
7
|
+
*/
|
|
8
|
+
export class CredentialsError extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "CredentialsError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
1
14
|
export function handleDynamicImportError(err) {
|
|
2
15
|
if (err instanceof Error) {
|
|
3
16
|
// We check if it's module import error and retrieve package name from the error message with a regex
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export function unwrapListData(response) {
|
|
2
|
+
if (!response) {
|
|
3
|
+
return [];
|
|
4
|
+
}
|
|
5
|
+
if (Array.isArray(response)) {
|
|
6
|
+
return response;
|
|
7
|
+
}
|
|
8
|
+
return response.data ?? [];
|
|
9
|
+
}
|
|
10
|
+
function unwrapListMeta(response) {
|
|
11
|
+
if (!response || Array.isArray(response)) {
|
|
12
|
+
return { hasMore: false };
|
|
13
|
+
}
|
|
14
|
+
return response.meta ?? { hasMore: false };
|
|
15
|
+
}
|
|
16
|
+
export async function createPaginatedList({ response, fetchPage, mapItem, query, seenCursors, }) {
|
|
17
|
+
const meta = unwrapListMeta(response);
|
|
18
|
+
const data = await Promise.all(unwrapListData(response).map(mapItem));
|
|
19
|
+
const cursors = new Set(seenCursors);
|
|
20
|
+
if (query?.cursor) {
|
|
21
|
+
cursors.add(query.cursor);
|
|
22
|
+
}
|
|
23
|
+
const list = {
|
|
24
|
+
data,
|
|
25
|
+
meta,
|
|
26
|
+
get hasMore() {
|
|
27
|
+
return Boolean(meta.hasMore);
|
|
28
|
+
},
|
|
29
|
+
get nextCursor() {
|
|
30
|
+
return meta.nextCursor || undefined;
|
|
31
|
+
},
|
|
32
|
+
async nextPage() {
|
|
33
|
+
const cursor = list.nextCursor;
|
|
34
|
+
if (!cursor) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (cursors.has(cursor)) {
|
|
38
|
+
throw new Error("Pagination returned a repeated cursor");
|
|
39
|
+
}
|
|
40
|
+
const nextQuery = { ...(query ?? {}), cursor };
|
|
41
|
+
const nextSeenCursors = new Set(cursors);
|
|
42
|
+
nextSeenCursors.add(cursor);
|
|
43
|
+
return createPaginatedList({
|
|
44
|
+
response: await fetchPage(nextQuery),
|
|
45
|
+
fetchPage,
|
|
46
|
+
mapItem,
|
|
47
|
+
query: nextQuery,
|
|
48
|
+
seenCursors: nextSeenCursors,
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
async autoPagingEach(onItem) {
|
|
52
|
+
for await (const item of list) {
|
|
53
|
+
const shouldContinue = await onItem(item);
|
|
54
|
+
if (shouldContinue === false) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
async autoPagingToArray(options) {
|
|
60
|
+
if (!options || !Number.isFinite(options.limit) || options.limit <= 0) {
|
|
61
|
+
throw new Error("autoPagingToArray requires a positive limit");
|
|
62
|
+
}
|
|
63
|
+
const items = [];
|
|
64
|
+
for await (const item of list) {
|
|
65
|
+
items.push(item);
|
|
66
|
+
if (items.length >= options.limit) {
|
|
67
|
+
return items;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return items;
|
|
71
|
+
},
|
|
72
|
+
async *[Symbol.asyncIterator]() {
|
|
73
|
+
let page = list;
|
|
74
|
+
while (page) {
|
|
75
|
+
for (const item of page.data) {
|
|
76
|
+
yield item;
|
|
77
|
+
}
|
|
78
|
+
page = await page.nextPage();
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
return list;
|
|
83
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createPaginatedList, unwrapListData } from "./pagination.js";
|
|
3
|
+
describe("pagination helpers", () => {
|
|
4
|
+
it("unwraps legacy array and paginated list responses", () => {
|
|
5
|
+
expect(unwrapListData([{ name: "legacy" }])).toEqual([{ name: "legacy" }]);
|
|
6
|
+
expect(unwrapListData({
|
|
7
|
+
data: [{ name: "paginated" }],
|
|
8
|
+
meta: { hasMore: false },
|
|
9
|
+
})).toEqual([{ name: "paginated" }]);
|
|
10
|
+
});
|
|
11
|
+
it("returns page data and lets callers request the next page", async () => {
|
|
12
|
+
const cursors = [];
|
|
13
|
+
const fetchPage = async (query) => {
|
|
14
|
+
await Promise.resolve();
|
|
15
|
+
cursors.push(query?.cursor);
|
|
16
|
+
if (!query?.cursor) {
|
|
17
|
+
return {
|
|
18
|
+
data: ["first"],
|
|
19
|
+
meta: { hasMore: true, nextCursor: "next-page" },
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
data: ["second"],
|
|
24
|
+
meta: { hasMore: false },
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
const page = await createPaginatedList({
|
|
28
|
+
response: await fetchPage(),
|
|
29
|
+
fetchPage,
|
|
30
|
+
mapItem: (item) => item.toUpperCase(),
|
|
31
|
+
});
|
|
32
|
+
expect(page.data).toEqual(["FIRST"]);
|
|
33
|
+
expect(page.hasMore).toBe(true);
|
|
34
|
+
expect(page.nextCursor).toBe("next-page");
|
|
35
|
+
const nextPage = await page.nextPage();
|
|
36
|
+
expect(nextPage?.data).toEqual(["SECOND"]);
|
|
37
|
+
expect(cursors).toEqual([undefined, "next-page"]);
|
|
38
|
+
});
|
|
39
|
+
it("supports auto paging with an explicit limit", async () => {
|
|
40
|
+
const fetchPage = async (query) => {
|
|
41
|
+
await Promise.resolve();
|
|
42
|
+
if (!query?.cursor) {
|
|
43
|
+
return {
|
|
44
|
+
data: ["first"],
|
|
45
|
+
meta: { hasMore: true, nextCursor: "next-page" },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
data: ["second"],
|
|
50
|
+
meta: { hasMore: false },
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
const page = await createPaginatedList({
|
|
54
|
+
response: await fetchPage(),
|
|
55
|
+
fetchPage,
|
|
56
|
+
mapItem: (item) => item,
|
|
57
|
+
});
|
|
58
|
+
await expect(page.autoPagingToArray({ limit: 2 })).resolves.toEqual(["first", "second"]);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
import yaml from 'yaml';
|
|
2
2
|
import { ApiKey } from "../authentication/apikey.js";
|
|
3
3
|
import { ClientCredentials } from "../authentication/clientcredentials.js";
|
|
4
|
+
import { MissingCredentials } from "../authentication/credentials.js";
|
|
4
5
|
import { authentication } from "../authentication/index.js";
|
|
5
6
|
import { env } from "../common/env.js";
|
|
7
|
+
import { CredentialsError } from "../common/errors.js";
|
|
6
8
|
import { fs, os, path } from "../common/node.js";
|
|
9
|
+
/**
|
|
10
|
+
* Build an actionable "credentials missing" message naming exactly which piece
|
|
11
|
+
* is absent, based on the env vars currently set.
|
|
12
|
+
*/
|
|
13
|
+
function missingCredentialsMessage() {
|
|
14
|
+
const hasWorkspace = !!env.BL_WORKSPACE;
|
|
15
|
+
const hasApiKey = !!env.BL_API_KEY;
|
|
16
|
+
if (hasWorkspace && !hasApiKey) {
|
|
17
|
+
return "Blaxel API key is missing. Set the BL_API_KEY environment variable, or run `bl login`, to authenticate (BL_WORKSPACE is already set).";
|
|
18
|
+
}
|
|
19
|
+
if (hasApiKey && !hasWorkspace) {
|
|
20
|
+
return "Blaxel workspace is missing. Set the BL_WORKSPACE environment variable, or run `bl login`, to authenticate (BL_API_KEY is already set).";
|
|
21
|
+
}
|
|
22
|
+
return "No Blaxel credentials found. Set the BL_API_KEY and BL_WORKSPACE environment variables, or run `bl login`.";
|
|
23
|
+
}
|
|
7
24
|
// Build info - these placeholders are replaced at build time by build:replace-imports
|
|
8
|
-
const BUILD_VERSION = "0.2.
|
|
9
|
-
const BUILD_COMMIT = "
|
|
25
|
+
const BUILD_VERSION = "0.2.87-dev.173";
|
|
26
|
+
const BUILD_COMMIT = "43df206258fd8138bafddefa56333aadab2be731";
|
|
10
27
|
const BUILD_SENTRY_DSN = "https://fd5e60e1c9820e1eef5ccebb84a07127@o4508714045276160.ingest.us.sentry.io/4510465864564736";
|
|
11
|
-
const BLAXEL_API_VERSION = "2026-04-
|
|
28
|
+
const BLAXEL_API_VERSION = "2026-04-28";
|
|
12
29
|
// Cache for config.yaml tracking value
|
|
13
30
|
let configTrackingValue = null;
|
|
14
31
|
let configTrackingLoaded = false;
|
|
@@ -167,6 +184,7 @@ class Settings {
|
|
|
167
184
|
return env.BL_API_VERSION || BLAXEL_API_VERSION;
|
|
168
185
|
}
|
|
169
186
|
get headers() {
|
|
187
|
+
this.assertCredentials();
|
|
170
188
|
const osArch = getOsArch();
|
|
171
189
|
return {
|
|
172
190
|
"x-blaxel-authorization": this.authorization,
|
|
@@ -252,7 +270,27 @@ class Settings {
|
|
|
252
270
|
}
|
|
253
271
|
return 0;
|
|
254
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* Fail fast with a clear, actionable error when credentials are missing or
|
|
275
|
+
* incomplete, instead of sending empty workspace/authorization headers and
|
|
276
|
+
* surfacing a misleading server-side "workspace is required". Skipped for
|
|
277
|
+
* `forceUrl` sandbox sessions, which carry their own headers and never read
|
|
278
|
+
* `settings.headers`. Leaves `workspace` itself non-throwing so telemetry
|
|
279
|
+
* tagging stays safe.
|
|
280
|
+
*/
|
|
281
|
+
assertCredentials() {
|
|
282
|
+
const hasConfigCredentials = !!(this.config.apikey ||
|
|
283
|
+
this.config.apiKey ||
|
|
284
|
+
this.config.clientCredentials);
|
|
285
|
+
if (!hasConfigCredentials && this.credentials instanceof MissingCredentials) {
|
|
286
|
+
throw new CredentialsError(missingCredentialsMessage());
|
|
287
|
+
}
|
|
288
|
+
if (!this.workspace) {
|
|
289
|
+
throw new CredentialsError("Blaxel workspace is missing. Set the BL_WORKSPACE environment variable, or run `bl login`, to authenticate your requests.");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
255
292
|
async authenticate() {
|
|
293
|
+
this.assertCredentials();
|
|
256
294
|
await this.credentials.authenticate();
|
|
257
295
|
}
|
|
258
296
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { ApiKey } from '../authentication/apikey.js';
|
|
2
3
|
import { env } from './env.js';
|
|
3
4
|
describe('Settings.apiVersion', () => {
|
|
4
5
|
beforeEach(() => {
|
|
@@ -7,14 +8,22 @@ describe('Settings.apiVersion', () => {
|
|
|
7
8
|
afterEach(() => {
|
|
8
9
|
delete env.BL_API_VERSION;
|
|
9
10
|
});
|
|
10
|
-
it('defaults to 2026-04-
|
|
11
|
+
it('defaults to 2026-04-28 when BL_API_VERSION is not set', async () => {
|
|
11
12
|
delete env.BL_API_VERSION;
|
|
12
13
|
const { settings } = await import('./settings.js');
|
|
13
|
-
expect(settings.apiVersion).toBe('2026-04-
|
|
14
|
+
expect(settings.apiVersion).toBe('2026-04-28');
|
|
14
15
|
});
|
|
15
16
|
it('headers include Blaxel-Version set to the default', async () => {
|
|
16
17
|
delete env.BL_API_VERSION;
|
|
17
18
|
const { settings } = await import('./settings.js');
|
|
18
|
-
|
|
19
|
+
// headers now requires resolvable credentials; run in an authenticated context
|
|
20
|
+
const previous = settings.credentials;
|
|
21
|
+
settings.credentials = new ApiKey({ apiKey: 'test-key', workspace: 'test-ws' });
|
|
22
|
+
try {
|
|
23
|
+
expect(settings.headers['Blaxel-Version']).toBe('2026-04-28');
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
settings.credentials = previous;
|
|
27
|
+
}
|
|
19
28
|
});
|
|
20
29
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { v4 as uuidv4 } from "uuid";
|
|
2
2
|
import { createDrive, deleteDrive, getDrive, listDrives, updateDrive } from "../client/index.js";
|
|
3
|
+
import { createPaginatedList } from "../common/pagination.js";
|
|
3
4
|
import { settings } from "../common/settings.js";
|
|
4
5
|
export class DriveInstance {
|
|
5
6
|
drive;
|
|
@@ -83,9 +84,44 @@ export class DriveInstance {
|
|
|
83
84
|
});
|
|
84
85
|
return new DriveInstance(data);
|
|
85
86
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
/**
|
|
88
|
+
* List one page of drives.
|
|
89
|
+
*
|
|
90
|
+
* The returned page exposes `data` for the current page, `meta` for cursor
|
|
91
|
+
* metadata, and helpers to fetch more pages only when you need them.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```ts
|
|
95
|
+
* const page = await DriveInstance.list({ limit: 50 });
|
|
96
|
+
*
|
|
97
|
+
* for (const drive of page.data) {
|
|
98
|
+
* console.log(drive.name);
|
|
99
|
+
* }
|
|
100
|
+
*
|
|
101
|
+
* const nextPage = await page.nextPage();
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* const allDrives = await (await DriveInstance.list()).autoPagingToArray({
|
|
107
|
+
* limit: 1000,
|
|
108
|
+
* });
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
static async list(query) {
|
|
112
|
+
const fetchPage = async (pageQuery) => {
|
|
113
|
+
const { data } = await listDrives({
|
|
114
|
+
query: pageQuery,
|
|
115
|
+
throwOnError: true,
|
|
116
|
+
});
|
|
117
|
+
return data;
|
|
118
|
+
};
|
|
119
|
+
return createPaginatedList({
|
|
120
|
+
response: await fetchPage(query),
|
|
121
|
+
fetchPage,
|
|
122
|
+
mapItem: (drive) => new DriveInstance(drive),
|
|
123
|
+
query,
|
|
124
|
+
});
|
|
89
125
|
}
|
|
90
126
|
static async delete(driveName) {
|
|
91
127
|
const { data } = await deleteDrive({
|
|
@@ -7,6 +7,7 @@ export * from "./common/node.js";
|
|
|
7
7
|
export * from "./common/errors.js";
|
|
8
8
|
export * from "./common/internal.js";
|
|
9
9
|
export * from "./common/logger.js";
|
|
10
|
+
export * from "./common/pagination.js";
|
|
10
11
|
export * from "./common/settings.js";
|
|
11
12
|
export * from "./common/webhook.js";
|
|
12
13
|
export * from "./drive/index.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createJobExecution, deleteJobExecution, getJobExecution, listJobExecutions, } from "../client/index.js";
|
|
2
2
|
import { logger } from "../common/logger.js";
|
|
3
|
+
import { createPaginatedList } from "../common/pagination.js";
|
|
3
4
|
import { settings } from "../common/settings.js";
|
|
4
5
|
import { startSpan } from "../telemetry/telemetry.js";
|
|
5
6
|
class BlJob {
|
|
@@ -75,18 +76,50 @@ class BlJob {
|
|
|
75
76
|
return data;
|
|
76
77
|
}
|
|
77
78
|
/**
|
|
78
|
-
* List
|
|
79
|
+
* List one page of executions for this job.
|
|
80
|
+
*
|
|
81
|
+
* The returned page exposes `data` for the current page, `meta` for cursor
|
|
82
|
+
* metadata, and helpers to fetch more pages only when you need them.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* const job = blJob("daily-import");
|
|
87
|
+
* const page = await job.listExecutions({ limit: 50 });
|
|
88
|
+
*
|
|
89
|
+
* for (const execution of page.data) {
|
|
90
|
+
* console.log(execution.status);
|
|
91
|
+
* }
|
|
92
|
+
*
|
|
93
|
+
* const nextPage = await page.nextPage();
|
|
94
|
+
* ```
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* const job = blJob("daily-import");
|
|
99
|
+
* const executions = await (await job.listExecutions()).autoPagingToArray({
|
|
100
|
+
* limit: 1000,
|
|
101
|
+
* });
|
|
102
|
+
* ```
|
|
79
103
|
*/
|
|
80
|
-
async listExecutions() {
|
|
104
|
+
async listExecutions(query) {
|
|
81
105
|
logger.debug(`Listing executions for job: ${this.jobName}`);
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
106
|
+
const fetchPage = async (pageQuery) => {
|
|
107
|
+
const { data } = await listJobExecutions({
|
|
108
|
+
path: {
|
|
109
|
+
jobId: this.jobName,
|
|
110
|
+
},
|
|
111
|
+
query: pageQuery,
|
|
112
|
+
headers: settings.headers,
|
|
113
|
+
throwOnError: true,
|
|
114
|
+
});
|
|
115
|
+
return data;
|
|
116
|
+
};
|
|
117
|
+
return createPaginatedList({
|
|
118
|
+
response: await fetchPage(query),
|
|
119
|
+
fetchPage,
|
|
120
|
+
mapItem: (execution) => execution,
|
|
121
|
+
query,
|
|
88
122
|
});
|
|
89
|
-
return data ?? [];
|
|
90
123
|
}
|
|
91
124
|
/**
|
|
92
125
|
* Get the status of a specific execution
|
|
@@ -86,7 +86,7 @@ export const getDrivesMount = (options) => {
|
|
|
86
86
|
};
|
|
87
87
|
/**
|
|
88
88
|
* Attach a drive to a local path
|
|
89
|
-
* Mounts an agent drive using the blfs binary to a local path, optionally mounting a subpath within the drive
|
|
89
|
+
* Mounts an agent drive using the blfs binary to a local path, optionally mounting a subpath within the drive. Supports optional UID/GID mapping to remap file ownership between the local sandbox and the filer (always mapped to filer UID/GID 0). Mapping values can be set per-request via uidMap/gidMap fields, or globally via BLFS_UID_MAP/BLFS_GID_MAP environment variables (request values take precedence).
|
|
90
90
|
*/
|
|
91
91
|
export const postDrivesMount = (options) => {
|
|
92
92
|
return (options.client ?? _heyApiClient).post({
|