@tangle-network/agent-app 0.43.26 → 0.43.28
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/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/index.d.ts +2 -0
- package/dist/index.js +76 -60
- 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/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/package.json +15 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `agent-app-preflight` — run a product's declared secret-liveness probes as a
|
|
4
|
+
* DEPLOY step and fail the deploy if any critical secret is dead.
|
|
5
|
+
*
|
|
6
|
+
* Reads `preflight.config.mjs` from the current working directory (override
|
|
7
|
+
* with `PREFLIGHT_CONFIG`). That file default-exports the probes, built from
|
|
8
|
+
* `process.env` at load time — the deploy already has the real secrets in its
|
|
9
|
+
* environment, which is the one place they can be probed for liveness (CI
|
|
10
|
+
* cannot hold them). Exit 0 when every critical probe is live, 1 when one is
|
|
11
|
+
* dead (deploy fails), 2 on a config/usage error.
|
|
12
|
+
*
|
|
13
|
+
* Wire it as a step BEFORE `wrangler deploy`:
|
|
14
|
+
* agent-app-preflight
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync } from 'node:fs'
|
|
17
|
+
import { resolve } from 'node:path'
|
|
18
|
+
import { pathToFileURL } from 'node:url'
|
|
19
|
+
|
|
20
|
+
import { formatPreflightReport, runPreflight } from '../dist/preflight/index.js'
|
|
21
|
+
|
|
22
|
+
const configFile = process.env.PREFLIGHT_CONFIG ?? 'preflight.config.mjs'
|
|
23
|
+
|
|
24
|
+
async function loadProbes() {
|
|
25
|
+
const path = resolve(process.cwd(), configFile)
|
|
26
|
+
if (!existsSync(path)) {
|
|
27
|
+
console.error(`preflight: no config at ${path}`)
|
|
28
|
+
console.error(
|
|
29
|
+
`Create ${configFile} default-exporting probes built from process.env — see '@tangle-network/agent-app/preflight'.`,
|
|
30
|
+
)
|
|
31
|
+
process.exit(2)
|
|
32
|
+
}
|
|
33
|
+
const mod = await import(pathToFileURL(path).href)
|
|
34
|
+
const exported = mod.default ?? mod.probes
|
|
35
|
+
const resolved = typeof exported === 'function' ? await exported() : exported
|
|
36
|
+
const probes = Array.isArray(resolved) ? resolved : resolved?.probes
|
|
37
|
+
if (!Array.isArray(probes) || probes.length === 0) {
|
|
38
|
+
console.error(`preflight: ${configFile} must default-export a non-empty array of probes (or { probes }).`)
|
|
39
|
+
process.exit(2)
|
|
40
|
+
}
|
|
41
|
+
return probes
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const probes = await loadProbes()
|
|
45
|
+
const report = await runPreflight(probes)
|
|
46
|
+
console.log(formatPreflightReport(report))
|
|
47
|
+
process.exit(report.ok ? 0 : 1)
|
|
@@ -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":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export { DEFAULT_HARNESS, Harness, KNOWN_HARNESSES, ResolveSessionHarnessInput,
|
|
|
14
14
|
export { AgentAppConfig, AgentIdentityConfig, AgentIntegrationsConfig, AgentKnowledgeConfig, AgentTaxonomyConfig, AgentUiConfig, KnowledgeLoopConfig, KnowledgeSourceSpec, agentAppConfigJsonSchema, defineAgentApp } from './config/index.js';
|
|
15
15
|
export { D1Like, D1PreparedLike, DrizzleColumnLike, DrizzleSqliteCoreLike, PRESET_MIGRATION_SQL, PRESET_TABLES, PresetBillingOptions, PresetKnowledgeAccessorOptions, PresetToolHandlerOptions, VaultKv, createD1KnowledgeStateAccessor, createPresetDrizzleSchema, createPresetFieldCrypto, createPresetToolHandlers, createPresetWorkspaceKeyManager, createPresetWorkspaceKeyStore } from './preset-cloudflare/index.js';
|
|
16
16
|
export { KeyCrypto, KeyProvisioner, PlanLimit, PlatformBalanceInfo, PlatformBalanceManager, PlatformBalanceManagerOptions, PlatformBillingClient, PlatformIdentity, PlatformProductUsage, SharedBillingState, TcloudKeyClient, WorkspaceKeyManager, WorkspaceKeyManagerOptions, WorkspaceKeyRecord, WorkspaceKeyStore, WorkspaceModelKeyUsage, createPlatformBalanceManager, createTcloudKeyProvisioner, createWorkspaceKeyManager } from './billing/index.js';
|
|
17
|
+
export { HttpHeadProbeConfig, PreflightProbe, PreflightProbeResult, PreflightProbeVerdict, PreflightReport, RouterChatProbeConfig, SandboxAuthProbeConfig, formatPreflightReport, httpHeadProbe, routerChatProbe, runPreflight, sandboxAuthProbe } from './preflight/index.js';
|
|
17
18
|
export { B as BULK_DELETE_MAX_THREADS, C as ChatStoreInputError, t as threadTitleFromMessage } from './core-7qIM7svy.js';
|
|
18
19
|
export { C as ChatFilePart, a as ChatImagePart, b as ChatInteractionPart, c as ChatMessagePart, d as ChatNoticePart, e as ChatPartTime, f as ChatReasoningPart, g as ChatStepFinishPart, h as ChatStepStartPart, i as ChatSubtaskPart, j as ChatTextPart, k as ChatToolPart, l as ChatToolState, m as ChatToolStatus, n as ChatUsageTokens, S as StorableHarnessPartKind, o as isChatInteractionPart, p as isChatStepFinishPart, q as isChatTextPart, r as isChatToolPart, t as toChatMessageParts } from './parts-BcbitSNp.js';
|
|
19
20
|
export { DeriveKeyOptions, createFieldCrypto, decodeHexKey, decryptAesGcm, decryptBytes, decryptWithKey, deriveKey, encryptAesGcm, encryptBytes, encryptWithKey } from './crypto/index.js';
|
|
@@ -28,6 +29,7 @@ export { CookieOptions, JsonObject, KvLike, RateLimitResult, RequestContext, Sec
|
|
|
28
29
|
export { BuildRedactedDocumentOptions, DEFAULT_REDACTION_PATTERNS, RedactForIngestionOptions, RedactedDocSegment, RedactedDocument, RedactionPattern, RedactionSpan, RevealResult, RevealSpanOptions, buildRedactedDocument, detectSpans, maskSpans, redactForIngestion, revealSpan } from './redact/index.js';
|
|
29
30
|
export { ApprovalEvent, ApprovalEventSchema, AssetContentMap, AssetFormat, AssetSpec, AssetStatus, AssetVariant, BrandTokens, BrandTokensSchema, ConversionMetrics, ConversionMetricsSchema, CopyContent, CopyContentSchema, CopyPlatform, EmailBodySection, EmailContent, EmailContentSchema, EmailCtaSection, EmailDividerSection, EmailFeatureSection, EmailHeroSection, EmailSection, EmailTestimonialSection, ImageBackground, ImageContent, ImageContentSchema, ImageImageLayer, ImageLayer, ImageLayerType, ImageLogoLayer, ImageShapeLayer, ImageSlide, ImageTextLayer, VideoCaption, VideoContent, VideoContentSchema, VideoCountdownScene, VideoImageRevealScene, VideoScene, VideoSlideScene, VideoTextAnimationScene, parseAssetSpec, safeParseAssetSpec } from './assets/index.js';
|
|
30
31
|
export { AgentAppTheme, CanvasRenderPalette, darkTheme, lightTheme, themeColor, themeToCssVars } from './theme/index.js';
|
|
32
|
+
export { ThemeContractMiss, ThemeContractOptions, ThemeContractResult, checkThemeContract } from './theme-contract/index.js';
|
|
31
33
|
export { DistributionSummary, LoopTraceEventLike, MissionFlowStep, MissionTraceContext, StepSpanContext, TimedEvent, buildFlowTrace, childSpanContext, composeMissionFlowTrace, createMissionTraceContext, delegationActivityToFlowSpans, loopTraceEventsToFlowSpans, renderHistogram, renderWaterfall, stepActivityFlowTrace, summarize, timedEventsFromLines, traceEnv } from './trace/index.js';
|
|
32
34
|
export { M as MIN_SEQUENCE_CLIP_FRAMES, N as NewSequenceClip, a as NewSequenceDecision, b as NewSequenceTrack, S as SequenceClip, c as SequenceClipMedia, d as SequenceClipPatch, e as SequenceDecision, f as SequenceExportFormat, g as SequenceExportRecord, h as SequenceExportStatus, i as SequenceFrameSnapshot, j as SequenceMediaKind, k as SequenceMeta, l as SequenceStatus, m as SequenceStore, n as SequenceStoreScope, o as SequenceTimeline, p as SequenceTrack, q as SequenceTrackKind, T as TimelineClipBounds, r as TimelineInterval, s as assertClipFitsSequence, t as chooseCaptionPlacement, u as clampClipDuration, v as clampClipStart, w as formatSeconds, x as formatTimecode, y as framesToSeconds, z as secondsToFrames, A as snapshotFrame, B as trackIntervals } from './store-gckrNq-g.js';
|
|
33
35
|
export { A as AddCaptionOperation, C as CaptionTargetResolution, a as CreateTrackOperation, D as DeleteClipOperation, E as ExtendSequenceOperation, M as MoveClipOperation, P as PlaceClipOperation, Q as QueueExportOperation, S as SEQUENCE_OPERATION_TYPES, b as SequenceApplyResult, c as SequenceOperation, d as SequenceOperationContext, e as SequenceOperationType, f as SequencePlan, g as SetClipDisabledOperation, h as SetClipTextOperation, i as SplitClipOperation, T as TrimClipOperation, j as applySequenceOperation, k as applySequenceOperations, l as assertSequenceMediaUrl, m as captionTrackNameForLanguage, n as lastClipEndFrame, p as parseSequenceOperations, r as resolveCaptionPlacement, o as resolveCaptionTarget, q as resolvePlaceClipTrack, v as validateAddCaption, s as validateCreateTrack, t as validateDeleteClip, u as validateExtendSequence, w as validateMoveClip, x as validatePlaceClip, y as validateQueueExport, z as validateSequenceOperation, B as validateSequenceOperations, F as validateSetClipDisabled, G as validateSetClipText, H as validateSplitClip, I as validateTrimClip } from './apply-Gk4raT2j.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkThemeContract
|
|
3
|
+
} from "./chunk-NYATNLRK.js";
|
|
1
4
|
import "./chunk-UAKFCMWK.js";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_SEQUENCES_MCP_DESCRIPTION,
|
|
7
|
+
MAX_CAPTION_BATCH,
|
|
8
|
+
SEQUENCE_EXPORT_FORMATS,
|
|
9
|
+
SEQUENCE_MCP_TOOLS,
|
|
10
|
+
SEQUENCE_MEDIA_KINDS,
|
|
11
|
+
SEQUENCE_OPERATION_TYPES,
|
|
12
|
+
SEQUENCE_TRACK_KINDS,
|
|
13
|
+
applySequenceOperation,
|
|
14
|
+
applySequenceOperations,
|
|
15
|
+
assertSequenceMediaUrl,
|
|
16
|
+
buildCaptionChunks,
|
|
17
|
+
buildContactSheetManifest,
|
|
18
|
+
buildEdl,
|
|
19
|
+
buildOtio,
|
|
20
|
+
buildSequencesMcpServerEntry,
|
|
21
|
+
buildSrt,
|
|
22
|
+
buildVtt,
|
|
23
|
+
captionCoverage,
|
|
24
|
+
captionTrackNameForLanguage,
|
|
25
|
+
createSequencesMcpHandler,
|
|
26
|
+
findSequenceMcpTool,
|
|
27
|
+
lastClipEndFrame,
|
|
28
|
+
normalizeLanguageTag,
|
|
29
|
+
parseSequenceOperations,
|
|
30
|
+
planLanguageFanout,
|
|
31
|
+
resolveCaptionPlacement,
|
|
32
|
+
resolveCaptionTarget,
|
|
33
|
+
resolvePlaceClipTrack,
|
|
34
|
+
validateAddCaption,
|
|
35
|
+
validateCreateTrack,
|
|
36
|
+
validateDeleteClip,
|
|
37
|
+
validateExtendSequence,
|
|
38
|
+
validateMoveClip,
|
|
39
|
+
validatePlaceClip,
|
|
40
|
+
validateQueueExport,
|
|
41
|
+
validateSequenceOperation,
|
|
42
|
+
validateSequenceOperations,
|
|
43
|
+
validateSetClipDisabled,
|
|
44
|
+
validateSetClipText,
|
|
45
|
+
validateSplitClip,
|
|
46
|
+
validateTrimClip
|
|
47
|
+
} from "./chunk-NEOV2NQ3.js";
|
|
48
|
+
import {
|
|
49
|
+
MIN_SEQUENCE_CLIP_FRAMES,
|
|
50
|
+
assertClipFitsSequence,
|
|
51
|
+
chooseCaptionPlacement,
|
|
52
|
+
clampClipDuration,
|
|
53
|
+
clampClipStart,
|
|
54
|
+
formatSeconds,
|
|
55
|
+
formatTimecode,
|
|
56
|
+
framesToSeconds,
|
|
57
|
+
secondsToFrames,
|
|
58
|
+
snapshotFrame,
|
|
59
|
+
trackIntervals
|
|
60
|
+
} from "./chunk-ZYBWGSAZ.js";
|
|
2
61
|
import {
|
|
3
62
|
CANVAS_ELEMENT_KINDS,
|
|
4
63
|
CANVAS_MCP_TOOLS,
|
|
@@ -83,61 +142,10 @@ import {
|
|
|
83
142
|
safeParseAssetSpec
|
|
84
143
|
} from "./chunk-5PTGEJZL.js";
|
|
85
144
|
import {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
SEQUENCE_MEDIA_KINDS,
|
|
91
|
-
SEQUENCE_OPERATION_TYPES,
|
|
92
|
-
SEQUENCE_TRACK_KINDS,
|
|
93
|
-
applySequenceOperation,
|
|
94
|
-
applySequenceOperations,
|
|
95
|
-
assertSequenceMediaUrl,
|
|
96
|
-
buildCaptionChunks,
|
|
97
|
-
buildContactSheetManifest,
|
|
98
|
-
buildEdl,
|
|
99
|
-
buildOtio,
|
|
100
|
-
buildSequencesMcpServerEntry,
|
|
101
|
-
buildSrt,
|
|
102
|
-
buildVtt,
|
|
103
|
-
captionCoverage,
|
|
104
|
-
captionTrackNameForLanguage,
|
|
105
|
-
createSequencesMcpHandler,
|
|
106
|
-
findSequenceMcpTool,
|
|
107
|
-
lastClipEndFrame,
|
|
108
|
-
normalizeLanguageTag,
|
|
109
|
-
parseSequenceOperations,
|
|
110
|
-
planLanguageFanout,
|
|
111
|
-
resolveCaptionPlacement,
|
|
112
|
-
resolveCaptionTarget,
|
|
113
|
-
resolvePlaceClipTrack,
|
|
114
|
-
validateAddCaption,
|
|
115
|
-
validateCreateTrack,
|
|
116
|
-
validateDeleteClip,
|
|
117
|
-
validateExtendSequence,
|
|
118
|
-
validateMoveClip,
|
|
119
|
-
validatePlaceClip,
|
|
120
|
-
validateQueueExport,
|
|
121
|
-
validateSequenceOperation,
|
|
122
|
-
validateSequenceOperations,
|
|
123
|
-
validateSetClipDisabled,
|
|
124
|
-
validateSetClipText,
|
|
125
|
-
validateSplitClip,
|
|
126
|
-
validateTrimClip
|
|
127
|
-
} from "./chunk-NEOV2NQ3.js";
|
|
128
|
-
import {
|
|
129
|
-
MIN_SEQUENCE_CLIP_FRAMES,
|
|
130
|
-
assertClipFitsSequence,
|
|
131
|
-
chooseCaptionPlacement,
|
|
132
|
-
clampClipDuration,
|
|
133
|
-
clampClipStart,
|
|
134
|
-
formatSeconds,
|
|
135
|
-
formatTimecode,
|
|
136
|
-
framesToSeconds,
|
|
137
|
-
secondsToFrames,
|
|
138
|
-
snapshotFrame,
|
|
139
|
-
trackIntervals
|
|
140
|
-
} from "./chunk-ZYBWGSAZ.js";
|
|
145
|
+
HubExecClient,
|
|
146
|
+
invokeIntegrationHub,
|
|
147
|
+
resolveIntegrationAction
|
|
148
|
+
} from "./chunk-L2TG5DBW.js";
|
|
141
149
|
import "./chunk-7LNGJDNA.js";
|
|
142
150
|
import {
|
|
143
151
|
DEFAULT_MISSION_STEP_KINDS,
|
|
@@ -187,6 +195,13 @@ import {
|
|
|
187
195
|
createTcloudKeyProvisioner,
|
|
188
196
|
createWorkspaceKeyManager
|
|
189
197
|
} from "./chunk-G3HCU7TA.js";
|
|
198
|
+
import {
|
|
199
|
+
formatPreflightReport,
|
|
200
|
+
httpHeadProbe,
|
|
201
|
+
routerChatProbe,
|
|
202
|
+
runPreflight,
|
|
203
|
+
sandboxAuthProbe
|
|
204
|
+
} from "./chunk-Q4TKVF3L.js";
|
|
190
205
|
import {
|
|
191
206
|
BULK_DELETE_MAX_THREADS,
|
|
192
207
|
ChatStoreInputError,
|
|
@@ -277,11 +292,6 @@ import {
|
|
|
277
292
|
resolveToolId,
|
|
278
293
|
resolveToolName
|
|
279
294
|
} from "./chunk-ATRJULKZ.js";
|
|
280
|
-
import {
|
|
281
|
-
HubExecClient,
|
|
282
|
-
invokeIntegrationHub,
|
|
283
|
-
resolveIntegrationAction
|
|
284
|
-
} from "./chunk-L2TG5DBW.js";
|
|
285
295
|
import {
|
|
286
296
|
createKnowledgeLoop,
|
|
287
297
|
createReviewerDecider,
|
|
@@ -550,6 +560,7 @@ export {
|
|
|
550
560
|
captionCoverage,
|
|
551
561
|
captionTrackNameForLanguage,
|
|
552
562
|
checkRateLimit,
|
|
563
|
+
checkThemeContract,
|
|
553
564
|
childSpanContext,
|
|
554
565
|
chooseCaptionPlacement,
|
|
555
566
|
clampClipDuration,
|
|
@@ -642,12 +653,14 @@ export {
|
|
|
642
653
|
findPreset,
|
|
643
654
|
findSequenceMcpTool,
|
|
644
655
|
flattenHistory,
|
|
656
|
+
formatPreflightReport,
|
|
645
657
|
formatSeconds,
|
|
646
658
|
formatTimecode,
|
|
647
659
|
framesToSeconds,
|
|
648
660
|
getClient,
|
|
649
661
|
getPartKey,
|
|
650
662
|
handleAppToolRequest,
|
|
663
|
+
httpHeadProbe,
|
|
651
664
|
instantiateTemplate,
|
|
652
665
|
interactionFromWireRequest,
|
|
653
666
|
interactionPartKey,
|
|
@@ -742,10 +755,13 @@ export {
|
|
|
742
755
|
restrictTaxonomy,
|
|
743
756
|
revealSpan,
|
|
744
757
|
reviewCandidate,
|
|
758
|
+
routerChatProbe,
|
|
745
759
|
runToolLoop as runAppToolLoop,
|
|
760
|
+
runPreflight,
|
|
746
761
|
runSandboxPrompt,
|
|
747
762
|
runSandboxToolPathSetup,
|
|
748
763
|
safeParseAssetSpec,
|
|
764
|
+
sandboxAuthProbe,
|
|
749
765
|
sandboxToolBinDir,
|
|
750
766
|
sandboxToolPath,
|
|
751
767
|
sandboxToolRootDir,
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/preflight` — deploy-time secret-liveness probes.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS: on 2026-07-15 four secrets were simultaneously dead in one
|
|
5
|
+
* production day — a dead `SANDBOX_API_KEY`, a stale `SANDBOX_API_URL`, and a
|
|
6
|
+
* dead LiteLLM router key + URL. Each one was present in `wrangler secret list`
|
|
7
|
+
* (so nothing looked wrong) yet invalid against its live endpoint, and nothing
|
|
8
|
+
* anywhere checked liveness. CI cannot hold production secrets, so this binds
|
|
9
|
+
* at DEPLOY time instead: a product declares a handful of probes built from its
|
|
10
|
+
* real env, the deploy workflow runs `agent-app-preflight` as a step, and a
|
|
11
|
+
* dead secret fails the deploy with a message that names exactly which secret
|
|
12
|
+
* to rotate.
|
|
13
|
+
*
|
|
14
|
+
* A probe is `{ name, run, critical? }`; `run()` returns `{ ok, detail? }`.
|
|
15
|
+
* The standard builders (`routerChatProbe`, `sandboxAuthProbe`, `httpHeadProbe`)
|
|
16
|
+
* each take explicit config — they read nothing global — so the same probe runs
|
|
17
|
+
* identically in a deploy step, a test, or a local check. `runPreflight` fans
|
|
18
|
+
* the probes out, times each, and folds them into a pass/fail report: any
|
|
19
|
+
* failed CRITICAL probe fails the whole run (probes are critical by default).
|
|
20
|
+
*
|
|
21
|
+
* Server-only: probes carry live API keys and hit live endpoints. This subpath
|
|
22
|
+
* must never reach a browser bundle.
|
|
23
|
+
*/
|
|
24
|
+
/** One probe's outcome. `detail` should name the secret to rotate on failure. */
|
|
25
|
+
interface PreflightProbeResult {
|
|
26
|
+
ok: boolean;
|
|
27
|
+
detail?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A liveness probe. `run` performs one cheap live call and maps the result to
|
|
31
|
+
* `{ ok, detail }`. `critical` defaults to `true` — a failed critical probe
|
|
32
|
+
* fails the whole preflight (and the deploy).
|
|
33
|
+
*/
|
|
34
|
+
interface PreflightProbe {
|
|
35
|
+
name: string;
|
|
36
|
+
run: () => Promise<PreflightProbeResult>;
|
|
37
|
+
critical?: boolean;
|
|
38
|
+
}
|
|
39
|
+
/** Per-probe verdict enriched with the resolved criticality and measured latency. */
|
|
40
|
+
interface PreflightProbeVerdict {
|
|
41
|
+
name: string;
|
|
42
|
+
ok: boolean;
|
|
43
|
+
critical: boolean;
|
|
44
|
+
latencyMs: number;
|
|
45
|
+
detail?: string;
|
|
46
|
+
}
|
|
47
|
+
/** Aggregate of every probe verdict plus the overall pass/fail decision. */
|
|
48
|
+
interface PreflightReport {
|
|
49
|
+
/** `false` if any critical probe failed. */
|
|
50
|
+
ok: boolean;
|
|
51
|
+
probes: PreflightProbeVerdict[];
|
|
52
|
+
passed: number;
|
|
53
|
+
failed: number;
|
|
54
|
+
criticalFailures: number;
|
|
55
|
+
durationMs: number;
|
|
56
|
+
}
|
|
57
|
+
interface RouterChatProbeConfig {
|
|
58
|
+
/** LLM router base URL (LiteLLM / OpenAI-compatible), e.g. `https://router…`. */
|
|
59
|
+
baseUrl: string;
|
|
60
|
+
apiKey: string;
|
|
61
|
+
/** A cheap model id available on the router. */
|
|
62
|
+
model: string;
|
|
63
|
+
/** Probe name in the report. Default `'router-chat'`. */
|
|
64
|
+
name?: string;
|
|
65
|
+
/** Default `true`. */
|
|
66
|
+
critical?: boolean;
|
|
67
|
+
/** Env-var name of the API key, named verbatim in a dead-key failure. */
|
|
68
|
+
keySecret?: string;
|
|
69
|
+
/** Env-var name of the base URL, named verbatim in an unreachable failure. */
|
|
70
|
+
urlSecret?: string;
|
|
71
|
+
/** Per-probe deadline. Default 10s. */
|
|
72
|
+
timeoutMs?: number;
|
|
73
|
+
/** Injection seam for tests; defaults to global `fetch`. */
|
|
74
|
+
fetchImpl?: typeof fetch;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Probe an OpenAI-compatible LLM router with one cheap `POST /chat/completions`
|
|
78
|
+
* (`max_tokens: 1`). 200 → live; 401/403 → dead router key; 503 → upstream
|
|
79
|
+
* provider down (key still valid); timeout / unreachable → check the router URL.
|
|
80
|
+
*/
|
|
81
|
+
declare function routerChatProbe(config: RouterChatProbeConfig): PreflightProbe;
|
|
82
|
+
interface SandboxAuthProbeConfig {
|
|
83
|
+
/** Sandbox API base URL. */
|
|
84
|
+
baseUrl: string;
|
|
85
|
+
apiKey: string;
|
|
86
|
+
/** Probe name in the report. Default `'sandbox-auth'`. */
|
|
87
|
+
name?: string;
|
|
88
|
+
/** Default `true`. */
|
|
89
|
+
critical?: boolean;
|
|
90
|
+
/** Env-var name of the API key, named verbatim in a dead-key failure. */
|
|
91
|
+
keySecret?: string;
|
|
92
|
+
/** Env-var name of the base URL, named verbatim in an unreachable failure. */
|
|
93
|
+
urlSecret?: string;
|
|
94
|
+
/** Per-probe deadline. Default 10s. */
|
|
95
|
+
timeoutMs?: number;
|
|
96
|
+
/** Injection seam for tests; defaults to global `fetch`. */
|
|
97
|
+
fetchImpl?: typeof fetch;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Probe the sandbox API with a cheap authed `GET /v1/sandboxes?limit=1`.
|
|
101
|
+
* 200 → live; 401/403 → dead sandbox key; 503 → sandbox platform down (key
|
|
102
|
+
* still valid); timeout / unreachable → check the sandbox URL.
|
|
103
|
+
*/
|
|
104
|
+
declare function sandboxAuthProbe(config: SandboxAuthProbeConfig): PreflightProbe;
|
|
105
|
+
interface HttpHeadProbeConfig {
|
|
106
|
+
/** Probe name in the report. */
|
|
107
|
+
name: string;
|
|
108
|
+
/** URL to `HEAD`. */
|
|
109
|
+
url: string;
|
|
110
|
+
/**
|
|
111
|
+
* Accepted status(es). A single number requires an exact match; an array
|
|
112
|
+
* requires membership. Omitted → any 2xx/3xx (the host is up and the path
|
|
113
|
+
* resolves) counts as live.
|
|
114
|
+
*/
|
|
115
|
+
expectStatus?: number | number[];
|
|
116
|
+
/** Default `true`. */
|
|
117
|
+
critical?: boolean;
|
|
118
|
+
/** Env-var name of the URL, named verbatim in a failure. */
|
|
119
|
+
urlSecret?: string;
|
|
120
|
+
/** Per-probe deadline. Default 10s. */
|
|
121
|
+
timeoutMs?: number;
|
|
122
|
+
/** Injection seam for tests; defaults to global `fetch`. */
|
|
123
|
+
fetchImpl?: typeof fetch;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Probe a plain reachability endpoint (e.g. a platform base URL) with a `HEAD`.
|
|
127
|
+
* Confirms the URL is live and resolving — the class of failure behind a stale
|
|
128
|
+
* platform URL that still sits in the secret store.
|
|
129
|
+
*/
|
|
130
|
+
declare function httpHeadProbe(config: HttpHeadProbeConfig): PreflightProbe;
|
|
131
|
+
/**
|
|
132
|
+
* Run every probe (concurrently), time each, and fold into a report. The run
|
|
133
|
+
* fails (`ok: false`) iff a critical probe fails; a failed non-critical probe
|
|
134
|
+
* is a warning that does not block the deploy.
|
|
135
|
+
*/
|
|
136
|
+
declare function runPreflight(probes: PreflightProbe[]): Promise<PreflightReport>;
|
|
137
|
+
/** Render a report as an aligned, operator-readable table + verdict line. Pure
|
|
138
|
+
* (no I/O) so it is trivially testable and reusable by the bin. */
|
|
139
|
+
declare function formatPreflightReport(report: PreflightReport): string;
|
|
140
|
+
|
|
141
|
+
export { type HttpHeadProbeConfig, type PreflightProbe, type PreflightProbeResult, type PreflightProbeVerdict, type PreflightReport, type RouterChatProbeConfig, type SandboxAuthProbeConfig, formatPreflightReport, httpHeadProbe, routerChatProbe, runPreflight, sandboxAuthProbe };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatPreflightReport,
|
|
3
|
+
httpHeadProbe,
|
|
4
|
+
routerChatProbe,
|
|
5
|
+
runPreflight,
|
|
6
|
+
sandboxAuthProbe
|
|
7
|
+
} from "../chunk-Q4TKVF3L.js";
|
|
8
|
+
export {
|
|
9
|
+
formatPreflightReport,
|
|
10
|
+
httpHeadProbe,
|
|
11
|
+
routerChatProbe,
|
|
12
|
+
runPreflight,
|
|
13
|
+
sandboxAuthProbe
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
MembersPanel
|
|
3
|
-
} from "../chunk-GNL3MG5J.js";
|
|
4
1
|
import {
|
|
5
2
|
InvitationsPanel
|
|
6
3
|
} from "../chunk-7C64WKAH.js";
|
|
7
4
|
import {
|
|
8
5
|
InviteAcceptPage
|
|
9
6
|
} from "../chunk-VCPZ3HTN.js";
|
|
7
|
+
import {
|
|
8
|
+
MembersPanel
|
|
9
|
+
} from "../chunk-GNL3MG5J.js";
|
|
10
10
|
import "../chunk-63CE7FEZ.js";
|
|
11
11
|
export {
|
|
12
12
|
InvitationsPanel,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
checkThemeContract
|
|
4
|
+
} from "../chunk-NYATNLRK.js";
|
|
5
|
+
|
|
6
|
+
// src/theme-contract/cli.ts
|
|
7
|
+
function parseArgs(argv) {
|
|
8
|
+
const out = { srcDirs: [], extraCss: [], allow: [] };
|
|
9
|
+
for (let i = 0; i < argv.length; i++) {
|
|
10
|
+
const flag = argv[i];
|
|
11
|
+
const take = () => {
|
|
12
|
+
const v = argv[++i];
|
|
13
|
+
if (v === void 0) fail(`${flag} needs a value`);
|
|
14
|
+
return v;
|
|
15
|
+
};
|
|
16
|
+
switch (flag) {
|
|
17
|
+
case "--src":
|
|
18
|
+
out.srcDirs.push(take());
|
|
19
|
+
break;
|
|
20
|
+
case "--extra-css":
|
|
21
|
+
out.extraCss.push(take());
|
|
22
|
+
break;
|
|
23
|
+
case "--allow":
|
|
24
|
+
out.allow.push(take());
|
|
25
|
+
break;
|
|
26
|
+
case "--tokens":
|
|
27
|
+
out.tokens = take();
|
|
28
|
+
break;
|
|
29
|
+
case "-h":
|
|
30
|
+
case "--help":
|
|
31
|
+
printUsage();
|
|
32
|
+
process.exit(0);
|
|
33
|
+
break;
|
|
34
|
+
default:
|
|
35
|
+
fail(`unknown argument: ${flag}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
function printUsage() {
|
|
41
|
+
process.stdout.write(
|
|
42
|
+
"Usage: agent-app-theme-check --src <dir> [--src <dir>\u2026] [--extra-css <file>\u2026] [--tokens <file>] [--allow <--var>\u2026]\n"
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
function fail(msg) {
|
|
46
|
+
process.stderr.write(`agent-app-theme-check: ${msg}
|
|
47
|
+
`);
|
|
48
|
+
printUsage();
|
|
49
|
+
process.exit(2);
|
|
50
|
+
}
|
|
51
|
+
function main() {
|
|
52
|
+
const args = parseArgs(process.argv.slice(2));
|
|
53
|
+
if (args.srcDirs.length === 0) fail("at least one --src <dir> is required");
|
|
54
|
+
const { ok, missing } = checkThemeContract({
|
|
55
|
+
srcDirs: args.srcDirs,
|
|
56
|
+
tokensCss: args.tokens,
|
|
57
|
+
extraTokensCss: args.extraCss,
|
|
58
|
+
allowlist: args.allow
|
|
59
|
+
});
|
|
60
|
+
if (ok) {
|
|
61
|
+
process.stdout.write(`theme contract OK \u2014 every referenced token is defined (${args.srcDirs.join(", ")})
|
|
62
|
+
`);
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
process.stderr.write(
|
|
66
|
+
`theme contract FAILED \u2014 ${missing.length} token reference(s) resolve to nothing (surface ships transparent):
|
|
67
|
+
|
|
68
|
+
`
|
|
69
|
+
);
|
|
70
|
+
for (const m of missing) process.stderr.write(` ${m.varName}
|
|
71
|
+
referenced in ${m.referencedIn}
|
|
72
|
+
`);
|
|
73
|
+
process.stderr.write(
|
|
74
|
+
"\nDefine these in your tokens.css (or `import '@tangle-network/agent-app/styles'`),\npass the defining CSS via --extra-css, or suppress a deliberately-external one with --allow.\n"
|
|
75
|
+
);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
main();
|
|
79
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/theme-contract/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * agent-app-theme-check — CI guard against the invisible-surface incident class.\n *\n * A consumer app runs this over its own source; it fails (exit 1) when a\n * component references a theme token — `var(--popover)` or a preset-mapped\n * utility like `bg-surface-container-high` — that the app's shipped CSS never\n * defines, which would paint that surface transparent with no error at runtime.\n *\n * agent-app-theme-check --src src --src packages/ui/src \\\n * --extra-css src/app-tokens.css\n *\n * Flags (all repeatable except --tokens):\n * --src <dir> source dir to scan for token references (required, 1+)\n * --extra-css <file> extra CSS whose --name: definitions also count as defined\n * --tokens <file> override the base tokens.css (defaults to the one\n * agent-app ships as `@tangle-network/agent-app/styles`)\n * --allow <--var> suppress a token name from the missing report\n *\n * Wire it as a CI step: `\"theme-check\": \"agent-app-theme-check --src src\"`.\n */\n\nimport { checkThemeContract } from './index'\n\ninterface ParsedArgs {\n srcDirs: string[]\n extraCss: string[]\n allow: string[]\n tokens?: string\n}\n\nfunction parseArgs(argv: string[]): ParsedArgs {\n const out: ParsedArgs = { srcDirs: [], extraCss: [], allow: [] }\n for (let i = 0; i < argv.length; i++) {\n const flag = argv[i]\n const take = () => {\n const v = argv[++i]\n if (v === undefined) fail(`${flag} needs a value`)\n return v!\n }\n switch (flag) {\n case '--src':\n out.srcDirs.push(take())\n break\n case '--extra-css':\n out.extraCss.push(take())\n break\n case '--allow':\n out.allow.push(take())\n break\n case '--tokens':\n out.tokens = take()\n break\n case '-h':\n case '--help':\n printUsage()\n process.exit(0)\n break\n default:\n fail(`unknown argument: ${flag}`)\n }\n }\n return out\n}\n\nfunction printUsage(): void {\n process.stdout.write(\n 'Usage: agent-app-theme-check --src <dir> [--src <dir>…] ' +\n '[--extra-css <file>…] [--tokens <file>] [--allow <--var>…]\\n',\n )\n}\n\nfunction fail(msg: string): never {\n process.stderr.write(`agent-app-theme-check: ${msg}\\n`)\n printUsage()\n process.exit(2)\n}\n\nfunction main(): void {\n const args = parseArgs(process.argv.slice(2))\n if (args.srcDirs.length === 0) fail('at least one --src <dir> is required')\n\n const { ok, missing } = checkThemeContract({\n srcDirs: args.srcDirs,\n tokensCss: args.tokens,\n extraTokensCss: args.extraCss,\n allowlist: args.allow,\n })\n\n if (ok) {\n process.stdout.write(`theme contract OK — every referenced token is defined (${args.srcDirs.join(', ')})\\n`)\n process.exit(0)\n }\n\n process.stderr.write(\n `theme contract FAILED — ${missing.length} token reference(s) resolve to nothing (surface ships transparent):\\n\\n`,\n )\n for (const m of missing) process.stderr.write(` ${m.varName}\\n referenced in ${m.referencedIn}\\n`)\n process.stderr.write(\n '\\nDefine these in your tokens.css (or `import \\'@tangle-network/agent-app/styles\\'`),\\n' +\n 'pass the defining CSS via --extra-css, or suppress a deliberately-external one with --allow.\\n',\n )\n process.exit(1)\n}\n\nmain()\n"],"mappings":";;;;;;AA+BA,SAAS,UAAU,MAA4B;AAC7C,QAAM,MAAkB,EAAE,SAAS,CAAC,GAAG,UAAU,CAAC,GAAG,OAAO,CAAC,EAAE;AAC/D,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,OAAO,KAAK,CAAC;AACnB,UAAM,OAAO,MAAM;AACjB,YAAM,IAAI,KAAK,EAAE,CAAC;AAClB,UAAI,MAAM,OAAW,MAAK,GAAG,IAAI,gBAAgB;AACjD,aAAO;AAAA,IACT;AACA,YAAQ,MAAM;AAAA,MACZ,KAAK;AACH,YAAI,QAAQ,KAAK,KAAK,CAAC;AACvB;AAAA,MACF,KAAK;AACH,YAAI,SAAS,KAAK,KAAK,CAAC;AACxB;AAAA,MACF,KAAK;AACH,YAAI,MAAM,KAAK,KAAK,CAAC;AACrB;AAAA,MACF,KAAK;AACH,YAAI,SAAS,KAAK;AAClB;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AACH,mBAAW;AACX,gBAAQ,KAAK,CAAC;AACd;AAAA,MACF;AACE,aAAK,qBAAqB,IAAI,EAAE;AAAA,IACpC;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,aAAmB;AAC1B,UAAQ,OAAO;AAAA,IACb;AAAA,EAEF;AACF;AAEA,SAAS,KAAK,KAAoB;AAChC,UAAQ,OAAO,MAAM,0BAA0B,GAAG;AAAA,CAAI;AACtD,aAAW;AACX,UAAQ,KAAK,CAAC;AAChB;AAEA,SAAS,OAAa;AACpB,QAAM,OAAO,UAAU,QAAQ,KAAK,MAAM,CAAC,CAAC;AAC5C,MAAI,KAAK,QAAQ,WAAW,EAAG,MAAK,sCAAsC;AAE1E,QAAM,EAAE,IAAI,QAAQ,IAAI,mBAAmB;AAAA,IACzC,SAAS,KAAK;AAAA,IACd,WAAW,KAAK;AAAA,IAChB,gBAAgB,KAAK;AAAA,IACrB,WAAW,KAAK;AAAA,EAClB,CAAC;AAED,MAAI,IAAI;AACN,YAAQ,OAAO,MAAM,+DAA0D,KAAK,QAAQ,KAAK,IAAI,CAAC;AAAA,CAAK;AAC3G,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,UAAQ,OAAO;AAAA,IACb,gCAA2B,QAAQ,MAAM;AAAA;AAAA;AAAA,EAC3C;AACA,aAAW,KAAK,QAAS,SAAQ,OAAO,MAAM,KAAK,EAAE,OAAO;AAAA,oBAAuB,EAAE,YAAY;AAAA,CAAI;AACrG,UAAQ,OAAO;AAAA,IACb;AAAA,EAEF;AACA,UAAQ,KAAK,CAAC;AAChB;AAEA,KAAK;","names":[]}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exportable theme-token contract checker — the incident guard for the
|
|
3
|
+
* invisible-popover class of bugs.
|
|
4
|
+
*
|
|
5
|
+
* The failure mode (tax-agent's transparent model dropdown; the whole
|
|
6
|
+
* `bg-surface-container-*` family): a consumer app ships a component that
|
|
7
|
+
* references a theme token — either as `var(--popover)` or as a Tailwind class
|
|
8
|
+
* like `bg-surface-container-high` that the agent-app preset maps to
|
|
9
|
+
* `hsl(var(--popover))` — but the app's OWN build never emits that custom
|
|
10
|
+
* property (it forgot `import '@tangle-network/agent-app/styles'`, or dropped a
|
|
11
|
+
* token in its local tokens.css). CSS resolves the missing var to nothing, the
|
|
12
|
+
* surface paints transparent, and NOTHING errors. It ships invisible.
|
|
13
|
+
*
|
|
14
|
+
* `tests/theme/tokens-contract.test.ts` guards agent-app's OWN components. This
|
|
15
|
+
* module lifts that walking logic into a function every CONSUMER app can run
|
|
16
|
+
* against ITS OWN source in CI, comparing references to the tokens.css agent-app
|
|
17
|
+
* ships plus any extra CSS the app defines.
|
|
18
|
+
*
|
|
19
|
+
* ── What each check covers (scope is deliberately honest) ────────────────────
|
|
20
|
+
*
|
|
21
|
+
* 1. var(--…) check — COMPLETE. Every `var(--name)` literal in the scanned
|
|
22
|
+
* source (inline styles, `bg-[var(--name)]` arbitrary Tailwind values, CSS
|
|
23
|
+
* template strings) is matched and compared against the defined token set.
|
|
24
|
+
* This is exact: a `var(--x)` reference is unambiguous. It is a raw-text
|
|
25
|
+
* scan (no AST), so a `var(--x)` written inside a comment or string literal
|
|
26
|
+
* counts too — deliberate: it keeps the single-source logic identical to the
|
|
27
|
+
* agent-app self-test, and a dangling `var(--x)` in a comment is a smell
|
|
28
|
+
* worth surfacing. Suppress a deliberate one with `allowlist`.
|
|
29
|
+
*
|
|
30
|
+
* 2. Tailwind-utility check — INTENTIONALLY PARTIAL. Bare classes like
|
|
31
|
+
* `bg-card` carry no `var(--)` and so are invisible to check 1; Tailwind
|
|
32
|
+
* resolves them to `hsl(var(--card))` at build via the preset. Fully
|
|
33
|
+
* resolving arbitrary Tailwind config is out of scope (it would mean
|
|
34
|
+
* re-implementing Tailwind). Instead we check the SPECIFIC known-dangerous
|
|
35
|
+
* families that have actually shipped invisible: the MD3 surface ladder
|
|
36
|
+
* (`surface-container` / `-high` / `-highest`) and the `card` / `popover`
|
|
37
|
+
* elevation pairs — exactly the utilities the agent-app tailwind-preset
|
|
38
|
+
* registers onto elevation tokens (see src/theme/tailwind-preset.ts, the
|
|
39
|
+
* source of truth for this mapping). The canvas/sequence aliases
|
|
40
|
+
* (`--bg-input`, `--text-primary`, …) are consumed as `bg-[var(--…)]`
|
|
41
|
+
* arbitrary values and so are already covered fully by check 1 — they need
|
|
42
|
+
* no entry here.
|
|
43
|
+
*
|
|
44
|
+
* Node-only (reads the filesystem) → this lives in the `./theme-contract`
|
|
45
|
+
* subpath, NOT `./theme`, which must stay browser-clean (it's in the
|
|
46
|
+
* browser-safe manifest test).
|
|
47
|
+
*/
|
|
48
|
+
interface ThemeContractOptions {
|
|
49
|
+
/** Consumer source directories to scan for token references (recursively). */
|
|
50
|
+
srcDirs: string[];
|
|
51
|
+
/**
|
|
52
|
+
* Path to the base tokens.css whose `--name:` definitions are the ground
|
|
53
|
+
* truth. Defaults to the tokens.css agent-app ships (`./styles`) — the set a
|
|
54
|
+
* consumer gets from `import '@tangle-network/agent-app/styles'`.
|
|
55
|
+
*/
|
|
56
|
+
tokensCss?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Additional CSS files whose `--name:` definitions also count as defined —
|
|
59
|
+
* the app's own overrides/extensions layered on top of the base tokens.
|
|
60
|
+
*/
|
|
61
|
+
extraTokensCss?: string[];
|
|
62
|
+
/**
|
|
63
|
+
* Token names (e.g. `--my-app-accent`) to treat as always-defined, suppressing
|
|
64
|
+
* them from the missing list. For app-specific vars defined outside any CSS
|
|
65
|
+
* the checker can see (injected at runtime, from a third-party stylesheet, …).
|
|
66
|
+
*/
|
|
67
|
+
allowlist?: string[];
|
|
68
|
+
}
|
|
69
|
+
interface ThemeContractMiss {
|
|
70
|
+
/** The undefined custom property, e.g. `--popover`. */
|
|
71
|
+
varName: string;
|
|
72
|
+
/**
|
|
73
|
+
* Where it was referenced: `path/to/file.tsx`, or
|
|
74
|
+
* `path/to/file.tsx (via bg-surface-container-high)` when the reference is a
|
|
75
|
+
* Tailwind utility that resolves to the token rather than a literal var().
|
|
76
|
+
*/
|
|
77
|
+
referencedIn: string;
|
|
78
|
+
}
|
|
79
|
+
interface ThemeContractResult {
|
|
80
|
+
ok: boolean;
|
|
81
|
+
missing: ThemeContractMiss[];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Check that every theme token a consumer's source references is actually
|
|
85
|
+
* defined in the CSS that consumer ships. Returns the full missing set; the
|
|
86
|
+
* caller decides how to fail (the bin exits non-zero on any miss).
|
|
87
|
+
*/
|
|
88
|
+
declare function checkThemeContract(opts: ThemeContractOptions): ThemeContractResult;
|
|
89
|
+
|
|
90
|
+
export { type ThemeContractMiss, type ThemeContractOptions, type ThemeContractResult, checkThemeContract };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-app",
|
|
3
|
-
"version": "0.43.
|
|
3
|
+
"version": "0.43.28",
|
|
4
4
|
"packageManager": "pnpm@10.33.4",
|
|
5
5
|
"description": "Application-shell framework for Tangle agent products: a bounded tool loop, the structured agent→app tool side channel, integration-hub client, per-workspace billing, and crypto — composed over the Tangle agent substrate through typed seams.",
|
|
6
6
|
"keywords": [
|
|
@@ -30,8 +30,12 @@
|
|
|
30
30
|
"types": "./dist/index.d.ts",
|
|
31
31
|
"files": [
|
|
32
32
|
"dist",
|
|
33
|
+
"bin",
|
|
33
34
|
".claude/skills"
|
|
34
35
|
],
|
|
36
|
+
"bin": {
|
|
37
|
+
"agent-app-theme-check": "./dist/theme-contract/cli.js"
|
|
38
|
+
},
|
|
35
39
|
"exports": {
|
|
36
40
|
".": {
|
|
37
41
|
"types": "./dist/index.d.ts",
|
|
@@ -138,6 +142,11 @@
|
|
|
138
142
|
"import": "./dist/billing/index.js",
|
|
139
143
|
"default": "./dist/billing/index.js"
|
|
140
144
|
},
|
|
145
|
+
"./preflight": {
|
|
146
|
+
"types": "./dist/preflight/index.d.ts",
|
|
147
|
+
"import": "./dist/preflight/index.js",
|
|
148
|
+
"default": "./dist/preflight/index.js"
|
|
149
|
+
},
|
|
141
150
|
"./chat-store": {
|
|
142
151
|
"types": "./dist/chat-store/index.d.ts",
|
|
143
152
|
"import": "./dist/chat-store/index.js",
|
|
@@ -359,6 +368,11 @@
|
|
|
359
368
|
"import": "./dist/theme/tailwind-preset.js",
|
|
360
369
|
"default": "./dist/theme/tailwind-preset.js"
|
|
361
370
|
},
|
|
371
|
+
"./theme-contract": {
|
|
372
|
+
"types": "./dist/theme-contract/index.d.ts",
|
|
373
|
+
"import": "./dist/theme-contract/index.js",
|
|
374
|
+
"default": "./dist/theme-contract/index.js"
|
|
375
|
+
},
|
|
362
376
|
"./styles": "./dist/theme/tokens.css"
|
|
363
377
|
},
|
|
364
378
|
"scripts": {
|