modal 0.3.14 → 0.3.15

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.cjs CHANGED
@@ -18869,7 +18869,8 @@ function createBaseFunctionHandleMetadata() {
18869
18869
  methodHandleMetadata: {},
18870
18870
  functionSchema: void 0,
18871
18871
  inputPlaneUrl: void 0,
18872
- inputPlaneRegion: void 0
18872
+ inputPlaneRegion: void 0,
18873
+ maxObjectSizeBytes: void 0
18873
18874
  };
18874
18875
  }
18875
18876
  var FunctionHandleMetadata = {
@@ -18910,6 +18911,9 @@ var FunctionHandleMetadata = {
18910
18911
  if (message.inputPlaneRegion !== void 0) {
18911
18912
  writer.uint32(378).string(message.inputPlaneRegion);
18912
18913
  }
18914
+ if (message.maxObjectSizeBytes !== void 0) {
18915
+ writer.uint32(384).uint64(message.maxObjectSizeBytes);
18916
+ }
18913
18917
  return writer;
18914
18918
  },
18915
18919
  decode(input, length) {
@@ -19006,6 +19010,13 @@ var FunctionHandleMetadata = {
19006
19010
  message.inputPlaneRegion = reader.string();
19007
19011
  continue;
19008
19012
  }
19013
+ case 48: {
19014
+ if (tag !== 384) {
19015
+ break;
19016
+ }
19017
+ message.maxObjectSizeBytes = longToNumber(reader.uint64());
19018
+ continue;
19019
+ }
19009
19020
  }
19010
19021
  if ((tag & 7) === 4 || tag === 0) {
19011
19022
  break;
@@ -19033,7 +19044,8 @@ var FunctionHandleMetadata = {
19033
19044
  ) : {},
19034
19045
  functionSchema: isSet3(object.functionSchema) ? FunctionSchema.fromJSON(object.functionSchema) : void 0,
19035
19046
  inputPlaneUrl: isSet3(object.inputPlaneUrl) ? globalThis.String(object.inputPlaneUrl) : void 0,
19036
- inputPlaneRegion: isSet3(object.inputPlaneRegion) ? globalThis.String(object.inputPlaneRegion) : void 0
19047
+ inputPlaneRegion: isSet3(object.inputPlaneRegion) ? globalThis.String(object.inputPlaneRegion) : void 0,
19048
+ maxObjectSizeBytes: isSet3(object.maxObjectSizeBytes) ? globalThis.Number(object.maxObjectSizeBytes) : void 0
19037
19049
  };
19038
19050
  },
19039
19051
  toJSON(message) {
@@ -19080,6 +19092,9 @@ var FunctionHandleMetadata = {
19080
19092
  if (message.inputPlaneRegion !== void 0) {
19081
19093
  obj.inputPlaneRegion = message.inputPlaneRegion;
19082
19094
  }
19095
+ if (message.maxObjectSizeBytes !== void 0) {
19096
+ obj.maxObjectSizeBytes = Math.round(message.maxObjectSizeBytes);
19097
+ }
19083
19098
  return obj;
19084
19099
  },
19085
19100
  create(base) {
@@ -19104,6 +19119,7 @@ var FunctionHandleMetadata = {
19104
19119
  message.functionSchema = object.functionSchema !== void 0 && object.functionSchema !== null ? FunctionSchema.fromPartial(object.functionSchema) : void 0;
19105
19120
  message.inputPlaneUrl = object.inputPlaneUrl ?? void 0;
19106
19121
  message.inputPlaneRegion = object.inputPlaneRegion ?? void 0;
19122
+ message.maxObjectSizeBytes = object.maxObjectSizeBytes ?? void 0;
19107
19123
  return message;
19108
19124
  }
19109
19125
  };
@@ -38670,7 +38686,7 @@ function authMiddleware(profile) {
38670
38686
  options.metadata ??= new import_nice_grpc.Metadata();
38671
38687
  options.metadata.set(
38672
38688
  "x-modal-client-type",
38673
- String(7 /* CLIENT_TYPE_LIBMODAL */)
38689
+ String(8 /* CLIENT_TYPE_LIBMODAL_JS */)
38674
38690
  );
38675
38691
  options.metadata.set("x-modal-client-version", "1.0.0");
38676
38692
  options.metadata.set("x-modal-token-id", tokenId);
@@ -39191,7 +39207,7 @@ var Tunnel = class {
39191
39207
  return [this.unencryptedHost, this.unencryptedPort];
39192
39208
  }
39193
39209
  };
39194
- var Sandbox2 = class {
39210
+ var Sandbox2 = class _Sandbox {
39195
39211
  sandboxId;
39196
39212
  stdin;
39197
39213
  stdout;
@@ -39233,11 +39249,13 @@ var Sandbox2 = class {
39233
39249
  }
39234
39250
  async exec(command, options) {
39235
39251
  const taskId = await this.#getTaskId();
39252
+ const secretIds = options?.secrets ? options.secrets.map((secret) => secret.secretId) : [];
39236
39253
  const resp = await client.containerExec({
39237
39254
  taskId,
39238
39255
  command,
39239
39256
  workdir: options?.workdir,
39240
- timeoutSecs: options?.timeout ? options.timeout / 1e3 : 0
39257
+ timeoutSecs: options?.timeout ? options.timeout / 1e3 : 0,
39258
+ secretIds
39241
39259
  });
39242
39260
  return new ContainerProcess(resp.execId, options);
39243
39261
  }
@@ -39271,7 +39289,7 @@ var Sandbox2 = class {
39271
39289
  timeout: 55
39272
39290
  });
39273
39291
  if (resp.result) {
39274
- return resp.result.exitcode;
39292
+ return _Sandbox.#getReturnCode(resp.result);
39275
39293
  }
39276
39294
  }
39277
39295
  }
