totemlayer 0.1.21 → 0.1.25

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.
@@ -594,6 +594,8 @@ export async function runRuntime(settings) {
594
594
  const shutdown = new AbortController();
595
595
  const signal = AbortSignal.any([settings.signal, shutdown.signal]);
596
596
  const identity = await client.agent({ signal });
597
+ settings.connectionOnly ||= Boolean(identity.listenerConnectionId);
598
+ if (settings.connectionOnly) settings.allowAgents = [];
597
599
  if (settings.expectedAgentId && identity.id !== settings.expectedAgentId)
598
600
  throw Object.assign(new Error("The runtime credential identifies another agent."), {
599
601
  code: "AGENT_IDENTITY_MISMATCH",
@@ -608,7 +610,25 @@ export async function runRuntime(settings) {
608
610
  const directory = join(settings.stateDir ?? join(homedir(), ".totem", "runtime"), scope);
609
611
  const release = await runtimeLock(directory);
610
612
  const journal = new Journal(directory);
613
+ const runId = settings.runId ?? randomUUID();
614
+ const verify = typeof client.runtime?.start === "function";
615
+ if (settings.verifyReadiness && !verify) {
616
+ await release();
617
+ throw Object.assign(new Error("Upgrade the Totem SDK to test and authorize this listener."), {
618
+ code: "READINESS_UNSUPPORTED",
619
+ });
620
+ }
621
+ if (verify && settings.codexThreadId) {
622
+ await release();
623
+ throw Object.assign(
624
+ new Error(
625
+ "Existing Codex chat queueing cannot verify a controlled receive/wake/reply round trip. Configure the isolated Codex adapter instead.",
626
+ ),
627
+ { code: "READINESS_UNSUPPORTED" },
628
+ );
629
+ }
611
630
  let heartbeat;
631
+ let probeFailed = false;
612
632
  let inbox;
613
633
  let humanDone = false;
614
634
  let fatalError;
@@ -618,14 +638,15 @@ export async function runRuntime(settings) {
618
638
  await readFile(new URL("../package.json", import.meta.url), "utf8"),
619
639
  ).version;
620
640
  const report = (error, context = {}) => {
641
+ const reconnecting = !context.messageId && transientNetworkError(error);
621
642
  diagnostics = {
622
- phase: "blocked",
643
+ phase: reconnecting ? "reconnecting" : "blocked",
623
644
  errorCode: runtimeErrorCode(error),
624
645
  ...(typeof context.messageId === "string" ? { messageId: context.messageId } : {}),
625
646
  };
626
647
  if (settings.once) process.exitCode = 1;
627
648
  emit({
628
- type: "runtime_error",
649
+ type: reconnecting ? "runtime_reconnecting" : "runtime_error",
629
650
  ...context,
630
651
  error: {
631
652
  code: error.code ?? "RUNTIME_FAILED",
@@ -641,10 +662,56 @@ export async function runRuntime(settings) {
641
662
  });
642
663
  };
643
664
  try {
665
+ if (verify) {
666
+ await client.runtime.start(
667
+ {
668
+ runId,
669
+ provider: settings.provider === "ollama" ? "ollama-local" : settings.provider,
670
+ hostKind: settings.hostKind ?? "local",
671
+ },
672
+ { signal },
673
+ );
674
+ try {
675
+ const probe = await client.runtime.probe(runId, { signal });
676
+ if (
677
+ probe.runId !== runId ||
678
+ probe.protocol !== "totem-readiness-v1" ||
679
+ !/^[a-f0-9]{64}$/.test(probe.challenge)
680
+ )
681
+ throw new Error("The readiness challenge is invalid.");
682
+ if (!probe.completed) {
683
+ const answer = await generate({
684
+ client,
685
+ identity,
686
+ journal,
687
+ key: `readiness:${runId}:${probe.challenge}`,
688
+ text: `Controlled readiness check. Reply with exactly TOTEM_READY:${probe.challenge} and no other text. No tools, files, external actions or business work are authorized.`,
689
+ history: [],
690
+ settings: { ...settings, system: "", allowAgents: [], modelTimeoutMs: 30000 },
691
+ signal,
692
+ allowTools: false,
693
+ });
694
+ await client.runtime.replyProbe({ runId, reply: String(answer).trim() }, { signal });
695
+ }
696
+ emit({ type: "runtime_ready", data: { runId, provider: settings.provider, ready: true } });
697
+ } catch (error) {
698
+ probeFailed = true;
699
+ await client.runtime
700
+ .heartbeat({ runId, state: "error", errorCode: "READINESS_PROBE_FAILED" }, { signal })
701
+ .catch(() => {});
702
+ throw Object.assign(
703
+ new Error(
704
+ "The bounded receive/wake/reply check failed. Check the selected adapter and its authorization, then restart this listener.",
705
+ ),
706
+ { code: "READINESS_PROBE_FAILED", cause: error },
707
+ );
708
+ }
709
+ }
644
710
  const beat = async () => {
645
711
  const result = await client.runtime.heartbeat(
646
712
  {
647
713
  label: `${settings.provider}:${settings.model}`.slice(0, 100),
714
+ ...(verify ? { runId, state: "online" } : {}),
648
715
  ...(diagnosticsSupported
649
716
  ? {
650
717
  diagnostics: {
@@ -672,7 +739,7 @@ export async function runRuntime(settings) {
672
739
  } catch (error) {
673
740
  if (signal.aborted) break;
674
741
  report(error);
675
- if (error.status === 401 || error.status === 403) {
742
+ if (error.status === 401 || error.status === 403 || error.code === "RUNTIME_SUPERSEDED") {
676
743
  fatalError = error;
677
744
  shutdown.abort();
678
745
  break;
@@ -688,6 +755,7 @@ export async function runRuntime(settings) {
688
755
  model: settings.model,
689
756
  allowedAgents: settings.allowAgents,
690
757
  inspection: settings.inspector.mode,
758
+ ready: verify,
691
759
  },
692
760
  });
693
761
  diagnostics = { phase: "waiting" };
@@ -700,8 +768,17 @@ export async function runRuntime(settings) {
700
768
  once: settings.once,
701
769
  onPollError: (error) => report(error),
702
770
  shouldContinue: () => !settings.once || !humanDone,
703
- messages: (signal) =>
704
- client.messages.iterate({ direction: "inbound", unread: true, limit: 100 }, { signal }),
771
+ messages: async function* (signal) {
772
+ for await (const message of client.messages.iterate(
773
+ { direction: "inbound", unread: true, limit: 100 },
774
+ { signal },
775
+ ))
776
+ yield message;
777
+ if (diagnostics.phase === "reconnecting") {
778
+ diagnostics = { phase: "waiting" };
779
+ emit({ type: "runtime_resumed" });
780
+ }
781
+ },
705
782
  eligible: async (message) => {
706
783
  const blocker = await journal.read(`message-blocker:${message.id}`);
707
784
  // Older runtimes persisted inspector outages as permanent blockers.
@@ -713,13 +790,22 @@ export async function runRuntime(settings) {
713
790
  diagnostics = { phase: "working", messageId: message.id };
714
791
  emit({ type: "message_started", data: { messageId: message.id } });
715
792
  if (settings.codexThreadId) {
716
- const delivery = await queueCodexChatMessage({ settings, client, identity, journal, message, signal });
793
+ const delivery = await queueCodexChatMessage({
794
+ settings,
795
+ client,
796
+ identity,
797
+ journal,
798
+ message,
799
+ signal,
800
+ });
717
801
  if (delivery.state === "uncertain") {
718
802
  report({ code: "AGENT_OUTCOME_UNCERTAIN" }, { messageId: message.id });
719
803
  } else {
720
804
  diagnostics = { phase: "waiting" };
721
- emit({ type: delivery.state === "confirmed" ? "message_handled" : "chat_queued",
722
- data: { messageId: message.id, threadId: settings.codexThreadId } });
805
+ emit({
806
+ type: delivery.state === "confirmed" ? "message_handled" : "chat_queued",
807
+ data: { messageId: message.id, threadId: settings.codexThreadId },
808
+ });
723
809
  }
724
810
  return;
725
811
  }
@@ -734,11 +820,20 @@ export async function runRuntime(settings) {
734
820
  const thread = [];
735
821
  let cursor;
736
822
  for (let page = 0; page < 5; page++) {
737
- const result = await client.conversations.messages(
738
- message.conversationId,
739
- { limit: 100, ...(cursor ? { cursor } : {}) },
740
- { signal },
741
- );
823
+ const result = settings.connectionOnly
824
+ ? await client.messages.list(
825
+ {
826
+ conversationId: message.conversationId,
827
+ limit: 100,
828
+ ...(cursor ? { cursor } : {}),
829
+ },
830
+ { signal },
831
+ )
832
+ : await client.conversations.messages(
833
+ message.conversationId,
834
+ { limit: 100, ...(cursor ? { cursor } : {}) },
835
+ { signal },
836
+ );
742
837
  thread.push(...result.data);
743
838
  if (thread.some((item) => item.id === message.id) || !result.nextCursor) break;
744
839
  cursor = result.nextCursor;
@@ -832,7 +927,7 @@ export async function runRuntime(settings) {
832
927
  });
833
928
  // An owner-bound desktop chat consumes its own prompts. It does not need
834
929
  // the independent Totem website-chat command queue or a fresh model turn.
835
- if (settings.codexThreadId) {
930
+ if (settings.codexThreadId || settings.connectionOnly) {
836
931
  humanDone = true;
837
932
  await inbox;
838
933
  if (fatalError) throw fatalError;
@@ -871,7 +966,8 @@ export async function runRuntime(settings) {
871
966
  let leaseError;
872
967
  const renewal = (async () => {
873
968
  while (!commandSignal.aborted) {
874
- await pause(30000, commandSignal);
969
+ // Observe human interruption promptly without repeating provider execution.
970
+ await pause(5000, commandSignal);
875
971
  try {
876
972
  await client.chatCommands.renew(command.id, claim.leaseToken, {
877
973
  signal: commandSignal,
@@ -980,6 +1076,10 @@ export async function runRuntime(settings) {
980
1076
  shutdown.abort();
981
1077
  await heartbeat;
982
1078
  await inbox;
1079
+ if (verify && !probeFailed)
1080
+ await client.runtime
1081
+ .heartbeat({ runId, state: "offline" }, { signal: AbortSignal.timeout(2000) })
1082
+ .catch(() => {});
983
1083
  await release();
984
1084
  emit({ type: "runtime_stopped", data: { agent: identity.address } });
985
1085
  }
@@ -32,6 +32,10 @@ export const RUNTIME_ERROR_CODES = new Set([
32
32
  "AGENT_IDENTITY_MISMATCH",
33
33
  "MODEL_ERROR",
34
34
  "MAX_STEPS",
35
+ "READINESS_UNSUPPORTED",
36
+ "READINESS_PROBE_FAILED",
37
+ "RUNTIME_AUTHORIZATION_REJECTED",
38
+ "RUNTIME_SUPERSEDED",
35
39
  "INTERNAL_ERROR",
36
40
  ]);
37
41
  export function runtimeErrorCode(error) {
@@ -251,6 +251,7 @@ export async function installRuntimeService(runtime, env = process.env, options
251
251
  "TOTEM_INSPECTOR_MODEL",
252
252
  "TOTEM_INSPECTOR_TIMEOUT_MS",
253
253
  "TOTEM_PROVIDER",
254
+ "TOTEM_RUNTIME_HOST_KIND",
254
255
  "TOTEM_MODEL",
255
256
  "TOTEM_BASE_URL",
256
257
  "TOTEM_AGENT_ID",
@@ -0,0 +1,236 @@
1
+ import type { JsonValue, RequestOptions, TotemOptions } from "./types.js";
2
+ export interface TotemActionsOptions extends Omit<TotemOptions, "apiKey" | "address"> {
3
+ /** Explicitly separate scoped Action Checks credential; never a Network tn_ key. */
4
+ actionChecksApiKey: string;
5
+ }
6
+ export interface AssistantCheckOptions extends RequestOptions {
7
+ /** Same scoped credential used by the separate TotemActions client. */
8
+ actionChecksApiKey: string;
9
+ }
10
+ export interface AssistantActionCheck {
11
+ suite: ActionCheckSuite;
12
+ /** Use this exact returned action for grant issuance and execution. */
13
+ action: ActionProposal;
14
+ missionId: string;
15
+ workItemId: string;
16
+ executionAuthorized: false;
17
+ checksUrl: string;
18
+ }
19
+ export interface AssistantActionReceipt {
20
+ id: string;
21
+ outcome: ActionExecutionReceipt["outcome"];
22
+ reconciliationState: ActionExecutionReceipt["reconciliationStatus"];
23
+ executorAdapter: string;
24
+ completedAt: string | null;
25
+ }
26
+ export interface AssistantActionCheckList {
27
+ checks: Array<{
28
+ id: string;
29
+ /** Original private handoff suite, verified through immutable rerun lineage. */
30
+ handoffSuiteId?: string;
31
+ actionDigest: string;
32
+ status: ActionCheckSuite["status"];
33
+ conclusion: ActionSuiteConclusion | null;
34
+ expiresAt: string;
35
+ staleAt: string | null;
36
+ executionReceipts: AssistantActionReceipt[];
37
+ checksUrl: string;
38
+ }>;
39
+ }
40
+ export interface AssistantActionReceiptResult {
41
+ receipt: AssistantActionReceipt & {
42
+ receiptDigest: string;
43
+ };
44
+ missionId: string;
45
+ workItemId: string;
46
+ }
47
+ export type ActionSideEffectClass = "READ_ONLY" | "DATA_EGRESS" | "EXTERNAL_COMMUNICATION" | "FINANCIAL" | "DESTRUCTIVE" | "AUTHORITY_CHANGE" | "SECURITY_OPERATION";
48
+ export type ActionReversibility = "REVERSIBLE" | "COMPENSATABLE" | "IRREVERSIBLE" | "UNKNOWN";
49
+ export type ActionDataClassification = "PUBLIC" | "INTERNAL" | "CONFIDENTIAL" | "PERSONAL" | "FINANCIAL" | "AUTHENTICATION" | "SOURCE_CODE" | "SECRET";
50
+ export type ActionExecutorAdapter = "NOOP_READ" | "LOOPBACK_ECHO" | "FAKE_LEDGER" | "TEMP_FIXTURE_DELETE" | "SUPPLYCO_QUOTE" | "CREDENTIAL_ISSUER" | "RECEIVER_API_KEY" | "CREDENTIAL_VERIFY";
51
+ export interface ActionFinancialContext {
52
+ actionType: "PAYMENT" | "PURCHASE" | "TRANSFER" | "REFUND" | "WITHDRAWAL" | "DEPOSIT" | "TRADE" | "SUBSCRIPTION" | "INVOICE_PAYMENT" | "CRYPTO" | "QUOTE_REQUEST";
53
+ movesValue: boolean;
54
+ amount: number | null;
55
+ currency: string | null;
56
+ sourceAccount: string | null;
57
+ beneficiary: string | null;
58
+ destinationAccount: string | null;
59
+ beneficiaryIsNew: boolean | null;
60
+ reversible: boolean | null;
61
+ }
62
+ /** Exact proposed action. The server derives tenant authority from the scoped credential. */
63
+ export interface ActionProposal {
64
+ version: "1";
65
+ /** Totem identity-agent ID from assistant.context().agent.identityAgentId. */
66
+ agentId: string;
67
+ credentialId?: string | null;
68
+ automationRunId?: string | null;
69
+ toolName: string;
70
+ actionType: string;
71
+ sideEffectClass: ActionSideEffectClass;
72
+ executorAdapter: ActionExecutorAdapter;
73
+ target: {
74
+ resourceType: string;
75
+ resourceId?: string | null;
76
+ ownedTemporaryFixture?: boolean;
77
+ };
78
+ destination?: {
79
+ protocol: "local:" | "http:" | "https:";
80
+ host: string;
81
+ port?: number | null;
82
+ path: string;
83
+ tenant?: string | null;
84
+ redirects?: "DENY" | "SAME_ORIGIN";
85
+ } | null;
86
+ data: {
87
+ classifications?: ActionDataClassification[];
88
+ fields?: string[];
89
+ payload?: JsonValue;
90
+ minimized?: boolean;
91
+ byteLength?: number | null;
92
+ };
93
+ financial?: ActionFinancialContext | null;
94
+ reversibility: ActionReversibility;
95
+ justification: string;
96
+ provenance: Array<{
97
+ kind: "AUTHENTICATED_USER" | "TRUSTED_POLICY" | "INTERNAL_DATA" | "TOOL_OUTPUT" | "WEB_CONTENT" | "EMAIL" | "MESSAGE" | "OTHER_AGENT" | "UNTRUSTED_TEXT";
98
+ /** A caller-provided flag is advisory; the server derives source trust. */
99
+ trusted: boolean;
100
+ reference?: string | null;
101
+ content?: string | null;
102
+ }>;
103
+ idempotencyKey: string;
104
+ executionAttemptId: string;
105
+ expiresAt: string;
106
+ payload?: JsonValue;
107
+ }
108
+ export type ActionCheckConclusion = "PASS" | "WARN" | "REQUIRE_APPROVAL" | "BLOCK" | "ERROR" | "SKIPPED";
109
+ export type ActionSuiteConclusion = "ALLOW" | "ALLOW_WITH_WARNINGS" | "REQUIRES_APPROVAL" | "BLOCK" | "ERROR";
110
+ export type ActionRiskLevel = "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
111
+ export interface ActionExecutionReceipt {
112
+ id: string;
113
+ outcome: "SUCCEEDED" | "FAILED" | "UNKNOWN" | "COMPENSATED";
114
+ executorAdapter: string;
115
+ externalResponseSummary: string | null;
116
+ idempotencyStatus: "RECORDED";
117
+ reconciliationStatus: "NOT_REQUIRED" | "PENDING" | "CONFIRMED" | "FAILED";
118
+ compensationStatus: "RECORDED" | "NOT_REQUIRED";
119
+ createdAt: string;
120
+ executedAt: string | null;
121
+ completedAt: string | null;
122
+ }
123
+ /** Redacted server view; do not reconstruct an executable action from this projection. */
124
+ export interface ActionCheckSuite {
125
+ id: string;
126
+ status: "QUEUED" | "RUNNING" | "COMPLETED" | "STALE";
127
+ conclusion: ActionSuiteConclusion | null;
128
+ riskLevel: ActionRiskLevel;
129
+ explanation: string | null;
130
+ policyVersion: string;
131
+ requiredProviders: string[];
132
+ actionDigest: string;
133
+ createdAt: string;
134
+ updatedAt: string;
135
+ startedAt: string | null;
136
+ completedAt: string | null;
137
+ expiresAt: string;
138
+ staleAt: string | null;
139
+ staleReason: string | null;
140
+ supersededBySuiteId: string | null;
141
+ action: {
142
+ id: string;
143
+ agentId: string;
144
+ agent: {
145
+ id: string;
146
+ name: string;
147
+ } | null;
148
+ toolName: string;
149
+ actionType: string;
150
+ sideEffectClass: ActionSideEffectClass;
151
+ target: {
152
+ resourceType: string;
153
+ resourceId: string | null;
154
+ };
155
+ destination: {
156
+ protocol: string;
157
+ host: string;
158
+ port: number | null;
159
+ path: string;
160
+ tenant: string | null;
161
+ } | null;
162
+ dataClassifications: JsonValue;
163
+ financial: {
164
+ actionType: string;
165
+ movesValue: boolean | null;
166
+ amount: number | null;
167
+ currency: string | null;
168
+ beneficiary: string | null;
169
+ destinationAccount: string | null;
170
+ } | null;
171
+ reversibility: ActionReversibility;
172
+ justification: string;
173
+ expiresAt: string;
174
+ };
175
+ runs: Array<{
176
+ id: string;
177
+ providerName: string;
178
+ providerVersion: string;
179
+ required: boolean;
180
+ status: "QUEUED" | "RUNNING" | "COMPLETED" | "TIMED_OUT";
181
+ conclusion: ActionCheckConclusion | null;
182
+ reasonCode: string | null;
183
+ summary: string | null;
184
+ findings: Array<{
185
+ id: string;
186
+ severity: "INFO" | ActionRiskLevel;
187
+ category: string;
188
+ reasonCode: string;
189
+ explanation: string;
190
+ fieldReference: string | null;
191
+ sensitiveFingerprint: string | null;
192
+ remediation: string | null;
193
+ evidenceSource: string;
194
+ confidence: number;
195
+ }>;
196
+ startedAt: string | null;
197
+ completedAt: string | null;
198
+ timedOutAt: string | null;
199
+ }>;
200
+ approval: {
201
+ id: string;
202
+ status: "APPROVED" | "REJECTED" | "REVOKED" | "EXPIRED";
203
+ reason: string | null;
204
+ approver: {
205
+ id: string;
206
+ name?: string | null;
207
+ email?: string;
208
+ };
209
+ createdAt: string;
210
+ expiresAt: string;
211
+ rejectedAt: string | null;
212
+ revokedAt: string | null;
213
+ } | null;
214
+ grant: {
215
+ id: string;
216
+ status: "ACTIVE" | "CONSUMED" | "REVOKED" | "EXPIRED";
217
+ createdAt: string;
218
+ expiresAt: string;
219
+ consumedAt: string | null;
220
+ invalidatedAt: string | null;
221
+ } | null;
222
+ receipt: ActionExecutionReceipt | null;
223
+ confirmationPhrase: string | null;
224
+ demoAction: ActionProposal | null;
225
+ }
226
+ export interface ActionExecutionGrant {
227
+ /** One-use secret, bound to the checked action and issuing principal. */
228
+ token: string;
229
+ expiresAt: string;
230
+ suite: ActionCheckSuite;
231
+ }
232
+ export interface CheckedExecutionResult {
233
+ suite: ActionCheckSuite;
234
+ receipt: ActionExecutionReceipt | null;
235
+ receiptId: string;
236
+ }
@@ -0,0 +1,19 @@
1
+ import type { RequestOptions } from "./types.js";
2
+ import type { ActionCheckSuite, ActionExecutionGrant, ActionProposal, CheckedExecutionResult, TotemActionsOptions } from "./action-types.js";
3
+ export * from "./action-types.js";
4
+ /**
5
+ * Separately authenticated Action Checks client. Human approval stays in Totem's UI.
6
+ * Writes are never automatically replayed, even when the action has an idempotency key.
7
+ * After an ambiguous execution failure, use get() to inspect the receipt before proceeding.
8
+ */
9
+ export declare class TotemActions {
10
+ #private;
11
+ constructor(options: TotemActionsOptions);
12
+ create(action: ActionProposal, options?: RequestOptions): Promise<ActionCheckSuite>;
13
+ get(suiteId: string, options?: RequestOptions): Promise<ActionCheckSuite>;
14
+ issueGrant(suiteId: string, action: ActionProposal, options?: RequestOptions): Promise<ActionExecutionGrant>;
15
+ execute(suiteId: string, input: {
16
+ action: ActionProposal;
17
+ grant: string;
18
+ }, options?: RequestOptions): Promise<CheckedExecutionResult>;
19
+ }
@@ -0,0 +1,30 @@
1
+ export declare class TotemError extends Error {
2
+ readonly code: string;
3
+ readonly status?: number;
4
+ readonly requestId?: string;
5
+ readonly retryAfterSeconds?: number;
6
+ constructor(message: string, options: {
7
+ code: string;
8
+ status?: number;
9
+ requestId?: string;
10
+ retryAfterSeconds?: number;
11
+ cause?: unknown;
12
+ });
13
+ }
14
+ export declare class TotemAuthenticationError extends TotemError {
15
+ }
16
+ export declare class TotemPermissionError extends TotemError {
17
+ }
18
+ export declare class TotemNotFoundError extends TotemError {
19
+ }
20
+ export declare class TotemConflictError extends TotemError {
21
+ }
22
+ export declare class TotemRateLimitError extends TotemError {
23
+ }
24
+ export declare class TotemTimeoutError extends TotemError {
25
+ }
26
+ export declare class TotemNetworkError extends TotemError {
27
+ }
28
+ export declare class TotemCanceledError extends TotemError {
29
+ }
30
+ export declare function apiError(status: number, body: unknown, requestId?: string): TotemError;
package/bin/sdk/errors.js CHANGED
@@ -2,12 +2,14 @@ export class TotemError extends Error {
2
2
  code;
3
3
  status;
4
4
  requestId;
5
+ retryAfterSeconds;
5
6
  constructor(message, options) {
6
7
  super(message, { cause: options.cause });
7
8
  this.name = new.target.name;
8
9
  this.code = options.code;
9
10
  this.status = options.status;
10
11
  this.requestId = options.requestId;
12
+ this.retryAfterSeconds = options.retryAfterSeconds;
11
13
  }
12
14
  }
13
15
  export class TotemAuthenticationError extends TotemError {
@@ -50,5 +52,16 @@ export function apiError(status, body, requestId) {
50
52
  : status === 429
51
53
  ? TotemRateLimitError
52
54
  : TotemError;
53
- return new Constructor(message, { code, status, requestId: id });
55
+ const details = "details" in error ? error.details : undefined;
56
+ const retryAfter = details && typeof details === "object" && "retryAfterSeconds" in details
57
+ ? details.retryAfterSeconds
58
+ : undefined;
59
+ const retryAfterSeconds = status === 429 &&
60
+ typeof retryAfter === "number" &&
61
+ Number.isSafeInteger(retryAfter) &&
62
+ retryAfter > 0 &&
63
+ retryAfter <= 86400
64
+ ? retryAfter
65
+ : undefined;
66
+ return new Constructor(message, { code, status, requestId: id, retryAfterSeconds });
54
67
  }