@zuoyehaoduoa/wire 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/README.md +668 -0
- package/dist/index.cjs +880 -0
- package/dist/index.d.cts +2500 -0
- package/dist/index.d.mts +2500 -0
- package/dist/index.mjs +796 -0
- package/package.json +62 -0
- package/src/index.ts +5 -0
- package/src/messageMeta.ts +24 -0
- package/src/messages.test.ts +165 -0
- package/src/messages.ts +97 -0
- package/src/remote-auth-errors.ts +87 -0
- package/src/remote-crypto.test.ts +244 -0
- package/src/remote-crypto.ts +778 -0
- package/src/remote-protocol.test.ts +61 -0
- package/src/remote-protocol.ts +53 -0
- package/src/sessionProtocol.test.ts +170 -0
- package/src/sessionProtocol.ts +141 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
import { isCuid, createId } from '@paralleldrive/cuid2';
|
|
3
|
+
import * as opaque from '@serenity-kit/opaque';
|
|
4
|
+
import tweetnacl from 'tweetnacl';
|
|
5
|
+
|
|
6
|
+
const sessionRoleSchema = z.enum(["user", "agent"]);
|
|
7
|
+
const sessionTextEventSchema = z.object({
|
|
8
|
+
t: z.literal("text"),
|
|
9
|
+
text: z.string(),
|
|
10
|
+
thinking: z.boolean().optional()
|
|
11
|
+
});
|
|
12
|
+
const sessionServiceMessageEventSchema = z.object({
|
|
13
|
+
t: z.literal("service"),
|
|
14
|
+
text: z.string()
|
|
15
|
+
});
|
|
16
|
+
const sessionToolCallStartEventSchema = z.object({
|
|
17
|
+
t: z.literal("tool-call-start"),
|
|
18
|
+
call: z.string(),
|
|
19
|
+
name: z.string(),
|
|
20
|
+
title: z.string(),
|
|
21
|
+
description: z.string(),
|
|
22
|
+
args: z.record(z.string(), z.unknown())
|
|
23
|
+
});
|
|
24
|
+
const sessionToolCallEndEventSchema = z.object({
|
|
25
|
+
t: z.literal("tool-call-end"),
|
|
26
|
+
call: z.string()
|
|
27
|
+
});
|
|
28
|
+
const sessionFileEventSchema = z.object({
|
|
29
|
+
t: z.literal("file"),
|
|
30
|
+
ref: z.string(),
|
|
31
|
+
name: z.string(),
|
|
32
|
+
size: z.number(),
|
|
33
|
+
image: z.object({
|
|
34
|
+
width: z.number(),
|
|
35
|
+
height: z.number(),
|
|
36
|
+
thumbhash: z.string()
|
|
37
|
+
}).optional()
|
|
38
|
+
});
|
|
39
|
+
const sessionTurnStartEventSchema = z.object({
|
|
40
|
+
t: z.literal("turn-start")
|
|
41
|
+
});
|
|
42
|
+
const sessionStartEventSchema = z.object({
|
|
43
|
+
t: z.literal("start"),
|
|
44
|
+
title: z.string().optional()
|
|
45
|
+
});
|
|
46
|
+
const sessionTurnEndStatusSchema = z.enum([
|
|
47
|
+
"completed",
|
|
48
|
+
"failed",
|
|
49
|
+
"cancelled"
|
|
50
|
+
]);
|
|
51
|
+
const sessionTurnEndEventSchema = z.object({
|
|
52
|
+
t: z.literal("turn-end"),
|
|
53
|
+
status: sessionTurnEndStatusSchema
|
|
54
|
+
});
|
|
55
|
+
const sessionStopEventSchema = z.object({
|
|
56
|
+
t: z.literal("stop")
|
|
57
|
+
});
|
|
58
|
+
const sessionEventSchema = z.discriminatedUnion("t", [
|
|
59
|
+
sessionTextEventSchema,
|
|
60
|
+
sessionServiceMessageEventSchema,
|
|
61
|
+
sessionToolCallStartEventSchema,
|
|
62
|
+
sessionToolCallEndEventSchema,
|
|
63
|
+
sessionFileEventSchema,
|
|
64
|
+
sessionTurnStartEventSchema,
|
|
65
|
+
sessionStartEventSchema,
|
|
66
|
+
sessionTurnEndEventSchema,
|
|
67
|
+
sessionStopEventSchema
|
|
68
|
+
]);
|
|
69
|
+
const sessionEnvelopeSchema = z.object({
|
|
70
|
+
id: z.string(),
|
|
71
|
+
time: z.number(),
|
|
72
|
+
role: sessionRoleSchema,
|
|
73
|
+
turn: z.string().optional(),
|
|
74
|
+
subagent: z.string().refine((value) => isCuid(value), {
|
|
75
|
+
message: "subagent must be a cuid2 value"
|
|
76
|
+
}).optional(),
|
|
77
|
+
ev: sessionEventSchema
|
|
78
|
+
}).superRefine((envelope, ctx) => {
|
|
79
|
+
if (envelope.ev.t === "service" && envelope.role !== "agent") {
|
|
80
|
+
ctx.addIssue({
|
|
81
|
+
code: z.ZodIssueCode.custom,
|
|
82
|
+
message: 'service events must use role "agent"',
|
|
83
|
+
path: ["role"]
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if ((envelope.ev.t === "start" || envelope.ev.t === "stop") && envelope.role !== "agent") {
|
|
87
|
+
ctx.addIssue({
|
|
88
|
+
code: z.ZodIssueCode.custom,
|
|
89
|
+
message: `${envelope.ev.t} events must use role "agent"`,
|
|
90
|
+
path: ["role"]
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
function createEnvelope(role, ev, opts = {}) {
|
|
95
|
+
return sessionEnvelopeSchema.parse({
|
|
96
|
+
id: opts.id ?? createId(),
|
|
97
|
+
time: opts.time ?? Date.now(),
|
|
98
|
+
role,
|
|
99
|
+
...opts.turn ? { turn: opts.turn } : {},
|
|
100
|
+
...opts.subagent ? { subagent: opts.subagent } : {},
|
|
101
|
+
ev
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const MessageMetaSchema = z.object({
|
|
106
|
+
sentFrom: z.string().optional(),
|
|
107
|
+
permissionMode: z.enum([
|
|
108
|
+
"default",
|
|
109
|
+
"acceptEdits",
|
|
110
|
+
"bypassPermissions",
|
|
111
|
+
"plan",
|
|
112
|
+
"read-only",
|
|
113
|
+
"safe-yolo",
|
|
114
|
+
"yolo"
|
|
115
|
+
]).optional(),
|
|
116
|
+
model: z.string().nullable().optional(),
|
|
117
|
+
fallbackModel: z.string().nullable().optional(),
|
|
118
|
+
customSystemPrompt: z.string().nullable().optional(),
|
|
119
|
+
appendSystemPrompt: z.string().nullable().optional(),
|
|
120
|
+
allowedTools: z.array(z.string()).nullable().optional(),
|
|
121
|
+
disallowedTools: z.array(z.string()).nullable().optional(),
|
|
122
|
+
displayText: z.string().optional()
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const SessionMessageContentSchema = z.object({
|
|
126
|
+
c: z.string(),
|
|
127
|
+
t: z.literal("encrypted")
|
|
128
|
+
});
|
|
129
|
+
const SessionMessageSchema = z.object({
|
|
130
|
+
id: z.string(),
|
|
131
|
+
seq: z.number(),
|
|
132
|
+
localId: z.string().nullish(),
|
|
133
|
+
content: SessionMessageContentSchema,
|
|
134
|
+
createdAt: z.number(),
|
|
135
|
+
updatedAt: z.number()
|
|
136
|
+
});
|
|
137
|
+
const SessionProtocolMessageSchema = z.object({
|
|
138
|
+
role: z.literal("session"),
|
|
139
|
+
content: sessionEnvelopeSchema,
|
|
140
|
+
meta: MessageMetaSchema.optional()
|
|
141
|
+
});
|
|
142
|
+
const MessageContentSchema = SessionProtocolMessageSchema;
|
|
143
|
+
const VersionedEncryptedValueSchema = z.object({
|
|
144
|
+
version: z.number(),
|
|
145
|
+
value: z.string()
|
|
146
|
+
});
|
|
147
|
+
const VersionedNullableEncryptedValueSchema = z.object({
|
|
148
|
+
version: z.number(),
|
|
149
|
+
value: z.string().nullable()
|
|
150
|
+
});
|
|
151
|
+
const UpdateNewMessageBodySchema = z.object({
|
|
152
|
+
t: z.literal("new-message"),
|
|
153
|
+
sid: z.string(),
|
|
154
|
+
message: SessionMessageSchema
|
|
155
|
+
});
|
|
156
|
+
const UpdateSessionBodySchema = z.object({
|
|
157
|
+
t: z.literal("update-session"),
|
|
158
|
+
id: z.string(),
|
|
159
|
+
metadata: VersionedEncryptedValueSchema.nullish(),
|
|
160
|
+
agentState: VersionedNullableEncryptedValueSchema.nullish()
|
|
161
|
+
});
|
|
162
|
+
const VersionedMachineEncryptedValueSchema = z.object({
|
|
163
|
+
version: z.number(),
|
|
164
|
+
value: z.string()
|
|
165
|
+
});
|
|
166
|
+
const UpdateMachineBodySchema = z.object({
|
|
167
|
+
t: z.literal("update-machine"),
|
|
168
|
+
machineId: z.string(),
|
|
169
|
+
metadata: VersionedMachineEncryptedValueSchema.nullish(),
|
|
170
|
+
daemonState: VersionedMachineEncryptedValueSchema.nullish(),
|
|
171
|
+
active: z.boolean().optional(),
|
|
172
|
+
activeAt: z.number().optional()
|
|
173
|
+
});
|
|
174
|
+
const CoreUpdateBodySchema = z.discriminatedUnion("t", [
|
|
175
|
+
UpdateNewMessageBodySchema,
|
|
176
|
+
UpdateSessionBodySchema,
|
|
177
|
+
UpdateMachineBodySchema
|
|
178
|
+
]);
|
|
179
|
+
const CoreUpdateContainerSchema = z.object({
|
|
180
|
+
id: z.string(),
|
|
181
|
+
seq: z.number(),
|
|
182
|
+
body: CoreUpdateBodySchema,
|
|
183
|
+
createdAt: z.number()
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const PASSWORD_SECRET_SALT = "codexdeck-password-bootstrap-v2";
|
|
187
|
+
const PASSWORD_SECRET_ITERATIONS = 2e5;
|
|
188
|
+
const PASSWORD_SECRET_LENGTH_BITS = 256;
|
|
189
|
+
const TRANSPORT_BUNDLE_VERSION = 0;
|
|
190
|
+
const REMOTE_SECRET_LENGTH_BYTES = 32;
|
|
191
|
+
const REMOTE_OPAQUE_KEY_STRETCHING = "memory-constrained";
|
|
192
|
+
const REMOTE_RELAY_KEY_SALT = "codexdeck:remote-relay-salt:v2";
|
|
193
|
+
const REMOTE_RELAY_KEY_INFO = "codexdeck:remote-relay-info:v2";
|
|
194
|
+
const REMOTE_OPAQUE_SERVER_IDENTIFIER_PREFIX = "codexdeck:remote-server:v1:";
|
|
195
|
+
const REMOTE_RPC_SIGNATURE_VERSION = "codexdeck:remote-rpc-signature:v1";
|
|
196
|
+
const encoder = new TextEncoder();
|
|
197
|
+
const decoder = new TextDecoder();
|
|
198
|
+
function isCryptoAvailable(cryptoObject) {
|
|
199
|
+
return !!cryptoObject?.subtle && typeof cryptoObject.getRandomValues === "function";
|
|
200
|
+
}
|
|
201
|
+
let cryptoPromise = null;
|
|
202
|
+
async function getCrypto() {
|
|
203
|
+
const cryptoObject = globalThis.crypto;
|
|
204
|
+
if (isCryptoAvailable(cryptoObject)) {
|
|
205
|
+
return cryptoObject;
|
|
206
|
+
}
|
|
207
|
+
if (!cryptoPromise) {
|
|
208
|
+
cryptoPromise = (async () => {
|
|
209
|
+
const processObject = globalThis.process;
|
|
210
|
+
if (!processObject?.versions?.node) {
|
|
211
|
+
throw new Error("Web Crypto API is required");
|
|
212
|
+
}
|
|
213
|
+
const nodeCryptoModule = await import('node:crypto');
|
|
214
|
+
if (!isCryptoAvailable(nodeCryptoModule.webcrypto)) {
|
|
215
|
+
throw new Error("Web Crypto API is required");
|
|
216
|
+
}
|
|
217
|
+
return nodeCryptoModule.webcrypto;
|
|
218
|
+
})();
|
|
219
|
+
}
|
|
220
|
+
return cryptoPromise;
|
|
221
|
+
}
|
|
222
|
+
function concatBytes(...chunks) {
|
|
223
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
224
|
+
const output = new Uint8Array(totalLength);
|
|
225
|
+
let offset = 0;
|
|
226
|
+
for (const chunk of chunks) {
|
|
227
|
+
output.set(chunk, offset);
|
|
228
|
+
offset += chunk.length;
|
|
229
|
+
}
|
|
230
|
+
return output;
|
|
231
|
+
}
|
|
232
|
+
function normalizeUsername(username) {
|
|
233
|
+
return username.normalize("NFKC").trim().toLowerCase();
|
|
234
|
+
}
|
|
235
|
+
function normalizePassword(password) {
|
|
236
|
+
return password.normalize("NFKC");
|
|
237
|
+
}
|
|
238
|
+
function encodeBase64(bytes) {
|
|
239
|
+
if (typeof Buffer !== "undefined") {
|
|
240
|
+
return Buffer.from(bytes).toString("base64");
|
|
241
|
+
}
|
|
242
|
+
let binary = "";
|
|
243
|
+
for (const byte of bytes) {
|
|
244
|
+
binary += String.fromCharCode(byte);
|
|
245
|
+
}
|
|
246
|
+
return btoa(binary);
|
|
247
|
+
}
|
|
248
|
+
function decodeBase64(base64) {
|
|
249
|
+
const normalized = base64.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
|
250
|
+
if (typeof Buffer !== "undefined") {
|
|
251
|
+
return new Uint8Array(Buffer.from(normalized, "base64"));
|
|
252
|
+
}
|
|
253
|
+
const binary = atob(normalized);
|
|
254
|
+
const output = new Uint8Array(binary.length);
|
|
255
|
+
for (let i = 0; i < binary.length; i += 1) {
|
|
256
|
+
output[i] = binary.charCodeAt(i);
|
|
257
|
+
}
|
|
258
|
+
return output;
|
|
259
|
+
}
|
|
260
|
+
function normalizeRealmId(realmId) {
|
|
261
|
+
const normalized = realmId.trim();
|
|
262
|
+
if (!normalized) {
|
|
263
|
+
throw new Error("Realm id must not be empty");
|
|
264
|
+
}
|
|
265
|
+
return normalized;
|
|
266
|
+
}
|
|
267
|
+
function normalizeCredentialInputs(username, password, realmId) {
|
|
268
|
+
const normalizedUsername = normalizeUsername(username);
|
|
269
|
+
if (!normalizedUsername) {
|
|
270
|
+
throw new Error("Username must not be empty");
|
|
271
|
+
}
|
|
272
|
+
const normalizedPassword = normalizePassword(password);
|
|
273
|
+
if (normalizedPassword.trim().length === 0) {
|
|
274
|
+
throw new Error("Password must not be empty");
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
username: normalizedUsername,
|
|
278
|
+
password: normalizedPassword,
|
|
279
|
+
realmId: normalizeRealmId(realmId)
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
async function deriveCredentialSecret(username, password, realmId) {
|
|
283
|
+
const normalized = normalizeCredentialInputs(username, password, realmId);
|
|
284
|
+
const cryptoObject = await getCrypto();
|
|
285
|
+
const baseKey = await cryptoObject.subtle.importKey(
|
|
286
|
+
"raw",
|
|
287
|
+
encoder.encode(normalized.password),
|
|
288
|
+
"PBKDF2",
|
|
289
|
+
false,
|
|
290
|
+
["deriveBits"]
|
|
291
|
+
);
|
|
292
|
+
const bits = await cryptoObject.subtle.deriveBits(
|
|
293
|
+
{
|
|
294
|
+
name: "PBKDF2",
|
|
295
|
+
hash: "SHA-256",
|
|
296
|
+
salt: encoder.encode(
|
|
297
|
+
`${PASSWORD_SECRET_SALT}
|
|
298
|
+
${normalized.realmId}
|
|
299
|
+
${normalized.username}`
|
|
300
|
+
),
|
|
301
|
+
iterations: PASSWORD_SECRET_ITERATIONS
|
|
302
|
+
},
|
|
303
|
+
baseKey,
|
|
304
|
+
PASSWORD_SECRET_LENGTH_BITS
|
|
305
|
+
);
|
|
306
|
+
return new Uint8Array(bits);
|
|
307
|
+
}
|
|
308
|
+
function normalizeSecretBytes(secret) {
|
|
309
|
+
if (secret.length !== REMOTE_SECRET_LENGTH_BYTES) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
`Remote secret must be ${REMOTE_SECRET_LENGTH_BYTES} bytes`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
return Uint8Array.from(secret);
|
|
315
|
+
}
|
|
316
|
+
function deriveSubkey(secret, label) {
|
|
317
|
+
const digest = tweetnacl.hash(
|
|
318
|
+
concatBytes(secret, encoder.encode(`codexdeck:${label}`))
|
|
319
|
+
);
|
|
320
|
+
return digest.slice(0, 32);
|
|
321
|
+
}
|
|
322
|
+
function deriveContentKeyPair(secret) {
|
|
323
|
+
const seed = deriveSubkey(secret, "content-seed");
|
|
324
|
+
const boxSecretKey = tweetnacl.hash(seed).slice(0, 32);
|
|
325
|
+
const keyPair = tweetnacl.box.keyPair.fromSecretKey(boxSecretKey);
|
|
326
|
+
return {
|
|
327
|
+
publicKey: keyPair.publicKey,
|
|
328
|
+
secretKey: keyPair.secretKey
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function deriveRemoteMaterialFromSecret(secret) {
|
|
332
|
+
const normalizedSecret = normalizeSecretBytes(secret);
|
|
333
|
+
const authKeyPair = tweetnacl.sign.keyPair.fromSeed(normalizedSecret);
|
|
334
|
+
const contentKeyPair = deriveContentKeyPair(normalizedSecret);
|
|
335
|
+
const relayKey = deriveSubkey(normalizedSecret, "remote-relay");
|
|
336
|
+
return {
|
|
337
|
+
secret: normalizedSecret,
|
|
338
|
+
relayKey,
|
|
339
|
+
authKeyPair,
|
|
340
|
+
contentKeyPair,
|
|
341
|
+
publicKeyHex: Array.from(authKeyPair.publicKey).map((value) => value.toString(16).padStart(2, "0")).join("").toUpperCase()
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function deriveRemoteAuthMaterialFromSecret(secret) {
|
|
345
|
+
const normalizedSecret = typeof secret === "string" ? decodeBase64(secret) : secret;
|
|
346
|
+
return deriveRemoteMaterialFromSecret(normalizedSecret);
|
|
347
|
+
}
|
|
348
|
+
async function digestSha256(input) {
|
|
349
|
+
const cryptoObject = await getCrypto();
|
|
350
|
+
const digest = await cryptoObject.subtle.digest("SHA-256", input);
|
|
351
|
+
return new Uint8Array(digest);
|
|
352
|
+
}
|
|
353
|
+
async function ensureOpaqueReady() {
|
|
354
|
+
await opaque.ready;
|
|
355
|
+
}
|
|
356
|
+
function normalizeRemoteOpaquePassword(password) {
|
|
357
|
+
const normalized = normalizePassword(password);
|
|
358
|
+
if (normalized.trim().length === 0) {
|
|
359
|
+
throw new Error("Password must not be empty");
|
|
360
|
+
}
|
|
361
|
+
return normalized;
|
|
362
|
+
}
|
|
363
|
+
function buildRemoteOpaqueIdentifiers(loginHandle, realmId) {
|
|
364
|
+
return {
|
|
365
|
+
client: normalizeLoginHandle(loginHandle),
|
|
366
|
+
server: `${REMOTE_OPAQUE_SERVER_IDENTIFIER_PREFIX}${normalizeRealmId(realmId)}`
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function normalizeLoginHandle(loginHandle) {
|
|
370
|
+
const normalized = loginHandle.trim().toLowerCase();
|
|
371
|
+
if (!normalized) {
|
|
372
|
+
throw new Error("Login handle must not be empty");
|
|
373
|
+
}
|
|
374
|
+
return normalized;
|
|
375
|
+
}
|
|
376
|
+
async function deriveRemoteLoginHandle(username, realmId) {
|
|
377
|
+
const normalizedUsername = normalizeUsername(username);
|
|
378
|
+
if (!normalizedUsername) {
|
|
379
|
+
throw new Error("Username must not be empty");
|
|
380
|
+
}
|
|
381
|
+
const digest = await digestSha256(
|
|
382
|
+
encoder.encode(
|
|
383
|
+
`codexdeck:remote-login-handle:v1:${normalizeRealmId(realmId)}:${normalizedUsername}`
|
|
384
|
+
)
|
|
385
|
+
);
|
|
386
|
+
return Array.from(digest).map((value) => value.toString(16).padStart(2, "0")).join("");
|
|
387
|
+
}
|
|
388
|
+
async function createRemoteOpaqueServerSetup() {
|
|
389
|
+
await ensureOpaqueReady();
|
|
390
|
+
return opaque.server.createSetup();
|
|
391
|
+
}
|
|
392
|
+
async function getRemoteOpaqueServerPublicKey(serverSetup) {
|
|
393
|
+
await ensureOpaqueReady();
|
|
394
|
+
return opaque.server.getPublicKey(serverSetup);
|
|
395
|
+
}
|
|
396
|
+
async function deriveRemoteRelayKeyFromExportKey(exportKey) {
|
|
397
|
+
const normalizedExportKey = typeof exportKey === "string" ? decodeBase64(exportKey) : exportKey;
|
|
398
|
+
if (normalizedExportKey.length === 0) {
|
|
399
|
+
throw new Error("Export key must not be empty");
|
|
400
|
+
}
|
|
401
|
+
const cryptoObject = await getCrypto();
|
|
402
|
+
const baseKey = await cryptoObject.subtle.importKey(
|
|
403
|
+
"raw",
|
|
404
|
+
normalizedExportKey,
|
|
405
|
+
"HKDF",
|
|
406
|
+
false,
|
|
407
|
+
["deriveBits"]
|
|
408
|
+
);
|
|
409
|
+
const bits = await cryptoObject.subtle.deriveBits(
|
|
410
|
+
{
|
|
411
|
+
name: "HKDF",
|
|
412
|
+
hash: "SHA-256",
|
|
413
|
+
salt: encoder.encode(REMOTE_RELAY_KEY_SALT),
|
|
414
|
+
info: encoder.encode(REMOTE_RELAY_KEY_INFO)
|
|
415
|
+
},
|
|
416
|
+
baseKey,
|
|
417
|
+
256
|
|
418
|
+
);
|
|
419
|
+
return new Uint8Array(bits);
|
|
420
|
+
}
|
|
421
|
+
async function startRemoteOpaqueRegistration(password) {
|
|
422
|
+
await ensureOpaqueReady();
|
|
423
|
+
return opaque.client.startRegistration({
|
|
424
|
+
password: normalizeRemoteOpaquePassword(password)
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
async function createRemoteOpaqueRegistrationResponse(params) {
|
|
428
|
+
await ensureOpaqueReady();
|
|
429
|
+
return opaque.server.createRegistrationResponse({
|
|
430
|
+
serverSetup: params.serverSetup,
|
|
431
|
+
userIdentifier: normalizeLoginHandle(params.loginHandle),
|
|
432
|
+
registrationRequest: params.registrationRequest
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
async function finishRemoteOpaqueRegistration(params) {
|
|
436
|
+
await ensureOpaqueReady();
|
|
437
|
+
const result = opaque.client.finishRegistration({
|
|
438
|
+
password: normalizeRemoteOpaquePassword(params.password),
|
|
439
|
+
clientRegistrationState: params.clientRegistrationState,
|
|
440
|
+
registrationResponse: params.registrationResponse,
|
|
441
|
+
keyStretching: REMOTE_OPAQUE_KEY_STRETCHING,
|
|
442
|
+
identifiers: buildRemoteOpaqueIdentifiers(
|
|
443
|
+
params.loginHandle,
|
|
444
|
+
params.realmId
|
|
445
|
+
)
|
|
446
|
+
});
|
|
447
|
+
if (params.expectedServerPublicKey && result.serverStaticPublicKey !== params.expectedServerPublicKey) {
|
|
448
|
+
throw new Error("Remote server public key mismatch");
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
...result,
|
|
452
|
+
relayKey: await deriveRemoteRelayKeyFromExportKey(result.exportKey)
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
async function startRemoteOpaqueLogin(password) {
|
|
456
|
+
await ensureOpaqueReady();
|
|
457
|
+
return opaque.client.startLogin({
|
|
458
|
+
password: normalizeRemoteOpaquePassword(password)
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
async function startRemoteOpaqueServerLogin(params) {
|
|
462
|
+
await ensureOpaqueReady();
|
|
463
|
+
return opaque.server.startLogin({
|
|
464
|
+
serverSetup: params.serverSetup,
|
|
465
|
+
registrationRecord: params.registrationRecord,
|
|
466
|
+
startLoginRequest: params.startLoginRequest,
|
|
467
|
+
userIdentifier: normalizeLoginHandle(params.loginHandle),
|
|
468
|
+
identifiers: buildRemoteOpaqueIdentifiers(
|
|
469
|
+
params.loginHandle,
|
|
470
|
+
params.realmId
|
|
471
|
+
)
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
async function finishRemoteOpaqueLogin(params) {
|
|
475
|
+
await ensureOpaqueReady();
|
|
476
|
+
const result = opaque.client.finishLogin({
|
|
477
|
+
password: normalizeRemoteOpaquePassword(params.password),
|
|
478
|
+
clientLoginState: params.clientLoginState,
|
|
479
|
+
loginResponse: params.loginResponse,
|
|
480
|
+
keyStretching: REMOTE_OPAQUE_KEY_STRETCHING,
|
|
481
|
+
identifiers: buildRemoteOpaqueIdentifiers(
|
|
482
|
+
params.loginHandle,
|
|
483
|
+
params.realmId
|
|
484
|
+
)
|
|
485
|
+
});
|
|
486
|
+
if (!result) {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
if (params.expectedServerPublicKey && result.serverStaticPublicKey !== params.expectedServerPublicKey) {
|
|
490
|
+
throw new Error("Remote server public key mismatch");
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
...result,
|
|
494
|
+
relayKey: await deriveRemoteRelayKeyFromExportKey(result.exportKey)
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
async function finishRemoteOpaqueServerLogin(params) {
|
|
498
|
+
await ensureOpaqueReady();
|
|
499
|
+
return opaque.server.finishLogin({
|
|
500
|
+
serverLoginState: params.serverLoginState,
|
|
501
|
+
finishLoginRequest: params.finishLoginRequest,
|
|
502
|
+
identifiers: buildRemoteOpaqueIdentifiers(
|
|
503
|
+
params.loginHandle,
|
|
504
|
+
params.realmId
|
|
505
|
+
)
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
function encodeRemoteStoredSecret(secret) {
|
|
509
|
+
return encodeBase64(normalizeSecretBytes(secret));
|
|
510
|
+
}
|
|
511
|
+
function decodeRemoteStoredSecret(secret) {
|
|
512
|
+
return normalizeSecretBytes(decodeBase64(secret));
|
|
513
|
+
}
|
|
514
|
+
async function deriveRemoteStoredAccessMaterial(storedSecret) {
|
|
515
|
+
return deriveRemoteAuthMaterialFromSecret(
|
|
516
|
+
decodeRemoteStoredSecret(storedSecret)
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
async function deriveRemoteAccessStorageSecret(username, password, realmId) {
|
|
520
|
+
const material = await deriveRemoteAccessMaterial(
|
|
521
|
+
username,
|
|
522
|
+
password,
|
|
523
|
+
realmId
|
|
524
|
+
);
|
|
525
|
+
return encodeRemoteStoredSecret(material.secret);
|
|
526
|
+
}
|
|
527
|
+
async function deriveRemoteAccessMaterial(username, password, realmId) {
|
|
528
|
+
const normalized = normalizeCredentialInputs(username, password, realmId);
|
|
529
|
+
const secret = await deriveCredentialSecret(
|
|
530
|
+
normalized.username,
|
|
531
|
+
normalized.password,
|
|
532
|
+
normalized.realmId
|
|
533
|
+
);
|
|
534
|
+
return {
|
|
535
|
+
...deriveRemoteMaterialFromSecret(secret),
|
|
536
|
+
loginHandle: await deriveRemoteLoginHandle(
|
|
537
|
+
normalized.username,
|
|
538
|
+
normalized.realmId
|
|
539
|
+
),
|
|
540
|
+
realmId: normalized.realmId
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
async function deriveRemoteAuthMaterial(username, password, realmId) {
|
|
544
|
+
return deriveRemoteAccessMaterial(username, password, realmId);
|
|
545
|
+
}
|
|
546
|
+
function buildRemoteAuthChallengeMessage(binding) {
|
|
547
|
+
const machineId = binding.machineId?.trim() || "";
|
|
548
|
+
return encoder.encode(
|
|
549
|
+
[
|
|
550
|
+
"codexdeck-auth-v1",
|
|
551
|
+
binding.challengeId,
|
|
552
|
+
binding.challenge,
|
|
553
|
+
binding.clientKind,
|
|
554
|
+
machineId
|
|
555
|
+
].join("\n")
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
function signRemoteAuthChallenge(secretKey, binding) {
|
|
559
|
+
const signature = tweetnacl.sign.detached(
|
|
560
|
+
buildRemoteAuthChallengeMessage(binding),
|
|
561
|
+
secretKey
|
|
562
|
+
);
|
|
563
|
+
return encodeBase64(signature);
|
|
564
|
+
}
|
|
565
|
+
function verifyRemoteAuthChallengeSignature(publicKey, signature, binding) {
|
|
566
|
+
return tweetnacl.sign.detached.verify(
|
|
567
|
+
buildRemoteAuthChallengeMessage(binding),
|
|
568
|
+
signature,
|
|
569
|
+
publicKey
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
function parseSigningPublicKey(publicKey) {
|
|
573
|
+
const parsed = typeof publicKey === "string" ? decodeBase64(publicKey) : publicKey;
|
|
574
|
+
if (parsed.length !== tweetnacl.sign.publicKeyLength) {
|
|
575
|
+
throw new Error("Invalid remote RPC signing public key");
|
|
576
|
+
}
|
|
577
|
+
return parsed;
|
|
578
|
+
}
|
|
579
|
+
function parseSigningSecretKey(secretKey) {
|
|
580
|
+
const parsed = typeof secretKey === "string" ? decodeBase64(secretKey) : secretKey;
|
|
581
|
+
if (parsed.length !== tweetnacl.sign.secretKeyLength) {
|
|
582
|
+
throw new Error("Invalid remote RPC signing secret key");
|
|
583
|
+
}
|
|
584
|
+
return parsed;
|
|
585
|
+
}
|
|
586
|
+
function parseSigningSignature(signature) {
|
|
587
|
+
const parsed = typeof signature === "string" ? decodeBase64(signature) : signature;
|
|
588
|
+
if (parsed.length !== tweetnacl.sign.signatureLength) {
|
|
589
|
+
throw new Error("Invalid remote RPC signature");
|
|
590
|
+
}
|
|
591
|
+
return parsed;
|
|
592
|
+
}
|
|
593
|
+
function buildRemoteRpcSignatureMessage(params) {
|
|
594
|
+
const machineId = params.machineId.trim();
|
|
595
|
+
const requestId = params.requestId.trim();
|
|
596
|
+
if (!machineId) {
|
|
597
|
+
throw new Error("Remote RPC signature machineId must not be empty");
|
|
598
|
+
}
|
|
599
|
+
if (!requestId) {
|
|
600
|
+
throw new Error("Remote RPC signature requestId must not be empty");
|
|
601
|
+
}
|
|
602
|
+
if (!params.encryptedResult.trim()) {
|
|
603
|
+
throw new Error("Remote RPC signature payload must not be empty");
|
|
604
|
+
}
|
|
605
|
+
return encoder.encode(
|
|
606
|
+
[
|
|
607
|
+
REMOTE_RPC_SIGNATURE_VERSION,
|
|
608
|
+
machineId,
|
|
609
|
+
requestId,
|
|
610
|
+
params.encryptedResult
|
|
611
|
+
].join("\n")
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
function generateRemoteRpcSigningKeyPair() {
|
|
615
|
+
const keyPair = tweetnacl.sign.keyPair();
|
|
616
|
+
return {
|
|
617
|
+
publicKey: encodeBase64(keyPair.publicKey),
|
|
618
|
+
secretKey: encodeBase64(keyPair.secretKey)
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
function signRemoteRpcResult(params) {
|
|
622
|
+
const signature = tweetnacl.sign.detached(
|
|
623
|
+
buildRemoteRpcSignatureMessage(params),
|
|
624
|
+
parseSigningSecretKey(params.secretKey)
|
|
625
|
+
);
|
|
626
|
+
return encodeBase64(signature);
|
|
627
|
+
}
|
|
628
|
+
function verifyRemoteRpcResultSignature(params) {
|
|
629
|
+
return tweetnacl.sign.detached.verify(
|
|
630
|
+
buildRemoteRpcSignatureMessage(params),
|
|
631
|
+
parseSigningSignature(params.signature),
|
|
632
|
+
parseSigningPublicKey(params.publicKey)
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
async function importAesKey(key) {
|
|
636
|
+
const cryptoObject = await getCrypto();
|
|
637
|
+
return cryptoObject.subtle.importKey(
|
|
638
|
+
"raw",
|
|
639
|
+
key,
|
|
640
|
+
{
|
|
641
|
+
name: "AES-GCM",
|
|
642
|
+
length: 256
|
|
643
|
+
},
|
|
644
|
+
false,
|
|
645
|
+
["encrypt", "decrypt"]
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
async function encryptRemotePayload(key, payload) {
|
|
649
|
+
const cryptoObject = await getCrypto();
|
|
650
|
+
const iv = cryptoObject.getRandomValues(new Uint8Array(12));
|
|
651
|
+
const cryptoKey = await importAesKey(key);
|
|
652
|
+
const encrypted = await cryptoObject.subtle.encrypt(
|
|
653
|
+
{
|
|
654
|
+
name: "AES-GCM",
|
|
655
|
+
iv
|
|
656
|
+
},
|
|
657
|
+
cryptoKey,
|
|
658
|
+
encoder.encode(JSON.stringify(payload))
|
|
659
|
+
);
|
|
660
|
+
const bundle = concatBytes(
|
|
661
|
+
Uint8Array.of(TRANSPORT_BUNDLE_VERSION),
|
|
662
|
+
iv,
|
|
663
|
+
new Uint8Array(encrypted)
|
|
664
|
+
);
|
|
665
|
+
return encodeBase64(bundle);
|
|
666
|
+
}
|
|
667
|
+
async function decryptRemotePayload(key, encoded) {
|
|
668
|
+
const bundle = decodeBase64(encoded);
|
|
669
|
+
if (bundle.length < 13 || bundle[0] !== TRANSPORT_BUNDLE_VERSION) {
|
|
670
|
+
throw new Error("Invalid encrypted payload");
|
|
671
|
+
}
|
|
672
|
+
const iv = bundle.slice(1, 13);
|
|
673
|
+
const ciphertext = bundle.slice(13);
|
|
674
|
+
const cryptoKey = await importAesKey(key);
|
|
675
|
+
const cryptoObject = await getCrypto();
|
|
676
|
+
const decrypted = await cryptoObject.subtle.decrypt(
|
|
677
|
+
{
|
|
678
|
+
name: "AES-GCM",
|
|
679
|
+
iv
|
|
680
|
+
},
|
|
681
|
+
cryptoKey,
|
|
682
|
+
ciphertext
|
|
683
|
+
);
|
|
684
|
+
return JSON.parse(decoder.decode(decrypted));
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const RemoteRpcEnvelopeSchema = z.object({
|
|
688
|
+
requestId: z.string(),
|
|
689
|
+
body: z.unknown()
|
|
690
|
+
});
|
|
691
|
+
const RemoteRpcResultEnvelopeSchema = z.discriminatedUnion("ok", [
|
|
692
|
+
z.object({
|
|
693
|
+
ok: z.literal(true),
|
|
694
|
+
requestId: z.string(),
|
|
695
|
+
body: z.unknown()
|
|
696
|
+
}),
|
|
697
|
+
z.object({
|
|
698
|
+
ok: z.literal(false),
|
|
699
|
+
requestId: z.string(),
|
|
700
|
+
error: z.string()
|
|
701
|
+
})
|
|
702
|
+
]);
|
|
703
|
+
const RemoteMachineMetadataSchema = z.object({
|
|
704
|
+
machineId: z.string(),
|
|
705
|
+
label: z.string(),
|
|
706
|
+
host: z.string(),
|
|
707
|
+
platform: z.string(),
|
|
708
|
+
codexDir: z.string(),
|
|
709
|
+
cliVersion: z.string(),
|
|
710
|
+
rpcSigningPublicKey: z.string().min(1)
|
|
711
|
+
});
|
|
712
|
+
const RemoteMachineStateSchema = z.object({
|
|
713
|
+
status: z.enum(["running", "offline", "error"]),
|
|
714
|
+
connectedAt: z.number(),
|
|
715
|
+
lastHeartbeatAt: z.number(),
|
|
716
|
+
localWebUrl: z.string().nullable().optional()
|
|
717
|
+
});
|
|
718
|
+
const RemoteMachineDescriptionSchema = z.object({
|
|
719
|
+
id: z.string(),
|
|
720
|
+
metadata: RemoteMachineMetadataSchema,
|
|
721
|
+
state: RemoteMachineStateSchema.nullable(),
|
|
722
|
+
active: z.boolean(),
|
|
723
|
+
activeAt: z.number()
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
const RemoteAuthErrorCode = {
|
|
727
|
+
accountNotFound: "remote_account_not_found",
|
|
728
|
+
accountMismatch: "remote_account_mismatch",
|
|
729
|
+
setupTokensMissing: "remote_setup_tokens_missing",
|
|
730
|
+
setupTokenInvalid: "remote_setup_token_invalid",
|
|
731
|
+
loginAlreadyBound: "remote_login_already_bound",
|
|
732
|
+
machineAlreadyBound: "remote_machine_already_bound",
|
|
733
|
+
invalidLogin: "remote_invalid_login",
|
|
734
|
+
invalidRegister: "remote_invalid_register",
|
|
735
|
+
invalidLoginState: "remote_invalid_login_state",
|
|
736
|
+
adminPasswordInvalid: "remote_admin_password_invalid",
|
|
737
|
+
authUpgradeRequired: "remote_auth_upgrade_required"
|
|
738
|
+
};
|
|
739
|
+
const REMOTE_AUTH_ERROR_CODE_SET = new Set(
|
|
740
|
+
Object.values(RemoteAuthErrorCode)
|
|
741
|
+
);
|
|
742
|
+
function isRemoteAuthErrorCode(value) {
|
|
743
|
+
return typeof value === "string" && REMOTE_AUTH_ERROR_CODE_SET.has(value);
|
|
744
|
+
}
|
|
745
|
+
function getRemoteAuthHint(params) {
|
|
746
|
+
const code = isRemoteAuthErrorCode(params.code) ? params.code : null;
|
|
747
|
+
if (code === RemoteAuthErrorCode.invalidLogin) {
|
|
748
|
+
if (params.context === "cli") {
|
|
749
|
+
return "Remote password is incorrect for this registered CLI account. Update --remote-password (or CODEXDECK_REMOTE_PASSWORD) to match the target server credentials.";
|
|
750
|
+
}
|
|
751
|
+
if (params.context === "admin") {
|
|
752
|
+
return "Admin password is incorrect.";
|
|
753
|
+
}
|
|
754
|
+
return "Incorrect password for this remote username. Use the same password configured on the target CLI.";
|
|
755
|
+
}
|
|
756
|
+
if (code === RemoteAuthErrorCode.accountNotFound) {
|
|
757
|
+
if (params.context === "cli") {
|
|
758
|
+
return "Remote account not found. Register this CLI first using a valid --remote-setup-token, --remote-username, and --remote-password.";
|
|
759
|
+
}
|
|
760
|
+
return "Remote account not found. Start the CLI first with --remote-setup-token, --remote-username, and --remote-password.";
|
|
761
|
+
}
|
|
762
|
+
if (code === RemoteAuthErrorCode.accountMismatch) {
|
|
763
|
+
return "This login is bound to a different machine. Use the credentials originally registered for this machine, or configure a different --remote-machine-id.";
|
|
764
|
+
}
|
|
765
|
+
if (code === RemoteAuthErrorCode.setupTokenInvalid) {
|
|
766
|
+
return "Setup token is invalid or disabled. Generate a valid setup token in the server admin page and retry.";
|
|
767
|
+
}
|
|
768
|
+
if (code === RemoteAuthErrorCode.setupTokensMissing) {
|
|
769
|
+
return "Remote server has no setup tokens configured. Add setup tokens in server admin before registering a CLI.";
|
|
770
|
+
}
|
|
771
|
+
if (code === RemoteAuthErrorCode.invalidLoginState) {
|
|
772
|
+
return "Remote login attempt expired or is no longer valid. Retry login.";
|
|
773
|
+
}
|
|
774
|
+
if (code === RemoteAuthErrorCode.invalidRegister) {
|
|
775
|
+
return "Remote registration attempt expired or is no longer valid. Retry CLI registration.";
|
|
776
|
+
}
|
|
777
|
+
if (code === RemoteAuthErrorCode.loginAlreadyBound || code === RemoteAuthErrorCode.machineAlreadyBound) {
|
|
778
|
+
return "This remote login or machine is already bound to different credentials. Verify --remote-username, --remote-password, and --remote-machine-id.";
|
|
779
|
+
}
|
|
780
|
+
if (code === RemoteAuthErrorCode.authUpgradeRequired) {
|
|
781
|
+
return "Legacy remote auth is no longer supported. Re-register this CLI using the current codex-deck version.";
|
|
782
|
+
}
|
|
783
|
+
if (code === RemoteAuthErrorCode.adminPasswordInvalid) {
|
|
784
|
+
return "Admin password is incorrect.";
|
|
785
|
+
}
|
|
786
|
+
const fallback = params.error?.trim();
|
|
787
|
+
if (fallback) {
|
|
788
|
+
return fallback;
|
|
789
|
+
}
|
|
790
|
+
if (params.context === "admin") {
|
|
791
|
+
return "Remote admin login failed.";
|
|
792
|
+
}
|
|
793
|
+
return "Remote login failed.";
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export { CoreUpdateBodySchema, CoreUpdateContainerSchema, MessageContentSchema, MessageMetaSchema, RemoteAuthErrorCode, RemoteMachineDescriptionSchema, RemoteMachineMetadataSchema, RemoteMachineStateSchema, RemoteRpcEnvelopeSchema, RemoteRpcResultEnvelopeSchema, SessionMessageContentSchema, SessionMessageSchema, SessionProtocolMessageSchema, UpdateMachineBodySchema, UpdateNewMessageBodySchema, UpdateSessionBodySchema, VersionedEncryptedValueSchema, VersionedMachineEncryptedValueSchema, VersionedNullableEncryptedValueSchema, buildRemoteAuthChallengeMessage, createEnvelope, createRemoteOpaqueRegistrationResponse, createRemoteOpaqueServerSetup, decodeBase64, decodeRemoteStoredSecret, decryptRemotePayload, deriveRemoteAccessMaterial, deriveRemoteAccessStorageSecret, deriveRemoteAuthMaterial, deriveRemoteAuthMaterialFromSecret, deriveRemoteLoginHandle, deriveRemoteRelayKeyFromExportKey, deriveRemoteStoredAccessMaterial, encodeBase64, encodeRemoteStoredSecret, encryptRemotePayload, finishRemoteOpaqueLogin, finishRemoteOpaqueRegistration, finishRemoteOpaqueServerLogin, generateRemoteRpcSigningKeyPair, getRemoteAuthHint, getRemoteOpaqueServerPublicKey, isRemoteAuthErrorCode, sessionEnvelopeSchema, sessionEventSchema, sessionFileEventSchema, sessionRoleSchema, sessionServiceMessageEventSchema, sessionStartEventSchema, sessionStopEventSchema, sessionTextEventSchema, sessionToolCallEndEventSchema, sessionToolCallStartEventSchema, sessionTurnEndEventSchema, sessionTurnEndStatusSchema, sessionTurnStartEventSchema, signRemoteAuthChallenge, signRemoteRpcResult, startRemoteOpaqueLogin, startRemoteOpaqueRegistration, startRemoteOpaqueServerLogin, verifyRemoteAuthChallengeSignature, verifyRemoteRpcResultSignature };
|