@volter/supercode-teams 0.3.159 → 0.3.161
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/connector/index.mjs +2 -2
- package/live-sessions.mjs +12 -3
- package/machine-records.mjs +113 -45
- package/machine.mjs +75 -63
- package/mail/carrier.mjs +202 -157
- package/mail/change-stream.mjs +143 -0
- package/mail/delivery-status.mjs +42 -0
- package/mail/hooks.mjs +90 -15
- package/mail/reads.mjs +2 -1
- package/mail/relay-routing.mjs +41 -0
- package/mail/relay.mjs +120 -204
- package/mail/send.mjs +56 -32
- package/mail/sqlite-store.mjs +166 -16
- package/mail/sqlite.mjs +42 -4
- package/package.json +1 -1
- package/record-state.mjs +143 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.161",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
|
|
6
6
|
"license": "MIT",
|
package/record-state.mjs
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Machine-record snapshots are rows, and their deltas share the mail database's
|
|
2
|
+
// sequence. A normal change never serializes every followed machine to a file.
|
|
3
|
+
import { lstatSync, readFileSync, unlinkSync } from 'node:fs';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
export const RECORD_SCHEMA = `
|
|
7
|
+
CREATE TABLE IF NOT EXISTS machine_record_heads (
|
|
8
|
+
role TEXT NOT NULL,
|
|
9
|
+
machine TEXT NOT NULL,
|
|
10
|
+
value TEXT NOT NULL CHECK (json_valid(value)),
|
|
11
|
+
PRIMARY KEY (role, machine)
|
|
12
|
+
) STRICT;
|
|
13
|
+
CREATE TABLE IF NOT EXISTS machine_record_rows (
|
|
14
|
+
role TEXT NOT NULL,
|
|
15
|
+
machine TEXT NOT NULL,
|
|
16
|
+
section TEXT NOT NULL,
|
|
17
|
+
key TEXT NOT NULL,
|
|
18
|
+
position INTEGER NOT NULL,
|
|
19
|
+
value TEXT NOT NULL CHECK (json_valid(value)),
|
|
20
|
+
PRIMARY KEY (role, machine, section, key)
|
|
21
|
+
) STRICT;
|
|
22
|
+
CREATE INDEX IF NOT EXISTS machine_record_order ON machine_record_rows(role, machine, section, position, key);
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
export class RecordState {
|
|
26
|
+
constructor(store, role) { this.store = store; this.role = role; }
|
|
27
|
+
|
|
28
|
+
load(machine) {
|
|
29
|
+
const head = this.store.statement('SELECT value FROM machine_record_heads WHERE role = ? AND machine = ?').get(this.role, machine);
|
|
30
|
+
if (!head) return null;
|
|
31
|
+
const { record: summary, meta } = JSON.parse(head.value);
|
|
32
|
+
if (summary == null) return { record: null, meta };
|
|
33
|
+
const record = { ...summary, sessions: summary.sessions_rows ? [] : null, mail: {}, panes: null, capabilities: null, unread: {} };
|
|
34
|
+
delete record.sessions_rows;
|
|
35
|
+
for (const { section, key, value } of this.store.statement(`SELECT section, key, value FROM machine_record_rows
|
|
36
|
+
WHERE role = ? AND machine = ? ORDER BY section, position, key`).all(this.role, machine)) {
|
|
37
|
+
const parsed = JSON.parse(value);
|
|
38
|
+
if (section === 'sessions') record.sessions.push(parsed);
|
|
39
|
+
else if (section === 'mail') record.mail[key] = parsed;
|
|
40
|
+
else record[section] = parsed;
|
|
41
|
+
}
|
|
42
|
+
return { record, meta };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
all() {
|
|
46
|
+
return this.store.statement('SELECT machine FROM machine_record_heads WHERE role = ? ORDER BY machine').all(this.role)
|
|
47
|
+
.map(({ machine }) => [machine, this.load(machine)]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
save(record, changed = {}, { meta = {}, publish = false } = {}) {
|
|
51
|
+
const machine = record.machine;
|
|
52
|
+
return this.store.transaction(() => {
|
|
53
|
+
const remove = this.store.statement('DELETE FROM machine_record_rows WHERE role = ? AND machine = ? AND section = ? AND key = ?');
|
|
54
|
+
const put = this.store.statement(`INSERT INTO machine_record_rows(role, machine, section, key, position, value) VALUES (?, ?, ?, ?, ?, ?)
|
|
55
|
+
ON CONFLICT(role, machine, section, key) DO UPDATE SET position = excluded.position, value = excluded.value
|
|
56
|
+
WHERE position <> excluded.position OR value <> excluded.value`);
|
|
57
|
+
for (const [section, delta] of Object.entries(changed)) {
|
|
58
|
+
if (section === 'sessions') {
|
|
59
|
+
const rows = Array.isArray(record.sessions) ? record.sessions : [];
|
|
60
|
+
const positions = new Map(rows.map((row, at) => [row.address, at]));
|
|
61
|
+
if (delta?.delta) {
|
|
62
|
+
for (const key of delta.removed ?? []) remove.run(this.role, machine, section, key);
|
|
63
|
+
for (const row of delta.upsert ?? []) put.run(this.role, machine, section, row.address, positions.get(row.address) ?? 0, JSON.stringify(row));
|
|
64
|
+
// Only moved positions are written; the session objects stay rows.
|
|
65
|
+
const move = this.store.statement(`UPDATE machine_record_rows SET position = ?
|
|
66
|
+
WHERE role = ? AND machine = ? AND section = 'sessions' AND key = ? AND position <> ?`);
|
|
67
|
+
for (const [key, position] of positions) move.run(position, this.role, machine, key, position);
|
|
68
|
+
} else {
|
|
69
|
+
const keys = new Set(rows.map((row) => row.address));
|
|
70
|
+
for (const { key } of this.store.statement("SELECT key FROM machine_record_rows WHERE role = ? AND machine = ? AND section = 'sessions'").all(this.role, machine)) {
|
|
71
|
+
if (!keys.has(key)) remove.run(this.role, machine, section, key);
|
|
72
|
+
}
|
|
73
|
+
for (const row of rows) put.run(this.role, machine, section, row.address, positions.get(row.address), JSON.stringify(row));
|
|
74
|
+
}
|
|
75
|
+
} else if (section === 'mail') {
|
|
76
|
+
const boxes = delta && '*' in delta ? delta['*'] ?? {} : delta ?? {};
|
|
77
|
+
if (delta && '*' in delta) {
|
|
78
|
+
for (const { key } of this.store.statement("SELECT key FROM machine_record_rows WHERE role = ? AND machine = ? AND section = 'mail'").all(this.role, machine)) {
|
|
79
|
+
if (!(key in boxes)) remove.run(this.role, machine, section, key);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const [key, box] of Object.entries(boxes)) {
|
|
83
|
+
if (box == null) remove.run(this.role, machine, section, key);
|
|
84
|
+
else put.run(this.role, machine, section, key, 0, JSON.stringify(box));
|
|
85
|
+
}
|
|
86
|
+
} else put.run(this.role, machine, section, '', 0, JSON.stringify(record[section] ?? null));
|
|
87
|
+
}
|
|
88
|
+
const cursor = publish ? this.store.changes.append('machine-record', { machine }, { seq: record.seq, at: record.at, sections: changed }) : record.cursor;
|
|
89
|
+
const { sessions, mail, panes, capabilities, unread, ...summary } = record;
|
|
90
|
+
const head = { record: { ...summary, ...(cursor ? { cursor } : {}), sessions_rows: Array.isArray(sessions) }, meta };
|
|
91
|
+
this.store.statement(`INSERT INTO machine_record_heads(role, machine, value) VALUES (?, ?, ?)
|
|
92
|
+
ON CONFLICT(role, machine) DO UPDATE SET value = excluded.value`).run(this.role, machine, JSON.stringify(head));
|
|
93
|
+
return cursor;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
saveEmpty(machine, meta) {
|
|
98
|
+
this.store.transaction(() => {
|
|
99
|
+
this.store.statement('DELETE FROM machine_record_rows WHERE role = ? AND machine = ?').run(this.role, machine);
|
|
100
|
+
this.store.statement(`INSERT INTO machine_record_heads(role, machine, value) VALUES (?, ?, ?)
|
|
101
|
+
ON CONFLICT(role, machine) DO UPDATE SET value = excluded.value`).run(this.role, machine, JSON.stringify({ record: null, meta }));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
remove(machine) {
|
|
106
|
+
this.store.transaction(() => {
|
|
107
|
+
this.store.statement('DELETE FROM machine_record_rows WHERE role = ? AND machine = ?').run(this.role, machine);
|
|
108
|
+
this.store.statement('DELETE FROM machine_record_heads WHERE role = ? AND machine = ?').run(this.role, machine);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** One-time read of the old holder file. Only an unchanged, regular source
|
|
113
|
+
* is removed after its snapshots and receipt commit together. */
|
|
114
|
+
importLegacy(file) {
|
|
115
|
+
const receiptKey = 'machine_records_file_import';
|
|
116
|
+
const previous = this.store.database.metadata(receiptKey);
|
|
117
|
+
let stat, bytes;
|
|
118
|
+
try { stat = lstatSync(file); }
|
|
119
|
+
catch (error) { if (error.code === 'ENOENT') return; throw error; }
|
|
120
|
+
if (!stat.isFile()) throw new Error(`legacy machine records at ${file} are not a regular file; retained`);
|
|
121
|
+
if (stat.size > 64 * 1024 * 1024) throw new Error(`legacy machine records at ${file} exceed the 64 MiB import bound; retained`);
|
|
122
|
+
bytes = readFileSync(file);
|
|
123
|
+
const digest = createHash('sha256').update(bytes).digest('hex');
|
|
124
|
+
if (previous && (previous.file !== file || previous.sha256 !== digest)) throw new Error(`legacy machine records changed after import; ${file} is retained`);
|
|
125
|
+
if (!previous) {
|
|
126
|
+
const parsed = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
127
|
+
if (parsed.version !== 1 || !parsed.machines || typeof parsed.machines !== 'object' || Array.isArray(parsed.machines)) throw new Error(`unrecognized legacy machine records at ${file}; retained`);
|
|
128
|
+
this.store.transaction(() => {
|
|
129
|
+
for (const [machine, held] of Object.entries(parsed.machines)) {
|
|
130
|
+
// Older holders saved linked machines that had not supplied any
|
|
131
|
+
// snapshot yet. They have no rows to import; presence follows them.
|
|
132
|
+
if (held && typeof held === 'object' && !Array.isArray(held) && held.record == null) continue;
|
|
133
|
+
if (!held?.record || held.record.machine !== machine || !Number.isSafeInteger(held.record.seq) || held.record.seq < 0 || (held.record.sessions != null && !Array.isArray(held.record.sessions)) || (held.record.mail != null && (typeof held.record.mail !== 'object' || Array.isArray(held.record.mail)))) throw new Error(`invalid legacy record of ${machine}; ${file} is retained`);
|
|
134
|
+
if (!this.load(machine)) this.save(held.record, { sessions: held.record.sessions, mail: { '*': held.record.mail ?? {} }, panes: held.record.panes, capabilities: held.record.capabilities, unread: held.record.unread ?? {} }, { meta: { received_at: held.received_at ?? null } });
|
|
135
|
+
}
|
|
136
|
+
this.store.database.setMetadata(receiptKey, { file, sha256: digest, at_ms: Date.now() });
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const now = lstatSync(file);
|
|
140
|
+
if (!now.isFile() || now.ino !== stat.ino || now.dev !== stat.dev || !readFileSync(file).equals(bytes)) throw new Error(`legacy machine records changed before retirement; ${file} is retained`);
|
|
141
|
+
unlinkSync(file);
|
|
142
|
+
}
|
|
143
|
+
}
|