@rubytech/create-maxy 1.0.762 → 1.0.764
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/package.json +1 -1
- package/payload/platform/neo4j/schema.cypher +50 -0
- package/payload/platform/package-lock.json +56 -1
- package/payload/platform/package.json +1 -0
- package/payload/platform/plugins/docs/references/outlook-guide.md +69 -0
- package/payload/platform/plugins/outlook/PLUGIN.md +48 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.js +94 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/log.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/log.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/log.test.js +31 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/log.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.js +213 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/token-store.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/token-store.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/token-store.test.js +130 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/token-store.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/pkce-flow.d.ts +65 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/pkce-flow.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/pkce-flow.js +261 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/pkce-flow.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/token-store.d.ts +61 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/token-store.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/token-store.js +170 -0
- package/payload/platform/plugins/outlook/mcp/dist/auth/token-store.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/index.d.ts +18 -0
- package/payload/platform/plugins/outlook/mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/index.js +152 -0
- package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts +60 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js +189 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/log.d.ts +23 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/log.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/log.js +53 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/log.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.d.ts +26 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.js +50 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-event.d.ts +12 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-event.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-event.js +32 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-event.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-list.d.ts +59 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-list.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-list.js +54 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-list.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/contacts-list.d.ts +14 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/contacts-list.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/contacts-list.js +45 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/contacts-list.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-list.d.ts +15 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-list.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-list.js +48 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-list.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.d.ts +8 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.js +49 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-info.d.ts +19 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-info.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-info.js +58 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-info.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/package.json +20 -0
- package/payload/platform/plugins/outlook/mcp/scripts/verify-doc-impl.sh +109 -0
- package/payload/platform/plugins/outlook/references/auth.md +118 -0
- package/payload/platform/plugins/outlook/references/graph-surfaces.md +114 -0
- package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +65 -0
- package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
- package/payload/server/chunk-EIQT6QDH.js +9562 -0
- package/payload/server/chunk-S3M2NZMA.js +3136 -0
- package/payload/server/chunk-SGBNY4NP.js +9540 -0
- package/payload/server/client-pool-5V5GX3UT.js +28 -0
- package/payload/server/maxy-edge.js +2 -2
- package/payload/server/public/assets/{admin-7vGwd7wu.js → admin-V6NDkEoR.js} +2 -2
- package/payload/server/public/index.html +1 -1
- package/payload/server/server.js +104 -6
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { _internals, startPkceFlow } from "../auth/pkce-flow.js";
|
|
4
|
+
const { sameLengthEqual } = _internals;
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// sameLengthEqual: timing-safe state comparison
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
test("sameLengthEqual matches identical strings", () => {
|
|
9
|
+
assert.equal(sameLengthEqual("abcdef0123456789", "abcdef0123456789"), true);
|
|
10
|
+
});
|
|
11
|
+
test("sameLengthEqual rejects different equal-length strings", () => {
|
|
12
|
+
assert.equal(sameLengthEqual("abcdef0123456789", "abcdef0123456780"), false);
|
|
13
|
+
});
|
|
14
|
+
test("sameLengthEqual rejects length mismatch without throwing", () => {
|
|
15
|
+
assert.equal(sameLengthEqual("short", "longerstring"), false);
|
|
16
|
+
});
|
|
17
|
+
function mockFetch(handler) {
|
|
18
|
+
const calls = [];
|
|
19
|
+
const original = globalThis.fetch;
|
|
20
|
+
globalThis.fetch = (async (url, init) => {
|
|
21
|
+
const urlStr = typeof url === "string" ? url : url instanceof URL ? url.toString() : url.url;
|
|
22
|
+
// Pass loopback callbacks through to the real fetch — only the upstream
|
|
23
|
+
// token endpoint is intercepted. Otherwise the test's own callback
|
|
24
|
+
// request would loop back into the mock and never reach the bound server.
|
|
25
|
+
if (urlStr.startsWith("http://127.0.0.1:") || urlStr.startsWith("http://localhost:")) {
|
|
26
|
+
return original(url, init);
|
|
27
|
+
}
|
|
28
|
+
calls.push({ url: urlStr, init });
|
|
29
|
+
return handler(urlStr, init);
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
calls,
|
|
33
|
+
restore: () => {
|
|
34
|
+
globalThis.fetch = original;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function tokenSuccess() {
|
|
39
|
+
return new Response(JSON.stringify({
|
|
40
|
+
access_token: "test-access",
|
|
41
|
+
refresh_token: "test-refresh",
|
|
42
|
+
expires_in: 3600,
|
|
43
|
+
token_type: "Bearer",
|
|
44
|
+
scope: "offline_access User.Read Mail.Read Calendars.Read Contacts.Read",
|
|
45
|
+
}), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
46
|
+
}
|
|
47
|
+
function extractStateFromAuthUrl(authUrl) {
|
|
48
|
+
const url = new URL(authUrl);
|
|
49
|
+
const state = url.searchParams.get("state");
|
|
50
|
+
assert.ok(state, "auth URL must include state param");
|
|
51
|
+
return state;
|
|
52
|
+
}
|
|
53
|
+
function extractRedirectUriPort(redirectUri) {
|
|
54
|
+
const url = new URL(redirectUri);
|
|
55
|
+
return Number(url.port);
|
|
56
|
+
}
|
|
57
|
+
test("PKCE happy path: state matches → tokens exchanged", async () => {
|
|
58
|
+
const fetchMock = mockFetch(() => tokenSuccess());
|
|
59
|
+
try {
|
|
60
|
+
const flow = await startPkceFlow({
|
|
61
|
+
clientId: "test-client",
|
|
62
|
+
tenantId: "common",
|
|
63
|
+
accountId: "acct-pkce-1",
|
|
64
|
+
});
|
|
65
|
+
const state = extractStateFromAuthUrl(flow.authUrl);
|
|
66
|
+
const port = extractRedirectUriPort(flow.redirectUri);
|
|
67
|
+
// Drive the callback synthetically, as Microsoft would after consent.
|
|
68
|
+
const callbackResp = await fetch(`http://127.0.0.1:${port}/callback?code=fake-code&state=${state}`);
|
|
69
|
+
assert.equal(callbackResp.status, 200);
|
|
70
|
+
const result = await flow.result;
|
|
71
|
+
assert.equal(result.tokenResponse.access_token, "test-access");
|
|
72
|
+
assert.equal(result.tokenResponse.refresh_token, "test-refresh");
|
|
73
|
+
assert.deepEqual(result.scopes.sort(), [
|
|
74
|
+
"Calendars.Read",
|
|
75
|
+
"Contacts.Read",
|
|
76
|
+
"Mail.Read",
|
|
77
|
+
"User.Read",
|
|
78
|
+
"offline_access",
|
|
79
|
+
].sort());
|
|
80
|
+
// Token endpoint was called with code + verifier + redirect_uri.
|
|
81
|
+
assert.equal(fetchMock.calls.length, 1);
|
|
82
|
+
const body = String(fetchMock.calls[0].init?.body ?? "");
|
|
83
|
+
assert.match(body, /code=fake-code/);
|
|
84
|
+
assert.match(body, /code_verifier=/);
|
|
85
|
+
assert.match(body, /grant_type=authorization_code/);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
fetchMock.restore();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
test("PKCE state mismatch → flow rejects, token endpoint NOT called", async () => {
|
|
92
|
+
const fetchMock = mockFetch(() => {
|
|
93
|
+
throw new Error("token endpoint should not be called on state mismatch");
|
|
94
|
+
});
|
|
95
|
+
try {
|
|
96
|
+
const flow = await startPkceFlow({
|
|
97
|
+
clientId: "test-client",
|
|
98
|
+
tenantId: "common",
|
|
99
|
+
accountId: "acct-pkce-2",
|
|
100
|
+
});
|
|
101
|
+
// Subscribe BEFORE driving the callback — otherwise the rejection lands
|
|
102
|
+
// before any handler is attached and Node flags it as unhandled.
|
|
103
|
+
const rejection = assert.rejects(flow.result, /State mismatch/i);
|
|
104
|
+
const port = extractRedirectUriPort(flow.redirectUri);
|
|
105
|
+
const cbResp = await fetch(`http://127.0.0.1:${port}/callback?code=fake-code&state=wrong-state-value`);
|
|
106
|
+
assert.equal(cbResp.status, 400);
|
|
107
|
+
await rejection;
|
|
108
|
+
assert.equal(fetchMock.calls.length, 0, "token endpoint must not be called");
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
fetchMock.restore();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
test("PKCE OAuth error in callback → flow rejects, no token call", async () => {
|
|
115
|
+
const fetchMock = mockFetch(() => {
|
|
116
|
+
throw new Error("token endpoint should not be called on OAuth error");
|
|
117
|
+
});
|
|
118
|
+
try {
|
|
119
|
+
const flow = await startPkceFlow({
|
|
120
|
+
clientId: "test-client",
|
|
121
|
+
tenantId: "common",
|
|
122
|
+
accountId: "acct-pkce-3",
|
|
123
|
+
});
|
|
124
|
+
const rejection = assert.rejects(flow.result, /OAuth error: access_denied/);
|
|
125
|
+
const state = extractStateFromAuthUrl(flow.authUrl);
|
|
126
|
+
const port = extractRedirectUriPort(flow.redirectUri);
|
|
127
|
+
const cbResp = await fetch(`http://127.0.0.1:${port}/callback?error=access_denied&error_description=user%20cancelled&state=${state}`);
|
|
128
|
+
assert.equal(cbResp.status, 400);
|
|
129
|
+
await rejection;
|
|
130
|
+
assert.equal(fetchMock.calls.length, 0);
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
fetchMock.restore();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
test("PKCE callback missing code → flow rejects", async () => {
|
|
137
|
+
const fetchMock = mockFetch(() => {
|
|
138
|
+
throw new Error("token endpoint should not be called");
|
|
139
|
+
});
|
|
140
|
+
try {
|
|
141
|
+
const flow = await startPkceFlow({
|
|
142
|
+
clientId: "test-client",
|
|
143
|
+
tenantId: "common",
|
|
144
|
+
accountId: "acct-pkce-4",
|
|
145
|
+
});
|
|
146
|
+
const rejection = assert.rejects(flow.result, /No authorization code received/);
|
|
147
|
+
const state = extractStateFromAuthUrl(flow.authUrl);
|
|
148
|
+
const port = extractRedirectUriPort(flow.redirectUri);
|
|
149
|
+
const cbResp = await fetch(`http://127.0.0.1:${port}/callback?state=${state}`);
|
|
150
|
+
assert.equal(cbResp.status, 400);
|
|
151
|
+
await rejection;
|
|
152
|
+
assert.equal(fetchMock.calls.length, 0);
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
fetchMock.restore();
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
test("PKCE auth URL has S256 challenge + correct scopes + prompt=select_account", async () => {
|
|
159
|
+
// No mock needed — we assert URL structure and abort the flow without
|
|
160
|
+
// exchanging tokens. Sending an OAuth error in the callback closes the
|
|
161
|
+
// server and rejects flow.result loudly; we catch it explicitly.
|
|
162
|
+
const flow = await startPkceFlow({
|
|
163
|
+
clientId: "client-X",
|
|
164
|
+
tenantId: "common",
|
|
165
|
+
accountId: "acct-pkce-5",
|
|
166
|
+
});
|
|
167
|
+
const expectedRejection = assert.rejects(flow.result, /OAuth error/);
|
|
168
|
+
try {
|
|
169
|
+
const url = new URL(flow.authUrl);
|
|
170
|
+
assert.equal(url.searchParams.get("client_id"), "client-X");
|
|
171
|
+
assert.equal(url.searchParams.get("response_type"), "code");
|
|
172
|
+
assert.equal(url.searchParams.get("code_challenge_method"), "S256");
|
|
173
|
+
assert.equal(url.searchParams.get("prompt"), "select_account");
|
|
174
|
+
const challenge = url.searchParams.get("code_challenge");
|
|
175
|
+
assert.ok(challenge && challenge.length > 30, "challenge should be base64url-encoded SHA-256");
|
|
176
|
+
const scope = url.searchParams.get("scope") ?? "";
|
|
177
|
+
assert.match(scope, /offline_access/);
|
|
178
|
+
assert.match(scope, /User\.Read/);
|
|
179
|
+
assert.match(scope, /Mail\.Read/);
|
|
180
|
+
assert.match(scope, /Calendars\.Read/);
|
|
181
|
+
assert.match(scope, /Contacts\.Read/);
|
|
182
|
+
// Abort the flow by sending an OAuth error. Server closes; result rejects.
|
|
183
|
+
const port = extractRedirectUriPort(flow.redirectUri);
|
|
184
|
+
const state = url.searchParams.get("state");
|
|
185
|
+
await fetch(`http://127.0.0.1:${port}/callback?error=access_denied&state=${state}`);
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
await expectedRejection;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
test("PKCE redirect URI uses ephemeral loopback port (not fixed 49152)", async () => {
|
|
192
|
+
// No token-exchange mock — abort each flow with an OAuth error so the
|
|
193
|
+
// server closes without invoking exchangeCode.
|
|
194
|
+
const flow1 = await startPkceFlow({ clientId: "c", tenantId: "common", accountId: "a1" });
|
|
195
|
+
const flow2 = await startPkceFlow({ clientId: "c", tenantId: "common", accountId: "a2" });
|
|
196
|
+
const r1 = assert.rejects(flow1.result, /OAuth error/);
|
|
197
|
+
const r2 = assert.rejects(flow2.result, /OAuth error/);
|
|
198
|
+
try {
|
|
199
|
+
const port1 = extractRedirectUriPort(flow1.redirectUri);
|
|
200
|
+
const port2 = extractRedirectUriPort(flow2.redirectUri);
|
|
201
|
+
assert.ok(port1 > 0 && port1 < 65536);
|
|
202
|
+
assert.ok(port2 > 0 && port2 < 65536);
|
|
203
|
+
assert.notEqual(port1, port2, "two parallel flows must allocate different ephemeral ports");
|
|
204
|
+
const s1 = extractStateFromAuthUrl(flow1.authUrl);
|
|
205
|
+
const s2 = extractStateFromAuthUrl(flow2.authUrl);
|
|
206
|
+
await fetch(`http://127.0.0.1:${port1}/callback?error=access_denied&state=${s1}`);
|
|
207
|
+
await fetch(`http://127.0.0.1:${port2}/callback?error=access_denied&state=${s2}`);
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
await Promise.all([r1, r2]);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
//# sourceMappingURL=pkce-flow.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkce-flow.test.js","sourceRoot":"","sources":["../../src/__tests__/pkce-flow.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-store.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/token-store.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, rmSync, statSync, writeFileSync, readFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { TokenStore } from "../auth/token-store.js";
|
|
7
|
+
function makeAccountsDir() {
|
|
8
|
+
const dir = mkdtempSync(join(tmpdir(), "outlook-test-"));
|
|
9
|
+
return {
|
|
10
|
+
dir,
|
|
11
|
+
cleanup: () => rmSync(dir, { recursive: true, force: true }),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
test("TokenStore round-trip preserves the blob", () => {
|
|
15
|
+
const { dir, cleanup } = makeAccountsDir();
|
|
16
|
+
try {
|
|
17
|
+
const store = new TokenStore("acct-A", dir);
|
|
18
|
+
store.store("access-1", "refresh-1", 3600, {
|
|
19
|
+
graphUserId: "user-1",
|
|
20
|
+
scopes: ["Mail.Read", "Calendars.Read"],
|
|
21
|
+
});
|
|
22
|
+
const fresh = new TokenStore("acct-A", dir);
|
|
23
|
+
const blob = fresh.read();
|
|
24
|
+
assert.ok(blob, "expected blob to exist after round-trip");
|
|
25
|
+
assert.equal(blob.accessToken, "access-1");
|
|
26
|
+
assert.equal(blob.refreshToken, "refresh-1");
|
|
27
|
+
assert.equal(blob.graphUserId, "user-1");
|
|
28
|
+
assert.deepEqual(blob.scopes, ["Mail.Read", "Calendars.Read"]);
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
cleanup();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
test("TokenStore corrupt cipher fails loud", () => {
|
|
35
|
+
const { dir, cleanup } = makeAccountsDir();
|
|
36
|
+
try {
|
|
37
|
+
const store = new TokenStore("acct-X", dir);
|
|
38
|
+
store.store("access-X", "refresh-X", 3600);
|
|
39
|
+
// Tamper with the ciphertext on disk.
|
|
40
|
+
const tokensPath = join(dir, "acct-X", "secrets", "outlook", "tokens.enc");
|
|
41
|
+
writeFileSync(tokensPath, "garbage-not-a-cipher");
|
|
42
|
+
const fresh = new TokenStore("acct-X", dir);
|
|
43
|
+
assert.throws(() => fresh.read(), /token-decrypt-failed|bad decrypt|wrong final block length|invalid|wrong tag/i);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
cleanup();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
test("TokenStore per-account isolation — two stores never decrypt each other", () => {
|
|
50
|
+
const { dir, cleanup } = makeAccountsDir();
|
|
51
|
+
try {
|
|
52
|
+
const storeA = new TokenStore("acct-A", dir);
|
|
53
|
+
const storeB = new TokenStore("acct-B", dir);
|
|
54
|
+
storeA.store("access-A", "refresh-A", 3600, { graphUserId: "user-A", scopes: [] });
|
|
55
|
+
storeB.store("access-B", "refresh-B", 3600, { graphUserId: "user-B", scopes: [] });
|
|
56
|
+
// Read each independently.
|
|
57
|
+
const blobA = new TokenStore("acct-A", dir).read();
|
|
58
|
+
const blobB = new TokenStore("acct-B", dir).read();
|
|
59
|
+
assert.equal(blobA?.accessToken, "access-A");
|
|
60
|
+
assert.equal(blobB?.accessToken, "access-B");
|
|
61
|
+
assert.notEqual(blobA?.accessToken, blobB?.accessToken);
|
|
62
|
+
// Sentinel: swap the .key files between accounts. The store should now
|
|
63
|
+
// fail to decrypt — proves keys are genuinely per-account, not shared.
|
|
64
|
+
const keyA = readFileSync(join(dir, "acct-A", "secrets", "outlook", ".key"), "utf-8");
|
|
65
|
+
const keyB = readFileSync(join(dir, "acct-B", "secrets", "outlook", ".key"), "utf-8");
|
|
66
|
+
assert.notEqual(keyA, keyB, "keys must differ across accounts");
|
|
67
|
+
writeFileSync(join(dir, "acct-A", "secrets", "outlook", ".key"), keyB);
|
|
68
|
+
const tampered = new TokenStore("acct-A", dir);
|
|
69
|
+
assert.throws(() => tampered.read(), /bad decrypt|wrong final|invalid|token-decrypt-failed/i);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
cleanup();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
test("TokenStore directory permissions are 0700, file permissions are 0600", () => {
|
|
76
|
+
const { dir, cleanup } = makeAccountsDir();
|
|
77
|
+
try {
|
|
78
|
+
const store = new TokenStore("acct-mode", dir);
|
|
79
|
+
store.store("a", "r", 3600);
|
|
80
|
+
const secretsDir = join(dir, "acct-mode", "secrets", "outlook");
|
|
81
|
+
const dirMode = statSync(secretsDir).mode & 0o777;
|
|
82
|
+
assert.equal(dirMode, 0o700, "secrets dir must be 0700");
|
|
83
|
+
const keyMode = statSync(join(secretsDir, ".key")).mode & 0o777;
|
|
84
|
+
assert.equal(keyMode, 0o600, "key file must be 0600");
|
|
85
|
+
const tokensMode = statSync(join(secretsDir, "tokens.enc")).mode & 0o777;
|
|
86
|
+
assert.equal(tokensMode, 0o600, "tokens file must be 0600");
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
cleanup();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
test("TokenStore.needsRefresh returns true within the threshold window", () => {
|
|
93
|
+
const { dir, cleanup } = makeAccountsDir();
|
|
94
|
+
try {
|
|
95
|
+
const store = new TokenStore("acct-r", dir);
|
|
96
|
+
store.store("a", "r", 60); // 60s expiry — well under the 300s threshold
|
|
97
|
+
assert.equal(store.needsRefresh(), true);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
cleanup();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
test("TokenStore.needsRefresh returns false outside the threshold window", () => {
|
|
104
|
+
const { dir, cleanup } = makeAccountsDir();
|
|
105
|
+
try {
|
|
106
|
+
const store = new TokenStore("acct-f", dir);
|
|
107
|
+
store.store("a", "r", 3600); // 1h expiry
|
|
108
|
+
assert.equal(store.needsRefresh(), false);
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
cleanup();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
test("TokenStore.clear removes the token blob but leaves the key", () => {
|
|
115
|
+
const { dir, cleanup } = makeAccountsDir();
|
|
116
|
+
try {
|
|
117
|
+
const store = new TokenStore("acct-c", dir);
|
|
118
|
+
store.store("a", "r", 3600);
|
|
119
|
+
store.clear();
|
|
120
|
+
assert.equal(store.read(), null);
|
|
121
|
+
// Key file still present — re-register reuses it.
|
|
122
|
+
const fresh = new TokenStore("acct-c", dir);
|
|
123
|
+
fresh.store("a2", "r2", 3600);
|
|
124
|
+
assert.equal(fresh.read()?.accessToken, "a2");
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
cleanup();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
//# sourceMappingURL=token-store.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-store.test.js","sourceRoot":"","sources":["../../src/__tests__/token-store.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 Authorization Code + PKCE flow for Microsoft Identity Platform.
|
|
3
|
+
*
|
|
4
|
+
* Adapted from nsakki55/outlook-mcp@e49d8e68ac8d69db653c25803127dc3b98626cda
|
|
5
|
+
* (MIT). Adaptations:
|
|
6
|
+
* - Ephemeral loopback port via `server.listen(0)` (upstream uses fixed
|
|
7
|
+
* 49152). The Entra app is registered with `http://localhost` (no port);
|
|
8
|
+
* Microsoft Identity Platform treats native-app loopback redirects with
|
|
9
|
+
* port-flexible matching.
|
|
10
|
+
* - No `xdg-open` browser auto-launch; the auth URL is logged to stderr
|
|
11
|
+
* and returned to the caller for the operator to open in the VNC browser.
|
|
12
|
+
* - Tool handler awaits the full PKCE flow synchronously (5-min timeout)
|
|
13
|
+
* instead of upstream's background-promise + immediate-error pattern.
|
|
14
|
+
*/
|
|
15
|
+
export interface PkceFlowConfig {
|
|
16
|
+
clientId: string;
|
|
17
|
+
tenantId: string;
|
|
18
|
+
accountId: string;
|
|
19
|
+
}
|
|
20
|
+
export interface TokenResponse {
|
|
21
|
+
access_token: string;
|
|
22
|
+
refresh_token?: string;
|
|
23
|
+
expires_in: number;
|
|
24
|
+
token_type: string;
|
|
25
|
+
scope?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface PkceFlowResult {
|
|
28
|
+
tokenResponse: TokenResponse;
|
|
29
|
+
scopes: string[];
|
|
30
|
+
}
|
|
31
|
+
export interface PkceFlowStart {
|
|
32
|
+
authUrl: string;
|
|
33
|
+
redirectUri: string;
|
|
34
|
+
result: Promise<PkceFlowResult>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Start the PKCE flow:
|
|
38
|
+
* 1. Generate code_verifier + code_challenge (S256) + state.
|
|
39
|
+
* 2. Bind a one-shot HTTP server on an ephemeral loopback port.
|
|
40
|
+
* 3. Build the authorize URL with the actual port; return it to the caller.
|
|
41
|
+
* 4. The returned `result` promise resolves when the callback arrives (or
|
|
42
|
+
* rejects on state mismatch / OAuth error / timeout) and tokens are
|
|
43
|
+
* exchanged.
|
|
44
|
+
*
|
|
45
|
+
* The caller (the outlook-account-register tool) prints the auth URL,
|
|
46
|
+
* returns it in the tool response, and awaits `result`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function startPkceFlow(config: PkceFlowConfig): Promise<PkceFlowStart>;
|
|
49
|
+
export declare function refreshAccessToken(args: {
|
|
50
|
+
clientId: string;
|
|
51
|
+
tenantId: string;
|
|
52
|
+
refreshToken: string;
|
|
53
|
+
}): Promise<TokenResponse>;
|
|
54
|
+
/**
|
|
55
|
+
* Constant-time equality on equal-length strings. Returns false (not throws)
|
|
56
|
+
* for length mismatch so the caller can branch without leaking length via
|
|
57
|
+
* timing. State comparison MUST go through this helper, not `!==`.
|
|
58
|
+
*/
|
|
59
|
+
declare function sameLengthEqual(a: string, b: string): boolean;
|
|
60
|
+
export declare const _internals: {
|
|
61
|
+
SCOPES: string[];
|
|
62
|
+
sameLengthEqual: typeof sameLengthEqual;
|
|
63
|
+
};
|
|
64
|
+
export {};
|
|
65
|
+
//# sourceMappingURL=pkce-flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkce-flow.d.ts","sourceRoot":"","sources":["../../src/auth/pkce-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AA8BH,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,aAAa,CAAC;IAC7B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;CACjC;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAsDlF;AAgJD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,aAAa,CAAC,CAmBzB;AAuBD;;;;GAIG;AACH,iBAAS,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAKtD;AAED,eAAO,MAAM,UAAU;;;CAA8B,CAAC"}
|