deepline 0.1.299 → 0.1.301

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.
@@ -54,8 +54,8 @@ interface RequestOptions {
54
54
  * Treat HTTP 403 as a regular API error instead of a generic {@link AuthError}.
55
55
  * Use for endpoints that return 403 with a meaningful server error message
56
56
  * (e.g. feature-flagged-off billing subscription checkout) so the server
57
- * message is preserved and surfaced loudly. HTTP 401 still maps to
58
- * {@link AuthError}.
57
+ * message is preserved and surfaced loudly. A provider-originated HTTP 401
58
+ * remains a regular API error when its response envelope says so.
59
59
  */
60
60
  forbiddenAsApiError?: boolean;
61
61
  /**
@@ -311,7 +311,7 @@ export class HttpClient {
311
311
  * @param path - API path (e.g. `"/api/v2/tools"`)
312
312
  * @param options - HTTP method, body, headers, and timeout
313
313
  * @returns Parsed JSON response body
314
- * @throws {@link AuthError} on HTTP 401/403 (immediate, no retry)
314
+ * @throws {@link AuthError} on Deepline-auth HTTP 401/403 (immediate, no retry)
315
315
  * @throws {@link RateLimitError} on HTTP 429 after all retries exhausted
316
316
  * @throws {@link DeeplineError} on other API errors or connection failures
317
317
  */
@@ -374,13 +374,6 @@ export class HttpClient {
374
374
 
375
375
  clearTimeout(timeoutId);
376
376
 
377
- if (
378
- response.status === 401 ||
379
- (response.status === 403 && !options?.forbiddenAsApiError)
380
- ) {
381
- throw new AuthError();
382
- }
383
-
384
377
  if (response.status === 429) {
385
378
  const retryAfter = parseRetryAfter(response);
386
379
  lastError = new RateLimitError(retryAfter);
@@ -392,11 +385,14 @@ export class HttpClient {
392
385
  }
393
386
 
394
387
  const body = await response.text();
395
- let parsed: unknown;
396
- try {
397
- parsed = JSON.parse(body);
398
- } catch {
399
- parsed = body;
388
+ const parsed = parseResponseBody(body);
389
+
390
+ if (
391
+ (response.status === 401 &&
392
+ !isProviderOriginatedHttpError(parsed)) ||
393
+ (response.status === 403 && !options?.forbiddenAsApiError)
394
+ ) {
395
+ throw new AuthError();
400
396
  }
401
397
 
402
398
  if (!response.ok) {
@@ -451,16 +447,7 @@ export class HttpClient {
451
447
  'string'
452
448
  ? (parsed as Record<string, string>).message
453
449
  : `HTTP ${response.status}`;
454
- const apiErrorCode =
455
- errorValue &&
456
- typeof errorValue === 'object' &&
457
- typeof (errorValue as Record<string, unknown>).code === 'string'
458
- ? (errorValue as Record<string, string>).code
459
- : typeof parsed === 'object' &&
460
- parsed &&
461
- typeof (parsed as Record<string, unknown>).code === 'string'
462
- ? (parsed as Record<string, string>).code
463
- : 'API_ERROR';
450
+ const apiErrorCode = apiErrorCodeFromResponse(parsed);
464
451
  lastError = new DeeplineError(msg, response.status, apiErrorCode, {
465
452
  response: parsed,
466
453
  });
@@ -547,11 +534,16 @@ export class HttpClient {
547
534
  signal: options?.signal,
548
535
  });
549
536
 
550
- if (response.status === 401 || response.status === 403) {
551
- throw new AuthError();
552
- }
553
537
  if (!response.ok) {
554
538
  const body = await response.text();
539
+ const parsed = parseResponseBody(body);
540
+ if (
541
+ (response.status === 401 &&
542
+ !isProviderOriginatedHttpError(parsed)) ||
543
+ response.status === 403
544
+ ) {
545
+ throw new AuthError();
546
+ }
555
547
  const htmlError = detectHtmlErrorBody(
556
548
  body,
557
549
  response.headers.get('content-type'),
@@ -570,11 +562,10 @@ export class HttpClient {
570
562
  },
571
563
  );
572
564
  }
573
- const parsed = parseResponseBody(body);
574
565
  throw new DeeplineError(
575
566
  apiErrorMessage(parsed, response.status),
576
567
  response.status,
577
- 'API_ERROR',
568
+ apiErrorCodeFromResponse(parsed),
578
569
  { response: parsed },
579
570
  );
580
571
  }
@@ -679,6 +670,33 @@ function parseResponseBody(body: string): unknown {
679
670
  }
680
671
  }
681
672
 
673
+ /**
674
+ * A provider can reject its own credential with HTTP 401 while the caller's
675
+ * Deepline API key remains valid. The API envelope is authoritative in that
676
+ * case, so preserve it as a regular DeeplineError rather than prompting the
677
+ * caller to replace DEEPLINE_API_KEY.
678
+ */
679
+ function isProviderOriginatedHttpError(parsed: unknown): boolean {
680
+ const response = asRecord(parsed);
681
+ const error = asRecord(response?.error);
682
+ const failureOrigin = error?.failure_origin ?? response?.failure_origin;
683
+ const code = error?.code ?? response?.code;
684
+ return failureOrigin === 'provider' || code === 'UPSTREAM_BLOCKED';
685
+ }
686
+
687
+ function apiErrorCodeFromResponse(parsed: unknown): string {
688
+ const response = asRecord(parsed);
689
+ const error = asRecord(response?.error);
690
+ const code = error?.code ?? response?.code;
691
+ return typeof code === 'string' ? code : 'API_ERROR';
692
+ }
693
+
694
+ function asRecord(value: unknown): Record<string, unknown> | null {
695
+ return typeof value === 'object' && value !== null
696
+ ? (value as Record<string, unknown>)
697
+ : null;
698
+ }
699
+
682
700
  function isRetryableApiErrorResponse(status: number): boolean {
683
701
  return status === 408 || status === 425 || (status >= 500 && status < 600);
684
702
  }
@@ -155,7 +155,7 @@ export const SDK_RELEASE = {
155
155
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
156
156
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
157
157
  // Operators use the checkout-local deepline-admin binary instead.
158
- version: '0.1.299',
158
+ version: '0.1.301',
159
159
  contracts: {
160
160
  api: {
161
161
  name: 'sdk-http-api',
@@ -124,6 +124,7 @@ export class RuntimeReceiptStoreAdapter {
124
124
  maxBatchBytes?: number;
125
125
  maxFlushMs?: number;
126
126
  maxBufferedBytes?: number;
127
+ onRetryTelemetry?: (line: string) => void;
127
128
  }) {
128
129
  const context: WorkerRuntimeApiContext = {
129
130
  ...options.context,
@@ -137,6 +138,14 @@ export class RuntimeReceiptStoreAdapter {
137
138
  maxFlushMs: options.maxFlushMs,
138
139
  maxBufferedBytes: options.maxBufferedBytes,
139
140
  classifyRetryableError: classifyReceiptRetry,
141
+ onRetryEvent: (event) =>
142
+ emitReceiptWriterRetryTelemetry({
143
+ event,
144
+ playName: options.playName,
145
+ runId: options.runId,
146
+ gatewayHost: runtimeGatewayHost(context.baseUrl),
147
+ onTelemetry: options.onRetryTelemetry,
148
+ }),
140
149
  send: async (commands, { signal }) =>
141
150
  await sendReceiptCommands({
142
151
  context: { ...context, signal },
@@ -260,6 +269,100 @@ export class RuntimeReceiptStoreAdapter {
260
269
  }
261
270
  }
262
271
 
272
+ const RECEIPT_WRITER_RETRY_TELEMETRY_TAG =
273
+ '[perf][worker.receipt_writer.retry]';
274
+
275
+ function isPowerOfTwo(value: number): boolean {
276
+ return value > 0 && (value & (value - 1)) === 0;
277
+ }
278
+
279
+ function runtimeGatewayHost(baseUrl: string): string | null {
280
+ try {
281
+ return new URL(baseUrl).host || null;
282
+ } catch {
283
+ return null;
284
+ }
285
+ }
286
+
287
+ function receiptCommandTelemetryAction(command: ReceiptCommand): string {
288
+ return command.kind === 'settle'
289
+ ? `${command.kind}_${command.outcome}`
290
+ : command.kind;
291
+ }
292
+
293
+ function receiptRetryErrorTelemetry(error: unknown): Record<string, unknown> {
294
+ if (isAppRuntimeApiCapacityError(error)) {
295
+ return {
296
+ failureKind: 'capacity',
297
+ errorName: error.name,
298
+ status: error.status,
299
+ code: error.code,
300
+ requestId: error.requestId,
301
+ };
302
+ }
303
+ if (error instanceof AppRuntimeApiTransportError) {
304
+ return {
305
+ failureKind: 'transport',
306
+ errorName: error.name,
307
+ transportAttempts: error.attempts,
308
+ transportAction: error.action,
309
+ };
310
+ }
311
+ if (error instanceof AppRuntimeApiResponseError) {
312
+ return {
313
+ failureKind: 'http',
314
+ errorName: error.name,
315
+ status: error.status,
316
+ code: error.code,
317
+ requestId: error.requestId,
318
+ };
319
+ }
320
+ return {
321
+ failureKind: 'unknown',
322
+ errorName: error instanceof Error ? error.name : typeof error,
323
+ };
324
+ }
325
+
326
+ function emitReceiptWriterRetryTelemetry(input: {
327
+ event: import('./runtime-receipt-writer').RuntimeReceiptWriterRetryEvent<ReceiptCommand>;
328
+ playName: string;
329
+ runId: string;
330
+ gatewayHost: string | null;
331
+ onTelemetry?: (line: string) => void;
332
+ }): void {
333
+ const { event } = input;
334
+ if (event.phase === 'retry' && !isPowerOfTwo(event.attempt)) return;
335
+ const payload = {
336
+ phase: event.phase,
337
+ playName: input.playName,
338
+ runId: input.runId,
339
+ gatewayHost: input.gatewayHost,
340
+ action: receiptCommandTelemetryAction(event.firstInput),
341
+ batchId: event.batchId,
342
+ batchSize: event.batchSize,
343
+ attempt: event.attempt,
344
+ elapsedMs: event.elapsedMs,
345
+ retryAfterMs: event.retryAfterMs,
346
+ queued: event.queued,
347
+ blocked: event.blocked,
348
+ bufferedBytes: event.bufferedBytes,
349
+ ...(event.error === undefined
350
+ ? { failureKind: null }
351
+ : receiptRetryErrorTelemetry(event.error)),
352
+ };
353
+ try {
354
+ const serialized = JSON.stringify(payload);
355
+ input.onTelemetry?.(`${RECEIPT_WRITER_RETRY_TELEMETRY_TAG} ${serialized}`);
356
+ if (event.phase === 'retry') {
357
+ console.warn(RECEIPT_WRITER_RETRY_TELEMETRY_TAG, serialized);
358
+ } else {
359
+ console.info(RECEIPT_WRITER_RETRY_TELEMETRY_TAG, serialized);
360
+ }
361
+ } catch {
362
+ // Receipt telemetry must never affect durable delivery.
363
+ }
364
+ }
365
+
263
366
  function receiptCommandBatchKey(command: ReceiptCommand): string {
264
367
  switch (command.kind) {
265
368
  case 'get':
@@ -22,6 +22,20 @@ export type RuntimeReceiptWriterStats = {
22
22
  bufferedBytes: number;
23
23
  };
24
24
 
25
+ export type RuntimeReceiptWriterRetryEvent<Input> = {
26
+ phase: 'retry' | 'recovered';
27
+ attempt: number;
28
+ batchId: number;
29
+ batchSize: number;
30
+ firstInput: Input;
31
+ elapsedMs: number;
32
+ retryAfterMs: number;
33
+ queued: number;
34
+ blocked: number;
35
+ bufferedBytes: number;
36
+ error?: unknown;
37
+ };
38
+
25
39
  export class RuntimeReceiptWriterBufferLimitError extends Error {
26
40
  constructor(
27
41
  readonly inputBytes: number,
@@ -76,11 +90,15 @@ export class RuntimeReceiptWriter<Input, Output> {
76
90
  readonly #maxBatchBytes: number;
77
91
  readonly #maxBufferedBytes: number;
78
92
  readonly #maxFlushMs: number;
93
+ readonly #onRetryEvent:
94
+ | ((event: RuntimeReceiptWriterRetryEvent<Input>) => void)
95
+ | null;
79
96
  readonly #queued: Array<PendingWrite<Input, Output>> = [];
80
97
  readonly #blocked: Array<PendingWrite<Input, Output>> = [];
81
98
  readonly #closeController = new AbortController();
82
99
 
83
100
  #bufferedBytes = 0;
101
+ #nextBatchId = 0;
84
102
  #activeBatch: Array<PendingWrite<Input, Output>> | null = null;
85
103
  #pump: Promise<void> | null = null;
86
104
  #flushTimer: ReturnType<typeof setTimeout> | null = null;
@@ -103,6 +121,7 @@ export class RuntimeReceiptWriter<Input, Output> {
103
121
  maxBatchBytes?: number;
104
122
  maxBufferedBytes?: number;
105
123
  maxFlushMs?: number;
124
+ onRetryEvent?: (event: RuntimeReceiptWriterRetryEvent<Input>) => void;
106
125
  }) {
107
126
  this.#batchKey = options.batchKey;
108
127
  this.#send = options.send;
@@ -119,6 +138,7 @@ export class RuntimeReceiptWriter<Input, Output> {
119
138
  this.#maxBufferedBytes,
120
139
  );
121
140
  this.#maxFlushMs = Math.max(0, Math.floor(options.maxFlushMs ?? 5));
141
+ this.#onRetryEvent = options.onRetryEvent ?? null;
122
142
  }
123
143
 
124
144
  write(input: Input, options: { signal?: AbortSignal } = {}): Promise<Output> {
@@ -243,9 +263,10 @@ export class RuntimeReceiptWriter<Input, Output> {
243
263
  while (this.#queued.length > 0) {
244
264
  const batch = this.#takeBatch();
245
265
  if (batch.length === 0) continue;
266
+ const batchId = ++this.#nextBatchId;
246
267
  this.#activeBatch = batch;
247
268
  try {
248
- const outputs = await this.#sendWithRetry(batch);
269
+ const outputs = await this.#sendWithRetry(batch, batchId);
249
270
  if (outputs.length !== batch.length) {
250
271
  throw new RuntimeReceiptWriterResultCountError(
251
272
  batch.length,
@@ -305,9 +326,13 @@ export class RuntimeReceiptWriter<Input, Output> {
305
326
 
306
327
  async #sendWithRetry(
307
328
  batch: readonly PendingWrite<Input, Output>[],
329
+ batchId: number,
308
330
  ): Promise<readonly Output[]> {
309
331
  const inputs = batch.map((pending) => pending.input);
332
+ const startedAt = Date.now();
333
+ let attempt = 0;
310
334
  while (true) {
335
+ attempt += 1;
311
336
  if (this.#closeController.signal.aborted) {
312
337
  throw this.#closeReason;
313
338
  }
@@ -315,20 +340,68 @@ export class RuntimeReceiptWriter<Input, Output> {
315
340
  throw batch[0]?.aborted;
316
341
  }
317
342
  try {
318
- return await this.#send(inputs, {
343
+ const outputs = await this.#send(inputs, {
319
344
  signal: this.#closeController.signal,
320
345
  });
346
+ if (attempt > 1) {
347
+ this.#emitRetryEvent({
348
+ phase: 'recovered',
349
+ attempt,
350
+ batchId,
351
+ batch,
352
+ startedAt,
353
+ retryAfterMs: 0,
354
+ });
355
+ }
356
+ return outputs;
321
357
  } catch (error) {
322
358
  const retry = this.#classifyRetryableError(error);
323
359
  if (!retry) throw error;
324
- await this.#waitForRetry(
325
- jitteredRetryDelayMs(retry.retryAfterMs),
360
+ const retryAfterMs = jitteredRetryDelayMs(retry.retryAfterMs);
361
+ this.#emitRetryEvent({
362
+ phase: 'retry',
363
+ attempt,
364
+ batchId,
326
365
  batch,
327
- );
366
+ startedAt,
367
+ retryAfterMs,
368
+ error,
369
+ });
370
+ await this.#waitForRetry(retryAfterMs, batch);
328
371
  }
329
372
  }
330
373
  }
331
374
 
375
+ #emitRetryEvent(input: {
376
+ phase: 'retry' | 'recovered';
377
+ attempt: number;
378
+ batchId: number;
379
+ batch: readonly PendingWrite<Input, Output>[];
380
+ startedAt: number;
381
+ retryAfterMs: number;
382
+ error?: unknown;
383
+ }): void {
384
+ const firstInput = input.batch[0]?.input;
385
+ if (!this.#onRetryEvent || firstInput === undefined) return;
386
+ try {
387
+ this.#onRetryEvent({
388
+ phase: input.phase,
389
+ attempt: input.attempt,
390
+ batchId: input.batchId,
391
+ batchSize: input.batch.length,
392
+ firstInput,
393
+ elapsedMs: Date.now() - input.startedAt,
394
+ retryAfterMs: input.retryAfterMs,
395
+ queued: this.#queued.length,
396
+ blocked: this.#blocked.length,
397
+ bufferedBytes: this.#bufferedBytes,
398
+ ...(input.error === undefined ? {} : { error: input.error }),
399
+ });
400
+ } catch {
401
+ // Receipt telemetry must never affect durable delivery.
402
+ }
403
+ }
404
+
332
405
  async #waitForRetry(
333
406
  delayMs: number,
334
407
  batch: readonly PendingWrite<Input, Output>[],
package/dist/cli/index.js CHANGED
@@ -718,7 +718,7 @@ var SDK_RELEASE = {
718
718
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
719
719
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
720
720
  // Operators use the checkout-local deepline-admin binary instead.
721
- version: "0.1.299",
721
+ version: "0.1.301",
722
722
  contracts: {
723
723
  api: {
724
724
  name: "sdk-http-api",
@@ -1063,7 +1063,7 @@ var HttpClient = class {
1063
1063
  * @param path - API path (e.g. `"/api/v2/tools"`)
1064
1064
  * @param options - HTTP method, body, headers, and timeout
1065
1065
  * @returns Parsed JSON response body
1066
- * @throws {@link AuthError} on HTTP 401/403 (immediate, no retry)
1066
+ * @throws {@link AuthError} on Deepline-auth HTTP 401/403 (immediate, no retry)
1067
1067
  * @throws {@link RateLimitError} on HTTP 429 after all retries exhausted
1068
1068
  * @throws {@link DeeplineError} on other API errors or connection failures
1069
1069
  */
@@ -1102,9 +1102,6 @@ var HttpClient = class {
1102
1102
  signal: controller.signal
1103
1103
  });
1104
1104
  clearTimeout(timeoutId);
1105
- if (response.status === 401 || response.status === 403 && !options?.forbiddenAsApiError) {
1106
- throw new AuthError();
1107
- }
1108
1105
  if (response.status === 429) {
1109
1106
  const retryAfter = parseRetryAfter(response);
1110
1107
  lastError = new RateLimitError(retryAfter);
@@ -1115,11 +1112,9 @@ var HttpClient = class {
1115
1112
  throw lastError;
1116
1113
  }
1117
1114
  const body = await response.text();
1118
- let parsed;
1119
- try {
1120
- parsed = JSON.parse(body);
1121
- } catch {
1122
- parsed = body;
1115
+ const parsed = parseResponseBody(body);
1116
+ if (response.status === 401 && !isProviderOriginatedHttpError(parsed) || response.status === 403 && !options?.forbiddenAsApiError) {
1117
+ throw new AuthError();
1123
1118
  }
1124
1119
  if (!response.ok) {
1125
1120
  const retryableApiError = options?.retryApiErrors === true && isRetryableApiErrorResponse(response.status);
@@ -1146,7 +1141,7 @@ var HttpClient = class {
1146
1141
  }
1147
1142
  const errorValue = typeof parsed === "object" && parsed && "error" in parsed ? parsed.error : void 0;
1148
1143
  const msg = typeof errorValue === "string" ? errorValue : errorValue && typeof errorValue === "object" && "message" in errorValue && typeof errorValue.message === "string" ? errorValue.message : typeof parsed === "object" && parsed && "message" in parsed && typeof parsed.message === "string" ? parsed.message : `HTTP ${response.status}`;
1149
- const apiErrorCode2 = errorValue && typeof errorValue === "object" && typeof errorValue.code === "string" ? errorValue.code : typeof parsed === "object" && parsed && typeof parsed.code === "string" ? parsed.code : "API_ERROR";
1144
+ const apiErrorCode2 = apiErrorCodeFromResponse(parsed);
1150
1145
  lastError = new DeeplineError(msg, response.status, apiErrorCode2, {
1151
1146
  response: parsed
1152
1147
  });
@@ -1220,11 +1215,12 @@ var HttpClient = class {
1220
1215
  body: options?.body !== void 0 ? JSON.stringify(options.body) : void 0,
1221
1216
  signal: options?.signal
1222
1217
  });
1223
- if (response.status === 401 || response.status === 403) {
1224
- throw new AuthError();
1225
- }
1226
1218
  if (!response.ok) {
1227
1219
  const body = await response.text();
1220
+ const parsed = parseResponseBody(body);
1221
+ if (response.status === 401 && !isProviderOriginatedHttpError(parsed) || response.status === 403) {
1222
+ throw new AuthError();
1223
+ }
1228
1224
  const htmlError = detectHtmlErrorBody(
1229
1225
  body,
1230
1226
  response.headers.get("content-type")
@@ -1241,11 +1237,10 @@ var HttpClient = class {
1241
1237
  }
1242
1238
  );
1243
1239
  }
1244
- const parsed = parseResponseBody(body);
1245
1240
  throw new DeeplineError(
1246
1241
  apiErrorMessage(parsed, response.status),
1247
1242
  response.status,
1248
- "API_ERROR",
1243
+ apiErrorCodeFromResponse(parsed),
1249
1244
  { response: parsed }
1250
1245
  );
1251
1246
  }
@@ -1320,6 +1315,22 @@ function parseResponseBody(body) {
1320
1315
  return body;
1321
1316
  }
1322
1317
  }
1318
+ function isProviderOriginatedHttpError(parsed) {
1319
+ const response = asRecord(parsed);
1320
+ const error = asRecord(response?.error);
1321
+ const failureOrigin = error?.failure_origin ?? response?.failure_origin;
1322
+ const code = error?.code ?? response?.code;
1323
+ return failureOrigin === "provider" || code === "UPSTREAM_BLOCKED";
1324
+ }
1325
+ function apiErrorCodeFromResponse(parsed) {
1326
+ const response = asRecord(parsed);
1327
+ const error = asRecord(response?.error);
1328
+ const code = error?.code ?? response?.code;
1329
+ return typeof code === "string" ? code : "API_ERROR";
1330
+ }
1331
+ function asRecord(value) {
1332
+ return typeof value === "object" && value !== null ? value : null;
1333
+ }
1323
1334
  function isRetryableApiErrorResponse(status) {
1324
1335
  return status === 408 || status === 425 || status >= 500 && status < 600;
1325
1336
  }
@@ -24965,7 +24976,7 @@ function monitorsErrorExitCode(error) {
24965
24976
  }
24966
24977
  function monitorsFailureNextCommand(error, exitCode) {
24967
24978
  if (error instanceof DeeplineError) {
24968
- const response = asRecord(asRecord(error.details)?.response);
24979
+ const response = asRecord2(asRecord2(error.details)?.response);
24969
24980
  const serverNextAction = asString(response?.next_action);
24970
24981
  if (serverNextAction) return serverNextAction;
24971
24982
  }
@@ -24981,10 +24992,10 @@ function monitorsFailureNextCommand(error, exitCode) {
24981
24992
  }
24982
24993
  function readValidationIssues(error) {
24983
24994
  if (!(error instanceof DeeplineError)) return [];
24984
- const response = asRecord(asRecord(error.details)?.response);
24995
+ const response = asRecord2(asRecord2(error.details)?.response);
24985
24996
  const issues = Array.isArray(response?.issues) ? response.issues : [];
24986
24997
  return issues.flatMap((raw) => {
24987
- const issue = asRecord(raw);
24998
+ const issue = asRecord2(raw);
24988
24999
  if (!issue) return [];
24989
25000
  return [
24990
25001
  {
@@ -25077,7 +25088,7 @@ or from a file / stdin:
25077
25088
  cat monitor.json | ${input2.command} --file -`
25078
25089
  );
25079
25090
  }
25080
- function asRecord(value) {
25091
+ function asRecord2(value) {
25081
25092
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
25082
25093
  }
25083
25094
  function asString(value) {
@@ -25087,7 +25098,7 @@ function asFiniteNumber(value) {
25087
25098
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
25088
25099
  }
25089
25100
  function monitorPricingLine(rawPricing) {
25090
- const pricing = asRecord(rawPricing);
25101
+ const pricing = asRecord2(rawPricing);
25091
25102
  if (!pricing) return void 0;
25092
25103
  const display = asString(pricing.display);
25093
25104
  if (display) return display;
@@ -25102,16 +25113,16 @@ function yesNo(value) {
25102
25113
  return "unknown";
25103
25114
  }
25104
25115
  function readDeployOutputContract(payload) {
25105
- const contract = asRecord(payload.output_contract);
25116
+ const contract = asRecord2(payload.output_contract);
25106
25117
  if (!contract) return { streams: [] };
25107
25118
  const rawOutputs = Array.isArray(contract.outputs) ? contract.outputs : [];
25108
25119
  const streams = rawOutputs.flatMap((raw) => {
25109
- const output2 = asRecord(raw);
25120
+ const output2 = asRecord2(raw);
25110
25121
  const stream = output2 ? asString(output2.stream) : void 0;
25111
25122
  const table = output2 ? asString(output2.table) : void 0;
25112
25123
  if (!output2 || !stream || !table) return [];
25113
25124
  const columns = Array.isArray(output2.columns) ? output2.columns.flatMap((rawColumn) => {
25114
- const column = asRecord(rawColumn);
25125
+ const column = asRecord2(rawColumn);
25115
25126
  const name = column ? asString(column.name) : void 0;
25116
25127
  return name ? [name] : [];
25117
25128
  }) : [];
@@ -25128,7 +25139,7 @@ function readDeployOutputContract(payload) {
25128
25139
  return { tool: asString(contract.tool), streams };
25129
25140
  }
25130
25141
  function renderMonitorDeployCompletion(payload) {
25131
- const monitor = asRecord(payload.monitor);
25142
+ const monitor = asRecord2(payload.monitor);
25132
25143
  const key = monitor ? asString(monitor.key) : void 0;
25133
25144
  const { tool, streams } = readDeployOutputContract(payload);
25134
25145
  if (!key || streams.length === 0) return void 0;
@@ -25179,13 +25190,13 @@ function renderMonitorDeployPlan(payload) {
25179
25190
  if (!valid) {
25180
25191
  const issues = Array.isArray(payload.issues) ? payload.issues : [];
25181
25192
  for (const raw of issues) {
25182
- const issue = asRecord(raw);
25193
+ const issue = asRecord2(raw);
25183
25194
  const path = issue ? asString(issue.path) : void 0;
25184
25195
  const message = issue ? asString(issue.message) : void 0;
25185
25196
  if (message) lines.push(` - ${path ? `${path}: ` : ""}${message}`);
25186
25197
  }
25187
25198
  }
25188
- const estimate = asRecord(payload.deploy_cost_estimate);
25199
+ const estimate = asRecord2(payload.deploy_cost_estimate);
25189
25200
  const credits = estimate ? asFiniteNumber(estimate.credits) : void 0;
25190
25201
  if (credits !== void 0) {
25191
25202
  const note = estimate ? asString(estimate.note) : void 0;
@@ -25203,7 +25214,7 @@ function renderMonitorDeployPlan(payload) {
25203
25214
  }
25204
25215
  const candidates = Array.isArray(payload.reuse_candidates) ? payload.reuse_candidates : [];
25205
25216
  const candidateLines = candidates.flatMap((raw) => {
25206
- const candidate = asRecord(raw);
25217
+ const candidate = asRecord2(raw);
25207
25218
  const key = candidate ? asString(candidate.key) : void 0;
25208
25219
  if (!candidate || !key) return [];
25209
25220
  const name = asString(candidate.name);
@@ -25229,7 +25240,7 @@ function renderMonitorDeployPlan(payload) {
25229
25240
  `;
25230
25241
  }
25231
25242
  function renderMonitorDeletePlan(payload, fallbackKey) {
25232
- const plan = asRecord(payload.plan);
25243
+ const plan = asRecord2(payload.plan);
25233
25244
  const key = (plan ? asString(plan.monitor_key) : void 0) ?? fallbackKey;
25234
25245
  const lines = [
25235
25246
  "DRY RUN \u2014 nothing was deleted.",
@@ -25243,7 +25254,7 @@ function renderMonitorDeletePlan(payload, fallbackKey) {
25243
25254
  `;
25244
25255
  }
25245
25256
  function renderMonitorReactivatePlan(payload, key) {
25246
- const estimate = asRecord(payload.cost_estimate);
25257
+ const estimate = asRecord2(payload.cost_estimate);
25247
25258
  const credits = estimate ? asFiniteNumber(estimate.credits) : void 0;
25248
25259
  const lines = [
25249
25260
  "DRY RUN \u2014 nothing was reactivated.",
@@ -25295,7 +25306,7 @@ function renderAvailableToolsText(payload) {
25295
25306
  ];
25296
25307
  let currentProvider;
25297
25308
  for (const raw of tools) {
25298
- const entry = asRecord(raw);
25309
+ const entry = asRecord2(raw);
25299
25310
  const id = entry ? asString(entry.tool) : void 0;
25300
25311
  if (!entry || !id) continue;
25301
25312
  const name = asString(entry.name) ?? asString(entry.display_name);
@@ -25365,7 +25376,7 @@ function renderDeployedListText(payload, requestedStatus) {
25365
25376
  const header = monitors.length === 0 ? "No deployed monitors matched." : total !== void 0 && total > monitors.length ? `Deployed monitors (${monitors.length} of ${total}):` : `Deployed monitors (${monitors.length}):`;
25366
25377
  const lines = [header];
25367
25378
  for (const raw of monitors) {
25368
- const entry = asRecord(raw);
25379
+ const entry = asRecord2(raw);
25369
25380
  const key = entry ? asString(entry.key) ?? asString(entry.monitor_key) : void 0;
25370
25381
  if (!entry || !key) continue;
25371
25382
  const status = asString(entry.status);
@@ -25450,11 +25461,11 @@ function renderMonitorGet(payload) {
25450
25461
  const tool = asString(payload.tool);
25451
25462
  if (!key || !tool) return void 0;
25452
25463
  const status = asString(payload.status);
25453
- const definition = asRecord(payload.definition);
25464
+ const definition = asRecord2(payload.definition);
25454
25465
  const pricingLine = monitorPricingLine(payload.pricing);
25455
- const billing = asRecord(payload.billing);
25466
+ const billing = asRecord2(payload.billing);
25456
25467
  const nextRenewalAt = billing ? asString(billing.next_renewal_at) : void 0;
25457
- const dependents = asRecord(payload.dependents);
25468
+ const dependents = asRecord2(payload.dependents);
25458
25469
  const plays = dependents && Array.isArray(dependents.plays) ? dependents.plays : [];
25459
25470
  const lines = [
25460
25471
  `Monitor: ${key}`,
@@ -25471,7 +25482,7 @@ function renderMonitorGet(payload) {
25471
25482
  lines.push(" none");
25472
25483
  } else {
25473
25484
  for (const raw of plays) {
25474
- const play = asRecord(raw);
25485
+ const play = asRecord2(raw);
25475
25486
  const name = play ? asString(play.name) : void 0;
25476
25487
  if (!play || !name) continue;
25477
25488
  const listener = asString(play.listener_key);
@@ -31216,7 +31227,7 @@ function parseCapturedJson(stdout) {
31216
31227
  return null;
31217
31228
  }
31218
31229
  }
31219
- function asRecord2(value) {
31230
+ function asRecord3(value) {
31220
31231
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
31221
31232
  }
31222
31233
  function safeRead(path) {
@@ -31918,8 +31929,8 @@ async function runSetupCommand(options) {
31918
31929
  status: "complete",
31919
31930
  phases
31920
31931
  });
31921
- const doctorChecks = asRecord2(doctorPayload?.checks);
31922
- const apiCheck = asRecord2(doctorChecks?.api);
31932
+ const doctorChecks = asRecord3(doctorPayload?.checks);
31933
+ const apiCheck = asRecord3(doctorChecks?.api);
31923
31934
  printCommandEnvelope(
31924
31935
  {
31925
31936
  ok: true,
@@ -703,7 +703,7 @@ var SDK_RELEASE = {
703
703
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
704
704
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
705
705
  // Operators use the checkout-local deepline-admin binary instead.
706
- version: "0.1.299",
706
+ version: "0.1.301",
707
707
  contracts: {
708
708
  api: {
709
709
  name: "sdk-http-api",
@@ -1048,7 +1048,7 @@ var HttpClient = class {
1048
1048
  * @param path - API path (e.g. `"/api/v2/tools"`)
1049
1049
  * @param options - HTTP method, body, headers, and timeout
1050
1050
  * @returns Parsed JSON response body
1051
- * @throws {@link AuthError} on HTTP 401/403 (immediate, no retry)
1051
+ * @throws {@link AuthError} on Deepline-auth HTTP 401/403 (immediate, no retry)
1052
1052
  * @throws {@link RateLimitError} on HTTP 429 after all retries exhausted
1053
1053
  * @throws {@link DeeplineError} on other API errors or connection failures
1054
1054
  */
@@ -1087,9 +1087,6 @@ var HttpClient = class {
1087
1087
  signal: controller.signal
1088
1088
  });
1089
1089
  clearTimeout(timeoutId);
1090
- if (response.status === 401 || response.status === 403 && !options?.forbiddenAsApiError) {
1091
- throw new AuthError();
1092
- }
1093
1090
  if (response.status === 429) {
1094
1091
  const retryAfter = parseRetryAfter(response);
1095
1092
  lastError = new RateLimitError(retryAfter);
@@ -1100,11 +1097,9 @@ var HttpClient = class {
1100
1097
  throw lastError;
1101
1098
  }
1102
1099
  const body = await response.text();
1103
- let parsed;
1104
- try {
1105
- parsed = JSON.parse(body);
1106
- } catch {
1107
- parsed = body;
1100
+ const parsed = parseResponseBody(body);
1101
+ if (response.status === 401 && !isProviderOriginatedHttpError(parsed) || response.status === 403 && !options?.forbiddenAsApiError) {
1102
+ throw new AuthError();
1108
1103
  }
1109
1104
  if (!response.ok) {
1110
1105
  const retryableApiError = options?.retryApiErrors === true && isRetryableApiErrorResponse(response.status);
@@ -1131,7 +1126,7 @@ var HttpClient = class {
1131
1126
  }
1132
1127
  const errorValue = typeof parsed === "object" && parsed && "error" in parsed ? parsed.error : void 0;
1133
1128
  const msg = typeof errorValue === "string" ? errorValue : errorValue && typeof errorValue === "object" && "message" in errorValue && typeof errorValue.message === "string" ? errorValue.message : typeof parsed === "object" && parsed && "message" in parsed && typeof parsed.message === "string" ? parsed.message : `HTTP ${response.status}`;
1134
- const apiErrorCode2 = errorValue && typeof errorValue === "object" && typeof errorValue.code === "string" ? errorValue.code : typeof parsed === "object" && parsed && typeof parsed.code === "string" ? parsed.code : "API_ERROR";
1129
+ const apiErrorCode2 = apiErrorCodeFromResponse(parsed);
1135
1130
  lastError = new DeeplineError(msg, response.status, apiErrorCode2, {
1136
1131
  response: parsed
1137
1132
  });
@@ -1205,11 +1200,12 @@ var HttpClient = class {
1205
1200
  body: options?.body !== void 0 ? JSON.stringify(options.body) : void 0,
1206
1201
  signal: options?.signal
1207
1202
  });
1208
- if (response.status === 401 || response.status === 403) {
1209
- throw new AuthError();
1210
- }
1211
1203
  if (!response.ok) {
1212
1204
  const body = await response.text();
1205
+ const parsed = parseResponseBody(body);
1206
+ if (response.status === 401 && !isProviderOriginatedHttpError(parsed) || response.status === 403) {
1207
+ throw new AuthError();
1208
+ }
1213
1209
  const htmlError = detectHtmlErrorBody(
1214
1210
  body,
1215
1211
  response.headers.get("content-type")
@@ -1226,11 +1222,10 @@ var HttpClient = class {
1226
1222
  }
1227
1223
  );
1228
1224
  }
1229
- const parsed = parseResponseBody(body);
1230
1225
  throw new DeeplineError(
1231
1226
  apiErrorMessage(parsed, response.status),
1232
1227
  response.status,
1233
- "API_ERROR",
1228
+ apiErrorCodeFromResponse(parsed),
1234
1229
  { response: parsed }
1235
1230
  );
1236
1231
  }
@@ -1305,6 +1300,22 @@ function parseResponseBody(body) {
1305
1300
  return body;
1306
1301
  }
1307
1302
  }
1303
+ function isProviderOriginatedHttpError(parsed) {
1304
+ const response = asRecord(parsed);
1305
+ const error = asRecord(response?.error);
1306
+ const failureOrigin = error?.failure_origin ?? response?.failure_origin;
1307
+ const code = error?.code ?? response?.code;
1308
+ return failureOrigin === "provider" || code === "UPSTREAM_BLOCKED";
1309
+ }
1310
+ function apiErrorCodeFromResponse(parsed) {
1311
+ const response = asRecord(parsed);
1312
+ const error = asRecord(response?.error);
1313
+ const code = error?.code ?? response?.code;
1314
+ return typeof code === "string" ? code : "API_ERROR";
1315
+ }
1316
+ function asRecord(value) {
1317
+ return typeof value === "object" && value !== null ? value : null;
1318
+ }
1308
1319
  function isRetryableApiErrorResponse(status) {
1309
1320
  return status === 408 || status === 425 || status >= 500 && status < 600;
1310
1321
  }
@@ -25001,7 +25012,7 @@ function monitorsErrorExitCode(error) {
25001
25012
  }
25002
25013
  function monitorsFailureNextCommand(error, exitCode) {
25003
25014
  if (error instanceof DeeplineError) {
25004
- const response = asRecord(asRecord(error.details)?.response);
25015
+ const response = asRecord2(asRecord2(error.details)?.response);
25005
25016
  const serverNextAction = asString(response?.next_action);
25006
25017
  if (serverNextAction) return serverNextAction;
25007
25018
  }
@@ -25017,10 +25028,10 @@ function monitorsFailureNextCommand(error, exitCode) {
25017
25028
  }
25018
25029
  function readValidationIssues(error) {
25019
25030
  if (!(error instanceof DeeplineError)) return [];
25020
- const response = asRecord(asRecord(error.details)?.response);
25031
+ const response = asRecord2(asRecord2(error.details)?.response);
25021
25032
  const issues = Array.isArray(response?.issues) ? response.issues : [];
25022
25033
  return issues.flatMap((raw) => {
25023
- const issue = asRecord(raw);
25034
+ const issue = asRecord2(raw);
25024
25035
  if (!issue) return [];
25025
25036
  return [
25026
25037
  {
@@ -25113,7 +25124,7 @@ or from a file / stdin:
25113
25124
  cat monitor.json | ${input2.command} --file -`
25114
25125
  );
25115
25126
  }
25116
- function asRecord(value) {
25127
+ function asRecord2(value) {
25117
25128
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
25118
25129
  }
25119
25130
  function asString(value) {
@@ -25123,7 +25134,7 @@ function asFiniteNumber(value) {
25123
25134
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
25124
25135
  }
25125
25136
  function monitorPricingLine(rawPricing) {
25126
- const pricing = asRecord(rawPricing);
25137
+ const pricing = asRecord2(rawPricing);
25127
25138
  if (!pricing) return void 0;
25128
25139
  const display = asString(pricing.display);
25129
25140
  if (display) return display;
@@ -25138,16 +25149,16 @@ function yesNo(value) {
25138
25149
  return "unknown";
25139
25150
  }
25140
25151
  function readDeployOutputContract(payload) {
25141
- const contract = asRecord(payload.output_contract);
25152
+ const contract = asRecord2(payload.output_contract);
25142
25153
  if (!contract) return { streams: [] };
25143
25154
  const rawOutputs = Array.isArray(contract.outputs) ? contract.outputs : [];
25144
25155
  const streams = rawOutputs.flatMap((raw) => {
25145
- const output2 = asRecord(raw);
25156
+ const output2 = asRecord2(raw);
25146
25157
  const stream = output2 ? asString(output2.stream) : void 0;
25147
25158
  const table = output2 ? asString(output2.table) : void 0;
25148
25159
  if (!output2 || !stream || !table) return [];
25149
25160
  const columns = Array.isArray(output2.columns) ? output2.columns.flatMap((rawColumn) => {
25150
- const column = asRecord(rawColumn);
25161
+ const column = asRecord2(rawColumn);
25151
25162
  const name = column ? asString(column.name) : void 0;
25152
25163
  return name ? [name] : [];
25153
25164
  }) : [];
@@ -25164,7 +25175,7 @@ function readDeployOutputContract(payload) {
25164
25175
  return { tool: asString(contract.tool), streams };
25165
25176
  }
25166
25177
  function renderMonitorDeployCompletion(payload) {
25167
- const monitor = asRecord(payload.monitor);
25178
+ const monitor = asRecord2(payload.monitor);
25168
25179
  const key = monitor ? asString(monitor.key) : void 0;
25169
25180
  const { tool, streams } = readDeployOutputContract(payload);
25170
25181
  if (!key || streams.length === 0) return void 0;
@@ -25215,13 +25226,13 @@ function renderMonitorDeployPlan(payload) {
25215
25226
  if (!valid) {
25216
25227
  const issues = Array.isArray(payload.issues) ? payload.issues : [];
25217
25228
  for (const raw of issues) {
25218
- const issue = asRecord(raw);
25229
+ const issue = asRecord2(raw);
25219
25230
  const path = issue ? asString(issue.path) : void 0;
25220
25231
  const message = issue ? asString(issue.message) : void 0;
25221
25232
  if (message) lines.push(` - ${path ? `${path}: ` : ""}${message}`);
25222
25233
  }
25223
25234
  }
25224
- const estimate = asRecord(payload.deploy_cost_estimate);
25235
+ const estimate = asRecord2(payload.deploy_cost_estimate);
25225
25236
  const credits = estimate ? asFiniteNumber(estimate.credits) : void 0;
25226
25237
  if (credits !== void 0) {
25227
25238
  const note = estimate ? asString(estimate.note) : void 0;
@@ -25239,7 +25250,7 @@ function renderMonitorDeployPlan(payload) {
25239
25250
  }
25240
25251
  const candidates = Array.isArray(payload.reuse_candidates) ? payload.reuse_candidates : [];
25241
25252
  const candidateLines = candidates.flatMap((raw) => {
25242
- const candidate = asRecord(raw);
25253
+ const candidate = asRecord2(raw);
25243
25254
  const key = candidate ? asString(candidate.key) : void 0;
25244
25255
  if (!candidate || !key) return [];
25245
25256
  const name = asString(candidate.name);
@@ -25265,7 +25276,7 @@ function renderMonitorDeployPlan(payload) {
25265
25276
  `;
25266
25277
  }
25267
25278
  function renderMonitorDeletePlan(payload, fallbackKey) {
25268
- const plan = asRecord(payload.plan);
25279
+ const plan = asRecord2(payload.plan);
25269
25280
  const key = (plan ? asString(plan.monitor_key) : void 0) ?? fallbackKey;
25270
25281
  const lines = [
25271
25282
  "DRY RUN \u2014 nothing was deleted.",
@@ -25279,7 +25290,7 @@ function renderMonitorDeletePlan(payload, fallbackKey) {
25279
25290
  `;
25280
25291
  }
25281
25292
  function renderMonitorReactivatePlan(payload, key) {
25282
- const estimate = asRecord(payload.cost_estimate);
25293
+ const estimate = asRecord2(payload.cost_estimate);
25283
25294
  const credits = estimate ? asFiniteNumber(estimate.credits) : void 0;
25284
25295
  const lines = [
25285
25296
  "DRY RUN \u2014 nothing was reactivated.",
@@ -25331,7 +25342,7 @@ function renderAvailableToolsText(payload) {
25331
25342
  ];
25332
25343
  let currentProvider;
25333
25344
  for (const raw of tools) {
25334
- const entry = asRecord(raw);
25345
+ const entry = asRecord2(raw);
25335
25346
  const id = entry ? asString(entry.tool) : void 0;
25336
25347
  if (!entry || !id) continue;
25337
25348
  const name = asString(entry.name) ?? asString(entry.display_name);
@@ -25401,7 +25412,7 @@ function renderDeployedListText(payload, requestedStatus) {
25401
25412
  const header = monitors.length === 0 ? "No deployed monitors matched." : total !== void 0 && total > monitors.length ? `Deployed monitors (${monitors.length} of ${total}):` : `Deployed monitors (${monitors.length}):`;
25402
25413
  const lines = [header];
25403
25414
  for (const raw of monitors) {
25404
- const entry = asRecord(raw);
25415
+ const entry = asRecord2(raw);
25405
25416
  const key = entry ? asString(entry.key) ?? asString(entry.monitor_key) : void 0;
25406
25417
  if (!entry || !key) continue;
25407
25418
  const status = asString(entry.status);
@@ -25486,11 +25497,11 @@ function renderMonitorGet(payload) {
25486
25497
  const tool = asString(payload.tool);
25487
25498
  if (!key || !tool) return void 0;
25488
25499
  const status = asString(payload.status);
25489
- const definition = asRecord(payload.definition);
25500
+ const definition = asRecord2(payload.definition);
25490
25501
  const pricingLine = monitorPricingLine(payload.pricing);
25491
- const billing = asRecord(payload.billing);
25502
+ const billing = asRecord2(payload.billing);
25492
25503
  const nextRenewalAt = billing ? asString(billing.next_renewal_at) : void 0;
25493
- const dependents = asRecord(payload.dependents);
25504
+ const dependents = asRecord2(payload.dependents);
25494
25505
  const plays = dependents && Array.isArray(dependents.plays) ? dependents.plays : [];
25495
25506
  const lines = [
25496
25507
  `Monitor: ${key}`,
@@ -25507,7 +25518,7 @@ function renderMonitorGet(payload) {
25507
25518
  lines.push(" none");
25508
25519
  } else {
25509
25520
  for (const raw of plays) {
25510
- const play = asRecord(raw);
25521
+ const play = asRecord2(raw);
25511
25522
  const name = play ? asString(play.name) : void 0;
25512
25523
  if (!play || !name) continue;
25513
25524
  const listener = asString(play.listener_key);
@@ -31281,7 +31292,7 @@ function parseCapturedJson(stdout) {
31281
31292
  return null;
31282
31293
  }
31283
31294
  }
31284
- function asRecord2(value) {
31295
+ function asRecord3(value) {
31285
31296
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
31286
31297
  }
31287
31298
  function safeRead(path) {
@@ -31983,8 +31994,8 @@ async function runSetupCommand(options) {
31983
31994
  status: "complete",
31984
31995
  phases
31985
31996
  });
31986
- const doctorChecks = asRecord2(doctorPayload?.checks);
31987
- const apiCheck = asRecord2(doctorChecks?.api);
31997
+ const doctorChecks = asRecord3(doctorPayload?.checks);
31998
+ const apiCheck = asRecord3(doctorChecks?.api);
31988
31999
  printCommandEnvelope(
31989
32000
  {
31990
32001
  ok: true,
package/dist/index.js CHANGED
@@ -438,7 +438,7 @@ var SDK_RELEASE = {
438
438
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
439
439
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
440
440
  // Operators use the checkout-local deepline-admin binary instead.
441
- version: "0.1.299",
441
+ version: "0.1.301",
442
442
  contracts: {
443
443
  api: {
444
444
  name: "sdk-http-api",
@@ -783,7 +783,7 @@ var HttpClient = class {
783
783
  * @param path - API path (e.g. `"/api/v2/tools"`)
784
784
  * @param options - HTTP method, body, headers, and timeout
785
785
  * @returns Parsed JSON response body
786
- * @throws {@link AuthError} on HTTP 401/403 (immediate, no retry)
786
+ * @throws {@link AuthError} on Deepline-auth HTTP 401/403 (immediate, no retry)
787
787
  * @throws {@link RateLimitError} on HTTP 429 after all retries exhausted
788
788
  * @throws {@link DeeplineError} on other API errors or connection failures
789
789
  */
@@ -822,9 +822,6 @@ var HttpClient = class {
822
822
  signal: controller.signal
823
823
  });
824
824
  clearTimeout(timeoutId);
825
- if (response.status === 401 || response.status === 403 && !options?.forbiddenAsApiError) {
826
- throw new AuthError();
827
- }
828
825
  if (response.status === 429) {
829
826
  const retryAfter = parseRetryAfter(response);
830
827
  lastError = new RateLimitError(retryAfter);
@@ -835,11 +832,9 @@ var HttpClient = class {
835
832
  throw lastError;
836
833
  }
837
834
  const body = await response.text();
838
- let parsed;
839
- try {
840
- parsed = JSON.parse(body);
841
- } catch {
842
- parsed = body;
835
+ const parsed = parseResponseBody(body);
836
+ if (response.status === 401 && !isProviderOriginatedHttpError(parsed) || response.status === 403 && !options?.forbiddenAsApiError) {
837
+ throw new AuthError();
843
838
  }
844
839
  if (!response.ok) {
845
840
  const retryableApiError = options?.retryApiErrors === true && isRetryableApiErrorResponse(response.status);
@@ -866,7 +861,7 @@ var HttpClient = class {
866
861
  }
867
862
  const errorValue = typeof parsed === "object" && parsed && "error" in parsed ? parsed.error : void 0;
868
863
  const msg = typeof errorValue === "string" ? errorValue : errorValue && typeof errorValue === "object" && "message" in errorValue && typeof errorValue.message === "string" ? errorValue.message : typeof parsed === "object" && parsed && "message" in parsed && typeof parsed.message === "string" ? parsed.message : `HTTP ${response.status}`;
869
- const apiErrorCode = errorValue && typeof errorValue === "object" && typeof errorValue.code === "string" ? errorValue.code : typeof parsed === "object" && parsed && typeof parsed.code === "string" ? parsed.code : "API_ERROR";
864
+ const apiErrorCode = apiErrorCodeFromResponse(parsed);
870
865
  lastError = new DeeplineError(msg, response.status, apiErrorCode, {
871
866
  response: parsed
872
867
  });
@@ -940,11 +935,12 @@ var HttpClient = class {
940
935
  body: options?.body !== void 0 ? JSON.stringify(options.body) : void 0,
941
936
  signal: options?.signal
942
937
  });
943
- if (response.status === 401 || response.status === 403) {
944
- throw new AuthError();
945
- }
946
938
  if (!response.ok) {
947
939
  const body = await response.text();
940
+ const parsed = parseResponseBody(body);
941
+ if (response.status === 401 && !isProviderOriginatedHttpError(parsed) || response.status === 403) {
942
+ throw new AuthError();
943
+ }
948
944
  const htmlError = detectHtmlErrorBody(
949
945
  body,
950
946
  response.headers.get("content-type")
@@ -961,11 +957,10 @@ var HttpClient = class {
961
957
  }
962
958
  );
963
959
  }
964
- const parsed = parseResponseBody(body);
965
960
  throw new DeeplineError(
966
961
  apiErrorMessage(parsed, response.status),
967
962
  response.status,
968
- "API_ERROR",
963
+ apiErrorCodeFromResponse(parsed),
969
964
  { response: parsed }
970
965
  );
971
966
  }
@@ -1040,6 +1035,22 @@ function parseResponseBody(body) {
1040
1035
  return body;
1041
1036
  }
1042
1037
  }
1038
+ function isProviderOriginatedHttpError(parsed) {
1039
+ const response = asRecord(parsed);
1040
+ const error = asRecord(response?.error);
1041
+ const failureOrigin = error?.failure_origin ?? response?.failure_origin;
1042
+ const code = error?.code ?? response?.code;
1043
+ return failureOrigin === "provider" || code === "UPSTREAM_BLOCKED";
1044
+ }
1045
+ function apiErrorCodeFromResponse(parsed) {
1046
+ const response = asRecord(parsed);
1047
+ const error = asRecord(response?.error);
1048
+ const code = error?.code ?? response?.code;
1049
+ return typeof code === "string" ? code : "API_ERROR";
1050
+ }
1051
+ function asRecord(value) {
1052
+ return typeof value === "object" && value !== null ? value : null;
1053
+ }
1043
1054
  function isRetryableApiErrorResponse(status) {
1044
1055
  return status === 408 || status === 425 || status >= 500 && status < 600;
1045
1056
  }
package/dist/index.mjs CHANGED
@@ -367,7 +367,7 @@ var SDK_RELEASE = {
367
367
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
368
368
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
369
369
  // Operators use the checkout-local deepline-admin binary instead.
370
- version: "0.1.299",
370
+ version: "0.1.301",
371
371
  contracts: {
372
372
  api: {
373
373
  name: "sdk-http-api",
@@ -712,7 +712,7 @@ var HttpClient = class {
712
712
  * @param path - API path (e.g. `"/api/v2/tools"`)
713
713
  * @param options - HTTP method, body, headers, and timeout
714
714
  * @returns Parsed JSON response body
715
- * @throws {@link AuthError} on HTTP 401/403 (immediate, no retry)
715
+ * @throws {@link AuthError} on Deepline-auth HTTP 401/403 (immediate, no retry)
716
716
  * @throws {@link RateLimitError} on HTTP 429 after all retries exhausted
717
717
  * @throws {@link DeeplineError} on other API errors or connection failures
718
718
  */
@@ -751,9 +751,6 @@ var HttpClient = class {
751
751
  signal: controller.signal
752
752
  });
753
753
  clearTimeout(timeoutId);
754
- if (response.status === 401 || response.status === 403 && !options?.forbiddenAsApiError) {
755
- throw new AuthError();
756
- }
757
754
  if (response.status === 429) {
758
755
  const retryAfter = parseRetryAfter(response);
759
756
  lastError = new RateLimitError(retryAfter);
@@ -764,11 +761,9 @@ var HttpClient = class {
764
761
  throw lastError;
765
762
  }
766
763
  const body = await response.text();
767
- let parsed;
768
- try {
769
- parsed = JSON.parse(body);
770
- } catch {
771
- parsed = body;
764
+ const parsed = parseResponseBody(body);
765
+ if (response.status === 401 && !isProviderOriginatedHttpError(parsed) || response.status === 403 && !options?.forbiddenAsApiError) {
766
+ throw new AuthError();
772
767
  }
773
768
  if (!response.ok) {
774
769
  const retryableApiError = options?.retryApiErrors === true && isRetryableApiErrorResponse(response.status);
@@ -795,7 +790,7 @@ var HttpClient = class {
795
790
  }
796
791
  const errorValue = typeof parsed === "object" && parsed && "error" in parsed ? parsed.error : void 0;
797
792
  const msg = typeof errorValue === "string" ? errorValue : errorValue && typeof errorValue === "object" && "message" in errorValue && typeof errorValue.message === "string" ? errorValue.message : typeof parsed === "object" && parsed && "message" in parsed && typeof parsed.message === "string" ? parsed.message : `HTTP ${response.status}`;
798
- const apiErrorCode = errorValue && typeof errorValue === "object" && typeof errorValue.code === "string" ? errorValue.code : typeof parsed === "object" && parsed && typeof parsed.code === "string" ? parsed.code : "API_ERROR";
793
+ const apiErrorCode = apiErrorCodeFromResponse(parsed);
799
794
  lastError = new DeeplineError(msg, response.status, apiErrorCode, {
800
795
  response: parsed
801
796
  });
@@ -869,11 +864,12 @@ var HttpClient = class {
869
864
  body: options?.body !== void 0 ? JSON.stringify(options.body) : void 0,
870
865
  signal: options?.signal
871
866
  });
872
- if (response.status === 401 || response.status === 403) {
873
- throw new AuthError();
874
- }
875
867
  if (!response.ok) {
876
868
  const body = await response.text();
869
+ const parsed = parseResponseBody(body);
870
+ if (response.status === 401 && !isProviderOriginatedHttpError(parsed) || response.status === 403) {
871
+ throw new AuthError();
872
+ }
877
873
  const htmlError = detectHtmlErrorBody(
878
874
  body,
879
875
  response.headers.get("content-type")
@@ -890,11 +886,10 @@ var HttpClient = class {
890
886
  }
891
887
  );
892
888
  }
893
- const parsed = parseResponseBody(body);
894
889
  throw new DeeplineError(
895
890
  apiErrorMessage(parsed, response.status),
896
891
  response.status,
897
- "API_ERROR",
892
+ apiErrorCodeFromResponse(parsed),
898
893
  { response: parsed }
899
894
  );
900
895
  }
@@ -969,6 +964,22 @@ function parseResponseBody(body) {
969
964
  return body;
970
965
  }
971
966
  }
967
+ function isProviderOriginatedHttpError(parsed) {
968
+ const response = asRecord(parsed);
969
+ const error = asRecord(response?.error);
970
+ const failureOrigin = error?.failure_origin ?? response?.failure_origin;
971
+ const code = error?.code ?? response?.code;
972
+ return failureOrigin === "provider" || code === "UPSTREAM_BLOCKED";
973
+ }
974
+ function apiErrorCodeFromResponse(parsed) {
975
+ const response = asRecord(parsed);
976
+ const error = asRecord(response?.error);
977
+ const code = error?.code ?? response?.code;
978
+ return typeof code === "string" ? code : "API_ERROR";
979
+ }
980
+ function asRecord(value) {
981
+ return typeof value === "object" && value !== null ? value : null;
982
+ }
972
983
  function isRetryableApiErrorResponse(status) {
973
984
  return status === 408 || status === 425 || status >= 500 && status < 600;
974
985
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.1.299",
3
+ "version": "0.1.301",
4
4
  "description": "Deepline SDK + CLI — B2B data enrichment powered by durable cloud execution",
5
5
  "license": "MIT",
6
6
  "repository": {