@juvantlabs/m365-graph-mcp-server 0.1.0
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/ARCHITECTURE.md +225 -0
- package/CHANGELOG.md +188 -0
- package/LICENSE +21 -0
- package/README.md +164 -0
- package/SECURITY.md +64 -0
- package/dist/auth/confirmation_tokens.d.ts +38 -0
- package/dist/auth/confirmation_tokens.d.ts.map +1 -0
- package/dist/auth/confirmation_tokens.js +85 -0
- package/dist/auth/confirmation_tokens.js.map +1 -0
- package/dist/auth/keyring.d.ts +20 -0
- package/dist/auth/keyring.d.ts.map +1 -0
- package/dist/auth/keyring.js +41 -0
- package/dist/auth/keyring.js.map +1 -0
- package/dist/auth/msal.d.ts +42 -0
- package/dist/auth/msal.d.ts.map +1 -0
- package/dist/auth/msal.js +96 -0
- package/dist/auth/msal.js.map +1 -0
- package/dist/auth/setup.d.ts +18 -0
- package/dist/auth/setup.d.ts.map +1 -0
- package/dist/auth/setup.js +110 -0
- package/dist/auth/setup.js.map +1 -0
- package/dist/client/graph.d.ts +30 -0
- package/dist/client/graph.d.ts.map +1 -0
- package/dist/client/graph.js +38 -0
- package/dist/client/graph.js.map +1 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +131 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/cancel_event.d.ts +18 -0
- package/dist/tools/cancel_event.d.ts.map +1 -0
- package/dist/tools/cancel_event.js +95 -0
- package/dist/tools/cancel_event.js.map +1 -0
- package/dist/tools/copy_file.d.ts +39 -0
- package/dist/tools/copy_file.d.ts.map +1 -0
- package/dist/tools/copy_file.js +168 -0
- package/dist/tools/copy_file.js.map +1 -0
- package/dist/tools/create_event.d.ts +29 -0
- package/dist/tools/create_event.d.ts.map +1 -0
- package/dist/tools/create_event.js +144 -0
- package/dist/tools/create_event.js.map +1 -0
- package/dist/tools/decline_event.d.ts +18 -0
- package/dist/tools/decline_event.d.ts.map +1 -0
- package/dist/tools/decline_event.js +105 -0
- package/dist/tools/decline_event.js.map +1 -0
- package/dist/tools/delete_file.d.ts +28 -0
- package/dist/tools/delete_file.d.ts.map +1 -0
- package/dist/tools/delete_file.js +103 -0
- package/dist/tools/delete_file.js.map +1 -0
- package/dist/tools/download_file.d.ts +43 -0
- package/dist/tools/download_file.d.ts.map +1 -0
- package/dist/tools/download_file.js +133 -0
- package/dist/tools/download_file.js.map +1 -0
- package/dist/tools/get_event.d.ts +27 -0
- package/dist/tools/get_event.d.ts.map +1 -0
- package/dist/tools/get_event.js +55 -0
- package/dist/tools/get_event.js.map +1 -0
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +61 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/list_calendars.d.ts +26 -0
- package/dist/tools/list_calendars.d.ts.map +1 -0
- package/dist/tools/list_calendars.js +60 -0
- package/dist/tools/list_calendars.js.map +1 -0
- package/dist/tools/list_drives.d.ts +27 -0
- package/dist/tools/list_drives.d.ts.map +1 -0
- package/dist/tools/list_drives.js +58 -0
- package/dist/tools/list_drives.js.map +1 -0
- package/dist/tools/list_events.d.ts +51 -0
- package/dist/tools/list_events.d.ts.map +1 -0
- package/dist/tools/list_events.js +119 -0
- package/dist/tools/list_events.js.map +1 -0
- package/dist/tools/list_items.d.ts +31 -0
- package/dist/tools/list_items.d.ts.map +1 -0
- package/dist/tools/list_items.js +81 -0
- package/dist/tools/list_items.js.map +1 -0
- package/dist/tools/move_file.d.ts +18 -0
- package/dist/tools/move_file.d.ts.map +1 -0
- package/dist/tools/move_file.js +60 -0
- package/dist/tools/move_file.js.map +1 -0
- package/dist/tools/search_events.d.ts +25 -0
- package/dist/tools/search_events.d.ts.map +1 -0
- package/dist/tools/search_events.js +71 -0
- package/dist/tools/search_events.js.map +1 -0
- package/dist/tools/search_events_content.d.ts +32 -0
- package/dist/tools/search_events_content.d.ts.map +1 -0
- package/dist/tools/search_events_content.js +106 -0
- package/dist/tools/search_events_content.js.map +1 -0
- package/dist/tools/search_files.d.ts +30 -0
- package/dist/tools/search_files.d.ts.map +1 -0
- package/dist/tools/search_files.js +82 -0
- package/dist/tools/search_files.js.map +1 -0
- package/dist/tools/update_event.d.ts +25 -0
- package/dist/tools/update_event.d.ts.map +1 -0
- package/dist/tools/update_event.js +123 -0
- package/dist/tools/update_event.js.map +1 -0
- package/dist/tools/upload_file.d.ts +38 -0
- package/dist/tools/upload_file.d.ts.map +1 -0
- package/dist/tools/upload_file.js +152 -0
- package/dist/tools/upload_file.js.map +1 -0
- package/dist/types/tool.d.ts +32 -0
- package/dist/types/tool.d.ts.map +1 -0
- package/dist/types/tool.js +10 -0
- package/dist/types/tool.js.map +1 -0
- package/dist/types/validators.d.ts +44 -0
- package/dist/types/validators.d.ts.map +1 -0
- package/dist/types/validators.js +78 -0
- package/dist/types/validators.js.map +1 -0
- package/package.json +72 -0
package/SECURITY.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Please report vulnerabilities **privately** via one of these channels:
|
|
6
|
+
|
|
7
|
+
1. **GitHub Security Advisory** (preferred) — go to this repo's
|
|
8
|
+
`Security` tab → `Report a vulnerability`. Your report stays
|
|
9
|
+
private between you and the maintainer until we publish a
|
|
10
|
+
coordinated advisory.
|
|
11
|
+
2. **Email** — `security@juvant.io`. Reports go to the primary
|
|
12
|
+
maintainer.
|
|
13
|
+
|
|
14
|
+
**Please do NOT** open a public issue or pull request that contains
|
|
15
|
+
reproduction details for the vulnerability. Once a public artifact
|
|
16
|
+
exposes the issue, the coordinated-disclosure window collapses.
|
|
17
|
+
|
|
18
|
+
## What we commit to
|
|
19
|
+
|
|
20
|
+
This repo follows the
|
|
21
|
+
[juvantlabs Security Disclosure Process](https://github.com/juvantlabs/handbook/blob/main/docs/security/disclosure-process.md).
|
|
22
|
+
SLOs:
|
|
23
|
+
|
|
24
|
+
| State | Target |
|
|
25
|
+
|---|---|
|
|
26
|
+
| Acknowledge receipt | ≤ 7 days |
|
|
27
|
+
| Initial triage + severity classification | ≤ 14 days |
|
|
28
|
+
| Patch prepared (high/critical) | ≤ 30 days |
|
|
29
|
+
| Patch prepared (moderate) | ≤ 90 days |
|
|
30
|
+
| Public advisory + CVE | Patch + 1–7 days |
|
|
31
|
+
|
|
32
|
+
## Supported versions
|
|
33
|
+
|
|
34
|
+
| Version | Supported |
|
|
35
|
+
|---|---|
|
|
36
|
+
| Latest `0.x` (current) | ✅ |
|
|
37
|
+
| Older `0.x` | ❌ End-of-life with each new release until `1.0` |
|
|
38
|
+
|
|
39
|
+
Once `1.0` ships, the supported-versions matrix expands to formally
|
|
40
|
+
back-port security fixes to the `N-1` major.
|
|
41
|
+
|
|
42
|
+
## Out of scope
|
|
43
|
+
|
|
44
|
+
- Issues in dependencies — please report those upstream. We track
|
|
45
|
+
upstream advisories via Dependabot and bump promptly.
|
|
46
|
+
- Issues in adopter customizations / forks of this server.
|
|
47
|
+
- Theoretical vulnerabilities without a reproduction path.
|
|
48
|
+
|
|
49
|
+
## Security-relevant dependencies
|
|
50
|
+
|
|
51
|
+
| Dependency | Version | Why it matters |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `@modelcontextprotocol/sdk` | `^1.25.2` | ≥ 1.25.2 required to avoid ReDoS (`GHSA-8r9q-7v3j-jr4g`) and DNS rebinding (`GHSA-w48q-cv73-mx4w`) advisories on earlier versions |
|
|
54
|
+
| _(vendor SDK)_ | | _(fill in as the vendor SDK is selected)_ |
|
|
55
|
+
|
|
56
|
+
## Crediting
|
|
57
|
+
|
|
58
|
+
Reporters are credited by name in advisories unless they request
|
|
59
|
+
anonymity at report time. Past reports + reporter acknowledgments
|
|
60
|
+
will be listed in `SECURITY-CREDITS.md` (created on first disclosure).
|
|
61
|
+
|
|
62
|
+
## Acknowledgments
|
|
63
|
+
|
|
64
|
+
No disclosures yet.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side state for the spec/approval confirmation-token pattern
|
|
3
|
+
* used by destructive tools (delete_file, cancel_event).
|
|
4
|
+
*
|
|
5
|
+
* Per the handbook MCP server spec § Tool design and ADR 0002:
|
|
6
|
+
* 1. First call: agent submits a spec describing what to delete /
|
|
7
|
+
* cancel; tool returns a preview + a confirmation_token.
|
|
8
|
+
* 2. Agent reviews the preview, returns a second call with the same
|
|
9
|
+
* destructive-op args plus the confirmation_token.
|
|
10
|
+
* 3. Tool verifies (token exists, not expired, not used, tied to
|
|
11
|
+
* THIS tool, args match the original spec) and executes. Token
|
|
12
|
+
* is then consumed (single-use).
|
|
13
|
+
*
|
|
14
|
+
* Token lifetime: 5 minutes. State lives in a module-level Map keyed
|
|
15
|
+
* by token. Cleared on process exit (per-tenant subprocess per
|
|
16
|
+
* handbook spec — no cross-process leakage). Garbage-collected on
|
|
17
|
+
* each issue/consume pass.
|
|
18
|
+
*
|
|
19
|
+
* Spec match is via SHA-256 of canonical JSON (keys sorted) so the
|
|
20
|
+
* agent can't pass a token issued for {item_id: "A"} together with
|
|
21
|
+
* args {item_id: "B"} and have the destructive call go through.
|
|
22
|
+
*/
|
|
23
|
+
export interface IssuedToken {
|
|
24
|
+
confirmation_token: string;
|
|
25
|
+
expires_at: string;
|
|
26
|
+
expires_in_seconds: number;
|
|
27
|
+
}
|
|
28
|
+
export declare function issueConfirmation(toolName: string, spec: Record<string, unknown>): IssuedToken;
|
|
29
|
+
export type ConsumeError = "token_unknown" | "token_expired" | "token_wrong_tool" | "spec_mismatch";
|
|
30
|
+
export type ConsumeResult = {
|
|
31
|
+
ok: true;
|
|
32
|
+
} | {
|
|
33
|
+
ok: false;
|
|
34
|
+
error: ConsumeError;
|
|
35
|
+
};
|
|
36
|
+
export declare function consumeConfirmation(token: string, toolName: string, spec: Record<string, unknown>): ConsumeResult;
|
|
37
|
+
export declare function _resetConfirmationTokens(): void;
|
|
38
|
+
//# sourceMappingURL=confirmation_tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"confirmation_tokens.d.ts","sourceRoot":"","sources":["../../src/auth/confirmation_tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAmCH,MAAM,WAAW,WAAW;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,WAAW,CAcb;AAED,MAAM,MAAM,YAAY,GACpB,eAAe,GACf,eAAe,GACf,kBAAkB,GAClB,eAAe,CAAC;AAEpB,MAAM,MAAM,aAAa,GACrB;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,YAAY,CAAA;CAAE,CAAC;AAEvC,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,aAAa,CAiBf;AAID,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side state for the spec/approval confirmation-token pattern
|
|
3
|
+
* used by destructive tools (delete_file, cancel_event).
|
|
4
|
+
*
|
|
5
|
+
* Per the handbook MCP server spec § Tool design and ADR 0002:
|
|
6
|
+
* 1. First call: agent submits a spec describing what to delete /
|
|
7
|
+
* cancel; tool returns a preview + a confirmation_token.
|
|
8
|
+
* 2. Agent reviews the preview, returns a second call with the same
|
|
9
|
+
* destructive-op args plus the confirmation_token.
|
|
10
|
+
* 3. Tool verifies (token exists, not expired, not used, tied to
|
|
11
|
+
* THIS tool, args match the original spec) and executes. Token
|
|
12
|
+
* is then consumed (single-use).
|
|
13
|
+
*
|
|
14
|
+
* Token lifetime: 5 minutes. State lives in a module-level Map keyed
|
|
15
|
+
* by token. Cleared on process exit (per-tenant subprocess per
|
|
16
|
+
* handbook spec — no cross-process leakage). Garbage-collected on
|
|
17
|
+
* each issue/consume pass.
|
|
18
|
+
*
|
|
19
|
+
* Spec match is via SHA-256 of canonical JSON (keys sorted) so the
|
|
20
|
+
* agent can't pass a token issued for {item_id: "A"} together with
|
|
21
|
+
* args {item_id: "B"} and have the destructive call go through.
|
|
22
|
+
*/
|
|
23
|
+
import crypto from "node:crypto";
|
|
24
|
+
const EXPIRY_MS = 5 * 60 * 1000;
|
|
25
|
+
const pending = new Map();
|
|
26
|
+
function canonicalize(spec) {
|
|
27
|
+
// Stable JSON: keys sorted alphabetically. Sufficient for our specs
|
|
28
|
+
// which are flat objects of primitives (strings, numbers, booleans);
|
|
29
|
+
// we don't need full recursive canonicalization.
|
|
30
|
+
const sortedKeys = Object.keys(spec).sort();
|
|
31
|
+
const sorted = {};
|
|
32
|
+
for (const k of sortedKeys)
|
|
33
|
+
sorted[k] = spec[k];
|
|
34
|
+
return JSON.stringify(sorted);
|
|
35
|
+
}
|
|
36
|
+
function hashSpec(spec) {
|
|
37
|
+
return crypto.createHash("sha256").update(canonicalize(spec)).digest("hex");
|
|
38
|
+
}
|
|
39
|
+
function gc() {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
for (const [k, v] of pending) {
|
|
42
|
+
if (v.expiresAt <= now)
|
|
43
|
+
pending.delete(k);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function issueConfirmation(toolName, spec) {
|
|
47
|
+
gc();
|
|
48
|
+
const token = crypto.randomBytes(16).toString("hex");
|
|
49
|
+
const expiresAt = Date.now() + EXPIRY_MS;
|
|
50
|
+
pending.set(token, {
|
|
51
|
+
toolName,
|
|
52
|
+
specHash: hashSpec(spec),
|
|
53
|
+
expiresAt,
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
confirmation_token: token,
|
|
57
|
+
expires_at: new Date(expiresAt).toISOString(),
|
|
58
|
+
expires_in_seconds: Math.floor(EXPIRY_MS / 1000),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function consumeConfirmation(token, toolName, spec) {
|
|
62
|
+
gc();
|
|
63
|
+
const entry = pending.get(token);
|
|
64
|
+
if (!entry)
|
|
65
|
+
return { ok: false, error: "token_unknown" };
|
|
66
|
+
if (entry.expiresAt <= Date.now()) {
|
|
67
|
+
pending.delete(token);
|
|
68
|
+
return { ok: false, error: "token_expired" };
|
|
69
|
+
}
|
|
70
|
+
if (entry.toolName !== toolName) {
|
|
71
|
+
return { ok: false, error: "token_wrong_tool" };
|
|
72
|
+
}
|
|
73
|
+
if (entry.specHash !== hashSpec(spec)) {
|
|
74
|
+
return { ok: false, error: "spec_mismatch" };
|
|
75
|
+
}
|
|
76
|
+
// Single-use: consume on success.
|
|
77
|
+
pending.delete(token);
|
|
78
|
+
return { ok: true };
|
|
79
|
+
}
|
|
80
|
+
// Test helper — clears the in-memory token store. Not exported as a
|
|
81
|
+
// tool. Tests import this directly to ensure isolation.
|
|
82
|
+
export function _resetConfirmationTokens() {
|
|
83
|
+
pending.clear();
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=confirmation_tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"confirmation_tokens.js","sourceRoot":"","sources":["../../src/auth/confirmation_tokens.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token persistence via the OS keychain.
|
|
3
|
+
*
|
|
4
|
+
* Uses `@napi-rs/keyring` (NOT `keytar` — archived since 2022 per
|
|
5
|
+
* handbook spec anti-pattern #10). Tokens are stored under a
|
|
6
|
+
* (service, account) pair where the account is keyed by tenant ID
|
|
7
|
+
* so multiple tenant configs don't collide.
|
|
8
|
+
*
|
|
9
|
+
* Platform backends:
|
|
10
|
+
* macOS → Keychain
|
|
11
|
+
* Linux → Secret Service / GNOME keyring
|
|
12
|
+
* Windows → Credential Manager
|
|
13
|
+
*/
|
|
14
|
+
export interface TokenStore {
|
|
15
|
+
load(): string | null;
|
|
16
|
+
save(serialized: string): void;
|
|
17
|
+
clear(): void;
|
|
18
|
+
}
|
|
19
|
+
export declare function getTokenStore(tenantId: string): TokenStore;
|
|
20
|
+
//# sourceMappingURL=keyring.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyring.d.ts","sourceRoot":"","sources":["../../src/auth/keyring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,MAAM,WAAW,UAAU;IACzB,IAAI,IAAI,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,KAAK,IAAI,IAAI,CAAC;CACf;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAsB1D"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token persistence via the OS keychain.
|
|
3
|
+
*
|
|
4
|
+
* Uses `@napi-rs/keyring` (NOT `keytar` — archived since 2022 per
|
|
5
|
+
* handbook spec anti-pattern #10). Tokens are stored under a
|
|
6
|
+
* (service, account) pair where the account is keyed by tenant ID
|
|
7
|
+
* so multiple tenant configs don't collide.
|
|
8
|
+
*
|
|
9
|
+
* Platform backends:
|
|
10
|
+
* macOS → Keychain
|
|
11
|
+
* Linux → Secret Service / GNOME keyring
|
|
12
|
+
* Windows → Credential Manager
|
|
13
|
+
*/
|
|
14
|
+
import { Entry } from "@napi-rs/keyring";
|
|
15
|
+
const SERVICE = "juvantlabs-m365-graph-mcp-server";
|
|
16
|
+
export function getTokenStore(tenantId) {
|
|
17
|
+
const entry = new Entry(SERVICE, `tenant:${tenantId}`);
|
|
18
|
+
return {
|
|
19
|
+
load() {
|
|
20
|
+
try {
|
|
21
|
+
return entry.getPassword();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// No entry yet — first run before setup, or it was cleared.
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
save(serialized) {
|
|
29
|
+
entry.setPassword(serialized);
|
|
30
|
+
},
|
|
31
|
+
clear() {
|
|
32
|
+
try {
|
|
33
|
+
entry.deletePassword();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// already absent
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=keyring.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyring.js","sourceRoot":"","sources":["../../src/auth/keyring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAEzC,MAAM,OAAO,GAAG,kCAAkC,CAAC;AAQnD,MAAM,UAAU,aAAa,CAAC,QAAgB;IAC5C,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,UAAU,QAAQ,EAAE,CAAC,CAAC;IACvD,OAAO;QACL,IAAI;YACF,IAAI,CAAC;gBACH,OAAO,KAAK,CAAC,WAAW,EAAE,CAAC;YAC7B,CAAC;YAAC,MAAM,CAAC;gBACP,4DAA4D;gBAC5D,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QACD,IAAI,CAAC,UAAkB;YACrB,KAAK,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;QAChC,CAAC;QACD,KAAK;YACH,IAAI,CAAC;gBACH,KAAK,CAAC,cAAc,EAAE,CAAC;YACzB,CAAC;YAAC,MAAM,CAAC;gBACP,iBAAiB;YACnB,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MSAL Node client factory + cache plugin wiring.
|
|
3
|
+
*
|
|
4
|
+
* Uses ConfidentialClientApplication (we have a client_secret) with the
|
|
5
|
+
* Authorization Code flow for delegated permissions. Per the handbook
|
|
6
|
+
* spec § Auth, we never roll our own OAuth — MSAL Node is Microsoft's
|
|
7
|
+
* official library and handles refresh, revocation, and edge cases.
|
|
8
|
+
*
|
|
9
|
+
* Tokens persist in the OS keychain via `src/auth/keyring.ts`. The MSAL
|
|
10
|
+
* cache plugin pattern is: load on first access, save when it changes.
|
|
11
|
+
*/
|
|
12
|
+
import { ConfidentialClientApplication, type ICachePlugin } from "@azure/msal-node";
|
|
13
|
+
/**
|
|
14
|
+
* Delegated scopes the MCP server requests. Order is irrelevant; MSAL
|
|
15
|
+
* normalizes. `offline_access` is required to get a refresh token.
|
|
16
|
+
*
|
|
17
|
+
* Add scopes here as new tools land — Files.ReadWrite for upload tools,
|
|
18
|
+
* Calendars.ReadWrite for calendar write tools, etc. (per handbook spec
|
|
19
|
+
* § Auth › Scopes: per-tool minimum, justified in ARCHITECTURE.md).
|
|
20
|
+
*/
|
|
21
|
+
export declare const DELEGATED_SCOPES: string[];
|
|
22
|
+
/**
|
|
23
|
+
* The redirect URI registered in the Entra app for the OAuth callback.
|
|
24
|
+
* Must exactly match one of the redirect URIs configured in the Entra
|
|
25
|
+
* app registration. See README § Local development.
|
|
26
|
+
*/
|
|
27
|
+
export declare const REDIRECT_URI = "http://localhost:3000/auth/callback";
|
|
28
|
+
/**
|
|
29
|
+
* Build the MSAL cache plugin for a given tenant. Exported so tests
|
|
30
|
+
* can drive the load/save lifecycle directly with a fake
|
|
31
|
+
* TokenCacheContext + spied keychain store.
|
|
32
|
+
*/
|
|
33
|
+
export declare function makeCachePlugin(tenantId: string): ICachePlugin;
|
|
34
|
+
export declare function makeMsalClient(): ConfidentialClientApplication;
|
|
35
|
+
/**
|
|
36
|
+
* Acquire an access token silently from the cache. Refreshes via the
|
|
37
|
+
* cached refresh token if the access token is expired. Throws if no
|
|
38
|
+
* cached account exists — the caller should run `npm run setup` to
|
|
39
|
+
* complete the initial OAuth flow.
|
|
40
|
+
*/
|
|
41
|
+
export declare function getAccessToken(client: ConfidentialClientApplication): Promise<string>;
|
|
42
|
+
//# sourceMappingURL=msal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"msal.d.ts","sourceRoot":"","sources":["../../src/auth/msal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACL,6BAA6B,EAE7B,KAAK,YAAY,EAElB,MAAM,kBAAkB,CAAC;AAI1B;;;;;;;GAOG;AAKH,eAAO,MAAM,gBAAgB,UAK5B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,YAAY,wCAAwC,CAAC;AAElE;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,CAe9D;AAED,wBAAgB,cAAc,IAAI,6BAA6B,CAa9D;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,6BAA6B,GACpC,OAAO,CAAC,MAAM,CAAC,CAoBjB"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MSAL Node client factory + cache plugin wiring.
|
|
3
|
+
*
|
|
4
|
+
* Uses ConfidentialClientApplication (we have a client_secret) with the
|
|
5
|
+
* Authorization Code flow for delegated permissions. Per the handbook
|
|
6
|
+
* spec § Auth, we never roll our own OAuth — MSAL Node is Microsoft's
|
|
7
|
+
* official library and handles refresh, revocation, and edge cases.
|
|
8
|
+
*
|
|
9
|
+
* Tokens persist in the OS keychain via `src/auth/keyring.ts`. The MSAL
|
|
10
|
+
* cache plugin pattern is: load on first access, save when it changes.
|
|
11
|
+
*/
|
|
12
|
+
import { ConfidentialClientApplication, } from "@azure/msal-node";
|
|
13
|
+
import { getTokenStore } from "./keyring.js";
|
|
14
|
+
/**
|
|
15
|
+
* Delegated scopes the MCP server requests. Order is irrelevant; MSAL
|
|
16
|
+
* normalizes. `offline_access` is required to get a refresh token.
|
|
17
|
+
*
|
|
18
|
+
* Add scopes here as new tools land — Files.ReadWrite for upload tools,
|
|
19
|
+
* Calendars.ReadWrite for calendar write tools, etc. (per handbook spec
|
|
20
|
+
* § Auth › Scopes: per-tool minimum, justified in ARCHITECTURE.md).
|
|
21
|
+
*/
|
|
22
|
+
// Files.ReadWrite subsumes Files.Read; Calendars.ReadWrite subsumes
|
|
23
|
+
// Calendars.Read. The Entra app permissions list still includes the
|
|
24
|
+
// narrower scopes (granted earlier) — they're harmless, just not
|
|
25
|
+
// requested at token acquisition time.
|
|
26
|
+
export const DELEGATED_SCOPES = [
|
|
27
|
+
"User.Read",
|
|
28
|
+
"Files.ReadWrite",
|
|
29
|
+
"Calendars.ReadWrite",
|
|
30
|
+
"offline_access",
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* The redirect URI registered in the Entra app for the OAuth callback.
|
|
34
|
+
* Must exactly match one of the redirect URIs configured in the Entra
|
|
35
|
+
* app registration. See README § Local development.
|
|
36
|
+
*/
|
|
37
|
+
export const REDIRECT_URI = "http://localhost:3000/auth/callback";
|
|
38
|
+
/**
|
|
39
|
+
* Build the MSAL cache plugin for a given tenant. Exported so tests
|
|
40
|
+
* can drive the load/save lifecycle directly with a fake
|
|
41
|
+
* TokenCacheContext + spied keychain store.
|
|
42
|
+
*/
|
|
43
|
+
export function makeCachePlugin(tenantId) {
|
|
44
|
+
const store = getTokenStore(tenantId);
|
|
45
|
+
return {
|
|
46
|
+
async beforeCacheAccess(cacheContext) {
|
|
47
|
+
const data = store.load();
|
|
48
|
+
if (data) {
|
|
49
|
+
cacheContext.tokenCache.deserialize(data);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
async afterCacheAccess(cacheContext) {
|
|
53
|
+
if (cacheContext.cacheHasChanged) {
|
|
54
|
+
store.save(cacheContext.tokenCache.serialize());
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function makeMsalClient() {
|
|
60
|
+
const tenantId = process.env.M365_TENANT_ID ?? "";
|
|
61
|
+
const config = {
|
|
62
|
+
auth: {
|
|
63
|
+
clientId: process.env.M365_CLIENT_ID ?? "",
|
|
64
|
+
clientSecret: process.env.M365_CLIENT_SECRET ?? "",
|
|
65
|
+
authority: `https://login.microsoftonline.com/${tenantId}`,
|
|
66
|
+
},
|
|
67
|
+
cache: {
|
|
68
|
+
cachePlugin: makeCachePlugin(tenantId),
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
return new ConfidentialClientApplication(config);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Acquire an access token silently from the cache. Refreshes via the
|
|
75
|
+
* cached refresh token if the access token is expired. Throws if no
|
|
76
|
+
* cached account exists — the caller should run `npm run setup` to
|
|
77
|
+
* complete the initial OAuth flow.
|
|
78
|
+
*/
|
|
79
|
+
export async function getAccessToken(client) {
|
|
80
|
+
const cache = client.getTokenCache();
|
|
81
|
+
const accounts = await cache.getAllAccounts();
|
|
82
|
+
if (accounts.length === 0) {
|
|
83
|
+
throw new Error("No cached account found in the keychain. Run `npm run setup` (or " +
|
|
84
|
+
"`m365-graph-mcp-server setup`) once to complete the OAuth flow.");
|
|
85
|
+
}
|
|
86
|
+
const result = await client.acquireTokenSilent({
|
|
87
|
+
account: accounts[0],
|
|
88
|
+
scopes: DELEGATED_SCOPES,
|
|
89
|
+
});
|
|
90
|
+
if (!result?.accessToken) {
|
|
91
|
+
throw new Error("Silent token acquisition returned no access token. The refresh " +
|
|
92
|
+
"token may have been revoked or expired. Re-run `npm run setup`.");
|
|
93
|
+
}
|
|
94
|
+
return result.accessToken;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=msal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"msal.js","sourceRoot":"","sources":["../../src/auth/msal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACL,6BAA6B,GAI9B,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C;;;;;;;GAOG;AACH,oEAAoE;AACpE,oEAAoE;AACpE,iEAAiE;AACjE,uCAAuC;AACvC,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,WAAW;IACX,iBAAiB;IACjB,qBAAqB;IACrB,gBAAgB;CACjB,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,qCAAqC,CAAC;AAElE;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC9C,MAAM,KAAK,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IACtC,OAAO;QACL,KAAK,CAAC,iBAAiB,CAAC,YAA+B;YACrD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;YAC1B,IAAI,IAAI,EAAE,CAAC;gBACT,YAAY,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;YAC5C,CAAC;QACH,CAAC;QACD,KAAK,CAAC,gBAAgB,CAAC,YAA+B;YACpD,IAAI,YAAY,CAAC,eAAe,EAAE,CAAC;gBACjC,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,SAAS,EAAE,CAAC,CAAC;YAClD,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,EAAE,CAAC;IAClD,MAAM,MAAM,GAAkB;QAC5B,IAAI,EAAE;YACJ,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,EAAE;YAC1C,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,EAAE;YAClD,SAAS,EAAE,qCAAqC,QAAQ,EAAE;SAC3D;QACD,KAAK,EAAE;YACL,WAAW,EAAE,eAAe,CAAC,QAAQ,CAAC;SACvC;KACF,CAAC;IACF,OAAO,IAAI,6BAA6B,CAAC,MAAM,CAAC,CAAC;AACnD,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,MAAqC;IAErC,MAAM,KAAK,GAAG,MAAM,CAAC,aAAa,EAAE,CAAC;IACrC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,cAAc,EAAE,CAAC;IAC9C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CACb,mEAAmE;YACjE,iEAAiE,CACpE,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC;QAC7C,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;QACpB,MAAM,EAAE,gBAAgB;KACzB,CAAC,CAAC;IACH,IAAI,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,iEAAiE;YAC/D,iEAAiE,CACpE,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC,WAAW,CAAC;AAC5B,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive OAuth setup — run once via `npm run setup` (or
|
|
3
|
+
* `m365-graph-mcp-server setup`) to populate the OS keychain with the
|
|
4
|
+
* initial token cache. After this, `npm run dev` (or `npx ...`) uses
|
|
5
|
+
* the cached refresh token silently for the lifetime of the refresh
|
|
6
|
+
* grant (Microsoft default ~90 days; rolling).
|
|
7
|
+
*
|
|
8
|
+
* Flow:
|
|
9
|
+
* 1. Build the authorization URL via MSAL.
|
|
10
|
+
* 2. Open the user's default browser at that URL.
|
|
11
|
+
* 3. Listen on http://localhost:3000/auth/callback for the
|
|
12
|
+
* ?code=... redirect.
|
|
13
|
+
* 4. Exchange the code for tokens (MSAL writes to the cache plugin
|
|
14
|
+
* → keychain).
|
|
15
|
+
* 5. Close the localhost listener and exit.
|
|
16
|
+
*/
|
|
17
|
+
export declare function runSetup(): Promise<void>;
|
|
18
|
+
//# sourceMappingURL=setup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/auth/setup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAkFH,wBAAsB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAoC9C"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive OAuth setup — run once via `npm run setup` (or
|
|
3
|
+
* `m365-graph-mcp-server setup`) to populate the OS keychain with the
|
|
4
|
+
* initial token cache. After this, `npm run dev` (or `npx ...`) uses
|
|
5
|
+
* the cached refresh token silently for the lifetime of the refresh
|
|
6
|
+
* grant (Microsoft default ~90 days; rolling).
|
|
7
|
+
*
|
|
8
|
+
* Flow:
|
|
9
|
+
* 1. Build the authorization URL via MSAL.
|
|
10
|
+
* 2. Open the user's default browser at that URL.
|
|
11
|
+
* 3. Listen on http://localhost:3000/auth/callback for the
|
|
12
|
+
* ?code=... redirect.
|
|
13
|
+
* 4. Exchange the code for tokens (MSAL writes to the cache plugin
|
|
14
|
+
* → keychain).
|
|
15
|
+
* 5. Close the localhost listener and exit.
|
|
16
|
+
*/
|
|
17
|
+
import { exec } from "node:child_process";
|
|
18
|
+
import http from "node:http";
|
|
19
|
+
import { URL } from "node:url";
|
|
20
|
+
import { DELEGATED_SCOPES, REDIRECT_URI, makeMsalClient } from "./msal.js";
|
|
21
|
+
const CALLBACK_PORT = 3000;
|
|
22
|
+
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
23
|
+
function openInBrowser(url) {
|
|
24
|
+
const cmd = process.platform === "darwin"
|
|
25
|
+
? `open "${url}"`
|
|
26
|
+
: process.platform === "win32"
|
|
27
|
+
? `start "" "${url}"`
|
|
28
|
+
: `xdg-open "${url}"`;
|
|
29
|
+
exec(cmd, (err) => {
|
|
30
|
+
if (err) {
|
|
31
|
+
console.error("[setup] Could not open browser automatically. Visit the URL above manually.");
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function waitForAuthCode() {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const timer = setTimeout(() => {
|
|
38
|
+
server.close();
|
|
39
|
+
reject(new Error(`Timed out waiting for OAuth callback after ${CALLBACK_TIMEOUT_MS / 1000}s`));
|
|
40
|
+
}, CALLBACK_TIMEOUT_MS);
|
|
41
|
+
const server = http.createServer((req, res) => {
|
|
42
|
+
const requestUrl = new URL(req.url ?? "/", `http://localhost:${CALLBACK_PORT}`);
|
|
43
|
+
if (requestUrl.pathname !== "/auth/callback") {
|
|
44
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
45
|
+
res.end("Not Found");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const code = requestUrl.searchParams.get("code");
|
|
49
|
+
const error = requestUrl.searchParams.get("error");
|
|
50
|
+
const errorDescription = requestUrl.searchParams.get("error_description");
|
|
51
|
+
if (error) {
|
|
52
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
53
|
+
res.end(`<html><body><h2>OAuth error</h2><p><b>${error}</b></p>` +
|
|
54
|
+
`<pre>${errorDescription ?? ""}</pre></body></html>`);
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
server.close();
|
|
57
|
+
reject(new Error(`${error}: ${errorDescription ?? "(no description)"}`));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!code) {
|
|
61
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
62
|
+
res.end("Missing authorization code in callback URL.");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
66
|
+
res.end("<html><body><h2>Authentication successful</h2>" +
|
|
67
|
+
"<p>You can close this tab and return to the terminal.</p></body></html>");
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
server.close();
|
|
70
|
+
resolve(code);
|
|
71
|
+
});
|
|
72
|
+
server.on("error", (err) => {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
reject(err);
|
|
75
|
+
});
|
|
76
|
+
server.listen(CALLBACK_PORT, "127.0.0.1");
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
export async function runSetup() {
|
|
80
|
+
const client = makeMsalClient();
|
|
81
|
+
const authUrl = await client.getAuthCodeUrl({
|
|
82
|
+
scopes: DELEGATED_SCOPES,
|
|
83
|
+
redirectUri: REDIRECT_URI,
|
|
84
|
+
prompt: "select_account",
|
|
85
|
+
});
|
|
86
|
+
console.error("[setup] Starting OAuth flow against tenant:", process.env.M365_TENANT_ID);
|
|
87
|
+
console.error("[setup] If your browser does not open automatically, visit:");
|
|
88
|
+
console.error("");
|
|
89
|
+
console.error(` ${authUrl}`);
|
|
90
|
+
console.error("");
|
|
91
|
+
console.error(`[setup] Listening for callback on ${REDIRECT_URI}`);
|
|
92
|
+
console.error("");
|
|
93
|
+
openInBrowser(authUrl);
|
|
94
|
+
const code = await waitForAuthCode();
|
|
95
|
+
console.error("[setup] Authorization code received. Exchanging for tokens…");
|
|
96
|
+
const tokenResponse = await client.acquireTokenByCode({
|
|
97
|
+
code,
|
|
98
|
+
scopes: DELEGATED_SCOPES,
|
|
99
|
+
redirectUri: REDIRECT_URI,
|
|
100
|
+
});
|
|
101
|
+
if (!tokenResponse?.account) {
|
|
102
|
+
throw new Error("Token acquisition succeeded but no account info was returned.");
|
|
103
|
+
}
|
|
104
|
+
console.error("[setup] ✓ Tokens cached in OS keychain for:");
|
|
105
|
+
console.error(` username: ${tokenResponse.account.username}`);
|
|
106
|
+
console.error(` homeAccountId: ${tokenResponse.account.homeAccountId}`);
|
|
107
|
+
console.error("");
|
|
108
|
+
console.error("[setup] You can now run `npm run dev` (or `npx @juvantlabs/m365-graph-mcp-server`).");
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=setup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../src/auth/setup.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Microsoft Graph client factory.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the official `@microsoft/microsoft-graph-client` with an auth
|
|
5
|
+
* provider that pulls cached tokens from MSAL on every request. MSAL
|
|
6
|
+
* handles refresh transparently; if the refresh fails (revoked
|
|
7
|
+
* token), the auth provider surfaces the error to the caller.
|
|
8
|
+
*
|
|
9
|
+
* Uses isomorphic-fetch (peer dep of the Graph client per its docs).
|
|
10
|
+
* Imported once at module load.
|
|
11
|
+
*/
|
|
12
|
+
import "isomorphic-fetch";
|
|
13
|
+
import { Client, type AuthenticationProvider, type AuthenticationProviderOptions } from "@microsoft/microsoft-graph-client";
|
|
14
|
+
import type { ConfidentialClientApplication } from "@azure/msal-node";
|
|
15
|
+
/**
|
|
16
|
+
* Authentication provider that bridges MSAL's token cache → the
|
|
17
|
+
* Microsoft Graph client. Each Graph request triggers
|
|
18
|
+
* `getAccessToken()`, which lets MSAL refresh transparently if the
|
|
19
|
+
* cached token is expired.
|
|
20
|
+
*
|
|
21
|
+
* Exported so tests can verify the bridge without instantiating the
|
|
22
|
+
* full Graph client.
|
|
23
|
+
*/
|
|
24
|
+
export declare class MsalAuthProvider implements AuthenticationProvider {
|
|
25
|
+
private readonly msal;
|
|
26
|
+
constructor(msal: ConfidentialClientApplication);
|
|
27
|
+
getAccessToken(_options?: AuthenticationProviderOptions): Promise<string>;
|
|
28
|
+
}
|
|
29
|
+
export declare function makeGraphClient(msal: ConfidentialClientApplication): Client;
|
|
30
|
+
//# sourceMappingURL=graph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/client/graph.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,kBAAkB,CAAC;AAE1B,OAAO,EACL,MAAM,EACN,KAAK,sBAAsB,EAC3B,KAAK,6BAA6B,EACnC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,kBAAkB,CAAC;AAItE;;;;;;;;GAQG;AACH,qBAAa,gBAAiB,YAAW,sBAAsB;IACjD,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,6BAA6B;IAE1D,cAAc,CAAC,QAAQ,CAAC,EAAE,6BAA6B,GAAG,OAAO,CAAC,MAAM,CAAC;CAGhF;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,6BAA6B,GAAG,MAAM,CAI3E"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Microsoft Graph client factory.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the official `@microsoft/microsoft-graph-client` with an auth
|
|
5
|
+
* provider that pulls cached tokens from MSAL on every request. MSAL
|
|
6
|
+
* handles refresh transparently; if the refresh fails (revoked
|
|
7
|
+
* token), the auth provider surfaces the error to the caller.
|
|
8
|
+
*
|
|
9
|
+
* Uses isomorphic-fetch (peer dep of the Graph client per its docs).
|
|
10
|
+
* Imported once at module load.
|
|
11
|
+
*/
|
|
12
|
+
import "isomorphic-fetch";
|
|
13
|
+
import { Client, } from "@microsoft/microsoft-graph-client";
|
|
14
|
+
import { getAccessToken } from "../auth/msal.js";
|
|
15
|
+
/**
|
|
16
|
+
* Authentication provider that bridges MSAL's token cache → the
|
|
17
|
+
* Microsoft Graph client. Each Graph request triggers
|
|
18
|
+
* `getAccessToken()`, which lets MSAL refresh transparently if the
|
|
19
|
+
* cached token is expired.
|
|
20
|
+
*
|
|
21
|
+
* Exported so tests can verify the bridge without instantiating the
|
|
22
|
+
* full Graph client.
|
|
23
|
+
*/
|
|
24
|
+
export class MsalAuthProvider {
|
|
25
|
+
msal;
|
|
26
|
+
constructor(msal) {
|
|
27
|
+
this.msal = msal;
|
|
28
|
+
}
|
|
29
|
+
async getAccessToken(_options) {
|
|
30
|
+
return getAccessToken(this.msal);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function makeGraphClient(msal) {
|
|
34
|
+
return Client.initWithMiddleware({
|
|
35
|
+
authProvider: new MsalAuthProvider(msal),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=graph.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph.js","sourceRoot":"","sources":["../../src/client/graph.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,kBAAkB,CAAC;AAE1B,OAAO,EACL,MAAM,GAGP,MAAM,mCAAmC,CAAC;AAG3C,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD;;;;;;;;GAQG;AACH,MAAM,OAAO,gBAAgB;IACE;IAA7B,YAA6B,IAAmC;QAAnC,SAAI,GAAJ,IAAI,CAA+B;IAAG,CAAC;IAEpE,KAAK,CAAC,cAAc,CAAC,QAAwC;QAC3D,OAAO,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC;CACF;AAED,MAAM,UAAU,eAAe,CAAC,IAAmC;IACjE,OAAO,MAAM,CAAC,kBAAkB,CAAC;QAC/B,YAAY,EAAE,IAAI,gBAAgB,CAAC,IAAI,CAAC;KACzC,CAAC,CAAC;AACL,CAAC"}
|