@@ -39304,6 +39322,53 @@ var Sandbox2 = class {
39304
39322
  }
39305
39323
  return this.#tunnels;
39306
39324
  }
39325
+ /**
39326
+ * Snapshot the filesystem of the Sandbox.
39327
+ *
39328
+ * Returns an `Image` object which can be used to spawn a new Sandbox with the same filesystem.
39329
+ *
39330
+ * @param timeout - Timeout for the snapshot operation in milliseconds
39331
+ * @returns Promise that resolves to an Image
39332
+ */
39333
+ async snapshotFilesystem(timeout = 55e3) {
39334
+ const resp = await client.sandboxSnapshotFs({
39335
+ sandboxId: this.sandboxId,
39336
+ timeout: timeout / 1e3
39337
+ });
39338
+ if (resp.result?.status !== 1 /* GENERIC_STATUS_SUCCESS */) {
39339
+ throw new Error(
39340
+ `Sandbox snapshot failed: ${resp.result?.exception || "Unknown error"}`
39341
+ );
39342
+ }
39343
+ if (!resp.imageId) {
39344
+ throw new Error("Sandbox snapshot response missing image ID");
39345
+ }
39346
+ return new Image2(resp.imageId);
39347
+ }
39348
+ /**
39349
+ * Check if the Sandbox has finished running.
39350
+ *
39351
+ * Returns `null` if the Sandbox is still running, else returns the exit code.
39352
+ */
39353
+ async poll() {
39354
+ const resp = await client.sandboxWait({
39355
+ sandboxId: this.sandboxId,
39356
+ timeout: 0
39357
+ });
39358
+ return _Sandbox.#getReturnCode(resp.result);
39359
+ }
39360
+ static #getReturnCode(result) {
39361
+ if (result === void 0 || result.status === 0 /* GENERIC_STATUS_UNSPECIFIED */) {
39362
+ return null;
39363
+ }
39364
+ if (result.status === 4 /* GENERIC_STATUS_TIMEOUT */) {
39365
+ return 124;
39366
+ } else if (result.status === 3 /* GENERIC_STATUS_TERMINATED */) {
39367
+ return 137;
39368
+ } else {
39369
+ return result.exitcode;
39370
+ }
39371
+ }
39307
39372
  };
39308
39373
  var ContainerProcess = class {
39309
39374
  stdin;
@@ -39544,6 +39609,7 @@ var App = class _App {
39544
39609
  }))
39545
39610
  );
39546
39611
  }
