unbrowse 3.1.0 → 3.2.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.
Files changed (107) hide show
  1. package/dist/cli.js +455 -96
  2. package/dist/index.js +2 -6
  3. package/dist/mcp.js +695 -46
  4. package/dist/server.js +25811 -0
  5. package/package.json +1 -2
  6. package/vendor/kuri/darwin-arm64/kuri +0 -0
  7. package/vendor/kuri/darwin-x64/kuri +0 -0
  8. package/vendor/kuri/linux-arm64/kuri +0 -0
  9. package/vendor/kuri/linux-x64/kuri +0 -0
  10. package/vendor/kuri/manifest.json +7 -10
  11. package/runtime-src/agent-outcome.ts +0 -166
  12. package/runtime-src/analytics-session.ts +0 -55
  13. package/runtime-src/api/browse-index.ts +0 -317
  14. package/runtime-src/api/browse-session.ts +0 -572
  15. package/runtime-src/api/browse-submit-prereqs.ts +0 -48
  16. package/runtime-src/api/browse-submit.ts +0 -1184
  17. package/runtime-src/api/routes.ts +0 -1823
  18. package/runtime-src/auth/browser-cookies.ts +0 -423
  19. package/runtime-src/auth/index.ts +0 -535
  20. package/runtime-src/auth/runtime.ts +0 -116
  21. package/runtime-src/browser/index.ts +0 -659
  22. package/runtime-src/browser/types.ts +0 -41
  23. package/runtime-src/build-info.generated.ts +0 -6
  24. package/runtime-src/capture/index.ts +0 -1794
  25. package/runtime-src/capture/prefetch.ts +0 -95
  26. package/runtime-src/capture/rsc.ts +0 -45
  27. package/runtime-src/cli/shortcuts.ts +0 -273
  28. package/runtime-src/cli.ts +0 -1572
  29. package/runtime-src/client/graph-client.ts +0 -100
  30. package/runtime-src/client/index.ts +0 -1425
  31. package/runtime-src/debug-trace.ts +0 -18
  32. package/runtime-src/domain.ts +0 -38
  33. package/runtime-src/execution/index.ts +0 -3397
  34. package/runtime-src/execution/retry.ts +0 -46
  35. package/runtime-src/execution/robots.ts +0 -167
  36. package/runtime-src/execution/search-forms.ts +0 -188
  37. package/runtime-src/extraction/index.ts +0 -1507
  38. package/runtime-src/foundry/publish-bundle.ts +0 -392
  39. package/runtime-src/graph/agent-augment.ts +0 -315
  40. package/runtime-src/graph/index.ts +0 -1524
  41. package/runtime-src/graph/local-fixtures.ts +0 -393
  42. package/runtime-src/graph/local-harness.ts +0 -646
  43. package/runtime-src/graph/planner.ts +0 -411
  44. package/runtime-src/graph/session.ts +0 -294
  45. package/runtime-src/graph/trace-store.ts +0 -136
  46. package/runtime-src/index.ts +0 -24
  47. package/runtime-src/indexer/index.ts +0 -465
  48. package/runtime-src/intent-match.ts +0 -1515
  49. package/runtime-src/kuri/client.ts +0 -1839
  50. package/runtime-src/logger.ts +0 -30
  51. package/runtime-src/marketplace/index.ts +0 -103
  52. package/runtime-src/mcp.ts +0 -1747
  53. package/runtime-src/orchestrator/browser-agent.ts +0 -374
  54. package/runtime-src/orchestrator/dag-advisor.ts +0 -59
  55. package/runtime-src/orchestrator/dag-feedback.ts +0 -257
  56. package/runtime-src/orchestrator/first-pass-action.ts +0 -403
  57. package/runtime-src/orchestrator/index.ts +0 -4480
  58. package/runtime-src/orchestrator/passive-publish.ts +0 -187
  59. package/runtime-src/orchestrator/timing-economics.ts +0 -80
  60. package/runtime-src/payments/cascade.ts +0 -137
  61. package/runtime-src/payments/index.ts +0 -270
  62. package/runtime-src/payments/lobster-pay.ts +0 -182
  63. package/runtime-src/payments/wallet.ts +0 -98
  64. package/runtime-src/publish/review-context.ts +0 -93
  65. package/runtime-src/publish/sanitize.ts +0 -197
  66. package/runtime-src/publish/schema-review.ts +0 -192
  67. package/runtime-src/publish-admission.ts +0 -388
  68. package/runtime-src/ratelimit/index.ts +0 -23
  69. package/runtime-src/reverse-engineer/bundle-scanner.ts +0 -127
  70. package/runtime-src/reverse-engineer/description-prompt.ts +0 -213
  71. package/runtime-src/reverse-engineer/index.ts +0 -1551
  72. package/runtime-src/router.ts +0 -17
  73. package/runtime-src/routing-telemetry.ts +0 -395
  74. package/runtime-src/runtime/browser-access.ts +0 -11
  75. package/runtime-src/runtime/browser-auth.ts +0 -12
  76. package/runtime-src/runtime/browser-host.ts +0 -48
  77. package/runtime-src/runtime/lifecycle.ts +0 -17
  78. package/runtime-src/runtime/local-server.ts +0 -311
  79. package/runtime-src/runtime/paths.ts +0 -99
  80. package/runtime-src/runtime/setup.ts +0 -251
  81. package/runtime-src/runtime/supervisor.ts +0 -69
  82. package/runtime-src/runtime/update-hints.ts +0 -351
  83. package/runtime-src/server.ts +0 -100
  84. package/runtime-src/session-logs.ts +0 -142
  85. package/runtime-src/settings.ts +0 -221
  86. package/runtime-src/single-binary.ts +0 -143
  87. package/runtime-src/site-policy.ts +0 -54
  88. package/runtime-src/stale-cleanup-runner.ts +0 -144
  89. package/runtime-src/stale-cleanup.ts +0 -133
  90. package/runtime-src/telemetry-attribution.ts +0 -120
  91. package/runtime-src/telemetry.ts +0 -253
  92. package/runtime-src/template-params.ts +0 -141
  93. package/runtime-src/transform/drift.ts +0 -60
  94. package/runtime-src/transform/index.ts +0 -277
  95. package/runtime-src/types/index.ts +0 -1
  96. package/runtime-src/types/skill.ts +0 -912
  97. package/runtime-src/vault/index.ts +0 -196
  98. package/runtime-src/verification/auth-gate.ts +0 -8
  99. package/runtime-src/verification/candidates.ts +0 -27
  100. package/runtime-src/verification/index.ts +0 -120
  101. package/runtime-src/verification/matrix.ts +0 -30
  102. package/runtime-src/version.ts +0 -148
  103. package/runtime-src/workflow/artifact.ts +0 -161
  104. package/runtime-src/workflow/compile.ts +0 -808
  105. package/runtime-src/workflow/publish.ts +0 -225
  106. package/runtime-src/workflow/runtime.ts +0 -213
  107. package/vendor/kuri/win-x64/kuri.exe +0 -0
