@threadbase-sh/streamer 1.57.0 → 1.58.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.d.cts CHANGED
@@ -1204,6 +1204,11 @@ interface DeviceRow {
1204
1204
  created_at: number;
1205
1205
  last_seen_at: number | null;
1206
1206
  revoked_at: number | null;
1207
+ /** Noise static public key, base64. Null on every row that predates E2EE. */
1208
+ e2ee_static_pub: string | null;
1209
+ /** The downgrade lock. 1 once this device has completed a handshake. */
1210
+ e2ee_required: number;
1211
+ e2ee_version: number | null;
1207
1212
  }
1208
1213
  /** A device as reported over the API. Deliberately carries no credential. */
1209
1214
  interface DeviceView {
@@ -1213,6 +1218,16 @@ interface DeviceView {
1213
1218
  createdAt: number;
1214
1219
  lastSeenAt: number | null;
1215
1220
  revokedAt: number | null;
1221
+ /**
1222
+ * Whether this device is pinned to encryption.
1223
+ *
1224
+ * A boolean, never the key and never a hash of it: `GET /api/devices` exists
1225
+ * so a user can spot a device they did not pair, and answering "is this one
1226
+ * encrypted" needs no key material to do it. Publishing the static key would
1227
+ * hand an attacker who reached this endpoint the value that identifies a
1228
+ * device.
1229
+ */
1230
+ e2ee: boolean;
1216
1231
  }
1217
1232
  interface RegisteredDevice {
1218
1233
  deviceId: string;
@@ -1229,6 +1244,8 @@ declare class DevicesRepository {
1229
1244
  private touchStmt;
1230
1245
  private deleteStmt;
1231
1246
  private deleteRevokedStmt;
1247
+ private byE2eeStaticPubStmt;
1248
+ private repairStmt;
1232
1249
  constructor(db: Database.Database);
1233
1250
  /**
1234
1251
  * Record a newly paired device and mint its token.
@@ -1241,7 +1258,16 @@ declare class DevicesRepository {
1241
1258
  name?: string | null;
1242
1259
  preset?: CapabilityPreset;
1243
1260
  now?: number;
1261
+ /**
1262
+ * The device's Noise static public key, base64, when pairing completed a
1263
+ * handshake. Absent on a plaintext pairing, which stays exactly as it is
1264
+ * today: no key, no pin, no behaviour change.
1265
+ */
1266
+ e2eeStaticPub?: string;
1267
+ e2eeVersion?: number;
1244
1268
  }): RegisteredDevice;
1269
+ /** The device that owns a Noise static key, or null. */
1270
+ getByE2eeStaticPub(staticPub: string): DeviceRow | null;
1245
1271
  /**
1246
1272
  * Resolve a presented token to a device, or null.
1247
1273
  *
package/dist/index.d.ts CHANGED
@@ -1204,6 +1204,11 @@ interface DeviceRow {
1204
1204
  created_at: number;
1205
1205
  last_seen_at: number | null;
1206
1206
  revoked_at: number | null;
1207
+ /** Noise static public key, base64. Null on every row that predates E2EE. */
1208
+ e2ee_static_pub: string | null;
1209
+ /** The downgrade lock. 1 once this device has completed a handshake. */
1210
+ e2ee_required: number;
1211
+ e2ee_version: number | null;
1207
1212
  }
1208
1213
  /** A device as reported over the API. Deliberately carries no credential. */
