deepline 0.1.317 → 0.1.318

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.
@@ -1127,6 +1127,13 @@ function isStagedUploadMintUnsupported(error: unknown): boolean {
1127
1127
  return error instanceof DeeplineError && error.statusCode === 404;
1128
1128
  }
1129
1129
 
1130
+ function isStagedUploadDirectEgressDenied(error: unknown): boolean {
1131
+ return (
1132
+ error instanceof DeeplineError &&
1133
+ error.code === 'STAGED_FILE_UPLOAD_EGRESS_DENIED'
1134
+ );
1135
+ }
1136
+
1130
1137
  function formatMegabytes(bytes: number): string {
1131
1138
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1132
1139
  }
@@ -1159,10 +1166,16 @@ async function uploadStagedFileToPresignedUrl(input: {
1159
1166
  return;
1160
1167
  }
1161
1168
  const text = await response.text().catch(() => '');
1169
+ const egressDenied =
1170
+ response.status === 403 &&
1171
+ response.headers.get('x-deny-reason')?.trim().toLowerCase() ===
1172
+ 'host_not_allowed';
1162
1173
  lastError = new DeeplineError(
1163
1174
  `Direct storage upload of ${input.logicalPath} failed: ${response.status} ${text.slice(0, 200)}`,
1164
1175
  response.status,
1165
- 'STAGED_FILE_UPLOAD_FAILED',
1176
+ egressDenied
1177
+ ? 'STAGED_FILE_UPLOAD_EGRESS_DENIED'
1178
+ : 'STAGED_FILE_UPLOAD_FAILED',
1166
1179
  { logicalPath: input.logicalPath },
1167
1180
  );
1168
1181
  // 4xx (other than throttling) will not recover on retry.
@@ -2502,7 +2515,10 @@ export class DeeplineClient {
2502
2515
  );
2503
2516
  }
2504
2517
 
2505
- await Promise.all(
2518
+ type DirectStageResult =
2519
+ | { ref: PlayStagedFileRef }
2520
+ | { fallbackFile: (typeof files)[number] };
2521
+ const directResults: DirectStageResult[] = await Promise.all(
2506
2522
  files.map(async (file) => {
2507
2523
  const upload = uploadByIdentity.get(
2508
2524
  stagedUploadIdentity(file.logicalPath, file.contentHash),
@@ -2515,32 +2531,46 @@ export class DeeplineClient {
2515
2531
  );
2516
2532
  }
2517
2533
  if (upload.alreadyStaged || !upload.uploadUrl) {
2518
- return;
2534
+ return { ref: upload.ref };
2535
+ }
2536
+ try {
2537
+ await uploadStagedFileToPresignedUrl({
2538
+ url: upload.uploadUrl,
2539
+ headers: upload.uploadHeaders ?? {
2540
+ 'content-type': file.contentType,
2541
+ },
2542
+ body: decodeBase64Bytes(file.contentBase64),
2543
+ logicalPath: file.logicalPath,
2544
+ });
2545
+ return { ref: upload.ref };
2546
+ } catch (error) {
2547
+ if (isStagedUploadDirectEgressDenied(error)) {
2548
+ return { fallbackFile: file };
2549
+ }
2550
+ throw error;
2519
2551
  }
2520
- await uploadStagedFileToPresignedUrl({
2521
- url: upload.uploadUrl,
2522
- headers: upload.uploadHeaders ?? {
2523
- 'content-type': file.contentType,
2524
- },
2525
- body: decodeBase64Bytes(file.contentBase64),
2526
- logicalPath: file.logicalPath,
2527
- });
2528
2552
  }),
2529
2553
  );
2530
2554
 
2531
- return files.map((file) => {
2532
- const upload = uploadByIdentity.get(
2533
- stagedUploadIdentity(file.logicalPath, file.contentHash),
2534
- );
2535
- if (!upload) {
2536
- throw new DeeplineError(
2537
- `The staging server did not return an upload target for ${file.logicalPath}.`,
2538
- undefined,
2539
- 'STAGED_FILE_MINT_INCOMPLETE',
2540
- );
2541
- }
2542
- return upload.ref;
2543
- });
2555
+ return await Promise.all(
2556
+ directResults.map(async (result) => {
2557
+ if ('fallbackFile' in result) {
2558
+ const [ref] = await this.stagePlayFilesViaMultipart(
2559
+ [result.fallbackFile],
2560
+ 'direct-egress-denied',
2561
+ );
2562
+ if (!ref) {
2563
+ throw new DeeplineError(
2564
+ `The staging server did not return an upload target for ${result.fallbackFile.logicalPath}.`,
2565
+ undefined,
2566
+ 'STAGED_FILE_MINT_INCOMPLETE',
2567
+ );
2568
+ }
2569
+ return ref;
2570
+ }
2571
+ return result.ref;
2572
+ }),
2573
+ );
2544
2574
  }
