@threahq/bot-runtime-client 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Threa contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # @threahq/bot-runtime-client
2
+
3
+ Protocol client for Threa's bot runtime. It owns the `/bot` Socket.IO
4
+ connection a runtime keeps open to Threa and routes the high-volume writes
5
+ (presence, claim renewal, trace steps) over it, falling back to the public HTTP
6
+ endpoints when the socket is down. It also carries the sealed-turn crypto an
7
+ end-to-end-encrypted bot needs.
8
+
9
+ Most connectors should use [`@threahq/remote-session`](https://www.npmjs.com/package/@threahq/remote-session),
10
+ which builds the whole session loop on top of this package. Use this package
11
+ directly when you want the socket and the write routing without the loop, for
12
+ example a mention-driven bot that claims work with plain HTTP.
13
+
14
+ The endpoints and events this package speaks are documented at
15
+ [threa.io/developers](https://threa.io/developers): the `Bot runtimes` and
16
+ `Bot invocations` sections of the API reference, and the "Connect your local
17
+ agent" recipe.
18
+
19
+ ## Install
20
+
21
+ ```sh
22
+ npm install @threahq/bot-runtime-client socket.io-client
23
+ ```
24
+
25
+ `socket.io-client` is a peer dependency. Node 20+ or Bun.
26
+
27
+ ## Transport
28
+
29
+ ```ts
30
+ import { BotRuntimeTransport } from "@threahq/bot-runtime-client"
31
+
32
+ const transport = new BotRuntimeTransport({
33
+ baseUrl: "https://app.threa.io",
34
+ workspaceId: process.env.THREA_WORKSPACE_ID!,
35
+ apiKey: process.env.THREA_API_KEY!, // a threa_bk_ bot key
36
+ hello: {
37
+ instanceId: "my-laptop-1",
38
+ runtimeKind: "custom",
39
+ supportedCapabilities: ["mentionable"],
40
+ },
41
+ callbacks: {
42
+ onInvocationAvailable: () => drainClaims(),
43
+ onBootstrap: (snapshot) => console.log(snapshot.availableInvocations.length, "claimable"),
44
+ },
45
+ log: (line) => console.error(line),
46
+ })
47
+
48
+ await transport.connect()
49
+ await transport.updatePresence({
50
+ runtimeKind: "custom",
51
+ instanceId: "my-laptop-1",
52
+ status: "available",
53
+ acceptingInvocations: true,
54
+ capabilities: {},
55
+ })
56
+ ```
57
+
58
+ `connect()` resolves the workspace's WebSocket hint from
59
+ `GET /api/workspaces/:id/config`, opens the `/bot` namespace with the bot key,
60
+ and sends `bot:hello`. The hello ack is the bootstrap snapshot
61
+ (`availableInvocations`, `ownedClaims`); `onInvocationAvailable` fires when new
62
+ work is claimable. If the hint cannot be resolved, the transport stays HTTP
63
+ only and every write goes to the REST endpoint instead. The socket itself is
64
+ not required for correctness; it removes the polling.
65
+
66
+ Three writes are routed:
67
+
68
+ | Method | Socket event | HTTP fallback | Failure policy |
69
+ | ------------------------------- | ---------------------- | --------------------------------- | -------------------------------------------------------------------------------- |
70
+ | `updatePresence(body)` | `bot:presence:update` | `POST /bot-runtime/presence` | retry over HTTP on any missing ack |
71
+ | `renewClaim(id, token, ttl)` | `bot:invocation:renew` | `POST /bot-invocations/:id/renew` | retry over HTTP on any missing ack; `{ notFound: true }` means the claim is gone |
72
+ | `recordSteps(id, token, steps)` | `bot:invocation:steps` | `POST /bot-invocations/:id/steps` | best effort; a frame in flight is not re-sent |
73
+
74
+ Claiming, completing and failing an invocation are low-frequency writes and
75
+ stay on HTTP; `@threahq/remote-session` exposes them as `ThreaClient`.
76
+
77
+ Server-side rejections arrive as an ack with `ok: false` and a `code`
78
+ (`NOT_FOUND`, `FORBIDDEN`, `INVALID_PAYLOAD`, ...). The transport logs them
79
+ through `log` and does not throw; a caller that needs the result reads the
80
+ return value (`renewClaim`) or checks `socketConnected`.
81
+
82
+ ## Sealed turns
83
+
84
+ An owner can invite a bot into an end-to-end-encrypted scratchpad. The server
85
+ then delivers the claim with a `sealedContext` instead of plaintext, and every
86
+ reply and trace step must be ciphertext under the stream key. `E2eKeyring`
87
+ holds the identity keys an owner wraps a stream key to — one per host by
88
+ default, so every runtime on a box shares it — keeping them in the OS keychain
89
+ or a `0600` file, `openSealedTurnContext` decrypts a claim,
90
+ `sealReply` and `sealStep` encrypt what goes back, and `recordSealedSteps`
91
+ sends sealed frames over the same socket. The wire format is the one the
92
+ "Connect an encrypted agent" recipe describes; the SDK handles all of it for
93
+ connectors, so only a runtime that bypasses the SDK needs these directly.
94
+
95
+ ## Versioning
96
+
97
+ The package tracks the current public API version. Threa's REST API is dated
98
+ (see the versioning page in the developer docs) and additive within a version,
99
+ so a given release of this package keeps working against newer servers.
@@ -0,0 +1,112 @@
1
+ /**
2
+ * How long a runtime survives its scratchpad being archived before winding
3
+ * down. An unarchive inside this window reattaches the live agent in place.
4
+ * Shared: Claude (`@threahq/remote-session`) and Pi run separate session
5
+ * implementations, and a grace tuned on one must not diverge from the other.
6
+ */
7
+ export declare const ARCHIVE_RESTORE_GRACE_MS: number;
8
+ /** Reattach-probe cadence while detached. Bounded by the grace window, so it cannot become a quota burn. */
9
+ export declare const ARCHIVE_RESTORE_PROBE_MS = 45000;
10
+ /**
11
+ * Poll cadence while the `/bot` socket is up: pushes deliver work within a
12
+ * frame, so the poll is only a backstop for a dropped one. Shared because it
13
+ * is also the worst case for a runtime to notice an archive it was not pushed,
14
+ * which is what any external reaper has to wait out.
15
+ */
16
+ export declare const WS_BACKSTOP_POLL_MS: number;
17
+ /**
18
+ * The archive → grace → wind-down state machine, shared by every harness
19
+ * runtime.
20
+ *
21
+ * Archiving a scratchpad ends its session server-side, so the worktree behind
22
+ * it is finished — but archiving is also how a mis-click gets undone, so the
23
+ * wind-down (hand the worktree to harnessd, kill the tmux window) waits out a
24
+ * grace window that an unarchive can cancel.
25
+ *
26
+ * This is deliberately one implementation rather than one per runtime. Every
27
+ * bug this machine has produced came from the same shape: state read before an
28
+ * `await` and acted on after it, once the deadline had already fired or a
29
+ * second caller had won. The `pending` object is the identity token — every
30
+ * resumption re-checks `this.pending !== pending` and bails, which is what
31
+ * makes the wind-down terminal.
32
+ */
33
+ export interface ArchiveGraceHooks {
34
+ /**
35
+ * Server truth for the attached direction. `undefined` means "could not
36
+ * tell" (transient failure, missing scope, outage) and never detaches — a
37
+ * diagnostic that did not run is not evidence the scratchpad is archived.
38
+ */
39
+ isArchived(rootStreamId: string): Promise<boolean | undefined>;
40
+ /**
41
+ * Server truth for the detached direction: try to revive this runtime's link.
42
+ * `true` once reattached, `false` while the scratchpad is still archived.
43
+ * Throwing is treated as `false` — the probe cadence retries.
44
+ */
45
+ reattach(rootStreamId: string): Promise<boolean>;
46
+ /** Detach effects: go offline, suspend claiming, pull the poll onto {@link probeDelayMs}. */
47
+ onDetached(rootStreamId: string, graceMs: number): Promise<void> | void;
48
+ /** Reattach effects: back to available, resume claiming. */
49
+ onReattached(rootStreamId: string): Promise<void> | void;
50
+ /**
51
+ * Terminal. Hand the worktree to harnessd ({@link markHarnessLinkWoundDown})
52
+ * and take the window down; the runtime usually dies here. Preserving the
53
+ * branch and removing the worktree is harnessd's job, never a runtime's —
54
+ * only harnessd holds the lock a concurrent revive also takes.
55
+ */
56
+ onWindDown(rootStreamId: string): Promise<void> | void;
57
+ log(message: string): void;
58
+ }
59
+ export interface ArchiveGraceOptions {
60
+ /** Override the grace window. Tests use a few hundred ms; production takes the shared default. */
61
+ graceMs?: number;
62
+ }
63
+ export declare class ArchiveGraceController {
64
+ private readonly hooks;
65
+ private pending;
66
+ private probing;
67
+ private stopped;
68
+ private transitions;
69
+ private readonly graceMs;
70
+ constructor(hooks: ArchiveGraceHooks, options?: ArchiveGraceOptions);
71
+ /** Detached and waiting out the grace: claims must stay suspended while this is true. */
72
+ get detached(): boolean;
73
+ get pendingRootStreamId(): string | undefined;
74
+ /**
75
+ * Bumped on every attach/detach transition. A runtime with a request already
76
+ * in flight snapshots this before awaiting and drops its result if the value
77
+ * moved — otherwise a link created before the archive lands is committed
78
+ * after it, resurrecting a link to a scratchpad that is archived
79
+ * server-side.
80
+ */
81
+ get generation(): number;
82
+ /**
83
+ * Poll cadence while detached, scaled so several probes always fit inside the
84
+ * grace even when it is shortened for a test. Bounded by the window, so it
85
+ * cannot become a quota burn.
86
+ */
87
+ get probeDelayMs(): number;
88
+ /**
89
+ * This root is archived (a `bot:session_archived` push, or a probe that
90
+ * found `archivedAt`). Callers scope the event to their own runtime session
91
+ * and current root before calling — a stale event for a retired root must
92
+ * never wind down the scratchpad now linked.
93
+ */
94
+ archived(rootStreamId: string): Promise<void>;
95
+ /**
96
+ * The scratchpad came back (a `bot:session_restored` push). Revives the link
97
+ * and cancels the wind-down; a transient failure keeps the detached state so
98
+ * the probe cadence retries inside the remaining window.
99
+ */
100
+ restored(): Promise<void>;
101
+ /**
102
+ * The poll-tick backstop. `bot:session_archived` is a one-shot push with no
103
+ * replay, so a runtime whose socket was down when the archive landed would
104
+ * otherwise hold its worktree forever. Re-derives from the server and drives
105
+ * whichever direction applies.
106
+ */
107
+ probe(currentRootStreamId: string | undefined): Promise<void>;
108
+ /** Teardown. Disarms the deadline so a shutting-down runtime cannot wind down behind itself. */
109
+ stop(): void;
110
+ private attemptReattach;
111
+ private windDown;
112
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * One downloaded attachment's filename, stripped of path separators and
3
+ * Windows-hostile characters. An over-long name loses stem, never extension:
4
+ * the agent (and a `THREA_ATTACH:` re-upload) picks the mime type off the
5
+ * suffix, so a truncated `.png` would land as `application/octet-stream`.
6
+ */
7
+ export declare function safeAttachmentFilename(filename: string): string;
8
+ /**
9
+ * A downloaded attachment lands in a per-attachment-id subdirectory: filenames
10
+ * are not unique (the same `image.png` pasted into two messages, or one file
11
+ * carried by both the source and a context message), so a flat directory
12
+ * silently clobbers the earlier download. The leaf keeps the original filename
13
+ * so a re-upload round-trips the name and extension unchanged.
14
+ */
15
+ export declare function attachmentLocalPath(dir: string, attachmentId: string, filename: string): string;
package/crypto.d.ts ADDED
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Vendored subset of `@threahq/crypto` (the repo's `packages/crypto`).
3
+ *
4
+ * The bot-runtime extensions ship standalone — they are copied to the user's
5
+ * machine (e.g. `~/.pi/agent/extensions/`) and installed there, where the
6
+ * private, unpublished `@threahq/crypto` workspace package cannot resolve. So the
7
+ * slice the sealed (E2EE) bot path needs is copied here verbatim and depends
8
+ * only on the published `@hpke/*` packages plus WebCrypto. Both harnesses
9
+ * (pi-remote, claude-code-remote via remote-session) consume this one copy.
10
+ *
11
+ * Source of truth: `packages/crypto/src/{encoding,hpke,stream-key,envelope,sealed-payload,attachment}.ts`.
12
+ * `crypto.parity.test.ts` imports BOTH this module and the canonical package and
13
+ * asserts byte-for-byte agreement on the AAD builders, envelope/payload versions,
14
+ * and cross seal/open + wrap/unwrap round-trips — so a drift here fails loudly.
15
+ * Keep the two in sync; if the canonical AAD layout or envelope version changes,
16
+ * the owner's client can no longer open this harness's sealed replies.
17
+ *
18
+ * Only the recipient/seal half lives here: the harness unwraps the stream key
19
+ * with its identity private key, opens history/prompt, and seals replies/steps
20
+ * under the stream key. It never wraps a key to another recipient, so the HPKE
21
+ * `seal`/`wrapStreamKey` direction is deliberately omitted.
22
+ */
23
+ type CryptoKey = Awaited<ReturnType<typeof crypto.subtle.importKey>>;
24
+ type CryptoKeyPair = {
25
+ publicKey: CryptoKey;
26
+ privateKey: CryptoKey;
27
+ };
28
+ export type { CryptoKey as WebCryptoKey };
29
+ export declare function bytesToBase64(bytes: Uint8Array | ArrayBuffer): string;
30
+ export declare function base64ToBytes(b64: string): Uint8Array<ArrayBuffer>;
31
+ export declare function utf8Encode(text: string): Uint8Array<ArrayBuffer>;
32
+ export declare function utf8Decode(bytes: Uint8Array): string;
33
+ export declare function concatBytes(...parts: Uint8Array[]): Uint8Array<ArrayBuffer>;
34
+ export declare function generateKeyPair(): Promise<CryptoKeyPair>;
35
+ export declare function importRecipientPrivateKey(raw: Uint8Array | ArrayBuffer): Promise<CryptoKey>;
36
+ export declare function exportPublicKey(key: CryptoKey): Promise<Uint8Array<ArrayBuffer>>;
37
+ export declare function exportPrivateKey(key: CryptoKey): Promise<Uint8Array<ArrayBuffer>>;
38
+ export declare function importRecipientPublicKey(raw: Uint8Array | ArrayBuffer): Promise<CryptoKey>;
39
+ /** Stream-envelope version; a reader switches on `envelope.v`. */
40
+ export declare const STREAM_ENVELOPE_VERSION = 2;
41
+ export interface StreamEnvelope {
42
+ /** Always `STREAM_ENVELOPE_VERSION`; old clients reject an unknown version loudly. */
43
+ v: number;
44
+ /** Which generation of the stream's SSK sealed this message. */
45
+ keyGeneration: number;
46
+ /** Base64-encoded AES-GCM IV. */
47
+ iv: string;
48
+ /** Base64-encoded AAD (caller-supplied binding bytes — see `buildMessageAad`). */
49
+ aad: string;
50
+ }
51
+ export interface SealMessageInput {
52
+ /** 32-byte SSK for `keyGeneration`. */
53
+ key: Uint8Array;
54
+ keyGeneration: number;
55
+ payload: Uint8Array | string;
56
+ /** Bytes bound into AEAD as additional-authenticated-data — use `buildMessageAad`. Required. */
57
+ aad: Uint8Array;
58
+ }
59
+ export interface SealMessageResult {
60
+ envelope: StreamEnvelope;
61
+ /** AES-256-GCM ciphertext (tag included). */
62
+ ciphertext: Uint8Array<ArrayBuffer>;
63
+ }
64
+ /** AEAD-seal a message payload under the stream's SSK for `keyGeneration`. */
65
+ export declare function sealMessage(input: SealMessageInput): Promise<SealMessageResult>;
66
+ export interface OpenMessageInput {
67
+ /** 32-byte SSK for `envelope.keyGeneration`. */
68
+ key: Uint8Array;
69
+ envelope: StreamEnvelope;
70
+ /** AES-256-GCM ciphertext (tag included). */
71
+ ciphertext: Uint8Array;
72
+ }
73
+ /** Open an SSK-sealed message. Throws on version mismatch, wrong key, or forged AAD. */
74
+ export declare function openMessage(input: OpenMessageInput): Promise<Uint8Array<ArrayBuffer>>;
75
+ export declare function openMessageAsString(input: OpenMessageInput): Promise<string>;
76
+ export interface UnwrapStreamKeyInput {
77
+ enc: Uint8Array;
78
+ ct: Uint8Array;
79
+ /** The recipient's HPKE private key (the harness's BIK private key). */
80
+ recipientPrivateKey: CryptoKey;
81
+ /** Must match the `aad` used at wrap time (see `buildWrapAad`). */
82
+ aad: Uint8Array;
83
+ }
84
+ export interface WrapStreamKeyInput {
85
+ /** The 32-byte SSK to wrap. */
86
+ key: Uint8Array;
87
+ /** The recipient's HPKE public key (imported via `importRecipientPublicKey`). */
88
+ recipientPublicKey: CryptoKey;
89
+ /** Slot binding — use `buildWrapAad`. Required. */
90
+ aad: Uint8Array;
91
+ }
92
+ /**
93
+ * HPKE-wrap an SSK to a recipient — used when a harness PROVISIONS a fresh
94
+ * stream key for its own E2E scratchpad (wrapping to the owner's UIK and its
95
+ * own BIK). Wire-identical to `@threahq/crypto`'s `wrapStreamKey`; the parity
96
+ * test asserts a vendored wrap opens with the vendored unwrap under the same
97
+ * AAD binding.
98
+ */
99
+ export declare function wrapStreamKey(input: WrapStreamKeyInput): Promise<{
100
+ enc: Uint8Array;
101
+ ct: Uint8Array;
102
+ }>;
103
+ /** A fresh random 32-byte SSK (AES-256). */
104
+ export declare function generateStreamKey(): Uint8Array<ArrayBuffer>;
105
+ /** Recover the SSK from a wrap. Throws if the key doesn't match or AAD is forged. */
106
+ export declare function unwrapStreamKey(input: UnwrapStreamKeyInput): Promise<Uint8Array<ArrayBuffer>>;
107
+ /**
108
+ * Canonical AAD for an SSK wrap. Binds a wrap to its `(streamId, keyGeneration,
109
+ * recipientKeyId)` slot so a malicious server can't relocate a wrap row. Keep
110
+ * stable — changing the layout breaks unwrapping of every existing wrap.
111
+ */
112
+ export declare function buildWrapAad(parts: {
113
+ streamId: string;
114
+ keyGeneration: number;
115
+ recipientKeyId: string;
116
+ }): Uint8Array<ArrayBuffer>;
117
+ /**
118
+ * Canonical AAD for an SSK-sealed message (and trace step — the `step_…` id
119
+ * rides the `messageId` slot). Binds the ciphertext to `streamId|messageId|senderId`
120
+ * so the server can't shuffle it onto another row. Keep stable.
121
+ */
122
+ export declare function buildMessageAad(parts: {
123
+ streamId: string;
124
+ messageId: string;
125
+ senderId: string;
126
+ }): Uint8Array<ArrayBuffer>;
127
+ /**
128
+ * Canonical AAD for a sealed decision card and for the note a member attaches
129
+ * to their answer. Mirrors `buildDecisionAad` / `buildDecisionNoteAad` in
130
+ * `@threahq/crypto`; the label keeps the two apart, and both apart from a
131
+ * sealed message body. Keep stable.
132
+ */
133
+ export declare function buildDecisionAad(parts: {
134
+ streamId: string;
135
+ decisionId: string;
136
+ requesterBotId: string;
137
+ }): Uint8Array<ArrayBuffer>;
138
+ export declare function buildDecisionNoteAad(parts: {
139
+ streamId: string;
140
+ decisionId: string;
141
+ decidedBy: string;
142
+ }): Uint8Array<ArrayBuffer>;
143
+ export declare const ATTACHMENT_AAD: Uint8Array<ArrayBuffer>;
144
+ /** Single-key scheme: attachment keys are per-file, never rotated. */
145
+ export declare const ATTACHMENT_KEY_GENERATION = 0;
146
+ export interface EncryptedAttachment {
147
+ /** Ciphertext bytes to upload as the opaque file body (a valid `BlobPart`). */
148
+ ciphertext: Uint8Array<ArrayBuffer>;
149
+ /** Base64 key + iv to stash in the message's `attachmentRefs`. */
150
+ key: string;
151
+ iv: string;
152
+ }
153
+ /**
154
+ * Encrypt a file's bytes under a fresh single-use key for upload to an E2E
155
+ * stream. Returns the ciphertext plus the key/iv the message payload must carry
156
+ * so a recipient can decrypt it later. Reuses the message seal primitive
157
+ * (AES-256-GCM) rather than a parallel raw-bytes path (INV-35).
158
+ */
159
+ export declare function encryptAttachmentBytes(plaintext: Uint8Array): Promise<EncryptedAttachment>;
160
+ /**
161
+ * Decrypt the opaque S3 ciphertext of an E2E attachment back to its bytes, using
162
+ * the `key`/`iv` carried in the message's `attachmentRef`. Reconstructs the
163
+ * single-key envelope (gen 0, the domain-separation AAD) and opens it. Throws if
164
+ * the key/iv don't match or the bytes were tampered (AES-GCM tag check).
165
+ */
166
+ export declare function decryptAttachmentBytes(input: {
167
+ ciphertext: Uint8Array;
168
+ key: string;
169
+ iv: string;
170
+ }): Promise<Uint8Array<ArrayBuffer>>;
171
+ export declare const E2E_PAYLOAD_VERSION = 1;
172
+ /** One citation source sealed into a payload (structural twin of `@threahq/types`' `SourceItem`). */
173
+ export interface SealedSourceItem {
174
+ type?: string;
175
+ title: string;
176
+ url: string;
177
+ snippet?: string;
178
+ }
179
+ /** A per-file attachment key sealed into a payload (structural twin of `@threahq/crypto`'s `AttachmentRef`). */
180
+ export interface AttachmentRef {
181
+ attachmentId: string;
182
+ key: string;
183
+ iv: string;
184
+ filename: string;
185
+ mimeType: string;
186
+ sizeBytes: number;
187
+ }
188
+ export interface SealedPayloadExtras {
189
+ attachmentRefs?: AttachmentRef[];
190
+ sources?: SealedSourceItem[];
191
+ draftContentJson?: unknown;
192
+ }
193
+ /** Build the bytes to seal: bare markdown, or the versioned wrapper when an adjunct rides along. */
194
+ export declare function serializeSealedPayload(contentMarkdown: string, extras?: SealedPayloadExtras): string;
195
+ export interface ParsedSealedPayload {
196
+ contentMarkdown: string;
197
+ attachmentRefs: AttachmentRef[];
198
+ sources: SealedSourceItem[];
199
+ draftContentJson: unknown | null;
200
+ }
201
+ /**
202
+ * Inverse of `serializeSealedPayload`. A decrypted string is either the bare
203
+ * markdown body or the versioned wrapper; anything that doesn't parse as our
204
+ * wrapper is treated as raw markdown so older messages keep opening unchanged.
205
+ */
206
+ export declare function parseSealedPayload(raw: string): ParsedSealedPayload;
package/index.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ export { BotRuntimeTransport } from "./transport.js";
2
+ export type { InvocationCancellation, InvocationInputUpdate, ObserveClaimParams, ObservedClaimHandle, } from "./invocation-control.js";
3
+ export { ARCHIVE_RESTORE_GRACE_MS, ARCHIVE_RESTORE_PROBE_MS, ArchiveGraceController, WS_BACKSTOP_POLL_MS, type ArchiveGraceHooks, type ArchiveGraceOptions, } from "./archive-grace.js";
4
+ export { attachmentLocalPath, safeAttachmentFilename } from "./attachment-files.js";
5
+ export { parseWsHint, buildBotSocketUrl, isObject, type WsHint } from "./ws-hint.js";
6
+ export type { BotWriteAck, StepFrame, BotRuntimeHello, BotHelloBootstrap, BotE2eGrantPayload, BotE2eRevokePayload, BotRuntimeTransportCallbacks, BotRuntimeTransportOptions, DelegationAvailableNudge, BotDecisionPayload, CreateDecisionRequestBody, DecisionOption, DecisionRequest, DecisionRequestStatus, DecisionResolution, } from "./types.js";
7
+ export { BotKeyring, mintE2eKeyRecord, THREA_CALLBACK_TOKEN_HEADER, mintStreamKeyWraps, openSealedAck, openSealedDecisionNote, openSealedTurnContext, parseSealedAckContext, parseSealedTurnContext, scrubSealedError, sealDecision, sealReply, sealStep, } from "./sealed.js";
8
+ export type { BotIdentityKey, DecryptedHistoryItem, OpenedSealedTurn, ProvisionRecipient, ProvisionedWrap, SealedAckContext, SealedDecisionCard, SealedDecisionContent, SealedMessageWire, SealedReplyBody, SealedSskWrap, SealedStepFrame, SealedTurnContext, SealingState, } from "./sealed.js";
9
+ export { base64ToBytes, buildDecisionAad, buildDecisionNoteAad, buildMessageAad, buildWrapAad, bytesToBase64, decryptAttachmentBytes, encryptAttachmentBytes, openMessageAsString, parseSealedPayload, sealMessage, serializeSealedPayload, type AttachmentRef, type EncryptedAttachment, type SealedPayloadExtras, type SealedSourceItem, type StreamEnvelope, } from "./crypto.js";
10
+ export { SealedStreamClient, SealedStreamApiError, keyringKeySource } from "./sealed-stream-client.js";
11
+ export type { OpenedSealedBody, SealedKeyIdentity, SealedKeySource, SealedMessageBody, SealedStreamClientOptions, SealedStreamMessage, SealedStreamPage, } from "./sealed-stream-client.js";
12
+ export { E2E_KEY_SCOPES, E2E_KEY_STORE_KINDS, E2eKeyring, FileKeyStore, MacKeychainStore, SecretServiceStore, e2eKeyAccount, e2eStreamKeyAccount, e2eUserKeyAccount, readLegacyBikFile, resolveKeyStore, type CommandRunner, type E2eKeyRecord, type E2eKeyScope, type E2eKeyStore, type E2eKeyStoreKind, type E2eKeyringOptions, type HeldE2eKey, type ResolveKeyStoreInput, } from "./keyring.js";
13
+ export { deriveKEK, unwrapPrivate, unlockUserKey, WrongPassphraseError, DEFAULT_KDF_PARAMS } from "./user-key.js";
14
+ export type { KdfParams, UnlockUserKeyInput } from "./user-key.js";