@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/cli.cjs
CHANGED
|
@@ -97011,7 +97011,12 @@ function toDeviceView(row) {
|
|
|
97011
97011
|
capabilities: parseCapabilities(row.capabilities),
|
|
97012
97012
|
createdAt: row.created_at,
|
|
97013
97013
|
lastSeenAt: row.last_seen_at,
|
|
97014
|
-
revokedAt: row.revoked_at
|
|
97014
|
+
revokedAt: row.revoked_at,
|
|
97015
|
+
// Reported from `e2ee_required` rather than from the key's presence. The
|
|
97016
|
+
// two agree today, but they answer different questions — the key is what a
|
|
97017
|
+
// handshake is checked against, `e2ee_required` is whether plaintext is
|
|
97018
|
+
// refused — and it is the second one a user is asking about.
|
|
97019
|
+
e2ee: row.e2ee_required === 1
|
|
97015
97020
|
};
|
|
97016
97021
|
}
|
|
97017
97022
|
var import_crypto3, DevicesRepository;
|
|
@@ -97029,13 +97034,29 @@ var init_devices_repository = __esm({
|
|
|
97029
97034
|
touchStmt;
|
|
97030
97035
|
deleteStmt;
|
|
97031
97036
|
deleteRevokedStmt;
|
|
97037
|
+
byE2eeStaticPubStmt;
|
|
97038
|
+
repairStmt;
|
|
97032
97039
|
constructor(db) {
|
|
97033
97040
|
this.insertStmt = db.prepare(`
|
|
97034
97041
|
INSERT INTO devices (
|
|
97035
|
-
device_id, public_key, token_hash, name, capabilities, created_at
|
|
97042
|
+
device_id, public_key, token_hash, name, capabilities, created_at,
|
|
97043
|
+
e2ee_static_pub, e2ee_required, e2ee_version
|
|
97036
97044
|
) VALUES (
|
|
97037
|
-
@device_id, @public_key, @token_hash, @name, @capabilities, @created_at
|
|
97045
|
+
@device_id, @public_key, @token_hash, @name, @capabilities, @created_at,
|
|
97046
|
+
@e2ee_static_pub, @e2ee_required, @e2ee_version
|
|
97038
97047
|
)
|
|
97048
|
+
`);
|
|
97049
|
+
this.byE2eeStaticPubStmt = db.prepare("SELECT * FROM devices WHERE e2ee_static_pub = ?");
|
|
97050
|
+
this.repairStmt = db.prepare(`
|
|
97051
|
+
UPDATE devices SET
|
|
97052
|
+
public_key = @public_key,
|
|
97053
|
+
token_hash = @token_hash,
|
|
97054
|
+
name = @name,
|
|
97055
|
+
capabilities = @capabilities,
|
|
97056
|
+
e2ee_required = 1,
|
|
97057
|
+
e2ee_version = @e2ee_version,
|
|
97058
|
+
revoked_at = NULL
|
|
97059
|
+
WHERE device_id = @device_id
|
|
97039
97060
|
`);
|
|
97040
97061
|
this.byTokenHashStmt = db.prepare("SELECT * FROM devices WHERE token_hash = ?");
|
|
97041
97062
|
this.byIdStmt = db.prepare("SELECT * FROM devices WHERE device_id = ?");
|
|
@@ -97052,19 +97073,42 @@ var init_devices_repository = __esm({
|
|
|
97052
97073
|
* moment it exists outside the client.
|
|
97053
97074
|
*/
|
|
97054
97075
|
register(args) {
|
|
97055
|
-
const deviceId = (0, import_crypto3.randomUUID)();
|
|
97056
97076
|
const deviceToken = generateDeviceToken();
|
|
97057
97077
|
const capabilities = capabilitiesForPreset(args.preset ?? "full");
|
|
97078
|
+
const now = args.now ?? Date.now();
|
|
97079
|
+
const existing = args.e2eeStaticPub ? this.byE2eeStaticPubStmt.get(args.e2eeStaticPub) : void 0;
|
|
97080
|
+
if (existing) {
|
|
97081
|
+
this.repairStmt.run({
|
|
97082
|
+
device_id: existing.device_id,
|
|
97083
|
+
public_key: args.publicKey,
|
|
97084
|
+
token_hash: hashDeviceToken(deviceToken),
|
|
97085
|
+
name: args.name ?? null,
|
|
97086
|
+
capabilities: JSON.stringify(capabilities),
|
|
97087
|
+
e2ee_version: args.e2eeVersion ?? null
|
|
97088
|
+
});
|
|
97089
|
+
return { deviceId: existing.device_id, deviceToken, capabilities };
|
|
97090
|
+
}
|
|
97091
|
+
const deviceId = (0, import_crypto3.randomUUID)();
|
|
97058
97092
|
this.insertStmt.run({
|
|
97059
97093
|
device_id: deviceId,
|
|
97060
97094
|
public_key: args.publicKey,
|
|
97061
97095
|
token_hash: hashDeviceToken(deviceToken),
|
|
97062
97096
|
name: args.name ?? null,
|
|
97063
97097
|
capabilities: JSON.stringify(capabilities),
|
|
97064
|
-
created_at:
|
|
97098
|
+
created_at: now,
|
|
97099
|
+
e2ee_static_pub: args.e2eeStaticPub ?? null,
|
|
97100
|
+
// The downgrade lock, set in the same write that records the key. Once
|
|
97101
|
+
// set, nothing a client can send clears it (design.md §6.3) — which is
|
|
97102
|
+
// what makes it a lock rather than a preference.
|
|
97103
|
+
e2ee_required: args.e2eeStaticPub ? 1 : 0,
|
|
97104
|
+
e2ee_version: args.e2eeVersion ?? null
|
|
97065
97105
|
});
|
|
97066
97106
|
return { deviceId, deviceToken, capabilities };
|
|
97067
97107
|
}
|
|
97108
|
+
/** The device that owns a Noise static key, or null. */
|
|
97109
|
+
getByE2eeStaticPub(staticPub) {
|
|
97110
|
+
return this.byE2eeStaticPubStmt.get(staticPub) ?? null;
|
|
97111
|
+
}
|
|
97068
97112
|
/**
|
|
97069
97113
|
* Resolve a presented token to a device, or null.
|
|
97070
97114
|
*
|
|
@@ -120638,11 +120682,19 @@ var init_codex_pty_runner = __esm({
|
|
|
120638
120682
|
onStatusChange;
|
|
120639
120683
|
onPhaseChange;
|
|
120640
120684
|
onReady;
|
|
120641
|
-
//
|
|
120642
|
-
//
|
|
120685
|
+
// Every Codex prompt the client can answer — startup gates (directory trust,
|
|
120686
|
+
// hooks review), command approvals, and the rate-limit model picker — is
|
|
120687
|
+
// broadcast through this one channel; null dismisses the card once the prompt
|
|
120688
|
+
// leaves the screen.
|
|
120689
|
+
//
|
|
120690
|
+
// Deliberately NOT onLiveQuestion/onLiveQuestionGone, which is Claude's
|
|
120691
|
+
// AskUserQuestion transport. Both channels land on the same mobile
|
|
120692
|
+
// QuestionCard, and the permission one is the correct fit for Codex: its menus
|
|
120693
|
+
// are answered by the option's real on-screen number (parseCodexNumberedOptions
|
|
120694
|
+
// emits `answerKeys: "2\r"`), which is exactly what `permissionIndices` carries
|
|
120695
|
+
// and what AskUserQuestion's down-arrow-count model cannot express. Wiring the
|
|
120696
|
+
// question channel as well would be a second path to the same card.
|
|
120643
120697
|
onPermissionChange;
|
|
120644
|
-
onLiveQuestion;
|
|
120645
|
-
onLiveQuestionGone;
|
|
120646
120698
|
onUserMessage;
|
|
120647
120699
|
log;
|
|
120648
120700
|
// Tracks sessions whose PTY has spawned but Codex hasn't yet reached its
|
|
@@ -120692,8 +120744,6 @@ var init_codex_pty_runner = __esm({
|
|
|
120692
120744
|
this.onPhaseChange = options.onPhaseChange;
|
|
120693
120745
|
this.onReady = options.onReady;
|
|
120694
120746
|
this.onPermissionChange = options.onPermissionChange;
|
|
120695
|
-
this.onLiveQuestion = options.onLiveQuestion;
|
|
120696
|
-
this.onLiveQuestionGone = options.onLiveQuestionGone;
|
|
120697
120747
|
this.onUserMessage = options.onUserMessage;
|
|
120698
120748
|
this.log = options.logger ?? getLogger("codex-pty");
|
|
120699
120749
|
}
|
|
@@ -148979,7 +149029,37 @@ var PairTokenStore = class {
|
|
|
148979
149029
|
expiresInSeconds: Math.floor(this.ttlMs / 1e3)
|
|
148980
149030
|
};
|
|
148981
149031
|
}
|
|
149032
|
+
/**
|
|
149033
|
+
* Whether `consume` would succeed right now, WITHOUT spending the token.
|
|
149034
|
+
*
|
|
149035
|
+
* Exists so a caller can reject a bad token before doing any work, and still
|
|
149036
|
+
* spend the token only once the work has succeeded. A pair token is
|
|
149037
|
+
* single-use and lives 180 seconds, so spending it on a request that then
|
|
149038
|
+
* fails costs the user a whole new QR — and, worse, makes their retry
|
|
149039
|
+
* indistinguishable from an attacker replaying a photographed code, which is
|
|
149040
|
+
* the one signal `design.md` §2.6 designates as replay detection.
|
|
149041
|
+
*
|
|
149042
|
+
* Advisory, not a reservation: it takes no lock and holds nothing. The
|
|
149043
|
+
* authoritative answer is still `consume`'s.
|
|
149044
|
+
*/
|
|
149045
|
+
verify(token) {
|
|
149046
|
+
const result = this.check(token);
|
|
149047
|
+
return result.ok ? { ok: true } : result;
|
|
149048
|
+
}
|
|
148982
149049
|
consume(token) {
|
|
149050
|
+
const result = this.check(token);
|
|
149051
|
+
if (!result.ok) return result;
|
|
149052
|
+
result.record.used = true;
|
|
149053
|
+
return { ok: true };
|
|
149054
|
+
}
|
|
149055
|
+
/**
|
|
149056
|
+
* The shared predicate behind `verify` and `consume`.
|
|
149057
|
+
*
|
|
149058
|
+
* One implementation on purpose: two copies of "is this token usable" is two
|
|
149059
|
+
* places for the expiry or single-use rule to drift, and a drift in this
|
|
149060
|
+
* direction fails open.
|
|
149061
|
+
*/
|
|
149062
|
+
check(token) {
|
|
148983
149063
|
const record3 = this.current;
|
|
148984
149064
|
if (!record3 || record3.token !== token) return { ok: false, reason: "unknown" };
|
|
148985
149065
|
if (Date.now() > record3.expiresAt) {
|
|
@@ -148987,8 +149067,7 @@ var PairTokenStore = class {
|
|
|
148987
149067
|
return { ok: false, reason: "expired" };
|
|
148988
149068
|
}
|
|
148989
149069
|
if (record3.used) return { ok: false, reason: "used" };
|
|
148990
|
-
|
|
148991
|
-
return { ok: true };
|
|
149070
|
+
return { ok: true, record: record3 };
|
|
148992
149071
|
}
|
|
148993
149072
|
peek() {
|
|
148994
149073
|
return this.current;
|
|
@@ -154006,9 +154085,9 @@ var StreamerServer = class {
|
|
|
154006
154085
|
json2(res, 400, { error: "Missing token or clientPublicKey" });
|
|
154007
154086
|
return;
|
|
154008
154087
|
}
|
|
154009
|
-
const
|
|
154010
|
-
if (!
|
|
154011
|
-
json2(res, 401, { error: `Pair token ${
|
|
154088
|
+
const precheck = this.pairTokens.verify(token);
|
|
154089
|
+
if (!precheck.ok) {
|
|
154090
|
+
json2(res, 401, { error: `Pair token ${precheck.reason}` });
|
|
154012
154091
|
return;
|
|
154013
154092
|
}
|
|
154014
154093
|
let sealed;
|
|
@@ -154019,6 +154098,11 @@ var StreamerServer = class {
|
|
|
154019
154098
|
json2(res, 400, { error: message });
|
|
154020
154099
|
return;
|
|
154021
154100
|
}
|
|
154101
|
+
const result = this.pairTokens.consume(token);
|
|
154102
|
+
if (!result.ok) {
|
|
154103
|
+
json2(res, 401, { error: `Pair token ${result.reason}` });
|
|
154104
|
+
return;
|
|
154105
|
+
}
|
|
154022
154106
|
const ts2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
154023
154107
|
this.log.info(`[pair] token exchanged from ${ip} at ${ts2}`, {
|
|
154024
154108
|
event: "pair.token_exchanged",
|