2545
2575
 
2546
2576
  /**
@@ -2572,11 +2602,16 @@ export class DeeplineClient {
2572
2602
  contentType: string;
2573
2603
  bytes: number;
2574
2604
  }>,
2605
+ reason: 'mint-unsupported' | 'direct-egress-denied' = 'mint-unsupported',
2575
2606
  ): Promise<PlayStagedFileRef[]> {
2576
2607
  for (const file of files) {
2577
2608
  if (file.bytes > STAGE_LEGACY_MULTIPART_MAX_BYTES) {
2609
+ const reasonMessage =
2610
+ reason === 'direct-egress-denied'
2611
+ ? "direct storage is blocked by this environment's network egress policy"
2612
+ : 'the connected Deepline server does not support direct-to-storage uploads';
2578
2613
  throw new DeeplineError(
2579
- `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.`,
2614
+ `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.`,
2580
2615
  413,
2581
2616
  'STAGED_FILE_TOO_LARGE_FOR_LEGACY_UPLOAD',
2582
2617
  {
@@ -157,7 +157,7 @@ export const SDK_RELEASE = {
157
157
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
158
158
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
159
159
  // Operators use the checkout-local deepline-admin binary instead.
160
- version: '0.1.317',
160
+ version: '0.1.318',
161
161
  contracts: {
162
162
  api: {
163
163
  name: 'sdk-http-api',
package/dist/cli/index.js CHANGED
@@ -1037,7 +1037,7 @@ var SDK_RELEASE = {
1037
1037
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
1038
1038
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
1039
1039
  // Operators use the checkout-local deepline-admin binary instead.
1040
- version: "0.1.317",
1040
+ version: "0.1.318",
1041
1041
  contracts: {
1042
1042
  api: {
1043
1043
  name: "sdk-http-api",
@@ -3527,6 +3527,9 @@ function stagedUploadIdentity(logicalPath, contentHash) {
3527
3527
  function isStagedUploadMintUnsupported(error) {
3528
3528
  return error instanceof DeeplineError && error.statusCode === 404;
3529
3529
  }
3530
+ function isStagedUploadDirectEgressDenied(error) {
3531
+ return error instanceof DeeplineError && error.code === "STAGED_FILE_UPLOAD_EGRESS_DENIED";
3532
+ }
3530
3533
  function formatMegabytes(bytes) {
3531
3534
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
3532
3535
  }
@@ -3547,10 +3550,11 @@ async function uploadStagedFileToPresignedUrl(input2) {
3547
3550
  return;
3548
3551
  }
3549
3552
  const text = await response.text().catch(() => "");
3553
+ const egressDenied = response.status === 403 && response.headers.get("x-deny-reason")?.trim().toLowerCase() === "host_not_allowed";
3550
3554
  lastError = new DeeplineError(
3551
3555
  `Direct storage upload of ${input2.logicalPath} failed: ${response.status} ${text.slice(0, 200)}`,
3552
3556
  response.status,
3553
- "STAGED_FILE_UPLOAD_FAILED",
3557
+ egressDenied ? "STAGED_FILE_UPLOAD_EGRESS_DENIED" : "STAGED_FILE_UPLOAD_FAILED",
3554
3558
  { logicalPath: input2.logicalPath }
3555
3559
  );
3556
3560
  if (response.status < 500 && response.status !== 429) {
@@ -4450,7 +4454,7 @@ var DeeplineClient = class {
4450
4454
  upload
4451
4455
  );
4452
4456
  }
4453
- await Promise.all(
4457
+ const directResults = await Promise.all(
4454
4458
  files.map(async (file) => {
4455
4459
  const upload = uploadByIdentity.get(
4456
4460
  stagedUploadIdentity(file.logicalPath, file.contentHash)
@@ -4463,31 +4467,45 @@ var DeeplineClient = class {
4463
4467
  );
4464
4468
  }
4465
4469
  if (upload.alreadyStaged || !upload.uploadUrl) {
4466
- return;
4470
+ return { ref: upload.ref };
4471
+ }
4472
+ try {
4473
+ await uploadStagedFileToPresignedUrl({
4474
+ url: upload.uploadUrl,
4475
+ headers: upload.uploadHeaders ?? {
4476
+ "content-type": file.contentType
4477
+ },
4478
+ body: decodeBase64Bytes(file.contentBase64),
4479
+ logicalPath: file.logicalPath
4480
+ });
4481
+ return { ref: upload.ref };
4482
+ } catch (error) {
4483
+ if (isStagedUploadDirectEgressDenied(error)) {
4484
+ return { fallbackFile: file };
4485
+ }
4486
+ throw error;
4467
4487
  }
4468
- await uploadStagedFileToPresignedUrl({
4469
- url: upload.uploadUrl,
4470
- headers: upload.uploadHeaders ?? {
4471
- "content-type": file.contentType
4472
- },
4473
- body: decodeBase64Bytes(file.contentBase64),
4474
- logicalPath: file.logicalPath
4475
- });
4476
4488
  })
4477
4489
  );
4478
- return files.map((file) => {
4479
- const upload = uploadByIdentity.get(
4480
- stagedUploadIdentity(file.logicalPath, file.contentHash)
4481
- );
4482
- if (!upload) {
4483
- throw new DeeplineError(
4484
- `The staging server did not return an upload target for ${file.logicalPath}.`,
4485
- void 0,
4486
- "STAGED_FILE_MINT_INCOMPLETE"
4487
- );
4488
- }
4489
- return upload.ref;
4490
- });
4490
+ return await Promise.all(
4491
+ directResults.map(async (result) => {
4492
+ if ("fallbackFile" in result) {
4493
+ const [ref] = await this.stagePlayFilesViaMultipart(
4494
+ [result.fallbackFile],
4495
+ "direct-egress-denied"
4496
+ );
4497
+ if (!ref) {
4498
+ throw new DeeplineError(
4499
+ `The staging server did not return an upload target for ${result.fallbackFile.logicalPath}.`,
4500
+ void 0,
4501
+ "STAGED_FILE_MINT_INCOMPLETE"
4502
+ );
4503
+ }
4504
+ return ref;
4505
+ }
4506
+ return result.ref;
4507
+ })
4508
+ );
4491
4509
  }
4492
4510
  /**
4493
4511
  * Mint short-lived presigned upload targets for staged play files.
@@ -4500,11 +4518,12 @@ var DeeplineClient = class {
4500
4518
  const response = await this.http.post("/api/v2/plays/files/stage/mint", { files });
4501
4519
  return response.uploads ?? [];
4502
4520
  }
4503
- async stagePlayFilesViaMultipart(files) {
4521
+ async stagePlayFilesViaMultipart(files, reason = "mint-unsupported") {
4504
4522
  for (const file of files) {
4505
4523
  if (file.bytes > STAGE_LEGACY_MULTIPART_MAX_BYTES) {
4524
+ 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";
4506
4525
  throw new DeeplineError(
4507
- `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.`,
4526
+ `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.`,
4508
4527
  413,
4509
4528
  "STAGED_FILE_TOO_LARGE_FOR_LEGACY_UPLOAD",
4510
4529
  {
@@ -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.318",
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
  {
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.318",
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.318",
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
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.1.317",
3
+ "version": "0.1.318",
4
4
  "description": "Deepline SDK + CLI — B2B data enrichment powered by durable cloud execution",
5
5
  "license": "MIT",
6
6
  "repository": {