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

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.d.cts CHANGED
@@ -188,6 +188,15 @@ interface ObservedSignal {
188
188
  readyReplicas?: number;
189
189
  };
190
190
  }
191
+ /**
192
+ * A per-tick credential source (docs/contracts/connector-config.md §9, ADR-230) — the shape the poll loop's
193
+ * `refreshCredentials` seam (index.ts) already consumes for the hosted profile. A credential that mints a
194
+ * short-lived token (a GCP service-account key → a ~1h Google access token) resolves to one of these instead
195
+ * of a fixed value: the loop calls it each tick to get that tick's credentials, and the source caches and
196
+ * re-mints behind its own expiry logic. `poll()` is unchanged — the connector still receives an opaque,
197
+ * already-minted `credentials` record and performs no auth handshake of its own (connectors.md §3).
198
+ */
199
+ type CredentialSource = () => Promise<Record<string, unknown>>;
191
200
 
192
201
  /**
193
202
  * What a provider's target-resolution step hands back for one signal. The
@@ -250,6 +259,7 @@ interface ConnectorRegistration {
250
259
  credentials: Record<string, unknown>;
251
260
  resolveTarget: ResolveConnectorTarget;
252
261
  intervalMs?: number;
262
+ refreshCredentials?: CredentialSource;
253
263
  }
254
264
 
255
265
  interface BuildApiOptions {
package/dist/index.d.ts CHANGED
@@ -188,6 +188,15 @@ interface ObservedSignal {
188
188
  readyReplicas?: number;
189
189
  };
190
190
  }
191
+ /**
192
+ * A per-tick credential source (docs/contracts/connector-config.md §9, ADR-230) — the shape the poll loop's
193
+ * `refreshCredentials` seam (index.ts) already consumes for the hosted profile. A credential that mints a
194
+ * short-lived token (a GCP service-account key → a ~1h Google access token) resolves to one of these instead
195
+ * of a fixed value: the loop calls it each tick to get that tick's credentials, and the source caches and
196
+ * re-mints behind its own expiry logic. `poll()` is unchanged — the connector still receives an opaque,
197
+ * already-minted `credentials` record and performs no auth handshake of its own (connectors.md §3).
198
+ */
199
+ type CredentialSource = () => Promise<Record<string, unknown>>;
191
200
 
