@songsid/agend 2.2.0-alpha.5 → 2.2.0-alpha.6

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.
Files changed (43) hide show
  1. package/dist/auth-api.d.ts +22 -0
  2. package/dist/auth-api.js +65 -1
  3. package/dist/auth-api.js.map +1 -1
  4. package/dist/channel/adapters/discord.js +9 -0
  5. package/dist/channel/adapters/discord.js.map +1 -1
  6. package/dist/command-table.d.ts +8 -1
  7. package/dist/command-table.js +13 -4
  8. package/dist/command-table.js.map +1 -1
  9. package/dist/fleet-manager.d.ts +38 -0
  10. package/dist/fleet-manager.js +128 -5
  11. package/dist/fleet-manager.js.map +1 -1
  12. package/dist/locale.js +10 -0
  13. package/dist/locale.js.map +1 -1
  14. package/dist/public-web-gateway.js +2 -2
  15. package/dist/public-web-gateway.js.map +1 -1
  16. package/dist/topic-commands.js +3 -2
  17. package/dist/topic-commands.js.map +1 -1
  18. package/dist/ui/chat-render.js +6 -0
  19. package/dist/ui/chat-strings.js +4 -0
  20. package/dist/ui/chat-thread.js +7 -3
  21. package/dist/ui/panel-chat.js +7 -0
  22. package/dist/ui/preview-panel.js +2 -1
  23. package/dist/ui/preview.js +49 -0
  24. package/dist/ui/shared/agend-auth.js +15 -7
  25. package/dist/ui/shared/app.css +8 -1
  26. package/dist/ui/shared/signin.js +38 -1
  27. package/dist/ui/signin.html +3 -0
  28. package/dist/web-chat-history.d.ts +97 -6
  29. package/dist/web-chat-history.js +189 -12
  30. package/dist/web-chat-history.js.map +1 -1
  31. package/dist/web-chat-store.d.ts +45 -0
  32. package/dist/web-chat-store.js +192 -0
  33. package/dist/web-chat-store.js.map +1 -0
  34. package/dist/web-login.d.ts +2 -0
  35. package/dist/web-login.js +4 -0
  36. package/dist/web-login.js.map +1 -1
  37. package/dist/web-returning-device.d.ts +84 -0
  38. package/dist/web-returning-device.js +204 -0
  39. package/dist/web-returning-device.js.map +1 -0
  40. package/dist/web-upload.d.ts +21 -0
  41. package/dist/web-upload.js +50 -1
  42. package/dist/web-upload.js.map +1 -1
  43. package/package.json +1 -1
