@lizard-build/sdk 0.1.34 → 0.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -439,8 +439,10 @@ var SandboxClient = class {
439
439
  projectId,
440
440
  metadata: opts?.metadata,
441
441
  envs: opts?.envs,
442
+ region: opts?.region,
442
443
  volumeId: opts?.volumeId,
443
- volumeName: opts?.volumeName
444
+ volumeName: opts?.volumeName,
445
+ lizardToken: opts?.lizardToken
444
446
  })
445
447
  });
446
448
  if (!res.ok) await handleApiError(res);
@@ -531,8 +533,14 @@ var Sandbox = class extends SandboxClient {
531
533
  /**
532
534
  * Connect to an existing sandbox by its ID.
533
535
  *
534
- * If the sandbox is currently paused, it will be automatically resumed
535
- * before this call returns.
536
+ * Verifies the sandbox exists and is reachable, then returns a handle to it.
537
+ * Throws `NotFoundError` if it has been killed or has expired.
538
+ *
539
+ * This used to call `resume` first, on the assumption that a sandbox you are
540
+ * reconnecting to might be paused. Sandboxes are pods now and pause/resume is a
541
+ * `501` on every one of them, so that call turned every `connect()` into an
542
+ * error against a perfectly healthy sandbox. Connecting does not need to change
543
+ * a sandbox's state, so it no longer tries to.
536
544
  *
537
545
  * @example
538
546
  * ```ts
@@ -540,7 +548,7 @@ var Sandbox = class extends SandboxClient {
540
548
  * ```
541
549
  */
542
550
  static async connect(sandboxId, opts) {
543
- await SandboxClient.resumeSandbox(sandboxId, opts);
551
+ await SandboxClient.getSandboxInfo(sandboxId, opts);
544
552
  return new this({ sandboxId, ...opts });
545
553
  }
546
554
  /**
@@ -563,37 +571,23 @@ var Sandbox = class extends SandboxClient {
563
571
  return SandboxClient.killSandbox(this.sandboxId, this.resolveOpts(opts));
564
572
  }
565
573
  /**
566
- * Pause the sandbox microVM by freezing its vCPUs.
567
- *
568
- * Memory, filesystem, and running processes are held in the host's RAM —
569
- * nothing is written to disk, so a paused sandbox does not survive a host
570
- * failure. Resume with `sandbox.resume()` or `Sandbox.connect(id)`.
571
- *
572
- * Pausing does not buy you time past the original deadline. Resuming pushes the
573
- * expiry forward by however long the sandbox was paused — so a pause/resume cycle
574
- * costs no runtime — but that adjustment only happens on `resume()`. A sandbox left
575
- * paused past its original `expiresAt` is deleted there, within a minute, and
576
- * `resume()` then returns `false`.
577
- *
578
- * In other words: pause and come back *before* the original deadline and you lose
579
- * nothing; leave it paused across the deadline and it is gone. Pass `timeoutMs: 0`
580
- * at create time to opt out of expiry entirely, which is the only way to park a
581
- * sandbox indefinitely.
574
+ * Pause the sandbox by freezing it in place.
582
575
  *
583
- * @returns `true` if paused successfully.
576
+ * @deprecated Not implemented for the current runtime — always throws
577
+ * `LizardError` with HTTP 501. Sandboxes run as pods, and the equivalent is a CRIU
578
+ * checkpoint of the pod, which is not built. To park work across a gap, put it on a
579
+ * {@link Volume} and create a fresh sandbox on that volume later; the volume is the
580
+ * part that is meant to outlive a sandbox.
584
581
  */
585
582
  async pause(opts) {
586
583
  return SandboxClient.pauseSandbox(this.sandboxId, this.resolveOpts(opts));
587
584
  }
588
585
  /**
589
- * Resume a paused sandbox by unfreezing its vCPUs.
586
+ * Resume a paused sandbox.
590
587
  *
591
- * The expiry is pushed forward by the time spent paused, so the timeout measures
592
- * running time rather than wall-clock — provided the original deadline had not
593
- * already passed while paused. See {@link pause}.
594
- *
595
- * @returns `true` if resumed successfully, `false` if the sandbox no longer exists
596
- * (including because it expired while paused).
588
+ * @deprecated Not implemented for the current runtime — always throws
589
+ * `LizardError` with HTTP 501. See {@link pause}. `Sandbox.connect()` no longer
590
+ * calls this, so reconnecting to a running sandbox works without it.
597
591
  */
598
592
  async resume(opts) {
599
593
  return SandboxClient.resumeSandbox(this.sandboxId, this.resolveOpts(opts));
@@ -1049,13 +1043,233 @@ var MetricsAPI = class {
1049
1043
  }
1050
1044
  };
1051
1045
 
1046
+ // src/platform/workspaces.ts
1047
+ var WorkspacesAPI = class {
1048
+ constructor(client) {
1049
+ this.client = client;
1050
+ }
1051
+ /**
1052
+ * List workspaces the calling credential can see.
1053
+ *
1054
+ * A scoped API key sees only what it is scoped to: a workspace-scoped key returns
1055
+ * that one workspace, and a project-scoped key returns the workspace containing its
1056
+ * project. A full key returns every workspace the account belongs to.
1057
+ */
1058
+ list() {
1059
+ return this.client.get("/api/workspaces");
1060
+ }
1061
+ /** Create a workspace. The caller becomes its owner. */
1062
+ create(opts) {
1063
+ return this.client.post("/api/workspaces", opts);
1064
+ }
1065
+ /**
1066
+ * Delete a workspace.
1067
+ *
1068
+ * Empty-only by default — the server refuses while any project or sandbox remains,
1069
+ * so this cannot quietly destroy a user's work. Pass `{ force: true }` to delete a
1070
+ * workspace and everything in it; that is irreversible.
1071
+ */
1072
+ delete(id, opts) {
1073
+ const qs = opts?.force ? "" : "?requireEmpty=true";
1074
+ return this.client.delete(`/api/workspaces/${id}${qs}`);
1075
+ }
1076
+ /**
1077
+ * Find a workspace by name, slug, or id, or `null` if there is no such workspace.
1078
+ * Matching is exact; id wins, then slug, then name.
1079
+ */
1080
+ async find(nameOrSlugOrId) {
1081
+ const all = await this.list();
1082
+ return all.find((w) => w.id === nameOrSlugOrId) ?? all.find((w) => w.slug === nameOrSlugOrId) ?? all.find((w) => w.name === nameOrSlugOrId) ?? null;
1083
+ }
1084
+ };
1085
+
1086
+ // src/platform/api-keys.ts
1087
+ var ApiKeysAPI = class {
1088
+ constructor(client) {
1089
+ this.client = client;
1090
+ }
1091
+ /** List the account's API keys. Previews only — full keys are never returned here. */
1092
+ list() {
1093
+ return this.client.get("/api/account/api-keys");
1094
+ }
1095
+ /**
1096
+ * Create an API key.
1097
+ *
1098
+ * The response is the only place the full key appears. Omitting every scope creates
1099
+ * a full-access key; the server rejects an attempt to create one from a key that is
1100
+ * itself scoped.
1101
+ */
1102
+ create(opts) {
1103
+ const scopes = [
1104
+ ...opts.scopes ?? [],
1105
+ ...(opts.workspaces ?? []).map((id) => ({ type: "workspace", id })),
1106
+ ...(opts.projects ?? []).map((id) => ({ type: "project", id }))
1107
+ ];
1108
+ return this.client.post("/api/account/api-keys", { name: opts.name, scopes });
1109
+ }
1110
+ /** Revoke a key by id. It stops working immediately, everywhere. */
1111
+ delete(id) {
1112
+ return this.client.delete(`/api/account/api-keys/${id}`);
1113
+ }
1114
+ };
1115
+
1116
+ // src/platform/regions.ts
1117
+ var RegionsAPI = class {
1118
+ constructor(client) {
1119
+ this.client = client;
1120
+ }
1121
+ /** List every region, including ones not currently accepting workloads. */
1122
+ list() {
1123
+ return this.client.get("/api/regions");
1124
+ }
1125
+ };
1126
+
1127
+ // src/platform/billing.ts
1128
+ var BillingAPI = class {
1129
+ constructor(client) {
1130
+ this.client = client;
1131
+ }
1132
+ /** Current balance, status, burn rate, and runway. */
1133
+ balance() {
1134
+ return this.client.get("/api/billing/balance");
1135
+ }
1136
+ /** A page of balance transactions, newest first. */
1137
+ transactions(opts) {
1138
+ const qs = new URLSearchParams();
1139
+ if (opts?.limit !== void 0) qs.set("limit", String(opts.limit));
1140
+ if (opts?.cursor) qs.set("cursor", opts.cursor);
1141
+ if (opts?.includeUsage) qs.set("includeUsage", "1");
1142
+ const q = qs.toString();
1143
+ return this.client.get(`/api/billing/transactions${q ? `?${q}` : ""}`);
1144
+ }
1145
+ /** Cost summary for the current billing period, broken down by resource. */
1146
+ summary() {
1147
+ return this.client.get("/api/billing/summary");
1148
+ }
1149
+ /** Live (not-yet-invoiced) usage accumulating right now. */
1150
+ live() {
1151
+ return this.client.get("/api/billing/live");
1152
+ }
1153
+ };
1154
+
1155
+ // src/volume/index.ts
1156
+ var Volume = class _Volume {
1157
+ constructor(opts) {
1158
+ this.volumeId = opts.volumeId;
1159
+ this.name = opts.name;
1160
+ this.config = new ConnectionConfig(opts);
1161
+ }
1162
+ /**
1163
+ * Create a volume. Throws `ConflictError` (409) if the project already has one with
1164
+ * this name — use {@link getOrCreate} when you want "make sure this exists" instead.
1165
+ */
1166
+ static async create(projectId, name, opts) {
1167
+ const config = new ConnectionConfig(opts);
1168
+ const res = await fetch(`${config.apiUrl}/api/projects/${projectId}/volumes`, {
1169
+ method: "POST",
1170
+ headers: config.headers,
1171
+ body: JSON.stringify({ name, sizeGb: opts?.sizeGb ?? 5, region: opts?.region })
1172
+ });
1173
+ if (!res.ok) await handleApiError(res);
1174
+ const vol = await res.json();
1175
+ return new _Volume({ volumeId: vol.id, name: vol.name, ...opts });
1176
+ }
1177
+ /**
1178
+ * Return the project's volume with this name, creating it first if it does not
1179
+ * exist yet. This is the reason a volume's name is its key: an agent that wants
1180
+ * "the scratch disk for this task" no longer has to store an id between runs.
1181
+ *
1182
+ * An existing volume is returned as-is — `sizeGb` applies only to a fresh create
1183
+ * and never resizes one that is already there.
1184
+ */
1185
+ static async getOrCreate(projectId, name, opts) {
1186
+ const config = new ConnectionConfig(opts);
1187
+ const res = await fetch(`${config.apiUrl}/api/projects/${projectId}/volumes`, {
1188
+ method: "POST",
1189
+ headers: config.headers,
1190
+ body: JSON.stringify({ name, sizeGb: opts?.sizeGb ?? 5, region: opts?.region, getOrCreate: true })
1191
+ });
1192
+ if (!res.ok) await handleApiError(res);
1193
+ const vol = await res.json();
1194
+ return new _Volume({ volumeId: vol.id, name: vol.name, ...opts });
1195
+ }
1196
+ /** Look a volume up by name or by id. */
1197
+ static async get(projectId, nameOrId, opts) {
1198
+ const config = new ConnectionConfig(opts);
1199
+ const res = await fetch(`${config.apiUrl}/api/projects/${projectId}/volumes/${encodeURIComponent(nameOrId)}`, {
1200
+ headers: config.headers
1201
+ });
1202
+ if (!res.ok) await handleApiError(res);
1203
+ const vol = await res.json();
1204
+ return new _Volume({ volumeId: vol.id, name: vol.name, ...opts });
1205
+ }
1206
+ static async list(projectId, opts) {
1207
+ const config = new ConnectionConfig(opts);
1208
+ const res = await fetch(`${config.apiUrl}/api/projects/${projectId}/volumes`, {
1209
+ headers: config.headers
1210
+ });
1211
+ if (!res.ok) await handleApiError(res);
1212
+ return res.json();
1213
+ }
1214
+ /** Delete a volume by name or by id, without constructing one first. */
1215
+ static async delete(projectId, nameOrId, opts) {
1216
+ const config = new ConnectionConfig(opts);
1217
+ const res = await fetch(`${config.apiUrl}/api/projects/${projectId}/volumes/${encodeURIComponent(nameOrId)}`, {
1218
+ method: "DELETE",
1219
+ headers: config.headers
1220
+ });
1221
+ if (!res.ok) await handleApiError(res);
1222
+ }
1223
+ async getInfo(projectId) {
1224
+ const res = await fetch(`${this.config.apiUrl}/api/projects/${projectId}/volumes/${this.volumeId}`, {
1225
+ headers: this.config.headers
1226
+ });
1227
+ if (!res.ok) await handleApiError(res);
1228
+ return res.json();
1229
+ }
1230
+ async delete(projectId) {
1231
+ const res = await fetch(`${this.config.apiUrl}/api/projects/${projectId}/volumes/${this.volumeId}`, {
1232
+ method: "DELETE",
1233
+ headers: this.config.headers
1234
+ });
1235
+ if (!res.ok) await handleApiError(res);
1236
+ }
1237
+ };
1238
+
1052
1239
  // src/lizard.ts
1053
1240
  var Lizard = class {
1054
1241
  constructor(opts) {
1242
+ // ── Volumes, bound to this client's project ───────────────────────────────
1243
+ /**
1244
+ * Persistent volumes in this client's project.
1245
+ *
1246
+ * The same calls as the static {@link Volume} methods, minus the `projectId`
1247
+ * argument — the client already knows it.
1248
+ *
1249
+ * @requires `project` to be set in the constructor.
1250
+ *
1251
+ * @example
1252
+ * ```ts
1253
+ * const lizard = new Lizard({ project: 'my-project' })
1254
+ * const vol = await lizard.volumes.getOrCreate('scratch', { sizeGb: 10 })
1255
+ * const sb = await lizard.create('codex', { volumeName: 'scratch' })
1256
+ * ```
1257
+ */
1258
+ this.volumes = {
1259
+ create: async (name, opts) => Volume.create(await this.projectId(), name, { ...await this.connectionOpts(), ...opts }),
1260
+ getOrCreate: async (name, opts) => Volume.getOrCreate(await this.projectId(), name, { ...await this.connectionOpts(), ...opts }),
1261
+ get: async (nameOrId, opts) => Volume.get(await this.projectId(), nameOrId, { ...await this.connectionOpts(), ...opts }),
1262
+ list: async (opts) => Volume.list(await this.projectId(), { ...await this.connectionOpts(), ...opts }),
1263
+ delete: async (nameOrId, opts) => Volume.delete(await this.projectId(), nameOrId, { ...await this.connectionOpts(), ...opts })
1264
+ };
1055
1265
  this.config = new ConnectionConfig(opts);
1056
1266
  this.projectRef = opts.project;
1057
1267
  const platform = new PlatformClient(opts);
1058
1268
  this._platform = platform;
1269
+ this.workspaces = new WorkspacesAPI(platform);
1270
+ this.apiKeys = new ApiKeysAPI(platform);
1271
+ this.regions = new RegionsAPI(platform);
1272
+ this.billing = new BillingAPI(platform);
1059
1273
  this.projects = new ProjectsAPI(platform);
1060
1274
  this.services = new ServicesAPI(platform);
1061
1275
  this.addons = new AddonsAPI(platform);
@@ -1081,7 +1295,7 @@ var Lizard = class {
1081
1295
  const sandboxOpts = { ...opts, ...await this.connectionOpts(), projectId };
1082
1296
  return template ? Sandbox.create(template, sandboxOpts) : Sandbox.create(sandboxOpts);
1083
1297
  }
1084
- /** Connect to an existing sandbox by ID (resumes it if paused). */
1298
+ /** Connect to an existing sandbox by ID. Throws `NotFoundError` if it is gone. */
1085
1299
  async connect(sandboxId, opts) {
1086
1300
  return Sandbox.connect(sandboxId, await this.connectionOpts(opts));
1087
1301
  }
@@ -1089,6 +1303,20 @@ var Lizard = class {
1089
1303
  async list(opts) {
1090
1304
  return Sandbox.list(await this.connectionOpts(opts));
1091
1305
  }
1306
+ // ── Account ───────────────────────────────────────────────────────────────
1307
+ /**
1308
+ * The account this credential belongs to — the SDK's `lizard whoami`.
1309
+ *
1310
+ * Works for a scoped key as well as a full one: the identity is the account that
1311
+ * created the key, which is what the key's usage bills to.
1312
+ */
1313
+ async whoami() {
1314
+ return this.platform.get("/api/auth/me");
1315
+ }
1316
+ /** The underlying HTTP client, for endpoints this SDK does not wrap yet. */
1317
+ get platform() {
1318
+ return this._platform;
1319
+ }
1092
1320
  };
1093
1321
 
1094
1322
  // src/code-interpreter/types.ts
@@ -1280,100 +1508,27 @@ async function* readLines(body) {
1280
1508
  reader.releaseLock();
1281
1509
  }
1282
1510
  }
1283
-
1284
- // src/volume/index.ts
1285
- var Volume = class _Volume {
1286
- constructor(opts) {
1287
- this.volumeId = opts.volumeId;
1288
- this.name = opts.name;
1289
- this.config = new ConnectionConfig(opts);
1290
- }
1291
- /**
1292
- * Create a volume. Throws `ConflictError` (409) if the project already has one with
1293
- * this name — use {@link getOrCreate} when you want "make sure this exists" instead.
1294
- */
1295
- static async create(projectId, name, opts) {
1296
- const config = new ConnectionConfig(opts);
1297
- const res = await fetch(`${config.apiUrl}/api/projects/${projectId}/volumes`, {
1298
- method: "POST",
1299
- headers: config.headers,
1300
- body: JSON.stringify({ name, sizeGb: opts?.sizeGb ?? 5 })
1301
- });
1302
- if (!res.ok) await handleApiError(res);
1303
- const vol = await res.json();
1304
- return new _Volume({ volumeId: vol.id, name: vol.name, ...opts });
1305
- }
1306
- /**
1307
- * Return the project's volume with this name, creating it first if it does not
1308
- * exist yet. This is the reason a volume's name is its key: an agent that wants
1309
- * "the scratch disk for this task" no longer has to store an id between runs.
1310
- *
1311
- * An existing volume is returned as-is — `sizeGb` applies only to a fresh create
1312
- * and never resizes one that is already there.
1313
- */
1314
- static async getOrCreate(projectId, name, opts) {
1315
- const config = new ConnectionConfig(opts);
1316
- const res = await fetch(`${config.apiUrl}/api/projects/${projectId}/volumes`, {
1317
- method: "POST",
1318
- headers: config.headers,
1319
- body: JSON.stringify({ name, sizeGb: opts?.sizeGb ?? 5, getOrCreate: true })
1320
- });
1321
- if (!res.ok) await handleApiError(res);
1322
- const vol = await res.json();
1323
- return new _Volume({ volumeId: vol.id, name: vol.name, ...opts });
1324
- }
1325
- /** Look a volume up by name or by id. */
1326
- static async get(projectId, nameOrId, opts) {
1327
- const config = new ConnectionConfig(opts);
1328
- const res = await fetch(`${config.apiUrl}/api/projects/${projectId}/volumes/${encodeURIComponent(nameOrId)}`, {
1329
- headers: config.headers
1330
- });
1331
- if (!res.ok) await handleApiError(res);
1332
- const vol = await res.json();
1333
- return new _Volume({ volumeId: vol.id, name: vol.name, ...opts });
1334
- }
1335
- static async list(projectId, opts) {
1336
- const config = new ConnectionConfig(opts);
1337
- const res = await fetch(`${config.apiUrl}/api/projects/${projectId}/volumes`, {
1338
- headers: config.headers
1339
- });
1340
- if (!res.ok) await handleApiError(res);
1341
- return res.json();
1342
- }
1343
- /** Delete a volume by name or by id, without constructing one first. */
1344
- static async delete(projectId, nameOrId, opts) {
1345
- const config = new ConnectionConfig(opts);
1346
- const res = await fetch(`${config.apiUrl}/api/projects/${projectId}/volumes/${encodeURIComponent(nameOrId)}`, {
1347
- method: "DELETE",
1348
- headers: config.headers
1349
- });
1350
- if (!res.ok) await handleApiError(res);
1351
- }
1352
- async getInfo(projectId) {
1353
- const res = await fetch(`${this.config.apiUrl}/api/projects/${projectId}/volumes/${this.volumeId}`, {
1354
- headers: this.config.headers
1355
- });
1356
- if (!res.ok) await handleApiError(res);
1357
- return res.json();
1358
- }
1359
- async delete(projectId) {
1360
- const res = await fetch(`${this.config.apiUrl}/api/projects/${projectId}/volumes/${this.volumeId}`, {
1361
- method: "DELETE",
1362
- headers: this.config.headers
1363
- });
1364
- if (!res.ok) await handleApiError(res);
1365
- }
1366
- };
1367
1511
  export {
1512
+ AddonsAPI,
1513
+ ApiKeysAPI,
1368
1514
  AuthenticationError,
1515
+ BillingAPI,
1369
1516
  CodeSandbox,
1370
1517
  ConflictError,
1371
1518
  DeployHandle,
1519
+ DomainsAPI,
1372
1520
  Lizard,
1373
1521
  LizardError,
1522
+ MetricsAPI,
1374
1523
  NotFoundError,
1524
+ PlatformClient,
1525
+ ProjectsAPI,
1526
+ RegionsAPI,
1375
1527
  Sandbox,
1528
+ SecretsAPI,
1529
+ ServicesAPI,
1376
1530
  TimeoutError,
1377
1531
  Volume,
1532
+ WorkspacesAPI,
1378
1533
  resolveProjectId
1379
1534
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lizard-build/sdk",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "description": "Firecracker microVM sandboxes for AI agents — boot Linux in milliseconds, run code, write files, expose ports over HTTPS, then pause a session with memory intact and resume it later. TypeScript SDK for Lizard Sandboxes (lizard.build).",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",