@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/cli.cjs +49 -5
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +49 -5
- 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 +49 -5
- 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.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:
|
|
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
|
*
|