192
201
  /**
193
202
  * What a provider's target-resolution step hands back for one signal. The
@@ -250,6 +259,7 @@ interface ConnectorRegistration {
250
259
  credentials: Record<string, unknown>;
251
260
  resolveTarget: ResolveConnectorTarget;
252
261
  intervalMs?: number;
262
+ refreshCredentials?: CredentialSource;
253
263
  }
254
264
 
255
265
  interface BuildApiOptions {
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  routeSpanToProject,
3
3
  startDaemon
4
- } from "./chunk-F7I3G4KC.js";
4
+ } from "./chunk-SK2FEJKB.js";
5
5
  import {
6
6
  ProjectNameCollisionError,
7
7
  addProject,
@@ -37,7 +37,7 @@ import {
37
37
  thresholdForEdgeType,
38
38
  touchLastSeen,
39
39
  writeAtomically
40
- } from "./chunk-KIV7OB2K.js";
40
+ } from "./chunk-NFJ4W5T6.js";
41
41
  import {
42
42
  startOtelGrpcReceiver
43
43
  } from "./chunk-ERE47MCR.js";
package/dist/neatd.cjs CHANGED
@@ -6577,9 +6577,30 @@ function withRecoveredCodeAttrs(attrs, locus) {
6577
6577
  if (locus.codeLineno !== void 0) attrs[CODE_LINENO_ATTR] = locus.codeLineno;
6578
6578
  return attrs;
6579
6579
  }
6580
+ var SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
6581
+ "authorization",
6582
+ "proxy-authorization",
6583
+ "cookie",
6584
+ "set-cookie",
6585
+ "x-api-key",
6586
+ "api-key",
6587
+ "x-auth-token",
6588
+ "x-amz-security-token",
6589
+ "x-csrf-token"
6590
+ ]);
6591
+ function attrHeaderName(key) {
6592
+ const lower = key.toLowerCase();
6593
+ const marker = ".header.";
6594
+ const at = lower.lastIndexOf(marker);
6595
+ return at >= 0 ? lower.slice(at + marker.length) : lower;
6596
+ }
6597
+ function isSensitiveAttrKey(key) {
6598
+ return SENSITIVE_HEADER_NAMES.has(attrHeaderName(key));
6599
+ }
6580
6600
  function sanitizeAttributes(attrs) {
6581
6601
  const out = {};
6582
6602
  for (const [k, v] of Object.entries(attrs)) {
6603
+ if (isSensitiveAttrKey(k)) continue;
6583
6604
  if (typeof v === "bigint") out[k] = v.toString();
6584
6605
  else out[k] = v;
6585
6606
  }
@@ -7167,6 +7188,14 @@ async function readErrorFileTail(errorsPath, maxBytes) {
7167
7188
  await handle.close();
7168
7189
  }
7169
7190
  }
7191
+ function redactPersistedAttributes(ev) {
7192
+ if (!ev.attributes) return ev;
7193
+ const scrubbed = sanitizeAttributes(ev.attributes);
7194
+ if (Object.keys(scrubbed).length === Object.keys(ev.attributes).length) return ev;
7195
+ if (Object.keys(scrubbed).length > 0) return { ...ev, attributes: scrubbed };
7196
+ const { attributes: _dropped, ...rest } = ev;
7197
+ return rest;
7198
+ }
7170
7199
  async function readErrorEvents(errorsPath, opts) {
7171
7200
  let raw;
7172
7201
  try {
@@ -7175,7 +7204,7 @@ async function readErrorEvents(errorsPath, opts) {
7175
7204
  if (err.code === "ENOENT") return [];
7176
7205
  throw err;
7177
7206
  }
7178
- const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
7207
+ const events = raw.split("\n").filter((line) => line.length > 0).map((line) => redactPersistedAttributes(JSON.parse(line)));
7179
7208
  const deduped = dedupeIncidents(events);
7180
7209
  const cap = opts?.limit !== void 0 && opts.limit > 0 ? Math.min(opts.limit, INCIDENT_READ_MAX_EVENTS) : INCIDENT_READ_MAX_EVENTS;
7181
7210
  return deduped.length > cap ? deduped.slice(deduped.length - cap) : deduped;
@@ -19097,6 +19126,10 @@ function startConnectorPollLoop(connector, ctx, graph, resolveTarget, options =
19097
19126
  // src/connectors/registry.ts
19098
19127
  init_cjs_shims();
19099
19128
 
19129
+ // src/connectors/gcp-auth.ts
19130
+ init_cjs_shims();
19131
+ var import_node_crypto3 = require("crypto");
19132
+
19100
19133
  // src/connectors/junction.ts
19101
19134
  init_cjs_shims();
19102
19135
  var buckets = /* @__PURE__ */ new Map();
@@ -19358,6 +19391,108 @@ async function dbJunction(run, policy) {
19358
19391
  }
19359
19392
  }
19360
19393
 
