@tangle-network/agent-app 0.43.25 → 0.43.27
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/bin/preflight.mjs +47 -0
- package/dist/app-auth/index.d.ts +1 -1
- package/dist/app-auth/index.js +1 -1
- package/dist/assistant/index.d.ts +1 -0
- package/dist/assistant/index.js +2 -1
- package/dist/assistant/index.js.map +1 -1
- package/dist/chat-routes/index.d.ts +276 -0
- package/dist/chat-routes/index.js +564 -0
- package/dist/chat-routes/index.js.map +1 -0
- package/dist/chat-store/index.d.ts +3 -2
- package/dist/chat-store/index.js +7 -3
- package/dist/chat-store/index.js.map +1 -1
- package/dist/{chunk-4H77LX3V.js → chunk-5EQCITY3.js} +2 -20
- package/dist/chunk-5EQCITY3.js.map +1 -0
- package/dist/chunk-5SV5PSU7.js +80 -0
- package/dist/chunk-5SV5PSU7.js.map +1 -0
- package/dist/chunk-7LNGJDNA.js +1 -0
- package/dist/chunk-7LNGJDNA.js.map +1 -0
- package/dist/{chunk-77RLNLFP.js → chunk-ATRJULKZ.js} +41 -5
- package/dist/chunk-ATRJULKZ.js.map +1 -0
- package/dist/{chunk-PEPXQTJ3.js → chunk-FCQP75JT.js} +16 -5
- package/dist/chunk-FCQP75JT.js.map +1 -0
- package/dist/chunk-I2R2XT4M.js +62 -0
- package/dist/chunk-I2R2XT4M.js.map +1 -0
- package/dist/chunk-NYATNLRK.js +99 -0
- package/dist/chunk-NYATNLRK.js.map +1 -0
- package/dist/chunk-Q4TKVF3L.js +240 -0
- package/dist/chunk-Q4TKVF3L.js.map +1 -0
- package/dist/{chunk-AVBANQ67.js → chunk-TKVJE63N.js} +10 -1
- package/dist/{chunk-AVBANQ67.js.map → chunk-TKVJE63N.js.map} +1 -1
- package/dist/{chunk-U7DLCPJ6.js → chunk-Y4QHNQ75.js} +1 -1
- package/dist/core-7qIM7svy.d.ts +21 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +125 -104
- package/dist/interactions/index.js +2 -1
- package/dist/{parts-BeRnK54I.d.ts → parts-BcbitSNp.d.ts} +12 -21
- package/dist/platform/index.d.ts +1 -1
- package/dist/platform/index.js +3 -1
- package/dist/platform/index.js.map +1 -1
- package/dist/preflight/index.d.ts +141 -0
- package/dist/preflight/index.js +15 -0
- package/dist/preflight/index.js.map +1 -0
- package/dist/{sso-Df4wtL8D.d.ts → sso-CNOsARMJ.d.ts} +16 -1
- package/dist/stream/index.js +1 -1
- package/dist/teams-react/index.js +3 -3
- package/dist/theme-contract/cli.d.ts +1 -0
- package/dist/theme-contract/cli.js +79 -0
- package/dist/theme-contract/cli.js.map +1 -0
- package/dist/theme-contract/index.d.ts +90 -0
- package/dist/theme-contract/index.js +7 -0
- package/dist/theme-contract/index.js.map +1 -0
- package/dist/web-react/index.d.ts +24 -4
- package/dist/web-react/index.js +5 -1
- package/dist/wire-BaUF66AS.d.ts +61 -0
- package/package.json +20 -1
- package/dist/chunk-4H77LX3V.js.map +0 -1
- package/dist/chunk-77RLNLFP.js.map +0 -1
- package/dist/chunk-PEPXQTJ3.js.map +0 -1
- /package/dist/{chunk-U7DLCPJ6.js.map → chunk-Y4QHNQ75.js.map} +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/chat-store/parts.ts
|
|
2
|
+
function toChatMessageParts(parts) {
|
|
3
|
+
const out = [];
|
|
4
|
+
for (const part of parts) {
|
|
5
|
+
const typed = toChatMessagePart(part);
|
|
6
|
+
if (typed) out.push(typed);
|
|
7
|
+
}
|
|
8
|
+
return out;
|
|
9
|
+
}
|
|
10
|
+
var str = (value) => typeof value === "string";
|
|
11
|
+
function toChatMessagePart(part) {
|
|
12
|
+
if (!part || typeof part !== "object") return null;
|
|
13
|
+
const type = part.type;
|
|
14
|
+
switch (type) {
|
|
15
|
+
case "text":
|
|
16
|
+
case "reasoning":
|
|
17
|
+
return str(part.text) ? part : null;
|
|
18
|
+
case "tool":
|
|
19
|
+
return str(part.id) && str(part.tool) && part.state && typeof part.state === "object" ? part : null;
|
|
20
|
+
case "file":
|
|
21
|
+
case "image":
|
|
22
|
+
return part;
|
|
23
|
+
case "subtask":
|
|
24
|
+
return str(part.prompt) && str(part.description) && str(part.agent) ? part : null;
|
|
25
|
+
case "step-start":
|
|
26
|
+
return { type: "step-start" };
|
|
27
|
+
case "step-finish":
|
|
28
|
+
return part;
|
|
29
|
+
case "interaction":
|
|
30
|
+
return str(part.id) && str(part.kind) && str(part.title) && str(part.status) && part.answerSpec && typeof part.answerSpec === "object" ? part : null;
|
|
31
|
+
case "notice":
|
|
32
|
+
return str(part.id) && str(part.noticeKind) && str(part.text) ? part : null;
|
|
33
|
+
case void 0:
|
|
34
|
+
return null;
|
|
35
|
+
default: {
|
|
36
|
+
const _exhaustive = type;
|
|
37
|
+
void _exhaustive;
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function isChatToolPart(part) {
|
|
43
|
+
return part.type === "tool";
|
|
44
|
+
}
|
|
45
|
+
function isChatTextPart(part) {
|
|
46
|
+
return part.type === "text";
|
|
47
|
+
}
|
|
48
|
+
function isChatInteractionPart(part) {
|
|
49
|
+
return part.type === "interaction";
|
|
50
|
+
}
|
|
51
|
+
function isChatStepFinishPart(part) {
|
|
52
|
+
return part.type === "step-finish";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export {
|
|
56
|
+
toChatMessageParts,
|
|
57
|
+
isChatToolPart,
|
|
58
|
+
isChatTextPart,
|
|
59
|
+
isChatInteractionPart,
|
|
60
|
+
isChatStepFinishPart
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=chunk-I2R2XT4M.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/chat-store/parts.ts"],"sourcesContent":["/**\n * The stored shape of `message.parts` — one typed vocabulary for every part a\n * product persists into a chat transcript. NOT an ad-hoc union reverse-\n * engineered from product schemas; each member is matched field-for-field to\n * its canonical source:\n *\n * - `text` / `reasoning` / `tool`: the persisted projection `/stream`'s\n * `normalizePersistedPart` produces from the harness lane's\n * `message.part.updated` events (ADC sidecar\n * `apps/sidecar/src/events/session-events.ts:56` wraps the canonical part in\n * an `{id, sessionID, messageID}` envelope; the projection strips the\n * session/message ids and keeps the per-segment part id).\n * - `file` / `image` / `step-start` / `step-finish`: the sidecar's canonical\n * `MessagePartSchema` members (ADC\n * `apps/sidecar/src/schemas/agent-schemas.ts:50-154`); `step-finish` carries\n * the harness's per-step usage receipt — tokens\n * `{total, input, output, reasoning, cache{write, read}}` + `cost` — which is\n * also the shape the message-level token/cost columns mirror.\n * - `subtask`: `@tangle-network/agent-interface`'s `SubtaskPart` (a spawned\n * sub-agent task).\n * - `interaction` / `notice`: the persisted-part codecs in\n * `/web-react`'s chat-interactions contract (`interactionToPersistedPart`,\n * `noticePart`) — type-only imports, one source of truth for their statuses\n * and field shapes.\n *\n * `@tangle-network/agent-interface` exports the canonical wire `Part` union,\n * but its `PartBase` requires the `sessionID`/`messageID` stream envelope that\n * is deliberately NOT persisted, so the stored union is defined here as the\n * envelope-free projection (a type-level coverage check against the peer's\n * `Part['type']` lives in the tests). Contribute-down candidate: if\n * agent-interface grows envelope-free persisted-part types, re-export them\n * here and delete these definitions.\n *\n * Two transport lanes serialize into this SAME stored shape:\n * - harness lane: canonical `message.part.updated` parts, merged/normalized by\n * `/stream` (`mergePersistedPart`, `finalizeAssistantParts`);\n * - router/openai-compat lane: `text_delta`/`tool_call` stream events are\n * mapped INTO canonical part events first (`/runtime`'s `toLoopEvents` +\n * `/stream`'s `normalizeToolEvent`) and then persisted identically — the\n * store never sees a router-specific shape.\n */\n\nimport type { Part as HarnessWirePart } from '@tangle-network/agent-interface'\nimport type {\n ChatInteractionField,\n ChatInteractionStatus,\n InteractionPersistedPart,\n NoticeKind,\n NoticePersistedPart,\n} from '../web-react/chat-interactions'\n\n/** Start/end wall-clock millis, as normalized by `/stream`'s `normalizeTime`. */\nexport interface ChatPartTime {\n start?: number\n end?: number\n}\n\n/** `id` is the harness's per-segment identity; absent on legacy/router parts,\n * which collapse to a single logical text stream. Never invented client-side. */\nexport interface ChatTextPart {\n type: 'text'\n text: string\n id?: string\n}\n\nexport interface ChatReasoningPart {\n type: 'reasoning'\n text: string\n id?: string\n time?: ChatPartTime\n}\n\n/** Superset of the sidecar's status enum (`pending|running|completed|failed`)\n * and agent-interface's `ToolState` statuses; `error` is the persisted\n * terminal form `/stream`'s `normalizePersistedPart` settles on. */\nexport type ChatToolStatus = 'pending' | 'running' | 'completed' | 'error' | 'failed'\n\nexport interface ChatToolState {\n status: ChatToolStatus\n input?: unknown\n output?: unknown\n error?: string\n title?: string\n metadata?: Record<string, unknown>\n time?: ChatPartTime\n}\n\nexport interface ChatToolPart {\n type: 'tool'\n id: string\n tool: string\n callID?: string\n state: ChatToolState\n}\n\n/** Union of the sidecar's legacy (path-based) and AI-SDK (url-based) file\n * shapes; response-side every field besides `type` is optional. */\nexport interface ChatFilePart {\n type: 'file'\n id?: string\n filename?: string\n mediaType?: string\n url?: string\n path?: string\n content?: string\n}\n\nexport interface ChatImagePart {\n type: 'image'\n filename?: string\n mediaType?: string\n url?: string\n path?: string\n}\n\nexport interface ChatSubtaskPart {\n type: 'subtask'\n prompt: string\n description: string\n agent: string\n id?: string\n}\n\n/** OpenCode step-boundary marker — no renderable text; preserved so mappers\n * never coerce it into a \"[object Object]\" text part. */\nexport interface ChatStepStartPart {\n type: 'step-start'\n}\n\n/** Per-step usage receipt as the harness reports it (sidecar\n * `StepFinishPartSchema`). The message-level token/cost columns are this\n * shape flattened. */\nexport interface ChatUsageTokens {\n total?: number\n input?: number\n output?: number\n reasoning?: number\n cache?: {\n write?: number\n read?: number\n }\n}\n\nexport interface ChatStepFinishPart {\n type: 'step-finish'\n reason?: string\n tokens?: ChatUsageTokens\n cost?: number\n}\n\n/** Persisted human-in-the-loop ask — byte-matches\n * `interactionToPersistedPart` in `/web-react`'s chat-interactions contract. */\nexport interface ChatInteractionPart {\n type: 'interaction'\n id: string\n kind: string\n title: string\n body?: string\n answerSpec: { fields: ChatInteractionField[] }\n status: ChatInteractionStatus\n cancelReason?: string\n}\n\n/** Persisted one-line transcript notice — byte-matches `noticePart` in\n * `/web-react`'s chat-interactions contract. */\nexport interface ChatNoticePart {\n type: 'notice'\n id: string\n noticeKind: NoticeKind\n text: string\n}\n\n// The \"byte-matches\" claims above, enforced at compile time: the interaction\n// contract's codec output types and the stored part types must stay mutually\n// assignable, so a codec field added on one side without the other fails here.\ntype MutuallyAssignable<A extends B, B> = A\ntype _CodecEmitsStorableInteractionPart = MutuallyAssignable<InteractionPersistedPart, ChatInteractionPart>\ntype _StoredInteractionPartFeedsCodec = MutuallyAssignable<ChatInteractionPart, InteractionPersistedPart>\ntype _CodecEmitsStorableNoticePart = MutuallyAssignable<NoticePersistedPart, ChatNoticePart>\ntype _StoredNoticePartFeedsCodec = MutuallyAssignable<ChatNoticePart, NoticePersistedPart>\n\nexport type ChatMessagePart =\n | ChatTextPart\n | ChatReasoningPart\n | ChatToolPart\n | ChatFilePart\n | ChatImagePart\n | ChatSubtaskPart\n | ChatStepStartPart\n | ChatStepFinishPart\n | ChatInteractionPart\n | ChatNoticePart\n\n/** Every canonical harness wire-part kind must be storable — compile-time\n * guarantee that a new agent-interface part kind cannot silently fall out of\n * the persisted vocabulary. */\nexport type StorableHarnessPartKind = HarnessWirePart['type'] & ChatMessagePart['type']\n\n/**\n * The typed projection at the `/stream` → `/chat-store` boundary. The stream\n * normalizers (`normalizePersistedPart`/`mergePersistedPart`/\n * `finalizeAssistantParts`) deliberately produce untyped `JsonRecord`s — they\n * normalize wire shapes and do not own the stored vocabulary. THIS module\n * owns it, so this is where rows gain the `ChatMessagePart` type: each entry\n * is validated against its kind's required fields and narrowed, junk is\n * dropped, and — enforced by the exhaustiveness check below — no storable\n * kind can silently fall out (the step-finish/interaction trap).\n */\nexport function toChatMessageParts(parts: Array<Record<string, unknown>>): ChatMessagePart[] {\n const out: ChatMessagePart[] = []\n for (const part of parts) {\n const typed = toChatMessagePart(part)\n if (typed) out.push(typed)\n }\n return out\n}\n\nconst str = (value: unknown): value is string => typeof value === 'string'\n\nfunction toChatMessagePart(part: Record<string, unknown>): ChatMessagePart | null {\n if (!part || typeof part !== 'object') return null\n const type = part.type as ChatMessagePart['type'] | undefined\n switch (type) {\n case 'text':\n case 'reasoning':\n return str(part.text) ? (part as unknown as ChatTextPart | ChatReasoningPart) : null\n case 'tool':\n return str(part.id) && str(part.tool) && part.state && typeof part.state === 'object'\n ? (part as unknown as ChatToolPart)\n : null\n case 'file':\n case 'image':\n return part as unknown as ChatFilePart | ChatImagePart\n case 'subtask':\n return str(part.prompt) && str(part.description) && str(part.agent)\n ? (part as unknown as ChatSubtaskPart)\n : null\n case 'step-start':\n return { type: 'step-start' }\n case 'step-finish':\n return part as unknown as ChatStepFinishPart\n case 'interaction':\n return str(part.id) && str(part.kind) && str(part.title) && str(part.status) &&\n part.answerSpec && typeof part.answerSpec === 'object'\n ? (part as unknown as ChatInteractionPart)\n : null\n case 'notice':\n return str(part.id) && str(part.noticeKind) && str(part.text)\n ? (part as unknown as ChatNoticePart)\n : null\n case undefined:\n return null\n default: {\n // Compile-time exhaustiveness: a new ChatMessagePart kind that is not\n // handled above makes `type` non-never here and this line fails.\n const _exhaustive: never = type\n void _exhaustive\n return null\n }\n }\n}\n\nexport function isChatToolPart(part: ChatMessagePart): part is ChatToolPart {\n return part.type === 'tool'\n}\n\nexport function isChatTextPart(part: ChatMessagePart): part is ChatTextPart {\n return part.type === 'text'\n}\n\nexport function isChatInteractionPart(part: ChatMessagePart): part is ChatInteractionPart {\n return part.type === 'interaction'\n}\n\nexport function isChatStepFinishPart(part: ChatMessagePart): part is ChatStepFinishPart {\n return part.type === 'step-finish'\n}\n"],"mappings":";AAgNO,SAAS,mBAAmB,OAA0D;AAC3F,QAAM,MAAyB,CAAC;AAChC,aAAW,QAAQ,OAAO;AACxB,UAAM,QAAQ,kBAAkB,IAAI;AACpC,QAAI,MAAO,KAAI,KAAK,KAAK;AAAA,EAC3B;AACA,SAAO;AACT;AAEA,IAAM,MAAM,CAAC,UAAoC,OAAO,UAAU;AAElE,SAAS,kBAAkB,MAAuD;AAChF,MAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;AAC9C,QAAM,OAAO,KAAK;AAClB,UAAQ,MAAM;AAAA,IACZ,KAAK;AAAA,IACL,KAAK;AACH,aAAO,IAAI,KAAK,IAAI,IAAK,OAAuD;AAAA,IAClF,KAAK;AACH,aAAO,IAAI,KAAK,EAAE,KAAK,IAAI,KAAK,IAAI,KAAK,KAAK,SAAS,OAAO,KAAK,UAAU,WACxE,OACD;AAAA,IACN,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,IAAI,KAAK,MAAM,KAAK,IAAI,KAAK,WAAW,KAAK,IAAI,KAAK,KAAK,IAC7D,OACD;AAAA,IACN,KAAK;AACH,aAAO,EAAE,MAAM,aAAa;AAAA,IAC9B,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,IAAI,KAAK,EAAE,KAAK,IAAI,KAAK,IAAI,KAAK,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,MAAM,KACzE,KAAK,cAAc,OAAO,KAAK,eAAe,WAC3C,OACD;AAAA,IACN,KAAK;AACH,aAAO,IAAI,KAAK,EAAE,KAAK,IAAI,KAAK,UAAU,KAAK,IAAI,KAAK,IAAI,IACvD,OACD;AAAA,IACN,KAAK;AACH,aAAO;AAAA,IACT,SAAS;AAGP,YAAM,cAAqB;AAC3B,WAAK;AACL,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAEO,SAAS,eAAe,MAA6C;AAC1E,SAAO,KAAK,SAAS;AACvB;AAEO,SAAS,eAAe,MAA6C;AAC1E,SAAO,KAAK,SAAS;AACvB;AAEO,SAAS,sBAAsB,MAAoD;AACxF,SAAO,KAAK,SAAS;AACvB;AAEO,SAAS,qBAAqB,MAAmD;AACtF,SAAO,KAAK,SAAS;AACvB;","names":[]}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// src/theme-contract/index.ts
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
3
|
+
import { join, relative } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
var SOURCE_RE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
|
|
6
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "coverage"]);
|
|
7
|
+
var DANGEROUS_UTILITIES = [
|
|
8
|
+
{ suffix: "surface-container-highest", varName: "--secondary" },
|
|
9
|
+
{ suffix: "surface-container-high", varName: "--popover" },
|
|
10
|
+
{ suffix: "surface-container", varName: "--card" },
|
|
11
|
+
{ suffix: "card-foreground", varName: "--card-foreground" },
|
|
12
|
+
{ suffix: "popover-foreground", varName: "--popover-foreground" },
|
|
13
|
+
{ suffix: "card", varName: "--card" },
|
|
14
|
+
{ suffix: "popover", varName: "--popover" }
|
|
15
|
+
];
|
|
16
|
+
var UTILITY_PREFIXES = "bg|text|border|ring|fill|stroke";
|
|
17
|
+
function buildUtilityRe(suffix) {
|
|
18
|
+
return new RegExp(`(?<![\\w-])(?:${UTILITY_PREFIXES})-${suffix}(?![\\w-])`, "g");
|
|
19
|
+
}
|
|
20
|
+
function walkSources(dir) {
|
|
21
|
+
let entries;
|
|
22
|
+
try {
|
|
23
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
24
|
+
} catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
return entries.flatMap((e) => {
|
|
28
|
+
if (e.isDirectory()) return SKIP_DIRS.has(e.name) ? [] : walkSources(join(dir, e.name));
|
|
29
|
+
return SOURCE_RE.test(e.name) && !e.name.endsWith(".d.ts") ? [join(dir, e.name)] : [];
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function definedVars(cssFiles) {
|
|
33
|
+
const defs = /* @__PURE__ */ new Set();
|
|
34
|
+
for (const file of cssFiles) {
|
|
35
|
+
let css;
|
|
36
|
+
try {
|
|
37
|
+
css = readFileSync(file, "utf8");
|
|
38
|
+
} catch {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
for (const m of css.matchAll(/^\s*(--[a-z0-9-]+)\s*:/gim)) if (m[1]) defs.add(m[1]);
|
|
42
|
+
}
|
|
43
|
+
return defs;
|
|
44
|
+
}
|
|
45
|
+
function defaultTokensCss() {
|
|
46
|
+
const candidates = ["../theme/tokens.css", "./theme/tokens.css"].map(
|
|
47
|
+
(rel) => fileURLToPath(new URL(rel, import.meta.url))
|
|
48
|
+
);
|
|
49
|
+
return candidates.find((p) => existsSync(p)) ?? candidates[0];
|
|
50
|
+
}
|
|
51
|
+
function checkThemeContract(opts) {
|
|
52
|
+
const tokensCss = opts.tokensCss ?? defaultTokensCss();
|
|
53
|
+
const defined = definedVars([tokensCss, ...opts.extraTokensCss ?? []]);
|
|
54
|
+
const allow = new Set(opts.allowlist ?? []);
|
|
55
|
+
const isDefined = (name) => defined.has(name) || allow.has(name);
|
|
56
|
+
const files = opts.srcDirs.flatMap(walkSources);
|
|
57
|
+
const utilityMatchers = DANGEROUS_UTILITIES.map((u) => ({ ...u, re: buildUtilityRe(u.suffix) }));
|
|
58
|
+
const seenVar = /* @__PURE__ */ new Map();
|
|
59
|
+
const seenUtility = /* @__PURE__ */ new Map();
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
let text;
|
|
62
|
+
try {
|
|
63
|
+
text = readFileSync(file, "utf8");
|
|
64
|
+
} catch {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const where = displayPath(file);
|
|
68
|
+
for (const m of text.matchAll(/var\(\s*(--[a-z0-9-]+)/gi)) {
|
|
69
|
+
const name = m[1];
|
|
70
|
+
if (!name || isDefined(name) || seenVar.has(name)) continue;
|
|
71
|
+
seenVar.set(name, where);
|
|
72
|
+
}
|
|
73
|
+
for (const u of utilityMatchers) {
|
|
74
|
+
if (isDefined(u.varName)) continue;
|
|
75
|
+
const key = `${u.varName}::${u.suffix}`;
|
|
76
|
+
if (seenUtility.has(key)) continue;
|
|
77
|
+
u.re.lastIndex = 0;
|
|
78
|
+
if (u.re.test(text)) seenUtility.set(key, `${where} (via ${firstUtilityHit(text, u.suffix)})`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const missing = [
|
|
82
|
+
...[...seenVar].map(([varName, referencedIn]) => ({ varName, referencedIn })),
|
|
83
|
+
...[...seenUtility].map(([key, referencedIn]) => ({ varName: key.split("::")[0], referencedIn }))
|
|
84
|
+
];
|
|
85
|
+
return { ok: missing.length === 0, missing };
|
|
86
|
+
}
|
|
87
|
+
function firstUtilityHit(text, suffix) {
|
|
88
|
+
const m = buildUtilityRe(suffix).exec(text);
|
|
89
|
+
return m?.[0] ?? `<utility>-${suffix}`;
|
|
90
|
+
}
|
|
91
|
+
function displayPath(file) {
|
|
92
|
+
const rel = relative(process.cwd(), file);
|
|
93
|
+
return rel && !rel.startsWith("..") ? rel : file;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export {
|
|
97
|
+
checkThemeContract
|
|
98
|
+
};
|
|
99
|
+
//# sourceMappingURL=chunk-NYATNLRK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/theme-contract/index.ts"],"sourcesContent":["/**\n * Exportable theme-token contract checker — the incident guard for the\n * invisible-popover class of bugs.\n *\n * The failure mode (tax-agent's transparent model dropdown; the whole\n * `bg-surface-container-*` family): a consumer app ships a component that\n * references a theme token — either as `var(--popover)` or as a Tailwind class\n * like `bg-surface-container-high` that the agent-app preset maps to\n * `hsl(var(--popover))` — but the app's OWN build never emits that custom\n * property (it forgot `import '@tangle-network/agent-app/styles'`, or dropped a\n * token in its local tokens.css). CSS resolves the missing var to nothing, the\n * surface paints transparent, and NOTHING errors. It ships invisible.\n *\n * `tests/theme/tokens-contract.test.ts` guards agent-app's OWN components. This\n * module lifts that walking logic into a function every CONSUMER app can run\n * against ITS OWN source in CI, comparing references to the tokens.css agent-app\n * ships plus any extra CSS the app defines.\n *\n * ── What each check covers (scope is deliberately honest) ────────────────────\n *\n * 1. var(--…) check — COMPLETE. Every `var(--name)` literal in the scanned\n * source (inline styles, `bg-[var(--name)]` arbitrary Tailwind values, CSS\n * template strings) is matched and compared against the defined token set.\n * This is exact: a `var(--x)` reference is unambiguous. It is a raw-text\n * scan (no AST), so a `var(--x)` written inside a comment or string literal\n * counts too — deliberate: it keeps the single-source logic identical to the\n * agent-app self-test, and a dangling `var(--x)` in a comment is a smell\n * worth surfacing. Suppress a deliberate one with `allowlist`.\n *\n * 2. Tailwind-utility check — INTENTIONALLY PARTIAL. Bare classes like\n * `bg-card` carry no `var(--)` and so are invisible to check 1; Tailwind\n * resolves them to `hsl(var(--card))` at build via the preset. Fully\n * resolving arbitrary Tailwind config is out of scope (it would mean\n * re-implementing Tailwind). Instead we check the SPECIFIC known-dangerous\n * families that have actually shipped invisible: the MD3 surface ladder\n * (`surface-container` / `-high` / `-highest`) and the `card` / `popover`\n * elevation pairs — exactly the utilities the agent-app tailwind-preset\n * registers onto elevation tokens (see src/theme/tailwind-preset.ts, the\n * source of truth for this mapping). The canvas/sequence aliases\n * (`--bg-input`, `--text-primary`, …) are consumed as `bg-[var(--…)]`\n * arbitrary values and so are already covered fully by check 1 — they need\n * no entry here.\n *\n * Node-only (reads the filesystem) → this lives in the `./theme-contract`\n * subpath, NOT `./theme`, which must stay browser-clean (it's in the\n * browser-safe manifest test).\n */\n\nimport { type Dirent, existsSync, readFileSync, readdirSync } from 'node:fs'\nimport { join, relative } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nexport interface ThemeContractOptions {\n /** Consumer source directories to scan for token references (recursively). */\n srcDirs: string[]\n /**\n * Path to the base tokens.css whose `--name:` definitions are the ground\n * truth. Defaults to the tokens.css agent-app ships (`./styles`) — the set a\n * consumer gets from `import '@tangle-network/agent-app/styles'`.\n */\n tokensCss?: string\n /**\n * Additional CSS files whose `--name:` definitions also count as defined —\n * the app's own overrides/extensions layered on top of the base tokens.\n */\n extraTokensCss?: string[]\n /**\n * Token names (e.g. `--my-app-accent`) to treat as always-defined, suppressing\n * them from the missing list. For app-specific vars defined outside any CSS\n * the checker can see (injected at runtime, from a third-party stylesheet, …).\n */\n allowlist?: string[]\n}\n\nexport interface ThemeContractMiss {\n /** The undefined custom property, e.g. `--popover`. */\n varName: string\n /**\n * Where it was referenced: `path/to/file.tsx`, or\n * `path/to/file.tsx (via bg-surface-container-high)` when the reference is a\n * Tailwind utility that resolves to the token rather than a literal var().\n */\n referencedIn: string\n}\n\nexport interface ThemeContractResult {\n ok: boolean\n missing: ThemeContractMiss[]\n}\n\n/** Source extensions scanned for token references. */\nconst SOURCE_RE = /\\.(ts|tsx|js|jsx|mjs|cjs)$/\nconst SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage'])\n\n/**\n * Known-dangerous Tailwind utility families and the elevation token each\n * resolves to, mirroring src/theme/tailwind-preset.ts. Ordered longest-suffix\n * first so `surface-container-highest` is matched before `surface-container`.\n * The negative look-around in {@link buildUtilityRe} makes ordering belt-and-\n * suspenders rather than load-bearing.\n */\nconst DANGEROUS_UTILITIES: ReadonlyArray<{ suffix: string; varName: string }> = [\n { suffix: 'surface-container-highest', varName: '--secondary' },\n { suffix: 'surface-container-high', varName: '--popover' },\n { suffix: 'surface-container', varName: '--card' },\n { suffix: 'card-foreground', varName: '--card-foreground' },\n { suffix: 'popover-foreground', varName: '--popover-foreground' },\n { suffix: 'card', varName: '--card' },\n { suffix: 'popover', varName: '--popover' },\n]\n\n/** Tailwind color-utility prefixes that can carry a background/text/border color. */\nconst UTILITY_PREFIXES = 'bg|text|border|ring|fill|stroke'\n\n/**\n * Match a whole utility class for `suffix`, tolerant of variants (`hover:`,\n * `dark:`) and opacity (`/95`) but not of longer siblings: the trailing\n * `(?![\\w-])` stops `bg-surface-container` from matching inside\n * `bg-surface-container-high`, and `bg-card` from matching inside\n * `bg-card-foreground`.\n */\nfunction buildUtilityRe(suffix: string): RegExp {\n return new RegExp(`(?<![\\\\w-])(?:${UTILITY_PREFIXES})-${suffix}(?![\\\\w-])`, 'g')\n}\n\n/** Recursively collect scannable source files under a directory. */\nfunction walkSources(dir: string): string[] {\n let entries: Dirent[]\n try {\n entries = readdirSync(dir, { withFileTypes: true })\n } catch {\n return []\n }\n return entries.flatMap((e) => {\n if (e.isDirectory()) return SKIP_DIRS.has(e.name) ? [] : walkSources(join(dir, e.name))\n return SOURCE_RE.test(e.name) && !e.name.endsWith('.d.ts') ? [join(dir, e.name)] : []\n })\n}\n\n/**\n * Every `--name:` DEFINITION across the given CSS files. A definition is\n * `--name:` at the start of a (trimmed) line; RHS references like\n * `hsl(var(--card))` are mid-line and are never counted as definitions.\n */\nfunction definedVars(cssFiles: string[]): Set<string> {\n const defs = new Set<string>()\n for (const file of cssFiles) {\n let css: string\n try {\n css = readFileSync(file, 'utf8')\n } catch {\n continue\n }\n for (const m of css.matchAll(/^\\s*(--[a-z0-9-]+)\\s*:/gim)) if (m[1]) defs.add(m[1])\n }\n return defs\n}\n\n/**\n * Default tokens.css: the one agent-app ships as `./styles`. Resolved relative\n * to this module's URL, but tolerant of where the bundler lands the running\n * code — tsup code-splits shared logic into a chunk at the dist ROOT, so the\n * tokens.css sits one directory DIFFERENTLY depending on layout:\n * - source (src/theme-contract/index.ts) → ../theme/tokens.css (src/theme)\n * - split chunk (dist/contract-*.js) → ./theme/tokens.css (dist/theme)\n * - unsplit entry (dist/theme-contract/index.js) → ../theme/tokens.css\n * Probe both and return the one that exists; fall back to the first for a\n * sensible error path if neither is present.\n */\nfunction defaultTokensCss(): string {\n const candidates = ['../theme/tokens.css', './theme/tokens.css'].map((rel) =>\n fileURLToPath(new URL(rel, import.meta.url)),\n )\n return candidates.find((p) => existsSync(p)) ?? candidates[0]!\n}\n\n/**\n * Check that every theme token a consumer's source references is actually\n * defined in the CSS that consumer ships. Returns the full missing set; the\n * caller decides how to fail (the bin exits non-zero on any miss).\n */\nexport function checkThemeContract(opts: ThemeContractOptions): ThemeContractResult {\n const tokensCss = opts.tokensCss ?? defaultTokensCss()\n const defined = definedVars([tokensCss, ...(opts.extraTokensCss ?? [])])\n const allow = new Set(opts.allowlist ?? [])\n const isDefined = (name: string) => defined.has(name) || allow.has(name)\n\n const files = opts.srcDirs.flatMap(walkSources)\n const utilityMatchers = DANGEROUS_UTILITIES.map((u) => ({ ...u, re: buildUtilityRe(u.suffix) }))\n\n // Dedupe by varName (literal check) and by varName+utility (utility check),\n // keeping the FIRST referencing file — enough to locate the offender without\n // drowning the report when one token is referenced across many files.\n const seenVar = new Map<string, string>()\n const seenUtility = new Map<string, string>()\n\n for (const file of files) {\n let text: string\n try {\n text = readFileSync(file, 'utf8')\n } catch {\n continue\n }\n const where = displayPath(file)\n\n // Check 1 — literal var(--…) references.\n for (const m of text.matchAll(/var\\(\\s*(--[a-z0-9-]+)/gi)) {\n const name = m[1]\n if (!name || isDefined(name) || seenVar.has(name)) continue\n seenVar.set(name, where)\n }\n\n // Check 2 — known-dangerous Tailwind utility classes.\n for (const u of utilityMatchers) {\n if (isDefined(u.varName)) continue\n const key = `${u.varName}::${u.suffix}`\n if (seenUtility.has(key)) continue\n u.re.lastIndex = 0\n if (u.re.test(text)) seenUtility.set(key, `${where} (via ${firstUtilityHit(text, u.suffix)})`)\n }\n }\n\n const missing: ThemeContractMiss[] = [\n ...[...seenVar].map(([varName, referencedIn]) => ({ varName, referencedIn })),\n ...[...seenUtility].map(([key, referencedIn]) => ({ varName: key.split('::')[0]!, referencedIn })),\n ]\n return { ok: missing.length === 0, missing }\n}\n\n/** The literal utility class (with prefix) first seen in `text` for `suffix`, for the report. */\nfunction firstUtilityHit(text: string, suffix: string): string {\n const m = buildUtilityRe(suffix).exec(text)\n return m?.[0] ?? `<utility>-${suffix}`\n}\n\n/** Path relative to cwd when it stays inside it, else the path as given — for readable reports. */\nfunction displayPath(file: string): string {\n const rel = relative(process.cwd(), file)\n return rel && !rel.startsWith('..') ? rel : file\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// src/preflight/index.ts
|
|
2
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
3
|
+
function nowMs() {
|
|
4
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
5
|
+
}
|
|
6
|
+
function isAbortLike(err) {
|
|
7
|
+
return err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
8
|
+
}
|
|
9
|
+
function sanitizeUpstreamMessage(input) {
|
|
10
|
+
const message = input instanceof Error ? input.message : String(input);
|
|
11
|
+
return message.replace(/Bearer\s+[^\s]+/gi, "Bearer [redacted]").replace(/\b(?:sk|pk|tc)[_-][A-Za-z0-9_-]{8,}\b/g, "[redacted-key]");
|
|
12
|
+
}
|
|
13
|
+
function snippet(body) {
|
|
14
|
+
const trimmed = body.trim();
|
|
15
|
+
if (!trimmed) return "";
|
|
16
|
+
const clipped = trimmed.length > 180 ? `${trimmed.slice(0, 180)}\u2026` : trimmed;
|
|
17
|
+
return `: ${sanitizeUpstreamMessage(clipped)}`;
|
|
18
|
+
}
|
|
19
|
+
async function runHttp(call) {
|
|
20
|
+
let response;
|
|
21
|
+
try {
|
|
22
|
+
response = await call.fetchImpl(call.url, {
|
|
23
|
+
method: call.method,
|
|
24
|
+
headers: call.headers,
|
|
25
|
+
body: call.body,
|
|
26
|
+
signal: AbortSignal.timeout(call.timeoutMs)
|
|
27
|
+
});
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (isAbortLike(err)) return { kind: "timeout", timeoutMs: call.timeoutMs };
|
|
30
|
+
return { kind: "network", message: sanitizeUpstreamMessage(err) };
|
|
31
|
+
}
|
|
32
|
+
let bodyText = "";
|
|
33
|
+
try {
|
|
34
|
+
bodyText = await response.text();
|
|
35
|
+
} catch {
|
|
36
|
+
bodyText = "";
|
|
37
|
+
}
|
|
38
|
+
return { kind: "status", status: response.status, bodyText };
|
|
39
|
+
}
|
|
40
|
+
function classifyAuthed(outcome, ctx) {
|
|
41
|
+
switch (outcome.kind) {
|
|
42
|
+
case "status": {
|
|
43
|
+
const { status, bodyText } = outcome;
|
|
44
|
+
if (status >= 200 && status < 300) return { ok: true, detail: `${status} OK` };
|
|
45
|
+
if (status === 401 || status === 403) {
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
detail: `DEAD KEY \u2014 ${ctx.endpoint} returned ${status}; rotate ${ctx.keyLabel}`
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (status === 503) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
detail: `UPSTREAM DOWN \u2014 ${ctx.endpoint} returned 503; ${ctx.keyLabel} still looks valid, retry or check the provider (do NOT rotate)`
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { ok: false, detail: `UNEXPECTED ${status} from ${ctx.endpoint}${snippet(bodyText)}` };
|
|
58
|
+
}
|
|
59
|
+
case "timeout":
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
detail: `TIMEOUT after ${outcome.timeoutMs}ms reaching ${ctx.endpoint} \u2014 check ${ctx.urlLabel}`
|
|
63
|
+
};
|
|
64
|
+
case "network":
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
detail: `UNREACHABLE ${ctx.endpoint} (${outcome.message}) \u2014 check ${ctx.urlLabel}`
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function trimTrailingSlash(url) {
|
|
72
|
+
return url.replace(/\/+$/, "");
|
|
73
|
+
}
|
|
74
|
+
function routerChatProbe(config) {
|
|
75
|
+
const keyLabel = config.keySecret ?? "the router API key";
|
|
76
|
+
const urlLabel = config.urlSecret ?? "the router base URL";
|
|
77
|
+
return {
|
|
78
|
+
name: config.name ?? "router-chat",
|
|
79
|
+
critical: config.critical,
|
|
80
|
+
run: async () => {
|
|
81
|
+
const base = trimTrailingSlash(config.baseUrl);
|
|
82
|
+
const endpoint = `${base}/chat/completions`;
|
|
83
|
+
const outcome = await runHttp({
|
|
84
|
+
fetchImpl: config.fetchImpl ?? fetch,
|
|
85
|
+
url: endpoint,
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
89
|
+
"Content-Type": "application/json"
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify({
|
|
92
|
+
model: config.model,
|
|
93
|
+
messages: [{ role: "user", content: "ping" }],
|
|
94
|
+
max_tokens: 1
|
|
95
|
+
}),
|
|
96
|
+
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
97
|
+
});
|
|
98
|
+
return classifyAuthed(outcome, { endpoint, keyLabel, urlLabel });
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function sandboxAuthProbe(config) {
|
|
103
|
+
const keyLabel = config.keySecret ?? "the sandbox API key";
|
|
104
|
+
const urlLabel = config.urlSecret ?? "the sandbox base URL";
|
|
105
|
+
return {
|
|
106
|
+
name: config.name ?? "sandbox-auth",
|
|
107
|
+
critical: config.critical,
|
|
108
|
+
run: async () => {
|
|
109
|
+
const base = trimTrailingSlash(config.baseUrl);
|
|
110
|
+
const endpoint = `${base}/v1/sandboxes?limit=1`;
|
|
111
|
+
const outcome = await runHttp({
|
|
112
|
+
fetchImpl: config.fetchImpl ?? fetch,
|
|
113
|
+
url: endpoint,
|
|
114
|
+
method: "GET",
|
|
115
|
+
headers: { Authorization: `Bearer ${config.apiKey}` },
|
|
116
|
+
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
117
|
+
});
|
|
118
|
+
return classifyAuthed(outcome, { endpoint, keyLabel, urlLabel });
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function statusMatches(status, expect) {
|
|
123
|
+
if (expect === void 0) return status >= 200 && status < 400;
|
|
124
|
+
if (Array.isArray(expect)) return expect.includes(status);
|
|
125
|
+
return status === expect;
|
|
126
|
+
}
|
|
127
|
+
function describeExpected(expect) {
|
|
128
|
+
if (expect === void 0) return "2xx/3xx";
|
|
129
|
+
if (Array.isArray(expect)) return expect.join(" or ");
|
|
130
|
+
return String(expect);
|
|
131
|
+
}
|
|
132
|
+
function httpHeadProbe(config) {
|
|
133
|
+
const urlLabel = config.urlSecret ?? `the URL for ${config.name}`;
|
|
134
|
+
return {
|
|
135
|
+
name: config.name,
|
|
136
|
+
critical: config.critical,
|
|
137
|
+
run: async () => {
|
|
138
|
+
const outcome = await runHttp({
|
|
139
|
+
fetchImpl: config.fetchImpl ?? fetch,
|
|
140
|
+
url: config.url,
|
|
141
|
+
method: "HEAD",
|
|
142
|
+
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
143
|
+
});
|
|
144
|
+
switch (outcome.kind) {
|
|
145
|
+
case "status": {
|
|
146
|
+
if (statusMatches(outcome.status, config.expectStatus)) {
|
|
147
|
+
return { ok: true, detail: `${outcome.status} OK` };
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
detail: `UNEXPECTED ${outcome.status} from ${config.url} (expected ${describeExpected(config.expectStatus)}) \u2014 check ${urlLabel}`
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
case "timeout":
|
|
155
|
+
return {
|
|
156
|
+
ok: false,
|
|
157
|
+
detail: `TIMEOUT after ${outcome.timeoutMs}ms reaching ${config.url} \u2014 check ${urlLabel}`
|
|
158
|
+
};
|
|
159
|
+
case "network":
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
detail: `UNREACHABLE ${config.url} (${outcome.message}) \u2014 check ${urlLabel}`
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
async function runOne(probe) {
|
|
169
|
+
const critical = probe.critical ?? true;
|
|
170
|
+
const start = nowMs();
|
|
171
|
+
try {
|
|
172
|
+
const result = await probe.run();
|
|
173
|
+
return {
|
|
174
|
+
name: probe.name,
|
|
175
|
+
ok: result.ok,
|
|
176
|
+
critical,
|
|
177
|
+
latencyMs: Math.round(nowMs() - start),
|
|
178
|
+
detail: result.detail
|
|
179
|
+
};
|
|
180
|
+
} catch (err) {
|
|
181
|
+
return {
|
|
182
|
+
name: probe.name,
|
|
183
|
+
ok: false,
|
|
184
|
+
critical,
|
|
185
|
+
latencyMs: Math.round(nowMs() - start),
|
|
186
|
+
detail: `probe threw: ${sanitizeUpstreamMessage(err)}`
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function runPreflight(probes) {
|
|
191
|
+
const start = nowMs();
|
|
192
|
+
const verdicts = await Promise.all(probes.map(runOne));
|
|
193
|
+
const failed = verdicts.filter((v) => !v.ok);
|
|
194
|
+
const criticalFailures = failed.filter((v) => v.critical).length;
|
|
195
|
+
return {
|
|
196
|
+
ok: criticalFailures === 0,
|
|
197
|
+
probes: verdicts,
|
|
198
|
+
passed: verdicts.length - failed.length,
|
|
199
|
+
failed: failed.length,
|
|
200
|
+
criticalFailures,
|
|
201
|
+
durationMs: Math.round(nowMs() - start)
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function formatPreflightReport(report) {
|
|
205
|
+
const header = { status: "STATUS", name: "PROBE", latency: "LATENCY", detail: "DETAIL" };
|
|
206
|
+
const rows = report.probes.map((p) => ({
|
|
207
|
+
status: p.ok ? "PASS" : p.critical ? "FAIL" : "WARN",
|
|
208
|
+
name: p.name,
|
|
209
|
+
latency: `${p.latencyMs}ms`,
|
|
210
|
+
detail: p.detail ?? ""
|
|
211
|
+
}));
|
|
212
|
+
const statusW = Math.max(header.status.length, ...rows.map((r) => r.status.length));
|
|
213
|
+
const nameW = Math.max(header.name.length, ...rows.map((r) => r.name.length));
|
|
214
|
+
const latencyW = Math.max(header.latency.length, ...rows.map((r) => r.latency.length));
|
|
215
|
+
const line = (r) => `${r.status.padEnd(statusW)} ${r.name.padEnd(nameW)} ${r.latency.padStart(latencyW)} ${r.detail}`.trimEnd();
|
|
216
|
+
const out = [
|
|
217
|
+
line(header),
|
|
218
|
+
`${"-".repeat(statusW)} ${"-".repeat(nameW)} ${"-".repeat(latencyW)} ------`,
|
|
219
|
+
...rows.map(line),
|
|
220
|
+
""
|
|
221
|
+
];
|
|
222
|
+
if (report.ok) {
|
|
223
|
+
const warn = report.failed > 0 ? ` (${report.failed} non-critical warning(s))` : "";
|
|
224
|
+
out.push(`Preflight PASSED \u2014 ${report.passed}/${report.probes.length} probe(s) live${warn}`);
|
|
225
|
+
} else {
|
|
226
|
+
const dead = report.probes.filter((p) => !p.ok && p.critical).map((p) => p.name).join(", ");
|
|
227
|
+
out.push(`Preflight FAILED \u2014 ${report.criticalFailures} critical probe(s) dead: ${dead}`);
|
|
228
|
+
out.push("Rotate the secret named in each FAIL row above, then redeploy.");
|
|
229
|
+
}
|
|
230
|
+
return out.join("\n");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export {
|
|
234
|
+
routerChatProbe,
|
|
235
|
+
sandboxAuthProbe,
|
|
236
|
+
httpHeadProbe,
|
|
237
|
+
runPreflight,
|
|
238
|
+
formatPreflightReport
|
|
239
|
+
};
|
|
240
|
+
//# sourceMappingURL=chunk-Q4TKVF3L.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/preflight/index.ts"],"sourcesContent":["/**\n * `/preflight` — deploy-time secret-liveness probes.\n *\n * WHY THIS EXISTS: on 2026-07-15 four secrets were simultaneously dead in one\n * production day — a dead `SANDBOX_API_KEY`, a stale `SANDBOX_API_URL`, and a\n * dead LiteLLM router key + URL. Each one was present in `wrangler secret list`\n * (so nothing looked wrong) yet invalid against its live endpoint, and nothing\n * anywhere checked liveness. CI cannot hold production secrets, so this binds\n * at DEPLOY time instead: a product declares a handful of probes built from its\n * real env, the deploy workflow runs `agent-app-preflight` as a step, and a\n * dead secret fails the deploy with a message that names exactly which secret\n * to rotate.\n *\n * A probe is `{ name, run, critical? }`; `run()` returns `{ ok, detail? }`.\n * The standard builders (`routerChatProbe`, `sandboxAuthProbe`, `httpHeadProbe`)\n * each take explicit config — they read nothing global — so the same probe runs\n * identically in a deploy step, a test, or a local check. `runPreflight` fans\n * the probes out, times each, and folds them into a pass/fail report: any\n * failed CRITICAL probe fails the whole run (probes are critical by default).\n *\n * Server-only: probes carry live API keys and hit live endpoints. This subpath\n * must never reach a browser bundle.\n */\n\n/** One probe's outcome. `detail` should name the secret to rotate on failure. */\nexport interface PreflightProbeResult {\n ok: boolean\n detail?: string\n}\n\n/**\n * A liveness probe. `run` performs one cheap live call and maps the result to\n * `{ ok, detail }`. `critical` defaults to `true` — a failed critical probe\n * fails the whole preflight (and the deploy).\n */\nexport interface PreflightProbe {\n name: string\n run: () => Promise<PreflightProbeResult>\n critical?: boolean\n}\n\n/** Per-probe verdict enriched with the resolved criticality and measured latency. */\nexport interface PreflightProbeVerdict {\n name: string\n ok: boolean\n critical: boolean\n latencyMs: number\n detail?: string\n}\n\n/** Aggregate of every probe verdict plus the overall pass/fail decision. */\nexport interface PreflightReport {\n /** `false` if any critical probe failed. */\n ok: boolean\n probes: PreflightProbeVerdict[]\n passed: number\n failed: number\n criticalFailures: number\n durationMs: number\n}\n\n/** Deploy-time deadline for a single probe. Cold upstreams are slow; a dead\n * endpoint should still fail fast, so 10s is the ceiling, not the target. */\nconst DEFAULT_TIMEOUT_MS = 10_000\n\nfunction nowMs(): number {\n return typeof performance !== 'undefined' ? performance.now() : Date.now()\n}\n\nfunction isAbortLike(err: unknown): boolean {\n return err instanceof Error && (err.name === 'TimeoutError' || err.name === 'AbortError')\n}\n\n/** Strip bearer tokens / key material before an upstream string is surfaced in\n * a report (deploy logs are not always private). */\nfunction sanitizeUpstreamMessage(input: unknown): string {\n const message = input instanceof Error ? input.message : String(input)\n return message\n .replace(/Bearer\\s+[^\\s]+/gi, 'Bearer [redacted]')\n .replace(/\\b(?:sk|pk|tc)[_-][A-Za-z0-9_-]{8,}\\b/g, '[redacted-key]')\n}\n\nfunction snippet(body: string): string {\n const trimmed = body.trim()\n if (!trimmed) return ''\n const clipped = trimmed.length > 180 ? `${trimmed.slice(0, 180)}…` : trimmed\n return `: ${sanitizeUpstreamMessage(clipped)}`\n}\n\ntype ProbeOutcome =\n | { kind: 'status'; status: number; bodyText: string }\n | { kind: 'timeout'; timeoutMs: number }\n | { kind: 'network'; message: string }\n\ninterface HttpProbeCall {\n fetchImpl: typeof fetch\n url: string\n method: string\n headers?: Record<string, string>\n body?: string\n timeoutMs: number\n}\n\n/** One live HTTP call, folded to a probe outcome. Never throws: a timeout, a\n * DNS/connection failure, and any thrown error all become an outcome so the\n * probe can classify them into an actionable detail. */\nasync function runHttp(call: HttpProbeCall): Promise<ProbeOutcome> {\n let response: Response\n try {\n response = await call.fetchImpl(call.url, {\n method: call.method,\n headers: call.headers,\n body: call.body,\n signal: AbortSignal.timeout(call.timeoutMs),\n })\n } catch (err) {\n if (isAbortLike(err)) return { kind: 'timeout', timeoutMs: call.timeoutMs }\n return { kind: 'network', message: sanitizeUpstreamMessage(err) }\n }\n let bodyText = ''\n try {\n bodyText = await response.text()\n } catch {\n bodyText = ''\n }\n return { kind: 'status', status: response.status, bodyText }\n}\n\ninterface AuthedClassifyContext {\n /** Full endpoint reached, for the message. */\n endpoint: string\n /** How to name the API-key secret when the endpoint reports auth failure. */\n keyLabel: string\n /** How to name the URL secret when the endpoint is unreachable. */\n urlLabel: string\n}\n\n/**\n * Shared classification for an authed liveness endpoint (router, sandbox):\n * 2xx → live; 401/403 → the KEY is dead, name it; 503 → the UPSTREAM is down,\n * the key still looks valid, don't rotate; timeout / unreachable → the URL is\n * likely stale, name it; anything else → an unexpected status with a snippet.\n */\nfunction classifyAuthed(outcome: ProbeOutcome, ctx: AuthedClassifyContext): PreflightProbeResult {\n switch (outcome.kind) {\n case 'status': {\n const { status, bodyText } = outcome\n if (status >= 200 && status < 300) return { ok: true, detail: `${status} OK` }\n if (status === 401 || status === 403) {\n return {\n ok: false,\n detail: `DEAD KEY — ${ctx.endpoint} returned ${status}; rotate ${ctx.keyLabel}`,\n }\n }\n if (status === 503) {\n return {\n ok: false,\n detail: `UPSTREAM DOWN — ${ctx.endpoint} returned 503; ${ctx.keyLabel} still looks valid, retry or check the provider (do NOT rotate)`,\n }\n }\n return { ok: false, detail: `UNEXPECTED ${status} from ${ctx.endpoint}${snippet(bodyText)}` }\n }\n case 'timeout':\n return {\n ok: false,\n detail: `TIMEOUT after ${outcome.timeoutMs}ms reaching ${ctx.endpoint} — check ${ctx.urlLabel}`,\n }\n case 'network':\n return {\n ok: false,\n detail: `UNREACHABLE ${ctx.endpoint} (${outcome.message}) — check ${ctx.urlLabel}`,\n }\n }\n}\n\nfunction trimTrailingSlash(url: string): string {\n return url.replace(/\\/+$/, '')\n}\n\n// --- Standard probe builders --------------------------------------------------\n\nexport interface RouterChatProbeConfig {\n /** LLM router base URL (LiteLLM / OpenAI-compatible), e.g. `https://router…`. */\n baseUrl: string\n apiKey: string\n /** A cheap model id available on the router. */\n model: string\n /** Probe name in the report. Default `'router-chat'`. */\n name?: string\n /** Default `true`. */\n critical?: boolean\n /** Env-var name of the API key, named verbatim in a dead-key failure. */\n keySecret?: string\n /** Env-var name of the base URL, named verbatim in an unreachable failure. */\n urlSecret?: string\n /** Per-probe deadline. Default 10s. */\n timeoutMs?: number\n /** Injection seam for tests; defaults to global `fetch`. */\n fetchImpl?: typeof fetch\n}\n\n/**\n * Probe an OpenAI-compatible LLM router with one cheap `POST /chat/completions`\n * (`max_tokens: 1`). 200 → live; 401/403 → dead router key; 503 → upstream\n * provider down (key still valid); timeout / unreachable → check the router URL.\n */\nexport function routerChatProbe(config: RouterChatProbeConfig): PreflightProbe {\n const keyLabel = config.keySecret ?? 'the router API key'\n const urlLabel = config.urlSecret ?? 'the router base URL'\n return {\n name: config.name ?? 'router-chat',\n critical: config.critical,\n run: async () => {\n const base = trimTrailingSlash(config.baseUrl)\n const endpoint = `${base}/chat/completions`\n const outcome = await runHttp({\n fetchImpl: config.fetchImpl ?? fetch,\n url: endpoint,\n method: 'POST',\n headers: {\n Authorization: `Bearer ${config.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n model: config.model,\n messages: [{ role: 'user', content: 'ping' }],\n max_tokens: 1,\n }),\n timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n })\n return classifyAuthed(outcome, { endpoint, keyLabel, urlLabel })\n },\n }\n}\n\nexport interface SandboxAuthProbeConfig {\n /** Sandbox API base URL. */\n baseUrl: string\n apiKey: string\n /** Probe name in the report. Default `'sandbox-auth'`. */\n name?: string\n /** Default `true`. */\n critical?: boolean\n /** Env-var name of the API key, named verbatim in a dead-key failure. */\n keySecret?: string\n /** Env-var name of the base URL, named verbatim in an unreachable failure. */\n urlSecret?: string\n /** Per-probe deadline. Default 10s. */\n timeoutMs?: number\n /** Injection seam for tests; defaults to global `fetch`. */\n fetchImpl?: typeof fetch\n}\n\n/**\n * Probe the sandbox API with a cheap authed `GET /v1/sandboxes?limit=1`.\n * 200 → live; 401/403 → dead sandbox key; 503 → sandbox platform down (key\n * still valid); timeout / unreachable → check the sandbox URL.\n */\nexport function sandboxAuthProbe(config: SandboxAuthProbeConfig): PreflightProbe {\n const keyLabel = config.keySecret ?? 'the sandbox API key'\n const urlLabel = config.urlSecret ?? 'the sandbox base URL'\n return {\n name: config.name ?? 'sandbox-auth',\n critical: config.critical,\n run: async () => {\n const base = trimTrailingSlash(config.baseUrl)\n const endpoint = `${base}/v1/sandboxes?limit=1`\n const outcome = await runHttp({\n fetchImpl: config.fetchImpl ?? fetch,\n url: endpoint,\n method: 'GET',\n headers: { Authorization: `Bearer ${config.apiKey}` },\n timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n })\n return classifyAuthed(outcome, { endpoint, keyLabel, urlLabel })\n },\n }\n}\n\nexport interface HttpHeadProbeConfig {\n /** Probe name in the report. */\n name: string\n /** URL to `HEAD`. */\n url: string\n /**\n * Accepted status(es). A single number requires an exact match; an array\n * requires membership. Omitted → any 2xx/3xx (the host is up and the path\n * resolves) counts as live.\n */\n expectStatus?: number | number[]\n /** Default `true`. */\n critical?: boolean\n /** Env-var name of the URL, named verbatim in a failure. */\n urlSecret?: string\n /** Per-probe deadline. Default 10s. */\n timeoutMs?: number\n /** Injection seam for tests; defaults to global `fetch`. */\n fetchImpl?: typeof fetch\n}\n\nfunction statusMatches(status: number, expect?: number | number[]): boolean {\n if (expect === undefined) return status >= 200 && status < 400\n if (Array.isArray(expect)) return expect.includes(status)\n return status === expect\n}\n\nfunction describeExpected(expect?: number | number[]): string {\n if (expect === undefined) return '2xx/3xx'\n if (Array.isArray(expect)) return expect.join(' or ')\n return String(expect)\n}\n\n/**\n * Probe a plain reachability endpoint (e.g. a platform base URL) with a `HEAD`.\n * Confirms the URL is live and resolving — the class of failure behind a stale\n * platform URL that still sits in the secret store.\n */\nexport function httpHeadProbe(config: HttpHeadProbeConfig): PreflightProbe {\n const urlLabel = config.urlSecret ?? `the URL for ${config.name}`\n return {\n name: config.name,\n critical: config.critical,\n run: async () => {\n const outcome = await runHttp({\n fetchImpl: config.fetchImpl ?? fetch,\n url: config.url,\n method: 'HEAD',\n timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n })\n switch (outcome.kind) {\n case 'status': {\n if (statusMatches(outcome.status, config.expectStatus)) {\n return { ok: true, detail: `${outcome.status} OK` }\n }\n return {\n ok: false,\n detail: `UNEXPECTED ${outcome.status} from ${config.url} (expected ${describeExpected(config.expectStatus)}) — check ${urlLabel}`,\n }\n }\n case 'timeout':\n return {\n ok: false,\n detail: `TIMEOUT after ${outcome.timeoutMs}ms reaching ${config.url} — check ${urlLabel}`,\n }\n case 'network':\n return {\n ok: false,\n detail: `UNREACHABLE ${config.url} (${outcome.message}) — check ${urlLabel}`,\n }\n }\n },\n }\n}\n\n// --- Runner + report ----------------------------------------------------------\n\nasync function runOne(probe: PreflightProbe): Promise<PreflightProbeVerdict> {\n const critical = probe.critical ?? true\n const start = nowMs()\n try {\n const result = await probe.run()\n return {\n name: probe.name,\n ok: result.ok,\n critical,\n latencyMs: Math.round(nowMs() - start),\n detail: result.detail,\n }\n } catch (err) {\n return {\n name: probe.name,\n ok: false,\n critical,\n latencyMs: Math.round(nowMs() - start),\n detail: `probe threw: ${sanitizeUpstreamMessage(err)}`,\n }\n }\n}\n\n/**\n * Run every probe (concurrently), time each, and fold into a report. The run\n * fails (`ok: false`) iff a critical probe fails; a failed non-critical probe\n * is a warning that does not block the deploy.\n */\nexport async function runPreflight(probes: PreflightProbe[]): Promise<PreflightReport> {\n const start = nowMs()\n const verdicts = await Promise.all(probes.map(runOne))\n const failed = verdicts.filter((v) => !v.ok)\n const criticalFailures = failed.filter((v) => v.critical).length\n return {\n ok: criticalFailures === 0,\n probes: verdicts,\n passed: verdicts.length - failed.length,\n failed: failed.length,\n criticalFailures,\n durationMs: Math.round(nowMs() - start),\n }\n}\n\ninterface FormatRow {\n status: string\n name: string\n latency: string\n detail: string\n}\n\n/** Render a report as an aligned, operator-readable table + verdict line. Pure\n * (no I/O) so it is trivially testable and reusable by the bin. */\nexport function formatPreflightReport(report: PreflightReport): string {\n const header: FormatRow = { status: 'STATUS', name: 'PROBE', latency: 'LATENCY', detail: 'DETAIL' }\n const rows: FormatRow[] = report.probes.map((p) => ({\n status: p.ok ? 'PASS' : p.critical ? 'FAIL' : 'WARN',\n name: p.name,\n latency: `${p.latencyMs}ms`,\n detail: p.detail ?? '',\n }))\n const statusW = Math.max(header.status.length, ...rows.map((r) => r.status.length))\n const nameW = Math.max(header.name.length, ...rows.map((r) => r.name.length))\n const latencyW = Math.max(header.latency.length, ...rows.map((r) => r.latency.length))\n const line = (r: FormatRow): string =>\n `${r.status.padEnd(statusW)} ${r.name.padEnd(nameW)} ${r.latency.padStart(latencyW)} ${r.detail}`.trimEnd()\n\n const out: string[] = [\n line(header),\n `${'-'.repeat(statusW)} ${'-'.repeat(nameW)} ${'-'.repeat(latencyW)} ------`,\n ...rows.map(line),\n '',\n ]\n if (report.ok) {\n const warn = report.failed > 0 ? ` (${report.failed} non-critical warning(s))` : ''\n out.push(`Preflight PASSED — ${report.passed}/${report.probes.length} probe(s) live${warn}`)\n } else {\n const dead = report.probes\n .filter((p) => !p.ok && p.critical)\n .map((p) => p.name)\n .join(', ')\n out.push(`Preflight FAILED — ${report.criticalFailures} critical probe(s) dead: ${dead}`)\n out.push('Rotate the secret named in each FAIL row above, then redeploy.')\n }\n return out.join('\\n')\n}\n"],"mappings":"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","names":[]}
|
|
@@ -254,6 +254,14 @@ function createAuthGuard(opts) {
|
|
|
254
254
|
getOptionalSession: async (request) => await opts.getSession(request) ?? null
|
|
255
255
|
};
|
|
256
256
|
}
|
|
257
|
+
async function guardResolution(run) {
|
|
258
|
+
try {
|
|
259
|
+
return { ok: true, value: await run() };
|
|
260
|
+
} catch (err) {
|
|
261
|
+
if (err instanceof Response) return { ok: false, response: err };
|
|
262
|
+
throw err;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
257
265
|
function parseAdminEmails(raw) {
|
|
258
266
|
return (raw ?? "").split(/[,\s]+/).map((e) => e.trim().toLowerCase()).filter(Boolean);
|
|
259
267
|
}
|
|
@@ -288,8 +296,9 @@ export {
|
|
|
288
296
|
createBetterAuthSessionCookieMinter,
|
|
289
297
|
createTangleSsoHandlers,
|
|
290
298
|
createAuthGuard,
|
|
299
|
+
guardResolution,
|
|
291
300
|
parseAdminEmails,
|
|
292
301
|
createAdminGuard,
|
|
293
302
|
assertBillableBalance
|
|
294
303
|
};
|
|
295
|
-
//# sourceMappingURL=chunk-
|
|
304
|
+
//# sourceMappingURL=chunk-TKVJE63N.js.map
|