@stagewhisper/stagewhisper 0.51.0 → 0.53.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/dist/index.js +5246 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +12 -16
- package/api.ts +0 -4
- package/index.ts +0 -54
- package/openresponses.d.ts +0 -1361
- package/plugin-main.ts +0 -356
- package/src/channel.test.ts +0 -201
- package/src/channel.ts +0 -182
- package/src/client.ts +0 -191
- package/src/crypto.test.ts +0 -181
- package/src/crypto.ts +0 -203
- package/src/health.test.ts +0 -101
- package/src/health.ts +0 -94
- package/src/openresponses.ts +0 -107
- package/src/reasoning.test.ts +0 -116
- package/src/reasoning.ts +0 -210
- package/src/runtime.ts +0 -6
- package/src/service.ts +0 -952
package/src/channel.ts
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createChatChannelPlugin,
|
|
3
|
-
createChannelPluginBase,
|
|
4
|
-
DEFAULT_ACCOUNT_ID,
|
|
5
|
-
} from "openclaw/plugin-sdk/core";
|
|
6
|
-
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
7
|
-
|
|
8
|
-
export type StageWhisperAccount = {
|
|
9
|
-
accountId: string | null;
|
|
10
|
-
apiBaseUrl: string;
|
|
11
|
-
integrationId: string;
|
|
12
|
-
relayToken: string;
|
|
13
|
-
label: string;
|
|
14
|
-
pluginSecretKeyB64?: string;
|
|
15
|
-
desktopPublicKeyB64?: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
function getChannelSection(cfg: OpenClawConfig): Record<string, unknown> {
|
|
19
|
-
const channels = cfg.channels as
|
|
20
|
-
| Record<string, Record<string, unknown>>
|
|
21
|
-
| undefined;
|
|
22
|
-
return channels?.["stagewhisper"] ?? {};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function resolveAccount(
|
|
26
|
-
cfg: OpenClawConfig,
|
|
27
|
-
accountId?: string | null,
|
|
28
|
-
): StageWhisperAccount {
|
|
29
|
-
const section = getChannelSection(cfg);
|
|
30
|
-
const pluginCfg = (cfg as Record<string, unknown>)?.["plugins"] as
|
|
31
|
-
| Record<string, unknown>
|
|
32
|
-
| undefined;
|
|
33
|
-
const entries = pluginCfg?.["entries"] as
|
|
34
|
-
| Record<string, Record<string, unknown>>
|
|
35
|
-
| undefined;
|
|
36
|
-
const swConfig = entries?.["stagewhisper"]?.["config"] as
|
|
37
|
-
| Record<string, unknown>
|
|
38
|
-
| undefined;
|
|
39
|
-
|
|
40
|
-
const apiBaseUrl =
|
|
41
|
-
(section["apiBaseUrl"] as string) ||
|
|
42
|
-
(swConfig?.["apiBaseUrl"] as string) ||
|
|
43
|
-
"https://api.stagewhisper.io";
|
|
44
|
-
const integrationId =
|
|
45
|
-
(section["integrationId"] as string) ??
|
|
46
|
-
(swConfig?.["integrationId"] as string) ??
|
|
47
|
-
"";
|
|
48
|
-
const relayToken =
|
|
49
|
-
(section["relayToken"] as string) ??
|
|
50
|
-
(swConfig?.["relayToken"] as string) ??
|
|
51
|
-
"";
|
|
52
|
-
const label =
|
|
53
|
-
(section["label"] as string) ??
|
|
54
|
-
(swConfig?.["label"] as string) ??
|
|
55
|
-
"StageWhisper";
|
|
56
|
-
|
|
57
|
-
if (!apiBaseUrl) throw new Error("stagewhisper: apiBaseUrl is required");
|
|
58
|
-
if (!integrationId)
|
|
59
|
-
throw new Error("stagewhisper: integrationId is required");
|
|
60
|
-
if (!relayToken) throw new Error("stagewhisper: relayToken is required");
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
accountId: accountId ?? null,
|
|
64
|
-
apiBaseUrl,
|
|
65
|
-
integrationId,
|
|
66
|
-
relayToken,
|
|
67
|
-
label,
|
|
68
|
-
pluginSecretKeyB64: (section["pluginSecretKey"] as string) ?? (swConfig?.["pluginSecretKey"] as string) ?? undefined,
|
|
69
|
-
desktopPublicKeyB64: (section["desktopPublicKey"] as string) ?? (swConfig?.["desktopPublicKey"] as string) ?? undefined,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const base = createChannelPluginBase<StageWhisperAccount>({
|
|
74
|
-
id: "stagewhisper",
|
|
75
|
-
capabilities: {
|
|
76
|
-
chatTypes: ["direct"],
|
|
77
|
-
},
|
|
78
|
-
config: {
|
|
79
|
-
listAccountIds: (cfg: OpenClawConfig) => {
|
|
80
|
-
try {
|
|
81
|
-
resolveAccount(cfg);
|
|
82
|
-
return [DEFAULT_ACCOUNT_ID];
|
|
83
|
-
} catch {
|
|
84
|
-
return [];
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
resolveAccount,
|
|
88
|
-
inspectAccount(cfg: OpenClawConfig) {
|
|
89
|
-
try {
|
|
90
|
-
const account = resolveAccount(cfg);
|
|
91
|
-
return {
|
|
92
|
-
enabled: true,
|
|
93
|
-
configured: true,
|
|
94
|
-
label: account.label,
|
|
95
|
-
apiBaseUrl: account.apiBaseUrl,
|
|
96
|
-
};
|
|
97
|
-
} catch {
|
|
98
|
-
return {
|
|
99
|
-
enabled: false,
|
|
100
|
-
configured: false,
|
|
101
|
-
tokenStatus: "missing",
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
setup: {
|
|
107
|
-
applyAccountConfig({ cfg, input }) {
|
|
108
|
-
const channels = (cfg.channels ?? {}) as Record<
|
|
109
|
-
string,
|
|
110
|
-
Record<string, unknown>
|
|
111
|
-
>;
|
|
112
|
-
const section = { ...(channels["stagewhisper"] ?? {}) };
|
|
113
|
-
const raw = input as unknown as Record<string, string>;
|
|
114
|
-
|
|
115
|
-
if (raw["apiBaseUrl"]) section["apiBaseUrl"] = raw["apiBaseUrl"];
|
|
116
|
-
if (raw["integrationId"])
|
|
117
|
-
section["integrationId"] = raw["integrationId"];
|
|
118
|
-
if (raw["relayToken"]) section["relayToken"] = raw["relayToken"];
|
|
119
|
-
if (raw["label"]) section["label"] = raw["label"];
|
|
120
|
-
|
|
121
|
-
return {
|
|
122
|
-
...cfg,
|
|
123
|
-
channels: { ...channels, stagewhisper: section },
|
|
124
|
-
} as OpenClawConfig;
|
|
125
|
-
},
|
|
126
|
-
},
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
export const stagewhisperPlugin = createChatChannelPlugin<StageWhisperAccount>(
|
|
130
|
-
{
|
|
131
|
-
base: base as unknown as Parameters<typeof createChatChannelPlugin<StageWhisperAccount>>[0]["base"],
|
|
132
|
-
|
|
133
|
-
security: {
|
|
134
|
-
dm: {
|
|
135
|
-
channelKey: "stagewhisper",
|
|
136
|
-
resolvePolicy: () => "closed",
|
|
137
|
-
resolveAllowFrom: () => [],
|
|
138
|
-
defaultPolicy: "closed",
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
|
|
142
|
-
pairing: {
|
|
143
|
-
text: {
|
|
144
|
-
idLabel: "StageWhisper pairing code",
|
|
145
|
-
message: "Run this command to complete pairing:",
|
|
146
|
-
notify: async () => {},
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
|
|
150
|
-
threading: { topLevelReplyToMode: "reply" },
|
|
151
|
-
|
|
152
|
-
outbound: {
|
|
153
|
-
base: {
|
|
154
|
-
deliveryMode: "direct",
|
|
155
|
-
resolveTarget: (params: {
|
|
156
|
-
cfg?: OpenClawConfig;
|
|
157
|
-
to?: string;
|
|
158
|
-
allowFrom?: string[];
|
|
159
|
-
accountId?: string | null;
|
|
160
|
-
mode?: string;
|
|
161
|
-
}) => {
|
|
162
|
-
if (!params.to) return { ok: false as const, error: new Error("No delivery target") };
|
|
163
|
-
return { ok: true as const, to: params.to };
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
|
-
attachedResults: {
|
|
167
|
-
channel: "stagewhisper",
|
|
168
|
-
sendText: async (ctx) => {
|
|
169
|
-
const target = (ctx as Record<string, unknown>).to as string | undefined ?? "";
|
|
170
|
-
if (target.startsWith("sw-session-")) {
|
|
171
|
-
return { messageId: `sw-relay-ack-${Date.now()}`, ok: true };
|
|
172
|
-
}
|
|
173
|
-
console.warn(
|
|
174
|
-
`[stagewhisper] sendText called for unrecognised target "${target}"; ` +
|
|
175
|
-
`StageWhisper channel is inbound-only — task replies are routed by the relay service`,
|
|
176
|
-
);
|
|
177
|
-
return { messageId: `sw-dropped-${Date.now()}`, ok: false };
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
);
|
package/src/client.ts
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import type { IdentityKeypair } from "./crypto.js";
|
|
2
|
-
|
|
3
|
-
export type TaskPayload = {
|
|
4
|
-
id: string;
|
|
5
|
-
session_id: string;
|
|
6
|
-
title: string;
|
|
7
|
-
request_text: string;
|
|
8
|
-
action_type: string;
|
|
9
|
-
status: string;
|
|
10
|
-
evidence_payload: Record<string, unknown> | null;
|
|
11
|
-
created_at: string;
|
|
12
|
-
is_byo_encrypted?: boolean;
|
|
13
|
-
encrypted_title?: string;
|
|
14
|
-
encrypted_request?: string;
|
|
15
|
-
encrypted_evidence?: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export type PairCompleteResponse = {
|
|
19
|
-
integration_id: string;
|
|
20
|
-
relay_token: string;
|
|
21
|
-
label: string;
|
|
22
|
-
byo_public_key?: string;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export type HeartbeatResponse = {
|
|
26
|
-
ok: boolean;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export class StageWhisperClient {
|
|
30
|
-
private baseUrl: string;
|
|
31
|
-
private integrationId: string;
|
|
32
|
-
private relayToken: string;
|
|
33
|
-
private pluginKeypair_: IdentityKeypair | null = null;
|
|
34
|
-
private desktopPublicKey_: Uint8Array | null = null;
|
|
35
|
-
|
|
36
|
-
constructor(baseUrl: string, integrationId: string, relayToken: string) {
|
|
37
|
-
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
38
|
-
this.integrationId = integrationId;
|
|
39
|
-
this.relayToken = relayToken;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
setPluginKeypair(kp: IdentityKeypair): void {
|
|
43
|
-
this.pluginKeypair_ = kp;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
setDesktopPublicKey(key: Uint8Array): void {
|
|
47
|
-
this.desktopPublicKey_ = key;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
getPluginKeypair(): IdentityKeypair | null {
|
|
51
|
-
return this.pluginKeypair_;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
getDesktopPublicKey(): Uint8Array | null {
|
|
55
|
-
return this.desktopPublicKey_;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
get pluginKeypair(): IdentityKeypair | null {
|
|
59
|
-
return this.pluginKeypair_;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
get desktopPublicKey(): Uint8Array | null {
|
|
63
|
-
return this.desktopPublicKey_;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
private headers(): Record<string, string> {
|
|
67
|
-
return {
|
|
68
|
-
Authorization: `Bearer ${this.relayToken}`,
|
|
69
|
-
"Content-Type": "application/json",
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async completePairing(
|
|
74
|
-
pairingCode: string,
|
|
75
|
-
hostLabel: string,
|
|
76
|
-
pluginPublicKey?: string,
|
|
77
|
-
): Promise<PairCompleteResponse> {
|
|
78
|
-
const body: Record<string, unknown> = {
|
|
79
|
-
pairing_code: pairingCode,
|
|
80
|
-
host_label: hostLabel,
|
|
81
|
-
};
|
|
82
|
-
if (pluginPublicKey) {
|
|
83
|
-
body.plugin_public_key = pluginPublicKey;
|
|
84
|
-
}
|
|
85
|
-
const res = await fetch(`${this.baseUrl}/api/v1/openclaw/pair/complete`, {
|
|
86
|
-
method: "POST",
|
|
87
|
-
headers: { "Content-Type": "application/json" },
|
|
88
|
-
body: JSON.stringify(body),
|
|
89
|
-
});
|
|
90
|
-
if (!res.ok) {
|
|
91
|
-
const text = await res.text();
|
|
92
|
-
throw new Error(`Pairing failed (${res.status}): ${text}`);
|
|
93
|
-
}
|
|
94
|
-
return res.json();
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async updateTaskStatus(taskId: string, status: string, remoteTaskId?: string): Promise<void> {
|
|
98
|
-
const body: Record<string, unknown> = { status };
|
|
99
|
-
if (remoteTaskId) body.remote_task_id = remoteTaskId;
|
|
100
|
-
|
|
101
|
-
const res = await fetch(`${this.baseUrl}/api/v1/openclaw/tasks/${taskId}/status`, {
|
|
102
|
-
method: "POST",
|
|
103
|
-
headers: this.headers(),
|
|
104
|
-
body: JSON.stringify(body),
|
|
105
|
-
});
|
|
106
|
-
if (!res.ok) {
|
|
107
|
-
const text = await res.text();
|
|
108
|
-
throw new Error(`Status update failed (${res.status}): ${text}`);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async postReply(
|
|
113
|
-
taskId: string,
|
|
114
|
-
content: string,
|
|
115
|
-
remoteMessageId?: string,
|
|
116
|
-
encryptedSummary?: string,
|
|
117
|
-
): Promise<void> {
|
|
118
|
-
const body: Record<string, unknown> = { content };
|
|
119
|
-
if (remoteMessageId) body.remote_message_id = remoteMessageId;
|
|
120
|
-
if (encryptedSummary) body.encrypted_summary = encryptedSummary;
|
|
121
|
-
|
|
122
|
-
const res = await fetch(`${this.baseUrl}/api/v1/openclaw/tasks/${taskId}/reply`, {
|
|
123
|
-
method: "POST",
|
|
124
|
-
headers: this.headers(),
|
|
125
|
-
body: JSON.stringify(body),
|
|
126
|
-
});
|
|
127
|
-
if (!res.ok) {
|
|
128
|
-
const text = await res.text();
|
|
129
|
-
throw new Error(`Reply failed (${res.status}): ${text}`);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async heartbeat(capabilities?: Record<string, unknown>): Promise<HeartbeatResponse> {
|
|
134
|
-
const res = await fetch(
|
|
135
|
-
`${this.baseUrl}/api/v1/openclaw/integrations/${this.integrationId}/heartbeat`,
|
|
136
|
-
{
|
|
137
|
-
method: "POST",
|
|
138
|
-
headers: this.headers(),
|
|
139
|
-
body: capabilities ? JSON.stringify(capabilities) : undefined,
|
|
140
|
-
},
|
|
141
|
-
);
|
|
142
|
-
if (!res.ok) {
|
|
143
|
-
const text = await res.text();
|
|
144
|
-
throw new Error(`Heartbeat failed (${res.status}): ${text}`);
|
|
145
|
-
}
|
|
146
|
-
return res.json();
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async postReasoningResult(
|
|
150
|
-
jobId: string,
|
|
151
|
-
result: Record<string, unknown>,
|
|
152
|
-
correlationId?: string,
|
|
153
|
-
): Promise<void> {
|
|
154
|
-
const headers = this.headers();
|
|
155
|
-
if (correlationId) {
|
|
156
|
-
headers["X-Correlation-ID"] = correlationId;
|
|
157
|
-
}
|
|
158
|
-
const res = await fetch(`${this.baseUrl}/api/v1/openclaw/reasoning-jobs/${jobId}/complete`, {
|
|
159
|
-
method: "POST",
|
|
160
|
-
headers,
|
|
161
|
-
body: JSON.stringify(result),
|
|
162
|
-
});
|
|
163
|
-
if (!res.ok) {
|
|
164
|
-
const text = await res.text();
|
|
165
|
-
throw new Error(`Reasoning result post failed (${res.status}): ${text}`);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
streamUrl(): string {
|
|
170
|
-
return `${this.baseUrl}/api/v1/openclaw/integrations/${this.integrationId}/stream`;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
streamHeaders(): Record<string, string> {
|
|
174
|
-
return { Authorization: `Bearer ${this.relayToken}` };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async testReply(testId: string, content: string): Promise<void> {
|
|
178
|
-
const res = await fetch(
|
|
179
|
-
`${this.baseUrl}/api/v1/openclaw/integrations/${this.integrationId}/test-reply`,
|
|
180
|
-
{
|
|
181
|
-
method: "POST",
|
|
182
|
-
headers: this.headers(),
|
|
183
|
-
body: JSON.stringify({ test_id: testId, content }),
|
|
184
|
-
},
|
|
185
|
-
);
|
|
186
|
-
if (!res.ok) {
|
|
187
|
-
const text = await res.text();
|
|
188
|
-
throw new Error(`Test reply failed (${res.status}): ${text}`);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
package/src/crypto.test.ts
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
IdentityKeypair,
|
|
4
|
-
seal,
|
|
5
|
-
open,
|
|
6
|
-
sealJson,
|
|
7
|
-
openJson,
|
|
8
|
-
openChecked,
|
|
9
|
-
ReplayGuard,
|
|
10
|
-
ENVELOPE_VERSION,
|
|
11
|
-
type BYOEnvelope,
|
|
12
|
-
} from "./crypto";
|
|
13
|
-
|
|
14
|
-
function makeTestKey(): { key: Uint8Array; alice: IdentityKeypair; bob: IdentityKeypair } {
|
|
15
|
-
const alice = IdentityKeypair.generate();
|
|
16
|
-
const bob = IdentityKeypair.generate();
|
|
17
|
-
const key = alice.deriveEnvelopeKey(bob.publicKey);
|
|
18
|
-
return { key, alice, bob };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe("crypto envelope", () => {
|
|
22
|
-
it("seal and open roundtrip", () => {
|
|
23
|
-
const { key } = makeTestKey();
|
|
24
|
-
const plaintext = new TextEncoder().encode("hello world");
|
|
25
|
-
|
|
26
|
-
const envelope = seal(key, "desktop", "session-1", "corr-1", "reasoning_input", plaintext);
|
|
27
|
-
|
|
28
|
-
expect(envelope.version).toBe(ENVELOPE_VERSION);
|
|
29
|
-
expect(envelope.sender_role).toBe("desktop");
|
|
30
|
-
|
|
31
|
-
const decrypted = open(key, envelope);
|
|
32
|
-
expect(decrypted).toEqual(plaintext);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("wrong key fails", () => {
|
|
36
|
-
const { key: key1 } = makeTestKey();
|
|
37
|
-
const { key: key2 } = makeTestKey();
|
|
38
|
-
|
|
39
|
-
const envelope = seal(key1, "plugin", "s", "c", "reasoning_output", new TextEncoder().encode("secret"));
|
|
40
|
-
|
|
41
|
-
expect(() => open(key2, envelope)).toThrow();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("tampered ciphertext fails", () => {
|
|
45
|
-
const { key } = makeTestKey();
|
|
46
|
-
const envelope = seal(key, "desktop", "s", "c", "reasoning_input", new TextEncoder().encode("data"));
|
|
47
|
-
|
|
48
|
-
const bytes = Uint8Array.from(atob(envelope.ciphertext), (c) => c.codePointAt(0)!);
|
|
49
|
-
bytes[0] ^= 0xff;
|
|
50
|
-
const tampered: BYOEnvelope = {
|
|
51
|
-
...envelope,
|
|
52
|
-
ciphertext: btoa(Array.from(bytes, (b) => String.fromCodePoint(b)).join("")),
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
expect(() => open(key, tampered)).toThrow();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("tampered session_id fails", () => {
|
|
59
|
-
const { key } = makeTestKey();
|
|
60
|
-
const envelope = seal(key, "desktop", "session-1", "corr-1", "reasoning_input", new TextEncoder().encode("data"));
|
|
61
|
-
|
|
62
|
-
const tampered: BYOEnvelope = { ...envelope, session_id: "TAMPERED" };
|
|
63
|
-
expect(() => open(key, tampered)).toThrow();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("tampered sender_role fails", () => {
|
|
67
|
-
const { key } = makeTestKey();
|
|
68
|
-
const envelope = seal(key, "desktop", "s", "c", "reasoning_input", new TextEncoder().encode("data"));
|
|
69
|
-
|
|
70
|
-
const tampered: BYOEnvelope = { ...envelope, sender_role: "plugin" };
|
|
71
|
-
expect(() => open(key, tampered)).toThrow();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("tampered content_type fails", () => {
|
|
75
|
-
const { key } = makeTestKey();
|
|
76
|
-
const envelope = seal(key, "desktop", "s", "c", "reasoning_input", new TextEncoder().encode("data"));
|
|
77
|
-
|
|
78
|
-
const tampered: BYOEnvelope = { ...envelope, content_type: "task_content" };
|
|
79
|
-
expect(() => open(key, tampered)).toThrow();
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("unique message_ids", () => {
|
|
83
|
-
const { key } = makeTestKey();
|
|
84
|
-
const e1 = seal(key, "desktop", "s", "c", "reasoning_input", new TextEncoder().encode("a"));
|
|
85
|
-
const e2 = seal(key, "desktop", "s", "c", "reasoning_input", new TextEncoder().encode("a"));
|
|
86
|
-
expect(e1.message_id).not.toBe(e2.message_id);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("unique nonces", () => {
|
|
90
|
-
const { key } = makeTestKey();
|
|
91
|
-
const e1 = seal(key, "desktop", "s", "c", "reasoning_input", new TextEncoder().encode("a"));
|
|
92
|
-
const e2 = seal(key, "desktop", "s", "c", "reasoning_input", new TextEncoder().encode("a"));
|
|
93
|
-
expect(e1.nonce).not.toBe(e2.nonce);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("json roundtrip", () => {
|
|
97
|
-
const { key } = makeTestKey();
|
|
98
|
-
const payload = {
|
|
99
|
-
system_instruction: "You are an analyst",
|
|
100
|
-
user_prompt: "Analyze the transcript",
|
|
101
|
-
response_schema: { type: "object" },
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const envelope = sealJson(key, "desktop", "session-1", "corr-1", "reasoning_input", payload);
|
|
105
|
-
const decrypted = openJson(key, envelope);
|
|
106
|
-
expect(decrypted).toEqual(payload);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("serialization roundtrip", () => {
|
|
110
|
-
const { key } = makeTestKey();
|
|
111
|
-
const envelope = seal(key, "desktop", "session-1", "corr-1", "reasoning_input", new TextEncoder().encode("test payload"));
|
|
112
|
-
|
|
113
|
-
const json = JSON.stringify(envelope);
|
|
114
|
-
const restored: BYOEnvelope = JSON.parse(json);
|
|
115
|
-
|
|
116
|
-
const decrypted = open(key, restored);
|
|
117
|
-
expect(new TextDecoder().decode(decrypted)).toBe("test payload");
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("unsupported version rejected", () => {
|
|
121
|
-
const { key } = makeTestKey();
|
|
122
|
-
const envelope = seal(key, "desktop", "s", "c", "reasoning_input", new TextEncoder().encode("data"));
|
|
123
|
-
const bad: BYOEnvelope = { ...envelope, version: "static_v99" };
|
|
124
|
-
|
|
125
|
-
expect(() => open(key, bad)).toThrow(/Unsupported envelope version/);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe("replay guard", () => {
|
|
130
|
-
it("detects duplicate message", () => {
|
|
131
|
-
const { key } = makeTestKey();
|
|
132
|
-
const envelope = seal(key, "desktop", "s", "c", "reasoning_input", new TextEncoder().encode("data"));
|
|
133
|
-
|
|
134
|
-
const guard = new ReplayGuard();
|
|
135
|
-
expect(() => openChecked(key, envelope, guard)).not.toThrow();
|
|
136
|
-
expect(() => openChecked(key, envelope, guard)).toThrow(/Replay detected/);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("allows different messages", () => {
|
|
140
|
-
const { key } = makeTestKey();
|
|
141
|
-
const e1 = seal(key, "desktop", "s", "c", "reasoning_input", new TextEncoder().encode("a"));
|
|
142
|
-
const e2 = seal(key, "desktop", "s", "c", "reasoning_input", new TextEncoder().encode("b"));
|
|
143
|
-
|
|
144
|
-
const guard = new ReplayGuard();
|
|
145
|
-
expect(() => openChecked(key, e1, guard)).not.toThrow();
|
|
146
|
-
expect(() => openChecked(key, e2, guard)).not.toThrow();
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
describe("identity keypair", () => {
|
|
151
|
-
it("generate and DH agreement", () => {
|
|
152
|
-
const alice = IdentityKeypair.generate();
|
|
153
|
-
const bob = IdentityKeypair.generate();
|
|
154
|
-
|
|
155
|
-
const sharedA = alice.diffieHellman(bob.publicKey);
|
|
156
|
-
const sharedB = bob.diffieHellman(alice.publicKey);
|
|
157
|
-
expect(sharedA).toEqual(sharedB);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("derived envelope keys match", () => {
|
|
161
|
-
const alice = IdentityKeypair.generate();
|
|
162
|
-
const bob = IdentityKeypair.generate();
|
|
163
|
-
|
|
164
|
-
const keyA = alice.deriveEnvelopeKey(bob.publicKey);
|
|
165
|
-
const keyB = bob.deriveEnvelopeKey(alice.publicKey);
|
|
166
|
-
expect(keyA).toEqual(keyB);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it("base64 roundtrip", () => {
|
|
170
|
-
const kp = IdentityKeypair.generate();
|
|
171
|
-
const b64 = kp.publicKeyBase64();
|
|
172
|
-
const restored = IdentityKeypair.publicKeyFromBase64(b64);
|
|
173
|
-
expect(restored).toEqual(kp.publicKey);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("from secret bytes roundtrip", () => {
|
|
177
|
-
const original = IdentityKeypair.generate();
|
|
178
|
-
const restored = IdentityKeypair.fromSecretBytes(original.secretBytes());
|
|
179
|
-
expect(restored.publicKey).toEqual(original.publicKey);
|
|
180
|
-
});
|
|
181
|
-
});
|