@claxedo/workspace-relay 0.6.0 → 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/main.d.ts CHANGED
@@ -1,51 +1,69 @@
1
1
  import { type WorkspaceRelayBunDrainController } from "./bun";
2
2
  import { type WorkspaceRelayDirectory } from "./directory";
3
3
  import { type RelayHostPublicKey, type RevocationLookup, type WorkspaceRelayTarget } from "./server";
4
- import type { RelayKey } from "./auth";
4
+ import { type RelayKey } from "./auth";
5
5
  export { createCachedRevocationClient, createCachedTargetClient } from "./server";
6
6
  /**
7
- * D12 observability: Sentry on the relay (ops floor ADR
8
- * docs/plans/2026-07-11-016-wp-ops-floor-design.md §4).
7
+ * PostHog is the relay's error sink — one vendor carries error tracking for
8
+ * every runtime behind one distinct_id space.
9
9
  *
10
- * Gated on `CLAXEDO_RELAY_SENTRY_DSN` (the relay's OWN DSN env — separate
11
- * Sentry project from the control plane). Absent DSN → `Sentry.init` is never
12
- * called: zero SDK overhead, zero network — the documented no-op posture, safe
13
- * before the Sentry account exists. Release = git SHA passed by the D11
14
- * deploy workflow (`CLAXEDO_RELEASE`; `GIT_SHA` accepted as alias); events are
15
- * tagged unit=relay + deployment_mode (absent → "self-host", mirroring D9's
16
- * default). Tracing stays off (ADR: it is where Sentry gets expensive).
10
+ * Sending requires two independent opt-ins: `CLAXEDO_TELEMETRY_MODE=on` AND
11
+ * `CLAXEDO_POSTHOG_KEY` (`POSTHOG_KEY` accepted as a fallback — the same
12
+ * unprefixed name claxedo-server's posthog.ts reads). Any other combination
13
+ * constructs no `PostHog` client: zero SDK overhead, zero network. Host
14
+ * defaults to `https://us.i.posthog.com` (the ingest host, not the legacy
15
+ * `app.posthog.com` default some SDKs ship). Release is the git SHA from
16
+ * `CLAXEDO_RELEASE` (`GIT_SHA` accepted as alias); captures are tagged
17
+ * unit=relay + deployment_mode (absent → "local").
17
18
  *
18
- * The relay is a Bun process (`Bun.serve`), so this uses `@sentry/bun` —
19
- * Sentry's Bun-native SDK with the same init-gated pattern as @sentry/node
20
- * (https://docs.sentry.io/platforms/javascript/guides/bun/).
19
+ * `posthog-node` runs fine on Bun (`Bun.serve`), unlike the Cloudflare
20
+ * Worker, which keeps it off its import graph entirely (see worker.ts).
21
21
  */
22
22
  export type RelayObservabilityEnv = {
23
- CLAXEDO_RELAY_SENTRY_DSN?: string | undefined;
23
+ /** Only `on` permits sending, matched case-insensitively after trimming. */
24
+ CLAXEDO_TELEMETRY_MODE?: string | undefined;
25
+ CLAXEDO_POSTHOG_KEY?: string | undefined;
26
+ POSTHOG_KEY?: string | undefined;
27
+ CLAXEDO_POSTHOG_HOST?: string | undefined;
24
28
  CLAXEDO_RELEASE?: string | undefined;
25
29
  GIT_SHA?: string | undefined;
26
30
  CLAXEDO_DEPLOYMENT_MODE?: string | undefined;
27
31
  /** Accept process.env verbatim (extra keys are ignored). */
28
32
  [key: string]: string | undefined;
29
33
  };
