@threadbase-sh/streamer 1.57.0 → 1.58.1
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/cli.cjs +100 -16
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +100 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +100 -16
- package/dist/index.js.map +1 -1
- package/dist/runtime-migrations/004_add_device_e2ee.sql +47 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1305,11 +1305,19 @@ var CodexPtyRunner = class {
|
|
|
1305
1305
|
onStatusChange;
|
|
1306
1306
|
onPhaseChange;
|
|
1307
1307
|
onReady;
|
|
1308
|
-
//
|
|
1309
|
-
//
|
|
1308
|
+
// Every Codex prompt the client can answer — startup gates (directory trust,
|
|
1309
|
+
// hooks review), command approvals, and the rate-limit model picker — is
|
|
1310
|
+
// broadcast through this one channel; null dismisses the card once the prompt
|
|
1311
|
+
// leaves the screen.
|
|
1312
|
+
//
|
|
1313
|
+
// Deliberately NOT onLiveQuestion/onLiveQuestionGone, which is Claude's
|
|
1314
|
+
// AskUserQuestion transport. Both channels land on the same mobile
|
|
1315
|
+
// QuestionCard, and the permission one is the correct fit for Codex: its menus
|
|
1316
|
+
// are answered by the option's real on-screen number (parseCodexNumberedOptions
|
|
1317
|
+
// emits `answerKeys: "2\r"`), which is exactly what `permissionIndices` carries
|
|
1318
|
+
// and what AskUserQuestion's down-arrow-count model cannot express. Wiring the
|
|
1319
|
+
// question channel as well would be a second path to the same card.
|
|
1310
1320
|
onPermissionChange;
|
|
1311
|
-
onLiveQuestion;
|
|
1312
|
-
onLiveQuestionGone;
|
|
1313
1321
|
onUserMessage;
|
|
1314
1322
|
log;
|
|
1315
1323
|
// Tracks sessions whose PTY has spawned but Codex hasn't yet reached its
|
|
@@ -1359,8 +1367,6 @@ var CodexPtyRunner = class {
|
|
|
1359
1367
|
this.onPhaseChange = options.onPhaseChange;
|
|
1360
1368
|
this.onReady = options.onReady;
|
|
1361
1369
|
this.onPermissionChange = options.onPermissionChange;
|
|
1362
|
-
this.onLiveQuestion = options.onLiveQuestion;
|
|
1363
|
-
this.onLiveQuestionGone = options.onLiveQuestionGone;
|
|
1364
1370
|
this.onUserMessage = options.onUserMessage;
|
|
1365
1371
|
this.log = options.logger ?? getLogger("codex-pty");
|
|
1366
1372
|
}
|
|
@@ -4188,7 +4194,12 @@ function toDeviceView(row) {
|
|
|
4188
4194
|
capabilities: parseCapabilities(row.capabilities),
|
|
4189
4195
|
createdAt: row.created_at,
|
|
4190
4196
|
lastSeenAt: row.last_seen_at,
|
|
4191
|
-
revokedAt: row.revoked_at
|
|
4197
|
+
revokedAt: row.revoked_at,
|
|
4198
|
+
// Reported from `e2ee_required` rather than from the key's presence. The
|
|
4199
|
+
// two agree today, but they answer different questions — the key is what a
|
|
4200
|
+
// handshake is checked against, `e2ee_required` is whether plaintext is
|
|
4201
|
+
// refused — and it is the second one a user is asking about.
|
|
4202
|
+
e2ee: row.e2ee_required === 1
|
|
4192
4203
|
};
|
|
4193
4204
|
}
|
|
4194
4205
|
var DevicesRepository = class {
|
|
@@ -4200,14 +4211,30 @@ var DevicesRepository = class {
|
|
|
4200
4211
|
touchStmt;
|
|
4201
4212
|
deleteStmt;
|
|
4202
4213
|
deleteRevokedStmt;
|
|
4214
|
+
byE2eeStaticPubStmt;
|
|
4215
|
+
repairStmt;
|
|
4203
4216
|
constructor(db) {
|
|
4204
4217
|
this.insertStmt = db.prepare(`
|
|
4205
4218
|
INSERT INTO devices (
|
|
4206
|
-
device_id, public_key, token_hash, name, capabilities, created_at
|
|
4219
|
+
device_id, public_key, token_hash, name, capabilities, created_at,
|
|
4220
|
+
e2ee_static_pub, e2ee_required, e2ee_version
|
|
4207
4221
|
) VALUES (
|
|
4208
|
-
@device_id, @public_key, @token_hash, @name, @capabilities, @created_at
|
|
4222
|
+
@device_id, @public_key, @token_hash, @name, @capabilities, @created_at,
|
|
4223
|
+
@e2ee_static_pub, @e2ee_required, @e2ee_version
|
|
4209
4224
|
)
|
|
4210
4225
|
`);
|
|
4226
|
+
this.byE2eeStaticPubStmt = db.prepare("SELECT * FROM devices WHERE e2ee_static_pub = ?");
|
|
4227
|
+
this.repairStmt = db.prepare(`
|
|
4228
|
+
UPDATE devices SET
|
|
4229
|
+
public_key = @public_key,
|
|
4230
|
+
token_hash = @token_hash,
|
|
4231
|
+
name = @name,
|
|
4232
|
+
capabilities = @capabilities,
|
|
4233
|
+
e2ee_required = 1,
|
|
4234
|
+
e2ee_version = @e2ee_version,
|
|
4235
|
+
revoked_at = NULL
|
|
4236
|
+
WHERE device_id = @device_id
|
|
4237
|
+
`);
|
|
4211
4238
|
this.byTokenHashStmt = db.prepare("SELECT * FROM devices WHERE token_hash = ?");
|
|
4212
4239
|
this.byIdStmt = db.prepare("SELECT * FROM devices WHERE device_id = ?");
|
|
4213
4240
|
this.listStmt = db.prepare("SELECT * FROM devices ORDER BY created_at DESC");
|
|
@@ -4223,19 +4250,42 @@ var DevicesRepository = class {
|
|
|
4223
4250
|
* moment it exists outside the client.
|
|
4224
4251
|
*/
|
|
4225
4252
|
register(args) {
|
|
4226
|
-
const deviceId = (0, import_crypto4.randomUUID)();
|
|
4227
4253
|
const deviceToken = generateDeviceToken();
|
|
4228
4254
|
const capabilities = capabilitiesForPreset(args.preset ?? "full");
|
|
4255
|
+
const now = args.now ?? Date.now();
|
|
4256
|
+
const existing = args.e2eeStaticPub ? this.byE2eeStaticPubStmt.get(args.e2eeStaticPub) : void 0;
|
|
4257
|
+
if (existing) {
|
|
4258
|
+
this.repairStmt.run({
|
|
4259
|
+
device_id: existing.device_id,
|
|
4260
|
+
public_key: args.publicKey,
|
|
4261
|
+
token_hash: hashDeviceToken(deviceToken),
|
|
4262
|
+
name: args.name ?? null,
|
|
4263
|
+
capabilities: JSON.stringify(capabilities),
|
|
4264
|
+
e2ee_version: args.e2eeVersion ?? null
|
|
4265
|
+
});
|
|
4266
|
+
return { deviceId: existing.device_id, deviceToken, capabilities };
|
|
4267
|
+
}
|
|
4268
|
+
const deviceId = (0, import_crypto4.randomUUID)();
|
|
4229
4269
|
this.insertStmt.run({
|
|
4230
4270
|
device_id: deviceId,
|
|
4231
4271
|
public_key: args.publicKey,
|
|
4232
4272
|
token_hash: hashDeviceToken(deviceToken),
|
|
4233
4273
|
name: args.name ?? null,
|
|
4234
4274
|
capabilities: JSON.stringify(capabilities),
|
|
4235
|
-
created_at:
|
|
4275
|
+
created_at: now,
|
|
4276
|
+
e2ee_static_pub: args.e2eeStaticPub ?? null,
|
|
4277
|
+
// The downgrade lock, set in the same write that records the key. Once
|
|
4278
|
+
// set, nothing a client can send clears it (design.md §6.3) — which is
|
|
4279
|
+
// what makes it a lock rather than a preference.
|
|
4280
|
+
e2ee_required: args.e2eeStaticPub ? 1 : 0,
|
|
4281
|
+
e2ee_version: args.e2eeVersion ?? null
|
|
4236
4282
|
});
|
|
4237
4283
|
return { deviceId, deviceToken, capabilities };
|
|
4238
4284
|
}
|
|
4285
|
+
/** The device that owns a Noise static key, or null. */
|
|
4286
|
+
getByE2eeStaticPub(staticPub) {
|
|
4287
|
+
return this.byE2eeStaticPubStmt.get(staticPub) ?? null;
|
|
4288
|
+
}
|
|
4239
4289
|
/**
|
|
4240
4290
|
* Resolve a presented token to a device, or null.
|
|
4241
4291
|
*
|
|
@@ -11347,7 +11397,37 @@ var PairTokenStore = class {
|
|
|
11347
11397
|
expiresInSeconds: Math.floor(this.ttlMs / 1e3)
|
|
11348
11398
|
};
|
|
11349
11399
|
}
|
|
11400
|
+
/**
|
|
11401
|
+
* Whether `consume` would succeed right now, WITHOUT spending the token.
|
|
11402
|
+
*
|
|
11403
|
+
* Exists so a caller can reject a bad token before doing any work, and still
|
|
11404
|
+
* spend the token only once the work has succeeded. A pair token is
|
|
11405
|
+
* single-use and lives 180 seconds, so spending it on a request that then
|
|
11406
|
+
* fails costs the user a whole new QR — and, worse, makes their retry
|
|
11407
|
+
* indistinguishable from an attacker replaying a photographed code, which is
|
|
11408
|
+
* the one signal `design.md` §2.6 designates as replay detection.
|
|
11409
|
+
*
|
|
11410
|
+
* Advisory, not a reservation: it takes no lock and holds nothing. The
|
|
11411
|
+
* authoritative answer is still `consume`'s.
|
|
11412
|
+
*/
|
|
11413
|
+
verify(token) {
|
|
11414
|
+
const result = this.check(token);
|
|
11415
|
+
return result.ok ? { ok: true } : result;
|
|
11416
|
+
}
|
|
11350
11417
|
consume(token) {
|
|
11418
|
+
const result = this.check(token);
|
|
11419
|
+
if (!result.ok) return result;
|
|
11420
|
+
result.record.used = true;
|
|
11421
|
+
return { ok: true };
|
|
11422
|
+
}
|
|
11423
|
+
/**
|
|
11424
|
+
* The shared predicate behind `verify` and `consume`.
|
|
11425
|
+
*
|
|
11426
|
+
* One implementation on purpose: two copies of "is this token usable" is two
|
|
11427
|
+
* places for the expiry or single-use rule to drift, and a drift in this
|
|
11428
|
+
* direction fails open.
|
|
11429
|
+
*/
|
|
11430
|
+
check(token) {
|
|
11351
11431
|
const record2 = this.current;
|
|
11352
11432
|
if (!record2 || record2.token !== token) return { ok: false, reason: "unknown" };
|
|
11353
11433
|
if (Date.now() > record2.expiresAt) {
|
|
@@ -11355,8 +11435,7 @@ var PairTokenStore = class {
|
|
|
11355
11435
|
return { ok: false, reason: "expired" };
|
|
11356
11436
|
}
|
|
11357
11437
|
if (record2.used) return { ok: false, reason: "used" };
|
|
11358
|
-
|
|
11359
|
-
return { ok: true };
|
|
11438
|
+
return { ok: true, record: record2 };
|
|
11360
11439
|
}
|
|
11361
11440
|
peek() {
|
|
11362
11441
|
return this.current;
|
|
@@ -16146,9 +16225,9 @@ var StreamerServer = class {
|
|
|
16146
16225
|
json(res, 400, { error: "Missing token or clientPublicKey" });
|
|
16147
16226
|
return;
|
|
16148
16227
|
}
|
|
16149
|
-
const
|
|
16150
|
-
if (!
|
|
16151
|
-
json(res, 401, { error: `Pair token ${
|
|
16228
|
+
const precheck = this.pairTokens.verify(token);
|
|
16229
|
+
if (!precheck.ok) {
|
|
16230
|
+
json(res, 401, { error: `Pair token ${precheck.reason}` });
|
|
16152
16231
|
return;
|
|
16153
16232
|
}
|
|
16154
16233
|
let sealed;
|
|
@@ -16159,6 +16238,11 @@ var StreamerServer = class {
|
|
|
16159
16238
|
json(res, 400, { error: message });
|
|
16160
16239
|
return;
|
|
16161
16240
|
}
|
|
16241
|
+
const result = this.pairTokens.consume(token);
|
|
16242
|
+
if (!result.ok) {
|
|
16243
|
+
json(res, 401, { error: `Pair token ${result.reason}` });
|
|
16244
|
+
return;
|
|
16245
|
+
}
|
|
16162
16246
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
16163
16247
|
this.log.info(`[pair] token exchanged from ${ip} at ${ts}`, {
|
|
16164
16248
|
event: "pair.token_exchanged",
|