39612
+ const secretIds = options.secrets ? options.secrets.map((secret) => secret.secretId) : [];
39547
39613
  const createResp = await client.sandboxCreate({
39548
39614
  appId: this.appId,
39549
39615
  definition: {
@@ -39560,6 +39626,7 @@ var App = class _App {
39560
39626
  memoryMb: options.memory ?? 128
39561
39627
  },
39562
39628
  volumeMounts,
39629
+ secretIds,
39563
39630
  openPorts: openPorts.length > 0 ? { ports: openPorts } : void 0
39564
39631
  }
39565
39632
  });
package/dist/index.d.cts CHANGED
@@ -124,6 +124,20 @@ interface ModalWriteStream<R = any> extends WritableStream<R> {
124
124
  writeBytes(bytes: Uint8Array): Promise<void>;
125
125
  }
126
126
 
127
+ /** Options for `Secret.fromName()`. */
128
+ type SecretFromNameOptions = {
129
+ environment?: string;
130
+ requiredKeys?: string[];
131
+ };
132
+ /** Secrets provide a dictionary of environment variables for images. */
133
+ declare class Secret {
134
+ readonly secretId: string;
135
+ /** @ignore */
136
+ constructor(secretId: string);
137
+ /** Reference a Secret by its name. */
138
+ static fromName(name: string, options?: SecretFromNameOptions): Promise<Secret>;
139
+ }
140
+
127
141
  /**
128
142
  * Stdin is always present, but this option allow you to drop stdout or stderr
129
143
  * if you don't need them. The default is "pipe", matching Node.js behavior.
@@ -149,6 +163,8 @@ type ExecOptions = {
149
163
  workdir?: string;
150
164
  /** Timeout for the process in milliseconds. Defaults to 0 (no timeout). */
151
165
  timeout?: number;
166
+ /** Secrets with environment variables for the command. */
167
+ secrets?: [Secret];
152
168
  };
153
169
  /** A port forwarded from within a running Modal sandbox. */