@@ -0,0 +1,192 @@
1
+ /**
2
+ * #1565: the web chat's history on the local disk, so it survives a fleet restart — the latest messages per instance
3
+ * (the same bounds as in memory: 500 messages, about 1 MB of text), one file per instance:
4
+ * `<AGEND_HOME>/workspaces/<instance>/web-chat.json`, mode 0600, next to the instance's other files.
5
+ *
6
+ * Writes never block the fleet loop: they are debounced per instance, one in flight at a time, and go through
7
+ * fs.promises — a temporary file, fsync, then rename, so a crash leaves either the old file or the new one, never half
8
+ * of one. Reading happens once, at fleet start, for the instances that are configured; a file that is too large, not
9
+ * JSON, or not ours in shape is skipped with one log line and never stops the fleet.
10
+ */
11
+ import { closeSync, existsSync, openSync, readFileSync, fstatSync } from "node:fs";
12
+ import { mkdir, open, rename, unlink } from "node:fs/promises";
13
+ import { join } from "node:path";
14
+ import { constants } from "node:fs";
15
+ export const WEB_CHAT_FILE = "web-chat.json";
16
+ /** A file larger than this is not read (the bounds keep a real one far smaller). */
17
+ export const WEB_CHAT_FILE_MAX_BYTES = 8 * 1024 * 1024;
18
+ const FORMAT = 1;
19
+ /** The rule instance paths are held to: no separator, no control character, no traversal. */
20
+ export function isSafeInstanceDirName(name) {
21
+ return typeof name === "string" && name.length > 0 && name.length <= 128 && !/[/\\\u0000-\u001f\u007f]/.test(name) && name !== "." && !name.includes("..");
22
+ }
23
+ export class WebChatDiskStore {
24
+ home;
25
+ logger;
26
+ debounceMs;
27
+ /**
28
+ * Who the instance is now: a fleet agent's working directory, a ClassicBot room's channel. Written with the file
29
+ * and required to match when it is read, so a later instance that only shares the name (deleted while the fleet was
30
+ * down, then created again elsewhere) never gets another one's history. Null: not configured — not written.
31
+ */
32
+ ownerOf;
33
+ /** instance → its pending write (a timer) and whether one is in flight. */
34
+ pending = new Map();
35
+ /** Instances forgotten: a write already under way must not bring the file back. */
36
+ gone = new Set();
37
+ constructor(opts) {
38
+ this.home = opts.home;
39
+ this.ownerOf = opts.ownerOf;
40
+ this.logger = opts.logger;
41
+ this.debounceMs = opts.debounceMs ?? 300;
42
+ }
43
+ pathOf(instance) {
44
+ return isSafeInstanceDirName(instance) ? join(this.home, "workspaces", instance, WEB_CHAT_FILE) : null;
45
+ }
46
+ /**
47
+ * The stored messages of one instance, as written (validated later by the history), or null: no file, or one that
48
+ * is skipped (too large, unreadable, not JSON, not ours, another instance's) — said once in the log.
49
+ */
50
+ load(instance) {
51
+ const path = this.pathOf(instance);
52
+ if (!path || !existsSync(path))
53
+ return null;
54
+ let text;
55
+ try {
56
+ const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
57
+ try {
58
+ const st = fstatSync(fd);
59
+ if (!st.isFile() || st.size > WEB_CHAT_FILE_MAX_BYTES) {
60
+ this.logger.warn({ instance, path, size: st.size }, "web chat history: file skipped (not a file, or too large)");
61
+ return null;
62
+ }
63
+ text = readFileSync(fd, "utf-8");
64
+ }
65
+ finally {
66
+ closeSync(fd);
67
+ }
68
+ }
69
+ catch (err) {
70
+ this.logger.warn({ instance, path, err: err.message }, "web chat history: file skipped (unreadable)");
71
+ return null;
72
+ }
73
+ let data;
74
+ try {
75
+ data = JSON.parse(text);
76
+ }
77
+ catch {
78
+ this.logger.warn({ instance, path }, "web chat history: file skipped (not JSON)");
79
+ return null;
80
+ }
81
+ const d = data;
82
+ if (!d || typeof d !== "object" || d.format !== FORMAT || d.instance !== instance || !Array.isArray(d.messages)) {
83
+ this.logger.warn({ instance, path }, "web chat history: file skipped (not this instance's web chat history)");
84
+ return null;
85
+ }
86
+ const owner = this.ownerOf(instance);
87
+ if (owner === null || d.owner !== owner) {
88
+ this.logger.warn({ instance, path }, "web chat history: file skipped (written for another instance of this name)");
89
+ return null;
90
+ }
91
+ return d.messages;
92
+ }
93
+ /** The instance's history changed: write it soon (debounced; one write in flight; the latest snapshot wins). */
94
+ schedule(instance, snapshot) {
95
+ if (!this.pathOf(instance))
96
+ return;
97
+ this.gone.delete(instance);
98
+ let p = this.pending.get(instance);
99
+ if (!p) {
100
+ p = { timer: null, writing: null, again: false, snapshot };
101
+ this.pending.set(instance, p);
102
+ }
103
+ p.snapshot = snapshot;
104
+ if (p.writing) {
105
+ p.again = true;
106
+ return;
107
+ }
108
+ if (p.timer)
109
+ return;
110
+ p.timer = setTimeout(() => { p.timer = null; this.write(instance); }, this.debounceMs);
111
+ p.timer.unref?.();
112
+ }
113
+ write(instance) {
114
+ const p = this.pending.get(instance);
115
+ if (!p || this.gone.has(instance))
116
+ return;
117
+ const messages = p.snapshot();
118
+ p.writing = this.writeFile(instance, messages).catch((err) => {
119
+ this.logger.warn({ instance, err: err.message }, "web chat history: could not be written");
120
+ }).finally(() => {
121
+ p.writing = null;
122
+ if (p.again && !this.gone.has(instance)) {
123
+ p.again = false;
124
+ this.write(instance);
125
+ }
126
+ else if (!p.timer)
127
+ this.pending.delete(instance);
128
+ });
129
+ }
130
+ async writeFile(instance, messages) {
131
+ const path = this.pathOf(instance);
132
+ const owner = this.ownerOf(instance);
133
+ if (owner === null)
134
+ return; // not a configured instance: nothing to keep it for
135
+ const dir = join(this.home, "workspaces", instance);
136
+ await mkdir(dir, { recursive: true, mode: 0o700 });
137
+ const tmp = `${path}.tmp-${process.pid}`;
138
+ const body = JSON.stringify({ format: FORMAT, instance, owner, messages });
139
+ const fh = await open(tmp, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | (constants.O_NOFOLLOW ?? 0), 0o600);
140
+ try {
141
+ await fh.writeFile(body, "utf-8");
142
+ await fh.sync();
143
+ }
144
+ finally {
145
+ await fh.close();
146
+ }
147
+ if (this.gone.has(instance)) {
148
+ await unlink(tmp).catch(() => { });
149
+ return;
150
+ }
151
+ await rename(tmp, path);
152
+ }
153
+ /** Every pending write done (tests; a clean shutdown). */
154
+ async flush() {
155
+ for (;;) {
156
+ const busy = [...this.pending.entries()];
157
+ if (!busy.length)
158
+ return;
159
+ for (const [instance, p] of busy) {
160
+ if (p.timer) {
161
+ clearTimeout(p.timer);
162
+ p.timer = null;
163
+ this.write(instance);
164
+ }
165
+ }
166
+ await Promise.all([...this.pending.values()].map((p) => p.writing ?? Promise.resolve()));
167
+ if ([...this.pending.values()].every((p) => !p.writing && !p.timer && !p.again)) {
168
+ this.pending.clear();
169
+ return;
170
+ }
171
+ }
172
+ }
173
+ /** The instance is gone: no further write, and the file (and any temporary one of ours) is deleted. */
174
+ async remove(instance) {
175
+ const path = this.pathOf(instance);
176
+ if (!path)
177
+ return;
178
+ this.gone.add(instance);
179
+ const p = this.pending.get(instance);
180
+ if (p?.timer) {
181
+ clearTimeout(p.timer);
182
+ p.timer = null;
183
+ }
184
+ if (p?.writing)
185
+ await p.writing;
186
+ this.pending.delete(instance);
187
+ await unlink(path).catch((err) => { if (err.code !== "ENOENT")
188
+ this.logger.warn({ instance, err: err.message }, "web chat history: could not be deleted"); });
189
+ await unlink(`${path}.tmp-${process.pid}`).catch(() => { });
190
+ }
191
+ }
192
+ //# sourceMappingURL=web-chat-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-chat-store.js","sourceRoot":"","sources":["../src/web-chat-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACnF,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAGpC,MAAM,CAAC,MAAM,aAAa,GAAG,eAAe,CAAC;AAC7C,oFAAoF;AACpF,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AACvD,MAAM,MAAM,GAAG,CAAC,CAAC;AAIjB,6FAA6F;AAC7F,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,IAAI,GAAG,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AAC7J,CAAC;AAED,MAAM,OAAO,gBAAgB;IACV,IAAI,CAAS;IACb,MAAM,CAAqB;IAC3B,UAAU,CAAS;IACpC;;;;OAIG;IACc,OAAO,CAAsC;IAC9D,2EAA2E;IAC1D,OAAO,GAAG,IAAI,GAAG,EAAkJ,CAAC;IACrL,mFAAmF;IAClE,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAE1C,YAAY,IAAqH;QAC/H,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,GAAG,CAAC;IAC3C,CAAC;IAED,MAAM,CAAC,QAAgB;QACrB,OAAO,qBAAqB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACzG,CAAC;IAED;;;OAGG;IACH,IAAI,CAAC,QAAgB;QACnB,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACnC,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QAC5C,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,QAAQ,GAAG,CAAC,SAAS,CAAC,UAAU,IAAI,CAAC,CAAC,CAAC,CAAC;YAC5E,IAAI,CAAC;gBACH,MAAM,EAAE,GAAG,SAAS,CAAC,EAAE,CAAC,CAAC;gBACzB,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,IAAI,GAAG,uBAAuB,EAAE,CAAC;oBACtD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,2DAA2D,CAAC,CAAC;oBACjH,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,IAAI,GAAG,YAAY,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;YACnC,CAAC;oBAAS,CAAC;gBAAC,SAAS,CAAC,EAAE,CAAC,CAAC;YAAC,CAAC;QAC9B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAG,GAAa,CAAC,OAAO,EAAE,EAAE,6CAA6C,CAAC,CAAC;YACjH,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,IAAa,CAAC;QAClB,IAAI,CAAC;YAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC;YACtC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,2CAA2C,CAAC,CAAC;YAClF,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,CAAC,GAAG,IAAqF,CAAC;QAChG,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC;YAChH,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,uEAAuE,CAAC,CAAC;YAC9G,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;YACxC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,4EAA4E,CAAC,CAAC;YACnH,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,CAAC,CAAC,QAAQ,CAAC;IACpB,CAAC;IAED,gHAAgH;IAChH,QAAQ,CAAC,QAAgB,EAAE,QAAsC;QAC/D,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC;YAAE,OAAO;QACnC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC3B,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACnC,IAAI,CAAC,CAAC,EAAE,CAAC;YAAC,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;YAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;QAAC,CAAC;QACtG,CAAC,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACtB,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;YAAC,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC;YAAC,OAAO;QAAC,CAAC;QAC1C,IAAI,CAAC,CAAC,KAAK;YAAE,OAAO;QACpB,CAAC,CAAC,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,GAAG,CAAE,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;QACxF,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;IACpB,CAAC;IAEO,KAAK,CAAC,QAAgB;QAC5B,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,OAAO;QAC1C,MAAM,QAAQ,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC;QAC9B,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YAC3D,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAG,GAAa,CAAC,OAAO,EAAE,EAAE,wCAAwC,CAAC,CAAC;QACxG,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE;YACd,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC;YACjB,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAAC,CAAC,CAAC,KAAK,GAAG,KAAK,CAAC;gBAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YAAC,CAAC;iBAC9E,IAAI,CAAC,CAAC,CAAC,KAAK;gBAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACnD,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,SAAS,CAAC,QAAgB,EAAE,QAAgC;QACxE,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAE,CAAC;QACpC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,CAAuB,oDAAoD;QACtG,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC;QACpD,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACnD,MAAM,GAAG,GAAG,GAAG,IAAI,QAAQ,OAAO,CAAC,GAAG,EAAE,CAAC;QACzC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC3E,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,QAAQ,GAAG,SAAS,CAAC,OAAO,GAAG,SAAS,CAAC,OAAO,GAAG,CAAC,SAAS,CAAC,UAAU,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QAC5H,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YAClC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC;QAClB,CAAC;gBAAS,CAAC;YAAC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;QAAC,CAAC;QAC/B,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAAC,MAAM,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YAAC,OAAO;QAAC,CAAC;QAC3E,MAAM,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC1B,CAAC;IAED,0DAA0D;IAC1D,KAAK,CAAC,KAAK;QACT,SAAS,CAAC;YACR,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;YACzC,IAAI,CAAC,IAAI,CAAC,MAAM;gBAAE,OAAO;YACzB,KAAK,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;gBACjC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;oBAAC,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;oBAAC,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC;oBAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;gBAAC,CAAC;YAC/E,CAAC;YACD,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YACzF,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;gBAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;gBAAC,OAAO;YAAC,CAAC;QACpH,CAAC;IACH,CAAC;IAED,uGAAuG;IACvG,KAAK,CAAC,MAAM,CAAC,QAAgB;QAC3B,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACnC,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACxB,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC;YAAC,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;YAAC,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC;QAAC,CAAC;QACxD,IAAI,CAAC,EAAE,OAAO;YAAE,MAAM,CAAC,CAAC,OAAO,CAAC;QAChC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC9B,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,GAA0B,EAAE,EAAE,GAAG,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ;YAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAG,CAAC,OAAO,EAAE,EAAE,wCAAwC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACrL,MAAM,MAAM,CAAC,GAAG,IAAI,QAAQ,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC7D,CAAC;CACF"}
@@ -59,6 +59,8 @@ export declare class WebLoginCodes {
59
59
  audience?: string;
60
60
  owner?: LoginCodeOwner;
61
61
  }): IssuedLoginCode;