@@ -1,120 +0,0 @@
1
- export const TELEMETRY_ATTRIBUTION_KEYS = [
2
- "utm_source",
3
- "utm_medium",
4
- "utm_campaign",
5
- "utm_content",
6
- "utm_term",
7
- "utm_id",
8
- "gclid",
9
- "wbraid",
10
- "gbraid",
11
- "fbclid",
12
- "twclid",
13
- "ttclid",
14
- "msclkid",
15
- "li_fat_id",
16
- "referrer_host",
17
- "channel",
18
- "campaign_id",
19
- "campaign_name",
20
- "content_id",
21
- "content_type",
22
- "creative_id",
23
- "ad_id",
24
- "adset_id",
25
- "inferred_icp",
26
- "variant_id",
27
- "experiment_id",
28
- "icp",
29
- ] as const;
30
-
31
- export type TelemetryAttributionKey = (typeof TELEMETRY_ATTRIBUTION_KEYS)[number];
32
-
33
- export interface TelemetryAttribution {
34
- utm_source?: string;
35
- utm_medium?: string;
36
- utm_campaign?: string;
37
- utm_content?: string;
38
- utm_term?: string;
39
- utm_id?: string;
40
- gclid?: string;
41
- wbraid?: string;
42
- gbraid?: string;
43
- fbclid?: string;
44
- twclid?: string;
45
- ttclid?: string;
46
- msclkid?: string;
47
- li_fat_id?: string;
48
- referrer_host?: string;
49
- channel?: string;
50
- campaign_id?: string;
51
- campaign_name?: string;
52
- content_id?: string;
53
- content_type?: string;
54
- creative_id?: string;
55
- ad_id?: string;
56
- adset_id?: string;
57
- inferred_icp?: string;
58
- variant_id?: string;
59
- experiment_id?: string;
60
- icp?: string;
61
- }
62
-
63
- const MAX_ATTRIBUTION_VALUE_LENGTH = 160;
64
-
65
- export function sanitizeAttributionValue(value: unknown): string | undefined {
66
- if (typeof value !== "string") return undefined;
67
- const trimmed = value.trim();
68
- if (!trimmed) return undefined;
69
- return trimmed.slice(0, MAX_ATTRIBUTION_VALUE_LENGTH);
70
- }
71
-
72
- export function hasTelemetryAttribution(value: Partial<TelemetryAttribution> | null | undefined): boolean {
73
- if (!value) return false;
74
- return TELEMETRY_ATTRIBUTION_KEYS.some((key) => Boolean(sanitizeAttributionValue(value[key])));
75
- }
76
-
77
- export function sanitizeTelemetryAttribution(
78
- raw: Partial<Record<TelemetryAttributionKey, unknown>> | null | undefined,
79
- ): TelemetryAttribution | undefined {
80
- if (!raw) return undefined;
81
- const cleaned: TelemetryAttribution = {};
82
- for (const key of TELEMETRY_ATTRIBUTION_KEYS) {
83
- const value = sanitizeAttributionValue(raw[key]);
84
- if (value) cleaned[key] = value;
85
- }
86
- return hasTelemetryAttribution(cleaned) ? cleaned : undefined;
87
- }
88
-
89
- export function mergeTelemetryAttribution(
90
- base?: Partial<TelemetryAttribution> | null,
91
- incoming?: Partial<TelemetryAttribution> | null,
92
- ): TelemetryAttribution | undefined {
93
- const merged = sanitizeTelemetryAttribution({
94
- ...(base ?? {}),
95
- ...(incoming ?? {}),
96
- });
97
- return merged;
98
- }
99
-
100
- export function mergeTelemetryProperties(
101
- properties?: Record<string, unknown>,
102
- attribution?: Partial<TelemetryAttribution> | null,
103
- ): Record<string, unknown> | undefined {
104
- const cleanedAttribution = sanitizeTelemetryAttribution(attribution);
105
- if (!cleanedAttribution) return properties;
106
- return {
107
- ...cleanedAttribution,
108
- ...(properties ?? {}),
109
- };
110
- }
111
-
112
- export function decodeTelemetryAttribution(value: string | undefined): TelemetryAttribution | undefined {
113
- if (!value) return undefined;
114
- try {
115
- const decoded = Buffer.from(value, "base64").toString("utf8");
116
- return sanitizeTelemetryAttribution(JSON.parse(decoded) as Partial<Record<TelemetryAttributionKey, unknown>>);
117
- } catch {
118
- return undefined;
119
- }
120
- }
@@ -1,253 +0,0 @@
1
- /**
2
- * Route trace telemetry — issue #28
3
- *
4
- * Persists anonymized routing artifacts to ~/.unbrowse/traces/ for future
5
- * ML training (RAG over traces, contextual bandits, KGE extraction).
6
- *
7
- * Anonymization rules (per spec):
8
- * NEVER store: raw cookies, auth tokens, CSRF tokens, full request/response
9
- * bodies, user-entered secrets, raw chain-of-thought, PII.
10
- * DO store: normalized binding names, hashed binding values, response
11
- * schema shape, response hashes, route fingerprints,
12
- * error class / failure reason taxonomy.
13
- */
14
-
15
- import { createHash } from "node:crypto";
16
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
17
- import { join } from "node:path";
18
- import type { RouteTraceArtifact, TraceFailureReason } from "./types/index.js";
19
- import { TRACE_VERSION } from "./version.js";
20
-
21
- function getTraceDir(): string {
22
- return process.env.UNBROWSE_TRACES_DIR ??
23
- join(process.env.HOME ?? "/tmp", ".unbrowse", "traces");
24
- }
25
-
26
- /** Whether trace emission is enabled (opt-out via env). */
27
- export function isTracingEnabled(): boolean {
28
- return process.env.UNBROWSE_DISABLE_TRACES !== "1";
29
- }
30
-
31
- /**
32
- * SHA-256 hex digest of a string value.
33
- * Used to hash binding values so dependency matching works without exposing
34
- * the actual value.
35
- */
36
- export function hashValue(value: string): string {
37
- return createHash("sha256").update(value).digest("hex").slice(0, 16);
38
- }
39
-
40
- /** Sensitive field name patterns — values of matching keys are stripped. */
41
- const SENSITIVE_PATTERNS = [
42
- /cookie/i, /token/i, /auth/i, /key/i, /secret/i, /password/i,
43
- /csrf/i, /session/i, /bearer/i, /credential/i, /apikey/i,
44
- ];
45
-
46
- function isSensitiveName(name: string): boolean {
47
- return SENSITIVE_PATTERNS.some((re) => re.test(name));
48
- }
49
-
50
- /**
51
- * Return only the binding *names* that are safe to surface in a trace.
52
- * Values are never included; sensitive names are omitted entirely.
53
- */
54
- export function safeBindingNames(bindings: Record<string, unknown>): string[] {
55
- return Object.keys(bindings).filter((k) => !isSensitiveName(k));
56
- }
57
-
58
- /**
59
- * Strip the query string and fragment from a URL so traces never contain
60
- * user-entered search terms or personal identifiers embedded in URLs.
61
- */
62
- export function anonymizeUrl(url: string): string {
63
- try {
64
- const u = new URL(url);
65
- return `${u.protocol}//${u.host}${u.pathname}`;
66
- } catch {
67
- return url.replace(/\?.*$/, "").replace(/#.*$/, "");
68
- }
69
- }
70
-
71
- /**
72
- * Produce a SHA-256 hex digest of the serialized result body.
73
- * This lets the backend detect duplicate responses without storing content.
74
- */
75
- export function hashResponseBody(result: unknown): string {
76
- const str = typeof result === "string" ? result : JSON.stringify(result ?? "");
77
- return createHash("sha256").update(str).digest("hex").slice(0, 32);
78
- }
79
-
80
- /**
81
- * Classify an error message into a `TraceFailureReason` taxonomy value so
82
- * negative traces carry structured labels instead of free-form strings.
83
- */
84
- export function classifyFailure(error: string | undefined): TraceFailureReason {
85
- if (!error) return "unknown";
86
- const e = error.toLowerCase();
87
- if (e.includes("auth") || e.includes("401") || e.includes("403")) return "auth_failed";
88
- if (e.includes("schema") || e.includes("mismatch")) return "schema_mismatch";
89
- if (e.includes("timeout") || e.includes("timed out")) return "timeout";
90
- if (e.includes("empty") || e.includes("no result")) return "empty_response";
91
- if (e.includes("missing") || e.includes("dependency")) return "dependency_missing";
92
- if (e.includes("robots") || e.includes("disallowed")) return "robots_disallowed";
93
- return "unknown";
94
- }
95
-
96
- export interface EmitTraceParams {
97
- trace_id: string;
98
- session_scope: string;
99
- goal: string;
100
- domain: string;
101
- started_at: string;
102
- skill_id?: string;
103
- endpoint_id?: string;
104
- source: RouteTraceArtifact["source"];
105
- status_code?: number;
106
- response_bytes: number;
107
- result?: unknown;
108
- schema_match?: boolean;
109
- candidates_considered: number;
110
- bindings_before: Record<string, unknown>;
111
- bindings_resolved: Record<string, unknown>;
112
- bindings_missing: string[];
113
- outcome: RouteTraceArtifact["outcome"];
114
- error?: string;
115
- rationale?: string;
116
- }
117
-
118
- /**
119
- * Write an anonymized `RouteTraceArtifact` to the local trace directory.
120
- * Returns the file path written, or null if tracing is disabled or the write fails.
121
- */
122
- export function emitRouteTrace(params: EmitTraceParams): string | null {
123
- if (!isTracingEnabled()) return null;
124
-
125
- const now = new Date().toISOString();
126
- const artifact: RouteTraceArtifact = {
127
- trace_id: params.trace_id,
128
- session_scope: params.session_scope,
129
- goal: params.goal,
130
- domain: params.domain,
131
- started_at: params.started_at,
132
- completed_at: now,
133
- latency_ms: Date.now() - new Date(params.started_at).getTime(),
134
- skill_id: params.skill_id,
135
- endpoint_id: params.endpoint_id,
136
- source: params.source,
137
- status_code: params.status_code,
138
- response_bytes: params.response_bytes,
139
- response_hash: params.result != null ? hashResponseBody(params.result) : undefined,
140
- schema_match: params.schema_match,
141
- candidates_considered: params.candidates_considered,
142
- bindings_before: safeBindingNames(params.bindings_before),
143
- bindings_resolved: safeBindingNames(params.bindings_resolved),
144
- bindings_missing: params.bindings_missing.filter((k) => !isSensitiveName(k)),
145
- outcome: params.outcome,
146
- failure_reason: params.outcome !== "success"
147
- ? classifyFailure(params.error)
148
- : undefined,
149
- rationale: params.rationale,
150
- trace_version: TRACE_VERSION,
151
- };
152
-
153
- try {
154
- const traceDir = getTraceDir();
155
- if (!existsSync(traceDir)) mkdirSync(traceDir, { recursive: true });
156
- const stamp = params.started_at.replace(/[:.]/g, "-");
157
- const file = join(traceDir, `${stamp}-${params.outcome}-${params.trace_id}.json`);
158
- writeFileSync(file, JSON.stringify(artifact, null, 2), "utf-8");
159
- return file;
160
- } catch {
161
- return null;
162
- }
163
- }
164
-
165
- // ---------------------------------------------------------------------------
166
- // Error accumulator — auto-file GitHub issues on repeated failures
167
- // ---------------------------------------------------------------------------
168
-
169
- interface AccumulatedError {
170
- count: number;
171
- firstSeen: string;
172
- lastSeen: string;
173
- error: string;
174
- intent: string;
175
- url?: string;
176
- domain: string;
177
- skillId?: string;
178
- endpointId?: string;
179
- }
180
-
181
- const errorAccumulator = new Map<string, AccumulatedError>();
182
- const AUTO_FILE_THRESHOLD = 3;
183
- const autoFiledKeys = new Set<string>();
184
-
185
- export interface FailureContext {
186
- skillId?: string;
187
- endpointId?: string;
188
- domain: string;
189
- intent: string;
190
- url?: string;
191
- error: string;
192
- }
193
-
194
- /**
195
- * Record a failure. After AUTO_FILE_THRESHOLD consecutive failures for the
196
- * same skill+endpoint, auto-file a GitHub issue via the backend.
197
- */
198
- export function recordFailure(ctx: FailureContext): void {
199
- const key = `${ctx.skillId ?? "unknown"}:${ctx.endpointId ?? "unknown"}`;
200
- const existing = errorAccumulator.get(key);
201
- const now = new Date().toISOString();
202
-
203
- if (existing) {
204
- existing.count++;
205
- existing.lastSeen = now;
206
- existing.error = ctx.error;
207
- } else {
208
- errorAccumulator.set(key, {
209
- count: 1,
210
- firstSeen: now,
211
- lastSeen: now,
212
- error: ctx.error,
213
- intent: ctx.intent,
214
- url: ctx.url,
215
- domain: ctx.domain,
216
- skillId: ctx.skillId,
217
- endpointId: ctx.endpointId,
218
- });
219
- }
220
-
221
- const entry = errorAccumulator.get(key)!;
222
- if (entry.count >= AUTO_FILE_THRESHOLD && !autoFiledKeys.has(key)) {
223
- autoFiledKeys.add(key);
224
- // Fire-and-forget auto-file
225
- import("./client/index.js")
226
- .then((client) =>
227
- client.autoFileIssue({
228
- skill_id: ctx.skillId ?? "unknown",
229
- endpoint_id: ctx.endpointId ?? "unknown",
230
- domain: ctx.domain,
231
- intent: ctx.intent,
232
- url: ctx.url,
233
- error: ctx.error,
234
- failure_count: entry.count,
235
- first_seen: entry.firstSeen,
236
- last_seen: entry.lastSeen,
237
- kuri_version: process.env.KURI_VERSION ?? "unknown",
238
- }),
239
- )
240
- .catch((err) =>
241
- console.warn(`[telemetry] auto-file failed: ${(err as Error).message}`),
242
- );
243
- }
244
- }
245
-
246
- export function getAccumulatedErrors(): Map<string, AccumulatedError> {
247
- return new Map(errorAccumulator);
248
- }
249
-
250
- export function resetErrorAccumulatorForTests(): void {
251
- errorAccumulator.clear();
252
- autoFiledKeys.clear();
253
- }
@@ -1,141 +0,0 @@
1
- function sanitizeBindingKey(key: string): string {
2
- return key
3
- .replace(/\[\]/g, "_item")
4
- .replace(/\[([^\]]+)\]/g, (_m, inner: string) => `_${inner}`)
5
- .replace(/[{}]/g, "")
6
- .replace(/[^a-zA-Z0-9_]+/g, "_")
7
- .replace(/_+/g, "_")
8
- .replace(/^_+|_+$/g, "");
9
- }
10
-
11
- export function normalizeQueryBindingKey(key: string): string {
12
- const normalized = sanitizeBindingKey(key);
13
- if (!normalized) return "query";
14
- return /^[0-9]/.test(normalized) ? `query_${normalized}` : normalized;
15
- }
16
-
17
- export function buildQueryBindingMap(keys: Iterable<string>): Record<string, string> {
18
- const out: Record<string, string> = {};
19
- const used = new Set<string>();
20
- for (const rawKey of keys) {
21
- if (!rawKey || out[rawKey]) continue;
22
- const base = normalizeQueryBindingKey(rawKey);
23
- let next = base;
24
- let suffix = 2;
25
- while (used.has(next)) next = `${base}_${suffix++}`;
26
- used.add(next);
27
- out[rawKey] = next;
28
- }
29
- return out;
30
- }
31
-
32
- export function extractTemplateQueryBindings(urlTemplate: string): Record<string, string> {
33
- try {
34
- const templateUrl = new URL(urlTemplate);
35
- const out: Record<string, string> = {};
36
- for (const [key, value] of templateUrl.searchParams.entries()) {
37
- const placeholder = value.match(/^\{([^}]+)\}$/)?.[1];
38
- if (placeholder) out[key] = placeholder;
39
- }
40
- return out;
41
- } catch {
42
- return {};
43
- }
44
- }
45
-
46
- export function deriveTemplateParamsFromContextUrl(
47
- urlTemplate: string,
48
- contextUrl?: string,
49
- ): Record<string, string> {
50
- if (!contextUrl) return {};
51
- try {
52
- const templateUrl = new URL(urlTemplate);
53
- const actualUrl = new URL(contextUrl);
54
- const out: Record<string, string> = {};
55
-
56
- const templateParts = templateUrl.pathname.split("/").filter(Boolean).map((part) => decodeURIComponent(part));
57
- const actualParts = actualUrl.pathname.split("/").filter(Boolean);
58
- if (templateParts.length === actualParts.length) {
59
- let compatible = true;
60
- for (let i = 0; i < templateParts.length; i++) {
61
- const templatePart = templateParts[i];
62
- const actualPart = actualParts[i];
63
- const placeholder = templatePart.match(/^\{([^}]+)\}$/)?.[1];
64
- if (placeholder) {
65
- out[placeholder] = decodeURIComponent(actualPart);
66
- continue;
67
- }
68
- if (templatePart !== actualPart) {
69
- compatible = false;
70
- break;
71
- }
72
- }
73
- if (!compatible) {
74
- for (const key of Object.keys(out)) delete out[key];
75
- }
76
- }
77
-
78
- for (const [key, value] of templateUrl.searchParams.entries()) {
79
- const placeholder = value.match(/^\{([^}]+)\}$/)?.[1];
80
- if (!placeholder) continue;
81
- const actualValue = actualUrl.searchParams.get(key);
82
- if (actualValue != null && actualValue !== "") {
83
- out[placeholder] = actualValue;
84
- }
85
- }
86
- return out;
87
- } catch {
88
- return {};
89
- }
90
- }
91
-
92
- function deriveSemanticTemplateParams(
93
- urlTemplate: string,
94
- contextUrl?: string,
95
- ): Record<string, string> {
96
- if (!contextUrl) return {};
97
- try {
98
- const templateNames = [...urlTemplate.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]).filter(Boolean);
99
- if (templateNames.length === 0) return {};
100
- const actualUrl = new URL(contextUrl);
101
- const actualParts = actualUrl.pathname.split("/").filter(Boolean).map((part) => decodeURIComponent(part));
102
- const lowerParts = actualParts.map((part) => part.toLowerCase());
103
- const out: Record<string, string> = {};
104
-
105
- for (const name of templateNames) {
106
- const lower = name.toLowerCase();
107
- if (lower === "tag" || lower === "tags") {
108
- const prefixIndex = lowerParts.findIndex((part) => part === "t" || part === "tag" || part === "tags");
109
- if (prefixIndex >= 0 && actualParts[prefixIndex + 1]) out[name] = actualParts[prefixIndex + 1]!;
110
- }
111
- }
112
-
113
- return out;
114
- } catch {
115
- return {};
116
- }
117
- }
118
-
119
- export function mergeContextTemplateParams(
120
- params: Record<string, unknown>,
121
- urlTemplate: string,
122
- contextUrl?: string,
123
- ): Record<string, unknown> {
124
- const merged: Record<string, unknown> = { ...params };
125
- const inferred = deriveTemplateParamsFromContextUrl(urlTemplate, contextUrl);
126
- for (const [key, value] of Object.entries(inferred)) {
127
- if (merged[key] == null || merged[key] === "") merged[key] = value;
128
- }
129
- for (const [key, value] of Object.entries(deriveSemanticTemplateParams(urlTemplate, contextUrl))) {
130
- if (merged[key] == null || merged[key] === "") merged[key] = value;
131
- }
132
- for (const [rawKey, placeholder] of Object.entries(extractTemplateQueryBindings(urlTemplate))) {
133
- if ((merged[placeholder] == null || merged[placeholder] === "") && merged[rawKey] != null && merged[rawKey] !== "") {
134
- merged[placeholder] = merged[rawKey];
135
- }
136
- if ((merged[rawKey] == null || merged[rawKey] === "") && merged[placeholder] != null && merged[placeholder] !== "") {
137
- merged[rawKey] = merged[placeholder];
138
- }
139
- }
140
- return merged;
141
- }
@@ -1,60 +0,0 @@
1
- import type { DriftResult, ResponseSchema } from "../types/index.js";
2
- import { inferSchema } from "./index.js";
3
-
4
- /**
5
- * Collect all field paths from a schema (dot-notation).
6
- */
7
- function collectPaths(schema: ResponseSchema, prefix = ""): Map<string, string> {
8
- const paths = new Map<string, string>();
9
- if (schema.type === "object" && schema.properties) {
10
- for (const [key, val] of Object.entries(schema.properties)) {
11
- const fullPath = prefix ? `${prefix}.${key}` : key;
12
- paths.set(fullPath, val.type);
13
- if (val.type === "object" && val.properties) {
14
- for (const [p, t] of collectPaths(val, fullPath)) {
15
- paths.set(p, t);
16
- }
17
- }
18
- if (val.type === "array" && val.items?.type === "object") {
19
- for (const [p, t] of collectPaths(val.items, `${fullPath}[]`)) {
20
- paths.set(p, t);
21
- }
22
- }
23
- }
24
- }
25
- return paths;
26
- }
27
-
28
- /**
29
- * Compare stored schema against a fresh response sample.
30
- * Returns added/removed fields and type changes.
31
- */
32
- export function detectSchemaDrift(existing: ResponseSchema, newSample: unknown): DriftResult {
33
- const newSchema = inferSchema([newSample]);
34
- const oldPaths = collectPaths(existing);
35
- const newPaths = collectPaths(newSchema);
36
-
37
- const added_fields: string[] = [];
38
- const removed_fields: string[] = [];
39
- const type_changes: Array<{ path: string; was: string; now: string }> = [];
40
-
41
- for (const [path, type] of newPaths) {
42
- if (!oldPaths.has(path)) {
43
- added_fields.push(path);
44
- } else if (oldPaths.get(path) !== type) {
45
- type_changes.push({ path, was: oldPaths.get(path)!, now: type });
46
- }
47
- }
48
- for (const path of oldPaths.keys()) {
49
- if (!newPaths.has(path)) {
50
- removed_fields.push(path);
51
- }
52
- }
53
-
54
- return {
55
- drifted: added_fields.length > 0 || removed_fields.length > 0 || type_changes.length > 0,
56
- added_fields,
57
- removed_fields,
58
- type_changes,
59
- };
60
- }