154
170
  declare class Tunnel {
@@ -196,6 +212,21 @@ declare class Sandbox {
196
212
  * @returns A dictionary of Tunnel objects which are keyed by the container port.
197
213
  */
198
214
  tunnels(timeout?: number): Promise<Record<number, Tunnel>>;
215
+ /**
216
+ * Snapshot the filesystem of the Sandbox.
217
+ *
218
+ * Returns an `Image` object which can be used to spawn a new Sandbox with the same filesystem.
219
+ *
220
+ * @param timeout - Timeout for the snapshot operation in milliseconds
221
+ * @returns Promise that resolves to an Image
222
+ */
223
+ snapshotFilesystem(timeout?: number): Promise<Image>;
224
+ /**
225
+ * Check if the Sandbox has finished running.
226
+ *
227
+ * Returns `null` if the Sandbox is still running, else returns the exit code.
228
+ */
229
+ poll(): Promise<number | null>;
199
230
  }
200
231
  declare class ContainerProcess<R extends string | Uint8Array = any> {
201
232
  #private;
@@ -208,20 +239,6 @@ declare class ContainerProcess<R extends string | Uint8Array = any> {
208
239
  wait(): Promise<number>;
209
240
  }
210
241
 
211
- /** Options for `Secret.fromName()`. */
212
- type SecretFromNameOptions = {
213
- environment?: string;
214
- requiredKeys?: string[];
215
- };
216
- /** Secrets provide a dictionary of environment variables for images. */
217
- declare class Secret {
218
- readonly secretId: string;
219
- /** @ignore */
220
- constructor(secretId: string);
221
- /** Reference a Secret by its name. */
222
- static fromName(name: string, options?: SecretFromNameOptions): Promise<Secret>;
223
- }
224
-
225
242
  /** Options for `Volume.fromName()`. */
226
243
  type VolumeFromNameOptions = {
227
244
  environment?: string;
@@ -261,6 +278,8 @@ type SandboxCreateOptions = {
261
278
  * Default behavior is to sleep indefinitely until timeout or termination.
262
279
  */
263
280
  command?: string[];
281
+ /** Secrets to inject into the sandbox. */
282
+ secrets?: Secret[];
264
283
  /** Mount points for Modal Volumes. */
265
284
  volumes?: Record<string, Volume>;
266
285
  /** List of ports to tunnel into the sandbox. Encrypted ports are tunneled with TLS. */
package/dist/index.d.ts CHANGED
@@ -124,6 +124,20 @@ interface ModalWriteStream<R = any> extends WritableStream<R> {
124
124
  writeBytes(bytes: Uint8Array): Promise<void>;
125
125
  }
126
126
 
127
+ /** Options for `Secret.fromName()`. */
128
+ type SecretFromNameOptions = {
129
+ environment?: string;
130
+ requiredKeys?: string[];
131
+ };
132
+ /** Secrets provide a dictionary of environment variables for images. */
133
+ declare class Secret {
134
+ readonly secretId: string;
135
+ /** @ignore */
136
+ constructor(secretId: string);
137
+ /** Reference a Secret by its name. */
138
+ static fromName(name: string, options?: SecretFromNameOptions): Promise<Secret>;
139
+ }
140
+
127
141
  /**
128
142
  * Stdin is always present, but this option allow you to drop stdout or stderr
129
143
  * if you don't need them. The default is "pipe", matching Node.js behavior.
@@ -149,6 +163,8 @@ type ExecOptions = {
149
163
  workdir?: string;
150
164
  /** Timeout for the process in milliseconds. Defaults to 0 (no timeout). */
151
165
  timeout?: number;
166
+ /** Secrets with environment variables for the command. */
167
+ secrets?: [Secret];
152
168
  };
153
169
  /** A port forwarded from within a running Modal sandbox. */
154
170
  declare class Tunnel {
@@ -196,6 +212,21 @@ declare class Sandbox {
196
212
  * @returns A dictionary of Tunnel objects which are keyed by the container port.
197
213
  */
198
214
  tunnels(timeout?: number): Promise<Record<number, Tunnel>>;
215
+ /**
216
+ * Snapshot the filesystem of the Sandbox.
217
+ *
218
+ * Returns an `Image` object which can be used to spawn a new Sandbox with the same filesystem.
219
+ *
220
+ * @param timeout - Timeout for the snapshot operation in milliseconds
221
+ * @returns Promise that resolves to an Image
222
+ */
223
+ snapshotFilesystem(timeout?: number): Promise<Image>;
224
+ /**
225
+ * Check if the Sandbox has finished running.
226
+ *
227
+ * Returns `null` if the Sandbox is still running, else returns the exit code.
228
+ */
229
+ poll(): Promise<number | null>;
199
230
  }
200
231
  declare class ContainerProcess<R extends string | Uint8Array = any> {
201
232
  #private;
@@ -208,20 +239,6 @@ declare class ContainerProcess<R extends string | Uint8Array = any> {
208
239
  wait(): Promise<number>;
209
240
  }
210
241
 
211
- /** Options for `Secret.fromName()`. */
212
- type SecretFromNameOptions = {
213
- environment?: string;
214
- requiredKeys?: string[];
215
- };
216
- /** Secrets provide a dictionary of environment variables for images. */
217
- declare class Secret {
218
- readonly secretId: string;
219
- /** @ignore */
220
- constructor(secretId: string);
221
- /** Reference a Secret by its name. */
222
- static fromName(name: string, options?: SecretFromNameOptions): Promise<Secret>;
223
- }
224
-
225
242
  /** Options for `Volume.fromName()`. */
226
243
  type VolumeFromNameOptions = {
227
244
  environment?: string;
@@ -261,6 +278,8 @@ type SandboxCreateOptions = {
261
278
  * Default behavior is to sleep indefinitely until timeout or termination.
262
279
  */
263
280
  command?: string[];
281
+ /** Secrets to inject into the sandbox. */
282
+ secrets?: Secret[];
264
283
  /** Mount points for Modal Volumes. */
265
284
  volumes?: Record<string, Volume>;
266
285
  /** List of ports to tunnel into the sandbox. Encrypted ports are tunneled with TLS. */
package/dist/index.js CHANGED
@@ -18813,7 +18813,8 @@ function createBaseFunctionHandleMetadata() {
18813
18813
  methodHandleMetadata: {},
18814
18814
  functionSchema: void 0,
18815
18815
  inputPlaneUrl: void 0,
18816
- inputPlaneRegion: void 0
18816
+ inputPlaneRegion: void 0,
18817
+ maxObjectSizeBytes: void 0
18817
18818
  };
18818
18819
  }
18819
18820
  var FunctionHandleMetadata = {
@@ -18854,6 +18855,9 @@ var FunctionHandleMetadata = {
18854
18855
  if (message.inputPlaneRegion !== void 0) {
18855
18856
  writer.uint32(378).string(message.inputPlaneRegion);
18856
18857
  }
18858
+ if (message.maxObjectSizeBytes !== void 0) {
18859
+ writer.uint32(384).uint64(message.maxObjectSizeBytes);
18860
+ }
18857
18861
  return writer;
18858
18862
  },
18859
18863
  decode(input, length) {
@@ -18950,6 +18954,13 @@ var FunctionHandleMetadata = {
18950
18954
  message.inputPlaneRegion = reader.string();
18951
18955
  continue;
18952
18956
  }
18957
+ case 48: {
18958
+ if (tag !== 384) {
18959
+ break;
18960
+ }
18961
+ message.maxObjectSizeBytes = longToNumber(reader.uint64());
18962
+ continue;
18963
+ }
18953
18964
  }
18954
18965
  if ((tag & 7) === 4 || tag === 0) {
18955
18966
  break;
@@ -18977,7 +18988,8 @@ var FunctionHandleMetadata = {
18977
18988
  ) : {},
18978
18989
  functionSchema: isSet3(object.functionSchema) ? FunctionSchema.fromJSON(object.functionSchema) : void 0,
18979
18990
  inputPlaneUrl: isSet3(object.inputPlaneUrl) ? globalThis.String(object.inputPlaneUrl) : void 0,
18980
- inputPlaneRegion: isSet3(object.inputPlaneRegion) ? globalThis.String(object.inputPlaneRegion) : void 0
18991
+ inputPlaneRegion: isSet3(object.inputPlaneRegion) ? globalThis.String(object.inputPlaneRegion) : void 0,
18992
+ maxObjectSizeBytes: isSet3(object.maxObjectSizeBytes) ? globalThis.Number(object.maxObjectSizeBytes) : void 0
18981
18993
  };
18982
18994
  },
18983
18995
  toJSON(message) {
@@ -19024,6 +19036,9 @@ var FunctionHandleMetadata = {
19024
19036
  if (message.inputPlaneRegion !== void 0) {
19025
19037
  obj.inputPlaneRegion = message.inputPlaneRegion;
19026
19038
  }
19039
+ if (message.maxObjectSizeBytes !== void 0) {
19040
+ obj.maxObjectSizeBytes = Math.round(message.maxObjectSizeBytes);
19041
+ }
19027
19042
  return obj;
19028
19043
  },
19029
19044
  create(base) {
@@ -19048,6 +19063,7 @@ var FunctionHandleMetadata = {
19048
19063
  message.functionSchema = object.functionSchema !== void 0 && object.functionSchema !== null ? FunctionSchema.fromPartial(object.functionSchema) : void 0;
19049
19064
  message.inputPlaneUrl = object.inputPlaneUrl ?? void 0;
19050
19065
  message.inputPlaneRegion = object.inputPlaneRegion ?? void 0;
19066
+ message.maxObjectSizeBytes = object.maxObjectSizeBytes ?? void 0;
19051
19067
  return message;
19052
19068
  }
19053
19069
  };
@@ -38620,7 +38636,7 @@ function authMiddleware(profile) {
38620
38636
  options.metadata ??= new Metadata();
38621
38637
  options.metadata.set(
38622
38638
  "x-modal-client-type",
38623
- String(7 /* CLIENT_TYPE_LIBMODAL */)
38639
+ String(8 /* CLIENT_TYPE_LIBMODAL_JS */)
38624
38640
  );
38625
38641
  options.metadata.set("x-modal-client-version", "1.0.0");
38626
38642
  options.metadata.set("x-modal-token-id", tokenId);
@@ -39141,7 +39157,7 @@ var Tunnel = class {
39141
39157
  return [this.unencryptedHost, this.unencryptedPort];
39142
39158
  }
39143
39159
  };
39144
- var Sandbox2 = class {
39160
+ var Sandbox2 = class _Sandbox {
39145
39161
  sandboxId;
39146
39162
  stdin;
39147
39163
  stdout;
@@ -39183,11 +39199,13 @@ var Sandbox2 = class {
39183
39199
  }
39184
39200
  async exec(command, options) {
39185
39201
  const taskId = await this.#getTaskId();
39202
+ const secretIds = options?.secrets ? options.secrets.map((secret) => secret.secretId) : [];
39186
39203
  const resp = await client.containerExec({
39187
39204
  taskId,
39188
39205
  command,
39189
39206
  workdir: options?.workdir,
39190
- timeoutSecs: options?.timeout ? options.timeout / 1e3 : 0
39207
+ timeoutSecs: options?.timeout ? options.timeout / 1e3 : 0,
39208
+ secretIds
39191
39209
  });
39192
39210
  return new ContainerProcess(resp.execId, options);
39193
39211
  }
@@ -39221,7 +39239,7 @@ var Sandbox2 = class {
39221
39239
  timeout: 55
39222
39240
  });
39223
39241
  if (resp.result) {
39224
- return resp.result.exitcode;
39242
+ return _Sandbox.#getReturnCode(resp.result);
39225
39243
  }
39226
39244
  }
39227
39245
  }
@@ -39254,6 +39272,53 @@ var Sandbox2 = class {
39254
39272
  }
39255
39273
  return this.#tunnels;
39256
39274
  }
39275
+ /**
39276
+ * Snapshot the filesystem of the Sandbox.
39277
+ *
39278
+ * Returns an `Image` object which can be used to spawn a new Sandbox with the same filesystem.
39279
+ *
39280
+ * @param timeout - Timeout for the snapshot operation in milliseconds
39281
+ * @returns Promise that resolves to an Image
39282
+ */
39283
+ async snapshotFilesystem(timeout = 55e3) {
39284
+ const resp = await client.sandboxSnapshotFs({
39285
+ sandboxId: this.sandboxId,
39286
+ timeout: timeout / 1e3
39287
+ });
39288
+ if (resp.result?.status !== 1 /* GENERIC_STATUS_SUCCESS */) {
39289
+ throw new Error(
39290
+ `Sandbox snapshot failed: ${resp.result?.exception || "Unknown error"}`
39291
+ );
39292
+ }
39293
+ if (!resp.imageId) {
39294
+ throw new Error("Sandbox snapshot response missing image ID");
39295
+ }
39296
+ return new Image2(resp.imageId);
39297
+ }
39298
+ /**
39299
+ * Check if the Sandbox has finished running.
39300
+ *
39301
+ * Returns `null` if the Sandbox is still running, else returns the exit code.
39302
+ */
39303
+ async poll() {
39304
+ const resp = await client.sandboxWait({
39305
+ sandboxId: this.sandboxId,
39306
+ timeout: 0
39307
+ });
39308
+ return _Sandbox.#getReturnCode(resp.result);
39309
+ }
39310
+ static #getReturnCode(result) {
39311
+ if (result === void 0 || result.status === 0 /* GENERIC_STATUS_UNSPECIFIED */) {
39312
+ return null;
39313
+ }
39314
+ if (result.status === 4 /* GENERIC_STATUS_TIMEOUT */) {
39315
+ return 124;
39316
+ } else if (result.status === 3 /* GENERIC_STATUS_TERMINATED */) {
39317
+ return 137;
39318
+ } else {
39319
+ return result.exitcode;
39320
+ }
39321
+ }
39257
39322
  };
39258
39323
  var ContainerProcess = class {
39259
39324
  stdin;
@@ -39494,6 +39559,7 @@ var App = class _App {
39494
39559
  }))
39495
39560
  );
39496
39561
  }
39562
+ const secretIds = options.secrets ? options.secrets.map((secret) => secret.secretId) : [];
39497
39563
  const createResp = await client.sandboxCreate({
39498
39564
  appId: this.appId,
39499
39565
  definition: {
@@ -39510,6 +39576,7 @@ var App = class _App {
39510
39576
  memoryMb: options.memory ?? 128
39511
39577
  },
39512
39578
  volumeMounts,
39579
+ secretIds,
39513
39580
  openPorts: openPorts.length > 0 ? { ports: openPorts } : void 0
39514
39581
  }
39515
39582
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modal",
3
- "version": "0.3.14",
3
+ "version": "0.3.15",
4
4
  "description": "Modal client library for JavaScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://modal.com/docs",