deepline 0.1.317 → 0.1.319

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.
@@ -1022,7 +1022,7 @@ var SDK_RELEASE = {
1022
1022
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
1023
1023
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
1024
1024
  // Operators use the checkout-local deepline-admin binary instead.
1025
- version: "0.1.317",
1025
+ version: "0.1.319",
1026
1026
  contracts: {
1027
1027
  api: {
1028
1028
  name: "sdk-http-api",
@@ -3512,6 +3512,9 @@ function stagedUploadIdentity(logicalPath, contentHash) {
3512
3512
  function isStagedUploadMintUnsupported(error) {
3513
3513
  return error instanceof DeeplineError && error.statusCode === 404;
3514
3514
  }
3515
+ function isStagedUploadDirectEgressDenied(error) {
3516
+ return error instanceof DeeplineError && error.code === "STAGED_FILE_UPLOAD_EGRESS_DENIED";
3517
+ }
3515
3518
  function formatMegabytes(bytes) {
3516
3519
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
3517
3520
  }
@@ -3532,10 +3535,11 @@ async function uploadStagedFileToPresignedUrl(input2) {
3532
3535
  return;
3533
3536
  }
3534
3537
  const text = await response.text().catch(() => "");
3538
+ const egressDenied = response.status === 403 && response.headers.get("x-deny-reason")?.trim().toLowerCase() === "host_not_allowed";
3535
3539
  lastError = new DeeplineError(
3536
3540
  `Direct storage upload of ${input2.logicalPath} failed: ${response.status} ${text.slice(0, 200)}`,
3537
3541
  response.status,
3538
- "STAGED_FILE_UPLOAD_FAILED",
3542
+ egressDenied ? "STAGED_FILE_UPLOAD_EGRESS_DENIED" : "STAGED_FILE_UPLOAD_FAILED",
3539
3543
  { logicalPath: input2.logicalPath }
3540
3544
  );
3541
3545
  if (response.status < 500 && response.status !== 429) {
@@ -4435,7 +4439,7 @@ var DeeplineClient = class {
4435
4439
  upload
4436
4440
  );
4437
4441
  }
4438
- await Promise.all(
4442
+ const directResults = await Promise.all(
4439
4443
  files.map(async (file) => {
4440
4444
  const upload = uploadByIdentity.get(
4441
4445
  stagedUploadIdentity(file.logicalPath, file.contentHash)
@@ -4448,31 +4452,45 @@ var DeeplineClient = class {
4448
4452
  );
4449
4453
  }
4450
4454
  if (upload.alreadyStaged || !upload.uploadUrl) {
4451
- return;
4455
+ return { ref: upload.ref };
4456
+ }
4457
+ try {
4458
+ await uploadStagedFileToPresignedUrl({
4459
+ url: upload.uploadUrl,
4460
+ headers: upload.uploadHeaders ?? {
4461
+ "content-type": file.contentType
4462
+ },
4463
+ body: decodeBase64Bytes(file.contentBase64),
4464
+ logicalPath: file.logicalPath
4465
+ });
4466
+ return { ref: upload.ref };
4467
+ } catch (error) {
4468
+ if (isStagedUploadDirectEgressDenied(error)) {
4469
+ return { fallbackFile: file };
4470
+ }
4471
+ throw error;
4452
4472
  }
4453
- await uploadStagedFileToPresignedUrl({
4454
- url: upload.uploadUrl,
4455
- headers: upload.uploadHeaders ?? {
4456
- "content-type": file.contentType
4457
- },
4458
- body: decodeBase64Bytes(file.contentBase64),
4459
- logicalPath: file.logicalPath
4460
- });
4461
4473
  })
4462
4474
  );
4463
- return files.map((file) => {
4464
- const upload = uploadByIdentity.get(
4465
- stagedUploadIdentity(file.logicalPath, file.contentHash)
4466
- );
4467
- if (!upload) {
4468
- throw new DeeplineError(
4469
- `The staging server did not return an upload target for ${file.logicalPath}.`,
4470
- void 0,
4471
- "STAGED_FILE_MINT_INCOMPLETE"
4472
- );
4473
- }
4474
- return upload.ref;
4475
- });
4475
+ return await Promise.all(
4476
+ directResults.map(async (result) => {
4477
+ if ("fallbackFile" in result) {
4478
+ const [ref] = await this.stagePlayFilesViaMultipart(
4479
+ [result.fallbackFile],
4480
+ "direct-egress-denied"
4481
+ );
4482
+ if (!ref) {
4483
+ throw new DeeplineError(
4484
+ `The staging server did not return an upload target for ${result.fallbackFile.logicalPath}.`,
4485
+ void 0,
4486
+ "STAGED_FILE_MINT_INCOMPLETE"
4487
+ );
4488
+ }
4489
+ return ref;
4490
+ }
4491
+ return result.ref;
4492
+ })
4493
+ );
4476
4494
  }
4477
4495
  /**
4478
4496
  * Mint short-lived presigned upload targets for staged play files.
@@ -4485,11 +4503,12 @@ var DeeplineClient = class {
4485
4503
  const response = await this.http.post("/api/v2/plays/files/stage/mint", { files });
4486
4504
  return response.uploads ?? [];
4487
4505
  }
4488
- async stagePlayFilesViaMultipart(files) {
4506
+ async stagePlayFilesViaMultipart(files, reason = "mint-unsupported") {
4489
4507
  for (const file of files) {
4490
4508
  if (file.bytes > STAGE_LEGACY_MULTIPART_MAX_BYTES) {
4509
+ const reasonMessage = reason === "direct-egress-denied" ? "direct storage is blocked by this environment's network egress policy" : "the connected Deepline server does not support direct-to-storage uploads";
4491
4510
  throw new DeeplineError(
4492
- `Cannot stage ${file.logicalPath} (${formatMegabytes(file.bytes)}): the connected Deepline server does not support direct-to-storage uploads, and this file exceeds the ~4.5MB request-body limit the legacy upload path is subject to. Update the Deepline server (or target code.deepline.com) to stage files this large.`,
4511
+ `Cannot stage ${file.logicalPath} (${formatMegabytes(file.bytes)}): ${reasonMessage}, and this file exceeds the ~4.5MB request-body limit the legacy upload path is subject to.`,
4493
4512
  413,
4494
4513
  "STAGED_FILE_TOO_LARGE_FOR_LEGACY_UPLOAD",
4495
4514
  {
@@ -28618,14 +28637,23 @@ function toolContractJsonForDescribe(tool, requestedToolId) {
28618
28637
  "deeplineUsdPerPricingUnit",
28619
28638
  "deepline_usd_per_pricing_unit"
28620
28639
  );
28621
- const starterScript = !isPlayLikeTool(tool) && extractedLists.length > 0 ? starterScriptJson(
28640
+ const deprecation = recordField2(tool, "deprecation");
28641
+ const replacementToolId = stringField2(
28642
+ deprecation,
28643
+ "replacementToolId",
28644
+ "replacement_tool_id"
28645
+ );
28646
+ const deprecationMessage = stringField2(deprecation, "message");
28647
+ const deprecationExecution = stringField2(deprecation, "execution");
28648
+ const deprecated = tool.deprecated === true || stringField2(tool, "credentialStatus", "credential_status") === "deprecated";
28649
+ const starterScript = !deprecated && !isPlayLikeTool(tool) && extractedLists.length > 0 ? starterScriptJson(
28622
28650
  seedToolListScript({
28623
28651
  toolId,
28624
28652
  payload: samplePayloadForInputFields(inputFields),
28625
28653
  rows: []
28626
28654
  })
28627
28655
  ) : null;
28628
- const executeCommand = isPlayLikeTool(tool) ? playRunCommandForTool(tool, toolId) : `deepline tools execute ${toolId} --input '{...}' --json`;
28656
+ const executeCommand = deprecated && replacementToolId ? `deepline tools execute ${replacementToolId} --input '{...}' --json` : isPlayLikeTool(tool) ? playRunCommandForTool(tool, toolId) : `deepline tools execute ${toolId} --input '{...}' --json`;
28629
28657
  return {
28630
28658
  schemaVersion: 1,
28631
28659
  toolId,
@@ -28633,6 +28661,15 @@ function toolContractJsonForDescribe(tool, requestedToolId) {
28633
28661
  displayName: tool.displayName,
28634
28662
  description: tool.description,
28635
28663
  categories: tool.categories,
28664
+ ...deprecated ? {
28665
+ deprecated: true,
28666
+ callable: tool.callable !== false,
28667
+ deprecation: {
28668
+ replacementToolId,
28669
+ message: deprecationMessage,
28670
+ ...deprecationExecution ? { execution: deprecationExecution } : {}
28671
+ }
28672
+ } : {},
28636
28673
  inputFields: inputFields.map((field) => ({
28637
28674
  name: field.name,
28638
28675
  type: field.type ?? "unknown",
@@ -28775,6 +28812,29 @@ function printCompactToolContract(tool, requestedToolId) {
28775
28812
  if (Array.isArray(contract.categories) && contract.categories.length) {
28776
28813
  console.log(`Tags: ${contract.categories.join(", ")}`);
28777
28814
  }
28815
+ if (contract.deprecated === true) {
28816
+ const deprecation = isRecord11(contract.deprecation) ? contract.deprecation : {};
28817
+ const message = stringField2(deprecation, "message");
28818
+ const replacementToolId = stringField2(
28819
+ deprecation,
28820
+ "replacementToolId",
28821
+ "replacement_tool_id"
28822
+ );
28823
+ console.log(
28824
+ contract.callable === false ? "Status: deprecated \u2014 this tool cannot be executed" : "Status: deprecated \u2014 legacy executions forward for compatibility"
28825
+ );
28826
+ if (message) console.log(`Migration: ${message}`);
28827
+ if (replacementToolId) {
28828
+ console.log(
28829
+ `Use: deepline tools execute ${replacementToolId} --input '{...}'`
28830
+ );
28831
+ }
28832
+ console.log("");
28833
+ console.log(
28834
+ `More: deepline tools describe ${replacementToolId || contract.toolId} --json`
28835
+ );
28836
+ return;
28837
+ }
28778
28838
  printToolPricingOnly(tool, requestedToolId, { heading: "Cost" });
28779
28839
  if (inputFields.length) {
28780
28840
  console.log("");
@@ -29274,6 +29334,32 @@ function samplePayload(samples, key) {
29274
29334
  function commandEnvelopeFromRawResponse(rawResponse) {
29275
29335
  return isRecord11(rawResponse) ? { ...rawResponse } : { status: "completed", result: rawResponse };
29276
29336
  }
29337
+ function extractToolExecutionWarningMessages(rawResponse) {
29338
+ if (!isRecord11(rawResponse)) return [];
29339
+ const candidates = [
29340
+ recordField2(recordField2(rawResponse, "toolResponse"), "meta"),
29341
+ recordField2(
29342
+ recordField2(recordField2(rawResponse, "toolResponse"), "raw"),
29343
+ "meta"
29344
+ ),
29345
+ recordField2(rawResponse, "meta"),
29346
+ recordField2(recordField2(rawResponse, "result"), "meta")
29347
+ ];
29348
+ const messages = [];
29349
+ for (const candidate of candidates) {
29350
+ const warnings = candidate?.warnings;
29351
+ if (!Array.isArray(warnings)) continue;
29352
+ for (const warning of warnings) {
29353
+ if (typeof warning === "string" && warning.trim()) {
29354
+ messages.push(warning.trim());
29355
+ } else if (isRecord11(warning)) {
29356
+ const message = stringField2(warning, "message");
29357
+ if (message) messages.push(message);
29358
+ }
29359
+ }
29360
+ }
29361
+ return [...new Set(messages)];
29362
+ }
29277
29363
  function apifySyncRecoveryNext(rawResponse) {
29278
29364
  if (!isRecord11(rawResponse) || rawResponse.status !== "running") return null;
29279
29365
  const toolResponse = recordField2(rawResponse, "toolResponse");
@@ -29541,6 +29627,9 @@ export default definePlay(${JSON.stringify(playName)}, async (ctx) => {
29541
29627
  }
29542
29628
  function buildToolExecuteBaseEnvelope(input2) {
29543
29629
  const envelope = commandEnvelopeFromRawResponse(input2.rawResponse);
29630
+ const warningMessages = extractToolExecutionWarningMessages(
29631
+ input2.rawResponse
29632
+ );
29544
29633
  const apifyRecovery = apifySyncRecoveryNext(input2.rawResponse);
29545
29634
  const summaryEntries = Object.entries(input2.summary);
29546
29635
  const outputPreview = input2.listConversion ? {
@@ -29578,6 +29667,7 @@ function buildToolExecuteBaseEnvelope(input2) {
29578
29667
  ...envelope,
29579
29668
  ...envelopeHasCanonicalOutput || envelopeHasDeclaredOutput ? { output_preview: outputPreview } : { output: outputPreview },
29580
29669
  ...summaryEntries.length > 0 ? { summary: input2.summary } : {},
29670
+ ...warningMessages.length > 0 ? { warnings: warningMessages } : {},
29581
29671
  next: {
29582
29672
  inspect: inspectCommand,
29583
29673
  ...apifyRecovery ?? {},
@@ -29589,6 +29679,7 @@ function buildToolExecuteBaseEnvelope(input2) {
29589
29679
  },
29590
29680
  render: {
29591
29681
  sections: input2.listConversion ? [
29682
+ ...warningMessages.length > 0 ? [{ title: "warnings", lines: warningMessages }] : [],
29592
29683
  {
29593
29684
  title: "output",
29594
29685
  lines: [
@@ -29599,6 +29690,7 @@ function buildToolExecuteBaseEnvelope(input2) {
29599
29690
  ]
29600
29691
  }
29601
29692
  ] : [
29693
+ ...warningMessages.length > 0 ? [{ title: "warnings", lines: warningMessages }] : [],
29602
29694
  {
29603
29695
  title: "result",
29604
29696
  lines: apifyRecovery ? [
package/dist/index.d.mts CHANGED
@@ -355,16 +355,26 @@ interface ToolDefinition {
355
355
  }>;
356
356
  /**
357
357
  * Whether this tool is callable in the current workspace. `false` for a
358
- * bring-your-own-credential provider (e.g. Apollo) that has not been
359
- * connected — the agent should offer to connect it rather than call it.
358
+ * bring-your-own-credential provider that has not been connected.
360
359
  */
361
360
  connected?: boolean;
361
+ /** Whether the tool can be executed. Exact lookup may return non-callable deprecated aliases. */
362
+ callable?: boolean;
363
+ /** True when callers should migrate this exact tool id to its replacement. */
364
+ deprecated?: boolean;
365
+ /** Deprecation reason, replacement, and compatibility execution behavior. */
366
+ deprecation?: {
367
+ replacementToolId: string;
368
+ message: string;
369
+ execution?: 'terminal' | 'forward';
370
+ };
362
371
  /**
363
372
  * Connection status for discovery: `managed` (Deepline-run credentials),
364
373
  * `connected` (your own credential is connected), or `requires_connection`
365
- * (BYO provider not yet connected in this workspace).
374
+ * (BYO provider not yet connected in this workspace). `deprecated` means
375
+ * connecting credentials will not make the tool callable.
366
376
  */
367
- credentialStatus?: 'managed' | 'connected' | 'requires_connection';
377
+ credentialStatus?: 'managed' | 'connected' | 'requires_connection' | 'deprecated';
368
378
  /** True when the tool requires a customer-provided credential to run. */
369
379
  requiresOwnCredential?: boolean;
370
380
  /** Actionable message shown when a connection is required. */
package/dist/index.d.ts CHANGED
@@ -355,16 +355,26 @@ interface ToolDefinition {
355
355
  }>;
356
356
  /**
357
357
  * Whether this tool is callable in the current workspace. `false` for a
358
- * bring-your-own-credential provider (e.g. Apollo) that has not been
359
- * connected — the agent should offer to connect it rather than call it.
358
+ * bring-your-own-credential provider that has not been connected.
360
359
  */
361
360
  connected?: boolean;
361
+ /** Whether the tool can be executed. Exact lookup may return non-callable deprecated aliases. */
362
+ callable?: boolean;
363
+ /** True when callers should migrate this exact tool id to its replacement. */
364
+ deprecated?: boolean;
365
+ /** Deprecation reason, replacement, and compatibility execution behavior. */
366
+ deprecation?: {
367
+ replacementToolId: string;
368
+ message: string;
369
+ execution?: 'terminal' | 'forward';
370
+ };
362
371
  /**
363
372
  * Connection status for discovery: `managed` (Deepline-run credentials),
364
373
  * `connected` (your own credential is connected), or `requires_connection`
365
- * (BYO provider not yet connected in this workspace).
374
+ * (BYO provider not yet connected in this workspace). `deprecated` means
375
+ * connecting credentials will not make the tool callable.
366
376
  */
367
- credentialStatus?: 'managed' | 'connected' | 'requires_connection';
377
+ credentialStatus?: 'managed' | 'connected' | 'requires_connection' | 'deprecated';
368
378
  /** True when the tool requires a customer-provided credential to run. */
369
379
  requiresOwnCredential?: boolean;
370
380
  /** Actionable message shown when a connection is required. */
package/dist/index.js CHANGED
@@ -760,7 +760,7 @@ var SDK_RELEASE = {
760
760
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
761
761
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
762
762
  // Operators use the checkout-local deepline-admin binary instead.
763
- version: "0.1.317",
763
+ version: "0.1.319",
764
764
  contracts: {
765
765
  api: {
766
766
  name: "sdk-http-api",
@@ -3250,6 +3250,9 @@ function stagedUploadIdentity(logicalPath, contentHash) {
3250
3250
  function isStagedUploadMintUnsupported(error) {
3251
3251
  return error instanceof DeeplineError && error.statusCode === 404;
3252
3252
  }
3253
+ function isStagedUploadDirectEgressDenied(error) {
3254
+ return error instanceof DeeplineError && error.code === "STAGED_FILE_UPLOAD_EGRESS_DENIED";
3255
+ }
3253
3256
  function formatMegabytes(bytes) {
3254
3257
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
3255
3258
  }
@@ -3270,10 +3273,11 @@ async function uploadStagedFileToPresignedUrl(input) {
3270
3273
  return;
3271
3274
  }
3272
3275
  const text = await response.text().catch(() => "");
3276
+ const egressDenied = response.status === 403 && response.headers.get("x-deny-reason")?.trim().toLowerCase() === "host_not_allowed";
3273
3277
  lastError = new DeeplineError(
3274
3278
  `Direct storage upload of ${input.logicalPath} failed: ${response.status} ${text.slice(0, 200)}`,
3275
3279
  response.status,
3276
- "STAGED_FILE_UPLOAD_FAILED",
3280
+ egressDenied ? "STAGED_FILE_UPLOAD_EGRESS_DENIED" : "STAGED_FILE_UPLOAD_FAILED",
3277
3281
  { logicalPath: input.logicalPath }
3278
3282
  );
3279
3283
  if (response.status < 500 && response.status !== 429) {
@@ -4173,7 +4177,7 @@ var DeeplineClient = class {
4173
4177
  upload
4174
4178
  );
4175
4179
  }
4176
- await Promise.all(
4180
+ const directResults = await Promise.all(
4177
4181
  files.map(async (file) => {
4178
4182
  const upload = uploadByIdentity.get(
4179
4183
  stagedUploadIdentity(file.logicalPath, file.contentHash)
@@ -4186,31 +4190,45 @@ var DeeplineClient = class {
4186
4190
  );
4187
4191
  }
4188
4192
  if (upload.alreadyStaged || !upload.uploadUrl) {
4189
- return;
4193
+ return { ref: upload.ref };
4194
+ }
4195
+ try {
4196
+ await uploadStagedFileToPresignedUrl({
4197
+ url: upload.uploadUrl,
4198
+ headers: upload.uploadHeaders ?? {
4199
+ "content-type": file.contentType
4200
+ },
4201
+ body: decodeBase64Bytes(file.contentBase64),
4202
+ logicalPath: file.logicalPath
4203
+ });
4204
+ return { ref: upload.ref };
4205
+ } catch (error) {
4206
+ if (isStagedUploadDirectEgressDenied(error)) {
4207
+ return { fallbackFile: file };
4208
+ }
4209
+ throw error;
4190
4210
  }
4191
- await uploadStagedFileToPresignedUrl({
4192
- url: upload.uploadUrl,
4193
- headers: upload.uploadHeaders ?? {
4194
- "content-type": file.contentType
4195
- },
4196
- body: decodeBase64Bytes(file.contentBase64),
4197
- logicalPath: file.logicalPath
4198
- });
4199
4211
  })
4200
4212
  );
4201
- return files.map((file) => {
4202
- const upload = uploadByIdentity.get(
4203
- stagedUploadIdentity(file.logicalPath, file.contentHash)
4204
- );
4205
- if (!upload) {
4206
- throw new DeeplineError(
4207
- `The staging server did not return an upload target for ${file.logicalPath}.`,
4208
- void 0,
4209
- "STAGED_FILE_MINT_INCOMPLETE"
4210
- );
4211
- }
4212
- return upload.ref;
4213
- });
4213
+ return await Promise.all(
4214
+ directResults.map(async (result) => {
4215
+ if ("fallbackFile" in result) {
4216
+ const [ref] = await this.stagePlayFilesViaMultipart(
4217
+ [result.fallbackFile],
4218
+ "direct-egress-denied"
4219
+ );
4220
+ if (!ref) {
4221
+ throw new DeeplineError(
4222
+ `The staging server did not return an upload target for ${result.fallbackFile.logicalPath}.`,
4223
+ void 0,
4224
+ "STAGED_FILE_MINT_INCOMPLETE"
4225
+ );
4226
+ }
4227
+ return ref;
4228
+ }
4229
+ return result.ref;
4230
+ })
4231
+ );
4214
4232
  }
4215
4233
  /**
4216
4234
  * Mint short-lived presigned upload targets for staged play files.
@@ -4223,11 +4241,12 @@ var DeeplineClient = class {
4223
4241
  const response = await this.http.post("/api/v2/plays/files/stage/mint", { files });
4224
4242
  return response.uploads ?? [];
4225
4243
  }
4226
- async stagePlayFilesViaMultipart(files) {
4244
+ async stagePlayFilesViaMultipart(files, reason = "mint-unsupported") {
4227
4245
  for (const file of files) {
4228
4246
  if (file.bytes > STAGE_LEGACY_MULTIPART_MAX_BYTES) {
4247
+ const reasonMessage = reason === "direct-egress-denied" ? "direct storage is blocked by this environment's network egress policy" : "the connected Deepline server does not support direct-to-storage uploads";
4229
4248
  throw new DeeplineError(
4230
- `Cannot stage ${file.logicalPath} (${formatMegabytes(file.bytes)}): the connected Deepline server does not support direct-to-storage uploads, and this file exceeds the ~4.5MB request-body limit the legacy upload path is subject to. Update the Deepline server (or target code.deepline.com) to stage files this large.`,
4249
+ `Cannot stage ${file.logicalPath} (${formatMegabytes(file.bytes)}): ${reasonMessage}, and this file exceeds the ~4.5MB request-body limit the legacy upload path is subject to.`,
4231
4250
  413,
4232
4251
  "STAGED_FILE_TOO_LARGE_FOR_LEGACY_UPLOAD",
4233
4252
  {
package/dist/index.mjs CHANGED
@@ -686,7 +686,7 @@ var SDK_RELEASE = {
686
686
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
687
687
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
688
688
  // Operators use the checkout-local deepline-admin binary instead.
689
- version: "0.1.317",
689
+ version: "0.1.319",
690
690
  contracts: {
691
691
  api: {
692
692
  name: "sdk-http-api",
@@ -3176,6 +3176,9 @@ function stagedUploadIdentity(logicalPath, contentHash) {
3176
3176
  function isStagedUploadMintUnsupported(error) {
3177
3177
  return error instanceof DeeplineError && error.statusCode === 404;
3178
3178
  }
3179
+ function isStagedUploadDirectEgressDenied(error) {
3180
+ return error instanceof DeeplineError && error.code === "STAGED_FILE_UPLOAD_EGRESS_DENIED";
3181
+ }
3179
3182
  function formatMegabytes(bytes) {
3180
3183
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
3181
3184
  }
@@ -3196,10 +3199,11 @@ async function uploadStagedFileToPresignedUrl(input) {
3196
3199
  return;
3197
3200
  }
3198
3201
  const text = await response.text().catch(() => "");
3202
+ const egressDenied = response.status === 403 && response.headers.get("x-deny-reason")?.trim().toLowerCase() === "host_not_allowed";
3199
3203
  lastError = new DeeplineError(
3200
3204
  `Direct storage upload of ${input.logicalPath} failed: ${response.status} ${text.slice(0, 200)}`,
3201
3205
  response.status,
3202
- "STAGED_FILE_UPLOAD_FAILED",
3206
+ egressDenied ? "STAGED_FILE_UPLOAD_EGRESS_DENIED" : "STAGED_FILE_UPLOAD_FAILED",
3203
3207
  { logicalPath: input.logicalPath }
3204
3208
  );
3205
3209
  if (response.status < 500 && response.status !== 429) {
@@ -4099,7 +4103,7 @@ var DeeplineClient = class {
4099
4103
  upload
4100
4104
  );
4101
4105
  }
4102
- await Promise.all(
4106
+ const directResults = await Promise.all(
4103
4107
  files.map(async (file) => {
4104
4108
  const upload = uploadByIdentity.get(
4105
4109
  stagedUploadIdentity(file.logicalPath, file.contentHash)
@@ -4112,31 +4116,45 @@ var DeeplineClient = class {
4112
4116
  );
4113
4117
  }
4114
4118
  if (upload.alreadyStaged || !upload.uploadUrl) {
4115
- return;
4119
+ return { ref: upload.ref };
4120
+ }
4121
+ try {
4122
+ await uploadStagedFileToPresignedUrl({
4123
+ url: upload.uploadUrl,
4124
+ headers: upload.uploadHeaders ?? {
4125
+ "content-type": file.contentType
4126
+ },
4127
+ body: decodeBase64Bytes(file.contentBase64),
4128
+ logicalPath: file.logicalPath
4129
+ });
4130
+ return { ref: upload.ref };
4131
+ } catch (error) {
4132
+ if (isStagedUploadDirectEgressDenied(error)) {
4133
+ return { fallbackFile: file };
4134
+ }
4135
+ throw error;
4116
4136
  }
4117
- await uploadStagedFileToPresignedUrl({
4118
- url: upload.uploadUrl,
4119
- headers: upload.uploadHeaders ?? {
4120
- "content-type": file.contentType
4121
- },
4122
- body: decodeBase64Bytes(file.contentBase64),
4123
- logicalPath: file.logicalPath
4124
- });
4125
4137
  })
4126
4138
  );
4127
- return files.map((file) => {
4128
- const upload = uploadByIdentity.get(
4129
- stagedUploadIdentity(file.logicalPath, file.contentHash)
4130
- );
4131
- if (!upload) {
4132
- throw new DeeplineError(
4133
- `The staging server did not return an upload target for ${file.logicalPath}.`,
4134
- void 0,
4135
- "STAGED_FILE_MINT_INCOMPLETE"
4136
- );
4137
- }
4138
- return upload.ref;
4139
- });
4139
+ return await Promise.all(
4140
+ directResults.map(async (result) => {
4141
+ if ("fallbackFile" in result) {
4142
+ const [ref] = await this.stagePlayFilesViaMultipart(
4143
+ [result.fallbackFile],
4144
+ "direct-egress-denied"
4145
+ );
4146
+ if (!ref) {
4147
+ throw new DeeplineError(
4148
+ `The staging server did not return an upload target for ${result.fallbackFile.logicalPath}.`,
4149
+ void 0,
4150
+ "STAGED_FILE_MINT_INCOMPLETE"
4151
+ );
4152
+ }
4153
+ return ref;
4154
+ }
4155
+ return result.ref;
4156
+ })
4157
+ );
4140
4158
  }
4141
4159
  /**
4142
4160
  * Mint short-lived presigned upload targets for staged play files.
@@ -4149,11 +4167,12 @@ var DeeplineClient = class {
4149
4167
  const response = await this.http.post("/api/v2/plays/files/stage/mint", { files });
4150
4168
  return response.uploads ?? [];
4151
4169
  }
4152
- async stagePlayFilesViaMultipart(files) {
4170
+ async stagePlayFilesViaMultipart(files, reason = "mint-unsupported") {
4153
4171
  for (const file of files) {
4154
4172
  if (file.bytes > STAGE_LEGACY_MULTIPART_MAX_BYTES) {
4173
+ const reasonMessage = reason === "direct-egress-denied" ? "direct storage is blocked by this environment's network egress policy" : "the connected Deepline server does not support direct-to-storage uploads";
4155
4174
  throw new DeeplineError(
4156
- `Cannot stage ${file.logicalPath} (${formatMegabytes(file.bytes)}): the connected Deepline server does not support direct-to-storage uploads, and this file exceeds the ~4.5MB request-body limit the legacy upload path is subject to. Update the Deepline server (or target code.deepline.com) to stage files this large.`,
4175
+ `Cannot stage ${file.logicalPath} (${formatMegabytes(file.bytes)}): ${reasonMessage}, and this file exceeds the ~4.5MB request-body limit the legacy upload path is subject to.`,
4157
4176
  413,
4158
4177
  "STAGED_FILE_TOO_LARGE_FOR_LEGACY_UPLOAD",
4159
4178
  {