@xera-ai/core 0.4.4 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/internal.js +895 -537
- package/dist/src/index.js +109 -4
- package/package.json +6 -4
- package/src/artifact/status.ts +3 -0
- package/src/bin-internal/auth-setup.ts +116 -0
- package/src/bin-internal/exec.ts +42 -9
- package/src/bin-internal/graph-record.ts +3 -0
- package/src/bin-internal/index.ts +2 -0
- package/src/bin-internal/normalize.ts +13 -1
- package/src/bin-internal/report.ts +94 -2
- package/src/bin-internal/verify-prompts.ts +2 -1
- package/src/classifier/aggregate.ts +3 -0
- package/src/classifier/auth-expired.ts +44 -0
- package/src/classifier/contract-drift.ts +111 -0
- package/src/classifier/rate-limited.ts +25 -0
- package/src/config/schema.ts +51 -8
- package/src/graph/schema.ts +3 -0
- package/src/graph/types.ts +4 -1
- package/src/index.ts +2 -0
- package/src/scrub/index.ts +1 -0
- package/src/scrub/rules.ts +69 -0
- package/dist/adapter/types.d.ts +0 -70
- package/dist/adapter/types.d.ts.map +0 -1
- package/dist/artifact/hash.d.ts +0 -4
- package/dist/artifact/hash.d.ts.map +0 -1
- package/dist/artifact/meta.d.ts +0 -20
- package/dist/artifact/meta.d.ts.map +0 -1
- package/dist/artifact/paths.d.ts +0 -25
- package/dist/artifact/paths.d.ts.map +0 -1
- package/dist/artifact/status.d.ts +0 -75
- package/dist/artifact/status.d.ts.map +0 -1
- package/dist/auth/encrypt.d.ts +0 -4
- package/dist/auth/encrypt.d.ts.map +0 -1
- package/dist/auth/key.d.ts +0 -3
- package/dist/auth/key.d.ts.map +0 -1
- package/dist/auth/refresh.d.ts +0 -9
- package/dist/auth/refresh.d.ts.map +0 -1
- package/dist/auth/state.d.ts +0 -15
- package/dist/auth/state.d.ts.map +0 -1
- package/dist/bin-internal/disputes.d.ts +0 -2
- package/dist/bin-internal/disputes.d.ts.map +0 -1
- package/dist/bin-internal/doctor.d.ts +0 -5
- package/dist/bin-internal/doctor.d.ts.map +0 -1
- package/dist/bin-internal/eval-deterministic.d.ts +0 -5
- package/dist/bin-internal/eval-deterministic.d.ts.map +0 -1
- package/dist/bin-internal/eval-prepare.d.ts +0 -7
- package/dist/bin-internal/eval-prepare.d.ts.map +0 -1
- package/dist/bin-internal/eval-report.d.ts +0 -5
- package/dist/bin-internal/eval-report.d.ts.map +0 -1
- package/dist/bin-internal/exec.d.ts +0 -2
- package/dist/bin-internal/exec.d.ts.map +0 -1
- package/dist/bin-internal/fetch.d.ts +0 -5
- package/dist/bin-internal/fetch.d.ts.map +0 -1
- package/dist/bin-internal/graph-backfill.d.ts +0 -2
- package/dist/bin-internal/graph-backfill.d.ts.map +0 -1
- package/dist/bin-internal/graph-enrich.d.ts +0 -2
- package/dist/bin-internal/graph-enrich.d.ts.map +0 -1
- package/dist/bin-internal/graph-query.d.ts +0 -2
- package/dist/bin-internal/graph-query.d.ts.map +0 -1
- package/dist/bin-internal/graph-record-script.d.ts +0 -2
- package/dist/bin-internal/graph-record-script.d.ts.map +0 -1
- package/dist/bin-internal/graph-record.d.ts +0 -3
- package/dist/bin-internal/graph-record.d.ts.map +0 -1
- package/dist/bin-internal/graph-render.d.ts +0 -2
- package/dist/bin-internal/graph-render.d.ts.map +0 -1
- package/dist/bin-internal/graph-snapshot.d.ts +0 -2
- package/dist/bin-internal/graph-snapshot.d.ts.map +0 -1
- package/dist/bin-internal/heal-prepare.d.ts +0 -19
- package/dist/bin-internal/heal-prepare.d.ts.map +0 -1
- package/dist/bin-internal/impact-prepare.d.ts +0 -2
- package/dist/bin-internal/impact-prepare.d.ts.map +0 -1
- package/dist/bin-internal/index.d.ts +0 -2
- package/dist/bin-internal/index.d.ts.map +0 -1
- package/dist/bin-internal/lint.d.ts +0 -2
- package/dist/bin-internal/lint.d.ts.map +0 -1
- package/dist/bin-internal/normalize.d.ts +0 -2
- package/dist/bin-internal/normalize.d.ts.map +0 -1
- package/dist/bin-internal/post.d.ts +0 -2
- package/dist/bin-internal/post.d.ts.map +0 -1
- package/dist/bin-internal/promote.d.ts +0 -2
- package/dist/bin-internal/promote.d.ts.map +0 -1
- package/dist/bin-internal/report.d.ts +0 -2
- package/dist/bin-internal/report.d.ts.map +0 -1
- package/dist/bin-internal/status-cmd.d.ts +0 -2
- package/dist/bin-internal/status-cmd.d.ts.map +0 -1
- package/dist/bin-internal/typecheck.d.ts +0 -2
- package/dist/bin-internal/typecheck.d.ts.map +0 -1
- package/dist/bin-internal/unlock.d.ts +0 -2
- package/dist/bin-internal/unlock.d.ts.map +0 -1
- package/dist/bin-internal/validate-feature.d.ts +0 -2
- package/dist/bin-internal/validate-feature.d.ts.map +0 -1
- package/dist/bin-internal/verify-prompts.d.ts +0 -7
- package/dist/bin-internal/verify-prompts.d.ts.map +0 -1
- package/dist/classifier/aggregate.d.ts +0 -3
- package/dist/classifier/aggregate.d.ts.map +0 -1
- package/dist/classifier/history.d.ts +0 -13
- package/dist/classifier/history.d.ts.map +0 -1
- package/dist/classifier/types.d.ts +0 -26
- package/dist/classifier/types.d.ts.map +0 -1
- package/dist/config/define.d.ts +0 -3
- package/dist/config/define.d.ts.map +0 -1
- package/dist/config/load.d.ts +0 -3
- package/dist/config/load.d.ts.map +0 -1
- package/dist/config/schema.d.ts +0 -72
- package/dist/config/schema.d.ts.map +0 -1
- package/dist/eval/paths.d.ts +0 -15
- package/dist/eval/paths.d.ts.map +0 -1
- package/dist/eval/run-id.d.ts +0 -6
- package/dist/eval/run-id.d.ts.map +0 -1
- package/dist/eval/types.d.ts +0 -203
- package/dist/eval/types.d.ts.map +0 -1
- package/dist/graph/classify.d.ts +0 -42
- package/dist/graph/classify.d.ts.map +0 -1
- package/dist/graph/cost.d.ts +0 -21
- package/dist/graph/cost.d.ts.map +0 -1
- package/dist/graph/enrich.d.ts +0 -10
- package/dist/graph/enrich.d.ts.map +0 -1
- package/dist/graph/impact.d.ts +0 -31
- package/dist/graph/impact.d.ts.map +0 -1
- package/dist/graph/index.d.ts +0 -17
- package/dist/graph/index.d.ts.map +0 -1
- package/dist/graph/paths.d.ts +0 -10
- package/dist/graph/paths.d.ts.map +0 -1
- package/dist/graph/render.d.ts +0 -50
- package/dist/graph/render.d.ts.map +0 -1
- package/dist/graph/schema.d.ts +0 -180
- package/dist/graph/schema.d.ts.map +0 -1
- package/dist/graph/similarity.d.ts +0 -3
- package/dist/graph/similarity.d.ts.map +0 -1
- package/dist/graph/store.d.ts +0 -14
- package/dist/graph/store.d.ts.map +0 -1
- package/dist/graph/types.d.ts +0 -152
- package/dist/graph/types.d.ts.map +0 -1
- package/dist/graph/ulid.d.ts +0 -2
- package/dist/graph/ulid.d.ts.map +0 -1
- package/dist/index.d.ts +0 -20
- package/dist/index.d.ts.map +0 -1
- package/dist/jira/client.d.ts +0 -11
- package/dist/jira/client.d.ts.map +0 -1
- package/dist/jira/fields.d.ts +0 -7
- package/dist/jira/fields.d.ts.map +0 -1
- package/dist/jira/mcp-backend.d.ts +0 -3
- package/dist/jira/mcp-backend.d.ts.map +0 -1
- package/dist/jira/rest-backend.d.ts +0 -8
- package/dist/jira/rest-backend.d.ts.map +0 -1
- package/dist/jira/retry.d.ts +0 -8
- package/dist/jira/retry.d.ts.map +0 -1
- package/dist/jira/types.d.ts +0 -29
- package/dist/jira/types.d.ts.map +0 -1
- package/dist/lock/file-lock.d.ts +0 -12
- package/dist/lock/file-lock.d.ts.map +0 -1
- package/dist/logging/ndjson-logger.d.ts +0 -11
- package/dist/logging/ndjson-logger.d.ts.map +0 -1
- package/dist/reporter/jira-comment.d.ts +0 -9
- package/dist/reporter/jira-comment.d.ts.map +0 -1
- package/dist/reporter/status-writer.d.ts +0 -14
- package/dist/reporter/status-writer.d.ts.map +0 -1
package/dist/src/index.js
CHANGED
|
@@ -113,7 +113,10 @@ var ClassificationEnum = z2.enum([
|
|
|
113
113
|
"SELECTOR_DRIFT",
|
|
114
114
|
"FLAKY",
|
|
115
115
|
"TEST_BUG",
|
|
116
|
-
"TEST_OUTDATED"
|
|
116
|
+
"TEST_OUTDATED",
|
|
117
|
+
"CONTRACT_DRIFT",
|
|
118
|
+
"RATE_LIMITED",
|
|
119
|
+
"AUTH_EXPIRED"
|
|
117
120
|
]);
|
|
118
121
|
var ResultEnum = z2.enum(["PASS", "FAIL"]);
|
|
119
122
|
var ConfidenceEnum = z2.enum(["low", "medium", "high"]);
|
|
@@ -297,6 +300,32 @@ var WebSchema = z4.object({
|
|
|
297
300
|
message: "defaultEnv must exist in baseUrl map",
|
|
298
301
|
path: ["defaultEnv"]
|
|
299
302
|
});
|
|
303
|
+
var HttpAuthRoleSchema = z4.object({
|
|
304
|
+
tokenEnv: z4.string().optional(),
|
|
305
|
+
userEnv: z4.string().optional(),
|
|
306
|
+
passEnv: z4.string().optional(),
|
|
307
|
+
tokenUrl: z4.string().url().optional(),
|
|
308
|
+
clientIdEnv: z4.string().optional(),
|
|
309
|
+
clientSecretEnv: z4.string().optional(),
|
|
310
|
+
scope: z4.string().optional()
|
|
311
|
+
});
|
|
312
|
+
var HttpAuthSchema = z4.object({
|
|
313
|
+
strategy: z4.enum(["bearer", "apiKey", "basic", "oauth-cc", "custom", "none"]).default("none"),
|
|
314
|
+
ttl: z4.string().default("8h"),
|
|
315
|
+
refreshBuffer: z4.string().default("30m"),
|
|
316
|
+
roles: z4.record(z4.string(), HttpAuthRoleSchema).default({})
|
|
317
|
+
});
|
|
318
|
+
var HttpSchema = z4.object({
|
|
319
|
+
baseUrl: z4.record(z4.string(), z4.string().url()).refine((m) => Object.keys(m).length > 0, {
|
|
320
|
+
message: "baseUrl must have at least one environment"
|
|
321
|
+
}),
|
|
322
|
+
defaultEnv: z4.string(),
|
|
323
|
+
spec: z4.string().optional(),
|
|
324
|
+
auth: HttpAuthSchema.prefault({})
|
|
325
|
+
}).refine((h) => h.baseUrl[h.defaultEnv] !== undefined, {
|
|
326
|
+
message: "defaultEnv must exist in baseUrl map",
|
|
327
|
+
path: ["defaultEnv"]
|
|
328
|
+
});
|
|
300
329
|
var JiraSchema = z4.object({
|
|
301
330
|
baseUrl: z4.string().url(),
|
|
302
331
|
projectKeys: z4.array(z4.string().min(1)).min(1),
|
|
@@ -332,11 +361,17 @@ var RunSchema = z4.object({
|
|
|
332
361
|
}).prefault({});
|
|
333
362
|
var XeraConfigSchema = z4.object({
|
|
334
363
|
jira: JiraSchema,
|
|
335
|
-
web: WebSchema,
|
|
364
|
+
web: WebSchema.optional(),
|
|
365
|
+
http: HttpSchema.optional(),
|
|
336
366
|
ai: AISchema,
|
|
337
367
|
reporting: ReportingSchema,
|
|
338
368
|
run: RunSchema.prefault({}),
|
|
339
|
-
adapters: z4.array(z4.
|
|
369
|
+
adapters: z4.array(z4.enum(["web", "http"])).min(1).default(["web"])
|
|
370
|
+
}).refine((c) => c.web !== undefined || c.http !== undefined, {
|
|
371
|
+
message: "At least one of `web` or `http` must be configured"
|
|
372
|
+
}).refine((c) => c.adapters.every((a) => (a === "web" ? c.web : c.http) !== undefined), {
|
|
373
|
+
message: "Every adapter in `adapters` must have a corresponding config block",
|
|
374
|
+
path: ["adapters"]
|
|
340
375
|
});
|
|
341
376
|
|
|
342
377
|
// src/config/load.ts
|
|
@@ -585,7 +620,66 @@ class NdjsonLogger {
|
|
|
585
620
|
`).map((line) => JSON.parse(line));
|
|
586
621
|
}
|
|
587
622
|
}
|
|
588
|
-
|
|
623
|
+
// src/scrub/rules.ts
|
|
624
|
+
var SENSITIVE_HEADERS = [
|
|
625
|
+
"authorization",
|
|
626
|
+
"cookie",
|
|
627
|
+
"set-cookie",
|
|
628
|
+
"x-api-key",
|
|
629
|
+
"x-auth-token",
|
|
630
|
+
"x-csrf-token",
|
|
631
|
+
"proxy-authorization"
|
|
632
|
+
];
|
|
633
|
+
var SENSITIVE_BODY_KEYS = [
|
|
634
|
+
/password/i,
|
|
635
|
+
/passwd/i,
|
|
636
|
+
/token/i,
|
|
637
|
+
/secret/i,
|
|
638
|
+
/api[-_]?key/i,
|
|
639
|
+
/access[-_]?key/i,
|
|
640
|
+
/private[-_]?key/i,
|
|
641
|
+
/authorization/i,
|
|
642
|
+
/credit[-_]?card/i,
|
|
643
|
+
/card[-_]?number/i,
|
|
644
|
+
/cvv/i
|
|
645
|
+
];
|
|
646
|
+
var JWT_RE = /\beyJ[A-Za-z0-9_-]{7,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{5,}\b/;
|
|
647
|
+
var CREDIT_CARD_RE = /\b(?:\d{4}[-\s]?){3}\d{4}\b/;
|
|
648
|
+
var EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/;
|
|
649
|
+
var PHONE_RE = /(?:\+?\d[\d\s().-]{6,}\d)/;
|
|
650
|
+
var JWT_RE_G = new RegExp(JWT_RE.source, "g");
|
|
651
|
+
var CREDIT_CARD_RE_G = new RegExp(CREDIT_CARD_RE.source, "g");
|
|
652
|
+
var EMAIL_RE_G = new RegExp(EMAIL_RE.source, "g");
|
|
653
|
+
var PHONE_RE_G = new RegExp(PHONE_RE.source, "g");
|
|
654
|
+
var REDACTED = "[REDACTED]";
|
|
655
|
+
function scrubHeaders(headers) {
|
|
656
|
+
const out = {};
|
|
657
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
658
|
+
out[k] = SENSITIVE_HEADERS.includes(k.toLowerCase()) ? REDACTED : v;
|
|
659
|
+
}
|
|
660
|
+
return out;
|
|
661
|
+
}
|
|
662
|
+
function scrubBodyJson(body) {
|
|
663
|
+
if (Array.isArray(body))
|
|
664
|
+
return body.map(scrubBodyJson);
|
|
665
|
+
if (body && typeof body === "object") {
|
|
666
|
+
const out = {};
|
|
667
|
+
for (const [k, v] of Object.entries(body)) {
|
|
668
|
+
if (SENSITIVE_BODY_KEYS.some((re) => re.test(k))) {
|
|
669
|
+
out[k] = REDACTED;
|
|
670
|
+
} else {
|
|
671
|
+
out[k] = scrubBodyJson(v);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return out;
|
|
675
|
+
}
|
|
676
|
+
if (typeof body === "string")
|
|
677
|
+
return scrubFreeText(body);
|
|
678
|
+
return body;
|
|
679
|
+
}
|
|
680
|
+
function scrubFreeText(s) {
|
|
681
|
+
return s.replace(JWT_RE_G, REDACTED).replace(CREDIT_CARD_RE_G, REDACTED).replace(EMAIL_RE_G, REDACTED).replace(PHONE_RE_G, REDACTED);
|
|
682
|
+
}
|
|
589
683
|
// src/index.ts
|
|
590
684
|
var VERSION2 = "0.1.0";
|
|
591
685
|
export {
|
|
@@ -594,6 +688,9 @@ export {
|
|
|
594
688
|
writeAuthState,
|
|
595
689
|
withRetry,
|
|
596
690
|
updateMeta,
|
|
691
|
+
scrubHeaders,
|
|
692
|
+
scrubFreeText,
|
|
693
|
+
scrubBodyJson,
|
|
597
694
|
resolveAuthKey,
|
|
598
695
|
resolveArtifactPaths,
|
|
599
696
|
releaseLock,
|
|
@@ -621,9 +718,17 @@ export {
|
|
|
621
718
|
XeraConfigSchema,
|
|
622
719
|
VERSION2 as VERSION,
|
|
623
720
|
StatusJsonSchema,
|
|
721
|
+
SENSITIVE_HEADERS,
|
|
722
|
+
SENSITIVE_BODY_KEYS,
|
|
723
|
+
PHONE_RE_G,
|
|
724
|
+
PHONE_RE,
|
|
624
725
|
NdjsonLogger,
|
|
625
726
|
MetaJsonSchema,
|
|
727
|
+
JWT_RE,
|
|
626
728
|
HistoryEntrySchema,
|
|
729
|
+
EMAIL_RE_G,
|
|
730
|
+
EMAIL_RE,
|
|
731
|
+
CREDIT_CARD_RE,
|
|
627
732
|
AuthStateEntrySchema,
|
|
628
733
|
AUTH_KEY_ENV
|
|
629
734
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xera-ai/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -25,12 +25,14 @@
|
|
|
25
25
|
"bin"
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
|
-
"build": "bun build ./src/index.ts ./bin/internal.ts --outdir ./dist --target bun --external @playwright/test --external @xera-ai/web --external zod",
|
|
29
|
-
"typecheck": "tsc --noEmit"
|
|
28
|
+
"build": "bun build ./src/index.ts ./bin/internal.ts --outdir ./dist --target bun --external @playwright/test --external @xera-ai/web --external @xera-ai/http --external zod",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"prepublishOnly": "bun run build"
|
|
30
31
|
},
|
|
31
32
|
"dependencies": {
|
|
32
33
|
"zod": "4.4.3",
|
|
33
|
-
"@xera-ai/web": "^0.
|
|
34
|
+
"@xera-ai/web": "^0.8.0",
|
|
35
|
+
"@xera-ai/http": "^0.8.0",
|
|
34
36
|
"@playwright/test": "1.60.0",
|
|
35
37
|
"fflate": "0.8.3",
|
|
36
38
|
"yaml": "2.9.0"
|
package/src/artifact/status.ts
CHANGED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { loadConfig } from '../config/load';
|
|
5
|
+
|
|
6
|
+
interface AuthSetupOpts {
|
|
7
|
+
role?: string;
|
|
8
|
+
shape: 'web' | 'http' | 'all';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseOpts(argv: string[]): AuthSetupOpts {
|
|
12
|
+
const opts: AuthSetupOpts = { shape: 'all' };
|
|
13
|
+
for (let i = 0; i < argv.length; i++) {
|
|
14
|
+
const a = argv[i];
|
|
15
|
+
const next = argv[i + 1];
|
|
16
|
+
if (a === '--role' && next) {
|
|
17
|
+
opts.role = next;
|
|
18
|
+
i++;
|
|
19
|
+
} else if (a === '--shape' && next) {
|
|
20
|
+
if (next === 'web' || next === 'http' || next === 'all') opts.shape = next;
|
|
21
|
+
i++;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return opts;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function authSetupCmd(argv: string[]): Promise<number> {
|
|
28
|
+
const opts = parseOpts(argv);
|
|
29
|
+
const cwd = process.cwd();
|
|
30
|
+
const config = await loadConfig(cwd);
|
|
31
|
+
|
|
32
|
+
const authSetupScript = join(cwd, 'shared', 'auth-setup.ts');
|
|
33
|
+
if (!existsSync(authSetupScript)) {
|
|
34
|
+
console.error(
|
|
35
|
+
`[xera:auth-setup] auth-setup.ts not found at ${authSetupScript}. Run 'bunx @xera-ai/cli init' first.`,
|
|
36
|
+
);
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const mod = (await import(pathToFileURL(authSetupScript).href)) as {
|
|
41
|
+
web?: unknown;
|
|
42
|
+
http?: unknown;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
let exitCode = 0;
|
|
46
|
+
|
|
47
|
+
// Web roles
|
|
48
|
+
if (
|
|
49
|
+
(opts.shape === 'all' || opts.shape === 'web') &&
|
|
50
|
+
config.web &&
|
|
51
|
+
typeof mod.web === 'function'
|
|
52
|
+
) {
|
|
53
|
+
const { runAuthSetup } = await import('@xera-ai/web');
|
|
54
|
+
const { chromium } = await import('@playwright/test');
|
|
55
|
+
const browser = await chromium.launch();
|
|
56
|
+
try {
|
|
57
|
+
for (const [roleName, roleCreds] of Object.entries(config.web.auth.roles)) {
|
|
58
|
+
if (opts.role && roleName !== opts.role) continue;
|
|
59
|
+
const email = process.env[roleCreds.envEmail];
|
|
60
|
+
const password = process.env[roleCreds.envPassword];
|
|
61
|
+
if (!email || !password) {
|
|
62
|
+
console.error(
|
|
63
|
+
`[xera:auth-setup] missing env vars ${roleCreds.envEmail} / ${roleCreds.envPassword} for role '${roleName}'`,
|
|
64
|
+
);
|
|
65
|
+
exitCode = 1;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
await runAuthSetup({
|
|
70
|
+
role: roleName,
|
|
71
|
+
creds: { email, password },
|
|
72
|
+
setupScriptPath: authSetupScript,
|
|
73
|
+
authDir: join(cwd, '.xera', '.auth'),
|
|
74
|
+
browser,
|
|
75
|
+
});
|
|
76
|
+
console.log(`[xera:auth-setup] ✓ ${roleName}.json (web)`);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
console.error(`[xera:auth-setup] ✗ web/${roleName}: ${(e as Error).message}`);
|
|
79
|
+
exitCode = 1;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} finally {
|
|
83
|
+
await browser.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Http roles
|
|
88
|
+
if (
|
|
89
|
+
(opts.shape === 'all' || opts.shape === 'http') &&
|
|
90
|
+
config.http &&
|
|
91
|
+
typeof mod.http === 'function'
|
|
92
|
+
) {
|
|
93
|
+
// The auth-setup.ts template reads config via globalThis; set it for the user's function.
|
|
94
|
+
(globalThis as Record<string, unknown>).__XERA_HTTP_CONFIG__ = config.http;
|
|
95
|
+
|
|
96
|
+
const { runHttpAuthSetup } = await import('@xera-ai/http');
|
|
97
|
+
for (const roleName of Object.keys(config.http.auth.roles)) {
|
|
98
|
+
if (opts.role && roleName !== opts.role) continue;
|
|
99
|
+
try {
|
|
100
|
+
await runHttpAuthSetup({
|
|
101
|
+
authDir: join(cwd, '.xera', '.auth'),
|
|
102
|
+
role: roleName,
|
|
103
|
+
config: config.http,
|
|
104
|
+
setupFn: mod.http as Parameters<typeof runHttpAuthSetup>[0]['setupFn'],
|
|
105
|
+
creds: { email: '', password: '' },
|
|
106
|
+
});
|
|
107
|
+
console.log(`[xera:auth-setup] ✓ http/${roleName}.json`);
|
|
108
|
+
} catch (e) {
|
|
109
|
+
console.error(`[xera:auth-setup] ✗ http/${roleName}: ${(e as Error).message}`);
|
|
110
|
+
exitCode = 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return exitCode;
|
|
116
|
+
}
|
package/src/bin-internal/exec.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { chromium } from '@playwright/test';
|
|
4
4
|
import { runAuthSetup, runPlaywright, stagePlaywrightState } from '@xera-ai/web';
|
|
5
|
+
import { readMeta } from '../artifact/meta';
|
|
5
6
|
import { generateRunId, resolveArtifactPaths } from '../artifact/paths';
|
|
6
7
|
import { needsRefresh } from '../auth/refresh';
|
|
7
8
|
import { readAuthState } from '../auth/state';
|
|
@@ -42,16 +43,48 @@ export async function execCmd(argv: string[]): Promise<number> {
|
|
|
42
43
|
|
|
43
44
|
const t0 = Date.now();
|
|
44
45
|
try {
|
|
46
|
+
const meta = readMeta(paths.metaPath);
|
|
47
|
+
const adapter = meta?.adapter ?? 'web';
|
|
48
|
+
|
|
49
|
+
if (adapter === 'http') {
|
|
50
|
+
if (!config.http) {
|
|
51
|
+
throw new Error('http adapter requires http config block');
|
|
52
|
+
}
|
|
53
|
+
const env = process.env['XERA_ENV'] ?? config.http.defaultEnv;
|
|
54
|
+
const { HttpAdapter } = await import('@xera-ai/http');
|
|
55
|
+
const result = await HttpAdapter.execute({
|
|
56
|
+
ticketDir: paths.ticketDir,
|
|
57
|
+
config,
|
|
58
|
+
runId,
|
|
59
|
+
env,
|
|
60
|
+
});
|
|
61
|
+
log.log({
|
|
62
|
+
step: 'exec.complete',
|
|
63
|
+
runId,
|
|
64
|
+
outcome: result.outcome,
|
|
65
|
+
elapsedMs: Date.now() - t0,
|
|
66
|
+
});
|
|
67
|
+
console.log(`[xera:exec] runId=${runId} outcome=${result.outcome}`);
|
|
68
|
+
// Exit 3 means "test failed" (expected vs infra error); lock released in finally
|
|
69
|
+
return result.outcome === 'PASS' ? 0 : 3;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// adapter === 'web' — existing path below unchanged
|
|
73
|
+
if (!config.web) {
|
|
74
|
+
throw new Error('web adapter requires web config block');
|
|
75
|
+
}
|
|
76
|
+
const webConfig = config.web;
|
|
77
|
+
|
|
45
78
|
// Auth refresh per role declared in xera.config.ts
|
|
46
|
-
if (
|
|
79
|
+
if (webConfig.auth.strategy === 'storageState' && webConfig.auth.setupScript) {
|
|
47
80
|
const browser = await chromium.launch();
|
|
48
81
|
try {
|
|
49
|
-
for (const [roleName, roleCreds] of Object.entries(
|
|
82
|
+
for (const [roleName, roleCreds] of Object.entries(webConfig.auth.roles)) {
|
|
50
83
|
const entry = readAuthState(paths.authDir, roleName);
|
|
51
84
|
if (
|
|
52
85
|
needsRefresh(entry, {
|
|
53
|
-
ttl:
|
|
54
|
-
refreshBuffer:
|
|
86
|
+
ttl: webConfig.auth.ttl,
|
|
87
|
+
refreshBuffer: webConfig.auth.refreshBuffer,
|
|
55
88
|
})
|
|
56
89
|
) {
|
|
57
90
|
const email = process.env[roleCreds.envEmail];
|
|
@@ -65,7 +98,7 @@ export async function execCmd(argv: string[]): Promise<number> {
|
|
|
65
98
|
await runAuthSetup({
|
|
66
99
|
role: roleName,
|
|
67
100
|
creds: { email, password },
|
|
68
|
-
setupScriptPath: join(cwd,
|
|
101
|
+
setupScriptPath: join(cwd, webConfig.auth.setupScript),
|
|
69
102
|
authDir: paths.authDir,
|
|
70
103
|
browser,
|
|
71
104
|
});
|
|
@@ -80,8 +113,8 @@ export async function execCmd(argv: string[]): Promise<number> {
|
|
|
80
113
|
// Stage Playwright storageState files at predictable paths
|
|
81
114
|
// (.xera/.auth/.cache/<role>.json) — generated spec.ts references these
|
|
82
115
|
// via test.use({ storageState }) when an authenticated session is needed.
|
|
83
|
-
if (
|
|
84
|
-
for (const roleName of Object.keys(
|
|
116
|
+
if (webConfig.auth.strategy === 'storageState') {
|
|
117
|
+
for (const roleName of Object.keys(webConfig.auth.roles)) {
|
|
85
118
|
if (readAuthState(paths.authDir, roleName)) {
|
|
86
119
|
stagePlaywrightState(paths.authDir, roleName);
|
|
87
120
|
}
|
|
@@ -101,8 +134,8 @@ export async function execCmd(argv: string[]): Promise<number> {
|
|
|
101
134
|
const runDir = paths.runPath(runId).runDir;
|
|
102
135
|
mkdirSync(runDir, { recursive: true });
|
|
103
136
|
|
|
104
|
-
const envName = process.env.XERA_ENV ??
|
|
105
|
-
const baseURL =
|
|
137
|
+
const envName = process.env.XERA_ENV ?? webConfig.defaultEnv;
|
|
138
|
+
const baseURL = webConfig.baseUrl[envName] ?? webConfig.baseUrl[webConfig.defaultEnv]!;
|
|
106
139
|
|
|
107
140
|
const reportJsonPath = join(runDir, 'report.json');
|
|
108
141
|
|
|
@@ -261,6 +261,9 @@ export async function graphRecordCmd(argv: string[]): Promise<number> {
|
|
|
261
261
|
'FLAKY',
|
|
262
262
|
'PASS',
|
|
263
263
|
'TEST_OUTDATED',
|
|
264
|
+
'CONTRACT_DRIFT',
|
|
265
|
+
'RATE_LIMITED',
|
|
266
|
+
'AUTH_EXPIRED',
|
|
264
267
|
];
|
|
265
268
|
if (!validClass.includes(from) || !validClass.includes(to)) {
|
|
266
269
|
console.error(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { authSetupCmd } from './auth-setup';
|
|
1
2
|
import { disputesCmd } from './disputes';
|
|
2
3
|
import { doctorCmd } from './doctor';
|
|
3
4
|
import { evalDeterministicCmd } from './eval-deterministic';
|
|
@@ -25,6 +26,7 @@ import { validateFeatureCmd } from './validate-feature';
|
|
|
25
26
|
import { verifyPromptsCmd } from './verify-prompts';
|
|
26
27
|
|
|
27
28
|
const COMMANDS: Record<string, (argv: string[]) => Promise<number>> = {
|
|
29
|
+
'auth-setup': authSetupCmd,
|
|
28
30
|
disputes: disputesCmd,
|
|
29
31
|
doctor: doctorCmd,
|
|
30
32
|
'eval-deterministic': evalDeterministicCmd,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { readMeta } from '../artifact/meta';
|
|
4
4
|
import { resolveArtifactPaths } from '../artifact/paths';
|
|
5
5
|
|
|
6
6
|
export async function normalizeCmd(argv: string[]): Promise<number> {
|
|
@@ -26,6 +26,18 @@ export async function normalizeCmd(argv: string[]): Promise<number> {
|
|
|
26
26
|
console.error(`[xera:normalize] runs/${runId} missing`);
|
|
27
27
|
return 1;
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
const meta = readMeta(paths.metaPath);
|
|
31
|
+
const adapter = meta?.adapter ?? 'web';
|
|
32
|
+
|
|
33
|
+
if (adapter === 'http') {
|
|
34
|
+
const { normalizeHttpRun } = await import('@xera-ai/http');
|
|
35
|
+
await normalizeHttpRun({ runId, runDir });
|
|
36
|
+
console.log(`[xera:normalize] wrote normalized.json (http)`);
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { normalizeRun } = await import('@xera-ai/web');
|
|
29
41
|
const r = await normalizeRun({ runId, runDir });
|
|
30
42
|
console.log(
|
|
31
43
|
`[xera:normalize] wrote normalized.json (scrubbed_fields_count=${r.scrubbed_fields_count})`,
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { readMeta } from '../artifact/meta';
|
|
3
4
|
import { resolveArtifactPaths } from '../artifact/paths';
|
|
5
|
+
import { readAuthState } from '../auth/state';
|
|
4
6
|
import { aggregateScenarios } from '../classifier/aggregate';
|
|
7
|
+
import { type AuthFileSummary, classifyAuthExpired } from '../classifier/auth-expired';
|
|
8
|
+
import { classifyContractDrift } from '../classifier/contract-drift';
|
|
9
|
+
import { classifyRateLimited } from '../classifier/rate-limited';
|
|
5
10
|
import type { ScenarioClassification } from '../classifier/types';
|
|
11
|
+
import { loadConfig } from '../config/load';
|
|
6
12
|
import type { OutdatedDecision } from '../graph/classify';
|
|
7
13
|
import { enhanceClassification } from '../graph/classify';
|
|
8
14
|
import { deriveSnapshot, loadAllEvents } from '../graph/store';
|
|
@@ -22,10 +28,96 @@ export async function reportCmd(argv: string[]): Promise<number> {
|
|
|
22
28
|
console.error('[xera:report] usage: report <TICKET> --input=<classifier-output.json>');
|
|
23
29
|
return 1;
|
|
24
30
|
}
|
|
25
|
-
const
|
|
31
|
+
const cwd = process.cwd();
|
|
32
|
+
const paths = resolveArtifactPaths(cwd, ticket);
|
|
26
33
|
const input = JSON.parse(readFileSync(inputArg.slice('--input='.length), 'utf8')) as ReportInput;
|
|
27
34
|
|
|
28
|
-
|
|
35
|
+
// v0.7: apply deterministic HTTP classifier rules before aggregation.
|
|
36
|
+
// When adapter === 'http', scan normalized.json http.calls for rate-limit,
|
|
37
|
+
// auth-expired, and contract-drift signals and override failing scenario classes.
|
|
38
|
+
interface HttpRuleOverride {
|
|
39
|
+
class: ScenarioClassification['class'];
|
|
40
|
+
rationale: string;
|
|
41
|
+
}
|
|
42
|
+
let httpRuleOverride: HttpRuleOverride | null = null;
|
|
43
|
+
|
|
44
|
+
const meta = readMeta(paths.metaPath);
|
|
45
|
+
if (meta?.adapter === 'http') {
|
|
46
|
+
const config = await loadConfig(cwd);
|
|
47
|
+
if (config.http) {
|
|
48
|
+
const normalizedPath = join(paths.ticketDir, 'runs', input.runId, 'normalized.json');
|
|
49
|
+
if (existsSync(normalizedPath)) {
|
|
50
|
+
const norm = JSON.parse(readFileSync(normalizedPath, 'utf8')) as {
|
|
51
|
+
http?: {
|
|
52
|
+
calls?: Array<{ method: string; url: string; status: number; respBody?: unknown }>;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
const calls = norm.http?.calls ?? [];
|
|
56
|
+
|
|
57
|
+
// RATE_LIMITED
|
|
58
|
+
const rate = classifyRateLimited({ calls });
|
|
59
|
+
if (rate) httpRuleOverride = rate;
|
|
60
|
+
|
|
61
|
+
// AUTH_EXPIRED — needs auth files
|
|
62
|
+
if (!httpRuleOverride) {
|
|
63
|
+
const authFiles: Record<string, AuthFileSummary> = {};
|
|
64
|
+
const httpAuthDir = join(cwd, '.xera', '.auth', 'http');
|
|
65
|
+
for (const role of Object.keys(config.http.auth.roles)) {
|
|
66
|
+
const entry = readAuthState(httpAuthDir, role);
|
|
67
|
+
if (entry) {
|
|
68
|
+
const p = entry.payload as {
|
|
69
|
+
token: string;
|
|
70
|
+
type: 'bearer' | 'apiKey' | 'basic' | 'cookie';
|
|
71
|
+
};
|
|
72
|
+
if (typeof p.token === 'string' && typeof p.type === 'string') {
|
|
73
|
+
authFiles[role] = {
|
|
74
|
+
token: p.token,
|
|
75
|
+
type: p.type as AuthFileSummary['type'],
|
|
76
|
+
expires_at: entry.expires_at,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const authExp = classifyAuthExpired({ calls, authFiles });
|
|
82
|
+
if (authExp) httpRuleOverride = authExp;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// CONTRACT_DRIFT — needs openapi
|
|
86
|
+
if (!httpRuleOverride && config.http.spec) {
|
|
87
|
+
const { loadOpenApi } = await import('@xera-ai/http');
|
|
88
|
+
const openapi = await loadOpenApi(config.http.spec);
|
|
89
|
+
if (openapi) {
|
|
90
|
+
const drift = classifyContractDrift({
|
|
91
|
+
calls: calls.map((c) => ({
|
|
92
|
+
method: c.method,
|
|
93
|
+
url: c.url,
|
|
94
|
+
status: c.status,
|
|
95
|
+
respBody: c.respBody,
|
|
96
|
+
})),
|
|
97
|
+
openapi,
|
|
98
|
+
});
|
|
99
|
+
if (drift) httpRuleOverride = drift;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Apply override: if a deterministic rule fired, stamp every FAIL scenario with it.
|
|
107
|
+
const scenariosForAggregation: ScenarioClassification[] = httpRuleOverride
|
|
108
|
+
? input.scenarios.map((s) =>
|
|
109
|
+
s.outcome === 'FAIL'
|
|
110
|
+
? {
|
|
111
|
+
...s,
|
|
112
|
+
class: httpRuleOverride.class,
|
|
113
|
+
rationale: httpRuleOverride.rationale,
|
|
114
|
+
confidence: 'high' as const,
|
|
115
|
+
}
|
|
116
|
+
: s,
|
|
117
|
+
)
|
|
118
|
+
: input.scenarios;
|
|
119
|
+
|
|
120
|
+
const aggregated = aggregateScenarios(scenariosForAggregation);
|
|
29
121
|
|
|
30
122
|
// v0.6.1: TEST_OUTDATED enhancement.
|
|
31
123
|
// The /xera-report skill writes outdated-decisions.json BEFORE invoking this subcommand,
|
|
@@ -2,6 +2,9 @@ import type { ClassifyOutput, Confidence, ScenarioClassification } from './types
|
|
|
2
2
|
|
|
3
3
|
const CLASS_PRIORITY: Array<ClassifyOutput['overall']> = [
|
|
4
4
|
'REAL_BUG',
|
|
5
|
+
'CONTRACT_DRIFT',
|
|
6
|
+
'AUTH_EXPIRED',
|
|
7
|
+
'RATE_LIMITED',
|
|
5
8
|
'TEST_OUTDATED',
|
|
6
9
|
'TEST_BUG',
|
|
7
10
|
'SELECTOR_DRIFT',
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ClassifyResult, HttpCallSummary } from './rate-limited';
|
|
2
|
+
|
|
3
|
+
export interface AuthFileSummary {
|
|
4
|
+
token: string;
|
|
5
|
+
type: 'bearer' | 'apiKey' | 'basic' | 'cookie';
|
|
6
|
+
expires_at: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ClassifyAuthExpiredInput {
|
|
10
|
+
calls: readonly HttpCallSummary[];
|
|
11
|
+
authFiles: Record<string, AuthFileSummary>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function jwtExpPast(jwt: string, now: number): boolean {
|
|
15
|
+
const parts = jwt.split('.');
|
|
16
|
+
if (parts.length !== 3) return false;
|
|
17
|
+
try {
|
|
18
|
+
const payloadB64 = parts[1];
|
|
19
|
+
if (!payloadB64) return false;
|
|
20
|
+
const payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString('utf8')) as {
|
|
21
|
+
exp?: number;
|
|
22
|
+
};
|
|
23
|
+
return typeof payload.exp === 'number' && payload.exp * 1000 < now;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function classifyAuthExpired(input: ClassifyAuthExpiredInput): ClassifyResult | null {
|
|
30
|
+
const has401 = input.calls.some((c) => c.status === 401);
|
|
31
|
+
if (!has401) return null;
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
for (const [role, entry] of Object.entries(input.authFiles)) {
|
|
34
|
+
const fileExpired = new Date(entry.expires_at).getTime() < now;
|
|
35
|
+
const jwtExpired = entry.type === 'bearer' && jwtExpPast(entry.token, now);
|
|
36
|
+
if (fileExpired || jwtExpired) {
|
|
37
|
+
return {
|
|
38
|
+
class: 'AUTH_EXPIRED',
|
|
39
|
+
rationale: `HTTP 401 captured; auth file for role '${role}' is past expiry. Run: bun run xera:auth-setup --role ${role}`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|