@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 +21 -0
- package/README.md +99 -0
- package/archive-grace.d.ts +112 -0
- package/attachment-files.d.ts +15 -0
- package/crypto.d.ts +206 -0
- package/index.d.ts +14 -0
- package/index.js +2344 -0
- package/index.js.map +19 -0
- package/invocation-control.d.ts +128 -0
- package/keyring.d.ts +242 -0
- package/package.json +49 -0
- package/sealed-stream-client.d.ts +145 -0
- package/sealed.d.ts +305 -0
- package/transport-test-helpers.d.ts +53 -0
- package/transport.d.ts +132 -0
- package/types.d.ts +243 -0
- package/user-key.d.ts +53 -0
- package/ws-hint.d.ts +17 -0
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";
|