@rubytech/create-maxy-code 0.1.603 → 0.1.605
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/email-sampler-unit.test.js +22 -0
- package/dist/index.js +3 -0
- package/dist/port-resolution.js +9 -1
- package/package.json +1 -1
- package/payload/platform/lib/routine-templates/dist/roster.d.ts +46 -2
- package/payload/platform/lib/routine-templates/dist/roster.d.ts.map +1 -1
- package/payload/platform/lib/routine-templates/dist/roster.js +84 -32
- package/payload/platform/lib/routine-templates/dist/roster.js.map +1 -1
- package/payload/platform/lib/routine-templates/src/__tests__/roster.test.ts +0 -0
- package/payload/platform/lib/routine-templates/src/__tests__/seed.test.ts +48 -2
- package/payload/platform/lib/routine-templates/src/roster.ts +133 -35
- package/payload/platform/lib/telegram-store-read/dist/index.d.ts +59 -0
- package/payload/platform/lib/telegram-store-read/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/telegram-store-read/dist/index.js +219 -0
- package/payload/platform/lib/telegram-store-read/dist/index.js.map +1 -0
- package/payload/platform/lib/telegram-store-read/src/__tests__/fold.test.ts +124 -0
- package/payload/platform/lib/telegram-store-read/src/index.ts +262 -0
- package/payload/platform/lib/telegram-store-read/tsconfig.json +9 -0
- package/payload/platform/lib/telegram-store-read/vitest.config.ts +9 -0
- package/payload/platform/package.json +2 -2
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +39 -6
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +11 -0
- package/payload/platform/plugins/connector/PLUGIN.md +9 -1
- package/payload/platform/plugins/connector/mcp/dist/__tests__/queue-descriptor.test.d.ts +2 -0
- package/payload/platform/plugins/connector/mcp/dist/__tests__/queue-descriptor.test.d.ts.map +1 -0
- package/payload/platform/plugins/connector/mcp/dist/__tests__/queue-descriptor.test.js +67 -0
- package/payload/platform/plugins/connector/mcp/dist/__tests__/queue-descriptor.test.js.map +1 -0
- package/payload/platform/plugins/connector/mcp/dist/index.js +36 -5
- package/payload/platform/plugins/connector/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/connector/mcp/dist/lib/store.d.ts +35 -0
- package/payload/platform/plugins/connector/mcp/dist/lib/store.d.ts.map +1 -1
- package/payload/platform/plugins/connector/mcp/dist/lib/store.js +33 -0
- package/payload/platform/plugins/connector/mcp/dist/lib/store.js.map +1 -1
- package/payload/platform/plugins/connector/mcp/package.json +4 -2
- package/payload/platform/plugins/connector/mcp/vitest.config.ts +8 -0
- package/payload/platform/plugins/docs/references/admin-ui.md +2 -2
- package/payload/platform/plugins/docs/references/outlook-guide.md +21 -2
- package/payload/platform/plugins/docs/references/telegram-guide.md +15 -1
- package/payload/platform/plugins/email/PLUGIN.md +14 -1
- package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/retention.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/retention.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/retention.test.js +233 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/retention.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/sample-mailbox.test.js +30 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/sample-mailbox.test.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/sampler/main.js +68 -3
- package/payload/platform/plugins/email/mcp/dist/sampler/main.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/sampler/retention-sources.d.ts +3 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/retention-sources.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/retention-sources.js +146 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/retention-sources.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/retention.d.ts +150 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/retention.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/retention.js +199 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/retention.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/sample-mailbox.d.ts +5 -0
- package/payload/platform/plugins/email/mcp/dist/sampler/sample-mailbox.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/sampler/sample-mailbox.js +10 -5
- package/payload/platform/plugins/email/mcp/dist/sampler/sample-mailbox.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-section-properties-strip.test.js +11 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-section-properties-strip.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +8 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sample-outlook-mailbox.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sample-outlook-mailbox.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sample-outlook-mailbox.test.js +324 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sample-outlook-mailbox.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-cursor.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-cursor.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-cursor.test.js +102 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-cursor.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-enumerate.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-enumerate.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-enumerate.test.js +125 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/sampler-enumerate.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/arm.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/arm.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/arm.js +109 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/arm.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/cursor.d.ts +25 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/cursor.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/cursor.js +94 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/cursor.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/enumerate.d.ts +41 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/enumerate.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/enumerate.js +122 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/enumerate.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/sample-outlook-mailbox.d.ts +73 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/sample-outlook-mailbox.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/sample-outlook-mailbox.js +162 -0
- package/payload/platform/plugins/outlook/mcp/dist/sampler/sample-outlook-mailbox.js.map +1 -0
- package/payload/platform/plugins/scheduling/PLUGIN.md +8 -1
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-flag.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-flag.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-flag.test.js +90 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-flag.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate.test.js +26 -4
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate-flag.d.ts +78 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate-flag.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate-flag.js +76 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate-flag.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.d.ts +8 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.js +9 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/gate-dispatch-wiring.test.js +15 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/gate-dispatch-wiring.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts +12 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js +34 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +9 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.js +40 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js +435 -5
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +21 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.d.ts +294 -6
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js +553 -11
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js.map +1 -1
- package/payload/platform/plugins/telegram/PLUGIN.md +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/messages.test.d.ts +2 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/messages.test.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/messages.test.js +157 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/messages.test.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/public-pin.test.js +2 -2
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/public-pin.test.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/index.js +32 -34
- package/payload/platform/plugins/telegram/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/lib/public-pin.js +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/lib/public-pin.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/tools/messages.d.ts +27 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/messages.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/messages.js +152 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/messages.js.map +1 -0
- package/payload/platform/plugins/telegram/skills/build/SKILL.md +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
- package/payload/server/public/operator.html +1 -1
- package/payload/server/server.js +69 -5
- package/payload/platform/plugins/telegram/mcp/dist/tools/message-history.d.ts +0 -16
- package/payload/platform/plugins/telegram/mcp/dist/tools/message-history.d.ts.map +0 -1
- package/payload/platform/plugins/telegram/mcp/dist/tools/message-history.js +0 -68
- package/payload/platform/plugins/telegram/mcp/dist/tools/message-history.js.map +0 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { readCursor, writeCursor } from "../sampler/cursor.js";
|
|
7
|
+
function tmp() {
|
|
8
|
+
return mkdtempSync(join(tmpdir(), "outlook-cursor-"));
|
|
9
|
+
}
|
|
10
|
+
function pathFor(dir, accountId) {
|
|
11
|
+
return join(dir, accountId, "secrets", "outlook", "sampler-cursors.json");
|
|
12
|
+
}
|
|
13
|
+
test("a mailbox with no cursor reads null", () => {
|
|
14
|
+
assert.equal(readCursor(tmp(), "acct-1", "a@example.com"), null);
|
|
15
|
+
});
|
|
16
|
+
test("a written cursor reads back for that mailbox only", () => {
|
|
17
|
+
const dir = tmp();
|
|
18
|
+
writeCursor(dir, "acct-1", "a@example.com", {
|
|
19
|
+
newestReceivedAt: "2026-08-13T09:00:00Z",
|
|
20
|
+
idsAtNewest: ["id-1"],
|
|
21
|
+
});
|
|
22
|
+
assert.deepEqual(readCursor(dir, "acct-1", "a@example.com"), {
|
|
23
|
+
newestReceivedAt: "2026-08-13T09:00:00Z",
|
|
24
|
+
idsAtNewest: ["id-1"],
|
|
25
|
+
});
|
|
26
|
+
assert.equal(readCursor(dir, "acct-1", "b@example.com"), null);
|
|
27
|
+
});
|
|
28
|
+
test("a second mailbox's write preserves the first", () => {
|
|
29
|
+
const dir = tmp();
|
|
30
|
+
writeCursor(dir, "acct-1", "a@example.com", {
|
|
31
|
+
newestReceivedAt: "2026-08-13T09:00:00Z",
|
|
32
|
+
idsAtNewest: ["id-1"],
|
|
33
|
+
});
|
|
34
|
+
writeCursor(dir, "acct-1", "b@example.com", {
|
|
35
|
+
newestReceivedAt: "2026-08-13T10:00:00Z",
|
|
36
|
+
idsAtNewest: ["id-2"],
|
|
37
|
+
});
|
|
38
|
+
assert.equal(readCursor(dir, "acct-1", "a@example.com")?.newestReceivedAt, "2026-08-13T09:00:00Z");
|
|
39
|
+
assert.equal(readCursor(dir, "acct-1", "b@example.com")?.newestReceivedAt, "2026-08-13T10:00:00Z");
|
|
40
|
+
});
|
|
41
|
+
test("a second account's cursors are a separate file", () => {
|
|
42
|
+
const dir = tmp();
|
|
43
|
+
writeCursor(dir, "acct-1", "a@example.com", {
|
|
44
|
+
newestReceivedAt: "2026-08-13T09:00:00Z",
|
|
45
|
+
idsAtNewest: [],
|
|
46
|
+
});
|
|
47
|
+
assert.equal(readCursor(dir, "acct-2", "a@example.com"), null);
|
|
48
|
+
});
|
|
49
|
+
test("the mailbox key is case- and whitespace-insensitive", () => {
|
|
50
|
+
const dir = tmp();
|
|
51
|
+
writeCursor(dir, "acct-1", " A@Example.com ", {
|
|
52
|
+
newestReceivedAt: "2026-08-13T09:00:00Z",
|
|
53
|
+
idsAtNewest: [],
|
|
54
|
+
});
|
|
55
|
+
assert.ok(readCursor(dir, "acct-1", "a@example.com"));
|
|
56
|
+
});
|
|
57
|
+
test("the cursor file is written 0600 and holds the mailbox key", () => {
|
|
58
|
+
const dir = tmp();
|
|
59
|
+
writeCursor(dir, "acct-1", "a@example.com", {
|
|
60
|
+
newestReceivedAt: "2026-08-13T09:00:00Z",
|
|
61
|
+
idsAtNewest: [],
|
|
62
|
+
});
|
|
63
|
+
const path = pathFor(dir, "acct-1");
|
|
64
|
+
assert.equal(statSync(path).mode & 0o777, 0o600);
|
|
65
|
+
assert.ok(Object.hasOwn(JSON.parse(readFileSync(path, "utf-8")), "a@example.com"));
|
|
66
|
+
});
|
|
67
|
+
test("an unparseable cursor file reads as no cursor rather than throwing", () => {
|
|
68
|
+
const dir = tmp();
|
|
69
|
+
writeCursor(dir, "acct-1", "a@example.com", {
|
|
70
|
+
newestReceivedAt: "2026-08-13T09:00:00Z",
|
|
71
|
+
idsAtNewest: [],
|
|
72
|
+
});
|
|
73
|
+
writeFileSync(pathFor(dir, "acct-1"), "{not json");
|
|
74
|
+
assert.equal(readCursor(dir, "acct-1", "a@example.com"), null);
|
|
75
|
+
});
|
|
76
|
+
test("an entry with no instant reads as no cursor", () => {
|
|
77
|
+
const dir = tmp();
|
|
78
|
+
writeCursor(dir, "acct-1", "a@example.com", {
|
|
79
|
+
newestReceivedAt: "2026-08-13T09:00:00Z",
|
|
80
|
+
idsAtNewest: [],
|
|
81
|
+
});
|
|
82
|
+
writeFileSync(pathFor(dir, "acct-1"), JSON.stringify({ "a@example.com": { idsAtNewest: [] } }));
|
|
83
|
+
assert.equal(readCursor(dir, "acct-1", "a@example.com"), null);
|
|
84
|
+
});
|
|
85
|
+
test("an entry with no id list reads as an empty exclusion set", () => {
|
|
86
|
+
const dir = tmp();
|
|
87
|
+
writeCursor(dir, "acct-1", "a@example.com", {
|
|
88
|
+
newestReceivedAt: "2026-08-13T09:00:00Z",
|
|
89
|
+
idsAtNewest: [],
|
|
90
|
+
});
|
|
91
|
+
writeFileSync(pathFor(dir, "acct-1"), JSON.stringify({ "a@example.com": { newestReceivedAt: "2026-08-13T09:00:00Z" } }));
|
|
92
|
+
assert.deepEqual(readCursor(dir, "acct-1", "a@example.com")?.idsAtNewest, []);
|
|
93
|
+
});
|
|
94
|
+
test("an inherited property name is not read as a stored cursor", () => {
|
|
95
|
+
const dir = tmp();
|
|
96
|
+
writeCursor(dir, "acct-1", "a@example.com", {
|
|
97
|
+
newestReceivedAt: "2026-08-13T09:00:00Z",
|
|
98
|
+
idsAtNewest: [],
|
|
99
|
+
});
|
|
100
|
+
assert.equal(readCursor(dir, "acct-1", "constructor"), null);
|
|
101
|
+
});
|
|
102
|
+
//# sourceMappingURL=sampler-cursor.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sampler-cursor.test.js","sourceRoot":"","sources":["../../src/__tests__/sampler-cursor.test.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAE/D,SAAS,GAAG;IACV,OAAO,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,iBAAiB,CAAC,CAAC,CAAC;AACxD,CAAC;AAED,SAAS,OAAO,CAAC,GAAW,EAAE,SAAiB;IAC7C,OAAO,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,sBAAsB,CAAC,CAAC;AAC5E,CAAC;AAED,IAAI,CAAC,qCAAqC,EAAE,GAAG,EAAE;IAC/C,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,eAAe,CAAC,EAAE,IAAI,CAAC,CAAC;AACnE,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,mDAAmD,EAAE,GAAG,EAAE;IAC7D,MAAM,GAAG,GAAG,GAAG,EAAE,CAAC;IAClB,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,eAAe,EAAE;QAC1C,gBAAgB,EAAE,sBAAsB;QACxC,WAAW,EAAE,CAAC,MAAM,CAAC;KACtB,CAAC,CAAC;IACH,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,GAAG,EAAE,QAAQ,EAAE,eAAe,CAAC,EAAE;QAC3D,gBAAgB,EAAE,sBAAsB;QACxC,WAAW,EAAE,CAAC,MAAM,CAAC;KACtB,CAAC,CAAC;IACH,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,EAAE,QAAQ,EAAE,eAAe,CAAC,EAAE,IAAI,CAAC,CAAC;AACjE,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,8CAA8C,EAAE,GAAG,EAAE;IACxD,MAAM,GAAG,GAAG,GAAG,EAAE,CAAC;IAClB,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,eAAe,EAAE;QAC1C,gBAAgB,EAAE,sBAAsB;QACxC,WAAW,EAAE,CAAC,MAAM,CAAC;KACtB,CAAC,CAAC;IACH,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,eAAe,EAAE;QAC1C,gBAAgB,EAAE,sBAAsB;QACxC,WAAW,EAAE,CAAC,MAAM,CAAC;KACtB,CAAC,CAAC;IACH,MAAM,CAAC,KAAK,CACV,UAAU,CAAC,GAAG,EAAE,QAAQ,EAAE,eAAe,CAAC,EAAE,gBAAgB,EAC5D,sBAAsB,CACvB,CAAC;IACF,MAAM,CAAC,KAAK,CACV,UAAU,CAAC,GAAG,EAAE,QAAQ,EAAE,eAAe,CAAC,EAAE,gBAAgB,EAC5D,sBAAsB,CACvB,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,gDAAgD,EAAE,GAAG,EAAE;IAC1D,MAAM,GAAG,GAAG,GAAG,EAAE,CAAC;IAClB,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,eAAe,EAAE;QAC1C,gBAAgB,EAAE,sBAAsB;QACxC,WAAW,EAAE,EAAE;KAChB,CAAC,CAAC;IACH,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,EAAE,QAAQ,EAAE,eAAe,CAAC,EAAE,IAAI,CAAC,CAAC;AACjE,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,qDAAqD,EAAE,GAAG,EAAE;IAC/D,MAAM,GAAG,GAAG,GAAG,EAAE,CAAC;IAClB,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,iBAAiB,EAAE;QAC5C,gBAAgB,EAAE,sBAAsB;QACxC,WAAW,EAAE,EAAE;KAChB,CAAC,CAAC;IACH,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,QAAQ,EAAE,eAAe,CAAC,CAAC,CAAC;AACxD,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,2DAA2D,EAAE,GAAG,EAAE;IACrE,MAAM,GAAG,GAAG,GAAG,EAAE,CAAC;IAClB,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,eAAe,EAAE;QAC1C,gBAAgB,EAAE,sBAAsB;QACxC,WAAW,EAAE,EAAE;KAChB,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IACpC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,GAAG,KAAK,EAAE,KAAK,CAAC,CAAC;IACjD,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC,CAAC;AACrF,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,oEAAoE,EAAE,GAAG,EAAE;IAC9E,MAAM,GAAG,GAAG,GAAG,EAAE,CAAC;IAClB,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,eAAe,EAAE;QAC1C,gBAAgB,EAAE,sBAAsB;QACxC,WAAW,EAAE,EAAE;KAChB,CAAC,CAAC;IACH,aAAa,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,EAAE,WAAW,CAAC,CAAC;IACnD,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,EAAE,QAAQ,EAAE,eAAe,CAAC,EAAE,IAAI,CAAC,CAAC;AACjE,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,6CAA6C,EAAE,GAAG,EAAE;IACvD,MAAM,GAAG,GAAG,GAAG,EAAE,CAAC;IAClB,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,eAAe,EAAE;QAC1C,gBAAgB,EAAE,sBAAsB;QACxC,WAAW,EAAE,EAAE;KAChB,CAAC,CAAC;IACH,aAAa,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,eAAe,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;IAChG,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,EAAE,QAAQ,EAAE,eAAe,CAAC,EAAE,IAAI,CAAC,CAAC;AACjE,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,0DAA0D,EAAE,GAAG,EAAE;IACpE,MAAM,GAAG,GAAG,GAAG,EAAE,CAAC;IAClB,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,eAAe,EAAE;QAC1C,gBAAgB,EAAE,sBAAsB;QACxC,WAAW,EAAE,EAAE;KAChB,CAAC,CAAC;IACH,aAAa,CACX,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,EACtB,IAAI,CAAC,SAAS,CAAC,EAAE,eAAe,EAAE,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,EAAE,CAAC,CAClF,CAAC;IACF,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,GAAG,EAAE,QAAQ,EAAE,eAAe,CAAC,EAAE,WAAW,EAAE,EAAE,CAAC,CAAC;AAChF,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,2DAA2D,EAAE,GAAG,EAAE;IACrE,MAAM,GAAG,GAAG,GAAG,EAAE,CAAC;IAClB,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,eAAe,EAAE;QAC1C,gBAAgB,EAAE,sBAAsB;QACxC,WAAW,EAAE,EAAE;KAChB,CAAC,CAAC;IACH,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,EAAE,QAAQ,EAAE,aAAa,CAAC,EAAE,IAAI,CAAC,CAAC;AAC/D,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sampler-enumerate.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/sampler-enumerate.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { TokenStore } from "../auth/token-store.js";
|
|
7
|
+
import { DelegatedStore } from "../auth/delegated-store.js";
|
|
8
|
+
import { censusLine, enumerateSampleableMailboxes } from "../sampler/enumerate.js";
|
|
9
|
+
const ACCT = "11111111-2222-3333-4444-555555555555";
|
|
10
|
+
const ACCT_B = "99999999-8888-7777-6666-555555555555";
|
|
11
|
+
function tmp() {
|
|
12
|
+
return mkdtempSync(join(tmpdir(), "outlook-enum-"));
|
|
13
|
+
}
|
|
14
|
+
/** A provisioned account is a UUID directory holding a parseable account.json. */
|
|
15
|
+
function provision(dir, accountId) {
|
|
16
|
+
mkdirSync(join(dir, accountId), { recursive: true });
|
|
17
|
+
writeFileSync(join(dir, accountId, "account.json"), JSON.stringify({ accountId }));
|
|
18
|
+
}
|
|
19
|
+
function connect(dir, accountId, graphUserId, mail) {
|
|
20
|
+
new TokenStore(accountId, dir, graphUserId).store("access", "refresh", 3600, {
|
|
21
|
+
graphUserId,
|
|
22
|
+
mail,
|
|
23
|
+
scopes: ["Mail.Read"],
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
test("an absent accounts dir enumerates nothing", () => {
|
|
27
|
+
const dir = join(tmp(), "absent");
|
|
28
|
+
assert.deepEqual(enumerateSampleableMailboxes(dir), { sampleable: [], skipped: [], accounts: 0 });
|
|
29
|
+
});
|
|
30
|
+
test("a signed-in mailbox is sampleable with the /me prefix", () => {
|
|
31
|
+
const dir = tmp();
|
|
32
|
+
provision(dir, ACCT);
|
|
33
|
+
connect(dir, ACCT, "guid-1", "a@example.com");
|
|
34
|
+
const { sampleable, skipped } = enumerateSampleableMailboxes(dir);
|
|
35
|
+
assert.deepEqual(skipped, []);
|
|
36
|
+
assert.deepEqual(sampleable, [
|
|
37
|
+
{
|
|
38
|
+
accountId: ACCT,
|
|
39
|
+
mailbox: "a@example.com",
|
|
40
|
+
mailboxKey: "guid-1",
|
|
41
|
+
pathPrefix: "/me",
|
|
42
|
+
via: null,
|
|
43
|
+
},
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
test("a directory with no account.json is not a provisioned account", () => {
|
|
47
|
+
const dir = tmp();
|
|
48
|
+
connect(dir, ACCT, "guid-1", "a@example.com");
|
|
49
|
+
assert.deepEqual(enumerateSampleableMailboxes(dir).sampleable, []);
|
|
50
|
+
});
|
|
51
|
+
test("a non-UUID directory name is not a provisioned account", () => {
|
|
52
|
+
const dir = tmp();
|
|
53
|
+
provision(dir, "not-a-uuid");
|
|
54
|
+
connect(dir, "not-a-uuid", "guid-1", "a@example.com");
|
|
55
|
+
assert.deepEqual(enumerateSampleableMailboxes(dir).sampleable, []);
|
|
56
|
+
});
|
|
57
|
+
test("every provisioned account is enumerated", () => {
|
|
58
|
+
const dir = tmp();
|
|
59
|
+
provision(dir, ACCT);
|
|
60
|
+
provision(dir, ACCT_B);
|
|
61
|
+
connect(dir, ACCT, "guid-1", "a@example.com");
|
|
62
|
+
connect(dir, ACCT_B, "guid-2", "b@example.com");
|
|
63
|
+
const boxes = enumerateSampleableMailboxes(dir).sampleable.map((m) => m.mailbox).sort();
|
|
64
|
+
assert.deepEqual(boxes, ["a@example.com", "b@example.com"]);
|
|
65
|
+
});
|
|
66
|
+
test("a delegated mailbox is sampleable through its delegator's prefix", () => {
|
|
67
|
+
const dir = tmp();
|
|
68
|
+
provision(dir, ACCT);
|
|
69
|
+
connect(dir, ACCT, "guid-1", "a@example.com");
|
|
70
|
+
new DelegatedStore(ACCT, dir).add("shared@example.com", "guid-1", 1);
|
|
71
|
+
const { sampleable } = enumerateSampleableMailboxes(dir);
|
|
72
|
+
const shared = sampleable.find((m) => m.mailbox === "shared@example.com");
|
|
73
|
+
assert.deepEqual(shared, {
|
|
74
|
+
accountId: ACCT,
|
|
75
|
+
mailbox: "shared@example.com",
|
|
76
|
+
mailboxKey: "guid-1",
|
|
77
|
+
pathPrefix: `/users/${encodeURIComponent("shared@example.com")}`,
|
|
78
|
+
via: "a@example.com",
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
test("a delegated record whose delegator is gone is skipped with a reason", () => {
|
|
82
|
+
const dir = tmp();
|
|
83
|
+
provision(dir, ACCT);
|
|
84
|
+
new DelegatedStore(ACCT, dir).add("shared@example.com", "guid-missing", 1);
|
|
85
|
+
const { sampleable, skipped } = enumerateSampleableMailboxes(dir);
|
|
86
|
+
assert.deepEqual(sampleable, []);
|
|
87
|
+
assert.equal(skipped.length, 1);
|
|
88
|
+
assert.equal(skipped[0].mailbox, "shared@example.com");
|
|
89
|
+
assert.match(skipped[0].reason, /delegator-missing/);
|
|
90
|
+
});
|
|
91
|
+
test("a mailbox with a blank email is skipped, never sampled", () => {
|
|
92
|
+
const dir = tmp();
|
|
93
|
+
provision(dir, ACCT);
|
|
94
|
+
connect(dir, ACCT, "guid-2", null);
|
|
95
|
+
const { sampleable, skipped } = enumerateSampleableMailboxes(dir);
|
|
96
|
+
assert.deepEqual(sampleable, []);
|
|
97
|
+
assert.equal(skipped.length, 1);
|
|
98
|
+
assert.match(skipped[0].reason, /blank-email/);
|
|
99
|
+
});
|
|
100
|
+
test("an account with no outlook mailboxes contributes nothing", () => {
|
|
101
|
+
const dir = tmp();
|
|
102
|
+
provision(dir, ACCT);
|
|
103
|
+
// The account IS walked; it simply holds no mailbox. `accounts: 1` is what
|
|
104
|
+
// separates that from an accountsDir that resolved nowhere.
|
|
105
|
+
assert.deepEqual(enumerateSampleableMailboxes(dir), { sampleable: [], skipped: [], accounts: 1 });
|
|
106
|
+
});
|
|
107
|
+
test("the census is emitted with zeros when nothing is connected", () => {
|
|
108
|
+
const dir = tmp();
|
|
109
|
+
provision(dir, ACCT);
|
|
110
|
+
// The guarantee: an install with no Outlook mailbox still says so. Silence
|
|
111
|
+
// here would be indistinguishable from the arm never running.
|
|
112
|
+
assert.equal(censusLine(enumerateSampleableMailboxes(dir)), "op=sample-census provider=outlook accounts=1 mailboxes=0 delegated=0 skipped=0");
|
|
113
|
+
});
|
|
114
|
+
test("the census counts an absent accounts dir as zero accounts", () => {
|
|
115
|
+
assert.equal(censusLine(enumerateSampleableMailboxes(join(tmp(), "absent"))), "op=sample-census provider=outlook accounts=0 mailboxes=0 delegated=0 skipped=0");
|
|
116
|
+
});
|
|
117
|
+
test("the census separates delegated mailboxes from signed-in ones", () => {
|
|
118
|
+
const dir = tmp();
|
|
119
|
+
provision(dir, ACCT);
|
|
120
|
+
connect(dir, ACCT, "guid-1", "a@example.com");
|
|
121
|
+
new DelegatedStore(ACCT, dir).add("shared@example.com", "guid-1", 1);
|
|
122
|
+
new DelegatedStore(ACCT, dir).add("orphan@example.com", "guid-missing", 1);
|
|
123
|
+
assert.equal(censusLine(enumerateSampleableMailboxes(dir)), "op=sample-census provider=outlook accounts=1 mailboxes=2 delegated=1 skipped=1");
|
|
124
|
+
});
|
|
125
|
+
//# sourceMappingURL=sampler-enumerate.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sampler-enumerate.test.js","sourceRoot":"","sources":["../../src/__tests__/sampler-enumerate.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"arm.d.ts","sourceRoot":"","sources":["../../src/sampler/arm.ts"],"names":[],"mappings":"AA8DA,wBAAsB,aAAa,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAmE9E"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Outlook arm of the email sampler (Task 2683).
|
|
3
|
+
*
|
|
4
|
+
* One exported function, called once per sampler cycle by the email plugin's
|
|
5
|
+
* `sampler/main.ts`. It lives here rather than in the email plugin because every
|
|
6
|
+
* reuse target — the mailbox registry, the token store, the Graph client, the
|
|
7
|
+
* list and body tools, the archive dispatcher — is a sibling of this file. The
|
|
8
|
+
* email sampler reaches it by resolving this module's compiled path against
|
|
9
|
+
* PLATFORM_ROOT, the same way `scheduling`'s `check-due-events` reaches
|
|
10
|
+
* `plugins/outlook/mcp/dist/scripts/draft-reconcile.js`.
|
|
11
|
+
*
|
|
12
|
+
* Both arms write to one log, because Task 2683's verification names
|
|
13
|
+
* `logs-read.sh --tail email-sampler` filtered on `provider=outlook`.
|
|
14
|
+
*/
|
|
15
|
+
import { resolve } from "node:path";
|
|
16
|
+
import { MailboxRegistry } from "../auth/mailbox-registry.js";
|
|
17
|
+
import { dispatchOutlookMessagesToConversationArchive } from "../lib/conversation-archive-dispatch.js";
|
|
18
|
+
import { runMailFetchBody } from "../tools/mail-fetch-body.js";
|
|
19
|
+
import { runMailList } from "../tools/mail-list.js";
|
|
20
|
+
import { readCursor, writeCursor } from "./cursor.js";
|
|
21
|
+
import { censusLine, enumerateSampleableMailboxes, } from "./enumerate.js";
|
|
22
|
+
import { sampleOutlookMailbox } from "./sample-outlook-mailbox.js";
|
|
23
|
+
/** One page per list call; the sampler pages to exhaustion. 100 keeps the common
|
|
24
|
+
* case to a single call and stays well inside mail-list's 250 clamp. */
|
|
25
|
+
const PAGE_SIZE = 100;
|
|
26
|
+
/** Same derivation the MCP server's index.ts uses, so the arm and the tools
|
|
27
|
+
* address one accounts directory. */
|
|
28
|
+
function accountsDirFrom(platformRoot) {
|
|
29
|
+
return resolve(platformRoot, "..", "data/accounts");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A Graph config addressing exactly one mailbox. The tokenStore is always a
|
|
33
|
+
* signed-in mailbox's; a delegated mailbox has no sign-in of its own and is
|
|
34
|
+
* reached by `mailboxPathPrefix`, which `callGraph` substitutes for the `/me`
|
|
35
|
+
* segment the tools request.
|
|
36
|
+
*/
|
|
37
|
+
function configFor(target, accountsDir, clientId, tenantId) {
|
|
38
|
+
const registry = new MailboxRegistry(target.accountId, accountsDir);
|
|
39
|
+
return {
|
|
40
|
+
clientId,
|
|
41
|
+
tenantId,
|
|
42
|
+
accountId: target.accountId,
|
|
43
|
+
tokenStore: registry.storeFor(target.mailboxKey),
|
|
44
|
+
mailboxAddress: target.mailbox,
|
|
45
|
+
mailboxPathPrefix: target.pathPrefix,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export async function runOutlookArm(log) {
|
|
49
|
+
const clientId = process.env.OUTLOOK_CLIENT_ID ?? "";
|
|
50
|
+
const tenantId = process.env.OUTLOOK_TENANT_ID ?? "common";
|
|
51
|
+
if (!clientId) {
|
|
52
|
+
// A brand with no Azure app cannot mint or refresh a token. Saying so once
|
|
53
|
+
// per cycle beats one auth error per mailbox per cycle for ever.
|
|
54
|
+
log("op=sample-skip provider=outlook detail=no-client-id");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const platformRoot = process.env.PLATFORM_ROOT;
|
|
58
|
+
if (!platformRoot) {
|
|
59
|
+
log("op=sample-error mailbox=<enumeration> provider=outlook detail=PLATFORM_ROOT-unset");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const accountsDir = accountsDirFrom(platformRoot);
|
|
63
|
+
const enumerated = enumerateSampleableMailboxes(accountsDir);
|
|
64
|
+
const { sampleable, skipped } = enumerated;
|
|
65
|
+
// One census per cycle, before the per-mailbox lines and emitted even when
|
|
66
|
+
// every count is zero. Without it an install with no Outlook mailbox produces
|
|
67
|
+
// no output at all, which is indistinguishable from the arm never running,
|
|
68
|
+
// and those two have different remedies.
|
|
69
|
+
log(censusLine(enumerated));
|
|
70
|
+
for (const s of skipped) {
|
|
71
|
+
log(`op=sample-error mailbox=${s.mailbox} provider=outlook detail=${s.reason}`);
|
|
72
|
+
}
|
|
73
|
+
for (const target of sampleable) {
|
|
74
|
+
const config = configFor(target, accountsDir, clientId, tenantId);
|
|
75
|
+
const deps = {
|
|
76
|
+
list: (_t, since, cursor) => runMailList(config, { folder: "Inbox", top: PAGE_SIZE, since, cursor }),
|
|
77
|
+
fetchBody: async (_t, messageId) => {
|
|
78
|
+
const b = await runMailFetchBody(config, { messageId });
|
|
79
|
+
return {
|
|
80
|
+
id: b.id,
|
|
81
|
+
subject: b.subject,
|
|
82
|
+
from: b.from,
|
|
83
|
+
fromName: b.fromName,
|
|
84
|
+
to: b.to,
|
|
85
|
+
cc: b.cc,
|
|
86
|
+
conversationId: b.conversationId,
|
|
87
|
+
body: b.body,
|
|
88
|
+
receivedDateTime: b.receivedDateTime,
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
write: (m) => dispatchOutlookMessagesToConversationArchive(m),
|
|
92
|
+
readCursor: (t) => readCursor(accountsDir, t.accountId, t.mailbox),
|
|
93
|
+
writeCursor: (t, c) => writeCursor(accountsDir, t.accountId, t.mailbox, c),
|
|
94
|
+
log,
|
|
95
|
+
now: () => Date.now(),
|
|
96
|
+
};
|
|
97
|
+
try {
|
|
98
|
+
await sampleOutlookMailbox(deps, target);
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
// sampleOutlookMailbox handles its own fetch and write failures; this
|
|
102
|
+
// catches anything thrown outside them (a cursor write, a token-store
|
|
103
|
+
// read) so one mailbox never ends the cycle for the rest.
|
|
104
|
+
log(`op=sample-error mailbox=${target.mailbox} provider=outlook ` +
|
|
105
|
+
`detail=${e instanceof Error ? e.message : String(e)}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=arm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"arm.js","sourceRoot":"","sources":["../../src/sampler/arm.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface OutlookSamplerCursor {
|
|
2
|
+
/** Raw `receivedDateTime` of the newest message written, as Graph returned it.
|
|
3
|
+
* Raw so nothing here can shift an instant by reformatting it. */
|
|
4
|
+
newestReceivedAt: string;
|
|
5
|
+
/** Ids already written that carry exactly `newestReceivedAt`. */
|
|
6
|
+
idsAtNewest: string[];
|
|
7
|
+
}
|
|
8
|
+
export type OutlookSamplerCursors = Record<string, OutlookSamplerCursor>;
|
|
9
|
+
/**
|
|
10
|
+
* Every cursor for one account. A missing or unparseable file reads as an empty
|
|
11
|
+
* map rather than throwing: the sampler runs unattended, so a corrupt file must
|
|
12
|
+
* degrade to a re-backfill of that account, never stop the cycle.
|
|
13
|
+
*/
|
|
14
|
+
export declare function readCursors(accountsDir: string, accountId: string): OutlookSamplerCursors;
|
|
15
|
+
/** One mailbox's cursor, or null when it has never been sampled. An entry whose
|
|
16
|
+
* instant is not a string is treated as absent — a half-written record must
|
|
17
|
+
* read as "start again", never as a position. */
|
|
18
|
+
export declare function readCursor(accountsDir: string, accountId: string, mailbox: string): OutlookSamplerCursor | null;
|
|
19
|
+
/**
|
|
20
|
+
* Replace one mailbox's cursor, preserving every other mailbox's on the account.
|
|
21
|
+
* Atomic via temp + rename, so a crash mid-write leaves the previous file whole;
|
|
22
|
+
* a torn file would re-backfill every mailbox on the account.
|
|
23
|
+
*/
|
|
24
|
+
export declare function writeCursor(accountsDir: string, accountId: string, mailbox: string, cursor: OutlookSamplerCursor): void;
|
|
25
|
+
//# sourceMappingURL=cursor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor.d.ts","sourceRoot":"","sources":["../../src/sampler/cursor.ts"],"names":[],"mappings":"AA6BA,MAAM,WAAW,oBAAoB;IACnC;uEACmE;IACnE,gBAAgB,EAAE,MAAM,CAAC;IACzB,iEAAiE;IACjE,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,MAAM,qBAAqB,GAAG,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;AAezE;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,qBAAqB,CAUzF;AAED;;kDAEkD;AAClD,wBAAgB,UAAU,CACxB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,oBAAoB,GAAG,IAAI,CAW7B;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,oBAAoB,GAC3B,IAAI,CAqBN"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Outlook sampler's own cursor store (Task 2683).
|
|
3
|
+
*
|
|
4
|
+
* Outlook mailboxes have no `:EmailAccount` node, so there is no graph property
|
|
5
|
+
* to hold a cursor the way the IMAP arm holds `samplerCursorUid`. One JSON file
|
|
6
|
+
* per account, keyed by the mailbox's SMTP address, covers signed-in and
|
|
7
|
+
* delegated mailboxes with one shape. It holds no secret, which is the same
|
|
8
|
+
* position `delegated.json` already occupies in the same 0700 directory.
|
|
9
|
+
*
|
|
10
|
+
* The cursor is an instant plus the ids seen at that instant. A timestamp alone
|
|
11
|
+
* cannot be both lossless and duplicate-free: `receivedDateTime gt cursor` drops
|
|
12
|
+
* a second message that arrived at the identical instant, and `ge cursor`
|
|
13
|
+
* re-ingests the boundary message on every cycle for ever. Querying `ge` and
|
|
14
|
+
* excluding the ids already written at that exact instant loses nothing and
|
|
15
|
+
* repeats nothing.
|
|
16
|
+
*
|
|
17
|
+
* Written only by the sampler, which is a single process, so no lock is needed.
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
20
|
+
import { randomBytes } from "node:crypto";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
/** The one address form used as a key, matching `delegated-store.ts`. */
|
|
23
|
+
function key(mailbox) {
|
|
24
|
+
return mailbox.trim().toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
function secretsDir(accountsDir, accountId) {
|
|
27
|
+
return join(accountsDir, accountId, "secrets", "outlook");
|
|
28
|
+
}
|
|
29
|
+
function cursorsPath(accountsDir, accountId) {
|
|
30
|
+
return join(secretsDir(accountsDir, accountId), "sampler-cursors.json");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Every cursor for one account. A missing or unparseable file reads as an empty
|
|
34
|
+
* map rather than throwing: the sampler runs unattended, so a corrupt file must
|
|
35
|
+
* degrade to a re-backfill of that account, never stop the cycle.
|
|
36
|
+
*/
|
|
37
|
+
export function readCursors(accountsDir, accountId) {
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(readFileSync(cursorsPath(accountsDir, accountId), "utf-8"));
|
|
40
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
41
|
+
return {};
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** One mailbox's cursor, or null when it has never been sampled. An entry whose
|
|
49
|
+
* instant is not a string is treated as absent — a half-written record must
|
|
50
|
+
* read as "start again", never as a position. */
|
|
51
|
+
export function readCursor(accountsDir, accountId, mailbox) {
|
|
52
|
+
const all = readCursors(accountsDir, accountId);
|
|
53
|
+
const k = key(mailbox);
|
|
54
|
+
// Own-property check, not a bare index: a plain object answers "constructor"
|
|
55
|
+
// with an inherited function, which would read as a stored cursor.
|
|
56
|
+
const entry = Object.hasOwn(all, k) ? all[k] : undefined;
|
|
57
|
+
if (!entry || typeof entry.newestReceivedAt !== "string")
|
|
58
|
+
return null;
|
|
59
|
+
return {
|
|
60
|
+
newestReceivedAt: entry.newestReceivedAt,
|
|
61
|
+
idsAtNewest: Array.isArray(entry.idsAtNewest) ? entry.idsAtNewest : [],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Replace one mailbox's cursor, preserving every other mailbox's on the account.
|
|
66
|
+
* Atomic via temp + rename, so a crash mid-write leaves the previous file whole;
|
|
67
|
+
* a torn file would re-backfill every mailbox on the account.
|
|
68
|
+
*/
|
|
69
|
+
export function writeCursor(accountsDir, accountId, mailbox, cursor) {
|
|
70
|
+
const path = cursorsPath(accountsDir, accountId);
|
|
71
|
+
const next = {
|
|
72
|
+
...readCursors(accountsDir, accountId),
|
|
73
|
+
[key(mailbox)]: cursor,
|
|
74
|
+
};
|
|
75
|
+
mkdirSync(secretsDir(accountsDir, accountId), { recursive: true, mode: 0o700 });
|
|
76
|
+
const temp = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
77
|
+
try {
|
|
78
|
+
writeFileSync(temp, JSON.stringify(next, null, 2), { mode: 0o600 });
|
|
79
|
+
renameSync(temp, path);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
// Unique temp names are never reused, so a failed write or rename would
|
|
83
|
+
// otherwise leave a file nothing ever cleans up.
|
|
84
|
+
try {
|
|
85
|
+
if (existsSync(temp))
|
|
86
|
+
unlinkSync(temp);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
/* best effort — the original error is what matters */
|
|
90
|
+
}
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=cursor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor.js","sourceRoot":"","sources":["../../src/sampler/cursor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface SampleableMailbox {
|
|
2
|
+
accountId: string;
|
|
3
|
+
/** SMTP address. The cursor key and the `mailbox=` log field. */
|
|
4
|
+
mailbox: string;
|
|
5
|
+
/** The TokenStore key whose tokens perform the read. On a delegated mailbox
|
|
6
|
+
* this is the DELEGATOR's key — a shared mailbox never signs in. */
|
|
7
|
+
mailboxKey: string;
|
|
8
|
+
/** "/me" for a signed-in mailbox, "/users/<encoded address>" for a delegated one. */
|
|
9
|
+
pathPrefix: string;
|
|
10
|
+
/** The delegator's address on a delegated mailbox, else null. */
|
|
11
|
+
via: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface SkippedMailbox {
|
|
14
|
+
accountId: string;
|
|
15
|
+
mailbox: string;
|
|
16
|
+
reason: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Account directories that are provisioned: a UUID name holding a parseable
|
|
20
|
+
* `account.json`. A present-but-unparseable file excludes the directory, the
|
|
21
|
+
* same corruption discipline `account-enumeration` applies.
|
|
22
|
+
*/
|
|
23
|
+
export declare function listProvisionedAccountIds(accountsDir: string): string[];
|
|
24
|
+
export interface EnumerationResult {
|
|
25
|
+
sampleable: SampleableMailbox[];
|
|
26
|
+
skipped: SkippedMailbox[];
|
|
27
|
+
/** Provisioned accounts walked. Carried out so the census reports it without
|
|
28
|
+
* a second directory read, and so `accounts=0` is distinguishable from an
|
|
29
|
+
* accountsDir that resolved somewhere that does not exist. */
|
|
30
|
+
accounts: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The per-cycle census line, emitted even when every count is zero.
|
|
34
|
+
*
|
|
35
|
+
* Pure, so the "never silent" guarantee is testable without a directory. An arm
|
|
36
|
+
* that emits nothing on an install with no Outlook mailbox is indistinguishable
|
|
37
|
+
* from an arm that never ran, and those two have different remedies.
|
|
38
|
+
*/
|
|
39
|
+
export declare function censusLine(result: EnumerationResult): string;
|
|
40
|
+
export declare function enumerateSampleableMailboxes(accountsDir: string): EnumerationResult;
|
|
41
|
+
//# sourceMappingURL=enumerate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enumerate.d.ts","sourceRoot":"","sources":["../../src/sampler/enumerate.ts"],"names":[],"mappings":"AAuBA,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;IAChB;yEACqE;IACrE,UAAU,EAAE,MAAM,CAAC;IACnB,qFAAqF;IACrF,UAAU,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAMD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAkBvE;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,iBAAiB,EAAE,CAAC;IAChC,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B;;mEAE+D;IAC/D,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAM5D;AAED,wBAAgB,4BAA4B,CAAC,WAAW,EAAE,MAAM,GAAG,iBAAiB,CA6DnF"}
|