30
- export type RelaySentryOptions = {
31
- dsn: string;
34
+ export type RelayTelemetryOptions = {
35
+ key: string;
36
+ host: string;
32
37
  release?: string;
33
- tracesSampleRate: 0;
34
- initialScope: {
35
- tags: {
36
- unit: "relay";
37
- deployment_mode: string;
38
- };
38
+ tags: {
39
+ unit: "relay";
40
+ deployment_mode: string;
39
41
  };
40
42
  };
41
43
  /**
42
- * Pure env → Sentry options resolver. `undefined` = DSN absent = do NOT init
43
- * (no client, no network). Exported for tests.
44
+ * Pure env → PostHog options resolver. `undefined` = do NOT construct a client
45
+ * (no client, no network), which is the answer unless BOTH opt-ins are
46
+ * present. The mode is read before the key so that a deployment which has not
47
+ * said `on` stays silent no matter which keys reach its environment.
48
+ * Exported for tests.
44
49
  */
45
- export declare function relaySentryOptions(env: RelayObservabilityEnv): RelaySentryOptions | undefined;
50
+ export declare function relayTelemetryOptions(env: RelayObservabilityEnv): RelayTelemetryOptions | undefined;
46
51
  export declare function initRelayObservability(env?: RelayObservabilityEnv): {
47
52
  enabled: boolean;
48
53
  };
54
+ /**
55
+ * Capture-and-flush used by the fatal handlers and the startup catch. With no
56
+ * initialized client (key absent) this is a documented no-op. Never throws —
57
+ * observability must not preempt the exit path.
58
+ *
59
+ * `distinctId: "system"` mirrors the ops-plane convention: process-fatal
60
+ * events carry no user identity. `client.flush()` has no built-in timeout,
61
+ * so it races a 2s timer; the flush promise's
62
+ * rejection is swallowed even when the timer wins the race, so a slow network
63
+ * failure that resolves after the timeout never surfaces as a second
64
+ * unhandledRejection mid-shutdown.
65
+ */
66
+ export declare function reportFatal(error: unknown): Promise<void>;
49
67
  /**
50
68
  * Production fail-closed gate for the relay → claxedo-server resolver channel.
51
69
  *
@@ -102,17 +120,10 @@ export declare function loadRuntimeAccessKeyOrJwks(env: LoadRuntimeAccessKeyEnv)
102
120
  */
103
121
  export type LoadRelayHostKeyMaterialEnv = {
104
122
  /**
105
- * Used to gate the production fail-closed check for the RHT signing key.
106
- * If `production` and no signing key PEM is configured, the loader exits
107
- * with code 2 rather than silently generating an ephemeral key (which
108
- * would be different on every instance and break verification across a
109
- * multi-instance deploy).
110
- *
111
- * Note: T2's `validateProductionEnv` is the canonical home for top-level
112
- * env validation, but this check lives here because the loader already
113
- * inspects the signing-key env var and the failure mode is local. The
114
- * boot path could optionally re-route through `validateProductionEnv` in
115
- * a future refactor.
123
+ * Gates the production fail-closed check: with no signing-key PEM in
124
+ * `production` the loader exits 2 instead of generating an ephemeral key.
125
+ * The check lives here rather than in `validateProductionEnv` because the
126
+ * loader already inspects the signing-key env var.
116
127
  */
117
128
  NODE_ENV?: string | undefined;
118
129
  CLAXEDO_RELAY_HOST_SIGNING_KEY_PEM?: string | undefined;
@@ -128,7 +139,7 @@ export type RelayHostKeyMaterial = {
128
139
  };
129
140
  export declare function loadRelayHostKeyMaterial(env: LoadRelayHostKeyMaterialEnv): Promise<RelayHostKeyMaterial>;
130
141
  /**
131
- * T16: parse `CLAXEDO_RELAY_AUDIT_ACCEPT_SAMPLE_RATE` into a number in [0, 1].
142
+ * Parse `CLAXEDO_RELAY_AUDIT_ACCEPT_SAMPLE_RATE` into a number in [0, 1].
132
143
  * Falls back to a NODE_ENV-gated default (production: 0.1, dev/test: 1.0) if
133
144
  * unset, empty, or out of range. Exported for tests.
134
145
  */
@@ -138,7 +149,7 @@ export type ParseAuditAcceptSampleRateEnv = {
138
149
  };
139
150
  export declare function parseAuditAcceptSampleRate(env: ParseAuditAcceptSampleRateEnv): number;
140
151
  /**
141
- * T31: parse the `CLAXEDO_RELAY_METRICS_TOKEN` env var. Returns `undefined`
152
+ * Parse the `CLAXEDO_RELAY_METRICS_TOKEN` env var. Returns `undefined`
142
153
  * for missing/empty/whitespace-only values so the relay can fall back to its
143
154
  * loopback-only `/metrics` access policy.
144
155
  */
@@ -157,18 +168,28 @@ export declare function directHttpConcurrencyFromEnv(env: DirectHttpConcurrencyE
157
168
  export type ResolverClientCacheOptions = {
158
169
  targetCacheTtlMs?: number;
159
170
  revocationCacheTtlMs?: number;
171
+ /**
172
+ * Absolute URL of the control plane's host-generation lookup. Unset derives
173
+ * `<resolver base>/host-generation`, the route every resolver base serves
174
+ * next to `/target` and `/revocation`.
175
+ */
176
+ hostGenerationUrl?: string;
177
+ hostGenerationCacheTtlMs?: number;
160
178
  };
161
179
  export type ResolverClientCacheEnv = {
162
180
  CLAXEDO_RELAY_TARGET_CACHE_TTL_MS?: string | undefined;
163
181
  CLAXEDO_RELAY_REVOCATION_CACHE_TTL_MS?: string | undefined;
182
+ CLAXEDO_RELAY_HOST_GENERATION_URL?: string | undefined;
183
+ CLAXEDO_RELAY_HOST_GENERATION_CACHE_TTL_MS?: string | undefined;
164
184
  };
165
185
  export declare function resolverClientCacheOptionsFromEnv(env: ResolverClientCacheEnv): ResolverClientCacheOptions;
166
186
  export declare function createResolverClient(baseUrl: string, token: string | undefined, options?: ResolverClientCacheOptions): {
167
187
  target: (workspaceId: string, hostId: string) => Promise<WorkspaceRelayTarget | undefined>;
168
188
  revocation: RevocationLookup;
189
+ hostGeneration: import("./server").HostGenerationLookup;
169
190
  };
170
191
  /**
171
- * T9: graceful SIGTERM drain wiring.
192
+ * Graceful SIGTERM drain wiring.
172
193
  *
173
194
  * On `SIGTERM` (or `SIGINT` for local dev) the relay must:
174
195
  * 1. Flip the drain flag so `/health` reports 503 and Fly removes the
@@ -177,7 +198,7 @@ export declare function createResolverClient(baseUrl: string, token: string | un
177
198
  * 2. Wait up to `drainTimeoutMs` (default 30 s, override via
178
199
  * `CLAXEDO_RELAY_DRAIN_TIMEOUT_MS`) for the in-flight tunnel HTTP
179
200
  * response map to empty across every connected host tunnel.
180
- * 3. Release the directory's TTL sweep timer (T18 added `dispose()`) so
201
+ * 3. Release the directory's TTL sweep timer via `directory.dispose()` so
181
202
  * the process can exit cleanly without a dangling `setInterval`.
182
203
  * 4. Exit with code 0.
183
204
  *
@@ -199,6 +220,12 @@ export type ShutdownDrainHandlerOptions = {
199
220
  signals?: NodeJS.Signals[];
200
221
  /** Defaults to a no-op (we let Bun.serve get GC'd as part of process exit). */
201
222
  stopServer?: () => Promise<void> | void;
223
+ /**
224
+ * Upper bound on `stopServer`. A wedged socket close must not outlast the
225
+ * platform's kill timeout, or a graceful exit becomes a SIGKILL that skips
226
+ * `directory.dispose()`. Defaults to 5 s; tests override it to stay fast.
227
+ */
228
+ stopTimeoutMs?: number;
202
229
  };
203
230
  export type ShutdownDrainHandle = {
204
231
  trigger(): Promise<void>;
@@ -212,12 +239,14 @@ export type FatalProcessHandlerOptions = {
212
239
  log?: (message: string) => void;
213
240
  register?: boolean;
214
241
  /**
215
- * D12: error-tracker hook, invoked (and awaited) before teardown so a fatal
216
- * crash reaches Sentry before the process exits. Failures are logged and
217
- * never block the exit path. Defaults to none (tests, DSN-absent runs).
242
+ * Error-tracker hook, invoked (and awaited) before teardown so a fatal
243
+ * crash reaches PostHog before the process exits. Failures are logged and
244
+ * never block the exit path. Defaults to none (tests, key-absent runs).
218
245
  */
219
246
  report?: (error: unknown, source: "uncaughtException" | "unhandledRejection") => void | Promise<void>;
220
247
  stopServer?: () => Promise<void> | void;
248
+ /** Upper bound on `stopServer`; see `ShutdownDrainHandlerOptions.stopTimeoutMs`. */
249
+ stopTimeoutMs?: number;
221
250
  };
222
251
  export type FatalProcessHandle = {
223
252
  trigger(error: unknown, source: "uncaughtException" | "unhandledRejection"): Promise<void>;