@rubytech/create-maxy-code 0.1.457 → 0.1.459
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/lib/mcp-lifeline/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/mcp-lifeline/dist/index.js +22 -1
- package/payload/platform/lib/mcp-lifeline/dist/index.js.map +1 -1
- package/payload/platform/lib/mcp-lifeline/src/__tests__/lifeline.test.ts +26 -0
- package/payload/platform/lib/mcp-lifeline/src/index.ts +24 -1
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +322 -2
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.d.ts +2 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js +59 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +12 -1
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/cf-exec.js +156 -26
- package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/house-credential.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/house-credential.js +8 -2
- package/payload/platform/lib/storage-broker/dist/house-credential.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/index.d.ts +1 -0
- package/payload/platform/lib/storage-broker/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/index.js +1 -0
- package/payload/platform/lib/storage-broker/dist/index.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/object-limits.d.ts +53 -0
- package/payload/platform/lib/storage-broker/dist/object-limits.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/object-limits.js +72 -0
- package/payload/platform/lib/storage-broker/dist/object-limits.js.map +1 -0
- package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +362 -3
- package/payload/platform/lib/storage-broker/src/__tests__/object-limits.test.ts +76 -0
- package/payload/platform/lib/storage-broker/src/cf-exec.ts +230 -44
- package/payload/platform/lib/storage-broker/src/house-credential.ts +8 -2
- package/payload/platform/lib/storage-broker/src/index.ts +1 -0
- package/payload/platform/lib/storage-broker/src/object-limits.ts +75 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +5 -5
- package/payload/platform/plugins/cloudflare/bin/__tests__/cf-token.test.sh +145 -4
- package/payload/platform/plugins/cloudflare/bin/cf-token.sh +61 -3
- package/payload/platform/plugins/cloudflare/references/api.md +1 -1
- package/payload/platform/plugins/cloudflare/skills/cloudflare/SKILL.md +1 -1
- package/payload/platform/plugins/docs/references/outlook-guide.md +4 -4
- package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/outlook/PLUGIN.md +2 -2
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.js +138 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.js +121 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.js +102 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js +16 -3
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.js +108 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.js +209 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/index.js +10 -7
- package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/attach.d.ts +37 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attach.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attach.js +79 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/attach.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts +42 -15
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js +32 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/message.d.ts +8 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/message.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/message.js +4 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/message.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts +31 -8
- package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.js +40 -13
- package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.d.ts +9 -5
- package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.js +12 -8
- package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-event.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-event.js +3 -5
- package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-event.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts +36 -4
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js +56 -9
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts +13 -2
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js +18 -4
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js +3 -4
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-fetch-body.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-fetch-body.js +2 -4
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-fetch-body.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js +3 -20
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.d.ts +27 -3
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js +53 -8
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js.map +1 -1
- package/payload/platform/plugins/outlook/references/graph-surfaces.md +41 -6
- package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +22 -5
- package/payload/platform/plugins/storage-broker/PLUGIN.md +41 -2
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.d.ts +2 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.d.ts.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.js +85 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.js.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.d.ts +2 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.d.ts.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.js +77 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.js.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.d.ts +12 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.d.ts.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js +107 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/index.js +86 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/storage-broker/mcp/dist/put-source.d.ts +11 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/put-source.d.ts.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/put-source.js +35 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/put-source.js.map +1 -0
- package/payload/platform/scripts/logs-read-jsonl.test.sh +118 -0
- package/payload/platform/scripts/logs-read.sh +63 -9
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +4 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/server/{chunk-F4D35LKL.js → chunk-POBXIHOF.js} +34 -0
- package/payload/server/{chunk-SDYRKIYY.js → chunk-Q6W4U6HL.js} +153 -27
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/server.js +206 -6
- package/payload/server/{src-554BYJMN.js → src-3I2RYZFB.js} +9 -1
|
@@ -32,10 +32,64 @@ case "$scope" in pages|d1|storage|calendar-d1|dns|access) ;; *) die "unknown sco
|
|
|
32
32
|
# was used, surfaced on every lifeline so an operator can see hosting is on the
|
|
33
33
|
# isolated path, not a stray per-account token. Single-tenant installs keep the
|
|
34
34
|
# master in the per-account file, so no fallback fires and behaviour is unchanged.
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
35
|
+
#
|
|
36
|
+
# The fallback is gated on the caller being the house account: the master is
|
|
37
|
+
# account-wide (D1+R2 Edit and API Tokens Write), so handing it to a client
|
|
38
|
+
# sub-account would defeat the 1631 storage boundary as well as leak Pages/DNS.
|
|
39
|
+
# A specialist spawn is NOT a trust tier - it runs under whichever account owns
|
|
40
|
+
# the session, and carries PLATFORM_ROOT exactly as an admin spawn does
|
|
41
|
+
# (pty-spawner.ts:2174), so spawn shape cannot stand in for caller identity.
|
|
42
|
+
#
|
|
43
|
+
# WHAT THIS GATE IS AND IS NOT. It closes the SANCTIONED path: the platform no
|
|
44
|
+
# longer mints an account-wide token on a sub-account's behalf through its own
|
|
45
|
+
# supported seam. It is NOT a defence against a compromised or prompt-injected
|
|
46
|
+
# specialist. Every account's session runs as the SAME OS user (no uid/gid is
|
|
47
|
+
# dropped at spawn) and the house env path is deterministic, so an agent holding
|
|
48
|
+
# Bash can read that file directly or export a false ACCOUNT_ID. Real enforcement
|
|
49
|
+
# needs OS user separation, which was costed and declined as disproportionate
|
|
50
|
+
# (Task 1690, archived undone). This is therefore permanently a boundary of
|
|
51
|
+
# intent, not of permission -- no follow-up tracks closing it. Do not cite this
|
|
52
|
+
# gate as proof a sub-account cannot reach the master.
|
|
53
|
+
#
|
|
54
|
+
# "House" is an exact shell mirror of resolveAccountDir + isHouseAccount
|
|
55
|
+
# (platform/services/claude-session-manager/src/config.ts:238-323) -- the same
|
|
56
|
+
# predicate the spawn path uses to decide HOUSE_ADMIN_SCOPE, so the gate and
|
|
57
|
+
# cross-account authority can never disagree about which account is the house.
|
|
58
|
+
# Its three arms: exactly one role:"house" account (the caller must be it), no
|
|
59
|
+
# house plus a single account (the caller is it), and anything else -- including
|
|
60
|
+
# two or more role:"house" accounts -- is corruption and denies rather than
|
|
61
|
+
# picking one silently. An account dir counts when it holds a parseable
|
|
62
|
+
# account.json; like resolveAccountDir, the UUID shape of the dir name is not
|
|
63
|
+
# re-validated here (provisioning enforces it).
|
|
38
64
|
file_has_master() { grep -Eq '^[[:space:]]*CLOUDFLARE_API_TOKEN=.+' "$1" 2>/dev/null; }
|
|
65
|
+
|
|
66
|
+
# deny_reason is deliberately global: the caller reads it for the lifeline.
|
|
67
|
+
# Every other variable is local, so this function cannot clobber the scope
|
|
68
|
+
# table's state (suffix/key/names/res/legacy/regexes/zone_scoped/mtag).
|
|
69
|
+
deny_reason=""
|
|
70
|
+
caller_is_house() {
|
|
71
|
+
local adir cfg role houses cands d r
|
|
72
|
+
deny_reason=""
|
|
73
|
+
[ -n "${ACCOUNT_ID:-}" ] || { deny_reason="no-account-id"; return 1; }
|
|
74
|
+
[ -n "${PLATFORM_ROOT:-}" ] || { deny_reason="no-platform-root"; return 1; }
|
|
75
|
+
adir="$(dirname "$PLATFORM_ROOT")/data/accounts"
|
|
76
|
+
cfg="${adir}/${ACCOUNT_ID}/account.json"
|
|
77
|
+
[ -r "$cfg" ] || { deny_reason="no-account-json"; return 1; }
|
|
78
|
+
role=$(jq -r '.role // empty' "$cfg" 2>/dev/null) || { deny_reason="unparseable-account-json"; return 1; }
|
|
79
|
+
houses=0; cands=0
|
|
80
|
+
for d in "$adir"/*/; do
|
|
81
|
+
[ -r "${d}account.json" ] || continue
|
|
82
|
+
cands=$((cands+1))
|
|
83
|
+
r=$(jq -r '.role // empty' "${d}account.json" 2>/dev/null) || r=""
|
|
84
|
+
[ "$r" = "house" ] && houses=$((houses+1))
|
|
85
|
+
done
|
|
86
|
+
if [ "$houses" -gt 1 ]; then deny_reason="multiple-house-accounts"; return 1; fi
|
|
87
|
+
if [ "$houses" -eq 1 ] && [ "$role" = "house" ]; then return 0; fi
|
|
88
|
+
if [ "$houses" -eq 0 ] && [ "$cands" -eq 1 ]; then return 0; fi
|
|
89
|
+
deny_reason="not-house"
|
|
90
|
+
return 1
|
|
91
|
+
}
|
|
92
|
+
|
|
39
93
|
src=account
|
|
40
94
|
if ! file_has_master "$secrets"; then
|
|
41
95
|
house="${CF_HOUSE_ENV:-}"
|
|
@@ -43,6 +97,10 @@ if ! file_has_master "$secrets"; then
|
|
|
43
97
|
house="${PLATFORM_ROOT}/config/cloudflare-house.env"
|
|
44
98
|
fi
|
|
45
99
|
if [ -n "$house" ] && [ -r "$house" ] && file_has_master "$house"; then
|
|
100
|
+
if ! caller_is_house; then
|
|
101
|
+
echo "[cf-token] op=resolve scope=${scope} master=house action=deny result=error code=0 src=house caller=${ACCOUNT_ID:-unset} reason=${deny_reason}" >&2
|
|
102
|
+
die "account '${ACCOUNT_ID:-unset}' is not the house account, so it cannot mint the '${scope}' scope from the account-wide Cloudflare master, which is house-only. Run this from the house admin session."
|
|
103
|
+
fi
|
|
46
104
|
secrets="$house"; src=house
|
|
47
105
|
else
|
|
48
106
|
echo "[cf-token] op=resolve scope=${scope} master=none action=resolve result=error code=0 src=account" >&2
|
|
@@ -53,7 +53,7 @@ chmod 600 "${SECRETS_DIR}/cloudflare.env"
|
|
|
53
53
|
|
|
54
54
|
### Multi-tenant installs — the master lives house-only
|
|
55
55
|
|
|
56
|
-
The account-scoped storage above is correct for a single-tenant install (one Cloudflare account, one client). On a **multi-client install** (many client sub-accounts sharing one Cloudflare account, for example SiteDesk), an account-wide token in a sub-account's `cloudflare.env` lets that sub-account reach every other client's D1 and R2. There, the master lives house-only at `${PLATFORM_ROOT}/config/cloudflare-house.env` (a location no sub-account spawn sources), per-account `cloudflare.env` files carry no account-wide token, and a sub-account reaches D1/R2 only through the storage broker, which scopes every operation to the caller's account. See [`../../../../.docs/cloudflare-storage-isolation.md`](../../../../.docs/cloudflare-storage-isolation.md). The per-scope DNS/Pages/Access minting below stays admin-run, and its mint-or-reuse mechanism is unchanged; the one difference on a multi-tenant install is the source file. Because the per-account `cloudflare.env` no longer carries the master, `cf-token.sh` falls back to the house credential (`${PLATFORM_ROOT}/config/cloudflare-house.env`) for these scopes, mints and persists the scope token there, and reports `src=house` on its `[cf-token]` lifeline.
|
|
56
|
+
The account-scoped storage above is correct for a single-tenant install (one Cloudflare account, one client). On a **multi-client install** (many client sub-accounts sharing one Cloudflare account, for example SiteDesk), an account-wide token in a sub-account's `cloudflare.env` lets that sub-account reach every other client's D1 and R2. There, the master lives house-only at `${PLATFORM_ROOT}/config/cloudflare-house.env` (a location no sub-account spawn sources), per-account `cloudflare.env` files carry no account-wide token, and a sub-account reaches D1/R2 only through the storage broker, which scopes every operation to the caller's account. See [`../../../../.docs/cloudflare-storage-isolation.md`](../../../../.docs/cloudflare-storage-isolation.md). The per-scope DNS/Pages/Access minting below stays admin-run, and its mint-or-reuse mechanism is unchanged; the one difference on a multi-tenant install is the source file. Because the per-account `cloudflare.env` no longer carries the master, `cf-token.sh` falls back to the house credential (`${PLATFORM_ROOT}/config/cloudflare-house.env`) for these scopes, mints and persists the scope token there, and reports `src=house` on its `[cf-token]` lifeline. `cf-token.sh` gates that fallback on the caller being the house account (its `account.json` carries `role:"house"`, or it is the sole account), so the sanctioned path never mints a hosting token for a client sub-account; a denied caller gets an `action=deny` lifeline naming the reason. This gate is a boundary of intent, not of permission: every account's session runs as the same unix user and the house env path is deterministic, so an agent holding `Bash` can read the master directly or export a false `ACCOUNT_ID`. Do not cite the gate as proof a sub-account cannot reach the master. This is permanent, not pending: OS user separation was costed and declined as disproportionate, so nothing tracks closing it.
|
|
57
57
|
|
|
58
58
|
Load the master for a mint call:
|
|
59
59
|
|
|
@@ -29,7 +29,7 @@ This is the entry point for every Cloudflare task on the install. Pick the opera
|
|
|
29
29
|
|
|
30
30
|
Neither the master token nor any per-scope token is ever written into a project tree, committed, or echoed into chat — the per-scope tokens persist only in the account-scoped secrets file, never in a deployable project tree.
|
|
31
31
|
|
|
32
|
-
On a multi-client install where client sub-accounts share one Cloudflare account, the master lives house-only at `config/cloudflare-house.env` and sub-account D1/R2 data work goes through the storage broker, not raw `wrangler`. This skill's tunnel operations are per-brand isolated and unchanged. The admin-run scopes (Pages, DNS, Access) stay admin/house-run: because the per-account secrets file no longer carries the master, `cf-token.sh` resolves and mints those scope tokens against the house credential (`config/cloudflare-house.env`) and persists them there, surfacing `src=house` on its `[cf-token]` lifeline. Hosting a client site to Pages is therefore an admin/house action
|
|
32
|
+
On a multi-client install where client sub-accounts share one Cloudflare account, the master lives house-only at `config/cloudflare-house.env` and sub-account D1/R2 data work goes through the storage broker, not raw `wrangler`. This skill's tunnel operations are per-brand isolated and unchanged. The admin-run scopes (Pages, DNS, Access) stay admin/house-run: because the per-account secrets file no longer carries the master, `cf-token.sh` resolves and mints those scope tokens against the house credential (`config/cloudflare-house.env`) and persists them there, surfacing `src=house` on its `[cf-token]` lifeline. Hosting a client site to Pages is therefore an admin/house action: `cf-token.sh` mints the Pages/DNS/Access scopes only for the house account (or the sole account on a single-tenant install) and denies any other caller with an `action=deny` lifeline. Run deploys from the house admin session. The house credential in `config/cloudflare-house.env` is a **minter**, not a data token, on **every** install, not only multi-tenant ones: the booking calendar and storage broker read it house-only fleet-wide (no per-account fallback), so they mint their own account-scoped data tokens from that minter — `CF_STORAGE_TOKEN` (D1 + R2) and `CF_CALENDAR_D1_TOKEN` (D1) — rather than use the minter directly, which returns `10000` on D1/R2. Multi-tenant installs add per-account isolation on top of this same file; single-tenant installs simply read their calendar/broker credential here. See `references/api.md` and `.docs/cloudflare-storage-isolation.md`.
|
|
33
33
|
|
|
34
34
|
### Resolve the per-scope token before any `wrangler` / API call (binding)
|
|
35
35
|
|
|
@@ -33,9 +33,9 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
|
|
|
33
33
|
| `outlook-contacts-list` | Top contacts. Default top=50. |
|
|
34
34
|
| `outlook-mailbox-info` | Health probe for one mailbox — auth state, refresh-window, folder count. Name the mailbox with `mailbox` when several are attached. |
|
|
35
35
|
| `outlook-mailbox-list` | Every connected mailbox with its email, scopes, and token health. Local only — no network — so it also flags a mailbox whose stored credentials cannot be read. |
|
|
36
|
-
| `outlook-mail-send` | Send a message
|
|
37
|
-
| `outlook-draft` | Create a draft in Drafts (`POST /me/messages`). Same body model. Returns the draft id. Needs `Mail.ReadWrite`. |
|
|
38
|
-
| `outlook-draft-edit` | Update an existing draft in place (`PATCH /me/messages/{draftId}`). Graph drafts are mutable — pass draftId plus the fields to change (to/cc/bcc/subject/body/isHtml). Sent messages cannot be edited. Needs `Mail.ReadWrite`. |
|
|
36
|
+
| `outlook-mail-send` | Send a message. No attachments → `POST /me/sendMail`; with attachments → draft, attach, send (`/me/sendMail` carries attachments inline and cannot fit a large file). to/cc/bcc arrays, subject, body, isHtml, attachments (account-dir paths, 25 MB/file). Success asserted on Graph 202; no message id on either route. Needs `Mail.Send` (+ `Mail.ReadWrite` to attach). |
|
|
37
|
+
| `outlook-draft` | Create a draft in Drafts (`POST /me/messages`). Same body model, including attachments — files are attached before the id is returned, so the draft is ready to send. Returns the draft id. Needs `Mail.ReadWrite`. |
|
|
38
|
+
| `outlook-draft-edit` | Update an existing draft in place (`PATCH /me/messages/{draftId}`) and/or add attachments. Graph drafts are mutable — pass draftId plus the fields to change (to/cc/bcc/subject/body/isHtml/attachments). Attachments are additive; removal is not supported. Sent messages cannot be edited. Needs `Mail.ReadWrite`. |
|
|
39
39
|
| `outlook-draft-send` | Send an existing draft by id (`POST /me/messages/{id}/send`). Graph consumes the draft, returns 202. Needs `Mail.Send`. |
|
|
40
40
|
|
|
41
41
|
## Observability
|
|
@@ -110,4 +110,4 @@ Latency triage: `mail-list count=0 elapsedMs<200` consistent → permissions iss
|
|
|
110
110
|
|
|
111
111
|
## Out of scope
|
|
112
112
|
|
|
113
|
-
Mail move to arbitrary folders / flag, hard (permanent) delete, ingesting reviewed mail into the business graph (the `email-ingest` analogue), contacts write, recurring-series occurrence editing, the `scheduling` Neo4j calendar and any auto-sync with it, OneDrive / Files, push notifications, on-premises Exchange, M365 admin scopes (`User.Read.All`, `AuditLog.Read.All`), public-agent exposure, multi-tenant federation. Mail read, full-body read, inbound attachment list/download, reply, delete (to Deleted Items), and compose are supported (`outlook-mail-fetch-body`, `outlook-mail-attachment`, `outlook-mail-reply`, `outlook-mail-delete`, `outlook-mail-otp-extract`, `outlook-mail-send`, `outlook-draft`, `outlook-draft-edit`, `outlook-draft-send`), and the calendar is read plus control. See `platform/plugins/outlook/PLUGIN.md` for the full out-of-scope list.
|
|
113
|
+
Mail move to arbitrary folders / flag, hard (permanent) delete, removing or replacing an attachment already on a draft, ingesting reviewed mail into the business graph (the `email-ingest` analogue), contacts write, recurring-series occurrence editing, the `scheduling` Neo4j calendar and any auto-sync with it, OneDrive / Files, push notifications, on-premises Exchange, M365 admin scopes (`User.Read.All`, `AuditLog.Read.All`), public-agent exposure, multi-tenant federation. Mail read, full-body read, inbound attachment list/download, reply, delete (to Deleted Items), and compose — all four outbound paths carrying attachments — are supported (`outlook-mail-fetch-body`, `outlook-mail-attachment`, `outlook-mail-reply`, `outlook-mail-delete`, `outlook-mail-otp-extract`, `outlook-mail-send`, `outlook-draft`, `outlook-draft-edit`, `outlook-draft-send`), and the calendar is read plus control. See `platform/plugins/outlook/PLUGIN.md` for the full out-of-scope list.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "outlook",
|
|
3
|
-
"description": "Microsoft 365 / Outlook.com via Microsoft Graph. Mail is read (list, search, full-body), reply, delete, and compose (draft, draft-edit, send); contacts are read-only; calendar is read + control. Per-account OAuth device-code flow; no client secret. One account can hold several mailboxes at once; every operational tool takes an optional mailbox selector (email or graphUserId) and refuses ambiguously when two or more are attached and none is named. Tools — outlook-account-register / outlook-account-register-poll: device-code register (start + poll); outlook-mail-list / outlook-mail-search: inbox triage (~255-char preview) with a date window (list) or KQL from:/subject: filters (search) and a nextCursor for paging beyond the newest page; outlook-mail-fetch-body: full body of one message by id; outlook-mail-attachment: list a message's inbound attachments, or download one file attachment's bytes to the account uploads folder; outlook-mail-reply: in-thread reply (replyAll, cc/bcc, attachments); outlook-mail-delete: move to Deleted Items (recoverable); outlook-mail-otp-extract: poll for a one-time code; outlook-mail-send / outlook-draft / outlook-draft-edit / outlook-draft-send: compose, edit, and send; outlook-calendar-list / outlook-calendar-event: read calendar; outlook-calendar-create / outlook-calendar-update / outlook-calendar-cancel / outlook-calendar-respond / outlook-calendar-freebusy: control calendar; outlook-contacts-list: contacts; outlook-mailbox-info: auth state + folder count for one mailbox; outlook-mailbox-list: every connected mailbox.",
|
|
3
|
+
"description": "Microsoft 365 / Outlook.com via Microsoft Graph. Mail is read (list, search, full-body), reply, delete, and compose (draft, draft-edit, send); contacts are read-only; calendar is read + control. Per-account OAuth device-code flow; no client secret. One account can hold several mailboxes at once; every operational tool takes an optional mailbox selector (email or graphUserId) and refuses ambiguously when two or more are attached and none is named. Tools — outlook-account-register / outlook-account-register-poll: device-code register (start + poll); outlook-mail-list / outlook-mail-search: inbox triage (~255-char preview) with a date window (list) or KQL from:/subject: filters (search) and a nextCursor for paging beyond the newest page; outlook-mail-fetch-body: full body of one message by id; outlook-mail-attachment: list a message's inbound attachments, or download one file attachment's bytes to the account uploads folder; outlook-mail-reply: in-thread reply (replyAll, cc/bcc, attachments); outlook-mail-delete: move to Deleted Items (recoverable); outlook-mail-otp-extract: poll for a one-time code; outlook-mail-send / outlook-draft / outlook-draft-edit / outlook-draft-send: compose, edit, and send, all with attachments; outlook-calendar-list / outlook-calendar-event: read calendar; outlook-calendar-create / outlook-calendar-update / outlook-calendar-cancel / outlook-calendar-respond / outlook-calendar-freebusy: control calendar; outlook-contacts-list: contacts; outlook-mailbox-info: auth state + folder count for one mailbox; outlook-mailbox-list: every connected mailbox.",
|
|
4
4
|
"version": "0.1.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Rubytech LLC"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: outlook
|
|
3
|
-
description: Microsoft 365 / Outlook.com via Microsoft Graph. Mail is read (list, search, full-body), reply, delete, and compose (draft, draft-edit, send); contacts are read-only; calendar is read + control. Per-account OAuth device-code flow; no client secret. One account can hold several mailboxes at once; every operational tool takes an optional mailbox selector (email or graphUserId) and refuses ambiguously when two or more are attached and none is named. Tools — outlook-account-register / outlook-account-register-poll: device-code register (start + poll); outlook-mail-list / outlook-mail-search: inbox triage (~255-char preview) with a date window (list) or KQL from:/subject: filters (search) and a nextCursor for paging beyond the newest page; outlook-mail-fetch-body: full body of one message by id; outlook-mail-attachment: list a message's inbound attachments, or download one file attachment's bytes to the account uploads folder; outlook-mail-reply: in-thread reply (replyAll, cc/bcc, attachments); outlook-mail-delete: move to Deleted Items (recoverable); outlook-mail-otp-extract: poll for a one-time code; outlook-mail-send / outlook-draft / outlook-draft-edit / outlook-draft-send: compose, edit, and send; outlook-calendar-list / outlook-calendar-event: read calendar; outlook-calendar-create / outlook-calendar-update / outlook-calendar-cancel / outlook-calendar-respond / outlook-calendar-freebusy: control calendar; outlook-contacts-list: contacts; outlook-mailbox-info: auth state + folder count for one mailbox; outlook-mailbox-list: every connected mailbox.
|
|
3
|
+
description: Microsoft 365 / Outlook.com via Microsoft Graph. Mail is read (list, search, full-body), reply, delete, and compose (draft, draft-edit, send); contacts are read-only; calendar is read + control. Per-account OAuth device-code flow; no client secret. One account can hold several mailboxes at once; every operational tool takes an optional mailbox selector (email or graphUserId) and refuses ambiguously when two or more are attached and none is named. Tools — outlook-account-register / outlook-account-register-poll: device-code register (start + poll); outlook-mail-list / outlook-mail-search: inbox triage (~255-char preview) with a date window (list) or KQL from:/subject: filters (search) and a nextCursor for paging beyond the newest page; outlook-mail-fetch-body: full body of one message by id; outlook-mail-attachment: list a message's inbound attachments, or download one file attachment's bytes to the account uploads folder; outlook-mail-reply: in-thread reply (replyAll, cc/bcc, attachments); outlook-mail-delete: move to Deleted Items (recoverable); outlook-mail-otp-extract: poll for a one-time code; outlook-mail-send / outlook-draft / outlook-draft-edit / outlook-draft-send: compose, edit, and send, all with attachments; outlook-calendar-list / outlook-calendar-event: read calendar; outlook-calendar-create / outlook-calendar-update / outlook-calendar-cancel / outlook-calendar-respond / outlook-calendar-freebusy: control calendar; outlook-contacts-list: contacts; outlook-mailbox-info: auth state + folder count for one mailbox; outlook-mailbox-list: every connected mailbox.
|
|
4
4
|
tools:
|
|
5
5
|
- name: outlook-account-register
|
|
6
6
|
publicAllowlist: false
|
|
@@ -102,7 +102,7 @@ Microsoft Graph access for Outlook.com / Microsoft 365 mailboxes. Mail is read (
|
|
|
102
102
|
- **Mail (attachments):** `outlook-mail-attachment` lists or downloads a message's inbound attachments. Called with `messageId` alone it LISTS them (`id`, `name`, `contentType`, `size`, `isInline`, and a `kind` of `file`/`item`/`reference`) via `GET /me/messages/{id}/attachments` with `$select` (no bytes). Called with `messageId`+`attachmentId` it DOWNLOADS one file attachment: the bytes are written to `{accountDir}/uploads/outlook/<messageHash>/<bytesHash>-<name>` at `0o600` and the saved absolute path is returned, ready for the Read tool, `SendUserFile`, or an `outlook-mail-reply` attachment. Only file attachments download — `item` (embedded message/event) and `reference` (cloud-drive link) kinds are refused, as are empty payloads and anything over the 25 MB cap; in each case nothing is written. Requires `Mail.Read`.
|
|
103
103
|
- **Mail (reply / delete):** `outlook-mail-reply` replies in-thread (native `conversationId` threading) with `replyAll`, added cc/bcc, and account-scoped file attachments (up to 25 MB per file — files over Graph's ~3 MB inline limit stream through a chunked upload session); the body is prepended above the quoted original. `outlook-mail-delete` moves messages to Deleted Items (recoverable — never hard-deletes). Both require `Mail.ReadWrite` (reply also needs `Mail.Send`).
|
|
104
104
|
- **Mail (OTP):** `outlook-mail-otp-extract` polls the inbox for a one-time verification code from a named sender, reading full bodies to extract it — used during service authentication.
|
|
105
|
-
- **Mail (send / draft):** `outlook-mail-send` sends via `POST /me/sendMail` (
|
|
105
|
+
- **Mail (send / draft):** `outlook-mail-send` sends a new message — via `POST /me/sendMail` with no attachments, or composed as a draft, attached, and sent when `attachments` is present (`/me/sendMail` carries attachments inline and cannot fit a large file); `outlook-draft` creates a draft, attaches any files, and returns its id; `outlook-draft-edit` updates an existing draft in place (Graph drafts are mutable) and can add attachments (additive — it cannot remove one); `outlook-draft-send` sends an existing draft by id. Recipients are to/cc/bcc arrays; body is HTML or plain text via `isHtml`; `attachments` are file paths inside the account directory, capped at 25 MB per file and validated before any Graph write. Success asserted on Graph 202. Requires `Mail.Send` + `Mail.ReadWrite`; a mailbox on the old read-only consent returns an actionable "re-register to grant send" signal.
|
|
106
106
|
- **Calendar (read + control):** `outlook-calendar-list` and `outlook-calendar-event` read events; `outlook-calendar-create` books an event; `outlook-calendar-update` reschedules or edits one; `outlook-calendar-cancel` cancels a meeting (notifying attendees when the account organises it) or deletes a personal appointment; `outlook-calendar-respond` accepts, declines, or tentatively accepts an invite; `outlook-calendar-freebusy` reports availability for one or more addresses over a window. Writes require the `Calendars.ReadWrite` consent; a mailbox on stale read-only consent returns a distinct signal naming the scope and `outlook-account-register`.
|
|
107
107
|
- **Contacts (read-only):** `outlook-contacts-list` returns contacts.
|
|
108
108
|
- **Health:** `outlook-mailbox-info` reports auth state, refresh-window status, and top-level folder count for one mailbox (name it with `mailbox` when several are attached). `outlook-mailbox-list` lists every attached mailbox with its email, scopes, and token health, reading local stores only, never the network. It reports an `addressable` count and a per-mailbox `status` (`ok` / `blank-email` / `refresh-expired` / `unreadable`) so an authenticated, addressable mailbox is distinguishable from one that is merely present: a bare `count` of 1 is not "connected" unless that mailbox is `status:ok`. Use these to answer "did mailbox X auth?" without grepping logs.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attach.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/attach.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { 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 { attachFilesToDraft, resolveAttachmentsOrRefuse } from "../lib/attach.js";
|
|
8
|
+
import { AttachmentRefusedError, OUTLOOK_ATTACHMENT_INLINE_MAX_BYTES, OUTLOOK_ATTACHMENT_MAX_BYTES, resolveOutboundAttachments, } from "../lib/outbound-attachments.js";
|
|
9
|
+
function configWithToken(accountId) {
|
|
10
|
+
const dir = mkdtempSync(join(tmpdir(), "outlook-attach-"));
|
|
11
|
+
const tokenStore = new TokenStore(accountId, dir);
|
|
12
|
+
tokenStore.store("access-token", "refresh-token", 3600, { scopes: ["Mail.ReadWrite", "Mail.Send"] });
|
|
13
|
+
return { clientId: "test-client", tenantId: "common", accountId, tokenStore };
|
|
14
|
+
}
|
|
15
|
+
const UPLOAD_URL = "https://upload.example.com/session/attach";
|
|
16
|
+
function mockRouted() {
|
|
17
|
+
const calls = [];
|
|
18
|
+
const original = globalThis.fetch;
|
|
19
|
+
globalThis.fetch = (async (input, init) => {
|
|
20
|
+
const req = input instanceof Request ? input : null;
|
|
21
|
+
const url = req ? req.url : typeof input === "string" ? input : input.toString();
|
|
22
|
+
const method = req?.method ?? init?.method ?? "GET";
|
|
23
|
+
const isGraph = url.includes("graph.microsoft.com");
|
|
24
|
+
const isUpload = url.startsWith(UPLOAD_URL);
|
|
25
|
+
if (!isGraph && !isUpload)
|
|
26
|
+
return original(input, init);
|
|
27
|
+
let body = null;
|
|
28
|
+
if (req)
|
|
29
|
+
body = await req.clone().text().catch(() => null);
|
|
30
|
+
else if (typeof init?.body === "string")
|
|
31
|
+
body = init.body;
|
|
32
|
+
const contentRange = req?.headers.get("content-range") ?? new Headers(init?.headers).get("content-range");
|
|
33
|
+
calls.push({ url: contentRange ? `${url}#${contentRange}` : url, method, body });
|
|
34
|
+
if (isUpload) {
|
|
35
|
+
const m = contentRange?.match(/bytes (\d+)-(\d+)\/(\d+)/);
|
|
36
|
+
const final = m ? Number(m[2]) + 1 === Number(m[3]) : true;
|
|
37
|
+
return new Response(JSON.stringify({ id: "ATT1" }), {
|
|
38
|
+
status: final ? 201 : 200, headers: { "content-type": "application/json" },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (url.includes("/createUploadSession")) {
|
|
42
|
+
return new Response(JSON.stringify({ uploadUrl: UPLOAD_URL }), {
|
|
43
|
+
status: 201, headers: { "content-type": "application/json" },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return new Response(JSON.stringify({ id: "ATT1" }), { status: 201, headers: { "content-type": "application/json" } });
|
|
47
|
+
});
|
|
48
|
+
return { restore: () => { globalThis.fetch = original; }, calls };
|
|
49
|
+
}
|
|
50
|
+
test("attachFilesToDraft: a small file takes one inline attachments POST", async () => {
|
|
51
|
+
const dir = mkdtempSync(join(tmpdir(), "outlook-acct-a1-"));
|
|
52
|
+
process.env.ACCOUNT_DIR = dir;
|
|
53
|
+
const small = join(dir, "note.txt");
|
|
54
|
+
writeFileSync(small, "tiny");
|
|
55
|
+
const mock = mockRouted();
|
|
56
|
+
try {
|
|
57
|
+
const files = resolveOutboundAttachments("acct-a1", [small]);
|
|
58
|
+
await attachFilesToDraft(configWithToken("acct-a1"), "DRAFTX", files, "draft");
|
|
59
|
+
assert.ok(!mock.calls.some((c) => /createUploadSession/.test(c.url)), "no upload session for a small file");
|
|
60
|
+
const posts = mock.calls.filter((c) => c.method === "POST" && /\/DRAFTX\/attachments$/.test(c.url));
|
|
61
|
+
assert.equal(posts.length, 1, "exactly one inline attachments POST");
|
|
62
|
+
assert.match(posts[0].body ?? "", /#microsoft\.graph\.fileAttachment/);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
mock.restore();
|
|
66
|
+
delete process.env.ACCOUNT_DIR;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
test("attachFilesToDraft: a file over the inline limit streams through an upload session", async () => {
|
|
70
|
+
const dir = mkdtempSync(join(tmpdir(), "outlook-acct-a2-"));
|
|
71
|
+
process.env.ACCOUNT_DIR = dir;
|
|
72
|
+
const big = join(dir, "big.pdf");
|
|
73
|
+
writeFileSync(big, Buffer.alloc(OUTLOOK_ATTACHMENT_INLINE_MAX_BYTES + 1024 * 1024));
|
|
74
|
+
const mock = mockRouted();
|
|
75
|
+
try {
|
|
76
|
+
const files = resolveOutboundAttachments("acct-a2", [big]);
|
|
77
|
+
await attachFilesToDraft(configWithToken("acct-a2"), "DRAFTY", files, "mail-send");
|
|
78
|
+
assert.ok(mock.calls.some((c) => /\/createUploadSession$/.test(c.url)), "createUploadSession invoked");
|
|
79
|
+
assert.ok(mock.calls.some((c) => c.method === "PUT" && c.url.startsWith(UPLOAD_URL)), "chunk PUT issued");
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
mock.restore();
|
|
83
|
+
delete process.env.ACCOUNT_DIR;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
test("resolveOutboundAttachments: an oversize file throws AttachmentRefusedError reason=oversize", () => {
|
|
87
|
+
const dir = mkdtempSync(join(tmpdir(), "outlook-acct-a3-"));
|
|
88
|
+
process.env.ACCOUNT_DIR = dir;
|
|
89
|
+
const huge = join(dir, "huge.bin");
|
|
90
|
+
writeFileSync(huge, Buffer.alloc(OUTLOOK_ATTACHMENT_MAX_BYTES + 1));
|
|
91
|
+
try {
|
|
92
|
+
assert.throws(() => resolveOutboundAttachments("acct-a3", [huge]), (err) => {
|
|
93
|
+
assert.ok(err instanceof AttachmentRefusedError, "typed refusal");
|
|
94
|
+
assert.equal(err.reason, "oversize");
|
|
95
|
+
assert.equal(err.file, "huge.bin");
|
|
96
|
+
assert.equal(err.cap, OUTLOOK_ATTACHMENT_MAX_BYTES);
|
|
97
|
+
assert.match(err.message, /25 MB/);
|
|
98
|
+
return true;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
delete process.env.ACCOUNT_DIR;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
test("resolveOutboundAttachments: a path outside the account dir throws reason=bad-path", () => {
|
|
106
|
+
process.env.ACCOUNT_DIR = mkdtempSync(join(tmpdir(), "outlook-acct-a4-"));
|
|
107
|
+
const outside = join(mkdtempSync(join(tmpdir(), "elsewhere-a4-")), "x.txt");
|
|
108
|
+
writeFileSync(outside, "x");
|
|
109
|
+
try {
|
|
110
|
+
assert.throws(() => resolveOutboundAttachments("acct-a4", [outside]), (err) => {
|
|
111
|
+
assert.ok(err instanceof AttachmentRefusedError, "typed refusal");
|
|
112
|
+
assert.equal(err.reason, "bad-path");
|
|
113
|
+
assert.match(err.message, /outside the account directory/);
|
|
114
|
+
return true;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
delete process.env.ACCOUNT_DIR;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
test("resolveAttachmentsOrRefuse: rethrows the refusal and returns files on the happy path", () => {
|
|
122
|
+
const dir = mkdtempSync(join(tmpdir(), "outlook-acct-a5-"));
|
|
123
|
+
process.env.ACCOUNT_DIR = dir;
|
|
124
|
+
const ok = join(dir, "ok.txt");
|
|
125
|
+
writeFileSync(ok, "fine");
|
|
126
|
+
try {
|
|
127
|
+
assert.deepEqual(resolveAttachmentsOrRefuse("acct-a5", undefined, "draft"), []);
|
|
128
|
+
const files = resolveAttachmentsOrRefuse("acct-a5", [ok], "draft");
|
|
129
|
+
assert.equal(files.length, 1);
|
|
130
|
+
assert.equal(files[0].name, "ok.txt");
|
|
131
|
+
assert.equal(files[0].mode, "inline");
|
|
132
|
+
assert.throws(() => resolveAttachmentsOrRefuse("acct-a5", [join(dir, "nope.txt")], "draft"), /Attachment not found/);
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
delete process.env.ACCOUNT_DIR;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
//# sourceMappingURL=attach.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attach.test.js","sourceRoot":"","sources":["../../src/__tests__/attach.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draft-attachments.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/draft-attachments.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { 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 { runDraft } from "../tools/draft.js";
|
|
8
|
+
import { OUTLOOK_ATTACHMENT_INLINE_MAX_BYTES, OUTLOOK_ATTACHMENT_MAX_BYTES } from "../lib/outbound-attachments.js";
|
|
9
|
+
function configWithToken(accountId) {
|
|
10
|
+
const dir = mkdtempSync(join(tmpdir(), "outlook-draft-"));
|
|
11
|
+
const tokenStore = new TokenStore(accountId, dir);
|
|
12
|
+
tokenStore.store("access-token", "refresh-token", 3600, { scopes: ["Mail.ReadWrite", "Mail.Send"] });
|
|
13
|
+
return { clientId: "test-client", tenantId: "common", accountId, tokenStore };
|
|
14
|
+
}
|
|
15
|
+
const UPLOAD_URL = "https://upload.example.com/session/draft";
|
|
16
|
+
function mockRouted() {
|
|
17
|
+
const calls = [];
|
|
18
|
+
const original = globalThis.fetch;
|
|
19
|
+
globalThis.fetch = (async (input, init) => {
|
|
20
|
+
const req = input instanceof Request ? input : null;
|
|
21
|
+
const url = req ? req.url : typeof input === "string" ? input : input.toString();
|
|
22
|
+
const method = req?.method ?? init?.method ?? "GET";
|
|
23
|
+
const isGraph = url.includes("graph.microsoft.com");
|
|
24
|
+
const isUpload = url.startsWith(UPLOAD_URL);
|
|
25
|
+
if (!isGraph && !isUpload)
|
|
26
|
+
return original(input, init);
|
|
27
|
+
let body = null;
|
|
28
|
+
if (req)
|
|
29
|
+
body = await req.clone().text().catch(() => null);
|
|
30
|
+
else if (typeof init?.body === "string")
|
|
31
|
+
body = init.body;
|
|
32
|
+
const contentRange = req?.headers.get("content-range") ?? new Headers(init?.headers).get("content-range");
|
|
33
|
+
calls.push({ url: contentRange ? `${url}#${contentRange}` : url, method, body });
|
|
34
|
+
if (isUpload)
|
|
35
|
+
return new Response(JSON.stringify({ id: "ATT1" }), { status: 201, headers: { "content-type": "application/json" } });
|
|
36
|
+
if (url.includes("/createUploadSession")) {
|
|
37
|
+
return new Response(JSON.stringify({ uploadUrl: UPLOAD_URL }), { status: 201, headers: { "content-type": "application/json" } });
|
|
38
|
+
}
|
|
39
|
+
return new Response(JSON.stringify({ id: "D1" }), { status: 201, headers: { "content-type": "application/json" } });
|
|
40
|
+
});
|
|
41
|
+
return { restore: () => { globalThis.fetch = original; }, calls };
|
|
42
|
+
}
|
|
43
|
+
const BASE = { to: ["rob@ecolecltd.co.uk"], subject: "Pricing", body: "See attached" };
|
|
44
|
+
test("outlook-draft: no attachments creates the draft with one POST", async () => {
|
|
45
|
+
const mock = mockRouted();
|
|
46
|
+
try {
|
|
47
|
+
const r = await runDraft(configWithToken("acct-d1"), BASE);
|
|
48
|
+
assert.equal(r.draftId, "D1");
|
|
49
|
+
assert.deepEqual(r.attachments, []);
|
|
50
|
+
assert.equal(mock.calls.length, 1);
|
|
51
|
+
assert.match(mock.calls[0].url, /\/me\/messages$/);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
mock.restore();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
test("outlook-draft: a small attachment is attached to the created draft before the id returns", async () => {
|
|
58
|
+
const dir = mkdtempSync(join(tmpdir(), "outlook-acct-d2-"));
|
|
59
|
+
process.env.ACCOUNT_DIR = dir;
|
|
60
|
+
const file = join(dir, "schedule.pdf");
|
|
61
|
+
writeFileSync(file, "pricing");
|
|
62
|
+
const mock = mockRouted();
|
|
63
|
+
try {
|
|
64
|
+
const r = await runDraft(configWithToken("acct-d2"), { ...BASE, attachments: [file] });
|
|
65
|
+
assert.equal(r.draftId, "D1");
|
|
66
|
+
assert.deepEqual(r.attachments, ["schedule.pdf"]);
|
|
67
|
+
assert.match(mock.calls[0].url, /\/me\/messages$/, "draft created first");
|
|
68
|
+
const attachPosts = mock.calls.filter((c) => c.method === "POST" && /\/D1\/attachments$/.test(c.url));
|
|
69
|
+
assert.equal(attachPosts.length, 1, "exactly one inline attachments POST");
|
|
70
|
+
assert.match(attachPosts[0].body ?? "", /#microsoft\.graph\.fileAttachment/);
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
mock.restore();
|
|
74
|
+
delete process.env.ACCOUNT_DIR;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
test("outlook-draft: an attachment over the inline limit streams via an upload session", async () => {
|
|
78
|
+
const dir = mkdtempSync(join(tmpdir(), "outlook-acct-d3-"));
|
|
79
|
+
process.env.ACCOUNT_DIR = dir;
|
|
80
|
+
const big = join(dir, "big.pdf");
|
|
81
|
+
writeFileSync(big, Buffer.alloc(OUTLOOK_ATTACHMENT_INLINE_MAX_BYTES + 1024 * 1024));
|
|
82
|
+
const mock = mockRouted();
|
|
83
|
+
try {
|
|
84
|
+
await runDraft(configWithToken("acct-d3"), { ...BASE, attachments: [big] });
|
|
85
|
+
assert.ok(mock.calls.some((c) => /\/createUploadSession$/.test(c.url)), "createUploadSession invoked");
|
|
86
|
+
assert.ok(mock.calls.some((c) => c.method === "PUT" && c.url.startsWith(UPLOAD_URL)), "chunk PUT issued");
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
mock.restore();
|
|
90
|
+
delete process.env.ACCOUNT_DIR;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
test("outlook-draft: an oversize attachment is refused before any Graph call — no orphan draft", async () => {
|
|
94
|
+
const dir = mkdtempSync(join(tmpdir(), "outlook-acct-d4-"));
|
|
95
|
+
process.env.ACCOUNT_DIR = dir;
|
|
96
|
+
const huge = join(dir, "huge.bin");
|
|
97
|
+
writeFileSync(huge, Buffer.alloc(OUTLOOK_ATTACHMENT_MAX_BYTES + 1));
|
|
98
|
+
const mock = mockRouted();
|
|
99
|
+
try {
|
|
100
|
+
await assert.rejects(runDraft(configWithToken("acct-d4"), { ...BASE, attachments: [huge] }), /25 MB/);
|
|
101
|
+
assert.equal(mock.calls.length, 0, "zero Graph calls — nothing to orphan");
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
mock.restore();
|
|
105
|
+
delete process.env.ACCOUNT_DIR;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
test("outlook-draft: a bad path is refused before any Graph call — no orphan draft", async () => {
|
|
109
|
+
const dir = mkdtempSync(join(tmpdir(), "outlook-acct-d5-"));
|
|
110
|
+
process.env.ACCOUNT_DIR = dir;
|
|
111
|
+
const mock = mockRouted();
|
|
112
|
+
try {
|
|
113
|
+
await assert.rejects(runDraft(configWithToken("acct-d5"), { ...BASE, attachments: [join(dir, "ghost.pdf")] }), /Attachment not found/);
|
|
114
|
+
assert.equal(mock.calls.length, 0, "zero Graph calls — nothing to orphan");
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
mock.restore();
|
|
118
|
+
delete process.env.ACCOUNT_DIR;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
//# sourceMappingURL=draft-attachments.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draft-attachments.test.js","sourceRoot":"","sources":["../../src/__tests__/draft-attachments.test.ts"],"names":[],"mappings":"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"}
|
package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draft-edit-attachments.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/draft-edit-attachments.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { 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 { runDraftEdit } from "../tools/draft-edit.js";
|
|
8
|
+
import { OUTLOOK_ATTACHMENT_MAX_BYTES } from "../lib/outbound-attachments.js";
|
|
9
|
+
function configWithToken(accountId) {
|
|
10
|
+
const dir = mkdtempSync(join(tmpdir(), "outlook-de-"));
|
|
11
|
+
const tokenStore = new TokenStore(accountId, dir);
|
|
12
|
+
tokenStore.store("access-token", "refresh-token", 3600, { scopes: ["Mail.ReadWrite", "Mail.Send"] });
|
|
13
|
+
return { clientId: "test-client", tenantId: "common", accountId, tokenStore };
|
|
14
|
+
}
|
|
15
|
+
function mockRouted() {
|
|
16
|
+
const calls = [];
|
|
17
|
+
const original = globalThis.fetch;
|
|
18
|
+
globalThis.fetch = (async (input, init) => {
|
|
19
|
+
const req = input instanceof Request ? input : null;
|
|
20
|
+
const url = req ? req.url : typeof input === "string" ? input : input.toString();
|
|
21
|
+
const method = req?.method ?? init?.method ?? "GET";
|
|
22
|
+
if (!url.includes("graph.microsoft.com"))
|
|
23
|
+
return original(input, init);
|
|
24
|
+
let body = null;
|
|
25
|
+
if (req)
|
|
26
|
+
body = await req.clone().text().catch(() => null);
|
|
27
|
+
else if (typeof init?.body === "string")
|
|
28
|
+
body = init.body;
|
|
29
|
+
calls.push({ url, method, body });
|
|
30
|
+
return new Response(JSON.stringify({ id: "DE1" }), { status: 200, headers: { "content-type": "application/json" } });
|
|
31
|
+
});
|
|
32
|
+
return { restore: () => { globalThis.fetch = original; }, calls };
|
|
33
|
+
}
|
|
34
|
+
test("outlook-draft-edit: attachments alone is a valid edit — no PATCH, one attach POST", async () => {
|
|
35
|
+
const dir = mkdtempSync(join(tmpdir(), "outlook-acct-de1-"));
|
|
36
|
+
process.env.ACCOUNT_DIR = dir;
|
|
37
|
+
const file = join(dir, "added.pdf");
|
|
38
|
+
writeFileSync(file, "late addition");
|
|
39
|
+
const mock = mockRouted();
|
|
40
|
+
try {
|
|
41
|
+
const r = await runDraftEdit(configWithToken("acct-de1"), { draftId: "DE1", attachments: [file] });
|
|
42
|
+
assert.equal(r.draftId, "DE1");
|
|
43
|
+
assert.equal(r.updated, true);
|
|
44
|
+
assert.deepEqual(r.attachments, ["added.pdf"]);
|
|
45
|
+
assert.ok(!mock.calls.some((c) => c.method === "PATCH"), "no PATCH when only attachments change");
|
|
46
|
+
const attachPosts = mock.calls.filter((c) => c.method === "POST" && /\/DE1\/attachments$/.test(c.url));
|
|
47
|
+
assert.equal(attachPosts.length, 1, "exactly one inline attachments POST");
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
mock.restore();
|
|
51
|
+
delete process.env.ACCOUNT_DIR;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
test("outlook-draft-edit: a field plus an attachment PATCHes then attaches", async () => {
|
|
55
|
+
const dir = mkdtempSync(join(tmpdir(), "outlook-acct-de2-"));
|
|
56
|
+
process.env.ACCOUNT_DIR = dir;
|
|
57
|
+
const file = join(dir, "added.pdf");
|
|
58
|
+
writeFileSync(file, "late addition");
|
|
59
|
+
const mock = mockRouted();
|
|
60
|
+
try {
|
|
61
|
+
const r = await runDraftEdit(configWithToken("acct-de2"), {
|
|
62
|
+
draftId: "DE1", subject: "Revised", attachments: [file],
|
|
63
|
+
});
|
|
64
|
+
assert.deepEqual(r.attachments, ["added.pdf"]);
|
|
65
|
+
const patch = mock.calls.find((c) => c.method === "PATCH");
|
|
66
|
+
assert.ok(patch, "a PATCH updated the draft");
|
|
67
|
+
assert.match(patch.body ?? "", /Revised/);
|
|
68
|
+
const patchIdx = mock.calls.findIndex((c) => c.method === "PATCH");
|
|
69
|
+
const attachIdx = mock.calls.findIndex((c) => /\/DE1\/attachments$/.test(c.url));
|
|
70
|
+
assert.ok(patchIdx < attachIdx, "PATCH precedes the attach");
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
mock.restore();
|
|
74
|
+
delete process.env.ACCOUNT_DIR;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
test("outlook-draft-edit: no fields and no attachments is still refused", async () => {
|
|
78
|
+
const mock = mockRouted();
|
|
79
|
+
try {
|
|
80
|
+
await assert.rejects(runDraftEdit(configWithToken("acct-de3"), { draftId: "DE1" }), /at least one field/);
|
|
81
|
+
assert.equal(mock.calls.length, 0);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
mock.restore();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
test("outlook-draft-edit: an oversize attachment is refused before any Graph call", async () => {
|
|
88
|
+
const dir = mkdtempSync(join(tmpdir(), "outlook-acct-de4-"));
|
|
89
|
+
process.env.ACCOUNT_DIR = dir;
|
|
90
|
+
const huge = join(dir, "huge.bin");
|
|
91
|
+
writeFileSync(huge, Buffer.alloc(OUTLOOK_ATTACHMENT_MAX_BYTES + 1));
|
|
92
|
+
const mock = mockRouted();
|
|
93
|
+
try {
|
|
94
|
+
await assert.rejects(runDraftEdit(configWithToken("acct-de4"), { draftId: "DE1", subject: "x", attachments: [huge] }), /25 MB/);
|
|
95
|
+
assert.equal(mock.calls.length, 0, "zero Graph calls — the PATCH must not land either");
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
mock.restore();
|
|
99
|
+
delete process.env.ACCOUNT_DIR;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
//# sourceMappingURL=draft-edit-attachments.test.js.map
|
package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draft-edit-attachments.test.js","sourceRoot":"","sources":["../../src/__tests__/draft-edit-attachments.test.ts"],"names":[],"mappings":"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"}
|