@rubytech/create-maxy-code 0.1.441 → 0.1.442
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +99 -1
- package/package.json +1 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +37 -2
- package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
- package/payload/platform/plugins/docs/references/troubleshooting.md +35 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/device-flow.test.js +37 -4
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/device-flow.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.d.ts +27 -3
- package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.js +52 -11
- package/payload/platform/plugins/outlook/mcp/dist/auth/device-flow.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/index.js +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.d.ts +5 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.js +32 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.js.map +1 -1
- package/payload/platform/plugins/whatsapp/mcp/dist/index.js +20 -22
- package/payload/platform/plugins/whatsapp/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +3 -1
- package/payload/platform/scripts/logs-read.sh +24 -6
- package/payload/platform/scripts/logs-read.test.sh +42 -0
- package/payload/platform/scripts/rss-sampler.sh +107 -0
- package/payload/server/public/assets/{AdminLoginScreens-CukwQufW.js → AdminLoginScreens-BJVwtpLH.js} +1 -1
- package/payload/server/public/assets/{AdminShell-BwEBH6tT.js → AdminShell-CMu8_Xwn.js} +1 -1
- package/payload/server/public/assets/{Checkbox-B629mOPp.js → Checkbox-CzD0ORqA.js} +1 -1
- package/payload/server/public/assets/{admin-CdFRCUUZ.js → admin-D0yk2LMq.js} +1 -1
- package/payload/server/public/assets/{browser-BWR59IkB.js → browser-CvfbgznO.js} +1 -1
- package/payload/server/public/assets/{calendar-kuTcxX1t.js → calendar-fw4WethJ.js} +1 -1
- package/payload/server/public/assets/chat-DhD-NES_.js +1 -0
- package/payload/server/public/assets/chevron-left-B3xSeEDR.js +1 -0
- package/payload/server/public/assets/data-CUnDzDox.js +1 -0
- package/payload/server/public/assets/{file-download-4mKJxUi-.js → file-download-Px4s0Bhp.js} +1 -1
- package/payload/server/public/assets/{graph-CboWM2_-.js → graph-DbT8nA6R.js} +1 -1
- package/payload/server/public/assets/{graph-labels-B8YAdo3Z.js → graph-labels-C1YIyLiA.js} +1 -1
- package/payload/server/public/assets/{maximize-2-CxstTgoK.js → maximize-2-BwphzI9S.js} +1 -1
- package/payload/server/public/assets/{operator-oSaB1mya.js → operator-zcIfeB14.js} +1 -1
- package/payload/server/public/assets/{page-DMeJHVfA.js → page-6fCnIb45.js} +2 -2
- package/payload/server/public/assets/page-CfQFAJia.js +1 -0
- package/payload/server/public/assets/{public-C54tl6V4.js → public-C0ehZCdE.js} +1 -1
- package/payload/server/public/assets/{rotate-ccw-COGQTi7n.js → rotate-ccw-C7BqkMec.js} +1 -1
- package/payload/server/public/assets/{tasks-B97VpwEJ.js → tasks-DlgiCafL.js} +1 -1
- package/payload/server/public/assets/{time-entry-format-C09rDHo9.js → time-entry-format-CyNYVDkq.js} +1 -1
- package/payload/server/public/assets/{useCopyFeedback-DHoYHiK9.js → useCopyFeedback-ByGUMN94.js} +1 -1
- package/payload/server/public/assets/useSubAccountSwitcher-1izQvPiD.css +1 -0
- package/payload/server/public/assets/useVoiceRecorder-XgpWvXpk.js +2 -0
- package/payload/server/public/browser.html +5 -5
- package/payload/server/public/calendar.html +6 -6
- package/payload/server/public/chat.html +12 -12
- package/payload/server/public/data.html +10 -10
- package/payload/server/public/graph.html +11 -11
- package/payload/server/public/index.html +13 -13
- package/payload/server/public/operator.html +14 -14
- package/payload/server/public/public.html +12 -12
- package/payload/server/public/tasks.html +5 -5
- package/payload/server/server.js +431 -182
- package/payload/server/public/assets/chat-ejnq2rck.js +0 -1
- package/payload/server/public/assets/chevron-left-u4tjq7fI.js +0 -1
- package/payload/server/public/assets/data-dZbOyLdT.js +0 -1
- package/payload/server/public/assets/page-DEkI-Gl1.js +0 -1
- package/payload/server/public/assets/useSubAccountSwitcher-Dzmd7Aip.css +0 -1
- package/payload/server/public/assets/useVoiceRecorder-b7HzTFs0.js +0 -2
- /package/payload/server/public/assets/{useSubAccountSwitcher-DLJFISqf.js → useSubAccountSwitcher-s6WlRn52.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -856,6 +856,89 @@ function installSystemDeps() {
|
|
|
856
856
|
catch (err) {
|
|
857
857
|
console.error(` WARNING: failed to provision swap: ${err instanceof Error ? err.message : String(err)}`);
|
|
858
858
|
}
|
|
859
|
+
// Task 1593 — zram compressed-RAM swap ahead of the SD dphys swapfile. On a
|
|
860
|
+
// Pi, swapping to the SD card thrashes I/O and turns an approach-to-OOM into
|
|
861
|
+
// an apparent hard-freeze (the muvin 2026-07-13 incident). zram swaps to
|
|
862
|
+
// compressed RAM first (default priority 100 vs dphys -2), removing the
|
|
863
|
+
// thrash and adding effective headroom; dphys stays as the low-priority
|
|
864
|
+
// last-resort overflow tier. Best-effort: a failure warns but never aborts.
|
|
865
|
+
try {
|
|
866
|
+
console.log(" [privileged] apt-get install zram-tools");
|
|
867
|
+
shell("apt-get", ["install", "-y", "zram-tools"], { sudo: true, bestEffort: true });
|
|
868
|
+
const desiredZram = "ALGO=zstd\nPERCENT=50\n";
|
|
869
|
+
const zramConfPath = "/etc/default/zramswap";
|
|
870
|
+
let zramCurrent = "";
|
|
871
|
+
if (existsSync(zramConfPath)) {
|
|
872
|
+
try {
|
|
873
|
+
zramCurrent = readFileSync(zramConfPath, "utf-8");
|
|
874
|
+
}
|
|
875
|
+
catch {
|
|
876
|
+
zramCurrent = "";
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (zramCurrent.includes("ALGO=zstd") && zramCurrent.includes("PERCENT=50")) {
|
|
880
|
+
logFile(" zram: already configured (ALGO=zstd PERCENT=50)");
|
|
881
|
+
}
|
|
882
|
+
else {
|
|
883
|
+
const zramTmp = `/tmp/zramswap.${process.pid}`;
|
|
884
|
+
writeFileSync(zramTmp, desiredZram);
|
|
885
|
+
console.log(" [privileged] install /etc/default/zramswap (ALGO=zstd PERCENT=50)");
|
|
886
|
+
shell("cp", [zramTmp, zramConfPath], { sudo: true });
|
|
887
|
+
spawnSync("rm", ["-f", zramTmp]);
|
|
888
|
+
console.log(" [privileged] systemctl restart zramswap");
|
|
889
|
+
shell("systemctl", ["restart", "zramswap"], { sudo: true, bestEffort: true });
|
|
890
|
+
logFile(" zram: configured ALGO=zstd PERCENT=50 and restarted zramswap");
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
catch (err) {
|
|
894
|
+
console.error(` WARNING: failed to provision zram: ${err instanceof Error ? err.message : String(err)}`);
|
|
895
|
+
}
|
|
896
|
+
// Task 1593 — earlyoom userspace killer. Under sustained pressure it SIGTERMs
|
|
897
|
+
// the largest process at 10% MemAvailable (SIGKILL at 5%) BEFORE the kernel
|
|
898
|
+
// oom-killer wedges the box; systemd's Restart=on-failure then restarts the
|
|
899
|
+
// service, so a leak becomes "one process dies and comes back" instead of a
|
|
900
|
+
// hard freeze. --avoid protects sshd (keeps the box reachable, so SSH never
|
|
901
|
+
// drops during a kill) and systemd (init). Neo4j runs as comm=java and is
|
|
902
|
+
// crash-safe (recovers from its transaction log on restart), so it is
|
|
903
|
+
// intentionally not protected. Best-effort. The 10%/5% thresholds are the
|
|
904
|
+
// calibration target of the on-device induced-pressure test.
|
|
905
|
+
try {
|
|
906
|
+
console.log(" [privileged] apt-get install earlyoom");
|
|
907
|
+
shell("apt-get", ["install", "-y", "earlyoom"], { sudo: true, bestEffort: true });
|
|
908
|
+
// No inner quotes around the --avoid regex: the Debian earlyoom.service
|
|
909
|
+
// runs `ExecStart=/usr/bin/earlyoom $EARLYOOM_ARGS`, and systemd expands the
|
|
910
|
+
// variable by whitespace-splitting WITHOUT shell quote removal, so inner
|
|
911
|
+
// single-quotes would survive into the regex and never match sshd/systemd.
|
|
912
|
+
// The regex `^(sshd|systemd)$` contains no whitespace, so it stays one token.
|
|
913
|
+
const desiredEarlyoom = "EARLYOOM_ARGS=\"-m 10,5 -r 3600 --avoid ^(sshd|systemd)$\"\n";
|
|
914
|
+
const earlyoomConfPath = "/etc/default/earlyoom";
|
|
915
|
+
let earlyoomCurrent = "";
|
|
916
|
+
if (existsSync(earlyoomConfPath)) {
|
|
917
|
+
try {
|
|
918
|
+
earlyoomCurrent = readFileSync(earlyoomConfPath, "utf-8");
|
|
919
|
+
}
|
|
920
|
+
catch {
|
|
921
|
+
earlyoomCurrent = "";
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
if (earlyoomCurrent.includes("-m 10,5") && earlyoomCurrent.includes("--avoid ^(sshd|systemd)$")) {
|
|
925
|
+
logFile(" earlyoom: already configured (-m 10,5 --avoid sshd|systemd)");
|
|
926
|
+
}
|
|
927
|
+
else {
|
|
928
|
+
const eoTmp = `/tmp/earlyoom.${process.pid}`;
|
|
929
|
+
writeFileSync(eoTmp, desiredEarlyoom);
|
|
930
|
+
console.log(" [privileged] install /etc/default/earlyoom (-m 10,5 -r 3600)");
|
|
931
|
+
shell("cp", [eoTmp, earlyoomConfPath], { sudo: true });
|
|
932
|
+
spawnSync("rm", ["-f", eoTmp]);
|
|
933
|
+
console.log(" [privileged] systemctl enable --now earlyoom");
|
|
934
|
+
shell("systemctl", ["enable", "--now", "earlyoom"], { sudo: true, bestEffort: true });
|
|
935
|
+
shell("systemctl", ["restart", "earlyoom"], { sudo: true, bestEffort: true });
|
|
936
|
+
logFile(" earlyoom: configured -m 10,5 -r 3600 --avoid sshd|systemd and enabled");
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
catch (err) {
|
|
940
|
+
console.error(` WARNING: failed to provision earlyoom: ${err instanceof Error ? err.message : String(err)}`);
|
|
941
|
+
}
|
|
859
942
|
// Hostname resolution — four sources, in priority order:
|
|
860
943
|
// 1. --hostname flag (unconditional — the caller is the authority)
|
|
861
944
|
// 2. OS detection on same-brand upgrade (service exists → keep whatever is currently set)
|
|
@@ -4218,6 +4301,17 @@ function installService() {
|
|
|
4218
4301
|
const rssSamplerUnitName = `${BRAND.hostname}-rss-sampler.service`;
|
|
4219
4302
|
const rssSamplerScriptPath = resolve(INSTALL_DIR, "platform/scripts/rss-sampler.sh");
|
|
4220
4303
|
const rssSamplerLogPath = join(persistDir, "rss-sampler.log");
|
|
4304
|
+
// Task 1593 — the per-service unit names the sampler reads for its
|
|
4305
|
+
// op=mem-sample line, kept brand-agnostic by passing them as env. Neo4j runs
|
|
4306
|
+
// as a dedicated per-brand unit when it is off the default port, else the
|
|
4307
|
+
// shared system unit. MEM_PRESSURE_PCT sets the approach-to-OOM warning line
|
|
4308
|
+
// as a percentage of MemTotal; 20% sits above earlyoom's 10% SIGTERM line on
|
|
4309
|
+
// every fleet RAM size (8-16 GB), so the warning is logged before the kill
|
|
4310
|
+
// (a fixed MB value cannot stay above a percentage kill line across both).
|
|
4311
|
+
const memBrandService = `${BRAND.hostname}.service`;
|
|
4312
|
+
const memEdgeService = edgeUnitName;
|
|
4313
|
+
const memNeo4jUnit = NEO4J_DEDICATED ? `neo4j-${BRAND.hostname}.service` : "neo4j.service";
|
|
4314
|
+
const memPressurePct = "20";
|
|
4221
4315
|
const rssSamplerUnit = `[Unit]
|
|
4222
4316
|
Description=${BRAND.productName} RSS Sampler
|
|
4223
4317
|
|
|
@@ -4225,6 +4319,10 @@ Description=${BRAND.productName} RSS Sampler
|
|
|
4225
4319
|
Type=simple
|
|
4226
4320
|
ExecStart=/bin/bash ${rssSamplerScriptPath}
|
|
4227
4321
|
Environment=LOG_PATH=${rssSamplerLogPath}
|
|
4322
|
+
Environment=BRAND_SERVICE=${memBrandService}
|
|
4323
|
+
Environment=EDGE_SERVICE=${memEdgeService}
|
|
4324
|
+
Environment=NEO4J_UNIT=${memNeo4jUnit}
|
|
4325
|
+
Environment=MEM_PRESSURE_PCT=${memPressurePct}
|
|
4228
4326
|
Restart=on-failure
|
|
4229
4327
|
RestartSec=10
|
|
4230
4328
|
|
|
@@ -4232,7 +4330,7 @@ RestartSec=10
|
|
|
4232
4330
|
WantedBy=default.target
|
|
4233
4331
|
`;
|
|
4234
4332
|
writeFileSync(join(serviceDir, rssSamplerUnitName), rssSamplerUnit);
|
|
4235
|
-
logFile(` rss-sampler installed: ${rssSamplerUnitName} LOG_PATH=${rssSamplerLogPath}`);
|
|
4333
|
+
logFile(` rss-sampler installed: ${rssSamplerUnitName} LOG_PATH=${rssSamplerLogPath} BRAND_SERVICE=${memBrandService} EDGE_SERVICE=${memEdgeService} NEO4J_UNIT=${memNeo4jUnit} MEM_PRESSURE_PCT=${memPressurePct}`);
|
|
4236
4334
|
// the edge service: always-on front door that owns the public
|
|
4237
4335
|
// port (PORT) and the VNC stack (Xtigervnc + websockify). Its lifecycle is
|
|
4238
4336
|
// independent of the main brand service, so an in-place upgrade triggered
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: platform-architecture
|
|
3
3
|
description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
|
|
4
|
-
content-hash: sha256:
|
|
4
|
+
content-hash: sha256:93408025e99adfc1e44893f43734ca04b95c7991c596a4af5c7f471e00e884d8
|
|
5
5
|
brand: maxy-code
|
|
6
6
|
product-name: Maxy
|
|
7
7
|
---
|
|
@@ -2763,7 +2763,7 @@ either is a regression.
|
|
|
2763
2763
|
|
|
2764
2764
|
**`/chat` Claude-desktop transcript presentation.** The admin webchat keeps the shared `Transcript` shell (stream, follow-tail) but injects a /chat-only item renderer via the component's optional `renderItems` prop: `renderChatTimeline` in `app/chat/transcript-render.tsx`. Presentation: operator turns render as a right-aligned grey bubble showing the message text and, beneath it, a subtle always-visible time-of-day stamp (no label); delivered agent replies render as plain prose with the same time-of-day stamp beneath (the reply-document filename line stays, as prose); the stamp is HH:MM from the turn's `ts` (locale-formatted, right-aligned inside the operator bubble, left-aligned under agent prose), and a turn whose `ts` is null/unparseable shows no stamp, never an empty line — so only delivered operator and agent-reply turns are stamped, while tool runs, the collapsed "Thinking" block, and the agent-error banner stay time-free; a maximal consecutive run of `tool-call`/`tool-result` turns renders as one collapsed grey one-liner ("Used N tools ›") whose expansion shows every call and result payload — a lone call still gets the one-liner, and prose or a directive row ends a run. The WhatsApp/operator reader (`/whatsapp`, `OperatorConversations`) omits the prop and keeps the default `renderTimeline`, so its bubble/DM chrome is unchanged — except that since the attachment-render extraction the default renderer draws inbound `operator-inbound` uploads and `agent-file` deliveries inline (image thumbnail / pdf.js preview / voice-note player / download chip) via the attachment renderer shared with `/chat`, extracted to `app/chat/attachment-render.tsx` to break the cycle with `transcript-render.tsx`; `renderChatTimeline` is untouched. Both presentations are test-pinned (`app/whatsapp/__tests__/Transcript-*.test.tsx`, `app/chat/__tests__/transcript-render.test.tsx`, `app/chat/__tests__/pdf-preview.test.tsx`). **Day-divider:** both renderers insert a centered `.day-divider` row between two consecutive timeline items on different local calendar days (label `Today`/`Yesterday`, else `Sat 14 Jun 2026`), so a thread spanning midnight is never an ambiguous run of HH:MM; the first dated item gets a leading divider, a null/unparseable `ts` marks no boundary, dividers are computed over the filtered `visibleItems`, and in `/chat` a day crossover flushes any open tool/think run first so a collapsed run never spans it. Per-bubble HH:MM stamps are unchanged. Shared helpers `dayKey`/`dayLabel`/`itemTs`/`DayDivider` live in `app/whatsapp/Transcript.tsx`.
|
|
2765
2765
|
|
|
2766
|
-
**WhatsApp reader is read/write (WhatsApp-Web surface).** A WhatsApp store conversation in the operator Conversations reader (`OperatorConversations`) and the admin root channel viewer (`app/page.tsx`) no longer opens the plain read-only DM chrome — it opens `app/whatsapp/WhatsappWebConversation.tsx`, a WhatsApp-Web-styled surface: a conversation header, the delivered-only store transcript reskinned green/white under a `.wa-web` root class, and a reply composer (`app/whatsapp/ReplyComposer.tsx`). The operator can type text, insert emoji (a codepoint-data picker — no OS input, so the `check-no-emoji-in-jsx` gate stays green), record a voice note (MediaRecorder → the existing audio send path), attach a file **from device storage** (a modal browsing the DATA_ROOT tree via `GET /api/admin/files`, sent by reference — the OS file dialog is no longer the reader's picker), reply to a specific message (a per-bubble affordance that quotes the original in the composer and threads natively on WhatsApp), and search the loaded transcript (a header control that filters to matching turns and highlights the match). The header carries only that Search control — the former Video / Phone / menu icons are gone, because no call or presence data exists behind them. Sent files honour the webchat allowlist (`SUPPORTED_MIME_SET` / 50 MB / 5 files); a device-file reference is read server-side under the same account-scope guard the `/data` download route applies. **Send is reply-only and DM-only:** `POST /api/whatsapp-reader/reply` (`requireAdminSession`) refuses any target with no existing stored conversation (`reason=no-existing-conversation`, HTTP 403) — a cold first-contact send bans the account, so the composer is never a cold-contact path, enforced server-side — and refuses `@g.us` groups (`reason=group-send-blocked`), matching the agent's observe-only group invariant; a group thread shows a read-only note in place of the composer. The endpoint is the sole store writer for a manual send: right after each send resolves it appends the record itself, stamped `origin:'operator-manual'` (attributed "You", distinct from an assistant reply), because the Baileys echo that normally persists the store is gated on message text (`manager.ts:906`) and would leave an uncaptioned media send delivered-but-invisible. The record's `messageId` matches the echo's derivation so the async text echo's own persist dedups to a no-op; the send is confirmed by re-read (`op=recorded present=<bool>`). A sent send does not spawn an assistant turn (echo dispatch stays suppressed). Attachments deliver as WhatsApp media/documents and render **inline** in the store reader (image thumbnail / pdf preview / audio player / download chip), because the manual-send path records the message with the servable `attachmentId`. The `.wa-web` skin is scoped to WhatsApp store threads only, so `/chat`, the admin agent session view, and the public visitor webchat are unchanged; the visitor webchat gains no send capability. The in-bubble delivered-tick is cosmetic (no read-receipt data is stored); inbound bubbles keep their sender name so group threads stay attributable, and both directions carry a WhatsApp callout tail (`.wa-web`-scoped only). Non-WhatsApp reader rows (Telegram/webchat session JSONLs) keep the plain read-only `Transcript`. Observability rides `[wa-manual-send]` lines correlated by a per-request `sendId`.
|
|
2766
|
+
**WhatsApp reader is read/write (WhatsApp-Web surface).** A WhatsApp store conversation in the operator Conversations reader (`OperatorConversations`) and the admin root channel viewer (`app/page.tsx`) no longer opens the plain read-only DM chrome — it opens `app/whatsapp/WhatsappWebConversation.tsx`, a WhatsApp-Web-styled surface: a conversation header, the delivered-only store transcript reskinned green/white under a `.wa-web` root class, and a reply composer (`app/whatsapp/ReplyComposer.tsx`). The operator can type text, insert emoji (a codepoint-data picker — no OS input, so the `check-no-emoji-in-jsx` gate stays green), record a voice note (MediaRecorder → the existing audio send path), attach a file **from device storage** (a modal browsing the DATA_ROOT tree via `GET /api/admin/files`, sent by reference — the OS file dialog is no longer the reader's picker), reply to a specific message (a per-bubble affordance that quotes the original in the composer and threads natively on WhatsApp), and search the loaded transcript (a header control that filters to matching turns and highlights the match). The header carries only that Search control — the former Video / Phone / menu icons are gone, because no call or presence data exists behind them. Sent files honour the webchat allowlist (`SUPPORTED_MIME_SET` / 50 MB / 5 files); a device-file reference is read server-side under the same account-scope guard the `/data` download route applies. **Send is reply-only and DM-only:** `POST /api/whatsapp-reader/reply` (`requireAdminSession`) refuses any target with no existing stored conversation (`reason=no-existing-conversation`, HTTP 403) — a cold first-contact send bans the account, so the composer is never a cold-contact path, enforced server-side — and refuses `@g.us` groups (`reason=group-send-blocked`), matching the agent's observe-only group invariant; a group thread shows a read-only note in place of the composer. The endpoint is the sole store writer for a manual send: right after each send resolves it appends the record itself, stamped `origin:'operator-manual'` (attributed "You", distinct from an assistant reply), because the Baileys echo that normally persists the store is gated on message text (`manager.ts:906`) and would leave an uncaptioned media send delivered-but-invisible. The record's `messageId` matches the echo's derivation so the async text echo's own persist dedups to a no-op; the send is confirmed by re-read (`op=recorded present=<bool>`). A sent send does not spawn an assistant turn (echo dispatch stays suppressed). Attachments deliver as WhatsApp media/documents and render **inline** in the store reader (image thumbnail / pdf preview / audio player / download chip), because the manual-send path records the message with the servable `attachmentId`. The `.wa-web` skin is scoped to WhatsApp store threads only, so `/chat`, the admin agent session view, and the public visitor webchat are unchanged; the visitor webchat gains no send capability. The in-bubble delivered-tick is cosmetic (no read-receipt data is stored); inbound bubbles keep their sender name so group threads stay attributable, and both directions carry a WhatsApp callout tail (`.wa-web`-scoped only). Non-WhatsApp reader rows (Telegram/webchat session JSONLs) keep the plain read-only `Transcript`. Observability rides `[wa-manual-send]` lines correlated by a per-request `sendId`. **Composer sizing matches the webchat composer:** the reply textarea auto-grows to fit multi-line content (measured from `scrollHeight`, clamped to a one-row minimum and a `viewport/3` maximum, scrollable past the cap with the scrollbar hidden), and a top-border grab handle drags (pointer) or nudges (ArrowUp/ArrowDown, keyboard-focusable) to an explicit height in the same clamp; a manual drag pins the height and overrides auto-grow until the field is cleared, which releases it back to auto-grow. A completed manual resize logs `[operator-ui] op=wa-composer-resize mode=drag|key h=<px>`; a bare click on the handle (no drag) logs nothing.
|
|
2767
2767
|
|
|
2768
2768
|
**`/chat` live activity line.** While a turn is in flight the transcript tail shows one ephemeral `ChatActivity` line (mounted only while `busy`, admin/operator only). It tracks real activity, not a timer: while a `Task` subagent runs it shows that subagent's headline (`agentType · description`, prefixed `N agents ·` when ≥2 are concurrent), sourced from `agent-<hex>.meta.json` via named `activity` SSE events the admin reader pushes from the session's `subagents/` dir; with no subagent active it shows a neutral word that advances only on a real turn arrival. The line carries a turn-elapsed clock and flips to a `stalled` state once nothing has been written for 5 minutes (`now − lastEmitAt`), so the operator can tell a wedge from progress without SSH. It is never added to the persisted timeline. Detail and the `[webchat-activity]` observability live in [`admin-webchat-native-channel.md`](../../../.docs/admin-webchat-native-channel.md).
|
|
2769
2769
|
|
|
@@ -4720,6 +4720,41 @@ Maxy has access to all platform logs and can filter them for you.
|
|
|
4720
4720
|
|
|
4721
4721
|
---
|
|
4722
4722
|
|
|
4723
|
+
## Box went unreachable, suspected memory exhaustion
|
|
4724
|
+
|
|
4725
|
+
**Symptom:** The Pi stopped responding to everything (no web, no SSH, no ping) and needed a power-cycle. This is the signature of memory exhaustion: the box fills RAM and swap, then wedges while the kernel tries to free memory.
|
|
4726
|
+
|
|
4727
|
+
**What protects against it:** Every install runs a standing memory audit every 30 seconds, zram compressed swap so pressure hits RAM instead of the SD card, and earlyoom, which kills the single largest process before the kernel freezes the box. A kill shows up as one service restarting, not a whole-box outage, and SSH is protected so the box stays reachable.
|
|
4728
|
+
|
|
4729
|
+
**Check the approach to OOM after a reboot.** The audit log survives reboot. Read it:
|
|
4730
|
+
|
|
4731
|
+
```
|
|
4732
|
+
~/<install>/platform/scripts/logs-read.sh --tail mem 100
|
|
4733
|
+
```
|
|
4734
|
+
|
|
4735
|
+
Replace `<install>` with the install directory (for example `realagent-code`). Then:
|
|
4736
|
+
|
|
4737
|
+
- Look for `op=mem-pressure` lines. They fire when available memory crosses the low-water mark, before any kill, and name the heaviest service (`topSlice`).
|
|
4738
|
+
- A single service's reading (`ptys`, `neo4j`, `brand`, `edge`, `vnc`) rising over time while the others stay flat is a memory leak in that service.
|
|
4739
|
+
- `op=mem-sample` lines that read `na` for the per-service fields are normal until the kernel memory controller is enabled on that box; available memory, swap, and the pressure warning still work.
|
|
4740
|
+
|
|
4741
|
+
**Confirm a kill happened and the service came back:**
|
|
4742
|
+
|
|
4743
|
+
```
|
|
4744
|
+
journalctl -t earlyoom --since -1h
|
|
4745
|
+
journalctl --user -u <hostname>.service --since -1h | grep -iE "Started|Scheduled restart"
|
|
4746
|
+
```
|
|
4747
|
+
|
|
4748
|
+
**Check whether the kernel OOM-killer fired anyway** (it should not, if earlyoom pre-empted it):
|
|
4749
|
+
|
|
4750
|
+
```
|
|
4751
|
+
journalctl -b -1 -k | grep -i oom
|
|
4752
|
+
```
|
|
4753
|
+
|
|
4754
|
+
A kernel `invoked oom-killer` line means the mitigation did not pre-empt the kernel on that occasion. A gap in the `op=mem-sample` timestamps means the sampler itself stopped. Both are worth escalating.
|
|
4755
|
+
|
|
4756
|
+
---
|
|
4757
|
+
|
|
4723
4758
|
## Cloudflare Tunnel Down (Remote Access Broken)
|
|
4724
4759
|
|
|
4725
4760
|
**Symptom:** You can reach Maxy on your local network but not via your public domain.
|
|
@@ -75,7 +75,7 @@ either is a regression.
|
|
|
75
75
|
|
|
76
76
|
**`/chat` Claude-desktop transcript presentation.** The admin webchat keeps the shared `Transcript` shell (stream, follow-tail) but injects a /chat-only item renderer via the component's optional `renderItems` prop: `renderChatTimeline` in `app/chat/transcript-render.tsx`. Presentation: operator turns render as a right-aligned grey bubble showing the message text and, beneath it, a subtle always-visible time-of-day stamp (no label); delivered agent replies render as plain prose with the same time-of-day stamp beneath (the reply-document filename line stays, as prose); the stamp is HH:MM from the turn's `ts` (locale-formatted, right-aligned inside the operator bubble, left-aligned under agent prose), and a turn whose `ts` is null/unparseable shows no stamp, never an empty line — so only delivered operator and agent-reply turns are stamped, while tool runs, the collapsed "Thinking" block, and the agent-error banner stay time-free; a maximal consecutive run of `tool-call`/`tool-result` turns renders as one collapsed grey one-liner ("Used N tools ›") whose expansion shows every call and result payload — a lone call still gets the one-liner, and prose or a directive row ends a run. The WhatsApp/operator reader (`/whatsapp`, `OperatorConversations`) omits the prop and keeps the default `renderTimeline`, so its bubble/DM chrome is unchanged — except that since the attachment-render extraction the default renderer draws inbound `operator-inbound` uploads and `agent-file` deliveries inline (image thumbnail / pdf.js preview / voice-note player / download chip) via the attachment renderer shared with `/chat`, extracted to `app/chat/attachment-render.tsx` to break the cycle with `transcript-render.tsx`; `renderChatTimeline` is untouched. Both presentations are test-pinned (`app/whatsapp/__tests__/Transcript-*.test.tsx`, `app/chat/__tests__/transcript-render.test.tsx`, `app/chat/__tests__/pdf-preview.test.tsx`). **Day-divider:** both renderers insert a centered `.day-divider` row between two consecutive timeline items on different local calendar days (label `Today`/`Yesterday`, else `Sat 14 Jun 2026`), so a thread spanning midnight is never an ambiguous run of HH:MM; the first dated item gets a leading divider, a null/unparseable `ts` marks no boundary, dividers are computed over the filtered `visibleItems`, and in `/chat` a day crossover flushes any open tool/think run first so a collapsed run never spans it. Per-bubble HH:MM stamps are unchanged. Shared helpers `dayKey`/`dayLabel`/`itemTs`/`DayDivider` live in `app/whatsapp/Transcript.tsx`.
|
|
77
77
|
|
|
78
|
-
**WhatsApp reader is read/write (WhatsApp-Web surface).** A WhatsApp store conversation in the operator Conversations reader (`OperatorConversations`) and the admin root channel viewer (`app/page.tsx`) no longer opens the plain read-only DM chrome — it opens `app/whatsapp/WhatsappWebConversation.tsx`, a WhatsApp-Web-styled surface: a conversation header, the delivered-only store transcript reskinned green/white under a `.wa-web` root class, and a reply composer (`app/whatsapp/ReplyComposer.tsx`). The operator can type text, insert emoji (a codepoint-data picker — no OS input, so the `check-no-emoji-in-jsx` gate stays green), record a voice note (MediaRecorder → the existing audio send path), attach a file **from device storage** (a modal browsing the DATA_ROOT tree via `GET /api/admin/files`, sent by reference — the OS file dialog is no longer the reader's picker), reply to a specific message (a per-bubble affordance that quotes the original in the composer and threads natively on WhatsApp), and search the loaded transcript (a header control that filters to matching turns and highlights the match). The header carries only that Search control — the former Video / Phone / menu icons are gone, because no call or presence data exists behind them. Sent files honour the webchat allowlist (`SUPPORTED_MIME_SET` / 50 MB / 5 files); a device-file reference is read server-side under the same account-scope guard the `/data` download route applies. **Send is reply-only and DM-only:** `POST /api/whatsapp-reader/reply` (`requireAdminSession`) refuses any target with no existing stored conversation (`reason=no-existing-conversation`, HTTP 403) — a cold first-contact send bans the account, so the composer is never a cold-contact path, enforced server-side — and refuses `@g.us` groups (`reason=group-send-blocked`), matching the agent's observe-only group invariant; a group thread shows a read-only note in place of the composer. The endpoint is the sole store writer for a manual send: right after each send resolves it appends the record itself, stamped `origin:'operator-manual'` (attributed "You", distinct from an assistant reply), because the Baileys echo that normally persists the store is gated on message text (`manager.ts:906`) and would leave an uncaptioned media send delivered-but-invisible. The record's `messageId` matches the echo's derivation so the async text echo's own persist dedups to a no-op; the send is confirmed by re-read (`op=recorded present=<bool>`). A sent send does not spawn an assistant turn (echo dispatch stays suppressed). Attachments deliver as WhatsApp media/documents and render **inline** in the store reader (image thumbnail / pdf preview / audio player / download chip), because the manual-send path records the message with the servable `attachmentId`. The `.wa-web` skin is scoped to WhatsApp store threads only, so `/chat`, the admin agent session view, and the public visitor webchat are unchanged; the visitor webchat gains no send capability. The in-bubble delivered-tick is cosmetic (no read-receipt data is stored); inbound bubbles keep their sender name so group threads stay attributable, and both directions carry a WhatsApp callout tail (`.wa-web`-scoped only). Non-WhatsApp reader rows (Telegram/webchat session JSONLs) keep the plain read-only `Transcript`. Observability rides `[wa-manual-send]` lines correlated by a per-request `sendId`.
|
|
78
|
+
**WhatsApp reader is read/write (WhatsApp-Web surface).** A WhatsApp store conversation in the operator Conversations reader (`OperatorConversations`) and the admin root channel viewer (`app/page.tsx`) no longer opens the plain read-only DM chrome — it opens `app/whatsapp/WhatsappWebConversation.tsx`, a WhatsApp-Web-styled surface: a conversation header, the delivered-only store transcript reskinned green/white under a `.wa-web` root class, and a reply composer (`app/whatsapp/ReplyComposer.tsx`). The operator can type text, insert emoji (a codepoint-data picker — no OS input, so the `check-no-emoji-in-jsx` gate stays green), record a voice note (MediaRecorder → the existing audio send path), attach a file **from device storage** (a modal browsing the DATA_ROOT tree via `GET /api/admin/files`, sent by reference — the OS file dialog is no longer the reader's picker), reply to a specific message (a per-bubble affordance that quotes the original in the composer and threads natively on WhatsApp), and search the loaded transcript (a header control that filters to matching turns and highlights the match). The header carries only that Search control — the former Video / Phone / menu icons are gone, because no call or presence data exists behind them. Sent files honour the webchat allowlist (`SUPPORTED_MIME_SET` / 50 MB / 5 files); a device-file reference is read server-side under the same account-scope guard the `/data` download route applies. **Send is reply-only and DM-only:** `POST /api/whatsapp-reader/reply` (`requireAdminSession`) refuses any target with no existing stored conversation (`reason=no-existing-conversation`, HTTP 403) — a cold first-contact send bans the account, so the composer is never a cold-contact path, enforced server-side — and refuses `@g.us` groups (`reason=group-send-blocked`), matching the agent's observe-only group invariant; a group thread shows a read-only note in place of the composer. The endpoint is the sole store writer for a manual send: right after each send resolves it appends the record itself, stamped `origin:'operator-manual'` (attributed "You", distinct from an assistant reply), because the Baileys echo that normally persists the store is gated on message text (`manager.ts:906`) and would leave an uncaptioned media send delivered-but-invisible. The record's `messageId` matches the echo's derivation so the async text echo's own persist dedups to a no-op; the send is confirmed by re-read (`op=recorded present=<bool>`). A sent send does not spawn an assistant turn (echo dispatch stays suppressed). Attachments deliver as WhatsApp media/documents and render **inline** in the store reader (image thumbnail / pdf preview / audio player / download chip), because the manual-send path records the message with the servable `attachmentId`. The `.wa-web` skin is scoped to WhatsApp store threads only, so `/chat`, the admin agent session view, and the public visitor webchat are unchanged; the visitor webchat gains no send capability. The in-bubble delivered-tick is cosmetic (no read-receipt data is stored); inbound bubbles keep their sender name so group threads stay attributable, and both directions carry a WhatsApp callout tail (`.wa-web`-scoped only). Non-WhatsApp reader rows (Telegram/webchat session JSONLs) keep the plain read-only `Transcript`. Observability rides `[wa-manual-send]` lines correlated by a per-request `sendId`. **Composer sizing matches the webchat composer:** the reply textarea auto-grows to fit multi-line content (measured from `scrollHeight`, clamped to a one-row minimum and a `viewport/3` maximum, scrollable past the cap with the scrollbar hidden), and a top-border grab handle drags (pointer) or nudges (ArrowUp/ArrowDown, keyboard-focusable) to an explicit height in the same clamp; a manual drag pins the height and overrides auto-grow until the field is cleared, which releases it back to auto-grow. A completed manual resize logs `[operator-ui] op=wa-composer-resize mode=drag|key h=<px>`; a bare click on the handle (no drag) logs nothing.
|
|
79
79
|
|
|
80
80
|
**`/chat` live activity line.** While a turn is in flight the transcript tail shows one ephemeral `ChatActivity` line (mounted only while `busy`, admin/operator only). It tracks real activity, not a timer: while a `Task` subagent runs it shows that subagent's headline (`agentType · description`, prefixed `N agents ·` when ≥2 are concurrent), sourced from `agent-<hex>.meta.json` via named `activity` SSE events the admin reader pushes from the session's `subagents/` dir; with no subagent active it shows a neutral word that advances only on a real turn arrival. The line carries a turn-elapsed clock and flips to a `stalled` state once nothing has been written for 5 minutes (`now − lastEmitAt`), so the operator can tell a wedge from progress without SSH. It is never added to the persisted timeline. Detail and the `[webchat-activity]` observability live in [`admin-webchat-native-channel.md`](../../../.docs/admin-webchat-native-channel.md).
|
|
81
81
|
|
|
@@ -224,6 +224,41 @@ For specific plugin logs: "Show Telegram logs" or "Show contacts plugin logs."
|
|
|
224
224
|
|
|
225
225
|
---
|
|
226
226
|
|
|
227
|
+
## Box went unreachable, suspected memory exhaustion
|
|
228
|
+
|
|
229
|
+
**Symptom:** The Pi stopped responding to everything (no web, no SSH, no ping) and needed a power-cycle. This is the signature of memory exhaustion: the box fills RAM and swap, then wedges while the kernel tries to free memory.
|
|
230
|
+
|
|
231
|
+
**What protects against it:** Every install runs a standing memory audit every 30 seconds, zram compressed swap so pressure hits RAM instead of the SD card, and earlyoom, which kills the single largest process before the kernel freezes the box. A kill shows up as one service restarting, not a whole-box outage, and SSH is protected so the box stays reachable.
|
|
232
|
+
|
|
233
|
+
**Check the approach to OOM after a reboot.** The audit log survives reboot. Read it:
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
~/<install>/platform/scripts/logs-read.sh --tail mem 100
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Replace `<install>` with the install directory (for example `realagent-code`). Then:
|
|
240
|
+
|
|
241
|
+
- Look for `op=mem-pressure` lines. They fire when available memory crosses the low-water mark, before any kill, and name the heaviest service (`topSlice`).
|
|
242
|
+
- A single service's reading (`ptys`, `neo4j`, `brand`, `edge`, `vnc`) rising over time while the others stay flat is a memory leak in that service.
|
|
243
|
+
- `op=mem-sample` lines that read `na` for the per-service fields are normal until the kernel memory controller is enabled on that box; available memory, swap, and the pressure warning still work.
|
|
244
|
+
|
|
245
|
+
**Confirm a kill happened and the service came back:**
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
journalctl -t earlyoom --since -1h
|
|
249
|
+
journalctl --user -u <hostname>.service --since -1h | grep -iE "Started|Scheduled restart"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**Check whether the kernel OOM-killer fired anyway** (it should not, if earlyoom pre-empted it):
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
journalctl -b -1 -k | grep -i oom
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
A kernel `invoked oom-killer` line means the mitigation did not pre-empt the kernel on that occasion. A gap in the `op=mem-sample` timestamps means the sampler itself stopped. Both are worth escalating.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
227
262
|
## Cloudflare Tunnel Down (Remote Access Broken)
|
|
228
263
|
|
|
229
264
|
**Symptom:** You can reach {{productName}} on your local network but not via your public domain.
|
|
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { dirname, resolve } from "node:path";
|
|
6
|
-
import { _internals, startDeviceCode, pollDeviceToken } from "../auth/device-flow.js";
|
|
6
|
+
import { _internals, startDeviceCode, pollDeviceToken, parseAzureTokenError } from "../auth/device-flow.js";
|
|
7
7
|
function mockFetch(handler) {
|
|
8
8
|
const calls = [];
|
|
9
9
|
const original = globalThis.fetch;
|
|
@@ -75,15 +75,48 @@ test("pollDeviceToken maps slow_down / expired_token / access_denied", async ()
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
});
|
|
78
|
-
test("pollDeviceToken
|
|
79
|
-
const m = mockFetch(() => json({
|
|
78
|
+
test("pollDeviceToken returns a structured error (not a throw) on an unrecognised error", async () => {
|
|
79
|
+
const m = mockFetch(() => json({
|
|
80
|
+
error: "invalid_grant",
|
|
81
|
+
error_description: "AADSTS70008: The provided authorization code or refresh token has expired due to inactivity.",
|
|
82
|
+
error_codes: [70008],
|
|
83
|
+
suberror: "token_expired",
|
|
84
|
+
correlation_id: "corr-1",
|
|
85
|
+
trace_id: "trace-1",
|
|
86
|
+
timestamp: "2026-07-13 16:10:17Z",
|
|
87
|
+
}, 400));
|
|
80
88
|
try {
|
|
81
|
-
await
|
|
89
|
+
const out = await pollDeviceToken({ clientId: "c", tenantId: "common" }, "D");
|
|
90
|
+
assert.equal(out.kind, "error");
|
|
91
|
+
if (out.kind === "error") {
|
|
92
|
+
assert.equal(out.httpStatus, 400);
|
|
93
|
+
assert.equal(out.error, "invalid_grant");
|
|
94
|
+
assert.equal(out.aadsts, "AADSTS70008");
|
|
95
|
+
assert.equal(out.suberror, "token_expired");
|
|
96
|
+
assert.equal(out.correlationId, "corr-1");
|
|
97
|
+
assert.equal(out.traceId, "trace-1");
|
|
98
|
+
assert.match(out.errorDescription, /expired due to inactivity/);
|
|
99
|
+
}
|
|
82
100
|
}
|
|
83
101
|
finally {
|
|
84
102
|
m.restore();
|
|
85
103
|
}
|
|
86
104
|
});
|
|
105
|
+
test("parseAzureTokenError recovers the AADSTS code from the description when error_codes is absent", () => {
|
|
106
|
+
const e = parseAzureTokenError(400, JSON.stringify({
|
|
107
|
+
error: "invalid_grant",
|
|
108
|
+
error_description: "AADSTS50011: The redirect URI does not match.",
|
|
109
|
+
}));
|
|
110
|
+
assert.equal(e.aadsts, "AADSTS50011");
|
|
111
|
+
assert.equal(e.error, "invalid_grant");
|
|
112
|
+
});
|
|
113
|
+
test("parseAzureTokenError keeps a non-JSON body in raw without throwing", () => {
|
|
114
|
+
const e = parseAzureTokenError(502, "<html>Bad Gateway</html>");
|
|
115
|
+
assert.equal(e.kind, "error");
|
|
116
|
+
assert.equal(e.httpStatus, 502);
|
|
117
|
+
assert.equal(e.aadsts, null);
|
|
118
|
+
assert.equal(e.raw, "<html>Bad Gateway</html>");
|
|
119
|
+
});
|
|
87
120
|
test("pollDeviceToken 200 -> success with tokenResponse + scopes", async () => {
|
|
88
121
|
const m = mockFetch(() => json({
|
|
89
122
|
access_token: "AT", refresh_token: "RT", expires_in: 3600, token_type: "Bearer",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device-flow.test.js","sourceRoot":"","sources":["../../src/__tests__/device-flow.test.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"device-flow.test.js","sourceRoot":"","sources":["../../src/__tests__/device-flow.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -36,6 +36,22 @@ export declare function startDeviceCode(config: {
|
|
|
36
36
|
clientId: string;
|
|
37
37
|
tenantId: string;
|
|
38
38
|
}): Promise<DeviceCodeStart>;
|
|
39
|
+
export interface AzureTokenError {
|
|
40
|
+
kind: "error";
|
|
41
|
+
httpStatus: number;
|
|
42
|
+
error: string;
|
|
43
|
+
errorDescription: string;
|
|
44
|
+
/** The AADSTSnnnnn code, from `error_codes[0]` or parsed out of the description. */
|
|
45
|
+
aadsts: string | null;
|
|
46
|
+
/** OAuth sub-error (e.g. `token_expired`, `bad_token`, `consent_required`) — the
|
|
47
|
+
* field that distinguishes causes sharing one top-level `invalid_grant`. */
|
|
48
|
+
suberror: string | null;
|
|
49
|
+
correlationId: string | null;
|
|
50
|
+
traceId: string | null;
|
|
51
|
+
timestamp: string | null;
|
|
52
|
+
/** The verbatim response body — never discarded, so no diagnostic detail is lost. */
|
|
53
|
+
raw: string;
|
|
54
|
+
}
|
|
39
55
|
export type PollOutcome = {
|
|
40
56
|
kind: "pending";
|
|
41
57
|
} | {
|
|
@@ -48,11 +64,19 @@ export type PollOutcome = {
|
|
|
48
64
|
kind: "success";
|
|
49
65
|
tokenResponse: TokenResponse;
|
|
50
66
|
scopes: string[];
|
|
51
|
-
};
|
|
67
|
+
} | AzureTokenError;
|
|
68
|
+
/**
|
|
69
|
+
* Parse a Microsoft token-endpoint error body into structured fields. Every part
|
|
70
|
+
* Azure returns — AADSTS code, sub-error, description, correlation/trace IDs — is
|
|
71
|
+
* captured so a poll failure is diagnosable from the log without re-deriving it
|
|
72
|
+
* from an opaque string. A non-JSON body still yields a value: `raw` carries it.
|
|
73
|
+
*/
|
|
74
|
+
export declare function parseAzureTokenError(httpStatus: number, text: string): AzureTokenError;
|
|
52
75
|
/**
|
|
53
76
|
* One device-code token poll against `/oauth2/v2.0/token`. 200 yields tokens;
|
|
54
|
-
* the
|
|
55
|
-
*
|
|
77
|
+
* the four RFC 8628 polling states map to their kinds. Any other error is
|
|
78
|
+
* returned as a structured `AzureTokenError` (never thrown, never discarded) so
|
|
79
|
+
* the caller can log the full Microsoft reason instead of an opaque string.
|
|
56
80
|
*/
|
|
57
81
|
export declare function pollDeviceToken(config: {
|
|
58
82
|
clientId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device-flow.d.ts","sourceRoot":"","sources":["../../src/auth/device-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAwBH,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,uBAAuB,EAAE,MAAM,CAAC;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAWD;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,eAAe,CAAC,CA6B3B;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,aAAa,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"device-flow.d.ts","sourceRoot":"","sources":["../../src/auth/device-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAwBH,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,uBAAuB,EAAE,MAAM,CAAC;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAWD;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,eAAe,CAAC,CA6B3B;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,oFAAoF;IACpF,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB;iFAC6E;IAC7E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,qFAAqF;IACrF,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,aAAa,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,GACnE,eAAe,CAAC;AAEpB;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,CA6CtF;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EAC9C,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,WAAW,CAAC,CA+BtB;AAMD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,aAAa,CAAC,CAmBzB;AAED,eAAO,MAAM,UAAU;;CAAa,CAAC"}
|
|
@@ -60,10 +60,57 @@ export async function startDeviceCode(config) {
|
|
|
60
60
|
scopes: [...SCOPES],
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Parse a Microsoft token-endpoint error body into structured fields. Every part
|
|
65
|
+
* Azure returns — AADSTS code, sub-error, description, correlation/trace IDs — is
|
|
66
|
+
* captured so a poll failure is diagnosable from the log without re-deriving it
|
|
67
|
+
* from an opaque string. A non-JSON body still yields a value: `raw` carries it.
|
|
68
|
+
*/
|
|
69
|
+
export function parseAzureTokenError(httpStatus, text) {
|
|
70
|
+
let error = "";
|
|
71
|
+
let errorDescription = "";
|
|
72
|
+
let suberror = null;
|
|
73
|
+
let correlationId = null;
|
|
74
|
+
let traceId = null;
|
|
75
|
+
let timestamp = null;
|
|
76
|
+
let aadsts = null;
|
|
77
|
+
try {
|
|
78
|
+
const b = JSON.parse(text);
|
|
79
|
+
error = b.error ?? "";
|
|
80
|
+
errorDescription = b.error_description ?? "";
|
|
81
|
+
suberror = b.suberror ?? null;
|
|
82
|
+
correlationId = b.correlation_id ?? null;
|
|
83
|
+
traceId = b.trace_id ?? null;
|
|
84
|
+
timestamp = b.timestamp ?? null;
|
|
85
|
+
if (b.error_codes && b.error_codes.length > 0)
|
|
86
|
+
aadsts = `AADSTS${b.error_codes[0]}`;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Non-JSON body (gateway HTML, empty, etc.): keep the structured shape and let
|
|
90
|
+
// `raw` carry the body so the failure is still fully recorded.
|
|
91
|
+
}
|
|
92
|
+
if (!aadsts) {
|
|
93
|
+
const m = /AADSTS\d+/.exec(errorDescription || text);
|
|
94
|
+
aadsts = m ? m[0] : null;
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
kind: "error",
|
|
98
|
+
httpStatus,
|
|
99
|
+
error,
|
|
100
|
+
errorDescription,
|
|
101
|
+
aadsts,
|
|
102
|
+
suberror,
|
|
103
|
+
correlationId,
|
|
104
|
+
traceId,
|
|
105
|
+
timestamp,
|
|
106
|
+
raw: text,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
63
109
|
/**
|
|
64
110
|
* One device-code token poll against `/oauth2/v2.0/token`. 200 yields tokens;
|
|
65
|
-
* the
|
|
66
|
-
*
|
|
111
|
+
* the four RFC 8628 polling states map to their kinds. Any other error is
|
|
112
|
+
* returned as a structured `AzureTokenError` (never thrown, never discarded) so
|
|
113
|
+
* the caller can log the full Microsoft reason instead of an opaque string.
|
|
67
114
|
*/
|
|
68
115
|
export async function pollDeviceToken(config, deviceCode) {
|
|
69
116
|
const { tokenUrl } = buildUrls(config.tenantId);
|
|
@@ -83,14 +130,8 @@ export async function pollDeviceToken(config, deviceCode) {
|
|
|
83
130
|
return { kind: "success", tokenResponse, scopes };
|
|
84
131
|
}
|
|
85
132
|
const text = await response.text();
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
errorCode = JSON.parse(text).error ?? "";
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
errorCode = "";
|
|
92
|
-
}
|
|
93
|
-
switch (errorCode) {
|
|
133
|
+
const parsed = parseAzureTokenError(response.status, text);
|
|
134
|
+
switch (parsed.error) {
|
|
94
135
|
case "authorization_pending":
|
|
95
136
|
return { kind: "pending" };
|
|
96
137
|
case "slow_down":
|
|
@@ -100,7 +141,7 @@ export async function pollDeviceToken(config, deviceCode) {
|
|
|
100
141
|
case "access_denied":
|
|
101
142
|
return { kind: "denied" };
|
|
102
143
|
default:
|
|
103
|
-
|
|
144
|
+
return parsed;
|
|
104
145
|
}
|
|
105
146
|
}
|
|
106
147
|
// ---------------------------------------------------------------------------
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device-flow.js","sourceRoot":"","sources":["../../src/auth/device-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,MAAM,GAAG;IACb,gBAAgB;IAChB,WAAW;IACX,gBAAgB;IAChB,WAAW;IACX,qBAAqB;IACrB,eAAe;CAChB,CAAC;AAOF,SAAS,SAAS,CAAC,QAAgB;IACjC,MAAM,MAAM,GAAG,QAAQ,IAAI,QAAQ,CAAC;IACpC,OAAO;QACL,YAAY,EAAE,qCAAqC,MAAM,wBAAwB;QACjF,QAAQ,EAAE,qCAAqC,MAAM,oBAAoB;KAC1E,CAAC;AACJ,CAAC;AAiCD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,MAGrC;IACC,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,IAAI,QAAQ,CAAC;IAC3C,MAAM,GAAG,GAAG,qCAAqC,MAAM,yBAAyB,CAAC;IACjF,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;QACjC,SAAS,EAAE,MAAM,CAAC,QAAQ;QAC1B,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;KACxB,CAAC,CAAC;IACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAChC,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;QAChE,IAAI,EAAE,MAAM,CAAC,QAAQ,EAAE;KACxB,CAAC,CAAC;IACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CAAC,+BAA+B,QAAQ,CAAC,MAAM,MAAM,IAAI,EAAE,CAAC,CAAC;IAC9E,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAuB,CAAC;IAC3D,MAAM,uBAAuB,GAC3B,IAAI,CAAC,yBAAyB;QAC9B,GAAG,IAAI,CAAC,gBAAgB,QAAQ,IAAI,CAAC,SAAS,EAAE,CAAC;IACnD,OAAO;QACL,UAAU,EAAE,IAAI,CAAC,WAAW;QAC5B,QAAQ,EAAE,IAAI,CAAC,SAAS;QACxB,eAAe,EAAE,IAAI,CAAC,gBAAgB;QACtC,uBAAuB;QACvB,YAAY,EAAE,IAAI,CAAC,UAAU;QAC7B,WAAW,EAAE,IAAI,CAAC,QAAQ;QAC1B,MAAM,EAAE,CAAC,GAAG,MAAM,CAAC;KACpB,CAAC;AACJ,CAAC;
|
|
1
|
+
{"version":3,"file":"device-flow.js","sourceRoot":"","sources":["../../src/auth/device-flow.ts"],"names":[],"mappings":"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"}
|
|
@@ -161,7 +161,7 @@ lifelineTool(server, "outlook-account-register", "Start the OAuth device-code fl
|
|
|
161
161
|
return fail(`outlook-account-register failed: ${err.message}`);
|
|
162
162
|
}
|
|
163
163
|
});
|
|
164
|
-
lifelineTool(server, "outlook-account-register-poll", "Poll for completion of the device-code flow started by outlook-account-register. One check per call. Returns status: pending (wait intervalSec, then call again), registered (done — tokens persisted), expired (code timed out — restart with outlook-account-register),
|
|
164
|
+
lifelineTool(server, "outlook-account-register-poll", "Poll for completion of the device-code flow started by outlook-account-register. One check per call. Returns status: pending (wait intervalSec, then call again), registered (done — tokens persisted), expired (code timed out — restart with outlook-account-register), denied (operator declined), or error (Microsoft rejected the sign-in — message carries the AADSTS code + reason; a 4xx clears the pending code so you can restart, a 5xx keeps it for re-poll).", {}, async () => {
|
|
165
165
|
if (!ACCOUNT_ID)
|
|
166
166
|
return refuseNoAccount("outlook-account-register-poll");
|
|
167
167
|
try {
|