@north-light/api 0.3.436 → 0.3.438

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core/io.d.ts CHANGED
@@ -57,6 +57,8 @@ interface ErrorPayloadBase {
57
57
  * of `received` on purpose: `received` names the caller's offending VALUE, and
58
58
  * reporting an HTTP status there is a false claim about what was sent. */
59
59
  http_status?: number;
60
+ /** Seconds the provider asked the caller to wait before retrying (a rate limit). */
61
+ retry_after_s?: number;
60
62
  next: string;
61
63
  }
62
64
  export type ErrorPayload = ErrorPayloadBase & ({
@@ -109,7 +111,12 @@ export declare function diag(message: string): void;
109
111
  * reconstruct it verbatim so a gate/guard error (revive reopen-gate, push
110
112
  * worktree/finalization guard, …) surfaces byte-for-byte as the leaf raised
111
113
  * it before the re-plumb, preserving its SPECIFIC code + next.
112
- * 3. Otherwise map status→code (`STATUS_TO_CLI_CODE`) with the server message
114
+ * 3. A provider-origin refusal (`err.origin === 'provider'`) carries the
115
+ * provider's own error in `details` (`{code,message,field?,next?,received?}`)
116
+ * under the daemon's coarse `provider_*` code and the provider's HTTP
117
+ * status. The provider's code, `next`, `field` and `Retry-After` reach the
118
+ * caller unchanged; the status→code map would flatten them to `conflict`.
119
+ * 4. Otherwise map status→code (`STATUS_TO_CLI_CODE`) with the server message
113
120
  * and a caller-supplied-or-default `next`. */
114
121
  export declare function apiErrorToCliError(err: ApiError, next?: string): CrtrError;
115
122
  /** Terminal error handler. Command-level failures (bad input, not-found,
package/dist/core/io.js CHANGED
@@ -209,6 +209,7 @@ function payloadOf(e) {
209
209
  received: d.received,
210
210
  field: d.field,
211
211
  ...(typeof d.http_status === 'number' ? { http_status: d.http_status } : {}),
212
+ ...(typeof d.retry_after_s === 'number' ? { retry_after_s: d.retry_after_s } : {}),
212
213
  };
213
214
  return result === undefined
214
215
  ? { ...base, next }
@@ -274,7 +275,12 @@ const STATUS_TO_CLI_CODE = {
274
275
  * reconstruct it verbatim so a gate/guard error (revive reopen-gate, push
275
276
  * worktree/finalization guard, …) surfaces byte-for-byte as the leaf raised
276
277
  * it before the re-plumb, preserving its SPECIFIC code + next.
277
- * 3. Otherwise map status→code (`STATUS_TO_CLI_CODE`) with the server message
278
+ * 3. A provider-origin refusal (`err.origin === 'provider'`) carries the
279
+ * provider's own error in `details` (`{code,message,field?,next?,received?}`)
280
+ * under the daemon's coarse `provider_*` code and the provider's HTTP
281
+ * status. The provider's code, `next`, `field` and `Retry-After` reach the
282
+ * caller unchanged; the status→code map would flatten them to `conflict`.
283
+ * 4. Otherwise map status→code (`STATUS_TO_CLI_CODE`) with the server message
278
284
  * and a caller-supplied-or-default `next`. */
279
285
  export function apiErrorToCliError(err, next) {
280
286
  // A handover is NOT a down daemon: the successor is already up (the client
@@ -297,12 +303,23 @@ export function apiErrorToCliError(err, next) {
297
303
  }
298
304
  // A responding daemon can relay an unavailable provider or storage service.
299
305
  // HTTP 503 alone does not mean that crtrd is down.
306
+ const d = err.details;
307
+ const provider = providerSupplied(err, d);
300
308
  if (err.status === 503) {
301
- return new CrtrError(err.code, err.message, ExitCode.NETWORK, {
302
- next: next ?? 'Retry the command; if it persists, inspect the unavailable service named in the error.',
303
- });
309
+ const code = provider.code ?? err.code;
310
+ const details = {
311
+ next: provider.next ?? next ?? 'Retry the command; if it persists, inspect the unavailable service named in the error.',
312
+ };
313
+ if (provider.code !== undefined) {
314
+ if (provider.field !== undefined)
315
+ details['field'] = provider.field;
316
+ if (provider.received !== undefined)
317
+ details['received'] = provider.received;
318
+ if (provider.retryAfterS !== undefined)
319
+ details['retry_after_s'] = provider.retryAfterS;
320
+ }
321
+ return new CrtrError(code, err.message, ExitCode.NETWORK, details);
304
322
  }
305
- const d = err.details;
306
323
  // A `/v1` request validator reports each bad field in `details.violations`;
307
324
  // the message alone only says the request was invalid.
308
325
  const violations = Array.isArray(d?.violations)
@@ -312,14 +329,20 @@ export function apiErrorToCliError(err, next) {
312
329
  const payload = d && typeof d.error === 'string' && typeof d.message === 'string' && typeof d.next === 'string'
313
330
  ? d
314
331
  : {
315
- error: STATUS_TO_CLI_CODE[err.status] ?? err.code,
332
+ error: provider.code ?? STATUS_TO_CLI_CODE[err.status] ?? err.code,
333
+ ...(provider.field !== undefined ? { field: provider.field } : {}),
334
+ ...(provider.received !== undefined ? { received: provider.received } : {}),
335
+ ...(provider.retryAfterS !== undefined ? { retry_after_s: provider.retryAfterS } : {}),
316
336
  message: violations.length > 0 ? `${err.message}: ${violations.join('; ')}` : err.message.trim() !== '' ? err.message : `request refused (HTTP ${err.status}, ${err.code}) with no message`,
317
- next: next ??
318
- (err.status >= 500
319
- ? 'This is a crtr bug, not a bad call. Retry; if it persists, report it (the daemon log holds the full cause).'
320
- : err.status === 404
321
- ? 'List nodes with `crtr node inspect list`.'
322
- : 'Inspect the error and adjust the call. See -h for the schema.'),
337
+ next: provider.next ??
338
+ next ??
339
+ (provider.retryAfterS !== undefined
340
+ ? `Wait ${provider.retryAfterS}s, then retry; do not retry sooner.`
341
+ : err.status >= 500
342
+ ? 'This is a crtr bug, not a bad call. Retry; if it persists, report it (the daemon log holds the full cause).'
343
+ : err.status === 404
344
+ ? 'List nodes with `crtr node inspect list`.'
345
+ : 'Inspect the error and adjust the call. See -h for the schema.'),
323
346
  };
324
347
  // Preserve the exit-code class the server leaf carried instead of collapsing
325
348
  // every non-503 to USAGE(2): a not-found (404 / `not_found`) surfaces exit 3
@@ -327,6 +350,33 @@ export function apiErrorToCliError(err, next) {
327
350
  // (memory/pkg/sys) still emit so the taxonomy stays uniform across the CLI.
328
351
  return new InputError(payload, exitForCliCode(payload.error, err.status));
329
352
  }
353
+ /** What a provider put in its own error body, as the daemon relays it: the
354
+ * provider's code (`details.code`; the envelope `code` is only the daemon's
355
+ * coarse `provider_*` bucket), plus `next`, `field`, `received`, and the
356
+ * `Retry-After` the daemon lifts into the envelope. Empty for any error that
357
+ * did not originate at a provider, so non-provider status→code mapping is
358
+ * untouched. A code that is not a snake_case identifier, or that came with no
359
+ * provider HTTP status, is ignored. */
360
+ function providerSupplied(err, d) {
361
+ if (err.origin !== 'provider' || d === undefined || d === null || typeof d !== 'object')
362
+ return {};
363
+ const out = {};
364
+ // `details.status` is the provider's own HTTP status; the runtime's transport
365
+ // diagnostics (`{status: null, code: 'timeout'}`) are not a provider's code.
366
+ if (typeof d.status === 'number' && typeof d.code === 'string' && /^[a-z][a-z0-9_]*$/.test(d.code))
367
+ out.code = d.code;
368
+ if (typeof d.next === 'string' && d.next.trim() !== '')
369
+ out.next = d.next;
370
+ if (typeof d.field === 'string' && d.field !== '')
371
+ out.field = d.field;
372
+ else if (err.param !== undefined)
373
+ out.field = err.param;
374
+ if (d.received !== undefined && d.received !== null)
375
+ out.received = d.received;
376
+ if (typeof err.retryAfterS === 'number')
377
+ out.retryAfterS = err.retryAfterS;
378
+ return out;
379
+ }
330
380
  /** The `ExitCode` class a reconstructed API error should carry, keyed off the
331
381
  * round-tripped CLI code (and the 404 status as the not-found signal — the
332
382
  * server maps NOT_FOUND's exit class to status 404, but folds AMBIGUOUS and
@@ -129,6 +129,8 @@ export function renderError(p) {
129
129
  // the value the caller supplied.
130
130
  if (p.http_status !== undefined)
131
131
  lines.push(`http status: ${p.http_status}`);
132
+ if (p.retry_after_s !== undefined)
133
+ lines.push(`retry after: ${p.retry_after_s}s`);
132
134
  if ('primaryCompleted' in p && p.primaryCompleted === true) {
133
135
  lines.push('primaryCompleted: true');
134
136
  lines.push(`Primary result (the command already completed):\n${fencedJson(p.result)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/api",
3
- "version": "0.3.436",
3
+ "version": "0.3.438",
4
4
  "description": "Typed crtrd /v1 API contract — DTOs, route builders, the error contract, the CrtrClient, and the command-plugin manifest format. Zero runtime dependencies.",
5
5
  "type": "module",
6
6
  "main": "./dist/api/index.js",