1209
1214
  interface DeviceView {
@@ -1213,6 +1218,16 @@ interface DeviceView {
1213
1218
  createdAt: number;
1214
1219
  lastSeenAt: number | null;
1215
1220
  revokedAt: number | null;
1221
+ /**
1222
+ * Whether this device is pinned to encryption.
1223
+ *
1224
+ * A boolean, never the key and never a hash of it: `GET /api/devices` exists
1225
+ * so a user can spot a device they did not pair, and answering "is this one
1226
+ * encrypted" needs no key material to do it. Publishing the static key would
1227
+ * hand an attacker who reached this endpoint the value that identifies a
1228
+ * device.
1229
+ */
1230
+ e2ee: boolean;
1216
1231
  }
1217
1232
  interface RegisteredDevice {
1218
1233
  deviceId: string;
@@ -1229,6 +1244,8 @@ declare class DevicesRepository {
1229
1244
  private touchStmt;
1230
1245
  private deleteStmt;
1231
1246
  private deleteRevokedStmt;
1247
+ private byE2eeStaticPubStmt;
1248
+ private repairStmt;
1232
1249
  constructor(db: Database.Database);
1233
1250
  /**
1234
1251
  * Record a newly paired device and mint its token.
@@ -1241,7 +1258,16 @@ declare class DevicesRepository {
1241
1258
  name?: string | null;
1242
1259
  preset?: CapabilityPreset;
1243
1260
  now?: number;
1261
+ /**
1262
+ * The device's Noise static public key, base64, when pairing completed a
1263
+ * handshake. Absent on a plaintext pairing, which stays exactly as it is
1264
+ * today: no key, no pin, no behaviour change.
1265
+ */
1266
+ e2eeStaticPub?: string;
1267
+ e2eeVersion?: number;
1244
1268
  }): RegisteredDevice;
1269
+ /** The device that owns a Noise static key, or null. */
1270
+ getByE2eeStaticPub(staticPub: string): DeviceRow | null;
1245
1271
  /**
1246
1272
  * Resolve a presented token to a device, or null.
1247
1273
  *
package/dist/index.js CHANGED
@@ -4135,7 +4135,12 @@ function toDeviceView(row) {
4135
4135
  capabilities: parseCapabilities(row.capabilities),
4136
4136
  createdAt: row.created_at,
4137
4137
  lastSeenAt: row.last_seen_at,
4138
- revokedAt: row.revoked_at
4138
+ revokedAt: row.revoked_at,
4139
+ // Reported from `e2ee_required` rather than from the key's presence. The
4140
+ // two agree today, but they answer different questions — the key is what a
4141
+ // handshake is checked against, `e2ee_required` is whether plaintext is
4142
+ // refused — and it is the second one a user is asking about.
4143
+ e2ee: row.e2ee_required === 1
4139
4144
  };
4140
4145
  }
4141
4146
  var DevicesRepository = class {
@@ -4147,14 +4152,30 @@ var DevicesRepository = class {
4147
4152
  touchStmt;
4148
4153
  deleteStmt;
4149
4154
  deleteRevokedStmt;
4155
+ byE2eeStaticPubStmt;
4156
+ repairStmt;
4150
4157
  constructor(db) {
4151
4158
  this.insertStmt = db.prepare(`
4152
4159
  INSERT INTO devices (
4153
- device_id, public_key, token_hash, name, capabilities, created_at
4160
+ device_id, public_key, token_hash, name, capabilities, created_at,
4161
+ e2ee_static_pub, e2ee_required, e2ee_version
4154
4162
  ) VALUES (
4155
- @device_id, @public_key, @token_hash, @name, @capabilities, @created_at
4163
+ @device_id, @public_key, @token_hash, @name, @capabilities, @created_at,
4164
+ @e2ee_static_pub, @e2ee_required, @e2ee_version
4156
4165
  )
4157
4166
  `);
4167
+ this.byE2eeStaticPubStmt = db.prepare("SELECT * FROM devices WHERE e2ee_static_pub = ?");
4168
+ this.repairStmt = db.prepare(`
4169
+ UPDATE devices SET
4170
+ public_key = @public_key,
4171
+ token_hash = @token_hash,
4172
+ name = @name,
4173
+ capabilities = @capabilities,
4174
+ e2ee_required = 1,
4175
+ e2ee_version = @e2ee_version,
4176
+ revoked_at = NULL
4177
+ WHERE device_id = @device_id
4178
+ `);
4158
4179
  this.byTokenHashStmt = db.prepare("SELECT * FROM devices WHERE token_hash = ?");
4159
4180
  this.byIdStmt = db.prepare("SELECT * FROM devices WHERE device_id = ?");
4160
4181
  this.listStmt = db.prepare("SELECT * FROM devices ORDER BY created_at DESC");
@@ -4170,19 +4191,42 @@ var DevicesRepository = class {
4170
4191
  * moment it exists outside the client.
4171
4192
  */
4172
4193
  register(args) {
4173
- const deviceId = randomUUID3();
4174
4194
  const deviceToken = generateDeviceToken();
4175
4195
  const capabilities = capabilitiesForPreset(args.preset ?? "full");
4196
+ const now = args.now ?? Date.now();
4197
+ const existing = args.e2eeStaticPub ? this.byE2eeStaticPubStmt.get(args.e2eeStaticPub) : void 0;
4198
+ if (existing) {
4199
+ this.repairStmt.run({
4200
+ device_id: existing.device_id,
4201
+ public_key: args.publicKey,
4202
+ token_hash: hashDeviceToken(deviceToken),
4203
+ name: args.name ?? null,
4204
+ capabilities: JSON.stringify(capabilities),
4205
+ e2ee_version: args.e2eeVersion ?? null
4206
+ });
4207
+ return { deviceId: existing.device_id, deviceToken, capabilities };
4208
+ }
4209
+ const deviceId = randomUUID3();
4176
4210
  this.insertStmt.run({
4177
4211
  device_id: deviceId,
4178
4212
  public_key: args.publicKey,
4179
4213
  token_hash: hashDeviceToken(deviceToken),
4180
4214
  name: args.name ?? null,
4181
4215
  capabilities: JSON.stringify(capabilities),
4182
- created_at: args.now ?? Date.now()
4216
+ created_at: now,
4217
+ e2ee_static_pub: args.e2eeStaticPub ?? null,
4218
+ // The downgrade lock, set in the same write that records the key. Once
4219
+ // set, nothing a client can send clears it (design.md §6.3) — which is
4220
+ // what makes it a lock rather than a preference.
4221
+ e2ee_required: args.e2eeStaticPub ? 1 : 0,
4222
+ e2ee_version: args.e2eeVersion ?? null
4183
4223
  });
4184
4224
  return { deviceId, deviceToken, capabilities };
4185
4225
  }
4226
+ /** The device that owns a Noise static key, or null. */
4227
+ getByE2eeStaticPub(staticPub) {
4228
+ return this.byE2eeStaticPubStmt.get(staticPub) ?? null;
4229
+ }
4186
4230
  /**
4187
4231
  * Resolve a presented token to a device, or null.
4188
4232
  *