disk 0.8.9 → 0.8.11

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/cli.js CHANGED
@@ -99,6 +99,7 @@ var Disk = class {
99
99
  metrics;
100
100
  connectedClients;
101
101
  authorizedUsers;
102
+ allowedIps;
102
103
  /** @internal */
103
104
  _client;
104
105
  /** @internal */
@@ -120,6 +121,7 @@ var Disk = class {
120
121
  this.metrics = data.metrics;
121
122
  this.connectedClients = data.connectedClients;
122
123
  this.authorizedUsers = data.authorizedUsers;
124
+ this.allowedIps = data.allowedIps;
123
125
  this._client = client;
124
126
  this._archilRegion = archilRegion;
125
127
  }
@@ -139,7 +141,8 @@ var Disk = class {
139
141
  mounts: this.mounts,
140
142
  metrics: this.metrics,
141
143
  connectedClients: this.connectedClients,
142
- authorizedUsers: this.authorizedUsers
144
+ authorizedUsers: this.authorizedUsers,
145
+ allowedIps: this.allowedIps
143
146
  };
144
147
  }
145
148
  async addUser(user) {
@@ -175,6 +178,32 @@ var Disk = class {
175
178
  async removeTokenUser(identifier) {
176
179
  await this.removeUser("token", identifier);
177
180
  }
181
+ async getAllowedIPs() {
182
+ const data = await unwrap(
183
+ this._client.GET("/api/disks/{id}/allowed-ips", {
184
+ params: { path: { id: this.id } }
185
+ })
186
+ );
187
+ return data.allowedIps;
188
+ }
189
+ async setAllowedIPs(allowedIps) {
190
+ const data = await unwrap(
191
+ this._client.PUT("/api/disks/{id}/allowed-ips", {
192
+ params: { path: { id: this.id } },
193
+ body: { allowedIps }
194
+ })
195
+ );
196
+ return data.allowedIps;
197
+ }
198
+ async addAllowedIP(ip) {
199
+ const current = await this.getAllowedIPs();
200
+ if (current.includes(ip)) return current;
201
+ return this.setAllowedIPs([...current, ip]);
202
+ }
203
+ async removeAllowedIP(ip) {
204
+ const current = await this.getAllowedIPs();
205
+ return this.setAllowedIPs(current.filter((i) => i !== ip));
206
+ }
178
207
  async delete() {
179
208
  await unwrapEmpty(
180
209
  this._client.DELETE("/api/disks/{id}", {
@@ -309,9 +338,17 @@ var Tokens = class {
309
338
  };
310
339
 
311
340
  // src/archil.ts
341
+ function isExecMountSpec(m) {
342
+ return typeof m === "object" && m !== null && "disk" in m;
343
+ }
344
+ function diskIdFromMount(m) {
345
+ return typeof m === "string" ? m : m.id;
346
+ }
312
347
  var Archil = class {
313
348
  disks;
314
349
  tokens;
350
+ /** @internal */
351
+ _client;
315
352
  constructor(opts = {}) {
316
353
  const apiKey = opts.apiKey ?? process.env.ARCHIL_API_KEY;
317
354
  const region = opts.region ?? process.env.ARCHIL_REGION;
@@ -326,9 +363,35 @@ var Archil = class {
326
363
  region,
327
364
  baseUrl: opts.baseUrl
328
365
  });
366
+ this._client = client;
329
367
  this.disks = new Disks(client, region);
330
368
  this.tokens = new Tokens(client);
331
369
  }
370
+ /**
371
+ * Run a command in a container with multiple disks mounted simultaneously,
372
+ * each at its own relative path under `/mnt/archil`. Blocks until the
373
+ * command completes and returns its stdout, stderr, exit code, and timing.
374
+ */
375
+ async exec(opts) {
376
+ const disks = {};
377
+ for (const [relPath, mount] of Object.entries(opts.disks)) {
378
+ if (isExecMountSpec(mount)) {
379
+ const entry = {
380
+ disk: diskIdFromMount(mount.disk),
381
+ readOnly: mount.readOnly ?? false
382
+ };
383
+ if (mount.subdirectory !== void 0) entry.subdirectory = mount.subdirectory;
384
+ disks[relPath] = entry;
385
+ } else {
386
+ disks[relPath] = diskIdFromMount(mount);
387
+ }
388
+ }
389
+ return unwrap(
390
+ this._client.POST("/api/exec", {
391
+ body: { disks, command: opts.command }
392
+ })
393
+ );
394
+ }
332
395
  };
333
396
 
334
397
  // src/index.ts
package/dist/index.cjs CHANGED
@@ -39,6 +39,7 @@ __export(index_exports, {
39
39
  createApiKey: () => createApiKey,
40
40
  createDisk: () => createDisk,
41
41
  deleteApiKey: () => deleteApiKey,
42
+ exec: () => exec,
42
43
  getDisk: () => getDisk,
43
44
  listApiKeys: () => listApiKeys,
44
45
  listDisks: () => listDisks
@@ -141,6 +142,7 @@ var Disk = class {
141
142
  metrics;
142
143
  connectedClients;
143
144
  authorizedUsers;
145
+ allowedIps;
144
146
  /** @internal */
145
147
  _client;
146
148
  /** @internal */
@@ -162,6 +164,7 @@ var Disk = class {
162
164
  this.metrics = data.metrics;
163
165
  this.connectedClients = data.connectedClients;
164
166
  this.authorizedUsers = data.authorizedUsers;
167
+ this.allowedIps = data.allowedIps;
165
168
  this._client = client;
166
169
  this._archilRegion = archilRegion;
167
170
  }
@@ -181,7 +184,8 @@ var Disk = class {
181
184
  mounts: this.mounts,
182
185
  metrics: this.metrics,
183
186
  connectedClients: this.connectedClients,
184
- authorizedUsers: this.authorizedUsers
187
+ authorizedUsers: this.authorizedUsers,
188
+ allowedIps: this.allowedIps
185
189
  };
186
190
  }
187
191
  async addUser(user) {
@@ -217,6 +221,32 @@ var Disk = class {
217
221
  async removeTokenUser(identifier) {
218
222
  await this.removeUser("token", identifier);
219
223
  }
224
+ async getAllowedIPs() {
225
+ const data = await unwrap(
226
+ this._client.GET("/api/disks/{id}/allowed-ips", {
227
+ params: { path: { id: this.id } }
228
+ })
229
+ );
230
+ return data.allowedIps;
231
+ }
232
+ async setAllowedIPs(allowedIps) {
233
+ const data = await unwrap(
234
+ this._client.PUT("/api/disks/{id}/allowed-ips", {
235
+ params: { path: { id: this.id } },
236
+ body: { allowedIps }
237
+ })
238
+ );
239
+ return data.allowedIps;
240
+ }
241
+ async addAllowedIP(ip) {
242
+ const current = await this.getAllowedIPs();
243
+ if (current.includes(ip)) return current;
244
+ return this.setAllowedIPs([...current, ip]);
245
+ }
246
+ async removeAllowedIP(ip) {
247
+ const current = await this.getAllowedIPs();
248
+ return this.setAllowedIPs(current.filter((i) => i !== ip));
249
+ }
220
250
  async delete() {
221
251
  await unwrapEmpty(
222
252
  this._client.DELETE("/api/disks/{id}", {
@@ -351,9 +381,17 @@ var Tokens = class {
351
381
  };
352
382
 
353
383
  // src/archil.ts
384
+ function isExecMountSpec(m) {
385
+ return typeof m === "object" && m !== null && "disk" in m;
386
+ }
387
+ function diskIdFromMount(m) {
388
+ return typeof m === "string" ? m : m.id;
389
+ }
354
390
  var Archil = class {
355
391
  disks;
356
392
  tokens;
393
+ /** @internal */
394
+ _client;
357
395
  constructor(opts = {}) {
358
396
  const apiKey = opts.apiKey ?? process.env.ARCHIL_API_KEY;
359
397
  const region = opts.region ?? process.env.ARCHIL_REGION;
@@ -368,9 +406,35 @@ var Archil = class {
368
406
  region,
369
407
  baseUrl: opts.baseUrl
370
408
  });
409
+ this._client = client;
371
410
  this.disks = new Disks(client, region);
372
411
  this.tokens = new Tokens(client);
373
412
  }
413
+ /**
414
+ * Run a command in a container with multiple disks mounted simultaneously,
415
+ * each at its own relative path under `/mnt/archil`. Blocks until the
416
+ * command completes and returns its stdout, stderr, exit code, and timing.
417
+ */
418
+ async exec(opts) {
419
+ const disks = {};
420
+ for (const [relPath, mount] of Object.entries(opts.disks)) {
421
+ if (isExecMountSpec(mount)) {
422
+ const entry = {
423
+ disk: diskIdFromMount(mount.disk),
424
+ readOnly: mount.readOnly ?? false
425
+ };
426
+ if (mount.subdirectory !== void 0) entry.subdirectory = mount.subdirectory;
427
+ disks[relPath] = entry;
428
+ } else {
429
+ disks[relPath] = diskIdFromMount(mount);
430
+ }
431
+ }
432
+ return unwrap(
433
+ this._client.POST("/api/exec", {
434
+ body: { disks, command: opts.command }
435
+ })
436
+ );
437
+ }
374
438
  };
375
439
 
376
440
  // src/index.ts
@@ -404,6 +468,9 @@ function createApiKey(req) {
404
468
  function deleteApiKey(id) {
405
469
  return archil().tokens.delete(id);
406
470
  }
471
+ function exec(opts) {
472
+ return archil().exec(opts);
473
+ }
407
474
  // Annotate the CommonJS export names for ESM import in node:
408
475
  0 && (module.exports = {
409
476
  Archil,
@@ -415,6 +482,7 @@ function deleteApiKey(id) {
415
482
  createApiKey,
416
483
  createDisk,
417
484
  deleteApiKey,
485
+ exec,
418
486
  getDisk,
419
487
  listApiKeys,
420
488
  listDisks
package/dist/index.d.cts CHANGED
@@ -102,6 +102,30 @@ interface paths {
102
102
  patch?: never;
103
103
  trace?: never;
104
104
  };
105
+ "/api/disks/{id}/allowed-ips": {
106
+ parameters: {
107
+ query?: never;
108
+ header?: never;
109
+ path?: never;
110
+ cookie?: never;
111
+ };
112
+ /**
113
+ * Get IP allowlist
114
+ * @description Returns the current IP allowlist for a disk.
115
+ */
116
+ get: operations["getAllowedIPs"];
117
+ /**
118
+ * Set IP allowlist
119
+ * @description Replaces the IP allowlist for a disk. When non-empty, only clients connecting from listed IPs or CIDR ranges can mount the disk. Pass an empty array to remove all restrictions.
120
+ */
121
+ put: operations["setAllowedIPs"];
122
+ post?: never;
123
+ delete?: never;
124
+ options?: never;
125
+ head?: never;
126
+ patch?: never;
127
+ trace?: never;
128
+ };
105
129
  "/api/disks/{id}/exec": {
106
130
  parameters: {
107
131
  query?: never;
@@ -124,6 +148,33 @@ interface paths {
124
148
  patch?: never;
125
149
  trace?: never;
126
150
  };
151
+ "/api/exec": {
152
+ parameters: {
153
+ query?: never;
154
+ header?: never;
155
+ path?: never;
156
+ cookie?: never;
157
+ };
158
+ get?: never;
159
+ put?: never;
160
+ /**
161
+ * Execute a command with multiple disks mounted
162
+ * @description Launches a container with the supplied set of disks each mounted at its
163
+ * own relative path under `/mnt/archil`, runs the command to completion,
164
+ * and shuts down the container. Activation is atomic: every disk mounts
165
+ * or none of them do.
166
+ *
167
+ * Relative paths must be non-empty, non-absolute, and contain no `.` /
168
+ * `..` segments. Mounting two disks at the same relative path is an
169
+ * error.
170
+ */
171
+ post: operations["exec"];
172
+ delete?: never;
173
+ options?: never;
174
+ head?: never;
175
+ patch?: never;
176
+ trace?: never;
177
+ };
127
178
  "/api/tokens": {
128
179
  parameters: {
129
180
  query?: never;
@@ -197,6 +248,14 @@ interface components {
197
248
  name: string;
198
249
  /** @description Storage mount to attach. Omit for archil-managed storage. */
199
250
  mounts?: components["schemas"]["MountConfig"][];
251
+ /**
252
+ * @description IP allowlist for mount access. When non-empty, only clients connecting from these IPs or CIDR ranges can mount the disk. An empty list (default) allows all IPs.
253
+ * @example [
254
+ * "203.0.113.0/24",
255
+ * "198.51.100.42"
256
+ * ]
257
+ */
258
+ allowedIps?: string[];
200
259
  /**
201
260
  * @deprecated
202
261
  * @description Deprecated. Use AddDiskUser after creation instead. When provided, suppresses the default auto-generated token user.
@@ -438,6 +497,24 @@ interface components {
438
497
  metrics?: components["schemas"]["DiskMetrics"];
439
498
  connectedClients?: components["schemas"]["ConnectedClient"][];
440
499
  authorizedUsers?: components["schemas"]["AuthorizedUser"][];
500
+ /**
501
+ * @description IP allowlist for mount access. Empty means all IPs are allowed.
502
+ * @example [
503
+ * "203.0.113.0/24"
504
+ * ]
505
+ */
506
+ allowedIps?: string[];
507
+ /**
508
+ * @description Capabilities supported by this disk. Determined at creation
509
+ * time and immutable thereafter. Defined values:
510
+ *
511
+ * * `checkpoints` — checkpoints can be created on this disk. Not
512
+ * available for disks with bring-your-own buckets.
513
+ * @example [
514
+ * "checkpoints"
515
+ * ]
516
+ */
517
+ capabilities?: "checkpoints"[];
441
518
  };
442
519
  MountResponse: {
443
520
  /** @description Mount identifier */
@@ -538,6 +615,19 @@ interface components {
538
615
  success: boolean;
539
616
  data: components["schemas"]["AuthorizedUser"];
540
617
  };
618
+ ApiResponse_AllowedIPs: {
619
+ /** @example true */
620
+ success: boolean;
621
+ data: {
622
+ /**
623
+ * @example [
624
+ * "203.0.113.0/24",
625
+ * "198.51.100.42"
626
+ * ]
627
+ */
628
+ allowedIps: string[];
629
+ };
630
+ };
541
631
  CreateApiTokenRequest: {
542
632
  /** @description Token name */
543
633
  name: string;
@@ -616,6 +706,58 @@ interface components {
616
706
  success: boolean;
617
707
  data: components["schemas"]["ExecDiskResult"];
618
708
  };
709
+ ExecRequest: {
710
+ /**
711
+ * @description Map of relative path under `/mnt/archil` to the disk to mount
712
+ * there. At least one entry is required. Relative paths must be
713
+ * non-empty, non-absolute, and contain no `.` or `..` segments.
714
+ *
715
+ * Each value is either a plain disk ID string (mounts the disk's
716
+ * root, read-write) or an object that additionally selects a
717
+ * subdirectory of the disk and/or marks the mount as read-only.
718
+ * @example {
719
+ * "data": "dsk-abc123",
720
+ * "logs": {
721
+ * "disk": "dsk-def456",
722
+ * "subdirectory": "app/logs",
723
+ * "readOnly": true
724
+ * }
725
+ * }
726
+ */
727
+ disks: {
728
+ [key: string]: string | components["schemas"]["ExecMount"];
729
+ };
730
+ /**
731
+ * @description Shell command to execute inside the container
732
+ * @example ls -la /mnt/archil/data /mnt/archil/logs
733
+ */
734
+ command: string;
735
+ };
736
+ ExecMount: {
737
+ /**
738
+ * @description Disk ID to mount at this relative path
739
+ * @example dsk-abc123
740
+ */
741
+ disk: string;
742
+ /**
743
+ * @description Subdirectory of the disk to expose at the mountpoint. Must be a
744
+ * relative path with no `.` or `..` segments. When omitted, the
745
+ * disk's root is exposed.
746
+ * @example app/logs
747
+ */
748
+ subdirectory?: string;
749
+ /**
750
+ * @description When true, the disk is mounted read-only inside the container.
751
+ * Writes against the mount fail with EROFS.
752
+ * @default false
753
+ */
754
+ readOnly: boolean;
755
+ };
756
+ ApiResponse_Exec: {
757
+ /** @example true */
758
+ success: boolean;
759
+ data: components["schemas"]["ExecDiskResult"];
760
+ };
619
761
  };
620
762
  responses: {
621
763
  /** @description Invalid or missing authentication credentials */
@@ -871,6 +1013,72 @@ interface operations {
871
1013
  500: components["responses"]["InternalError"];
872
1014
  };
873
1015
  };
1016
+ getAllowedIPs: {
1017
+ parameters: {
1018
+ query?: never;
1019
+ header?: never;
1020
+ path: {
1021
+ /** @description Disk ID (format `dsk-{16 hex chars}`) */
1022
+ id: components["parameters"]["DiskId"];
1023
+ };
1024
+ cookie?: never;
1025
+ };
1026
+ requestBody?: never;
1027
+ responses: {
1028
+ /** @description Current IP allowlist */
1029
+ 200: {
1030
+ headers: {
1031
+ [name: string]: unknown;
1032
+ };
1033
+ content: {
1034
+ "application/json": components["schemas"]["ApiResponse_AllowedIPs"];
1035
+ };
1036
+ };
1037
+ 401: components["responses"]["Unauthorized"];
1038
+ 404: components["responses"]["NotFound"];
1039
+ 500: components["responses"]["InternalError"];
1040
+ };
1041
+ };
1042
+ setAllowedIPs: {
1043
+ parameters: {
1044
+ query?: never;
1045
+ header?: never;
1046
+ path: {
1047
+ /** @description Disk ID (format `dsk-{16 hex chars}`) */
1048
+ id: components["parameters"]["DiskId"];
1049
+ };
1050
+ cookie?: never;
1051
+ };
1052
+ requestBody: {
1053
+ content: {
1054
+ "application/json": {
1055
+ /**
1056
+ * @description List of IPs or CIDR ranges. Empty array removes restrictions.
1057
+ * @example [
1058
+ * "203.0.113.0/24",
1059
+ * "198.51.100.42"
1060
+ * ]
1061
+ */
1062
+ allowedIps: string[];
1063
+ };
1064
+ };
1065
+ };
1066
+ responses: {
1067
+ /** @description Allowlist updated */
1068
+ 200: {
1069
+ headers: {
1070
+ [name: string]: unknown;
1071
+ };
1072
+ content: {
1073
+ "application/json": components["schemas"]["ApiResponse_AllowedIPs"];
1074
+ };
1075
+ };
1076
+ 400: components["responses"]["ValidationError"];
1077
+ 401: components["responses"]["Unauthorized"];
1078
+ 404: components["responses"]["NotFound"];
1079
+ 500: components["responses"]["InternalError"];
1080
+ };
1081
+ };
874
1082
  execDisk: {
875
1083
  parameters: {
876
1084
  query?: never;
@@ -911,6 +1119,43 @@ interface operations {
911
1119
  };
912
1120
  };
913
1121
  };
1122
+ exec: {
1123
+ parameters: {
1124
+ query?: never;
1125
+ header?: never;
1126
+ path?: never;
1127
+ cookie?: never;
1128
+ };
1129
+ requestBody: {
1130
+ content: {
1131
+ "application/json": components["schemas"]["ExecRequest"];
1132
+ };
1133
+ };
1134
+ responses: {
1135
+ /** @description Command completed */
1136
+ 200: {
1137
+ headers: {
1138
+ [name: string]: unknown;
1139
+ };
1140
+ content: {
1141
+ "application/json": components["schemas"]["ApiResponse_Exec"];
1142
+ };
1143
+ };
1144
+ 400: components["responses"]["ValidationError"];
1145
+ 401: components["responses"]["Unauthorized"];
1146
+ 404: components["responses"]["NotFound"];
1147
+ 500: components["responses"]["InternalError"];
1148
+ /** @description Command timed out */
1149
+ 504: {
1150
+ headers: {
1151
+ [name: string]: unknown;
1152
+ };
1153
+ content: {
1154
+ "application/json": components["schemas"]["ErrorResponse"];
1155
+ };
1156
+ };
1157
+ };
1158
+ };
914
1159
  listApiTokens: {
915
1160
  parameters: {
916
1161
  query?: {
@@ -1014,6 +1259,7 @@ type AwsStsUser = components["schemas"]["AwsStsUser"];
1014
1259
  type CreateApiTokenRequest = components["schemas"]["CreateApiTokenRequest"];
1015
1260
  type ApiTokenResponse = components["schemas"]["ApiTokenResponse"];
1016
1261
  type ExecDiskResult = components["schemas"]["ExecDiskResult"];
1262
+ type ExecRequest = components["schemas"]["ExecRequest"];
1017
1263
  type DiskStatus = DiskResponse["status"];
1018
1264
 
1019
1265
  interface MountOptions {
@@ -1039,6 +1285,7 @@ declare class Disk {
1039
1285
  readonly metrics?: DiskMetrics;
1040
1286
  readonly connectedClients?: ConnectedClient[];
1041
1287
  readonly authorizedUsers?: AuthorizedUser[];
1288
+ readonly allowedIps?: string[];
1042
1289
  /** @internal */
1043
1290
  private readonly _client;
1044
1291
  /** @internal */
@@ -1053,6 +1300,10 @@ declare class Disk {
1053
1300
  identifier: string;
1054
1301
  }>;
1055
1302
  removeTokenUser(identifier: string): Promise<void>;
1303
+ getAllowedIPs(): Promise<string[]>;
1304
+ setAllowedIPs(allowedIps: string[]): Promise<string[]>;
1305
+ addAllowedIP(ip: string): Promise<string[]>;
1306
+ removeAllowedIP(ip: string): Promise<string[]>;
1056
1307
  delete(): Promise<void>;
1057
1308
  /**
1058
1309
  * Execute a command in a container with this disk mounted.
@@ -1120,10 +1371,57 @@ interface ArchilOptions {
1120
1371
  /** Override the control plane base URL (useful for testing). */
1121
1372
  baseUrl?: string;
1122
1373
  }
1374
+ /**
1375
+ * Options that apply to a single mounted disk in an exec request.
1376
+ * Use this object form when you need to pin the mount to a subdirectoryectory
1377
+ * of the disk and/or mount it read-only; for the default case (mount
1378
+ * the disk's root, read-write), pass a `Disk` or disk-id string instead.
1379
+ */
1380
+ interface ExecMountSpec {
1381
+ /** Disk to mount, by `Disk` instance or raw disk id string. */
1382
+ disk: Disk | string;
1383
+ /**
1384
+ * Subdirectory of the disk to expose at the mountpoint. Must be a
1385
+ * relative path with no `.` or `..` segments. When omitted, the disk's
1386
+ * root is exposed.
1387
+ */
1388
+ subdirectory?: string;
1389
+ /**
1390
+ * When true, mount the disk read-only inside the container. Writes
1391
+ * against the mount fail with EROFS. Defaults to false.
1392
+ */
1393
+ readOnly?: boolean;
1394
+ }
1395
+ /**
1396
+ * One disk to mount in an exec request. Either a `Disk`/disk-id string
1397
+ * (mounts the disk's root, read-write) or an `ExecMountSpec` object that
1398
+ * additionally selects a subdirectoryectory of the disk and/or marks the mount
1399
+ * as read-only. Used by Archil#exec — the map key is the relative path
1400
+ * under /mnt/archil at which to mount the disk.
1401
+ */
1402
+ type ExecMount = Disk | string | ExecMountSpec;
1403
+ interface ExecOptions {
1404
+ /**
1405
+ * Disks to mount, keyed by the relative path under `/mnt/archil` at which
1406
+ * to mount each one. At least one entry is required. Paths must be
1407
+ * non-empty, non-absolute, and contain no `.` or `..` segments.
1408
+ */
1409
+ disks: Record<string, ExecMount>;
1410
+ /** Shell command to run inside the container. */
1411
+ command: string;
1412
+ }
1123
1413
  declare class Archil {
1124
1414
  readonly disks: Disks;
1125
1415
  readonly tokens: Tokens;
1416
+ /** @internal */
1417
+ private readonly _client;
1126
1418
  constructor(opts?: ArchilOptions);
1419
+ /**
1420
+ * Run a command in a container with multiple disks mounted simultaneously,
1421
+ * each at its own relative path under `/mnt/archil`. Blocks until the
1422
+ * command completes and returns its stdout, stderr, exit code, and timing.
1423
+ */
1424
+ exec(opts: ExecOptions): Promise<ExecDiskResult>;
1127
1425
  }
1128
1426
 
1129
1427
  declare class ArchilApiError extends Error {
@@ -1141,5 +1439,11 @@ declare function createApiKey(req: CreateApiTokenRequest): Promise<ApiTokenRespo
1141
1439
  token?: string;
1142
1440
  }>;
1143
1441
  declare function deleteApiKey(id: string): Promise<void>;
1442
+ /**
1443
+ * Run a command in a container with multiple disks mounted simultaneously,
1444
+ * each at its own relative path under `/mnt/archil`. Blocks until the
1445
+ * command completes and returns its stdout, stderr, exit code, and timing.
1446
+ */
1447
+ declare function exec(opts: ExecOptions): Promise<ExecDiskResult>;
1144
1448
 
1145
- export { type ApiTokenResponse, Archil, ArchilApiError, type ArchilOptions, type AuthorizedUser, type AwsStsUser, type AzureBlobMount, type ConnectedClient, type CreateApiTokenRequest, type CreateDiskRequest, type CreateDiskResult, Disk, type DiskMetrics, type DiskResponse, type DiskStatus, type DiskUser, Disks, type ExecResult, type GCSMount, type ListDisksOptions, type ListTokensOptions, type MountConfig, type MountConfigResponse, type MountOptions, type MountResponse, type R2Mount, type S3CompatibleMount, type S3Mount, type TokenUser, Tokens, configure, createApiKey, createDisk, deleteApiKey, getDisk, listApiKeys, listDisks };
1449
+ export { type ApiTokenResponse, Archil, ArchilApiError, type ArchilOptions, type AuthorizedUser, type AwsStsUser, type AzureBlobMount, type ConnectedClient, type CreateApiTokenRequest, type CreateDiskRequest, type CreateDiskResult, Disk, type DiskMetrics, type DiskResponse, type DiskStatus, type DiskUser, Disks, type ExecMount, type ExecMountSpec, type ExecOptions, type ExecRequest, type ExecResult, type GCSMount, type ListDisksOptions, type ListTokensOptions, type MountConfig, type MountConfigResponse, type MountOptions, type MountResponse, type R2Mount, type S3CompatibleMount, type S3Mount, type TokenUser, Tokens, configure, createApiKey, createDisk, deleteApiKey, exec, getDisk, listApiKeys, listDisks };
package/dist/index.d.ts CHANGED
@@ -102,6 +102,30 @@ interface paths {
102
102
  patch?: never;
103
103
  trace?: never;
104
104
  };
105
+ "/api/disks/{id}/allowed-ips": {
106
+ parameters: {
107
+ query?: never;
108
+ header?: never;
109
+ path?: never;
110
+ cookie?: never;
111
+ };
112
+ /**
113
+ * Get IP allowlist
114
+ * @description Returns the current IP allowlist for a disk.
115
+ */
116
+ get: operations["getAllowedIPs"];
117
+ /**
118
+ * Set IP allowlist
119
+ * @description Replaces the IP allowlist for a disk. When non-empty, only clients connecting from listed IPs or CIDR ranges can mount the disk. Pass an empty array to remove all restrictions.
120
+ */
121
+ put: operations["setAllowedIPs"];
122
+ post?: never;
123
+ delete?: never;
124
+ options?: never;
125
+ head?: never;
126
+ patch?: never;
127
+ trace?: never;
128
+ };
105
129
  "/api/disks/{id}/exec": {
106
130
  parameters: {
107
131
  query?: never;
@@ -124,6 +148,33 @@ interface paths {
124
148
  patch?: never;
125
149
  trace?: never;
126
150
  };
151
+ "/api/exec": {
152
+ parameters: {
153
+ query?: never;
154
+ header?: never;
155
+ path?: never;
156
+ cookie?: never;
157
+ };
158
+ get?: never;
159
+ put?: never;
160
+ /**
161
+ * Execute a command with multiple disks mounted
162
+ * @description Launches a container with the supplied set of disks each mounted at its
163
+ * own relative path under `/mnt/archil`, runs the command to completion,
164
+ * and shuts down the container. Activation is atomic: every disk mounts
165
+ * or none of them do.
166
+ *
167
+ * Relative paths must be non-empty, non-absolute, and contain no `.` /
168
+ * `..` segments. Mounting two disks at the same relative path is an
169
+ * error.
170
+ */
171
+ post: operations["exec"];
172
+ delete?: never;
173
+ options?: never;
174
+ head?: never;
175
+ patch?: never;
176
+ trace?: never;
177
+ };
127
178
  "/api/tokens": {
128
179
  parameters: {
129
180
  query?: never;
@@ -197,6 +248,14 @@ interface components {
197
248
  name: string;
198
249
  /** @description Storage mount to attach. Omit for archil-managed storage. */
199
250
  mounts?: components["schemas"]["MountConfig"][];
251
+ /**
252
+ * @description IP allowlist for mount access. When non-empty, only clients connecting from these IPs or CIDR ranges can mount the disk. An empty list (default) allows all IPs.
253
+ * @example [
254
+ * "203.0.113.0/24",
255
+ * "198.51.100.42"
256
+ * ]
257
+ */
258
+ allowedIps?: string[];
200
259
  /**
201
260
  * @deprecated
202
261
  * @description Deprecated. Use AddDiskUser after creation instead. When provided, suppresses the default auto-generated token user.
@@ -438,6 +497,24 @@ interface components {
438
497
  metrics?: components["schemas"]["DiskMetrics"];
439
498
  connectedClients?: components["schemas"]["ConnectedClient"][];
440
499
  authorizedUsers?: components["schemas"]["AuthorizedUser"][];
500
+ /**
501
+ * @description IP allowlist for mount access. Empty means all IPs are allowed.
502
+ * @example [
503
+ * "203.0.113.0/24"
504
+ * ]
505
+ */
506
+ allowedIps?: string[];
507
+ /**
508
+ * @description Capabilities supported by this disk. Determined at creation
509
+ * time and immutable thereafter. Defined values:
510
+ *
511
+ * * `checkpoints` — checkpoints can be created on this disk. Not
512
+ * available for disks with bring-your-own buckets.
513
+ * @example [
514
+ * "checkpoints"
515
+ * ]
516
+ */
517
+ capabilities?: "checkpoints"[];
441
518
  };
442
519
  MountResponse: {
443
520
  /** @description Mount identifier */
@@ -538,6 +615,19 @@ interface components {
538
615
  success: boolean;
539
616
  data: components["schemas"]["AuthorizedUser"];
540
617
  };
618
+ ApiResponse_AllowedIPs: {
619
+ /** @example true */
620
+ success: boolean;
621
+ data: {
622
+ /**
623
+ * @example [
624
+ * "203.0.113.0/24",
625
+ * "198.51.100.42"
626
+ * ]
627
+ */
628
+ allowedIps: string[];
629
+ };
630
+ };
541
631
  CreateApiTokenRequest: {
542
632
  /** @description Token name */
543
633
  name: string;
@@ -616,6 +706,58 @@ interface components {
616
706
  success: boolean;
617
707
  data: components["schemas"]["ExecDiskResult"];
618
708
  };
709
+ ExecRequest: {
710
+ /**
711
+ * @description Map of relative path under `/mnt/archil` to the disk to mount
712
+ * there. At least one entry is required. Relative paths must be
713
+ * non-empty, non-absolute, and contain no `.` or `..` segments.
714
+ *
715
+ * Each value is either a plain disk ID string (mounts the disk's
716
+ * root, read-write) or an object that additionally selects a
717
+ * subdirectory of the disk and/or marks the mount as read-only.
718
+ * @example {
719
+ * "data": "dsk-abc123",
720
+ * "logs": {
721
+ * "disk": "dsk-def456",
722
+ * "subdirectory": "app/logs",
723
+ * "readOnly": true
724
+ * }
725
+ * }
726
+ */
727
+ disks: {
728
+ [key: string]: string | components["schemas"]["ExecMount"];
729
+ };
730
+ /**
731
+ * @description Shell command to execute inside the container
732
+ * @example ls -la /mnt/archil/data /mnt/archil/logs
733
+ */
734
+ command: string;
735
+ };
736
+ ExecMount: {
737
+ /**
738
+ * @description Disk ID to mount at this relative path
739
+ * @example dsk-abc123
740
+ */
741
+ disk: string;
742
+ /**
743
+ * @description Subdirectory of the disk to expose at the mountpoint. Must be a
744
+ * relative path with no `.` or `..` segments. When omitted, the
745
+ * disk's root is exposed.
746
+ * @example app/logs
747
+ */
748
+ subdirectory?: string;
749
+ /**
750
+ * @description When true, the disk is mounted read-only inside the container.
751
+ * Writes against the mount fail with EROFS.
752
+ * @default false
753
+ */
754
+ readOnly: boolean;
755
+ };
756
+ ApiResponse_Exec: {
757
+ /** @example true */
758
+ success: boolean;
759
+ data: components["schemas"]["ExecDiskResult"];
760
+ };
619
761
  };
620
762
  responses: {
621
763
  /** @description Invalid or missing authentication credentials */
@@ -871,6 +1013,72 @@ interface operations {
871
1013
  500: components["responses"]["InternalError"];
872
1014
  };
873
1015
  };
1016
+ getAllowedIPs: {
1017
+ parameters: {
1018
+ query?: never;
1019
+ header?: never;
1020
+ path: {
1021
+ /** @description Disk ID (format `dsk-{16 hex chars}`) */
1022
+ id: components["parameters"]["DiskId"];
1023
+ };
1024
+ cookie?: never;
1025
+ };
1026
+ requestBody?: never;
1027
+ responses: {
1028
+ /** @description Current IP allowlist */
1029
+ 200: {
1030
+ headers: {
1031
+ [name: string]: unknown;
1032
+ };
1033
+ content: {
1034
+ "application/json": components["schemas"]["ApiResponse_AllowedIPs"];
1035
+ };
1036
+ };
1037
+ 401: components["responses"]["Unauthorized"];
1038
+ 404: components["responses"]["NotFound"];
1039
+ 500: components["responses"]["InternalError"];
1040
+ };
1041
+ };
1042
+ setAllowedIPs: {
1043
+ parameters: {
1044
+ query?: never;
1045
+ header?: never;
1046
+ path: {
1047
+ /** @description Disk ID (format `dsk-{16 hex chars}`) */
1048
+ id: components["parameters"]["DiskId"];
1049
+ };
1050
+ cookie?: never;
1051
+ };
1052
+ requestBody: {
1053
+ content: {
1054
+ "application/json": {
1055
+ /**
1056
+ * @description List of IPs or CIDR ranges. Empty array removes restrictions.
1057
+ * @example [
1058
+ * "203.0.113.0/24",
1059
+ * "198.51.100.42"
1060
+ * ]
1061
+ */
1062
+ allowedIps: string[];
1063
+ };
1064
+ };
1065
+ };
1066
+ responses: {
1067
+ /** @description Allowlist updated */
1068
+ 200: {
1069
+ headers: {
1070
+ [name: string]: unknown;
1071
+ };
1072
+ content: {
1073
+ "application/json": components["schemas"]["ApiResponse_AllowedIPs"];
1074
+ };
1075
+ };
1076
+ 400: components["responses"]["ValidationError"];
1077
+ 401: components["responses"]["Unauthorized"];
1078
+ 404: components["responses"]["NotFound"];
1079
+ 500: components["responses"]["InternalError"];
1080
+ };
1081
+ };
874
1082
  execDisk: {
875
1083
  parameters: {
876
1084
  query?: never;
@@ -911,6 +1119,43 @@ interface operations {
911
1119
  };
912
1120
  };
913
1121
  };
1122
+ exec: {
1123
+ parameters: {
1124
+ query?: never;
1125
+ header?: never;
1126
+ path?: never;
1127
+ cookie?: never;
1128
+ };
1129
+ requestBody: {
1130
+ content: {
1131
+ "application/json": components["schemas"]["ExecRequest"];
1132
+ };
1133
+ };
1134
+ responses: {
1135
+ /** @description Command completed */
1136
+ 200: {
1137
+ headers: {
1138
+ [name: string]: unknown;
1139
+ };
1140
+ content: {
1141
+ "application/json": components["schemas"]["ApiResponse_Exec"];
1142
+ };
1143
+ };
1144
+ 400: components["responses"]["ValidationError"];
1145
+ 401: components["responses"]["Unauthorized"];
1146
+ 404: components["responses"]["NotFound"];
1147
+ 500: components["responses"]["InternalError"];
1148
+ /** @description Command timed out */
1149
+ 504: {
1150
+ headers: {
1151
+ [name: string]: unknown;
1152
+ };
1153
+ content: {
1154
+ "application/json": components["schemas"]["ErrorResponse"];
1155
+ };
1156
+ };
1157
+ };
1158
+ };
914
1159
  listApiTokens: {
915
1160
  parameters: {
916
1161
  query?: {
@@ -1014,6 +1259,7 @@ type AwsStsUser = components["schemas"]["AwsStsUser"];
1014
1259
  type CreateApiTokenRequest = components["schemas"]["CreateApiTokenRequest"];
1015
1260
  type ApiTokenResponse = components["schemas"]["ApiTokenResponse"];
1016
1261
  type ExecDiskResult = components["schemas"]["ExecDiskResult"];
1262
+ type ExecRequest = components["schemas"]["ExecRequest"];
1017
1263
  type DiskStatus = DiskResponse["status"];
1018
1264
 
1019
1265
  interface MountOptions {
@@ -1039,6 +1285,7 @@ declare class Disk {
1039
1285
  readonly metrics?: DiskMetrics;
1040
1286
  readonly connectedClients?: ConnectedClient[];
1041
1287
  readonly authorizedUsers?: AuthorizedUser[];
1288
+ readonly allowedIps?: string[];
1042
1289
  /** @internal */
1043
1290
  private readonly _client;
1044
1291
  /** @internal */
@@ -1053,6 +1300,10 @@ declare class Disk {
1053
1300
  identifier: string;
1054
1301
  }>;
1055
1302
  removeTokenUser(identifier: string): Promise<void>;
1303
+ getAllowedIPs(): Promise<string[]>;
1304
+ setAllowedIPs(allowedIps: string[]): Promise<string[]>;
1305
+ addAllowedIP(ip: string): Promise<string[]>;
1306
+ removeAllowedIP(ip: string): Promise<string[]>;
1056
1307
  delete(): Promise<void>;
1057
1308
  /**
1058
1309
  * Execute a command in a container with this disk mounted.
@@ -1120,10 +1371,57 @@ interface ArchilOptions {
1120
1371
  /** Override the control plane base URL (useful for testing). */
1121
1372
  baseUrl?: string;
1122
1373
  }
1374
+ /**
1375
+ * Options that apply to a single mounted disk in an exec request.
1376
+ * Use this object form when you need to pin the mount to a subdirectoryectory
1377
+ * of the disk and/or mount it read-only; for the default case (mount
1378
+ * the disk's root, read-write), pass a `Disk` or disk-id string instead.
1379
+ */
1380
+ interface ExecMountSpec {
1381
+ /** Disk to mount, by `Disk` instance or raw disk id string. */
1382
+ disk: Disk | string;
1383
+ /**
1384
+ * Subdirectory of the disk to expose at the mountpoint. Must be a
1385
+ * relative path with no `.` or `..` segments. When omitted, the disk's
1386
+ * root is exposed.
1387
+ */
1388
+ subdirectory?: string;
1389
+ /**
1390
+ * When true, mount the disk read-only inside the container. Writes
1391
+ * against the mount fail with EROFS. Defaults to false.
1392
+ */
1393
+ readOnly?: boolean;
1394
+ }
1395
+ /**
1396
+ * One disk to mount in an exec request. Either a `Disk`/disk-id string
1397
+ * (mounts the disk's root, read-write) or an `ExecMountSpec` object that
1398
+ * additionally selects a subdirectoryectory of the disk and/or marks the mount
1399
+ * as read-only. Used by Archil#exec — the map key is the relative path
1400
+ * under /mnt/archil at which to mount the disk.
1401
+ */
1402
+ type ExecMount = Disk | string | ExecMountSpec;
1403
+ interface ExecOptions {
1404
+ /**
1405
+ * Disks to mount, keyed by the relative path under `/mnt/archil` at which
1406
+ * to mount each one. At least one entry is required. Paths must be
1407
+ * non-empty, non-absolute, and contain no `.` or `..` segments.
1408
+ */
1409
+ disks: Record<string, ExecMount>;
1410
+ /** Shell command to run inside the container. */
1411
+ command: string;
1412
+ }
1123
1413
  declare class Archil {
1124
1414
  readonly disks: Disks;
1125
1415
  readonly tokens: Tokens;
1416
+ /** @internal */
1417
+ private readonly _client;
1126
1418
  constructor(opts?: ArchilOptions);
1419
+ /**
1420
+ * Run a command in a container with multiple disks mounted simultaneously,
1421
+ * each at its own relative path under `/mnt/archil`. Blocks until the
1422
+ * command completes and returns its stdout, stderr, exit code, and timing.
1423
+ */
1424
+ exec(opts: ExecOptions): Promise<ExecDiskResult>;
1127
1425
  }
1128
1426
 
1129
1427
  declare class ArchilApiError extends Error {
@@ -1141,5 +1439,11 @@ declare function createApiKey(req: CreateApiTokenRequest): Promise<ApiTokenRespo
1141
1439
  token?: string;
1142
1440
  }>;
1143
1441
  declare function deleteApiKey(id: string): Promise<void>;
1442
+ /**
1443
+ * Run a command in a container with multiple disks mounted simultaneously,
1444
+ * each at its own relative path under `/mnt/archil`. Blocks until the
1445
+ * command completes and returns its stdout, stderr, exit code, and timing.
1446
+ */
1447
+ declare function exec(opts: ExecOptions): Promise<ExecDiskResult>;
1144
1448
 
1145
- export { type ApiTokenResponse, Archil, ArchilApiError, type ArchilOptions, type AuthorizedUser, type AwsStsUser, type AzureBlobMount, type ConnectedClient, type CreateApiTokenRequest, type CreateDiskRequest, type CreateDiskResult, Disk, type DiskMetrics, type DiskResponse, type DiskStatus, type DiskUser, Disks, type ExecResult, type GCSMount, type ListDisksOptions, type ListTokensOptions, type MountConfig, type MountConfigResponse, type MountOptions, type MountResponse, type R2Mount, type S3CompatibleMount, type S3Mount, type TokenUser, Tokens, configure, createApiKey, createDisk, deleteApiKey, getDisk, listApiKeys, listDisks };
1449
+ export { type ApiTokenResponse, Archil, ArchilApiError, type ArchilOptions, type AuthorizedUser, type AwsStsUser, type AzureBlobMount, type ConnectedClient, type CreateApiTokenRequest, type CreateDiskRequest, type CreateDiskResult, Disk, type DiskMetrics, type DiskResponse, type DiskStatus, type DiskUser, Disks, type ExecMount, type ExecMountSpec, type ExecOptions, type ExecRequest, type ExecResult, type GCSMount, type ListDisksOptions, type ListTokensOptions, type MountConfig, type MountConfigResponse, type MountOptions, type MountResponse, type R2Mount, type S3CompatibleMount, type S3Mount, type TokenUser, Tokens, configure, createApiKey, createDisk, deleteApiKey, exec, getDisk, listApiKeys, listDisks };
package/dist/index.js CHANGED
@@ -93,6 +93,7 @@ var Disk = class {
93
93
  metrics;
94
94
  connectedClients;
95
95
  authorizedUsers;
96
+ allowedIps;
96
97
  /** @internal */
97
98
  _client;
98
99
  /** @internal */
@@ -114,6 +115,7 @@ var Disk = class {
114
115
  this.metrics = data.metrics;
115
116
  this.connectedClients = data.connectedClients;
116
117
  this.authorizedUsers = data.authorizedUsers;
118
+ this.allowedIps = data.allowedIps;
117
119
  this._client = client;
118
120
  this._archilRegion = archilRegion;
119
121
  }
@@ -133,7 +135,8 @@ var Disk = class {
133
135
  mounts: this.mounts,
134
136
  metrics: this.metrics,
135
137
  connectedClients: this.connectedClients,
136
- authorizedUsers: this.authorizedUsers
138
+ authorizedUsers: this.authorizedUsers,
139
+ allowedIps: this.allowedIps
137
140
  };
138
141
  }
139
142
  async addUser(user) {
@@ -169,6 +172,32 @@ var Disk = class {
169
172
  async removeTokenUser(identifier) {
170
173
  await this.removeUser("token", identifier);
171
174
  }
175
+ async getAllowedIPs() {
176
+ const data = await unwrap(
177
+ this._client.GET("/api/disks/{id}/allowed-ips", {
178
+ params: { path: { id: this.id } }
179
+ })
180
+ );
181
+ return data.allowedIps;
182
+ }
183
+ async setAllowedIPs(allowedIps) {
184
+ const data = await unwrap(
185
+ this._client.PUT("/api/disks/{id}/allowed-ips", {
186
+ params: { path: { id: this.id } },
187
+ body: { allowedIps }
188
+ })
189
+ );
190
+ return data.allowedIps;
191
+ }
192
+ async addAllowedIP(ip) {
193
+ const current = await this.getAllowedIPs();
194
+ if (current.includes(ip)) return current;
195
+ return this.setAllowedIPs([...current, ip]);
196
+ }
197
+ async removeAllowedIP(ip) {
198
+ const current = await this.getAllowedIPs();
199
+ return this.setAllowedIPs(current.filter((i) => i !== ip));
200
+ }
172
201
  async delete() {
173
202
  await unwrapEmpty(
174
203
  this._client.DELETE("/api/disks/{id}", {
@@ -303,9 +332,17 @@ var Tokens = class {
303
332
  };
304
333
 
305
334
  // src/archil.ts
335
+ function isExecMountSpec(m) {
336
+ return typeof m === "object" && m !== null && "disk" in m;
337
+ }
338
+ function diskIdFromMount(m) {
339
+ return typeof m === "string" ? m : m.id;
340
+ }
306
341
  var Archil = class {
307
342
  disks;
308
343
  tokens;
344
+ /** @internal */
345
+ _client;
309
346
  constructor(opts = {}) {
310
347
  const apiKey = opts.apiKey ?? process.env.ARCHIL_API_KEY;
311
348
  const region = opts.region ?? process.env.ARCHIL_REGION;
@@ -320,9 +357,35 @@ var Archil = class {
320
357
  region,
321
358
  baseUrl: opts.baseUrl
322
359
  });
360
+ this._client = client;
323
361
  this.disks = new Disks(client, region);
324
362
  this.tokens = new Tokens(client);
325
363
  }
364
+ /**
365
+ * Run a command in a container with multiple disks mounted simultaneously,
366
+ * each at its own relative path under `/mnt/archil`. Blocks until the
367
+ * command completes and returns its stdout, stderr, exit code, and timing.
368
+ */
369
+ async exec(opts) {
370
+ const disks = {};
371
+ for (const [relPath, mount] of Object.entries(opts.disks)) {
372
+ if (isExecMountSpec(mount)) {
373
+ const entry = {
374
+ disk: diskIdFromMount(mount.disk),
375
+ readOnly: mount.readOnly ?? false
376
+ };
377
+ if (mount.subdirectory !== void 0) entry.subdirectory = mount.subdirectory;
378
+ disks[relPath] = entry;
379
+ } else {
380
+ disks[relPath] = diskIdFromMount(mount);
381
+ }
382
+ }
383
+ return unwrap(
384
+ this._client.POST("/api/exec", {
385
+ body: { disks, command: opts.command }
386
+ })
387
+ );
388
+ }
326
389
  };
327
390
 
328
391
  // src/index.ts
@@ -356,6 +419,9 @@ function createApiKey(req) {
356
419
  function deleteApiKey(id) {
357
420
  return archil().tokens.delete(id);
358
421
  }
422
+ function exec(opts) {
423
+ return archil().exec(opts);
424
+ }
359
425
  export {
360
426
  Archil,
361
427
  ArchilApiError,
@@ -366,6 +432,7 @@ export {
366
432
  createApiKey,
367
433
  createDisk,
368
434
  deleteApiKey,
435
+ exec,
369
436
  getDisk,
370
437
  listApiKeys,
371
438
  listDisks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "disk",
3
- "version": "0.8.9",
3
+ "version": "0.8.11",
4
4
  "description": "Pure-JS client and CLI for Archil disks",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",