19394
+ // src/connectors/gcp-auth.ts
19395
+ var DEFAULT_TOKEN_URI = "https://oauth2.googleapis.com/token";
19396
+ var JWT_BEARER_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
19397
+ var ASSERTION_LIFETIME_S = 3600;
19398
+ var DEFAULT_REFRESH_SKEW_MS = 6e4;
19399
+ var GCP_LOGGING_READ_SCOPE = "https://www.googleapis.com/auth/logging.read";
19400
+ function parseServiceAccountKey(json) {
19401
+ let raw;
19402
+ try {
19403
+ raw = JSON.parse(json);
19404
+ } catch {
19405
+ throw new Error("gcp service-account key: not valid JSON");
19406
+ }
19407
+ if (!raw || typeof raw !== "object") {
19408
+ throw new Error("gcp service-account key: expected a JSON object");
19409
+ }
19410
+ const obj = raw;
19411
+ const clientEmail = obj["client_email"];
19412
+ const privateKey = obj["private_key"];
19413
+ const projectId = obj["project_id"];
19414
+ if (typeof clientEmail !== "string" || clientEmail.length === 0) {
19415
+ throw new Error("gcp service-account key: missing client_email");
19416
+ }
19417
+ if (typeof privateKey !== "string" || privateKey.length === 0) {
19418
+ throw new Error("gcp service-account key: missing private_key");
19419
+ }
19420
+ if (typeof projectId !== "string" || projectId.length === 0) {
19421
+ throw new Error("gcp service-account key: missing project_id");
19422
+ }
19423
+ const key = { client_email: clientEmail, private_key: privateKey, project_id: projectId };
19424
+ if (typeof obj["token_uri"] === "string" && obj["token_uri"].length > 0) {
19425
+ key.token_uri = obj["token_uri"];
19426
+ }
19427
+ return key;
19428
+ }
19429
+ function base64url(input) {
19430
+ return Buffer.from(input).toString("base64url");
19431
+ }
19432
+ function buildServiceAccountAssertion(key, scope, tokenUri, nowMs) {
19433
+ const iat = Math.floor(nowMs / 1e3);
19434
+ const header = { alg: "RS256", typ: "JWT" };
19435
+ const claims = {
19436
+ iss: key.client_email,
19437
+ scope,
19438
+ aud: tokenUri,
19439
+ iat,
19440
+ exp: iat + ASSERTION_LIFETIME_S
19441
+ };
19442
+ const signingInput = `${base64url(JSON.stringify(header))}.${base64url(JSON.stringify(claims))}`;
19443
+ const signature = (0, import_node_crypto3.createSign)("RSA-SHA256").update(signingInput).end().sign(key.private_key);
19444
+ return `${signingInput}.${base64url(signature)}`;
19445
+ }
19446
+ async function mintGcpAccessToken(key, scope, opts = {}) {
19447
+ const now = opts.now ?? Date.now;
19448
+ const tokenUri = opts.tokenUri ?? key.token_uri ?? DEFAULT_TOKEN_URI;
19449
+ const mintedAtMs = now();
19450
+ const assertion = buildServiceAccountAssertion(key, scope, tokenUri, mintedAtMs);
19451
+ const body = new URLSearchParams({ grant_type: JWT_BEARER_GRANT, assertion }).toString();
19452
+ const res = await junctionFetch(
19453
+ tokenUri,
19454
+ {
19455
+ method: "POST",
19456
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
19457
+ body
19458
+ },
19459
+ // accountKey: the GCP project id — an identifier, safe to log, the same
19460
+ // per-(provider, accountKey) rate-limit bucket key the GCP connectors use.
19461
+ { provider: "gcp-auth", accountKey: key.project_id, ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {} }
19462
+ );
19463
+ if (!res.ok) {
19464
+ throw new Error(`gcp token mint failed: ${res.status} ${res.statusText}`);
19465
+ }
19466
+ const json = await res.json();
19467
+ const accessToken = json.access_token;
19468
+ if (typeof accessToken !== "string" || accessToken.length === 0) {
19469
+ throw new Error("gcp token mint returned no access_token");
19470
+ }
19471
+ const expiresInS = typeof json.expires_in === "number" && json.expires_in > 0 ? json.expires_in : ASSERTION_LIFETIME_S;
19472
+ return { accessToken, expiresAtMs: mintedAtMs + expiresInS * 1e3 };
19473
+ }
19474
+ function createGcpTokenSource(key, scope, opts = {}) {
19475
+ const now = opts.now ?? Date.now;
19476
+ const skewMs = opts.refreshSkewMs ?? DEFAULT_REFRESH_SKEW_MS;
19477
+ let cached = null;
19478
+ let inFlight = null;
19479
+ const fresh = () => cached !== null && now() < cached.expiresAtMs - skewMs;
19480
+ return async () => {
19481
+ if (!fresh()) {
19482
+ if (!inFlight) {
19483
+ inFlight = mintGcpAccessToken(key, scope, opts).then((minted) => {
19484
+ cached = minted;
19485
+ return minted;
19486
+ }).finally(() => {
19487
+ inFlight = null;
19488
+ });
19489
+ }
19490
+ await inFlight;
19491
+ }
19492
+ return { projectId: key.project_id, accessToken: cached.accessToken };
19493
+ };
19494
+ }
19495
+
19361
19496
  // src/connectors/supabase/index.ts
