@ours.network/cowork 1.0.2 → 1.0.4
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/README.md +1 -1
- package/dist/cli.js +1 -1
- package/dist/daemon.js +773 -567
- package/package.json +3 -1
package/dist/daemon.js
CHANGED
|
@@ -5736,6 +5736,20 @@ function sameReply2(stored, observed) {
|
|
|
5736
5736
|
if (stored === void 0 || observed == null) return stored === void 0 && observed == null;
|
|
5737
5737
|
return stored.wire_id === observed.wire_id && stored.sentence === observed.sentence;
|
|
5738
5738
|
}
|
|
5739
|
+
async function queryStore(store, roomId, options) {
|
|
5740
|
+
if (store.query) return store.query(roomId, options);
|
|
5741
|
+
let records = await store.read(roomId);
|
|
5742
|
+
records = records.filter((record) => {
|
|
5743
|
+
const value = record;
|
|
5744
|
+
return (options.kind === void 0 || record.kind === options.kind) && (options.messageId === void 0 || value.message_id === options.messageId) && (options.fileId === void 0 || value.file_id === options.fileId) && (options.sourceMsgId === void 0 || value.source_msg_id === options.sourceMsgId) && (options.sourceFileId === void 0 || value.source_file_id === options.sourceFileId) && (options.recipientIdentity === void 0 || value.recipient_identity === options.recipientIdentity);
|
|
5745
|
+
});
|
|
5746
|
+
if (options.unresolvedResultKind) {
|
|
5747
|
+
const completed = new Set((await store.read(roomId)).filter((record) => record.kind === options.unresolvedResultKind).map((record) => record.intent_record_id));
|
|
5748
|
+
records = records.filter((record) => !completed.has(record.record_id));
|
|
5749
|
+
}
|
|
5750
|
+
if (options.descending) records.reverse();
|
|
5751
|
+
return records.slice(0, options.limit);
|
|
5752
|
+
}
|
|
5739
5753
|
function canonicalValue(value) {
|
|
5740
5754
|
if (Array.isArray(value)) return value.map(canonicalValue);
|
|
5741
5755
|
if (value !== null && typeof value === "object") {
|
|
@@ -5763,13 +5777,14 @@ function wireKind(category) {
|
|
|
5763
5777
|
return "room_msg";
|
|
5764
5778
|
}
|
|
5765
5779
|
}
|
|
5766
|
-
var INTAKE_BATCH_SIZE, IntakePump;
|
|
5780
|
+
var JOURNAL_WORK_BATCH_SIZE, INTAKE_BATCH_SIZE, IntakePump;
|
|
5767
5781
|
var init_intake = __esm({
|
|
5768
5782
|
"src/intake.ts"() {
|
|
5769
5783
|
"use strict";
|
|
5770
5784
|
init_zod();
|
|
5771
5785
|
init_contracts();
|
|
5772
5786
|
init_ulid();
|
|
5787
|
+
JOURNAL_WORK_BATCH_SIZE = 64;
|
|
5773
5788
|
INTAKE_BATCH_SIZE = 32;
|
|
5774
5789
|
IntakePump = class {
|
|
5775
5790
|
store;
|
|
@@ -5877,8 +5892,8 @@ var init_intake = __esm({
|
|
|
5877
5892
|
await packet.acknowledgeFile(item);
|
|
5878
5893
|
return;
|
|
5879
5894
|
}
|
|
5880
|
-
const
|
|
5881
|
-
let file = this.findSourceFile(
|
|
5895
|
+
const [storedFile] = await queryStore(this.store, roomId, { kind: "file", sourceFileId: item.file_id, limit: 1 });
|
|
5896
|
+
let file = this.findSourceFile(storedFile === void 0 ? [] : [storedFile], item);
|
|
5882
5897
|
if (!file) {
|
|
5883
5898
|
const recipientIdentities = unique(room.seats.filter((recipient) => recipient.state === "active").map((recipient) => recipient.identity).filter((identity) => identity !== seat.identity));
|
|
5884
5899
|
const bytes = Buffer.from(item.data);
|
|
@@ -5916,8 +5931,8 @@ var init_intake = __esm({
|
|
|
5916
5931
|
if (acknowledge) await this.acknowledgeMessage(roomId, packet, item);
|
|
5917
5932
|
return;
|
|
5918
5933
|
}
|
|
5919
|
-
const
|
|
5920
|
-
let message = this.findSourceMessage(
|
|
5934
|
+
const [storedMessage] = await queryStore(this.store, roomId, { kind: "message", sourceMsgId: item.msg_id, limit: 1 });
|
|
5935
|
+
let message = this.findSourceMessage(storedMessage === void 0 ? [] : [storedMessage], item);
|
|
5921
5936
|
if (!message) {
|
|
5922
5937
|
const recipientIdentities = unique(room.seats.filter((recipient) => recipient.state === "active").map((recipient) => recipient.identity).filter((identity) => identity !== seat.identity));
|
|
5923
5938
|
const appended = await this.store.append(roomId, {
|
|
@@ -5980,100 +5995,153 @@ var init_intake = __esm({
|
|
|
5980
5995
|
}
|
|
5981
5996
|
}
|
|
5982
5997
|
async completeSnapshotIntents(roomId) {
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
(
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
(
|
|
5989
|
-
|
|
5998
|
+
if (!this.store.recordsNeedingRelayIntents) {
|
|
5999
|
+
const records = await this.store.read(roomId);
|
|
6000
|
+
for (const message of records.filter(
|
|
6001
|
+
(record) => record.kind === "message"
|
|
6002
|
+
)) await this.completeMessageIntents(roomId, message);
|
|
6003
|
+
for (const file of records.filter(
|
|
6004
|
+
(record) => record.kind === "file"
|
|
6005
|
+
)) await this.completeFileIntents(roomId, file);
|
|
6006
|
+
return;
|
|
6007
|
+
}
|
|
6008
|
+
for (; ; ) {
|
|
6009
|
+
const records = await this.store.recordsNeedingRelayIntents(
|
|
6010
|
+
roomId,
|
|
6011
|
+
{ limit: JOURNAL_WORK_BATCH_SIZE }
|
|
6012
|
+
);
|
|
6013
|
+
if (records.length === 0) return;
|
|
6014
|
+
for (const record of records) {
|
|
6015
|
+
if (record.kind === "message") await this.completeMessageIntents(roomId, record);
|
|
6016
|
+
else if (record.kind === "file") await this.completeFileIntents(roomId, record);
|
|
6017
|
+
}
|
|
6018
|
+
}
|
|
5990
6019
|
}
|
|
5991
6020
|
async completeFileIntents(roomId, file) {
|
|
5992
|
-
|
|
5993
|
-
|
|
6021
|
+
if (this.store.relayRecipientsNeedingIntent) {
|
|
6022
|
+
for (const recipientIdentity of await this.store.relayRecipientsNeedingIntent(roomId, file.seq)) {
|
|
6023
|
+
await this.appendFileIntent(roomId, file.file_id, recipientIdentity);
|
|
6024
|
+
}
|
|
6025
|
+
return;
|
|
6026
|
+
}
|
|
6027
|
+
const records = await queryStore(this.store, roomId, { kind: "relay_intent", fileId: file.file_id });
|
|
6028
|
+
const intended = new Set(records.map((record) => record.recipient_identity));
|
|
5994
6029
|
for (const recipientIdentity of file.recipient_identities) {
|
|
5995
6030
|
if (intended.has(recipientIdentity)) continue;
|
|
5996
|
-
await this.
|
|
5997
|
-
version: 1,
|
|
5998
|
-
kind: "relay_intent",
|
|
5999
|
-
room_id: roomId,
|
|
6000
|
-
at: this.now(),
|
|
6001
|
-
file_id: file.file_id,
|
|
6002
|
-
recipient_identity: recipientIdentity
|
|
6003
|
-
});
|
|
6031
|
+
await this.appendFileIntent(roomId, file.file_id, recipientIdentity);
|
|
6004
6032
|
intended.add(recipientIdentity);
|
|
6005
6033
|
}
|
|
6006
6034
|
}
|
|
6035
|
+
async appendFileIntent(roomId, fileId, recipientIdentity) {
|
|
6036
|
+
await this.store.append(roomId, {
|
|
6037
|
+
version: 1,
|
|
6038
|
+
kind: "relay_intent",
|
|
6039
|
+
room_id: roomId,
|
|
6040
|
+
at: this.now(),
|
|
6041
|
+
file_id: fileId,
|
|
6042
|
+
recipient_identity: recipientIdentity
|
|
6043
|
+
});
|
|
6044
|
+
}
|
|
6007
6045
|
async completeMessageIntents(roomId, message) {
|
|
6008
|
-
|
|
6009
|
-
|
|
6046
|
+
if (this.store.relayRecipientsNeedingIntent) {
|
|
6047
|
+
for (const recipientIdentity of await this.store.relayRecipientsNeedingIntent(roomId, message.seq)) {
|
|
6048
|
+
await this.appendMessageIntent(roomId, message.message_id, recipientIdentity);
|
|
6049
|
+
}
|
|
6050
|
+
return;
|
|
6051
|
+
}
|
|
6052
|
+
const records = await queryStore(this.store, roomId, { kind: "relay_intent", messageId: message.message_id });
|
|
6053
|
+
const intended = new Set(records.map((record) => record.recipient_identity));
|
|
6010
6054
|
for (const recipientIdentity of message.recipient_identities) {
|
|
6011
6055
|
if (intended.has(recipientIdentity)) continue;
|
|
6012
|
-
await this.
|
|
6013
|
-
version: 1,
|
|
6014
|
-
kind: "relay_intent",
|
|
6015
|
-
room_id: roomId,
|
|
6016
|
-
at: this.now(),
|
|
6017
|
-
message_id: message.message_id,
|
|
6018
|
-
recipient_identity: recipientIdentity
|
|
6019
|
-
});
|
|
6056
|
+
await this.appendMessageIntent(roomId, message.message_id, recipientIdentity);
|
|
6020
6057
|
intended.add(recipientIdentity);
|
|
6021
6058
|
}
|
|
6022
6059
|
}
|
|
6060
|
+
async appendMessageIntent(roomId, messageId, recipientIdentity) {
|
|
6061
|
+
await this.store.append(roomId, {
|
|
6062
|
+
version: 1,
|
|
6063
|
+
kind: "relay_intent",
|
|
6064
|
+
room_id: roomId,
|
|
6065
|
+
at: this.now(),
|
|
6066
|
+
message_id: messageId,
|
|
6067
|
+
recipient_identity: recipientIdentity
|
|
6068
|
+
});
|
|
6069
|
+
}
|
|
6023
6070
|
async relayPendingUnlocked(roomId, packet) {
|
|
6024
|
-
const records = await this.store.read(roomId);
|
|
6025
6071
|
const room = await this.store.load(roomId);
|
|
6026
6072
|
const activeCids = new Set(room.seats.filter((seat) => seat.state === "active").map((seat) => seat.identity));
|
|
6027
6073
|
const removedCids = new Set(room.seats.filter((seat) => seat.state === "removed").map((seat) => seat.identity));
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6074
|
+
let after = 0;
|
|
6075
|
+
for (; ; ) {
|
|
6076
|
+
const pending = await queryStore(this.store, roomId, {
|
|
6077
|
+
kind: "relay_intent",
|
|
6078
|
+
unresolvedResultKind: "relay_result",
|
|
6079
|
+
after,
|
|
6080
|
+
limit: JOURNAL_WORK_BATCH_SIZE
|
|
6081
|
+
});
|
|
6082
|
+
if (pending.length === 0) return;
|
|
6083
|
+
for (const intent of pending) {
|
|
6084
|
+
after = intent.seq;
|
|
6085
|
+
const [message] = intent.message_id === void 0 ? [] : await queryStore(this.store, roomId, { kind: "message", messageId: intent.message_id, limit: 1 });
|
|
6086
|
+
const [file] = intent.file_id === void 0 ? [] : await queryStore(this.store, roomId, { kind: "file", fileId: intent.file_id, limit: 1 });
|
|
6087
|
+
if (message === void 0 === (file === void 0)) continue;
|
|
6088
|
+
const recipients = message?.recipient_identities ?? file.recipient_identities;
|
|
6089
|
+
if (!recipients.includes(intent.recipient_identity)) continue;
|
|
6090
|
+
if (!activeCids.has(intent.recipient_identity) && removedCids.has(intent.recipient_identity)) {
|
|
6091
|
+
const skipped = await this.store.append(roomId, {
|
|
6092
|
+
version: 1,
|
|
6093
|
+
kind: "relay_result",
|
|
6094
|
+
room_id: roomId,
|
|
6095
|
+
at: this.now(),
|
|
6096
|
+
intent_record_id: intent.record_id,
|
|
6097
|
+
...intent.message_id === void 0 ? {} : { message_id: intent.message_id },
|
|
6098
|
+
...intent.file_id === void 0 ? {} : { file_id: intent.file_id },
|
|
6099
|
+
recipient_identity: intent.recipient_identity,
|
|
6100
|
+
status: "skipped_removed"
|
|
6101
|
+
});
|
|
6102
|
+
if (skipped.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
|
|
6103
|
+
continue;
|
|
6104
|
+
}
|
|
6105
|
+
if (file !== void 0) {
|
|
6106
|
+
const author = file.author_alias === void 0 ? file.author : {
|
|
6107
|
+
identity: file.author_alias.participant_id,
|
|
6108
|
+
display_name: file.author_alias.alias,
|
|
6109
|
+
role: file.author.role
|
|
6110
|
+
};
|
|
6111
|
+
const metadata = await sendRoomBody(packet, intent.recipient_identity, {
|
|
6112
|
+
version: 1,
|
|
6113
|
+
kind: "room_file",
|
|
6114
|
+
room_id: roomId,
|
|
6115
|
+
room_name: room.room_name,
|
|
6116
|
+
file_id: file.file_id,
|
|
6117
|
+
author,
|
|
6118
|
+
filename: file.filename,
|
|
6119
|
+
mime: file.mime,
|
|
6120
|
+
size: file.size,
|
|
6121
|
+
sha256: file.sha256,
|
|
6122
|
+
at: file.at
|
|
6123
|
+
});
|
|
6124
|
+
if (metadata.status === "send_failed") {
|
|
6125
|
+
const failed = await this.store.append(roomId, {
|
|
6126
|
+
version: 1,
|
|
6127
|
+
kind: "relay_result",
|
|
6128
|
+
room_id: roomId,
|
|
6129
|
+
at: this.now(),
|
|
6130
|
+
intent_record_id: intent.record_id,
|
|
6131
|
+
file_id: file.file_id,
|
|
6132
|
+
recipient_identity: intent.recipient_identity,
|
|
6133
|
+
status: "send_failed"
|
|
6134
|
+
});
|
|
6135
|
+
if (failed.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
|
|
6136
|
+
continue;
|
|
6137
|
+
}
|
|
6138
|
+
const outcome2 = await packet.sendFile(
|
|
6139
|
+
intent.recipient_identity,
|
|
6140
|
+
file.filename,
|
|
6141
|
+
file.mime,
|
|
6142
|
+
Buffer.from(file.data_base64, "base64")
|
|
6143
|
+
);
|
|
6144
|
+
const appended2 = await this.store.append(roomId, {
|
|
6077
6145
|
version: 1,
|
|
6078
6146
|
kind: "relay_result",
|
|
6079
6147
|
room_id: roomId,
|
|
@@ -6081,66 +6149,45 @@ var init_intake = __esm({
|
|
|
6081
6149
|
intent_record_id: intent.record_id,
|
|
6082
6150
|
file_id: file.file_id,
|
|
6083
6151
|
recipient_identity: intent.recipient_identity,
|
|
6084
|
-
status:
|
|
6152
|
+
status: outcome2.status,
|
|
6153
|
+
...outcome2.wire_id === void 0 || outcome2.wire_id === "" ? {} : { wire_id: outcome2.wire_id },
|
|
6154
|
+
...metadata.wire_id === void 0 || metadata.wire_id === "" ? {} : { metadata_wire_id: metadata.wire_id }
|
|
6085
6155
|
});
|
|
6086
|
-
if (
|
|
6087
|
-
completed.add(intent.record_id);
|
|
6156
|
+
if (appended2.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
|
|
6088
6157
|
continue;
|
|
6089
6158
|
}
|
|
6090
|
-
const
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6159
|
+
const unsigned = {
|
|
6160
|
+
version: 1,
|
|
6161
|
+
kind: wireKind(message.category),
|
|
6162
|
+
room_id: roomId,
|
|
6163
|
+
room_name: room.room_name,
|
|
6164
|
+
message_id: message.message_id,
|
|
6165
|
+
// An anonymous author leaves the archive only in alias form.
|
|
6166
|
+
author: message.author_alias === void 0 ? message.author : {
|
|
6167
|
+
identity: message.author_alias.participant_id,
|
|
6168
|
+
display_name: message.author_alias.alias,
|
|
6169
|
+
role: message.author.role
|
|
6170
|
+
},
|
|
6171
|
+
text: message.text,
|
|
6172
|
+
at: message.at,
|
|
6173
|
+
...message.briefing_role === void 0 ? {} : { briefing_role: message.briefing_role },
|
|
6174
|
+
...message.briefing_version === void 0 ? {} : { briefing_version: message.briefing_version },
|
|
6175
|
+
...message.membership === void 0 ? {} : { membership: message.membership }
|
|
6176
|
+
};
|
|
6177
|
+
const outcome = await sendRoomBody(packet, intent.recipient_identity, unsigned);
|
|
6178
|
+
const appended = await this.store.append(roomId, {
|
|
6097
6179
|
version: 1,
|
|
6098
6180
|
kind: "relay_result",
|
|
6099
6181
|
room_id: roomId,
|
|
6100
6182
|
at: this.now(),
|
|
6101
6183
|
intent_record_id: intent.record_id,
|
|
6102
|
-
|
|
6184
|
+
message_id: intent.message_id,
|
|
6103
6185
|
recipient_identity: intent.recipient_identity,
|
|
6104
|
-
status:
|
|
6105
|
-
...
|
|
6106
|
-
...metadata.wire_id === void 0 || metadata.wire_id === "" ? {} : { metadata_wire_id: metadata.wire_id }
|
|
6186
|
+
status: outcome.status,
|
|
6187
|
+
...outcome.wire_id === void 0 || outcome.wire_id === "" ? {} : { wire_id: outcome.wire_id }
|
|
6107
6188
|
});
|
|
6108
|
-
if (
|
|
6109
|
-
completed.add(intent.record_id);
|
|
6110
|
-
continue;
|
|
6189
|
+
if (appended.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
|
|
6111
6190
|
}
|
|
6112
|
-
const unsigned = {
|
|
6113
|
-
version: 1,
|
|
6114
|
-
kind: wireKind(message.category),
|
|
6115
|
-
room_id: roomId,
|
|
6116
|
-
room_name: room.room_name,
|
|
6117
|
-
message_id: message.message_id,
|
|
6118
|
-
// An anonymous author leaves the archive only in alias form.
|
|
6119
|
-
author: message.author_alias === void 0 ? message.author : {
|
|
6120
|
-
identity: message.author_alias.participant_id,
|
|
6121
|
-
display_name: message.author_alias.alias,
|
|
6122
|
-
role: message.author.role
|
|
6123
|
-
},
|
|
6124
|
-
text: message.text,
|
|
6125
|
-
at: message.at,
|
|
6126
|
-
...message.briefing_role === void 0 ? {} : { briefing_role: message.briefing_role },
|
|
6127
|
-
...message.briefing_version === void 0 ? {} : { briefing_version: message.briefing_version },
|
|
6128
|
-
...message.membership === void 0 ? {} : { membership: message.membership }
|
|
6129
|
-
};
|
|
6130
|
-
const outcome = await sendRoomBody(packet, intent.recipient_identity, unsigned);
|
|
6131
|
-
const appended = await this.store.append(roomId, {
|
|
6132
|
-
version: 1,
|
|
6133
|
-
kind: "relay_result",
|
|
6134
|
-
room_id: roomId,
|
|
6135
|
-
at: this.now(),
|
|
6136
|
-
intent_record_id: intent.record_id,
|
|
6137
|
-
message_id: intent.message_id,
|
|
6138
|
-
recipient_identity: intent.recipient_identity,
|
|
6139
|
-
status: outcome.status,
|
|
6140
|
-
...outcome.wire_id === void 0 || outcome.wire_id === "" ? {} : { wire_id: outcome.wire_id }
|
|
6141
|
-
});
|
|
6142
|
-
if (appended.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
|
|
6143
|
-
completed.add(intent.record_id);
|
|
6144
6191
|
}
|
|
6145
6192
|
}
|
|
6146
6193
|
findSourceMessage(records, item) {
|
|
@@ -6199,6 +6246,21 @@ function byteBoundedHistoryPage(records) {
|
|
|
6199
6246
|
function activeSeats(room) {
|
|
6200
6247
|
return room.seats.filter((seat) => seat.state === "active");
|
|
6201
6248
|
}
|
|
6249
|
+
async function queryStore2(store, roomId, options) {
|
|
6250
|
+
if (store.query) return store.query(roomId, options);
|
|
6251
|
+
let records = await store.read(roomId);
|
|
6252
|
+
records = records.filter((record) => {
|
|
6253
|
+
const value = record;
|
|
6254
|
+
const membership = value.membership;
|
|
6255
|
+
return (options.kind === void 0 || record.kind === options.kind) && (options.after === void 0 || record.seq > options.after) && (options.messageId === void 0 || value.message_id === options.messageId) && (options.fileId === void 0 || value.file_id === options.fileId) && (options.intentRecordId === void 0 || value.intent_record_id === options.intentRecordId) && (options.recipientIdentity === void 0 || value.recipient_identity === options.recipientIdentity) && (options.category === void 0 || value.category === options.category) && (options.membershipEpoch === void 0 || membership?.epoch === options.membershipEpoch);
|
|
6256
|
+
});
|
|
6257
|
+
if (options.unresolvedResultKind) {
|
|
6258
|
+
const completed = new Set((await store.read(roomId)).filter((record) => record.kind === options.unresolvedResultKind).map((record) => record.intent_record_id));
|
|
6259
|
+
records = records.filter((record) => !completed.has(record.record_id));
|
|
6260
|
+
}
|
|
6261
|
+
if (options.descending) records.reverse();
|
|
6262
|
+
return records.slice(0, options.limit ?? Number.MAX_SAFE_INTEGER);
|
|
6263
|
+
}
|
|
6202
6264
|
function isCancelledExternalSeat(seat) {
|
|
6203
6265
|
return seat.state === "removed" && seat.accepted_at === void 0 && seat.requested_at !== void 0 && seat.invite_sha256 !== void 0;
|
|
6204
6266
|
}
|
|
@@ -6211,7 +6273,7 @@ function uniqueIdentities(identities) {
|
|
|
6211
6273
|
function currentContactIdentities(packet) {
|
|
6212
6274
|
return new Set(packet.listContacts().map((contact) => contact.container_id));
|
|
6213
6275
|
}
|
|
6214
|
-
var CreateInviteInputSchema, HistoryOptionsSchema, DeleteRoomInputSchema, RemoveParticipantInputSchema, ReplaceParticipantInputSchema, RoomServiceError, RoomService;
|
|
6276
|
+
var CreateInviteInputSchema, HistoryOptionsSchema, JOURNAL_WORK_BATCH_SIZE2, DeleteRoomInputSchema, RemoveParticipantInputSchema, ReplaceParticipantInputSchema, RoomServiceError, RoomService;
|
|
6215
6277
|
var init_service = __esm({
|
|
6216
6278
|
"src/service.ts"() {
|
|
6217
6279
|
"use strict";
|
|
@@ -6238,6 +6300,7 @@ var init_service = __esm({
|
|
|
6238
6300
|
limit: external_exports.number().int().positive().safe().optional(),
|
|
6239
6301
|
view: external_exports.enum(["operator", "participant"]).optional()
|
|
6240
6302
|
}).strict();
|
|
6303
|
+
JOURNAL_WORK_BATCH_SIZE2 = 64;
|
|
6241
6304
|
DeleteRoomInputSchema = external_exports.object({
|
|
6242
6305
|
confirm: external_exports.literal(true)
|
|
6243
6306
|
}).strict();
|
|
@@ -6720,8 +6783,11 @@ var init_service = __esm({
|
|
|
6720
6783
|
membership_epoch: Math.max(current.membership_epoch, intent.epoch)
|
|
6721
6784
|
}));
|
|
6722
6785
|
}
|
|
6723
|
-
const
|
|
6724
|
-
|
|
6786
|
+
const [existing] = await queryStore2(this.store, current.room_id, {
|
|
6787
|
+
kind: "membership_result",
|
|
6788
|
+
intentRecordId: intent.record_id,
|
|
6789
|
+
limit: 1
|
|
6790
|
+
});
|
|
6725
6791
|
let outcome;
|
|
6726
6792
|
if (existing !== void 0 && existing.kind === "membership_result") {
|
|
6727
6793
|
outcome = {
|
|
@@ -6760,8 +6826,13 @@ var init_service = __esm({
|
|
|
6760
6826
|
};
|
|
6761
6827
|
}
|
|
6762
6828
|
async ensureMembershipNotice(room, intent) {
|
|
6763
|
-
const records = await this.store
|
|
6764
|
-
|
|
6829
|
+
const records = await queryStore2(this.store, room.room_id, {
|
|
6830
|
+
kind: "message",
|
|
6831
|
+
category: "membership",
|
|
6832
|
+
membershipEpoch: intent.epoch,
|
|
6833
|
+
limit: 1
|
|
6834
|
+
});
|
|
6835
|
+
const already = records.length > 0;
|
|
6765
6836
|
if (already) return;
|
|
6766
6837
|
const remaining = activeSeats(room);
|
|
6767
6838
|
if (remaining.length === 0) return;
|
|
@@ -6971,7 +7042,7 @@ var init_service = __esm({
|
|
|
6971
7042
|
async history(roomId, options = {}) {
|
|
6972
7043
|
const id = LowerCrockfordUlidSchema.parse(roomId);
|
|
6973
7044
|
const { view, ...page } = HistoryOptionsSchema.parse(options);
|
|
6974
|
-
const records = await this.store
|
|
7045
|
+
const records = view === "participant" ? await queryStore2(this.store, id, { kind: "message", after: page.after, limit: page.limit }) : await this.store.read(id, page);
|
|
6975
7046
|
if (view !== "participant") return byteBoundedHistoryPage(records);
|
|
6976
7047
|
const projected = records.filter((record) => record.kind === "message").map((record) => {
|
|
6977
7048
|
const {
|
|
@@ -7238,13 +7309,19 @@ var init_service = __esm({
|
|
|
7238
7309
|
invites,
|
|
7239
7310
|
membership_epoch: room.membership_epoch + activatedPending.length + newSeats.length
|
|
7240
7311
|
});
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7312
|
+
let membershipAfter = 0;
|
|
7313
|
+
for (; ; ) {
|
|
7314
|
+
const journal = await queryStore2(this.store, next.room_id, {
|
|
7315
|
+
kind: "membership_intent",
|
|
7316
|
+
unresolvedResultKind: "membership_result",
|
|
7317
|
+
after: membershipAfter,
|
|
7318
|
+
limit: JOURNAL_WORK_BATCH_SIZE2
|
|
7319
|
+
});
|
|
7320
|
+
if (journal.length === 0) break;
|
|
7321
|
+
for (const intent of journal) {
|
|
7322
|
+
membershipAfter = intent.seq;
|
|
7323
|
+
({ room: next } = await this.completeRemovalUnlocked(next, intent));
|
|
7324
|
+
}
|
|
7248
7325
|
}
|
|
7249
7326
|
const requirementsMet = invites.filter((invite) => invite.state !== "revoked").every((invite) => invite.accepted_cids.length >= invite.min_accepts);
|
|
7250
7327
|
const admitted = [...activatedPending, ...newSeats];
|
|
@@ -7261,23 +7338,31 @@ var init_service = __esm({
|
|
|
7261
7338
|
const packet = this.packets.get(roomId);
|
|
7262
7339
|
if (packet) {
|
|
7263
7340
|
await packet.refreshContacts();
|
|
7264
|
-
let records = await this.store.read(roomId);
|
|
7265
7341
|
let contacts = currentContactIdentities(packet);
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7342
|
+
let closeAfter = 0;
|
|
7343
|
+
for (; ; ) {
|
|
7344
|
+
const pending = await queryStore2(this.store, roomId, {
|
|
7345
|
+
kind: "close_notice_intent",
|
|
7346
|
+
unresolvedResultKind: "close_notice_result",
|
|
7347
|
+
after: closeAfter,
|
|
7348
|
+
limit: JOURNAL_WORK_BATCH_SIZE2
|
|
7349
|
+
});
|
|
7350
|
+
if (pending.length === 0) break;
|
|
7351
|
+
for (const intent of pending) {
|
|
7352
|
+
closeAfter = intent.seq;
|
|
7353
|
+
if (contacts.has(intent.recipient_identity)) continue;
|
|
7354
|
+
await this.appendUncertainCloseResult(roomId, intent);
|
|
7355
|
+
}
|
|
7274
7356
|
}
|
|
7275
7357
|
for (const recipientIdentity of contacts) {
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7358
|
+
const [existingIntent] = await queryStore2(this.store, roomId, {
|
|
7359
|
+
kind: "close_notice_intent",
|
|
7360
|
+
recipientIdentity,
|
|
7361
|
+
unresolvedResultKind: "close_notice_result",
|
|
7362
|
+
descending: true,
|
|
7363
|
+
limit: 1
|
|
7364
|
+
});
|
|
7365
|
+
let intent = existingIntent;
|
|
7281
7366
|
if (!intent) {
|
|
7282
7367
|
const appended2 = await this.store.append(roomId, {
|
|
7283
7368
|
version: 1,
|
|
@@ -7320,13 +7405,19 @@ var init_service = __esm({
|
|
|
7320
7405
|
`room "${roomId}" live-state purge left residue: ${residue.join(", ") || "packet registry entry"}`
|
|
7321
7406
|
);
|
|
7322
7407
|
}
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7408
|
+
let purgeAfter = 0;
|
|
7409
|
+
for (; ; ) {
|
|
7410
|
+
const afterPurge = await queryStore2(this.store, roomId, {
|
|
7411
|
+
kind: "close_notice_intent",
|
|
7412
|
+
unresolvedResultKind: "close_notice_result",
|
|
7413
|
+
after: purgeAfter,
|
|
7414
|
+
limit: JOURNAL_WORK_BATCH_SIZE2
|
|
7415
|
+
});
|
|
7416
|
+
if (afterPurge.length === 0) break;
|
|
7417
|
+
for (const intent of afterPurge) {
|
|
7418
|
+
purgeAfter = intent.seq;
|
|
7419
|
+
await this.appendUncertainCloseResult(roomId, intent);
|
|
7420
|
+
}
|
|
7330
7421
|
}
|
|
7331
7422
|
return this.store.save(RoomSchema.parse({ ...room, state: "closed", closed_at: this.now() }));
|
|
7332
7423
|
}
|
|
@@ -7386,18 +7477,29 @@ var init_service = __esm({
|
|
|
7386
7477
|
});
|
|
7387
7478
|
}
|
|
7388
7479
|
async ensureBriefingKind(room, recipients, briefing) {
|
|
7389
|
-
|
|
7480
|
+
if (this.store.briefingDeliveryTimes) {
|
|
7481
|
+
const recipientIdentities = uniqueIdentities(recipients.map((seat) => seat.identity));
|
|
7482
|
+
const deliveries = await this.store.briefingDeliveryTimes(room.room_id, {
|
|
7483
|
+
category: briefing.category,
|
|
7484
|
+
briefingRole: briefing.briefing_role,
|
|
7485
|
+
briefingVersion: briefing.briefing_version
|
|
7486
|
+
}, recipientIdentities);
|
|
7487
|
+
const missing2 = recipients.filter((seat) => !deliveries.has(seat.identity));
|
|
7488
|
+
const appendedAt2 = await this.appendBriefingForMissing(room, missing2, briefing);
|
|
7489
|
+
return [...deliveries.values(), ...appendedAt2 === void 0 ? [] : [appendedAt2]].sort()[0] ?? this.now();
|
|
7490
|
+
}
|
|
7491
|
+
const records = await queryStore2(this.store, room.room_id, {
|
|
7492
|
+
kind: "message",
|
|
7493
|
+
category: briefing.category
|
|
7494
|
+
});
|
|
7390
7495
|
const matching = records.filter((record) => record.kind === "message" && record.category === briefing.category && record.briefing_role === briefing.briefing_role && (record.briefing_version ?? 1) === briefing.briefing_version);
|
|
7391
|
-
const intentsByMessage = /* @__PURE__ */ new Map();
|
|
7392
|
-
for (const record of records) {
|
|
7393
|
-
if (record.kind !== "relay_intent" || record.message_id === void 0) continue;
|
|
7394
|
-
const intents = intentsByMessage.get(record.message_id) ?? /* @__PURE__ */ new Set();
|
|
7395
|
-
intents.add(record.recipient_identity);
|
|
7396
|
-
intentsByMessage.set(record.message_id, intents);
|
|
7397
|
-
}
|
|
7398
7496
|
const covered = /* @__PURE__ */ new Set();
|
|
7399
7497
|
for (const message of matching) {
|
|
7400
|
-
const
|
|
7498
|
+
const intentRecords = await queryStore2(this.store, room.room_id, {
|
|
7499
|
+
kind: "relay_intent",
|
|
7500
|
+
messageId: message.message_id
|
|
7501
|
+
});
|
|
7502
|
+
const intents = new Set(intentRecords.map((record) => record.recipient_identity));
|
|
7401
7503
|
for (const recipientIdentity of message.recipient_identities) {
|
|
7402
7504
|
covered.add(recipientIdentity);
|
|
7403
7505
|
if (!intents.has(recipientIdentity)) {
|
|
@@ -7413,35 +7515,36 @@ var init_service = __esm({
|
|
|
7413
7515
|
}
|
|
7414
7516
|
}
|
|
7415
7517
|
const missing = recipients.filter((seat) => !covered.has(seat.identity));
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7518
|
+
const appendedAt = await this.appendBriefingForMissing(room, missing, briefing);
|
|
7519
|
+
return matching[0]?.at ?? appendedAt ?? this.now();
|
|
7520
|
+
}
|
|
7521
|
+
async appendBriefingForMissing(room, missing, briefing) {
|
|
7522
|
+
if (missing.length === 0) return void 0;
|
|
7523
|
+
const appended = await this.store.append(room.room_id, {
|
|
7524
|
+
version: 1,
|
|
7525
|
+
kind: "message",
|
|
7526
|
+
room_id: room.room_id,
|
|
7527
|
+
at: this.now(),
|
|
7528
|
+
message_id: LowerCrockfordUlidSchema.parse(this.nextMessageId()),
|
|
7529
|
+
author: { identity: room.identity_cid, display_name: room.identity_name, role: ROOM_ROLE },
|
|
7530
|
+
category: briefing.category,
|
|
7531
|
+
...briefing.briefing_role === void 0 ? {} : { briefing_role: briefing.briefing_role },
|
|
7532
|
+
briefing_version: briefing.briefing_version,
|
|
7533
|
+
text: briefing.text,
|
|
7534
|
+
recipient_identities: uniqueIdentities(missing.map((seat) => seat.identity))
|
|
7535
|
+
});
|
|
7536
|
+
if (appended.kind !== "message") throw new RoomServiceError("storage returned the wrong briefing record kind");
|
|
7537
|
+
for (const recipientIdentity of appended.recipient_identities) {
|
|
7538
|
+
await this.store.append(room.room_id, {
|
|
7419
7539
|
version: 1,
|
|
7420
|
-
kind: "
|
|
7540
|
+
kind: "relay_intent",
|
|
7421
7541
|
room_id: room.room_id,
|
|
7422
7542
|
at: this.now(),
|
|
7423
|
-
message_id:
|
|
7424
|
-
|
|
7425
|
-
category: briefing.category,
|
|
7426
|
-
...briefing.briefing_role === void 0 ? {} : { briefing_role: briefing.briefing_role },
|
|
7427
|
-
briefing_version: briefing.briefing_version,
|
|
7428
|
-
text: briefing.text,
|
|
7429
|
-
recipient_identities: uniqueIdentities(missing.map((seat) => seat.identity))
|
|
7543
|
+
message_id: appended.message_id,
|
|
7544
|
+
recipient_identity: recipientIdentity
|
|
7430
7545
|
});
|
|
7431
|
-
if (appended.kind !== "message") throw new RoomServiceError("storage returned the wrong briefing record kind");
|
|
7432
|
-
appendedAt = appended.at;
|
|
7433
|
-
for (const recipientIdentity of appended.recipient_identities) {
|
|
7434
|
-
await this.store.append(room.room_id, {
|
|
7435
|
-
version: 1,
|
|
7436
|
-
kind: "relay_intent",
|
|
7437
|
-
room_id: room.room_id,
|
|
7438
|
-
at: this.now(),
|
|
7439
|
-
message_id: appended.message_id,
|
|
7440
|
-
recipient_identity: recipientIdentity
|
|
7441
|
-
});
|
|
7442
|
-
}
|
|
7443
7546
|
}
|
|
7444
|
-
return
|
|
7547
|
+
return appended.at;
|
|
7445
7548
|
}
|
|
7446
7549
|
lock(roomId, work) {
|
|
7447
7550
|
return this.store.mutex(roomId).runExclusive(work);
|
|
@@ -7475,8 +7578,9 @@ var init_service = __esm({
|
|
|
7475
7578
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
7476
7579
|
import * as nodeFs2 from "node:fs";
|
|
7477
7580
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7478
|
-
import { dirname as dirname2, join as join3 } from "node:path";
|
|
7479
|
-
|
|
7581
|
+
import { basename, dirname as dirname2, join as join3 } from "node:path";
|
|
7582
|
+
import Database from "better-sqlite3";
|
|
7583
|
+
var DIRECTORY_MODE2, FILE_MODE2, NO_FOLLOW2, SQLITE_SCHEMA_VERSION, DEFAULT_WORK_BATCH_SIZE, utf8Decoder, CoworkStorageError, RoomQueue, CoworkStore;
|
|
7480
7584
|
var init_storage = __esm({
|
|
7481
7585
|
"src/storage.ts"() {
|
|
7482
7586
|
"use strict";
|
|
@@ -7485,6 +7589,8 @@ var init_storage = __esm({
|
|
|
7485
7589
|
DIRECTORY_MODE2 = 448;
|
|
7486
7590
|
FILE_MODE2 = 384;
|
|
7487
7591
|
NO_FOLLOW2 = nodeFs2.constants.O_NOFOLLOW ?? 0;
|
|
7592
|
+
SQLITE_SCHEMA_VERSION = 1;
|
|
7593
|
+
DEFAULT_WORK_BATCH_SIZE = 64;
|
|
7488
7594
|
utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
7489
7595
|
CoworkStorageError = class extends Error {
|
|
7490
7596
|
constructor(message, options) {
|
|
@@ -7512,20 +7618,20 @@ var init_storage = __esm({
|
|
|
7512
7618
|
CoworkStore = class {
|
|
7513
7619
|
stateDir;
|
|
7514
7620
|
fs;
|
|
7515
|
-
|
|
7516
|
-
// in-process room FIFO, not an on-disk lock with stale-owner recovery.
|
|
7621
|
+
beforeRecordCommit;
|
|
7517
7622
|
roomMutexes = /* @__PURE__ */ new Map();
|
|
7518
7623
|
lockOwnership = new AsyncLocalStorage();
|
|
7519
|
-
|
|
7624
|
+
reconciledBlobRooms = /* @__PURE__ */ new Set();
|
|
7520
7625
|
constructor(stateDir, options = {}) {
|
|
7521
7626
|
if (!stateDir) throw new CoworkStorageError("state directory is required");
|
|
7522
7627
|
this.stateDir = stateDir;
|
|
7523
7628
|
this.fs = options.fs ?? nodeFs2;
|
|
7629
|
+
this.beforeRecordCommit = options.beforeRecordCommit;
|
|
7524
7630
|
}
|
|
7525
7631
|
mutex(roomId, work) {
|
|
7526
|
-
const
|
|
7527
|
-
if (work) return this.withRoomMutex(
|
|
7528
|
-
return { runExclusive: (nested) => this.withRoomMutex(
|
|
7632
|
+
const id = this.roomId(roomId);
|
|
7633
|
+
if (work) return this.withRoomMutex(id, work);
|
|
7634
|
+
return { runExclusive: (nested) => this.withRoomMutex(id, nested) };
|
|
7529
7635
|
}
|
|
7530
7636
|
withRoomMutex(roomId, work) {
|
|
7531
7637
|
const inherited = this.lockOwnership.getStore();
|
|
@@ -7533,15 +7639,10 @@ var init_storage = __esm({
|
|
|
7533
7639
|
if (ownership?.active) {
|
|
7534
7640
|
const nested = Promise.resolve().then(work);
|
|
7535
7641
|
ownership.pending.add(nested);
|
|
7536
|
-
void nested.then(
|
|
7537
|
-
()
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
(error) => {
|
|
7541
|
-
ownership.pending.delete(nested);
|
|
7542
|
-
ownership.failures.push(error);
|
|
7543
|
-
}
|
|
7544
|
-
);
|
|
7642
|
+
void nested.then(() => ownership.pending.delete(nested), (error) => {
|
|
7643
|
+
ownership.pending.delete(nested);
|
|
7644
|
+
ownership.failures.push(error);
|
|
7645
|
+
});
|
|
7545
7646
|
return nested;
|
|
7546
7647
|
}
|
|
7547
7648
|
let queue = this.roomMutexes.get(roomId);
|
|
@@ -7563,9 +7664,7 @@ var init_storage = __esm({
|
|
|
7563
7664
|
rootFailed = true;
|
|
7564
7665
|
rootFailure = error;
|
|
7565
7666
|
}
|
|
7566
|
-
while (acquired.pending.size > 0)
|
|
7567
|
-
await Promise.allSettled([...acquired.pending]);
|
|
7568
|
-
}
|
|
7667
|
+
while (acquired.pending.size > 0) await Promise.allSettled([...acquired.pending]);
|
|
7569
7668
|
} finally {
|
|
7570
7669
|
acquired.active = false;
|
|
7571
7670
|
}
|
|
@@ -7583,20 +7682,20 @@ var init_storage = __esm({
|
|
|
7583
7682
|
this.rejectSymlink(roomDir, "room directory");
|
|
7584
7683
|
throw new CoworkStorageError(`room "${room.room_id}" already exists`);
|
|
7585
7684
|
}
|
|
7586
|
-
let
|
|
7685
|
+
let created = false;
|
|
7587
7686
|
try {
|
|
7588
7687
|
this.fs.mkdirSync(roomDir, { mode: DIRECTORY_MODE2 });
|
|
7589
|
-
|
|
7688
|
+
created = true;
|
|
7590
7689
|
this.fs.chmodSync(roomDir, DIRECTORY_MODE2);
|
|
7690
|
+
this.fs.mkdirSync(this.blobsDirectory(room.room_id), { mode: DIRECTORY_MODE2 });
|
|
7691
|
+
this.fs.chmodSync(this.blobsDirectory(room.room_id), DIRECTORY_MODE2);
|
|
7591
7692
|
this.fsyncDirectory(roomDir);
|
|
7592
7693
|
this.fsyncDirectory(this.roomsDirectory());
|
|
7593
|
-
this.
|
|
7694
|
+
this.withDatabase(room.room_id, () => void 0, true);
|
|
7594
7695
|
this.atomicMetadataWrite(this.metadataPath(room.room_id), room);
|
|
7595
|
-
this.nextSequences.set(room.room_id, 1);
|
|
7596
7696
|
return room;
|
|
7597
7697
|
} catch (error) {
|
|
7598
|
-
|
|
7599
|
-
if (roomCreated) {
|
|
7698
|
+
if (created) {
|
|
7600
7699
|
try {
|
|
7601
7700
|
this.fs.rmSync(roomDir, { recursive: true, force: true });
|
|
7602
7701
|
this.fsyncDirectory(this.roomsDirectory());
|
|
@@ -7608,8 +7707,8 @@ var init_storage = __esm({
|
|
|
7608
7707
|
});
|
|
7609
7708
|
}
|
|
7610
7709
|
async load(roomId) {
|
|
7611
|
-
const
|
|
7612
|
-
return this.mutex(
|
|
7710
|
+
const id = this.roomId(roomId);
|
|
7711
|
+
return this.mutex(id, () => this.loadUnlocked(id));
|
|
7613
7712
|
}
|
|
7614
7713
|
async save(input) {
|
|
7615
7714
|
const room = RoomSchema.parse(input);
|
|
@@ -7621,176 +7720,435 @@ var init_storage = __esm({
|
|
|
7621
7720
|
}
|
|
7622
7721
|
async list() {
|
|
7623
7722
|
this.ensureBaseDirectories();
|
|
7624
|
-
const
|
|
7723
|
+
const ids = this.fs.readdirSync(this.roomsDirectory(), { withFileTypes: true }).filter((entry) => entry.isDirectory() && LowerCrockfordUlidSchema.safeParse(entry.name).success).map((entry) => entry.name).sort();
|
|
7625
7724
|
const rooms = [];
|
|
7626
|
-
for (const
|
|
7725
|
+
for (const id of ids) rooms.push(await this.load(id));
|
|
7627
7726
|
return rooms;
|
|
7628
7727
|
}
|
|
7629
7728
|
async append(roomId, input) {
|
|
7630
|
-
const
|
|
7729
|
+
const id = this.roomId(roomId);
|
|
7631
7730
|
const draft = AppendRecordSchema.parse(input);
|
|
7632
|
-
if (draft.room_id !==
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
...draft,
|
|
7645
|
-
seq: nextSequence,
|
|
7646
|
-
record_id: `${validRoomId}:${nextSequence}`
|
|
7647
|
-
});
|
|
7648
|
-
const bytes = Buffer.from(`${JSON.stringify(record)}
|
|
7649
|
-
`, "utf8");
|
|
7650
|
-
let fd;
|
|
7651
|
-
let originalSize;
|
|
7652
|
-
let writeStarted = false;
|
|
7731
|
+
if (draft.room_id !== id) throw new CoworkStorageError(`record room_id "${draft.room_id}" does not match room "${id}"`);
|
|
7732
|
+
return this.mutex(id, () => {
|
|
7733
|
+
this.assertRoomDirectory(id);
|
|
7734
|
+
if (!this.reconciledBlobRooms.has(id)) this.withDatabase(id, () => void 0);
|
|
7735
|
+
let blob;
|
|
7736
|
+
let storedDraft = draft;
|
|
7737
|
+
if (draft.kind === "file") {
|
|
7738
|
+
const bytes = Buffer.from(draft.data_base64, "base64");
|
|
7739
|
+
blob = this.persistBlob(id, draft.sha256, bytes);
|
|
7740
|
+
const { data_base64: _bytes, ...withoutBytes } = draft;
|
|
7741
|
+
storedDraft = withoutBytes;
|
|
7742
|
+
}
|
|
7653
7743
|
try {
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7744
|
+
return this.withDatabase(id, (db) => {
|
|
7745
|
+
const transaction = db.transaction(() => {
|
|
7746
|
+
const next = db.prepare("SELECT COALESCE(MAX(seq), 0) + 1 AS next FROM records").get().next;
|
|
7747
|
+
const record = { ...draft, seq: next, record_id: `${id}:${next}` };
|
|
7748
|
+
const stored = { ...storedDraft, seq: next, record_id: record.record_id };
|
|
7749
|
+
const values = this.indexValues(record);
|
|
7750
|
+
db.prepare(`INSERT INTO records
|
|
7751
|
+
(seq, record_id, kind, at, payload_json, blob_path, message_id, file_id, intent_record_id,
|
|
7752
|
+
recipient_identity, source_msg_id, source_file_id, category, briefing_role, briefing_version, membership_epoch)
|
|
7753
|
+
VALUES (@seq,@record_id,@kind,@at,@payload_json,@blob_path,@message_id,@file_id,@intent_record_id,
|
|
7754
|
+
@recipient_identity,@source_msg_id,@source_file_id,@category,@briefing_role,@briefing_version,@membership_epoch)`).run({ ...values, payload_json: JSON.stringify(stored), blob_path: blob?.path ?? null });
|
|
7755
|
+
if (record.kind === "message" || record.kind === "file") {
|
|
7756
|
+
const insert = db.prepare(`INSERT INTO record_recipients
|
|
7757
|
+
(record_seq, recipient_identity, category, briefing_role, briefing_version)
|
|
7758
|
+
VALUES (?, ?, ?, ?, ?)`);
|
|
7759
|
+
const enqueue = db.prepare("INSERT INTO relay_intent_work(record_seq, recipient_identity) VALUES (?, ?)");
|
|
7760
|
+
for (const recipient of record.recipient_identities) {
|
|
7761
|
+
insert.run(
|
|
7762
|
+
record.seq,
|
|
7763
|
+
recipient,
|
|
7764
|
+
record.kind === "message" ? record.category : null,
|
|
7765
|
+
record.kind === "message" ? record.briefing_role ?? null : null,
|
|
7766
|
+
record.kind === "message" ? record.briefing_version ?? 1 : null
|
|
7767
|
+
);
|
|
7768
|
+
enqueue.run(record.seq, recipient);
|
|
7769
|
+
}
|
|
7770
|
+
} else if (record.kind === "relay_intent") {
|
|
7771
|
+
const sourceColumn = record.message_id === void 0 ? "file_id" : "message_id";
|
|
7772
|
+
const sourceId = record.message_id ?? record.file_id;
|
|
7773
|
+
db.prepare(`DELETE FROM relay_intent_work
|
|
7774
|
+
WHERE recipient_identity = ? AND record_seq IN (
|
|
7775
|
+
SELECT seq FROM records WHERE kind = ? AND ${sourceColumn} = ?
|
|
7776
|
+
)`).run(record.recipient_identity, record.message_id === void 0 ? "file" : "message", sourceId);
|
|
7777
|
+
}
|
|
7778
|
+
this.beforeRecordCommit?.();
|
|
7779
|
+
return record;
|
|
7780
|
+
});
|
|
7781
|
+
return transaction.immediate();
|
|
7782
|
+
});
|
|
7666
7783
|
} catch (error) {
|
|
7667
|
-
this.
|
|
7668
|
-
|
|
7669
|
-
try {
|
|
7670
|
-
this.fs.ftruncateSync(fd, originalSize);
|
|
7671
|
-
this.fs.fsyncSync(fd);
|
|
7672
|
-
} catch {
|
|
7673
|
-
}
|
|
7674
|
-
}
|
|
7675
|
-
throw this.wrap(`failed to append room "${validRoomId}" archive`, error);
|
|
7676
|
-
} finally {
|
|
7677
|
-
if (fd !== void 0) {
|
|
7678
|
-
try {
|
|
7679
|
-
this.fs.closeSync(fd);
|
|
7680
|
-
} catch {
|
|
7681
|
-
}
|
|
7682
|
-
}
|
|
7784
|
+
if (blob?.created) this.removeUnreferencedBlob(id, blob.path);
|
|
7785
|
+
throw this.wrap(`failed to append room "${id}" archive`, error);
|
|
7683
7786
|
}
|
|
7684
7787
|
});
|
|
7685
7788
|
}
|
|
7686
7789
|
async read(roomId, options = {}) {
|
|
7687
|
-
const
|
|
7790
|
+
const id = this.roomId(roomId);
|
|
7688
7791
|
const after = options.after ?? 0;
|
|
7689
7792
|
const limit = options.limit ?? Number.MAX_SAFE_INTEGER;
|
|
7690
|
-
|
|
7793
|
+
this.validatePage(after, limit);
|
|
7794
|
+
return this.mutex(id, () => {
|
|
7795
|
+
this.assertRoomDirectory(id);
|
|
7796
|
+
return this.withDatabase(id, (db) => this.decodeRows(id, db.prepare(
|
|
7797
|
+
"SELECT seq,payload_json,blob_path FROM records WHERE seq > ? ORDER BY seq ASC LIMIT ?"
|
|
7798
|
+
).all(after, limit)));
|
|
7799
|
+
});
|
|
7800
|
+
}
|
|
7801
|
+
async query(roomId, options) {
|
|
7802
|
+
const id = this.roomId(roomId);
|
|
7803
|
+
const limit = options.limit ?? Number.MAX_SAFE_INTEGER;
|
|
7691
7804
|
if (!Number.isSafeInteger(limit) || limit < 1) throw new CoworkStorageError("limit must be a positive safe integer");
|
|
7692
|
-
return this.mutex(
|
|
7693
|
-
this.assertRoomDirectory(
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7805
|
+
return this.mutex(id, () => {
|
|
7806
|
+
this.assertRoomDirectory(id);
|
|
7807
|
+
return this.withDatabase(id, (db) => {
|
|
7808
|
+
const clauses = [];
|
|
7809
|
+
const values = [];
|
|
7810
|
+
const add = (column, value) => {
|
|
7811
|
+
if (value !== void 0) {
|
|
7812
|
+
clauses.push(`r.${column} = ?`);
|
|
7813
|
+
values.push(value);
|
|
7814
|
+
}
|
|
7815
|
+
};
|
|
7816
|
+
add("kind", options.kind);
|
|
7817
|
+
add("message_id", options.messageId);
|
|
7818
|
+
add("file_id", options.fileId);
|
|
7819
|
+
add("source_msg_id", options.sourceMsgId);
|
|
7820
|
+
add("source_file_id", options.sourceFileId);
|
|
7821
|
+
add("intent_record_id", options.intentRecordId);
|
|
7822
|
+
add("recipient_identity", options.recipientIdentity);
|
|
7823
|
+
add("category", options.category);
|
|
7824
|
+
add("membership_epoch", options.membershipEpoch);
|
|
7825
|
+
if (options.after !== void 0) {
|
|
7826
|
+
clauses.push("r.seq > ?");
|
|
7827
|
+
values.push(options.after);
|
|
7828
|
+
}
|
|
7829
|
+
if (options.unresolvedResultKind) {
|
|
7830
|
+
clauses.push("NOT EXISTS (SELECT 1 FROM records result WHERE result.kind = ? AND result.intent_record_id = r.record_id)");
|
|
7831
|
+
values.push(options.unresolvedResultKind);
|
|
7832
|
+
}
|
|
7833
|
+
values.push(limit);
|
|
7834
|
+
const sql = `SELECT r.seq,r.payload_json,r.blob_path FROM records r${clauses.length ? ` WHERE ${clauses.join(" AND ")}` : ""} ORDER BY r.seq ${options.descending ? "DESC" : "ASC"} LIMIT ?`;
|
|
7835
|
+
return this.decodeRows(id, db.prepare(sql).all(...values));
|
|
7836
|
+
});
|
|
7697
7837
|
});
|
|
7698
7838
|
}
|
|
7839
|
+
async recipients(roomId, recordSeq) {
|
|
7840
|
+
const id = this.roomId(roomId);
|
|
7841
|
+
return this.mutex(id, () => this.withDatabase(id, (db) => db.prepare(
|
|
7842
|
+
"SELECT recipient_identity FROM record_recipients WHERE record_seq = ? ORDER BY recipient_identity"
|
|
7843
|
+
).all(recordSeq).map((row) => row.recipient_identity)));
|
|
7844
|
+
}
|
|
7845
|
+
async recordsNeedingRelayIntents(roomId, options = {}) {
|
|
7846
|
+
const id = this.roomId(roomId);
|
|
7847
|
+
const after = options.after ?? 0;
|
|
7848
|
+
const limit = options.limit ?? DEFAULT_WORK_BATCH_SIZE;
|
|
7849
|
+
this.validatePage(after, limit);
|
|
7850
|
+
return this.mutex(id, () => this.withDatabase(id, (db) => this.decodeRows(id, db.prepare(`
|
|
7851
|
+
SELECT source.seq,source.payload_json,source.blob_path
|
|
7852
|
+
FROM relay_intent_work work INDEXED BY relay_work_source
|
|
7853
|
+
JOIN records source ON source.seq = work.record_seq
|
|
7854
|
+
WHERE work.record_seq > ?
|
|
7855
|
+
GROUP BY source.seq
|
|
7856
|
+
ORDER BY source.seq ASC
|
|
7857
|
+
LIMIT ?
|
|
7858
|
+
`).all(after, limit))));
|
|
7859
|
+
}
|
|
7860
|
+
async relayRecipientsNeedingIntent(roomId, recordSeq, limit = DEFAULT_WORK_BATCH_SIZE) {
|
|
7861
|
+
const id = this.roomId(roomId);
|
|
7862
|
+
if (!Number.isSafeInteger(recordSeq) || recordSeq < 1) throw new CoworkStorageError("record sequence must be a positive safe integer");
|
|
7863
|
+
if (!Number.isSafeInteger(limit) || limit < 1) throw new CoworkStorageError("limit must be a positive safe integer");
|
|
7864
|
+
return this.mutex(id, () => this.withDatabase(id, (db) => db.prepare(
|
|
7865
|
+
"SELECT recipient_identity FROM relay_intent_work WHERE record_seq = ? ORDER BY recipient_identity LIMIT ?"
|
|
7866
|
+
).all(recordSeq, limit).map((row) => row.recipient_identity)));
|
|
7867
|
+
}
|
|
7868
|
+
async briefingDeliveryTimes(roomId, key, recipientIdentities) {
|
|
7869
|
+
const id = this.roomId(roomId);
|
|
7870
|
+
if (recipientIdentities.length === 0) return /* @__PURE__ */ new Map();
|
|
7871
|
+
return this.mutex(id, () => this.withDatabase(id, (db) => {
|
|
7872
|
+
const lookup = db.prepare(`SELECT records.at FROM record_recipients recipients
|
|
7873
|
+
INDEXED BY recipients_briefing_delivery
|
|
7874
|
+
JOIN records ON records.seq = recipients.record_seq
|
|
7875
|
+
WHERE recipients.recipient_identity = ? AND recipients.category = ?
|
|
7876
|
+
AND recipients.briefing_role IS ? AND recipients.briefing_version = ?
|
|
7877
|
+
ORDER BY recipients.record_seq ASC LIMIT 1`);
|
|
7878
|
+
const deliveries = /* @__PURE__ */ new Map();
|
|
7879
|
+
for (const recipient of recipientIdentities) {
|
|
7880
|
+
const row = lookup.get(
|
|
7881
|
+
recipient,
|
|
7882
|
+
key.category,
|
|
7883
|
+
key.briefingRole ?? null,
|
|
7884
|
+
key.briefingVersion
|
|
7885
|
+
);
|
|
7886
|
+
if (row) deliveries.set(recipient, row.at);
|
|
7887
|
+
}
|
|
7888
|
+
return deliveries;
|
|
7889
|
+
}));
|
|
7890
|
+
}
|
|
7891
|
+
async durability(roomId) {
|
|
7892
|
+
const id = this.roomId(roomId);
|
|
7893
|
+
return this.mutex(id, () => this.withDatabase(id, (db) => ({
|
|
7894
|
+
journalMode: db.pragma("journal_mode", { simple: true }),
|
|
7895
|
+
synchronous: db.pragma("synchronous", { simple: true })
|
|
7896
|
+
})));
|
|
7897
|
+
}
|
|
7699
7898
|
async delete(roomId) {
|
|
7700
|
-
const
|
|
7701
|
-
await this.mutex(
|
|
7899
|
+
const id = this.roomId(roomId);
|
|
7900
|
+
await this.mutex(id, () => {
|
|
7702
7901
|
this.ensureBaseDirectories();
|
|
7703
|
-
const roomDir = this.roomDirectory(
|
|
7902
|
+
const roomDir = this.roomDirectory(id);
|
|
7704
7903
|
if (!this.lstatIfPresent(roomDir)) {
|
|
7705
7904
|
this.fsyncDirectory(this.roomsDirectory());
|
|
7706
|
-
this.nextSequences.delete(validRoomId);
|
|
7707
7905
|
return;
|
|
7708
7906
|
}
|
|
7709
|
-
this.ensurePrivateDirectory(roomDir, false, `room "${
|
|
7710
|
-
const
|
|
7711
|
-
const
|
|
7712
|
-
const archivePresent = this.lstatIfPresent(archivePath) !== void 0;
|
|
7713
|
-
const
|
|
7907
|
+
this.ensurePrivateDirectory(roomDir, false, `room "${id}" directory`);
|
|
7908
|
+
const metadata = this.metadataPath(id);
|
|
7909
|
+
const metadataPresent = this.lstatIfPresent(metadata) !== void 0;
|
|
7910
|
+
const archivePresent = this.lstatIfPresent(this.archivePath(id)) !== void 0;
|
|
7911
|
+
const blobsPresent = this.lstatIfPresent(this.blobsDirectory(id)) !== void 0;
|
|
7714
7912
|
if (metadataPresent) {
|
|
7715
|
-
const room = this.loadUnlocked(
|
|
7716
|
-
if (room.state !== "closed") {
|
|
7717
|
-
throw new CoworkStorageError(`room "${validRoomId}" must be closed before deletion`);
|
|
7718
|
-
}
|
|
7913
|
+
const room = this.loadUnlocked(id);
|
|
7914
|
+
if (room.state !== "closed") throw new CoworkStorageError(`room "${id}" must be closed before deletion`);
|
|
7719
7915
|
this.removeProvisioningArtifacts(roomDir);
|
|
7720
|
-
} else if (archivePresent) {
|
|
7721
|
-
throw new CoworkStorageError(
|
|
7722
|
-
`room "${validRoomId}" has archive residue without deletion metadata`
|
|
7723
|
-
);
|
|
7916
|
+
} else if (archivePresent || blobsPresent) {
|
|
7917
|
+
throw new CoworkStorageError(`room "${id}" has archive residue without deletion metadata`);
|
|
7724
7918
|
}
|
|
7725
|
-
const expected = /* @__PURE__ */ new Set(["archive.
|
|
7919
|
+
const expected = /* @__PURE__ */ new Set(["archive.sqlite3", "archive.sqlite3-wal", "archive.sqlite3-shm", "blobs", "room.json", "room.json.v1.bak"]);
|
|
7726
7920
|
const unexpected = this.fs.readdirSync(roomDir).filter((name) => !expected.has(name));
|
|
7727
|
-
if (unexpected.length
|
|
7728
|
-
|
|
7921
|
+
if (unexpected.length) throw new CoworkStorageError(`room "${id}" contains live or unexpected residue: ${unexpected.join(", ")}`);
|
|
7922
|
+
for (const name of ["archive.sqlite3-wal", "archive.sqlite3-shm", "archive.sqlite3", "room.json.v1.bak", "room.json"]) {
|
|
7923
|
+
const path = join3(roomDir, name);
|
|
7924
|
+
if (this.lstatIfPresent(path)) {
|
|
7925
|
+
this.assertRegularFile(path, name);
|
|
7926
|
+
this.fs.unlinkSync(path);
|
|
7927
|
+
}
|
|
7729
7928
|
}
|
|
7730
|
-
const
|
|
7731
|
-
if (this.lstatIfPresent(
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7929
|
+
const blobs = this.blobsDirectory(id);
|
|
7930
|
+
if (this.lstatIfPresent(blobs)) this.fs.rmSync(blobs, { recursive: true, force: true });
|
|
7931
|
+
this.fsyncDirectory(roomDir);
|
|
7932
|
+
this.fs.rmdirSync(roomDir);
|
|
7933
|
+
this.fsyncDirectory(this.roomsDirectory());
|
|
7934
|
+
});
|
|
7935
|
+
}
|
|
7936
|
+
withDatabase(roomId, work, create = false) {
|
|
7937
|
+
const path = this.archivePath(roomId);
|
|
7938
|
+
let guardFd;
|
|
7939
|
+
if (!create) {
|
|
7940
|
+
this.assertRegularFile(path, "room archive database");
|
|
7941
|
+
guardFd = this.fs.openSync(path, nodeFs2.constants.O_RDONLY | NO_FOLLOW2);
|
|
7942
|
+
this.validateOpenPath(guardFd, path, "room archive database", "file", true);
|
|
7943
|
+
}
|
|
7944
|
+
let db;
|
|
7945
|
+
try {
|
|
7946
|
+
this.secureSqliteFiles(path);
|
|
7947
|
+
db = new Database(path, { fileMustExist: !create });
|
|
7948
|
+
if (guardFd !== void 0) this.validateOpenPath(guardFd, path, "room archive database", "file", true);
|
|
7949
|
+
this.fs.chmodSync(path, FILE_MODE2);
|
|
7950
|
+
db.pragma("journal_mode = WAL");
|
|
7951
|
+
db.pragma("synchronous = FULL");
|
|
7952
|
+
db.pragma("foreign_keys = ON");
|
|
7953
|
+
db.pragma("busy_timeout = 5000");
|
|
7954
|
+
if (create) {
|
|
7955
|
+
db.exec(`CREATE TABLE records (
|
|
7956
|
+
seq INTEGER PRIMARY KEY, record_id TEXT NOT NULL UNIQUE, kind TEXT NOT NULL, at TEXT NOT NULL,
|
|
7957
|
+
payload_json TEXT NOT NULL, blob_path TEXT, message_id TEXT, file_id TEXT, intent_record_id TEXT,
|
|
7958
|
+
recipient_identity TEXT, source_msg_id INTEGER, source_file_id INTEGER, category TEXT,
|
|
7959
|
+
briefing_role TEXT, briefing_version INTEGER, membership_epoch INTEGER
|
|
7960
|
+
);
|
|
7961
|
+
CREATE TABLE record_recipients (
|
|
7962
|
+
record_seq INTEGER NOT NULL REFERENCES records(seq) ON DELETE CASCADE,
|
|
7963
|
+
recipient_identity TEXT NOT NULL, category TEXT, briefing_role TEXT,
|
|
7964
|
+
briefing_version INTEGER, PRIMARY KEY(record_seq, recipient_identity)
|
|
7965
|
+
);
|
|
7966
|
+
CREATE TABLE relay_intent_work (
|
|
7967
|
+
record_seq INTEGER NOT NULL REFERENCES records(seq) ON DELETE CASCADE,
|
|
7968
|
+
recipient_identity TEXT NOT NULL, PRIMARY KEY(record_seq, recipient_identity)
|
|
7969
|
+
);
|
|
7970
|
+
CREATE INDEX relay_work_source ON relay_intent_work(record_seq, recipient_identity);
|
|
7971
|
+
CREATE INDEX records_kind_seq ON records(kind, seq);
|
|
7972
|
+
CREATE INDEX records_message ON records(message_id, kind, seq);
|
|
7973
|
+
CREATE INDEX records_file ON records(file_id, kind, seq);
|
|
7974
|
+
CREATE INDEX records_intent_result ON records(intent_record_id, kind);
|
|
7975
|
+
CREATE INDEX records_relay_recipient ON records(kind, recipient_identity, seq);
|
|
7976
|
+
CREATE UNIQUE INDEX records_source_message ON records(source_msg_id) WHERE kind='message' AND source_msg_id IS NOT NULL;
|
|
7977
|
+
CREATE UNIQUE INDEX records_source_file ON records(source_file_id) WHERE kind='file' AND source_file_id IS NOT NULL;
|
|
7978
|
+
CREATE INDEX records_briefing ON records(category, briefing_role, briefing_version, seq);
|
|
7979
|
+
CREATE INDEX records_membership_epoch ON records(category, membership_epoch);
|
|
7980
|
+
CREATE INDEX recipients_identity ON record_recipients(recipient_identity, record_seq);
|
|
7981
|
+
CREATE INDEX recipients_briefing_delivery ON record_recipients
|
|
7982
|
+
(recipient_identity, category, briefing_role, briefing_version, record_seq);`);
|
|
7983
|
+
db.pragma(`user_version = ${SQLITE_SCHEMA_VERSION}`);
|
|
7984
|
+
this.reconciledBlobRooms.add(roomId);
|
|
7985
|
+
} else {
|
|
7986
|
+
const version = db.pragma("user_version", { simple: true });
|
|
7987
|
+
if (version !== SQLITE_SCHEMA_VERSION) {
|
|
7988
|
+
throw new CoworkStorageError(`unsupported room archive schema version ${version}`);
|
|
7989
|
+
}
|
|
7990
|
+
if (!this.reconciledBlobRooms.has(roomId)) {
|
|
7991
|
+
this.reconcileBlobDirectory(roomId, db);
|
|
7992
|
+
this.reconciledBlobRooms.add(roomId);
|
|
7993
|
+
}
|
|
7735
7994
|
}
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7995
|
+
this.secureSqliteFiles(path);
|
|
7996
|
+
const result = work(db);
|
|
7997
|
+
this.secureSqliteFiles(path);
|
|
7998
|
+
return result;
|
|
7999
|
+
} catch (error) {
|
|
8000
|
+
throw this.wrap(`failed to access room "${roomId}" SQLite archive`, error);
|
|
8001
|
+
} finally {
|
|
8002
|
+
try {
|
|
8003
|
+
db?.close();
|
|
8004
|
+
} catch {
|
|
7740
8005
|
}
|
|
7741
|
-
if (
|
|
7742
|
-
this.
|
|
7743
|
-
|
|
7744
|
-
|
|
8006
|
+
if (guardFd !== void 0) try {
|
|
8007
|
+
this.fs.closeSync(guardFd);
|
|
8008
|
+
} catch {
|
|
8009
|
+
}
|
|
8010
|
+
this.secureSqliteFiles(path);
|
|
8011
|
+
}
|
|
8012
|
+
}
|
|
8013
|
+
indexValues(record) {
|
|
8014
|
+
const subject = record;
|
|
8015
|
+
return {
|
|
8016
|
+
seq: record.seq,
|
|
8017
|
+
record_id: record.record_id,
|
|
8018
|
+
kind: record.kind,
|
|
8019
|
+
at: record.at,
|
|
8020
|
+
message_id: subject.message_id ?? null,
|
|
8021
|
+
file_id: subject.file_id ?? null,
|
|
8022
|
+
intent_record_id: subject.intent_record_id ?? null,
|
|
8023
|
+
recipient_identity: subject.recipient_identity ?? null,
|
|
8024
|
+
source_msg_id: subject.source_msg_id ?? null,
|
|
8025
|
+
source_file_id: subject.source_file_id ?? null,
|
|
8026
|
+
category: subject.category ?? null,
|
|
8027
|
+
briefing_role: subject.briefing_role ?? null,
|
|
8028
|
+
briefing_version: subject.briefing_version ?? null,
|
|
8029
|
+
membership_epoch: typeof subject.membership === "object" && subject.membership !== null ? subject.membership.epoch ?? null : null
|
|
8030
|
+
};
|
|
8031
|
+
}
|
|
8032
|
+
decodeRows(roomId, rows) {
|
|
8033
|
+
return rows.map((row) => {
|
|
8034
|
+
let decoded;
|
|
8035
|
+
try {
|
|
8036
|
+
decoded = JSON.parse(row.payload_json);
|
|
8037
|
+
} catch (error) {
|
|
8038
|
+
throw new CoworkStorageError(`malformed JSON in room "${roomId}" archive at sequence ${row.seq}`, { cause: error });
|
|
8039
|
+
}
|
|
8040
|
+
if (row.blob_path !== null) {
|
|
8041
|
+
const subject = decoded;
|
|
8042
|
+
const expectedPath = subject.kind === "file" && typeof subject.sha256 === "string" && /^[0-9a-f]{64}$/.test(subject.sha256) ? join3("blobs", subject.sha256) : void 0;
|
|
8043
|
+
if (expectedPath === void 0 || row.blob_path !== expectedPath) {
|
|
8044
|
+
throw new CoworkStorageError(`invalid blob reference in room "${roomId}" archive at sequence ${row.seq}`);
|
|
8045
|
+
}
|
|
8046
|
+
const bytes = this.readFileNoFollow(join3(this.roomDirectory(roomId), expectedPath), "room file blob");
|
|
8047
|
+
decoded = { ...decoded, data_base64: bytes.toString("base64") };
|
|
8048
|
+
}
|
|
8049
|
+
try {
|
|
8050
|
+
const record = CommunicationRecordSchema.parse(decoded);
|
|
8051
|
+
if (record.room_id !== roomId || record.seq !== row.seq) throw new Error("indexed identity mismatch");
|
|
8052
|
+
return record;
|
|
8053
|
+
} catch (error) {
|
|
8054
|
+
throw new CoworkStorageError(`invalid record in room "${roomId}" archive at sequence ${row.seq}`, { cause: error });
|
|
7745
8055
|
}
|
|
7746
|
-
this.fs.rmdirSync(roomDir);
|
|
7747
|
-
this.fsyncDirectory(this.roomsDirectory());
|
|
7748
|
-
this.nextSequences.delete(validRoomId);
|
|
7749
8056
|
});
|
|
7750
8057
|
}
|
|
7751
|
-
|
|
7752
|
-
const
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
if (
|
|
7758
|
-
|
|
8058
|
+
persistBlob(roomId, digest, bytes) {
|
|
8059
|
+
const directory = this.blobsDirectory(roomId);
|
|
8060
|
+
this.ensurePrivateDirectory(directory, true, "room blobs directory");
|
|
8061
|
+
const final = join3(directory, digest);
|
|
8062
|
+
if (this.lstatIfPresent(final)) {
|
|
8063
|
+
this.assertRegularFile(final, "room file blob");
|
|
8064
|
+
if (this.readFileNoFollow(final, "room file blob").equals(bytes)) return { path: join3("blobs", digest), created: false };
|
|
8065
|
+
throw new CoworkStorageError(`immutable blob collision for ${digest}`);
|
|
8066
|
+
}
|
|
8067
|
+
const temp = join3(directory, `.tmp-${process.pid}-${randomBytes3(8).toString("hex")}`);
|
|
8068
|
+
let fd;
|
|
8069
|
+
try {
|
|
8070
|
+
fd = this.fs.openSync(temp, nodeFs2.constants.O_CREAT | nodeFs2.constants.O_EXCL | nodeFs2.constants.O_WRONLY | NO_FOLLOW2, FILE_MODE2);
|
|
8071
|
+
this.writeAll(fd, bytes);
|
|
8072
|
+
this.fs.fsyncSync(fd);
|
|
8073
|
+
this.fs.closeSync(fd);
|
|
8074
|
+
fd = void 0;
|
|
8075
|
+
this.fs.renameSync(temp, final);
|
|
8076
|
+
this.fsyncDirectory(directory);
|
|
8077
|
+
this.fs.chmodSync(final, FILE_MODE2);
|
|
8078
|
+
return { path: join3("blobs", digest), created: true };
|
|
8079
|
+
} finally {
|
|
8080
|
+
if (fd !== void 0) try {
|
|
8081
|
+
this.fs.closeSync(fd);
|
|
8082
|
+
} catch {
|
|
8083
|
+
}
|
|
8084
|
+
if (this.lstatIfPresent(temp)) try {
|
|
8085
|
+
this.fs.unlinkSync(temp);
|
|
8086
|
+
} catch {
|
|
7759
8087
|
}
|
|
7760
|
-
targets.push(join3(roomDir, stagingName));
|
|
7761
8088
|
}
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
8089
|
+
}
|
|
8090
|
+
removeUnreferencedBlob(roomId, relativePath) {
|
|
8091
|
+
const absolute = join3(this.roomDirectory(roomId), relativePath);
|
|
8092
|
+
try {
|
|
8093
|
+
const referenced = this.withDatabase(roomId, (db) => db.prepare(
|
|
8094
|
+
"SELECT EXISTS(SELECT 1 FROM records WHERE blob_path = ?) AS found"
|
|
8095
|
+
).get(relativePath).found !== 0);
|
|
8096
|
+
if (referenced || !this.lstatIfPresent(absolute)) return;
|
|
8097
|
+
this.assertRegularFile(absolute, "unreferenced room file blob");
|
|
8098
|
+
this.fs.unlinkSync(absolute);
|
|
8099
|
+
this.fsyncDirectory(dirname2(absolute));
|
|
8100
|
+
} catch {
|
|
7768
8101
|
}
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
8102
|
+
}
|
|
8103
|
+
reconcileBlobDirectory(roomId, db) {
|
|
8104
|
+
const directory = this.blobsDirectory(roomId);
|
|
8105
|
+
this.ensurePrivateDirectory(directory, true, "room blobs directory");
|
|
8106
|
+
const referenced = new Set(db.prepare(
|
|
8107
|
+
"SELECT blob_path FROM records WHERE blob_path IS NOT NULL"
|
|
8108
|
+
).all().map((row) => basename(row.blob_path)));
|
|
8109
|
+
let changed = false;
|
|
8110
|
+
for (const entry of this.fs.readdirSync(directory, { withFileTypes: true })) {
|
|
8111
|
+
const path = join3(directory, entry.name);
|
|
8112
|
+
if (/^\.tmp-[0-9]+-[0-9a-f]{16}$/.test(entry.name)) {
|
|
8113
|
+
this.assertRegularFile(path, "crash-left room blob temporary file");
|
|
8114
|
+
this.fs.unlinkSync(path);
|
|
8115
|
+
changed = true;
|
|
8116
|
+
continue;
|
|
8117
|
+
}
|
|
8118
|
+
if (!/^[0-9a-f]{64}$/.test(entry.name)) throw new CoworkStorageError(`unexpected room blob residue: ${entry.name}`);
|
|
8119
|
+
this.assertRegularFile(path, referenced.has(entry.name) ? "room file blob" : "unreferenced room file blob");
|
|
8120
|
+
this.fs.chmodSync(path, FILE_MODE2);
|
|
8121
|
+
if (referenced.has(entry.name)) continue;
|
|
8122
|
+
this.fs.unlinkSync(path);
|
|
8123
|
+
changed = true;
|
|
8124
|
+
}
|
|
8125
|
+
if (changed) this.fsyncDirectory(directory);
|
|
8126
|
+
}
|
|
8127
|
+
secureSqliteFiles(path) {
|
|
8128
|
+
for (const candidate of [path, `${path}-wal`, `${path}-shm`]) {
|
|
8129
|
+
if (!this.lstatIfPresent(candidate)) continue;
|
|
8130
|
+
this.assertRegularFile(candidate, `SQLite file ${basename(candidate)}`);
|
|
8131
|
+
this.fs.chmodSync(candidate, FILE_MODE2);
|
|
7772
8132
|
}
|
|
7773
8133
|
}
|
|
8134
|
+
validatePage(after, limit) {
|
|
8135
|
+
if (!Number.isSafeInteger(after) || after < 0) throw new CoworkStorageError("after must be a non-negative safe integer");
|
|
8136
|
+
if (!Number.isSafeInteger(limit) || limit < 1) throw new CoworkStorageError("limit must be a positive safe integer");
|
|
8137
|
+
}
|
|
7774
8138
|
loadUnlocked(roomId) {
|
|
7775
8139
|
this.assertRoomDirectory(roomId);
|
|
7776
8140
|
const path = this.metadataPath(roomId);
|
|
7777
8141
|
this.assertRegularFile(path, "room metadata");
|
|
8142
|
+
let decoded;
|
|
7778
8143
|
let bytes;
|
|
7779
8144
|
try {
|
|
7780
8145
|
bytes = this.readFileNoFollow(path, "room metadata");
|
|
7781
|
-
} catch (error) {
|
|
7782
|
-
throw this.wrap(`failed to read room "${roomId}" metadata`, error);
|
|
7783
|
-
}
|
|
7784
|
-
let decoded;
|
|
7785
|
-
try {
|
|
7786
8146
|
decoded = JSON.parse(utf8Decoder.decode(bytes));
|
|
7787
8147
|
} catch (error) {
|
|
7788
8148
|
throw this.wrap(`malformed metadata for room "${roomId}"`, error);
|
|
7789
8149
|
}
|
|
7790
8150
|
const room = this.isVersion1(decoded) ? this.migrateUnlocked(roomId, decoded, bytes) : RoomSchema.parse(decoded);
|
|
7791
|
-
if (!this.isVersion1(decoded) && this.persistedRoomName(decoded) !== room.room_name)
|
|
7792
|
-
this.atomicMetadataWrite(this.metadataPath(roomId), room);
|
|
7793
|
-
}
|
|
8151
|
+
if (!this.isVersion1(decoded) && this.persistedRoomName(decoded) !== room.room_name) this.atomicMetadataWrite(path, room);
|
|
7794
8152
|
if (room.room_id !== roomId) throw new CoworkStorageError(`metadata room_id does not match room "${roomId}"`);
|
|
7795
8153
|
return room;
|
|
7796
8154
|
}
|
|
@@ -7800,79 +8158,54 @@ var init_storage = __esm({
|
|
|
7800
8158
|
persistedRoomName(decoded) {
|
|
7801
8159
|
return typeof decoded === "object" && decoded !== null ? decoded.room_name : void 0;
|
|
7802
8160
|
}
|
|
7803
|
-
|
|
7804
|
-
* Lazy additive v1 → v2 migration: preserve the exact pre-migration
|
|
7805
|
-
* bytes once as room.json.v1.bak, then atomically persist the v2 metadata.
|
|
7806
|
-
*
|
|
7807
|
-
* THE BACKUP IS WRITTEN TEMP → FSYNC → RENAME, not opened in place, and the
|
|
7808
|
-
* reason is a crash window that an existence check cannot see. The earlier
|
|
7809
|
-
* version guarded with `if (!lstatIfPresent(backupPath))` and wrote straight
|
|
7810
|
-
* into the final path under O_CREAT|O_EXCL. A crash inside that write leaves a
|
|
7811
|
-
* PARTIAL file that nonetheless EXISTS, so the next load's existence check
|
|
7812
|
-
* skips the backup, writes v2, and the pre-migration bytes are gone — no
|
|
7813
|
-
* error, no warning, and the one artefact that exists to undo a bad migration
|
|
7814
|
-
* is a truncated fragment. Measured before the fix: a 40-byte prefix of a
|
|
7815
|
-
* 604-byte room, JSON.parse false, room.json already v2.
|
|
7816
|
-
*
|
|
7817
|
-
* A rename is atomic, so the final path now only ever appears complete. The
|
|
7818
|
-
* temp file is created with O_EXCL under a pid+random name and removed on
|
|
7819
|
-
* failure, so a crashed attempt leaves at most an orphan temp, never a
|
|
7820
|
-
* plausible-looking backup.
|
|
7821
|
-
*
|
|
7822
|
-
* AND AN EXISTING BACKUP IS PARSED BEFORE IT IS TRUSTED, which repairs the
|
|
7823
|
-
* case where a partial file is ALREADY on disk from a build without this fix —
|
|
7824
|
-
* exactly the state a host that ran the previous code could be in right now.
|
|
7825
|
-
* A backup that does not parse as v1 is replaced by the bytes we hold, because
|
|
7826
|
-
* those are the real pre-migration bytes and the fragment is worthless.
|
|
7827
|
-
*
|
|
7828
|
-
* WHAT THE BACKUP IS NOT: restoring it is NOT a rollback. See the note on
|
|
7829
|
-
* `restoreV1Backup` — re-migrating mints fresh participant ids.
|
|
7830
|
-
*/
|
|
7831
|
-
migrateUnlocked(roomId, decoded, originalBytes) {
|
|
8161
|
+
migrateUnlocked(roomId, decoded, original) {
|
|
7832
8162
|
const v1 = RoomV1Schema.parse(decoded);
|
|
7833
8163
|
if (v1.room_id !== roomId) throw new CoworkStorageError(`metadata room_id does not match room "${roomId}"`);
|
|
7834
8164
|
const migrated = migrateRoomV1(v1, generateUlid);
|
|
7835
|
-
const
|
|
7836
|
-
if (!this.hasIntactV1Backup(
|
|
7837
|
-
this.atomicBytesWrite(backupPath, originalBytes, "room metadata v1 backup");
|
|
7838
|
-
}
|
|
8165
|
+
const backup = `${this.metadataPath(roomId)}.v1.bak`;
|
|
8166
|
+
if (!this.hasIntactV1Backup(backup)) this.atomicBytesWrite(backup, original, "room metadata v1 backup");
|
|
7839
8167
|
this.atomicMetadataWrite(this.metadataPath(roomId), migrated);
|
|
7840
8168
|
return migrated;
|
|
7841
8169
|
}
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
*
|
|
7845
|
-
* Existence is not the question — a partial file exists. It must parse and
|
|
7846
|
-
* still claim to be the v1 metadata for this room; anything else is a fragment
|
|
7847
|
-
* and is better overwritten with the bytes we are holding right now.
|
|
7848
|
-
*/
|
|
7849
|
-
hasIntactV1Backup(backupPath) {
|
|
7850
|
-
if (!this.lstatIfPresent(backupPath)) return false;
|
|
8170
|
+
hasIntactV1Backup(path) {
|
|
8171
|
+
if (!this.lstatIfPresent(path)) return false;
|
|
7851
8172
|
try {
|
|
7852
|
-
|
|
7853
|
-
return this.isVersion1(decoded);
|
|
8173
|
+
return this.isVersion1(JSON.parse(utf8Decoder.decode(this.readFileNoFollow(path, "room metadata v1 backup"))));
|
|
7854
8174
|
} catch {
|
|
7855
8175
|
return false;
|
|
7856
8176
|
}
|
|
7857
8177
|
}
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
8178
|
+
removeProvisioningArtifacts(roomDir) {
|
|
8179
|
+
const journal = join3(roomDir, ".cowork-provisioning-stage");
|
|
8180
|
+
const targets = [join3(roomDir, "live"), join3(roomDir, "provisioning-residue")];
|
|
8181
|
+
if (this.lstatIfPresent(journal)) {
|
|
8182
|
+
this.assertRegularFile(journal, "provisioning staging journal");
|
|
8183
|
+
const name = this.fs.readFileSync(journal, "utf8").trim();
|
|
8184
|
+
if (!/^live\.staging-[0-9a-f]{32}$/.test(name)) throw new CoworkStorageError("invalid provisioning staging journal");
|
|
8185
|
+
targets.push(join3(roomDir, name));
|
|
8186
|
+
}
|
|
8187
|
+
for (const target of targets) {
|
|
8188
|
+
const stat = this.lstatIfPresent(target);
|
|
8189
|
+
if (!stat) continue;
|
|
8190
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) this.fs.unlinkSync(target);
|
|
8191
|
+
else this.fs.rmSync(target, { recursive: true, force: true });
|
|
8192
|
+
this.fsyncDirectory(roomDir);
|
|
8193
|
+
}
|
|
8194
|
+
if (this.lstatIfPresent(journal)) {
|
|
8195
|
+
this.fs.unlinkSync(journal);
|
|
8196
|
+
this.fsyncDirectory(roomDir);
|
|
8197
|
+
}
|
|
8198
|
+
}
|
|
8199
|
+
atomicMetadataWrite(path, room) {
|
|
8200
|
+
this.atomicBytesWrite(path, Buffer.from(`${JSON.stringify(room)}
|
|
8201
|
+
`), "room metadata");
|
|
8202
|
+
}
|
|
7864
8203
|
atomicBytesWrite(path, bytes, label) {
|
|
7865
8204
|
if (this.lstatIfPresent(path)) this.assertRegularFile(path, label);
|
|
7866
8205
|
const temp = `${path}.tmp-${process.pid}-${randomBytes3(8).toString("hex")}`;
|
|
7867
8206
|
let fd;
|
|
7868
8207
|
try {
|
|
7869
|
-
fd = this.fs.openSync(
|
|
7870
|
-
temp,
|
|
7871
|
-
nodeFs2.constants.O_CREAT | nodeFs2.constants.O_EXCL | nodeFs2.constants.O_WRONLY | NO_FOLLOW2,
|
|
7872
|
-
FILE_MODE2
|
|
7873
|
-
);
|
|
7874
|
-
this.validateOpenPath(fd, temp, `temporary ${label}`, "file", true);
|
|
7875
|
-
this.fs.fchmodSync(fd, FILE_MODE2);
|
|
8208
|
+
fd = this.fs.openSync(temp, nodeFs2.constants.O_CREAT | nodeFs2.constants.O_EXCL | nodeFs2.constants.O_WRONLY | NO_FOLLOW2, FILE_MODE2);
|
|
7876
8209
|
this.writeAll(fd, bytes);
|
|
7877
8210
|
this.fs.fsyncSync(fd);
|
|
7878
8211
|
this.fs.closeSync(fd);
|
|
@@ -7880,11 +8213,9 @@ var init_storage = __esm({
|
|
|
7880
8213
|
this.fs.renameSync(temp, path);
|
|
7881
8214
|
this.fsyncDirectory(dirname2(path));
|
|
7882
8215
|
} catch (error) {
|
|
7883
|
-
if (fd !== void 0) {
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
} catch {
|
|
7887
|
-
}
|
|
8216
|
+
if (fd !== void 0) try {
|
|
8217
|
+
this.fs.closeSync(fd);
|
|
8218
|
+
} catch {
|
|
7888
8219
|
}
|
|
7889
8220
|
try {
|
|
7890
8221
|
this.fs.rmSync(temp, { force: true });
|
|
@@ -7893,84 +8224,31 @@ var init_storage = __esm({
|
|
|
7893
8224
|
throw this.wrap(`failed to write ${label} at ${path}`, error);
|
|
7894
8225
|
}
|
|
7895
8226
|
}
|
|
7896
|
-
scanArchive(roomId) {
|
|
7897
|
-
const path = this.archivePath(roomId);
|
|
7898
|
-
this.assertRegularFile(path, "room archive");
|
|
7899
|
-
let bytes;
|
|
7900
|
-
try {
|
|
7901
|
-
bytes = this.readFileNoFollow(path, "room archive");
|
|
7902
|
-
} catch (error) {
|
|
7903
|
-
throw this.wrap(`failed to read room "${roomId}" archive`, error);
|
|
7904
|
-
}
|
|
7905
|
-
const records = [];
|
|
7906
|
-
let byteOffset = 0;
|
|
7907
|
-
let expectedSequence = 1;
|
|
7908
|
-
while (byteOffset < bytes.byteLength) {
|
|
7909
|
-
const newline = bytes.indexOf(10, byteOffset);
|
|
7910
|
-
if (newline === -1) {
|
|
7911
|
-
throw new CoworkStorageError(`partial JSON record in room "${roomId}" archive at byte offset ${byteOffset}`);
|
|
7912
|
-
}
|
|
7913
|
-
const line = bytes.subarray(byteOffset, newline);
|
|
7914
|
-
let decoded;
|
|
7915
|
-
try {
|
|
7916
|
-
decoded = JSON.parse(utf8Decoder.decode(line));
|
|
7917
|
-
} catch (error) {
|
|
7918
|
-
throw new CoworkStorageError(
|
|
7919
|
-
`malformed JSON in room "${roomId}" archive at byte offset ${byteOffset}`,
|
|
7920
|
-
{ cause: error }
|
|
7921
|
-
);
|
|
7922
|
-
}
|
|
7923
|
-
const observedSequence = typeof decoded === "object" && decoded !== null && "seq" in decoded ? decoded.seq : void 0;
|
|
7924
|
-
if (observedSequence !== expectedSequence) {
|
|
7925
|
-
throw new CoworkStorageError(
|
|
7926
|
-
`non-monotonic sequence in room "${roomId}" archive at byte offset ${byteOffset}: expected ${expectedSequence}, found ${String(observedSequence)}`
|
|
7927
|
-
);
|
|
7928
|
-
}
|
|
7929
|
-
let record;
|
|
7930
|
-
try {
|
|
7931
|
-
record = CommunicationRecordSchema.parse(decoded);
|
|
7932
|
-
} catch (error) {
|
|
7933
|
-
throw new CoworkStorageError(
|
|
7934
|
-
`invalid record in room "${roomId}" archive at byte offset ${byteOffset}`,
|
|
7935
|
-
{ cause: error }
|
|
7936
|
-
);
|
|
7937
|
-
}
|
|
7938
|
-
if (record.room_id !== roomId) {
|
|
7939
|
-
throw new CoworkStorageError(
|
|
7940
|
-
`record room_id mismatch in room "${roomId}" archive at byte offset ${byteOffset}`
|
|
7941
|
-
);
|
|
7942
|
-
}
|
|
7943
|
-
records.push(record);
|
|
7944
|
-
expectedSequence += 1;
|
|
7945
|
-
byteOffset = newline + 1;
|
|
7946
|
-
}
|
|
7947
|
-
return { records, nextSequence: expectedSequence };
|
|
7948
|
-
}
|
|
7949
8227
|
ensureBaseDirectories() {
|
|
7950
8228
|
this.ensurePrivateDirectory(this.stateDir, true, "state directory");
|
|
7951
8229
|
this.ensurePrivateDirectory(this.roomsDirectory(), true, "rooms directory");
|
|
7952
8230
|
}
|
|
8231
|
+
assertRoomDirectory(roomId) {
|
|
8232
|
+
this.ensureBaseDirectories();
|
|
8233
|
+
this.ensurePrivateDirectory(this.roomDirectory(roomId), false, `room "${roomId}" directory`);
|
|
8234
|
+
}
|
|
7953
8235
|
ensurePrivateDirectory(path, create, label) {
|
|
7954
8236
|
let stat = this.lstatIfPresent(path);
|
|
7955
|
-
|
|
7956
|
-
if (created) {
|
|
8237
|
+
if (!stat) {
|
|
7957
8238
|
if (!create) throw new CoworkStorageError(`${label} does not exist`);
|
|
7958
8239
|
this.createPrivateDirectoryTree(path, label);
|
|
7959
8240
|
stat = this.fs.lstatSync(path);
|
|
7960
8241
|
}
|
|
7961
|
-
|
|
7962
|
-
if (current.isSymbolicLink()) throw new CoworkStorageError(`${label} must not be a symbolic link (symlink)`);
|
|
7963
|
-
if (!current.isDirectory()) throw new CoworkStorageError(`${label} is not a directory`);
|
|
8242
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new CoworkStorageError(`${label} is not a private directory`);
|
|
7964
8243
|
this.fs.chmodSync(path, DIRECTORY_MODE2);
|
|
7965
8244
|
}
|
|
7966
8245
|
createPrivateDirectoryTree(path, label) {
|
|
7967
8246
|
const missing = [];
|
|
7968
8247
|
let cursor = path;
|
|
7969
8248
|
for (; ; ) {
|
|
7970
|
-
const
|
|
7971
|
-
if (
|
|
7972
|
-
if (
|
|
7973
|
-
if (!existing.isDirectory()) throw new CoworkStorageError(`${label} parent is not a directory`);
|
|
8249
|
+
const stat = this.lstatIfPresent(cursor);
|
|
8250
|
+
if (stat) {
|
|
8251
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new CoworkStorageError(`${label} parent is unsafe`);
|
|
7974
8252
|
break;
|
|
7975
8253
|
}
|
|
7976
8254
|
missing.push(cursor);
|
|
@@ -7978,120 +8256,45 @@ var init_storage = __esm({
|
|
|
7978
8256
|
if (parent === cursor) throw new CoworkStorageError(`cannot find existing parent for ${label}`);
|
|
7979
8257
|
cursor = parent;
|
|
7980
8258
|
}
|
|
7981
|
-
const
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
|
|
7985
|
-
|
|
7986
|
-
this.fs.chmodSync(directory, DIRECTORY_MODE2);
|
|
7987
|
-
this.fsyncDirectory(directory);
|
|
7988
|
-
this.fsyncDirectory(dirname2(directory));
|
|
7989
|
-
}
|
|
7990
|
-
} catch (error) {
|
|
7991
|
-
for (const directory of created.reverse()) {
|
|
7992
|
-
try {
|
|
7993
|
-
this.fs.rmdirSync(directory);
|
|
7994
|
-
this.fsyncDirectory(dirname2(directory));
|
|
7995
|
-
} catch {
|
|
7996
|
-
}
|
|
7997
|
-
}
|
|
7998
|
-
throw error;
|
|
8259
|
+
for (const directory of missing.reverse()) {
|
|
8260
|
+
this.fs.mkdirSync(directory, { mode: DIRECTORY_MODE2 });
|
|
8261
|
+
this.fs.chmodSync(directory, DIRECTORY_MODE2);
|
|
8262
|
+
this.fsyncDirectory(directory);
|
|
8263
|
+
this.fsyncDirectory(dirname2(directory));
|
|
7999
8264
|
}
|
|
8000
8265
|
}
|
|
8001
|
-
assertRoomDirectory(roomId) {
|
|
8002
|
-
this.ensureBaseDirectories();
|
|
8003
|
-
this.ensurePrivateDirectory(this.roomDirectory(roomId), false, `room "${roomId}" directory`);
|
|
8004
|
-
}
|
|
8005
8266
|
rejectSymlink(path, label) {
|
|
8006
|
-
if (this.fs.lstatSync(path).isSymbolicLink()) {
|
|
8007
|
-
throw new CoworkStorageError(`${label} must not be a symbolic link (symlink)`);
|
|
8008
|
-
}
|
|
8267
|
+
if (this.fs.lstatSync(path).isSymbolicLink()) throw new CoworkStorageError(`${label} must not be a symbolic link (symlink)`);
|
|
8009
8268
|
}
|
|
8010
8269
|
assertRegularFile(path, label) {
|
|
8011
8270
|
this.rejectSymlink(path, label);
|
|
8012
8271
|
const stat = this.fs.lstatSync(path);
|
|
8013
|
-
if (!stat.isFile()) throw new CoworkStorageError(`${label} is not a regular file`);
|
|
8014
|
-
if (stat.nlink !== 1) throw new CoworkStorageError(`${label} has unsafe hardlink link count ${stat.nlink}`);
|
|
8015
|
-
}
|
|
8016
|
-
lstatIfPresent(path) {
|
|
8017
|
-
try {
|
|
8018
|
-
return this.fs.lstatSync(path);
|
|
8019
|
-
} catch (error) {
|
|
8020
|
-
if (error.code === "ENOENT") return void 0;
|
|
8021
|
-
throw error;
|
|
8022
|
-
}
|
|
8272
|
+
if (!stat.isFile() || stat.nlink !== 1) throw new CoworkStorageError(`${label} is not a safe regular file`);
|
|
8023
8273
|
}
|
|
8024
8274
|
validateOpenPath(fd, path, label, kind, requireSingleLink) {
|
|
8025
8275
|
const opened = this.fs.fstatSync(fd);
|
|
8026
8276
|
const validKind = kind === "file" ? opened.isFile() : opened.isDirectory();
|
|
8027
8277
|
if (!validKind) throw new CoworkStorageError(`${label} open descriptor is not a regular ${kind}`);
|
|
8028
|
-
if (requireSingleLink && opened.nlink !== 1) {
|
|
8029
|
-
throw new CoworkStorageError(`${label} has unsafe hardlink link count ${opened.nlink}`);
|
|
8030
|
-
}
|
|
8278
|
+
if (requireSingleLink && opened.nlink !== 1) throw new CoworkStorageError(`${label} has unsafe hardlink link count ${opened.nlink}`);
|
|
8031
8279
|
const current = this.fs.lstatSync(path);
|
|
8032
8280
|
if (current.isSymbolicLink()) throw new CoworkStorageError(`${label} must not be a symbolic link (symlink)`);
|
|
8033
|
-
if (current.dev !== opened.dev || current.ino !== opened.ino) {
|
|
8034
|
-
throw new CoworkStorageError(`${label} inode changed during open`);
|
|
8035
|
-
}
|
|
8281
|
+
if (current.dev !== opened.dev || current.ino !== opened.ino) throw new CoworkStorageError(`${label} inode changed during open`);
|
|
8036
8282
|
return opened;
|
|
8037
8283
|
}
|
|
8038
|
-
|
|
8039
|
-
const path = this.archivePath(roomId);
|
|
8040
|
-
let fd;
|
|
8041
|
-
try {
|
|
8042
|
-
fd = this.fs.openSync(
|
|
8043
|
-
path,
|
|
8044
|
-
nodeFs2.constants.O_CREAT | nodeFs2.constants.O_EXCL | nodeFs2.constants.O_WRONLY | NO_FOLLOW2,
|
|
8045
|
-
FILE_MODE2
|
|
8046
|
-
);
|
|
8047
|
-
this.validateOpenPath(fd, path, "room archive", "file", true);
|
|
8048
|
-
this.fs.fchmodSync(fd, FILE_MODE2);
|
|
8049
|
-
this.fs.fsyncSync(fd);
|
|
8050
|
-
} finally {
|
|
8051
|
-
if (fd !== void 0) this.fs.closeSync(fd);
|
|
8052
|
-
}
|
|
8053
|
-
this.fsyncDirectory(dirname2(path));
|
|
8054
|
-
}
|
|
8055
|
-
atomicMetadataWrite(path, room) {
|
|
8056
|
-
if (this.lstatIfPresent(path)) this.assertRegularFile(path, "room metadata");
|
|
8057
|
-
const temp = `${path}.tmp-${process.pid}-${randomBytes3(8).toString("hex")}`;
|
|
8058
|
-
const bytes = Buffer.from(`${JSON.stringify(room)}
|
|
8059
|
-
`, "utf8");
|
|
8060
|
-
let fd;
|
|
8284
|
+
lstatIfPresent(path) {
|
|
8061
8285
|
try {
|
|
8062
|
-
|
|
8063
|
-
temp,
|
|
8064
|
-
nodeFs2.constants.O_CREAT | nodeFs2.constants.O_EXCL | nodeFs2.constants.O_WRONLY | NO_FOLLOW2,
|
|
8065
|
-
FILE_MODE2
|
|
8066
|
-
);
|
|
8067
|
-
this.validateOpenPath(fd, temp, "temporary room metadata", "file", true);
|
|
8068
|
-
this.fs.fchmodSync(fd, FILE_MODE2);
|
|
8069
|
-
this.writeAll(fd, bytes);
|
|
8070
|
-
this.fs.fsyncSync(fd);
|
|
8071
|
-
this.fs.closeSync(fd);
|
|
8072
|
-
fd = void 0;
|
|
8073
|
-
this.fs.renameSync(temp, path);
|
|
8074
|
-
this.fsyncDirectory(dirname2(path));
|
|
8286
|
+
return this.fs.lstatSync(path);
|
|
8075
8287
|
} catch (error) {
|
|
8076
|
-
if (
|
|
8077
|
-
|
|
8078
|
-
this.fs.closeSync(fd);
|
|
8079
|
-
} catch {
|
|
8080
|
-
}
|
|
8081
|
-
}
|
|
8082
|
-
try {
|
|
8083
|
-
this.fs.rmSync(temp, { force: true });
|
|
8084
|
-
} catch {
|
|
8085
|
-
}
|
|
8086
|
-
throw this.wrap(`failed to atomically persist ${path}`, error);
|
|
8288
|
+
if (error.code === "ENOENT") return void 0;
|
|
8289
|
+
throw error;
|
|
8087
8290
|
}
|
|
8088
8291
|
}
|
|
8089
8292
|
writeAll(fd, bytes) {
|
|
8090
8293
|
let offset = 0;
|
|
8091
|
-
while (offset < bytes.
|
|
8092
|
-
const
|
|
8093
|
-
if (
|
|
8094
|
-
offset +=
|
|
8294
|
+
while (offset < bytes.length) {
|
|
8295
|
+
const n = this.fs.writeSync(fd, bytes, offset, bytes.length - offset, null);
|
|
8296
|
+
if (n <= 0) throw new CoworkStorageError("write made no progress");
|
|
8297
|
+
offset += n;
|
|
8095
8298
|
}
|
|
8096
8299
|
}
|
|
8097
8300
|
readFileNoFollow(path, label) {
|
|
@@ -8129,10 +8332,13 @@ var init_storage = __esm({
|
|
|
8129
8332
|
return join3(this.roomDirectory(roomId), "room.json");
|
|
8130
8333
|
}
|
|
8131
8334
|
archivePath(roomId) {
|
|
8132
|
-
return join3(this.roomDirectory(roomId), "archive.
|
|
8335
|
+
return join3(this.roomDirectory(roomId), "archive.sqlite3");
|
|
8336
|
+
}
|
|
8337
|
+
blobsDirectory(roomId) {
|
|
8338
|
+
return join3(this.roomDirectory(roomId), "blobs");
|
|
8133
8339
|
}
|
|
8134
8340
|
wrap(message, error) {
|
|
8135
|
-
return
|
|
8341
|
+
return new CoworkStorageError(`${message}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
8136
8342
|
}
|
|
8137
8343
|
};
|
|
8138
8344
|
}
|
|
@@ -9043,7 +9249,7 @@ import { randomBytes as randomBytes4 } from "node:crypto";
|
|
|
9043
9249
|
import * as http from "node:http";
|
|
9044
9250
|
import * as net from "node:net";
|
|
9045
9251
|
import * as nodeFs4 from "node:fs";
|
|
9046
|
-
import { basename, dirname as dirname3, join as join5 } from "node:path";
|
|
9252
|
+
import { basename as basename2, dirname as dirname3, join as join5 } from "node:path";
|
|
9047
9253
|
function createServiceRoutes(service) {
|
|
9048
9254
|
return {
|
|
9049
9255
|
"room.create": { auth: true, run: (params2) => service.createRoom(params2) },
|
|
@@ -9709,7 +9915,7 @@ var init_transports = __esm({
|
|
|
9709
9915
|
}
|
|
9710
9916
|
async cleanupStalePrivateSockets() {
|
|
9711
9917
|
const directory = dirname3(this.options.socketPath);
|
|
9712
|
-
const prefix = `${
|
|
9918
|
+
const prefix = `${basename2(this.options.socketPath)}.private-`;
|
|
9713
9919
|
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
9714
9920
|
const candidates = this.fs.readdirSync(directory).filter((name) => name.startsWith(prefix)).sort().slice(0, MAX_STALE_PRIVATE_SOCKET_CLEANUP);
|
|
9715
9921
|
for (const name of candidates) {
|