@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 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: args.now ?? Date.now()
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
- // Broadcasts Codex's blocking startup gates (directory trust, hooks review)
120642
- // as question cards; null dismisses the card once the gate leaves the screen.
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
- record3.used = true;
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 result = this.pairTokens.consume(token);
154010
- if (!result.ok) {
154011
- json2(res, 401, { error: `Pair token ${result.reason}` });
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",