19362
19497
  init_cjs_shims();
19363
19498
 
@@ -20224,7 +20359,7 @@ var import_types73 = require("@neat.is/types");
20224
20359
 
20225
20360
  // src/connectors/cloudflare/client.ts
20226
20361
  init_cjs_shims();
20227
- var import_node_crypto3 = require("crypto");
20362
+ var import_node_crypto4 = require("crypto");
20228
20363
  var DEFAULT_BASE_URL = "https://api.cloudflare.com/client/v4";
20229
20364
  var DEFAULT_EVENT_LIMIT = 1e3;
20230
20365
  async function queryWorkerInvocations(ctx, config, window, fetchImpl = fetch) {
@@ -20237,7 +20372,7 @@ async function queryWorkerInvocations(ctx, config, window, fetchImpl = fetch) {
20237
20372
  const body = {
20238
20373
  // Cloudflare's schema requires an identifier per query even for an
20239
20374
  // ad-hoc, unsaved one — a fresh id per tick, never reused.
20240
- queryId: `neat-connector-${(0, import_node_crypto3.randomUUID)()}`,
20375
+ queryId: `neat-connector-${(0, import_node_crypto4.randomUUID)()}`,
20241
20376
  timeframe: { from: window.fromMs, to: window.toMs },
20242
20377
  view: "events",
20243
20378
  limit: config.eventLimit ?? DEFAULT_EVENT_LIMIT,
@@ -22239,7 +22374,8 @@ var PUSH_PROVIDER_DISPATCH = {
22239
22374
  function isPushProvider(provider) {
22240
22375
  return provider in PUSH_PROVIDER_DISPATCH;
22241
22376
  }
22242
- function resolveEntryCredentials(dispatch, entry2, env) {
22377
+ var GCP_SERVICE_ACCOUNT_PROVIDERS = /* @__PURE__ */ new Set(["cloud-run", "gcp-lb", "firebase"]);
22378
+ function resolveEntryCredentials(dispatch, entry2, env, fetchImpl) {
22243
22379
  let credentials;
22244
22380
  try {
22245
22381
  const resolved = resolveCredential(entry2.credential, env);
@@ -22248,6 +22384,15 @@ function resolveEntryCredentials(dispatch, entry2, env) {
22248
22384
  if (err instanceof EnvRefUnsetError) return { ok: false, kind: "unset-env", reason: err.message };
22249
22385
  return { ok: false, kind: "error", reason: err.message };
22250
22386
  }
22387
+ if (GCP_SERVICE_ACCOUNT_PROVIDERS.has(entry2.provider) && typeof credentials.serviceAccountKey === "string" && credentials.serviceAccountKey.length > 0) {
22388
+ try {
22389
+ const key = parseServiceAccountKey(credentials.serviceAccountKey);
22390
+ const source = createGcpTokenSource(key, GCP_LOGGING_READ_SCOPE, fetchImpl ? { fetchImpl } : {});
22391
+ return { ok: true, credentials: { projectId: key.project_id }, refreshCredentials: source };
22392
+ } catch (err) {
22393
+ return { ok: false, kind: "error", reason: err.message };
22394
+ }
22395
+ }
22251
22396
  const missingCreds = dispatch.requiredCredentialFields.filter((k) => !credentials[k]);
22252
22397
  if (missingCreds.length > 0) {
22253
22398
  return {
@@ -22297,7 +22442,10 @@ function buildRegistration(entry2, graph, env = process.env) {
22297
22442
  connector: built.connector,
22298
22443
  credentials,
22299
22444
  resolveTarget: built.resolveTarget,
22300
- ...intervalMs !== void 0 ? { intervalMs } : {}
22445
+ ...intervalMs !== void 0 ? { intervalMs } : {},
22446
+ // A refreshable credential (ADR-230) resolves to a per-tick token source; carry it so the daemon wires
22447
+ // it to the poll loop's `refreshCredentials`. Absent for a static credential.
22448
+ ...creds.refreshCredentials ? { refreshCredentials: creds.refreshCredentials } : {}
22301
22449
  }
22302
22450
  };
22303
22451
  }
@@ -22341,7 +22489,13 @@ async function startConnectorPolling(input) {
22341
22489
  },
22342
22490
  input.graph,
22343
22491
  registration.resolveTarget,
22344
- { intervalMs: registration.intervalMs, connectorId: registration.id }
22492
+ {
22493
+ intervalMs: registration.intervalMs,
22494
+ connectorId: registration.id,
22495
+ // A refreshable credential (ADR-230) mints a fresh token each tick; the static path leaves this unset
22496
+ // and the loop reuses ctx.credentials, exactly as before.
22497
+ ...registration.refreshCredentials ? { refreshCredentials: registration.refreshCredentials } : {}
22498
+ }
22345
22499
  )
22346
22500
  );
22347
22501
  return () => {
@@ -23299,6 +23453,37 @@ async function reconcileFrontierSurfaces(graph, errorsPath) {
23299
23453
 
23300
23454
  // src/connectors/hosted.ts
23301
23455
  init_cjs_shims();
23456
+
23457
+ // src/connectors/railway/target-ref.ts
23458
+ init_cjs_shims();
23459
+ function nonEmptyString(v) {
23460
+ return typeof v === "string" && v.length > 0;
23461
+ }
23462
+ function decodeRailwayTargetRef(ref) {
23463
+ if (typeof ref !== "string" || ref.length === 0) return null;
23464
+ let json;
23465
+ try {
23466
+ json = Buffer.from(ref, "base64url").toString("utf8");
23467
+ } catch {
23468
+ return null;
23469
+ }
23470
+ let parsed;
23471
+ try {
23472
+ parsed = JSON.parse(json);
23473
+ } catch {
23474
+ return null;
23475
+ }
23476
+ if (!parsed || typeof parsed !== "object") return null;
23477
+ const obj = parsed;
23478
+ if (!nonEmptyString(obj.environmentId) || !nonEmptyString(obj.serviceId)) return null;
23479
+ return {
23480
+ environmentId: obj.environmentId,
23481
+ serviceId: obj.serviceId,
23482
+ ...nonEmptyString(obj.serviceName) ? { serviceName: obj.serviceName } : {}
23483
+ };
23484
+ }
23485
+
23486
+ // src/connectors/hosted.ts
23302
23487
  var CREDENTIAL_REFRESH_SKEW_MS = 6e4;
23303
23488
  var CP_REQUEST_TIMEOUT_MS = 1e4;
23304
23489
  async function cpGet(path81, deps) {
@@ -23325,6 +23510,15 @@ function hostedOptions(provider, summary, serviceName) {
23325
23510
  if (!ref) return null;
23326
23511
  return { apiProjectRef: ref, nodeRef: `${ref}.supabase.co`, serviceName };
23327
23512
  }
23513
+ case "railway": {
23514
+ const target = summary.projectRef ? decodeRailwayTargetRef(summary.projectRef) : null;
23515
+ if (!target) return null;
23516
+ return {
23517
+ environmentId: target.environmentId,
23518
+ serviceId: target.serviceId,
23519
+ serviceNameById: { [target.serviceId]: serviceName }
23520
+ };
23521
+ }
23328
23522
  default:
23329
23523
  return null;
23330
23524
  }