62
+ /** #1570: how long redemption stays paused by the breaker (0 when it is closed). A code request is refused meanwhile. */
63
+ pausedForMs(): number;
62
64
  /** Whether a redemption right now would be compared against anything. */
63
65
  get hasOutstandingCode(): boolean;
64
66
  /** Withdraw the outstanding code without spending it (`/dashboard revoke`). */
package/dist/web-login.js CHANGED
@@ -61,6 +61,10 @@ export class WebLoginCodes {
61
61
  this.onEvent("issued");
62
62
  return { display: formatOneTimeCode(code), expiresAt, tier, issuanceId };
63
63
  }
64
+ /** #1570: how long redemption stays paused by the breaker (0 when it is closed). A code request is refused meanwhile. */
65
+ pausedForMs() {
66
+ return Math.max(0, this.pausedUntil - this.now());
67
+ }
64
68
  /** Whether a redemption right now would be compared against anything. */
65
69
  get hasOutstandingCode() {
66
70
  return this.activeCode() !== null;
@@ -1 +1 @@
1
- {"version":3,"file":"web-login.js","sourceRoot":"","sources":["../src/web-login.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"web-login.js","sourceRoot":"","sources":["../src/web-login.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,84 @@
1
+ import type { LoginCodeOwner } from "./web-login.js";
2
+ import type { SessionSurface } from "./web-session.js";
3
+ export declare const DEVICE_COOKIE = "agend_device";
4
+ /** On https (and always on the public link): a `__Host-` cookie cannot be planted by a sibling site. */
5
+ export declare const DEVICE_COOKIE_SECURE = "__Host-agend_device";
6
+ export declare const DEVICE_MAX_AGE_MS: number;
7
+ export declare const MAX_RETURNING_DEVICES = 16;
8
+ /** What the registry knows about one returning device. No cookie value; the owner minus the live binding. */
9
+ export interface ReturningDevice {
10
+ readonly idHash: string;
11
+ readonly owner: {
12
+ readonly adapterId: string;
13
+ readonly userId: string;
14
+ readonly chatId: string;
15
+ readonly threadId?: string;
16
+ };
17
+ readonly surface: SessionSurface;
18
+ readonly label: string;
19
+ readonly tokenEpoch: string;
20
+ readonly created: number;
21
+ readonly expires: number;
22
+ }
23
+ export declare class ReturningDevices {
24
+ private readonly byHash;
25
+ private readonly path;
26
+ private readonly now;
27
+ private readonly warn;
28
+ constructor(opts?: {
29
+ dataDir?: string;
30
+ now?: () => number;
31
+ onWarn?: (message: string) => void;
32
+ });
33
+ get size(): number;
34
+ /** Record this browser as a returning device of `owner`; the returned value is the cookie, given out once. */
35
+ remember(owner: LoginCodeOwner, input: {
36
+ surface: SessionSurface;
37
+ label: string;
38
+ tokenEpoch: string;
39
+ webToken: string;
40
+ }): string;
41
+ /** The device a cookie value proves, or null: bad shape, bad signature (another token), unknown, expired, old epoch. */
42
+ verify(value: string | undefined, webToken: string, tokenEpoch: string): ReturningDevice | null;
43
+ /** `/dashboard revoke`: no browser may ask for a code any more. Whether that holds after a restart. */
44
+ revokeAll(): {
45
+ count: number;
46
+ durable: boolean;
47
+ };
48
+ private load;
49
+ /**
50
+ * Write the registry; whether a restart now sees what memory sees. A failed write after a revoke must not leave the
51
+ * old file to bring revoked devices back: then the file is removed (every browser loses its button — the safe side).
52
+ */
53
+ private persist;
54
+ }
55
+ /** The device cookie a request presents: the `__Host-` one first; the plain one only off the public link. */
56
+ export declare function readDeviceCookie(cookieHeader: string | undefined, publicLink: boolean): string | undefined;
57
+ export declare function buildDeviceCookie(value: string, secure: boolean): string;
58
+ /** Per device: one request a minute and five an hour. For the whole fleet: one every 20 s and twenty an hour. */
59
+ export declare const CODE_REQUEST_LIMITS: {
60
+ readonly deviceGapMs: 60000;
61
+ readonly devicePerHour: 5;
62
+ readonly globalGapMs: 20000;
63
+ readonly globalPerHour: 20;
64
+ };
65
+ /** Rate limits for code requests, on a monotonic clock (a wall-clock jump neither opens nor closes them). */
66
+ export declare class CodeRequestLimiter {
67
+ private readonly now;
68
+ private readonly limits;
69
+ private readonly byDevice;
70
+ private global;
71
+ constructor(now?: () => number, limits?: {
72
+ readonly deviceGapMs: 60000;
73
+ readonly devicePerHour: 5;
74
+ readonly globalGapMs: 20000;
75
+ readonly globalPerHour: 20;
76
+ });
77
+ /** Admit one request for `deviceKey` now, or say how long until one would be admitted. */
78
+ admit(deviceKey: string): {
79
+ ok: true;
80
+ } | {
81
+ ok: false;
82
+ retryAfterMs: number;
83
+ };
84
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * #1570: a browser that signed in before may ask the sign-in page for a fresh code, which goes privately to the person
3
+ * whose `/dashboard` code it last redeemed. This deliberately bends `web-login.ts`'s "no outstanding code, nothing to
4
+ * guess" rule, so the door is narrow (design baseline: #1570 issuecomment-6098782305):
5
+ *
6
+ * - **Only a returning device.** The cookie is set only after a sign-in whose code carried a `LoginCodeOwner` (a code
7
+ * `/dashboard` delivered privately). It is `<deviceId>.<HMAC>`: a 128-bit random id and a signature keyed from
8
+ * `web.token`. It names no chat or user; the owner lives in this registry, which keeps `sha256(deviceId)` only, so the
9
+ * file on disk cannot be replayed as a cookie.
10
+ * - **Rotation and revoke.** The HMAC key and the recorded token epoch both come from `web.token`, so `agend web-token
11
+ * rotate` (from any process) makes every device cookie stop verifying. `/dashboard revoke` empties the registry.
12
+ * - **Rate limits** live here too (`CodeRequestLimiter`), on a monotonic clock.
13
+ *
14
+ * Who receives the code, and whether they still may, is decided by the fleet (FleetManager.requestReturningCode).
15
+ * Deliberately has no idea what an HTTP request is, beyond reading and building the cookie.
16
+ */
17
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
18
+ import { chmodSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ import { performance } from "node:perf_hooks";
21
+ export const DEVICE_COOKIE = "agend_device";
22
+ /** On https (and always on the public link): a `__Host-` cookie cannot be planted by a sibling site. */
23
+ export const DEVICE_COOKIE_SECURE = "__Host-agend_device";
24
+ export const DEVICE_MAX_AGE_MS = 90 * 24 * 60 * 60 * 1000;
25
+ export const MAX_RETURNING_DEVICES = 16;
26
+ const ID_PATTERN = /^[0-9a-f]{32}$/;
27
+ const SIG_PATTERN = /^[0-9a-f]{64}$/;
28
+ const HASH_PATTERN = /^[0-9a-f]{64}$/;
29
+ function signingKey(webToken) {
30
+ return createHmac("sha256", webToken).update("agend returning device v1").digest();
31
+ }
32
+ function sign(deviceId, webToken) {
33
+ return createHmac("sha256", signingKey(webToken)).update(deviceId).digest("hex");
34
+ }
35
+ const sha256 = (s) => createHash("sha256").update(s).digest("hex");
36
+ function isDevice(v) {
37
+ if (!v || typeof v !== "object")
38
+ return false;
39
+ const d = v, o = d.owner;
40
+ return typeof d.idHash === "string" && HASH_PATTERN.test(d.idHash)
41
+ && !!o && typeof o.adapterId === "string" && typeof o.userId === "string" && typeof o.chatId === "string"
42
+ && (o.threadId === undefined || typeof o.threadId === "string")
43
+ && (d.surface === "local" || d.surface === "gateway")
44
+ && typeof d.label === "string" && d.label.length <= 80
45
+ && typeof d.tokenEpoch === "string" && HASH_PATTERN.test(d.tokenEpoch)
46
+ && typeof d.created === "number" && Number.isFinite(d.created) && typeof d.expires === "number" && Number.isFinite(d.expires);
47
+ }
48
+ export class ReturningDevices {
49
+ byHash = new Map();
50
+ path;
51
+ now;
52
+ warn;
53
+ constructor(opts = {}) {
54
+ this.path = opts.dataDir ? join(opts.dataDir, "web-devices.json") : null;
55
+ this.now = opts.now ?? Date.now;
56
+ this.warn = opts.onWarn ?? (() => { });
57
+ this.load();
58
+ }
59
+ get size() { return this.byHash.size; }
60
+ /** Record this browser as a returning device of `owner`; the returned value is the cookie, given out once. */
61
+ remember(owner, input) {
62
+ const deviceId = randomBytes(16).toString("hex");
63
+ const now = this.now();
64
+ const device = {
65
+ idHash: sha256(deviceId),
66
+ owner: { adapterId: owner.adapterId, userId: owner.userId, chatId: owner.chatId, ...(owner.threadId !== undefined ? { threadId: owner.threadId } : {}) },
67
+ surface: input.surface, label: input.label.slice(0, 80), tokenEpoch: input.tokenEpoch, created: now, expires: now + DEVICE_MAX_AGE_MS,
68
+ };
69
+ this.byHash.set(device.idHash, device);
70
+ while (this.byHash.size > MAX_RETURNING_DEVICES) {
71
+ const oldest = [...this.byHash.values()].reduce((a, b) => (a.created <= b.created ? a : b));
72
+ this.byHash.delete(oldest.idHash);
73
+ }
74
+ this.persist();
75
+ return `${deviceId}.${sign(deviceId, input.webToken)}`;
76
+ }
77
+ /** The device a cookie value proves, or null: bad shape, bad signature (another token), unknown, expired, old epoch. */
78
+ verify(value, webToken, tokenEpoch) {
79
+ if (!value)
80
+ return null;
81
+ const dot = value.indexOf(".");
82
+ const id = value.slice(0, dot), sig = value.slice(dot + 1);
83
+ if (dot < 0 || !ID_PATTERN.test(id) || !SIG_PATTERN.test(sig))
84
+ return null;
85
+ const expected = Buffer.from(sign(id, webToken), "hex");
86
+ if (!timingSafeEqual(expected, Buffer.from(sig, "hex")))
87
+ return null;
88
+ const device = this.byHash.get(sha256(id));
89
+ if (!device || device.tokenEpoch !== tokenEpoch || this.now() >= device.expires)
90
+ return null;
91
+ return device;
92
+ }
93
+ /** `/dashboard revoke`: no browser may ask for a code any more. Whether that holds after a restart. */
94
+ revokeAll() {
95
+ const count = this.byHash.size;
96
+ this.byHash.clear();
97
+ return { count, durable: this.persist() };
98
+ }
99
+ load() {
100
+ if (!this.path)
101
+ return;
102
+ let raw;
103
+ try {
104
+ raw = JSON.parse(readFileSync(this.path, "utf8"));
105
+ }
106
+ catch {
107
+ return;
108
+ }
109
+ const devices = raw && typeof raw === "object" && Array.isArray(raw.devices) ? raw.devices : [];
110
+ const now = this.now();
111
+ for (const d of devices)
112
+ if (isDevice(d) && d.expires > now)
113
+ this.byHash.set(d.idHash, d);
114
+ }
115
+ /**
116
+ * Write the registry; whether a restart now sees what memory sees. A failed write after a revoke must not leave the
117
+ * old file to bring revoked devices back: then the file is removed (every browser loses its button — the safe side).
118
+ */
119
+ persist() {
120
+ if (!this.path)
121
+ return true;
122
+ const temp = `${this.path}.tmp-${process.pid}-${randomBytes(6).toString("hex")}`;
123
+ try {
124
+ mkdirSync(join(this.path, ".."), { recursive: true });
125
+ writeFileSync(temp, JSON.stringify({ version: 1, devices: [...this.byHash.values()] }), { encoding: "utf8", mode: 0o600, flag: "wx" });
126
+ renameSync(temp, this.path);
127
+ try {
128
+ chmodSync(this.path, 0o600);
129
+ }
130
+ catch { /* best effort */ }
131
+ return true;
132
+ }
133
+ catch (err) {
134
+ try {
135
+ unlinkSync(temp);
136
+ }
137
+ catch { /* never created */ }
138
+ try {
139
+ unlinkSync(this.path);
140
+ return true;
141
+ }
142
+ catch (e) {
143
+ if (e.code === "ENOENT")
144
+ return true;
145
+ this.warn(`Returning-device registry could not be saved or removed (${err.message}); a restart may bring revoked devices back until the web token is rotated`);
146
+ return false;
147
+ }
148
+ }
149
+ }
150
+ }
151
+ /** The device cookie a request presents: the `__Host-` one first; the plain one only off the public link. */
152
+ export function readDeviceCookie(cookieHeader, publicLink) {
153
+ const jar = new Map();
154
+ for (const part of (cookieHeader ?? "").split(";")) {
155
+ const eq = part.indexOf("=");
156
+ if (eq < 1)
157
+ continue;
158
+ jar.set(part.slice(0, eq).trim(), part.slice(eq + 1).trim());
159
+ }
160
+ return jar.get(DEVICE_COOKIE_SECURE) ?? (publicLink ? undefined : jar.get(DEVICE_COOKIE));
161
+ }
162
+ export function buildDeviceCookie(value, secure) {
163
+ return [`${secure ? DEVICE_COOKIE_SECURE : DEVICE_COOKIE}=${value}`, "Path=/", "HttpOnly", "SameSite=Strict",
164
+ `Max-Age=${Math.floor(DEVICE_MAX_AGE_MS / 1000)}`, ...(secure ? ["Secure"] : [])].join("; ");
165
+ }
166
+ /** Per device: one request a minute and five an hour. For the whole fleet: one every 20 s and twenty an hour. */
167
+ export const CODE_REQUEST_LIMITS = { deviceGapMs: 60_000, devicePerHour: 5, globalGapMs: 20_000, globalPerHour: 20 };
168
+ const HOUR_MS = 60 * 60 * 1000;
169
+ /** Rate limits for code requests, on a monotonic clock (a wall-clock jump neither opens nor closes them). */
170
+ export class CodeRequestLimiter {
171
+ now;
172
+ limits;
173
+ byDevice = new Map();
174
+ global = [];
175
+ constructor(now = () => performance.now(), limits = CODE_REQUEST_LIMITS) {
176
+ this.now = now;
177
+ this.limits = limits;
178
+ }
179
+ /** Admit one request for `deviceKey` now, or say how long until one would be admitted. */
180
+ admit(deviceKey) {
181
+ const now = this.now();
182
+ const recent = (this.byDevice.get(deviceKey) ?? []).filter(t => now - t < HOUR_MS);
183
+ this.global = this.global.filter(t => now - t < HOUR_MS);
184
+ const waits = [];
185
+ const last = recent.at(-1), lastGlobal = this.global.at(-1);
186
+ if (last !== undefined && now - last < this.limits.deviceGapMs)
187
+ waits.push(this.limits.deviceGapMs - (now - last));
188
+ if (recent.length >= this.limits.devicePerHour)
189
+ waits.push(HOUR_MS - (now - recent[0]));
190
+ if (lastGlobal !== undefined && now - lastGlobal < this.limits.globalGapMs)
191
+ waits.push(this.limits.globalGapMs - (now - lastGlobal));
192
+ if (this.global.length >= this.limits.globalPerHour)
193
+ waits.push(HOUR_MS - (now - this.global[0]));
194
+ if (waits.length) {
195
+ this.byDevice.set(deviceKey, recent);
196
+ return { ok: false, retryAfterMs: Math.max(...waits) };
197
+ }
198
+ recent.push(now);
199
+ this.global.push(now);
200
+ this.byDevice.set(deviceKey, recent);
201
+ return { ok: true };
202
+ }
203
+ }
204
+ //# sourceMappingURL=web-returning-device.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-returning-device.js","sourceRoot":"","sources":["../src/web-returning-device.ts"],"names":[],"mappings":"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"}
@@ -133,6 +133,27 @@ export declare class WebFileLedger {
133
133
  file: ServedFile;
134
134
  bytes: Buffer;
135
135
  } | null;
136
+ /**
137
+ * #1565: the path to keep with a message's file id, so the file can be served again after a restart — null when
138
+ * nothing is served under the id, or it is an upload no message has taken (web-pending-…): that is never kept.
139
+ */
140
+ storablePath(id: string): string | null;
141
+ /** #1565: these ids are no longer shown (their message left the history's window): nothing is served under them. */
142
+ dropServed(ids: Iterable<string>): void;
143
+ /**
144
+ * #1565: serve again, under the id it had, a file a message showed before the fleet restarted — only if it still
145
+ * passes every check: an id of ours in shape; never an upload no message took (web-pending-…); the path itself not a
146
+ * symlink, and a regular file within MAX_SERVED_BYTES; its real path inside this instance's inbox
147
+ * (`workspaces/<instance>/inbox`, itself not a link elsewhere). Null when any check fails (the message then shows the
148
+ * name as unavailable). Fetched by id only, as ever: the stored path is the fleet's own file, never a request's.
149
+ */
150
+ restoreServed(entry: {
151
+ id: unknown;
152
+ path: unknown;
153
+ name?: unknown;
154
+ mime?: unknown;
155
+ kind?: unknown;
156
+ }, instance: string, inboxDir: string): ServedFile | null;
136
157
  /** Forget an instance's files (it was deleted). The files themselves are its workspace's to remove. */
137
158
  forget(instance: string): void;
138
159
  /**
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import { randomBytes } from "node:crypto";
15
15
  import { constants as fsConstants, closeSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
16
- import { basename, dirname, extname, join } from "node:path";
16
+ import { basename, dirname, extname, join, sep } from "node:path";
17
17
  export const UPLOAD_LIMITS = {
18
18
  /** One file. */
19
19
  maxFileBytes: 10 * 1024 * 1024,
@@ -311,6 +311,55 @@ export class WebFileLedger {
311
311
  closeSync(fd);
312
312
  }
313
313
  }
314
+ /**
315
+ * #1565: the path to keep with a message's file id, so the file can be served again after a restart — null when
316
+ * nothing is served under the id, or it is an upload no message has taken (web-pending-…): that is never kept.
317
+ */
318
+ storablePath(id) {
319
+ const file = isFileId(id) ? this.served.get(id) : undefined;
320
+ return file && !basename(file.realPath).startsWith(PENDING_PREFIX) ? file.realPath : null;
321
+ }
322
+ /** #1565: these ids are no longer shown (their message left the history's window): nothing is served under them. */
323
+ dropServed(ids) { for (const id of ids)
324
+ this.served.delete(id); }
325
+ /**
326
+ * #1565: serve again, under the id it had, a file a message showed before the fleet restarted — only if it still
327
+ * passes every check: an id of ours in shape; never an upload no message took (web-pending-…); the path itself not a
328
+ * symlink, and a regular file within MAX_SERVED_BYTES; its real path inside this instance's inbox
329
+ * (`workspaces/<instance>/inbox`, itself not a link elsewhere). Null when any check fails (the message then shows the
330
+ * name as unavailable). Fetched by id only, as ever: the stored path is the fleet's own file, never a request's.
331
+ */
332
+ restoreServed(entry, instance, inboxDir) {
333
+ if (typeof entry.id !== "string" || !isFileId(entry.id) || typeof entry.path !== "string" || !entry.path)
334
+ return null;
335
+ if (basename(entry.path).startsWith(PENDING_PREFIX))
336
+ return null;
337
+ let real;
338
+ let own;
339
+ try {
340
+ own = lstatSync(entry.path);
341
+ if (own.isSymbolicLink() || !own.isFile() || own.size > MAX_SERVED_BYTES)
342
+ return null;
343
+ const inbox = realpathSync(inboxDir);
344
+ if (inbox !== join(realpathSync(dirname(inboxDir)), basename(inboxDir)))
345
+ return null;
346
+ real = realpathSync(entry.path);
347
+ if (!real.startsWith(inbox + sep))
348
+ return null;
349
+ }
350
+ catch {
351
+ return null;
352
+ }
353
+ const kind = entry.kind === "photo" || entry.kind === "document" ? entry.kind : undefined;
354
+ const file = this.registerServed({ id: entry.id, path: real, instance,
355
+ ...(typeof entry.name === "string" ? { name: entry.name } : {}), ...(typeof entry.mime === "string" ? { mime: entry.mime } : {}), ...(kind ? { kind } : {}) });
356
+ // The file checked is the file registered (nothing swapped in between).
357
+ if (file && (file.realPath !== real || file.dev !== own.dev || file.ino !== own.ino)) {
358
+ this.served.delete(file.id);
359
+ return null;
360
+ }
361
+ return file;
362
+ }
314
363
  /** Forget an instance's files (it was deleted). The files themselves are its workspace's to remove. */
315
364
  forget(instance) {
316
365
  for (const [id, e] of this.uploads)