@neat.is/core 0.9.20-dev.20260922 → 0.9.21-dev.20260923

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/index.cjs CHANGED
@@ -6614,9 +6614,30 @@ function withRecoveredCodeAttrs(attrs, locus) {
6614
6614
  if (locus.codeLineno !== void 0) attrs[CODE_LINENO_ATTR] = locus.codeLineno;
6615
6615
  return attrs;
6616
6616
  }
6617
+ var SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
6618
+ "authorization",
6619
+ "proxy-authorization",
6620
+ "cookie",
6621
+ "set-cookie",
6622
+ "x-api-key",
6623
+ "api-key",
6624
+ "x-auth-token",
6625
+ "x-amz-security-token",
6626
+ "x-csrf-token"
6627
+ ]);
6628
+ function attrHeaderName(key) {
6629
+ const lower = key.toLowerCase();
6630
+ const marker = ".header.";
6631
+ const at = lower.lastIndexOf(marker);
6632
+ return at >= 0 ? lower.slice(at + marker.length) : lower;
6633
+ }
6634
+ function isSensitiveAttrKey(key) {
6635
+ return SENSITIVE_HEADER_NAMES.has(attrHeaderName(key));
6636
+ }
6617
6637
  function sanitizeAttributes(attrs) {
6618
6638
  const out = {};
6619
6639
  for (const [k, v] of Object.entries(attrs)) {
6640
+ if (isSensitiveAttrKey(k)) continue;
6620
6641
  if (typeof v === "bigint") out[k] = v.toString();
6621
6642
  else out[k] = v;
6622
6643
  }
@@ -7207,6 +7228,14 @@ async function readErrorFileTail(errorsPath, maxBytes) {
7207
7228
  await handle.close();
7208
7229
  }
7209
7230
  }
