@yanolja-next/noya-sdk 0.1.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.
@@ -0,0 +1,594 @@
1
+ import { compileIssue } from "./issueCompiler.js";
2
+ import { createLocalDraftPr, createProviderIssue, updateProviderIssueAfterFix } from "./adapters.js";
3
+ import { collectHints, redactValue } from "./redaction.js";
4
+ import { fetchSentryEventContext } from "./sentry.js";
5
+ class HttpError extends Error {
6
+ status;
7
+ constructor(status, message) {
8
+ super(message);
9
+ this.status = status;
10
+ }
11
+ }
12
+ export async function receiveSentryWebhook(store, payload) {
13
+ const projectKey = stringAt(payload, "project_key") ??
14
+ stringAt(payload, "projectKey") ??
15
+ stringAt(payload, "project.slug") ??
16
+ "agency-platform";
17
+ const event = objectAt(payload, "event") ?? payload;
18
+ const user = objectAt(event, "user");
19
+ const tags = objectAt(event, "tags");
20
+ const request = objectAt(event, "request");
21
+ const contexts = objectAt(event, "contexts");
22
+ const replayId = stringAt(event, "replay_id") ??
23
+ stringAt(event, "replayId") ??
24
+ stringAt(contexts, "replay.replay_id");
25
+ const eventId = stringAt(event, "event_id") ?? stringAt(event, "eventID") ?? stringAt(event, "id");
26
+ return receiveReport(store, {
27
+ report_id: stringAt(payload, "report_id") ?? (eventId ? `sentry-${eventId}` : undefined),
28
+ project_key: projectKey,
29
+ environment: stringAt(event, "environment"),
30
+ release: stringAt(event, "release"),
31
+ route: stringAt(request, "url"),
32
+ message: stringAt(event, "title") ?? stringAt(event, "message"),
33
+ request_id: stringAt(tags, "request_id") ?? stringAt(event, "request_id"),
34
+ telemetry: {
35
+ provider: "sentry",
36
+ event_id: eventId,
37
+ replay_id: replayId,
38
+ feedback_id: stringAt(event, "feedback_id"),
39
+ event_url: stringAt(event, "web_url"),
40
+ },
41
+ context: {
42
+ user_id: stringAt(user, "id"),
43
+ email: stringAt(user, "email"),
44
+ tenant_id: stringAt(tags, "tenant_id"),
45
+ trace_id: stringAt(tags, "trace_id"),
46
+ },
47
+ });
48
+ }
49
+ export function upsertProject(store, input) {
50
+ if (!input || typeof input.project_key !== "string" || input.project_key.length === 0) {
51
+ throw new HttpError(400, "project_key is required");
52
+ }
53
+ if (input.issue_provider && !["local", "github", "jira"].includes(input.issue_provider)) {
54
+ throw new HttpError(400, `Unsupported issue_provider: ${input.issue_provider}`);
55
+ }
56
+ if (input.retention_days != null &&
57
+ (typeof input.retention_days !== "number" || input.retention_days < 0 || !Number.isFinite(input.retention_days))) {
58
+ throw new HttpError(400, "retention_days must be a non-negative number");
59
+ }
60
+ validateNoInlineCredentials(input.integrations);
61
+ const existing = store.state.projects[input.project_key];
62
+ const runner = mergeRunnerConfig(input.runner);
63
+ const telemetry = input.telemetry ?? legacyProjectTelemetry(input.sentry) ?? existing?.telemetry;
64
+ const project = {
65
+ project_key: input.project_key,
66
+ name: input.name ?? input.project_key,
67
+ environment: input.environment ?? "local",
68
+ issue_provider: input.issue_provider ?? "local",
69
+ artifact_dir: input.artifact_dir ?? defaultArtifactDir(input.project_key),
70
+ retention_days: input.retention_days ?? existing?.retention_days,
71
+ runner,
72
+ telemetry,
73
+ sentry: input.sentry ?? {},
74
+ integrations: input.integrations ?? {},
75
+ created_at: existing?.created_at ?? input.created_at ?? new Date().toISOString(),
76
+ };
77
+ store.state.projects[project.project_key] = project;
78
+ store.save();
79
+ store.audit("project.upserted", { project_key: project.project_key });
80
+ return project;
81
+ }
82
+ function defaultArtifactDir(projectKey) {
83
+ const root = process.env.NOYA_ARTIFACT_ROOT;
84
+ return root ? `${root.replace(/\/$/, "")}/${projectKey}` : ".noya/artifacts";
85
+ }
86
+ function legacyProjectTelemetry(sentry) {
87
+ if (!sentry)
88
+ return undefined;
89
+ return {
90
+ provider: "sentry",
91
+ org_slug: sentry.org_slug,
92
+ project_slug: sentry.project_slug,
93
+ };
94
+ }
95
+ function validateNoInlineCredentials(value, path = "integrations") {
96
+ if (!value || typeof value !== "object")
97
+ return;
98
+ if (Array.isArray(value)) {
99
+ value.forEach((child, index) => validateNoInlineCredentials(child, `${path}[${index}]`));
100
+ return;
101
+ }
102
+ for (const [key, child] of Object.entries(value)) {
103
+ const childPath = `${path}.${key}`;
104
+ if (isInlineCredentialKey(key)) {
105
+ throw new HttpError(400, `Inline credential is not allowed at ${childPath}; use an *_env setting instead`);
106
+ }
107
+ validateNoInlineCredentials(child, childPath);
108
+ }
109
+ }
110
+ function isInlineCredentialKey(key) {
111
+ if (/_env$/i.test(key))
112
+ return false;
113
+ return /^(authorization|bearer|password|secret|token|api[_-]?key|client[_-]?secret)$/i.test(key);
114
+ }
115
+ export async function receiveReport(store, envelope) {
116
+ const project = store.state.projects[envelope.project_key];
117
+ if (!project) {
118
+ throw new HttpError(404, `Unknown project_key: ${envelope.project_key}`);
119
+ }
120
+ const id = envelope.report_id || store.id("rep");
121
+ const redacted = redactValue(envelope, { mode: "strict" });
122
+ const telemetry = enrichTelemetry(project, {
123
+ ...(redacted.telemetry ?? {}),
124
+ ...(redacted.sentry ? legacySentryToTelemetry(redacted.sentry) : {}),
125
+ });
126
+ const sentry = telemetryToSentry(project, telemetry);
127
+ const sentryContext = redactValue(await fetchSentryEventContext(project, sentry), {
128
+ mode: "strict",
129
+ });
130
+ const contextTelemetry = enrichTelemetry(project, {
131
+ ...telemetry,
132
+ ...(sentryContext.telemetry ?? {}),
133
+ ...(sentryContext.sentry ? legacySentryToTelemetry(sentryContext.sentry) : {}),
134
+ });
135
+ const report = {
136
+ id,
137
+ project_key: envelope.project_key,
138
+ message: redacted.message ?? sentryContext.message,
139
+ impact: redacted.impact,
140
+ environment: redacted.environment ?? sentryContext.environment ?? project.environment,
141
+ release: redacted.release ?? sentryContext.release,
142
+ route: redacted.route ?? sentryContext.route,
143
+ browser: redacted.browser,
144
+ request_id: redacted.request_id ?? sentryContext.request_id,
145
+ screenshot_url: redacted.screenshot_url ?? sentryContext.screenshot_url,
146
+ feedback: redacted.feedback,
147
+ telemetry: contextTelemetry,
148
+ sentry: telemetryToSentry(project, contextTelemetry),
149
+ context: mergeObjects(sentryContext.context, redacted.context),
150
+ reproduction_steps: redacted.reproduction_steps ?? inferReproSteps(redacted),
151
+ expected_behavior: redacted.expected_behavior,
152
+ actual_behavior: redacted.actual_behavior,
153
+ suspected_area: redacted.suspected_area,
154
+ suspected_files: redacted.suspected_files ?? [],
155
+ hints: collectHints(redacted),
156
+ status: "received",
157
+ created_at: new Date().toISOString(),
158
+ };
159
+ report.hints = { ...collectHints(sentryContext), ...report.hints };
160
+ store.state.reports[id] = report;
161
+ store.audit("report.received", { project_key: project.project_key, report_id: id });
162
+ if (runnerCollectEnabled(project)) {
163
+ const job = createJob(store, {
164
+ type: "collect",
165
+ project_key: project.project_key,
166
+ report_id: id,
167
+ payload: {
168
+ report,
169
+ collector_plan: buildCollectorPlan(project, report),
170
+ },
171
+ });
172
+ report.status = "collect_queued";
173
+ store.save();
174
+ return { report, job, issue: null };
175
+ }
176
+ const issue = await createIssueFromReport(store, project, report);
177
+ return { report, job: null, issue };
178
+ }
179
+ export function nextRunnerJob(store, { projectKey, runnerId, capabilities, }) {
180
+ requeueStaleRunnerJobs(store, projectKey);
181
+ const project = projectKey ? store.state.projects[projectKey] : undefined;
182
+ if (!project || project.runner?.enabled === false)
183
+ return null;
184
+ const job = Object.values(store.state.jobs).find((candidate) => {
185
+ if (candidate.project_key !== projectKey || candidate.status !== "queued")
186
+ return false;
187
+ if (candidate.type === "fix" && !capabilities?.fix?.enabled)
188
+ return false;
189
+ if (candidate.type === "collect" && !capabilities?.collect?.enabled)
190
+ return false;
191
+ return true;
192
+ });
193
+ if (!job)
194
+ return null;
195
+ job.status = "leased";
196
+ job.runner_id = runnerId;
197
+ job.leased_at = new Date().toISOString();
198
+ job.heartbeat_at = job.leased_at;
199
+ job.lease_count = (job.lease_count ?? 0) + 1;
200
+ store.save();
201
+ store.audit("runner.job.leased", { job_id: job.id, runner_id: runnerId });
202
+ return job;
203
+ }
204
+ export function heartbeatJob(store, jobId) {
205
+ const job = requireJob(store, jobId);
206
+ job.heartbeat_at = new Date().toISOString();
207
+ store.save();
208
+ store.audit("runner.job.heartbeat", { job_id: job.id });
209
+ return job;
210
+ }
211
+ export async function completeJob(store, jobId, result) {
212
+ const job = requireJob(store, jobId);
213
+ const project = store.state.projects[job.project_key];
214
+ const validatedResult = validateRunnerResult(job, result);
215
+ job.status = "completed";
216
+ job.completed_at = new Date().toISOString();
217
+ job.result = redactValue(validatedResult, { mode: "strict" });
218
+ store.audit("runner.job.completed", { job_id: job.id, type: job.type });
219
+ if (job.type === "collect") {
220
+ auditCollectorPolicy(store, job);
221
+ if (!job.report_id)
222
+ throw new HttpError(409, "Collect job is missing report_id");
223
+ const report = store.state.reports[job.report_id];
224
+ if (!report)
225
+ throw new HttpError(404, `Unknown report: ${job.report_id}`);
226
+ if (!project)
227
+ throw new HttpError(404, `Unknown project: ${job.project_key}`);
228
+ report.status = "issue_created";
229
+ const issue = await createIssueFromReport(store, project, report, job.result);
230
+ store.save();
231
+ return { job, issue };
232
+ }
233
+ if (job.type === "fix") {
234
+ auditFixAgent(store, job);
235
+ if (!project)
236
+ throw new HttpError(404, `Unknown project: ${job.project_key}`);
237
+ if (!job.issue_id)
238
+ throw new HttpError(409, "Fix job is missing issue_id");
239
+ const issue = store.state.issues[job.issue_id];
240
+ if (!issue)
241
+ throw new HttpError(404, `Unknown issue: ${job.issue_id}`);
242
+ const draftPr = createLocalDraftPr({ store, project, job, result: job.result ?? {} });
243
+ let providerUpdate = null;
244
+ let providerUpdateError;
245
+ try {
246
+ providerUpdate = await updateProviderIssueAfterFix({
247
+ store,
248
+ project,
249
+ issue,
250
+ draftPr,
251
+ result: job.result ?? {},
252
+ });
253
+ }
254
+ catch (error) {
255
+ providerUpdateError = error instanceof Error ? error.message : String(error);
256
+ store.audit("fix.provider_update.failed", {
257
+ project_key: project.project_key,
258
+ issue_id: issue.id,
259
+ error: providerUpdateError,
260
+ });
261
+ }
262
+ issue.status = "local_draft_pr_created";
263
+ issue.fix = {
264
+ job_id: job.id,
265
+ draft_pr: draftPr,
266
+ provider_update: providerUpdate,
267
+ provider_update_error: providerUpdateError,
268
+ result: job.result,
269
+ };
270
+ store.save();
271
+ return { job, issue };
272
+ }
273
+ store.save();
274
+ return { job };
275
+ }
276
+ function validateRunnerResult(job, result) {
277
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
278
+ throw new HttpError(400, "Runner result must be a JSON object");
279
+ }
280
+ if (job.type === "collect") {
281
+ if (result.mode !== "collect")
282
+ throw new HttpError(400, "Collect result must have mode=collect");
283
+ if (result.context != null && !isObject(result.context)) {
284
+ throw new HttpError(400, "Collect result context must be an object");
285
+ }
286
+ if (result.policy != null && !Array.isArray(result.policy)) {
287
+ throw new HttpError(400, "Collect result policy must be an array");
288
+ }
289
+ }
290
+ if (job.type === "fix") {
291
+ if (result.mode !== "fix")
292
+ throw new HttpError(400, "Fix result must have mode=fix");
293
+ if (result.changed_files != null && !isStringArray(result.changed_files)) {
294
+ throw new HttpError(400, "Fix result changed_files must be a string array");
295
+ }
296
+ if (result.tests != null && !isStringArray(result.tests)) {
297
+ throw new HttpError(400, "Fix result tests must be a string array");
298
+ }
299
+ if (result.transcript != null && typeof result.transcript !== "string") {
300
+ throw new HttpError(400, "Fix result transcript must be a string");
301
+ }
302
+ }
303
+ return result;
304
+ }
305
+ function isObject(value) {
306
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
307
+ }
308
+ function isStringArray(value) {
309
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
310
+ }
311
+ function auditCollectorPolicy(store, job) {
312
+ const policy = job.result?.policy;
313
+ if (!Array.isArray(policy))
314
+ return;
315
+ for (const decision of policy) {
316
+ if (!decision || typeof decision !== "object")
317
+ continue;
318
+ store.audit("runner.collector.policy", {
319
+ job_id: job.id,
320
+ project_key: job.project_key,
321
+ source: String(decision.source ?? ""),
322
+ decision: String(decision.decision ?? ""),
323
+ reason: String(decision.reason ?? ""),
324
+ });
325
+ }
326
+ }
327
+ function auditFixAgent(store, job) {
328
+ const agent = job.result?.agent;
329
+ if (!agent || typeof agent !== "object" || Array.isArray(agent))
330
+ return;
331
+ store.audit("runner.fix.agent", {
332
+ job_id: job.id,
333
+ project_key: job.project_key,
334
+ issue_id: job.issue_id ?? "",
335
+ command_ref: String(agent.command_ref ?? ""),
336
+ exit_code: String(agent.exit_code ?? ""),
337
+ decision: String(agent.decision ?? ""),
338
+ });
339
+ }
340
+ export function failJob(store, jobId, error) {
341
+ const job = requireJob(store, jobId);
342
+ job.status = "failed";
343
+ job.failed_at = new Date().toISOString();
344
+ job.error = redactValue(error, { mode: "strict" });
345
+ store.save();
346
+ store.audit("runner.job.failed", { job_id: job.id });
347
+ return job;
348
+ }
349
+ export function approveFix(store, issueId) {
350
+ const issue = store.state.issues[issueId];
351
+ if (!issue) {
352
+ throw new HttpError(404, `Unknown issue: ${issueId}`);
353
+ }
354
+ const project = store.state.projects[issue.project_key];
355
+ if (!project)
356
+ throw new HttpError(404, `Unknown project: ${issue.project_key}`);
357
+ if (!runnerFixEnabled(project)) {
358
+ throw new HttpError(409, "Fix capability is disabled for this project");
359
+ }
360
+ if (issue.status === "local_draft_pr_created") {
361
+ throw new HttpError(409, "Fix already produced a local draft PR");
362
+ }
363
+ const existingJob = Object.values(store.state.jobs).find((job) => {
364
+ return (job.type === "fix" &&
365
+ job.issue_id === issue.id &&
366
+ ["queued", "leased", "completed"].includes(job.status));
367
+ });
368
+ if (existingJob) {
369
+ store.audit("fix.approval_reused", { issue_id: issue.id, job_id: existingJob.id });
370
+ return existingJob;
371
+ }
372
+ const job = createJob(store, {
373
+ type: "fix",
374
+ project_key: issue.project_key,
375
+ issue_id: issue.id,
376
+ report_id: issue.report_id,
377
+ payload: {
378
+ issue,
379
+ context_markdown: issue.body,
380
+ policy: {
381
+ merge: false,
382
+ deploy: false,
383
+ production_writes: false,
384
+ push: false,
385
+ },
386
+ },
387
+ });
388
+ issue.status = "fix_queued";
389
+ issue.fix_job_id = job.id;
390
+ store.save();
391
+ store.audit("fix.approved", { issue_id: issue.id, job_id: job.id });
392
+ return job;
393
+ }
394
+ function runnerCollectEnabled(project) {
395
+ return project.runner?.enabled !== false && project.runner?.collect?.enabled === true;
396
+ }
397
+ function runnerFixEnabled(project) {
398
+ return project.runner?.enabled !== false && project.runner?.fix?.enabled === true;
399
+ }
400
+ async function createIssueFromReport(store, project, report, collectorResult = null) {
401
+ const fingerprint = [project.project_key, report.telemetry?.event_id ?? report.sentry?.event_id, report.route, report.message]
402
+ .filter(Boolean)
403
+ .join("|");
404
+ const duplicate = Object.values(store.state.issues).find((issue) => issue.project_key === project.project_key && issue.fingerprint === fingerprint);
405
+ if (duplicate) {
406
+ duplicate.duplicate_report_ids = [...new Set([...(duplicate.duplicate_report_ids ?? []), report.id])];
407
+ store.audit("issue.duplicate.linked", { issue_id: duplicate.id, report_id: report.id });
408
+ store.save();
409
+ return duplicate;
410
+ }
411
+ const compiled = compileIssue({ report, project, collectorResult });
412
+ const issue = {
413
+ id: store.id("iss"),
414
+ project_key: project.project_key,
415
+ report_id: report.id,
416
+ title: compiled.title,
417
+ body: compiled.body,
418
+ fingerprint,
419
+ status: "issue_created",
420
+ created_at: new Date().toISOString(),
421
+ integration: null,
422
+ };
423
+ issue.integration = await createProviderIssue({ store, project, issue });
424
+ store.state.issues[issue.id] = issue;
425
+ store.save();
426
+ return issue;
427
+ }
428
+ function createJob(store, input) {
429
+ const job = {
430
+ id: store.id("job"),
431
+ type: input.type,
432
+ project_key: input.project_key,
433
+ report_id: input.report_id,
434
+ issue_id: input.issue_id,
435
+ status: "queued",
436
+ payload: input.payload,
437
+ created_at: new Date().toISOString(),
438
+ };
439
+ store.state.jobs[job.id] = job;
440
+ store.save();
441
+ store.audit("runner.job.queued", { job_id: job.id, type: job.type });
442
+ return job;
443
+ }
444
+ function requireJob(store, jobId) {
445
+ const job = store.state.jobs[jobId];
446
+ if (!job) {
447
+ throw new HttpError(404, `Unknown job: ${jobId}`);
448
+ }
449
+ return job;
450
+ }
451
+ function enrichSentry(project, sentry = {}) {
452
+ return {
453
+ ...sentry,
454
+ event_url: sentry.event_url ??
455
+ (sentry.event_id && project.sentry?.org_slug && project.sentry?.project_slug
456
+ ? `https://sentry.io/organizations/${project.sentry.org_slug}/issues/?query=${sentry.event_id}`
457
+ : undefined),
458
+ replay_url: sentry.replay_url ??
459
+ (sentry.replay_id && project.sentry?.org_slug
460
+ ? `https://sentry.io/organizations/${project.sentry.org_slug}/replays/${sentry.replay_id}/`
461
+ : undefined),
462
+ };
463
+ }
464
+ function enrichTelemetry(project, telemetry = {}) {
465
+ const provider = telemetry.provider ?? project.telemetry?.provider ?? "sentry";
466
+ return {
467
+ ...telemetry,
468
+ provider,
469
+ event_url: telemetry.event_url ??
470
+ (provider === "sentry" && telemetry.event_id && project.telemetry?.org_slug && project.telemetry?.project_slug
471
+ ? `https://sentry.io/organizations/${project.telemetry.org_slug}/issues/?query=${telemetry.event_id}`
472
+ : undefined),
473
+ replay_url: telemetry.replay_url ??
474
+ (provider === "sentry" && telemetry.replay_id && project.telemetry?.org_slug
475
+ ? `https://sentry.io/organizations/${project.telemetry.org_slug}/replays/${telemetry.replay_id}/`
476
+ : undefined),
477
+ };
478
+ }
479
+ function legacySentryToTelemetry(sentry = {}) {
480
+ return {
481
+ provider: "sentry",
482
+ event_id: sentry.event_id,
483
+ replay_id: sentry.replay_id,
484
+ feedback_id: sentry.feedback_id,
485
+ event_url: sentry.event_url,
486
+ replay_url: sentry.replay_url,
487
+ };
488
+ }
489
+ function telemetryToSentry(project, telemetry = {}) {
490
+ if (telemetry.provider && telemetry.provider !== "sentry")
491
+ return {};
492
+ return enrichSentry(project, {
493
+ event_id: telemetry.event_id,
494
+ replay_id: telemetry.replay_id,
495
+ feedback_id: telemetry.feedback_id,
496
+ event_url: telemetry.event_url,
497
+ replay_url: telemetry.replay_url,
498
+ });
499
+ }
500
+ function inferReproSteps(envelope) {
501
+ return [
502
+ envelope.route ? `Open ${envelope.route}.` : "Open the affected page.",
503
+ "Follow the user action sequence captured by session replay or report metadata.",
504
+ "Observe the reported failure.",
505
+ ];
506
+ }
507
+ function buildCollectorPlan(project, report) {
508
+ return {
509
+ project_key: project.project_key,
510
+ repo_path: project.runner?.repo_path ?? null,
511
+ hints: report.hints ?? {},
512
+ sources: project.runner?.sources ?? [],
513
+ };
514
+ }
515
+ function mergeRunnerConfig(input) {
516
+ const defaults = {
517
+ enabled: true,
518
+ repo_path: "/workspace/agency-platform",
519
+ lease_timeout_seconds: 120,
520
+ collect: { enabled: true, auto: true },
521
+ fix: { enabled: true, auto: false, require_approval: true },
522
+ sources: defaultCollectorSources(),
523
+ };
524
+ if (!input)
525
+ return defaults;
526
+ return {
527
+ ...defaults,
528
+ ...input,
529
+ collect: { ...defaults.collect, ...input.collect },
530
+ fix: { ...defaults.fix, ...input.fix },
531
+ sources: input.sources ?? defaults.sources,
532
+ };
533
+ }
534
+ function defaultCollectorSources() {
535
+ return [
536
+ { name: "api_logs_by_request", type: "fixture_logs", when: { has: "request_id" }, max_entries: 100 },
537
+ { name: "trip_snapshot", type: "fixture_domain", when: { has: "trip_id" } },
538
+ { name: "workspace_manifest", type: "command_ref", command_ref: "workspace_manifest_v1" },
539
+ ];
540
+ }
541
+ function requeueStaleRunnerJobs(store, projectKey) {
542
+ const project = projectKey ? store.state.projects[projectKey] : undefined;
543
+ const timeoutSeconds = project?.runner?.lease_timeout_seconds ?? 120;
544
+ const now = Date.now();
545
+ let changed = false;
546
+ for (const job of Object.values(store.state.jobs)) {
547
+ if (job.project_key !== projectKey || job.status !== "leased")
548
+ continue;
549
+ const lastSeen = Date.parse(job.heartbeat_at ?? job.leased_at ?? job.created_at);
550
+ if (!Number.isFinite(lastSeen))
551
+ continue;
552
+ if (now - lastSeen < timeoutSeconds * 1000)
553
+ continue;
554
+ job.status = "queued";
555
+ job.runner_id = undefined;
556
+ job.leased_at = undefined;
557
+ job.heartbeat_at = undefined;
558
+ changed = true;
559
+ store.audit("runner.job.requeued_stale", {
560
+ job_id: job.id,
561
+ project_key: job.project_key,
562
+ timeout_seconds: timeoutSeconds,
563
+ });
564
+ }
565
+ if (changed)
566
+ store.save();
567
+ }
568
+ function objectAt(value, path) {
569
+ const found = valueAt(value, path);
570
+ if (!found || typeof found !== "object" || Array.isArray(found))
571
+ return undefined;
572
+ return found;
573
+ }
574
+ function stringAt(value, path) {
575
+ const found = valueAt(value, path);
576
+ return typeof found === "string" && found.length > 0 ? found : undefined;
577
+ }
578
+ function valueAt(value, path) {
579
+ return path.split(".").reduce((current, key) => {
580
+ if (!current || typeof current !== "object")
581
+ return undefined;
582
+ return current[key];
583
+ }, value);
584
+ }
585
+ function mergeObjects(left, right) {
586
+ const output = {};
587
+ if (left && typeof left === "object" && !Array.isArray(left)) {
588
+ Object.assign(output, left);
589
+ }
590
+ if (right && typeof right === "object" && !Array.isArray(right)) {
591
+ Object.assign(output, right);
592
+ }
593
+ return output;
594
+ }
@@ -0,0 +1,6 @@
1
+ type RedactOptions = {
2
+ mode?: "strict" | "none";
3
+ };
4
+ export declare function redactValue<T>(value: T, options?: RedactOptions): T;
5
+ export declare function collectHints(input: unknown): Record<string, string>;
6
+ export {};
@@ -0,0 +1,53 @@
1
+ const SENSITIVE_KEY_PATTERN = /(authorization|bearer|cookie|email|passport|password|phone|secret|ssn|token)/i;
2
+ export function redactValue(value, options = {}) {
3
+ const mode = options.mode ?? "strict";
4
+ if (value == null)
5
+ return value;
6
+ if (typeof value === "string") {
7
+ let out = value;
8
+ out = out.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "[redacted:email]");
9
+ if (!containsUuidLike(out)) {
10
+ out = out.replace(/(?<![A-Za-z0-9/_-])\+?\d[\d\s().-]{7,}\d(?![A-Za-z0-9/_-])/g, "[redacted:phone]");
11
+ }
12
+ if (mode === "strict" && out.length > 300)
13
+ out = `${out.slice(0, 300)}...`;
14
+ return out;
15
+ }
16
+ if (Array.isArray(value))
17
+ return value.map((item) => redactValue(item, options));
18
+ if (typeof value === "object") {
19
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [
20
+ key,
21
+ SENSITIVE_KEY_PATTERN.test(key) ? "[redacted]" : redactValue(child, options),
22
+ ]));
23
+ }
24
+ return value;
25
+ }
26
+ function containsUuidLike(value) {
27
+ return /[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i.test(value);
28
+ }
29
+ export function collectHints(input) {
30
+ const hints = {};
31
+ const visit = (value) => {
32
+ if (!value || typeof value !== "object")
33
+ return;
34
+ for (const [key, child] of Object.entries(value)) {
35
+ if ([
36
+ "request_id",
37
+ "trace_id",
38
+ "tenant_id",
39
+ "user_id",
40
+ "trip_id",
41
+ "booking_id",
42
+ "proposal_id",
43
+ ].includes(key) &&
44
+ child != null) {
45
+ hints[key] = String(child);
46
+ }
47
+ if (typeof child === "object")
48
+ visit(child);
49
+ }
50
+ };
51
+ visit(input);
52
+ return hints;
53
+ }
@@ -0,0 +1,26 @@
1
+ import type { Capabilities, JsonObject } from "./types.js";
2
+ type RunnerConfig = {
3
+ server_url: string;
4
+ project_key: string;
5
+ runner_id?: string;
6
+ repo_path?: string;
7
+ artifact_dir?: string;
8
+ token_env?: string;
9
+ token?: string;
10
+ capabilities?: Capabilities;
11
+ agent?: {
12
+ fix?: {
13
+ command_ref?: string;
14
+ commands?: Record<string, {
15
+ bin: string;
16
+ args?: string[];
17
+ timeout_ms?: number;
18
+ }>;
19
+ };
20
+ };
21
+ };
22
+ type RunnerResult = {
23
+ idle: true;
24
+ } | JsonObject;
25
+ export declare function runOnce(config: RunnerConfig): Promise<RunnerResult>;
26
+ export {};