7231
+ function redactPersistedAttributes(ev) {
7232
+ if (!ev.attributes) return ev;
7233
+ const scrubbed = sanitizeAttributes(ev.attributes);
7234
+ if (Object.keys(scrubbed).length === Object.keys(ev.attributes).length) return ev;
7235
+ if (Object.keys(scrubbed).length > 0) return { ...ev, attributes: scrubbed };
7236
+ const { attributes: _dropped, ...rest } = ev;
7237
+ return rest;
7238
+ }
7210
7239
  async function readErrorEvents(errorsPath, opts) {
7211
7240
  let raw;
7212
7241
  try {
@@ -7215,7 +7244,7 @@ async function readErrorEvents(errorsPath, opts) {
7215
7244
  if (err.code === "ENOENT") return [];
7216
7245
  throw err;
7217
7246
  }
7218
- const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
7247
+ const events = raw.split("\n").filter((line) => line.length > 0).map((line) => redactPersistedAttributes(JSON.parse(line)));
7219
7248
  const deduped = dedupeIncidents(events);
7220
7249
  const cap = opts?.limit !== void 0 && opts.limit > 0 ? Math.min(opts.limit, INCIDENT_READ_MAX_EVENTS) : INCIDENT_READ_MAX_EVENTS;
7221
7250
  return deduped.length > cap ? deduped.slice(deduped.length - cap) : deduped;
@@ -19195,6 +19224,10 @@ function startConnectorPollLoop(connector, ctx, graph, resolveTarget, options =
19195
19224
  // src/connectors/registry.ts
19196
19225
  init_cjs_shims();
19197
19226
 
19227
+ // src/connectors/gcp-auth.ts
19228
+ init_cjs_shims();
19229
+ var import_node_crypto3 = require("crypto");
19230
+
19198
19231
  // src/connectors/junction.ts
19199
19232
  init_cjs_shims();
19200
19233
  var buckets = /* @__PURE__ */ new Map();
@@ -19456,6 +19489,108 @@ async function dbJunction(run, policy) {
19456
19489
  }
19457
19490
  }
19458
19491
 
19492
+ // src/connectors/gcp-auth.ts
19493
+ var DEFAULT_TOKEN_URI = "https://oauth2.googleapis.com/token";
19494
+ var JWT_BEARER_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
19495
+ var ASSERTION_LIFETIME_S = 3600;
19496
+ var DEFAULT_REFRESH_SKEW_MS = 6e4;
19497
+ var GCP_LOGGING_READ_SCOPE = "https://www.googleapis.com/auth/logging.read";
19498
+ function parseServiceAccountKey(json) {
19499
+ let raw;
19500
+ try {
19501
+ raw = JSON.parse(json);
19502
+ } catch {
19503
+ throw new Error("gcp service-account key: not valid JSON");
19504
+ }
19505
+ if (!raw || typeof raw !== "object") {
19506
+ throw new Error("gcp service-account key: expected a JSON object");
19507
+ }
19508
+ const obj = raw;
19509
+ const clientEmail = obj["client_email"];
19510
+ const privateKey = obj["private_key"];
19511
+ const projectId = obj["project_id"];
19512
+ if (typeof clientEmail !== "string" || clientEmail.length === 0) {
19513
+ throw new Error("gcp service-account key: missing client_email");
19514
+ }
19515
+ if (typeof privateKey !== "string" || privateKey.length === 0) {
19516
+ throw new Error("gcp service-account key: missing private_key");
19517
+ }
19518
+ if (typeof projectId !== "string" || projectId.length === 0) {
19519
+ throw new Error("gcp service-account key: missing project_id");
19520
+ }
19521
+ const key = { client_email: clientEmail, private_key: privateKey, project_id: projectId };
19522
+ if (typeof obj["token_uri"] === "string" && obj["token_uri"].length > 0) {
19523
+ key.token_uri = obj["token_uri"];
19524
+ }
19525
+ return key;
19526
+ }
19527
+ function base64url(input) {
19528
+ return Buffer.from(input).toString("base64url");
19529
+ }
19530
+ function buildServiceAccountAssertion(key, scope, tokenUri, nowMs) {
19531
+ const iat = Math.floor(nowMs / 1e3);
19532
+ const header = { alg: "RS256", typ: "JWT" };
19533
+ const claims = {
19534
+ iss: key.client_email,
19535
+ scope,
19536
+ aud: tokenUri,
19537
+ iat,
19538
+ exp: iat + ASSERTION_LIFETIME_S
19539
+ };
19540
+ const signingInput = `${base64url(JSON.stringify(header))}.${base64url(JSON.stringify(claims))}`;
19541
+ const signature = (0, import_node_crypto3.createSign)("RSA-SHA256").update(signingInput).end().sign(key.private_key);
19542
+ return `${signingInput}.${base64url(signature)}`;
19543
+ }
19544
+ async function mintGcpAccessToken(key, scope, opts = {}) {
19545
+ const now = opts.now ?? Date.now;
19546
+ const tokenUri = opts.tokenUri ?? key.token_uri ?? DEFAULT_TOKEN_URI;
19547
+ const mintedAtMs = now();
19548
+ const assertion = buildServiceAccountAssertion(key, scope, tokenUri, mintedAtMs);
19549
+ const body = new URLSearchParams({ grant_type: JWT_BEARER_GRANT, assertion }).toString();
19550
+ const res = await junctionFetch(
19551
+ tokenUri,
19552
+ {
19553
+ method: "POST",
19554
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
19555
+ body
19556
+ },
19557
+ // accountKey: the GCP project id — an identifier, safe to log, the same
19558
+ // per-(provider, accountKey) rate-limit bucket key the GCP connectors use.
19559
+ { provider: "gcp-auth", accountKey: key.project_id, ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {} }
19560
+ );
19561
+ if (!res.ok) {
19562
+ throw new Error(`gcp token mint failed: ${res.status} ${res.statusText}`);
19563
+ }
19564
+ const json = await res.json();
19565
+ const accessToken = json.access_token;
19566
+ if (typeof accessToken !== "string" || accessToken.length === 0) {
19567
+ throw new Error("gcp token mint returned no access_token");
19568
+ }
19569
+ const expiresInS = typeof json.expires_in === "number" && json.expires_in > 0 ? json.expires_in : ASSERTION_LIFETIME_S;
19570
+ return { accessToken, expiresAtMs: mintedAtMs + expiresInS * 1e3 };
19571
+ }
19572
+ function createGcpTokenSource(key, scope, opts = {}) {
19573
+ const now = opts.now ?? Date.now;
19574
+ const skewMs = opts.refreshSkewMs ?? DEFAULT_REFRESH_SKEW_MS;
19575
+ let cached = null;
19576
+ let inFlight = null;
19577
+ const fresh = () => cached !== null && now() < cached.expiresAtMs - skewMs;
19578
+ return async () => {
19579
+ if (!fresh()) {
19580
+ if (!inFlight) {
19581
+ inFlight = mintGcpAccessToken(key, scope, opts).then((minted) => {
19582
+ cached = minted;
19583
+ return minted;
19584
+ }).finally(() => {
19585
+ inFlight = null;
19586
+ });
19587
+ }
19588
+ await inFlight;
19589
+ }
19590
+ return { projectId: key.project_id, accessToken: cached.accessToken };
19591
+ };
19592
+ }
19593
+
19459
19594
  // src/connectors/supabase/index.ts
19460
19595
  init_cjs_shims();
19461
19596
 
@@ -20322,7 +20457,7 @@ var import_types73 = require("@neat.is/types");
20322
20457
 
20323
20458
  // src/connectors/cloudflare/client.ts
20324
20459
  init_cjs_shims();
20325
- var import_node_crypto3 = require("crypto");
20460
+ var import_node_crypto4 = require("crypto");
20326
20461
  var DEFAULT_BASE_URL = "https://api.cloudflare.com/client/v4";
20327
20462
  var DEFAULT_EVENT_LIMIT = 1e3;
20328
20463
  async function queryWorkerInvocations(ctx, config, window, fetchImpl = fetch) {
@@ -20335,7 +20470,7 @@ async function queryWorkerInvocations(ctx, config, window, fetchImpl = fetch) {
20335
20470
  const body = {
20336
20471
  // Cloudflare's schema requires an identifier per query even for an
20337
20472
  // ad-hoc, unsaved one — a fresh id per tick, never reused.
20338
- queryId: `neat-connector-${(0, import_node_crypto3.randomUUID)()}`,
20473
+ queryId: `neat-connector-${(0, import_node_crypto4.randomUUID)()}`,
20339
20474
  timeframe: { from: window.fromMs, to: window.toMs },
20340
20475
  view: "events",
20341
20476
  limit: config.eventLimit ?? DEFAULT_EVENT_LIMIT,
@@ -22337,7 +22472,8 @@ var PUSH_PROVIDER_DISPATCH = {
22337
22472
  function isPushProvider(provider) {
22338
22473
  return provider in PUSH_PROVIDER_DISPATCH;
22339
22474
  }
22340
- function resolveEntryCredentials(dispatch, entry, env) {
22475
+ var GCP_SERVICE_ACCOUNT_PROVIDERS = /* @__PURE__ */ new Set(["cloud-run", "gcp-lb", "firebase"]);
22476
+ function resolveEntryCredentials(dispatch, entry, env, fetchImpl) {
22341
22477
  let credentials;
22342
22478
  try {
22343
22479
  const resolved = resolveCredential(entry.credential, env);
@@ -22346,6 +22482,15 @@ function resolveEntryCredentials(dispatch, entry, env) {
22346
22482
  if (err instanceof EnvRefUnsetError) return { ok: false, kind: "unset-env", reason: err.message };
22347
22483
  return { ok: false, kind: "error", reason: err.message };
22348
22484
  }
22485
+ if (GCP_SERVICE_ACCOUNT_PROVIDERS.has(entry.provider) && typeof credentials.serviceAccountKey === "string" && credentials.serviceAccountKey.length > 0) {
22486
+ try {
22487
+ const key = parseServiceAccountKey(credentials.serviceAccountKey);
22488
+ const source = createGcpTokenSource(key, GCP_LOGGING_READ_SCOPE, fetchImpl ? { fetchImpl } : {});
22489
+ return { ok: true, credentials: { projectId: key.project_id }, refreshCredentials: source };
22490
+ } catch (err) {
22491
+ return { ok: false, kind: "error", reason: err.message };
22492
+ }
22493
+ }
22349
22494
  const missingCreds = dispatch.requiredCredentialFields.filter((k) => !credentials[k]);
22350
22495
  if (missingCreds.length > 0) {
22351
22496
  return {
@@ -22395,7 +22540,10 @@ function buildRegistration(entry, graph, env = process.env) {
22395
22540
  connector: built.connector,
22396
22541
  credentials,
22397
22542
  resolveTarget: built.resolveTarget,
22398
- ...intervalMs !== void 0 ? { intervalMs } : {}
22543
+ ...intervalMs !== void 0 ? { intervalMs } : {},
22544
+ // A refreshable credential (ADR-230) resolves to a per-tick token source; carry it so the daemon wires
22545
+ // it to the poll loop's `refreshCredentials`. Absent for a static credential.
22546
+ ...creds.refreshCredentials ? { refreshCredentials: creds.refreshCredentials } : {}
22399
22547
  }
22400
22548
  };
22401
22549
  }
@@ -22439,7 +22587,13 @@ async function startConnectorPolling(input) {
22439
22587
  },
22440
22588
  input.graph,
22441
22589
  registration.resolveTarget,
22442
- { intervalMs: registration.intervalMs, connectorId: registration.id }
22590
+ {
22591
+ intervalMs: registration.intervalMs,
22592
+ connectorId: registration.id,
22593
+ // A refreshable credential (ADR-230) mints a fresh token each tick; the static path leaves this unset
22594
+ // and the loop reuses ctx.credentials, exactly as before.
22595
+ ...registration.refreshCredentials ? { refreshCredentials: registration.refreshCredentials } : {}
22596
+ }
22443
22597
  )
22444
22598
  );
22445
22599
  return () => {
@@ -23405,6 +23559,37 @@ async function reconcileFrontierSurfaces(graph, errorsPath) {
23405
23559
 
23406
23560
  // src/connectors/hosted.ts
23407
23561
  init_cjs_shims();
23562
+
23563
+ // src/connectors/railway/target-ref.ts
23564
+ init_cjs_shims();
23565
+ function nonEmptyString(v) {
23566
+ return typeof v === "string" && v.length > 0;
23567
+ }
23568
+ function decodeRailwayTargetRef(ref) {
23569
+ if (typeof ref !== "string" || ref.length === 0) return null;
23570
+ let json;
23571
+ try {
23572
+ json = Buffer.from(ref, "base64url").toString("utf8");
23573
+ } catch {
23574
+ return null;
23575
+ }
23576
+ let parsed;
23577
+ try {
23578
+ parsed = JSON.parse(json);
23579
+ } catch {
23580
+ return null;
23581
+ }
23582
+ if (!parsed || typeof parsed !== "object") return null;
23583
+ const obj = parsed;
23584
+ if (!nonEmptyString(obj.environmentId) || !nonEmptyString(obj.serviceId)) return null;
23585
+ return {
23586
+ environmentId: obj.environmentId,
23587
+ serviceId: obj.serviceId,
23588
+ ...nonEmptyString(obj.serviceName) ? { serviceName: obj.serviceName } : {}
23589
+ };
23590
+ }
23591
+
23592
+ // src/connectors/hosted.ts
23408
23593
  var CREDENTIAL_REFRESH_SKEW_MS = 6e4;
23409
23594
  var CP_REQUEST_TIMEOUT_MS = 1e4;
23410
23595
  async function cpGet(path79, deps) {
@@ -23431,6 +23616,15 @@ function hostedOptions(provider, summary, serviceName) {
23431
23616
  if (!ref) return null;
23432
23617
  return { apiProjectRef: ref, nodeRef: `${ref}.supabase.co`, serviceName };
23433
23618
  }
23619
+ case "railway": {
23620
+ const target = summary.projectRef ? decodeRailwayTargetRef(summary.projectRef) : null;
23621
+ if (!target) return null;
23622
+ return {
23623
+ environmentId: target.environmentId,
23624
+ serviceId: target.serviceId,
23625
+ serviceNameById: { [target.serviceId]: serviceName }
23626
+ };
23627
+ }
23434
23628
  default:
23435
23629
  return null;
23436
23630
  }