@sylphx/sdk 0.8.0-rc.1 → 0.8.0-rc.2
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/README.md +35 -27
- package/dist/health/index.mjs +0 -10
- package/dist/health/index.mjs.map +1 -1
- package/dist/index.d.ts +42 -17
- package/dist/index.mjs +7 -4
- package/dist/index.mjs.map +1 -1
- package/dist/nextjs/index.mjs.map +1 -1
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.mjs +3 -3
- package/dist/react/index.mjs.map +1 -1
- package/dist/server/index.d.ts +11 -7
- package/dist/server/index.mjs +3 -3
- package/dist/server/index.mjs.map +1 -1
- package/dist/web-analytics.mjs.map +1 -1
- package/package.json +9 -15
- package/dist/health/index.js +0 -521
- package/dist/health/index.js.map +0 -1
- package/dist/index.d.cts +0 -9394
- package/dist/index.js +0 -11231
- package/dist/index.js.map +0 -1
- package/dist/nextjs/index.d.cts +0 -567
- package/dist/nextjs/index.js +0 -2081
- package/dist/nextjs/index.js.map +0 -1
- package/dist/react/index.d.cts +0 -14250
- package/dist/react/index.js +0 -81688
- package/dist/react/index.js.map +0 -1
- package/dist/server/index.d.cts +0 -1842
- package/dist/server/index.js +0 -3430
- package/dist/server/index.js.map +0 -1
- package/dist/web-analytics.js +0 -248
- package/dist/web-analytics.js.map +0 -1
package/README.md
CHANGED
|
@@ -26,21 +26,23 @@ bun add @sylphx/sdk
|
|
|
26
26
|
|
|
27
27
|
### 1. Environment Variables
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Your server connection URL and environment app ID from your
|
|
30
|
+
[Platform Console](https://sylphx.com/console):
|
|
30
31
|
|
|
31
32
|
```bash
|
|
32
33
|
# .env.local
|
|
33
|
-
|
|
34
|
-
NEXT_PUBLIC_SYLPHX_APP_ID=app_dev_xxxxxxxxxxxx
|
|
34
|
+
SYLPHX_URL=sylphx://sk_dev_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@bold-river-a1b2c3.api.sylphx.com
|
|
35
|
+
NEXT_PUBLIC_SYLPHX_APP_ID=app_dev_xxxxxxxxxxxx
|
|
35
36
|
```
|
|
36
37
|
|
|
37
38
|
That's it. No other config needed.
|
|
38
39
|
|
|
39
40
|
> **Key formats**
|
|
40
|
-
> - `
|
|
41
|
+
> - `sylphx://sk_*@<tenant-slug>.api.sylphx.com` — Server connection URL (server only, never expose)
|
|
41
42
|
> - `app_dev_*` / `app_stg_*` / `app_prod_*` — App ID (safe for client-side)
|
|
42
43
|
>
|
|
43
|
-
> Get both from **Console → Your App → API Keys**.
|
|
44
|
+
> Get both from **Console → Your App → API Keys**. The hosted BaaS API is always
|
|
45
|
+
> addressed as `<tenant-slug>.api.sylphx.com`.
|
|
44
46
|
|
|
45
47
|
---
|
|
46
48
|
|
|
@@ -73,11 +75,16 @@ Fetch config server-side once, pass to the provider:
|
|
|
73
75
|
// app/layout.tsx
|
|
74
76
|
import { getAppConfig } from '@sylphx/sdk/server'
|
|
75
77
|
import { SylphxProvider } from '@sylphx/sdk/react'
|
|
78
|
+
import { createServerClient } from '@sylphx/sdk'
|
|
76
79
|
|
|
77
80
|
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
81
|
+
const sylphx = createServerClient(process.env.SYLPHX_URL!)
|
|
82
|
+
const apiUrl = sylphx.baseUrl.replace(/\/v[0-9]+$/, '')
|
|
83
|
+
|
|
78
84
|
const config = await getAppConfig({
|
|
79
|
-
secretKey:
|
|
85
|
+
secretKey: sylphx.secretKey!,
|
|
80
86
|
appId: process.env.NEXT_PUBLIC_SYLPHX_APP_ID!,
|
|
87
|
+
platformUrl: apiUrl,
|
|
81
88
|
})
|
|
82
89
|
|
|
83
90
|
return (
|
|
@@ -86,6 +93,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
|
|
86
93
|
<SylphxProvider
|
|
87
94
|
config={config}
|
|
88
95
|
appId={process.env.NEXT_PUBLIC_SYLPHX_APP_ID!}
|
|
96
|
+
platformUrl={apiUrl}
|
|
89
97
|
>
|
|
90
98
|
{children}
|
|
91
99
|
</SylphxProvider>
|
|
@@ -159,19 +167,15 @@ const userId = await currentUserId()
|
|
|
159
167
|
### Server API Client
|
|
160
168
|
|
|
161
169
|
```ts
|
|
162
|
-
import { createServerClient } from '@sylphx/sdk
|
|
170
|
+
import { createServerClient, getPlans, track } from '@sylphx/sdk'
|
|
163
171
|
|
|
164
|
-
const
|
|
165
|
-
secretKey: process.env.SYLPHX_SECRET_KEY!,
|
|
166
|
-
})
|
|
172
|
+
const sylphx = createServerClient(process.env.SYLPHX_URL!)
|
|
167
173
|
|
|
168
|
-
//
|
|
169
|
-
const plans = await
|
|
174
|
+
// Billing
|
|
175
|
+
const plans = await getPlans(sylphx)
|
|
170
176
|
|
|
171
|
-
//
|
|
172
|
-
await
|
|
173
|
-
body: { events: [{ event: 'purchase', properties: { amount: 99 } }] },
|
|
174
|
-
})
|
|
177
|
+
// Analytics
|
|
178
|
+
await track(sylphx, { event: 'purchase', properties: { amount: 99 } })
|
|
175
179
|
```
|
|
176
180
|
|
|
177
181
|
### Prefetch App Config
|
|
@@ -183,10 +187,14 @@ import {
|
|
|
183
187
|
getFeatureFlags, // Feature flag definitions
|
|
184
188
|
getConsentTypes, // GDPR consent config
|
|
185
189
|
} from '@sylphx/sdk/server'
|
|
190
|
+
import { createServerClient } from '@sylphx/sdk'
|
|
191
|
+
|
|
192
|
+
const sylphx = createServerClient(process.env.SYLPHX_URL!)
|
|
186
193
|
|
|
187
194
|
const config = await getAppConfig({
|
|
188
|
-
secretKey:
|
|
195
|
+
secretKey: sylphx.secretKey!,
|
|
189
196
|
appId: process.env.NEXT_PUBLIC_SYLPHX_APP_ID!,
|
|
197
|
+
platformUrl: sylphx.baseUrl.replace(/\/v[0-9]+$/, ''),
|
|
190
198
|
})
|
|
191
199
|
// config.plans, config.featureFlags, config.oauthProviders, config.consentTypes
|
|
192
200
|
```
|
|
@@ -368,9 +376,9 @@ import { Protect } from '@sylphx/sdk/react'
|
|
|
368
376
|
For non-React environments or maximum control:
|
|
369
377
|
|
|
370
378
|
```ts
|
|
371
|
-
import {
|
|
379
|
+
import { createClient, signIn, track, getPlans } from '@sylphx/sdk'
|
|
372
380
|
|
|
373
|
-
const config =
|
|
381
|
+
const config = createClient(process.env.SYLPHX_URL!)
|
|
374
382
|
|
|
375
383
|
// Auth
|
|
376
384
|
const tokens = await signIn(config, { email, password })
|
|
@@ -440,18 +448,18 @@ If you're running your own Sylphx Platform deployment, configure the base URL vi
|
|
|
440
448
|
SYLPHX_API_URL=https://platform.your-domain.com sylphx deploy
|
|
441
449
|
```
|
|
442
450
|
|
|
443
|
-
Or in the SDK via
|
|
451
|
+
Or in the SDK via an explicit custom-domain connection URL:
|
|
444
452
|
|
|
445
453
|
```ts
|
|
446
|
-
import {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
454
|
+
import { createServerClient } from '@sylphx/sdk'
|
|
455
|
+
|
|
456
|
+
const config = createServerClient(
|
|
457
|
+
'sylphx://sk_prod_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@acme.api.example.com',
|
|
458
|
+
)
|
|
451
459
|
```
|
|
452
460
|
|
|
453
|
-
> **Note:**
|
|
454
|
-
>
|
|
461
|
+
> **Note:** hosted Sylphx uses `<tenant-slug>.api.sylphx.com`. Only use a custom
|
|
462
|
+
> host for self-hosted deployments or a documented legacy migration.
|
|
455
463
|
|
|
456
464
|
---
|
|
457
465
|
|
package/dist/health/index.mjs
CHANGED
|
@@ -192,16 +192,6 @@ function startUnixSocketServer(source, opts = {}) {
|
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
};
|
|
195
|
-
if (opts.installSignalHandlers === true) {
|
|
196
|
-
const onSignal = (sig) => {
|
|
197
|
-
void handle.shutdown().then(() => {
|
|
198
|
-
if (typeof process !== "undefined") process.exit(0);
|
|
199
|
-
});
|
|
200
|
-
void sig;
|
|
201
|
-
};
|
|
202
|
-
process.on("SIGTERM", () => onSignal("SIGTERM"));
|
|
203
|
-
process.on("SIGINT", () => onSignal("SIGINT"));
|
|
204
|
-
}
|
|
205
195
|
return handle;
|
|
206
196
|
}
|
|
207
197
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/health/effects.ts","../../src/health/types.ts","../../src/health/handler.ts","../../src/health/scoring.ts","../../src/health/signals/event-loop-lag.ts","../../src/health/unix-socket-server.ts","../../src/health/signals/error-rate.ts","../../src/health/signals/memory-pressure.ts","../../src/health/signals/queue-depth.ts","../../src/health/index.ts"],"sourcesContent":["/**\n * Effect TS integration for `@sylphx/sdk/health` (Rule 21 / ADR-058 Amendment).\n *\n * Effect-native services should NEVER call `Effect.runPromise` inside\n * business logic — this module exposes `evaluateEffect` so they can fold\n * the health computation into their own fiber graph.\n *\n * `effect` is an OPTIONAL peer dependency — apps that don't import this\n * module never pull it in (sideEffects: false + tree-shaking guarantees\n * this; verified in tests). The `Effect` type is imported from the\n * peer-dep at runtime; consumers either provide it or never reach this\n * code path.\n */\n\nimport { Effect } from 'effect'\nimport type { HealthError, HealthScore } from './types'\nimport { HealthError as HealthErrorClass } from './types'\n\n/**\n * Lift a `() => Promise<HealthScore>` evaluator into an Effect-native value.\n *\n * Errors thrown by the evaluator are tagged `HealthError` so callers can\n * use `Effect.catchTag('HealthError', …)` for typed recovery.\n *\n * @example\n * ```ts\n * import { Effect } from 'effect'\n * import { sylphxHealth } from '@sylphx/sdk/health'\n *\n * const health = sylphxHealth({ signals: [...] })\n *\n * const program = Effect.gen(function* () {\n * const { score, signals } = yield* health.evaluateEffect\n * yield* Effect.log(`health=${score.toFixed(2)} signals=${JSON.stringify(signals)}`)\n * return score\n * })\n *\n * // Only the entry point runs the Effect (Rule 21).\n * const finalScore = await Effect.runPromise(program)\n * ```\n */\nexport function evaluateEffect(\n\tevaluator: () => Promise<HealthScore>,\n): Effect.Effect<HealthScore, HealthError, never> {\n\treturn Effect.tryPromise({\n\t\ttry: () => evaluator(),\n\t\tcatch: (err) => new HealthErrorClass('health evaluation failed', err),\n\t})\n}\n","/**\n * Type SSOT for `@sylphx/sdk/health` (ADR-111 §4).\n *\n * Pure runtime types — framework-free, no schema library required at the\n * SDK boundary. Apps that want Standard Schema validation (per ADR-084)\n * can wrap the wire-format `HealthSnapshot` in their own schema.\n *\n * The wire shape (`HealthSnapshot`) is the **stable contract** the\n * `sylphx-health-agent` sidecar parses (see ADR-111 §3.2.4 +\n * `apps/health-agent/src/app-poll.ts::parseAppHealthBody`). Do not break it.\n */\n\n// ─── Signal — what an app registers ─────────────────────────────────────────\n\n/**\n * One reading produced by a `Signal.read()`. The aggregator turns each\n * reading into a `factor` in `[0, 1]` then folds them into the score via\n * the configured `ScoringStrategy`.\n *\n * `value` is the raw observation (ms, ratio, count, …) — surfaced verbatim\n * in the wire snapshot so operators can debug from JSON without re-running\n * the signal logic.\n *\n * `healthFactor` is the normalised health in `[0, 1]`:\n * - `1` = fully healthy\n * - `0` = dead (or \"we don't know\" if `unknown=true` and the policy says so)\n *\n * `unknown=true` means the signal **could not be measured this tick** (e.g.\n * cgroup file unreadable). Scoring strategies treat unknown signals as\n * `factor=1` (don't penalise an app for our own missing data).\n */\nexport interface SignalReading {\n\treadonly value: number | string | boolean\n\treadonly healthFactor: number\n\treadonly unknown?: boolean\n}\n\n/**\n * A health signal — one named, weighted measurement the score is built from.\n *\n * Two variants:\n * - `SyncSignal` — `read(): SignalReading` (e.g. event-loop lag)\n * - `AsyncSignal` — `read(): Promise<SignalReading>` (e.g. queue depth\n * fetched over IPC)\n *\n * The discriminated `Signal` union accepts both. The aggregator awaits\n * each reading uniformly via `Promise.resolve(signal.read())`.\n *\n * Implementations are pure — no internal mutation outside the closure-\n * captured monitor state (e.g. `monitorEventLoopDelay()`). Stop /\n * cleanup are exposed via `dispose()` for tests and graceful shutdown.\n */\nexport interface SignalBase {\n\t/** Unique stable identifier; appears in the wire `signals.<name>` map. */\n\treadonly name: string\n\t/**\n\t * Weight in the weighted-product score. Strictly `> 0` for active\n\t * signals; `0` is a no-op signal (kept for compatibility with\n\t * conditional registration but skipped in scoring).\n\t */\n\treadonly weight: number\n\t/**\n\t * Tear down any background work (timers, monitors, file watchers).\n\t * Called during graceful shutdown and from test cleanup.\n\t */\n\tdispose?(): void\n}\nexport interface SyncSignal extends SignalBase {\n\tread(): SignalReading\n}\nexport interface AsyncSignal extends SignalBase {\n\tread(): Promise<SignalReading>\n}\nexport type Signal = SyncSignal | AsyncSignal\n\n// ─── Score — what the sidecar reads ─────────────────────────────────────────\n\n/**\n * The complete health score + signal breakdown produced by `health.evaluate()`.\n *\n * `score` is the normalised aggregate in `[0, 1]`. Signal payload is\n * verbatim values from each `SignalReading.value` so operators can\n * cross-reference Grafana dashboards with the JSON.\n */\nexport interface HealthScore {\n\treadonly score: number\n\treadonly signals: Record<string, number | string | boolean>\n\treadonly lastTickAt: string\n}\n\n/**\n * Wire format the sidecar's `app-poll.ts::parseAppHealthBody` consumes.\n *\n * Pinned by ADR-111 §3.2.4 — keep stable. The sidecar tolerates extra\n * keys; do NOT remove existing ones without sequencing the sidecar update\n * first.\n */\nexport type HealthSnapshot = HealthScore\n\n// ─── Errors ─────────────────────────────────────────────────────────────────\n\n/**\n * Tagged error type for the Effect API. Promise consumers see the same\n * `message` via `Error.message` — the tag is for `Effect.catchTag`.\n */\nexport class HealthError extends Error {\n\treadonly _tag = 'HealthError' as const\n\treadonly cause?: unknown\n\tconstructor(message: string, cause?: unknown) {\n\t\tsuper(message)\n\t\tthis.name = 'HealthError'\n\t\tthis.cause = cause\n\t}\n}\n\n// ─── Scoring strategy ───────────────────────────────────────────────────────\n\n/**\n * `ScoringStrategy` collapses N readings → one `score` in `[0, 1]`.\n *\n * Default is `weightedProduct` (see `scoring.ts`). Custom strategies can\n * be plugged in via `sylphxHealth({ scoringStrategy: myStrategy })`.\n */\nexport type ScoringStrategy = (\n\treadings: ReadonlyArray<{ signal: Signal; reading: SignalReading }>,\n) => number\n\n// ─── Public option types ────────────────────────────────────────────────────\n\nexport interface SylphxHealthOptions {\n\t/**\n\t * Signals to register. If omitted, defaults to a single\n\t * `eventLoopLagSignal({ degradedMs: 5000, deadMs: 30000 })` per\n\t * ADR-111 §4.6 (\"sane out-of-box\").\n\t */\n\treadonly signals?: ReadonlyArray<Signal>\n\t/**\n\t * Strategy used to fold readings into a score. Defaults to\n\t * `weightedProduct` from `scoring.ts`.\n\t */\n\treadonly scoringStrategy?: ScoringStrategy\n\t/**\n\t * Optional injected clock — used by tests for deterministic\n\t * `lastTickAt` timestamps.\n\t */\n\treadonly now?: () => Date\n}\n","/**\n * Universal HTTP handler for `@sylphx/sdk/health` (ADR-111 §4.2).\n *\n * Framework-agnostic: returns either a `Web Fetch API` `Response`\n * (Hono / Bun.serve / itty-router / Next.js routes) or, via thin adapters,\n * a Node `(req, res) => void` (Express / Fastify-as-classic-handler).\n *\n * The handler always returns **HTTP 200** unless the SDK itself failed to\n * compute a score (rare; caught and returned as 500 for ops to triage).\n * The three-tier 200 / 503 gate (ADR-111 §4.4) lives in the **sidecar**,\n * not here — the SDK's job ends at exposing the score so the sidecar\n * can decide. This separation is documented in the README (\"apps don't\n * need to think about probe semantics\").\n */\n\nimport type { HealthScore } from './types'\n\n/**\n * Minimal contract a `health` instance must expose for the handler to\n * call. The full `health` object satisfies this implicitly via\n * `evaluate()` from `./index.ts`.\n */\nexport interface HealthEvaluator {\n\tevaluate(): Promise<HealthScore>\n}\n\n/**\n * Build a Web-Fetch-style handler. Suitable for:\n * - Hono: `app.get('/healthz', health.handler())`\n * - Bun.serve: `fetch: health.handler()`\n * - Next.js route.ts: `export const GET = health.handler()`\n * - itty-router / Hattip / standard `(Request) => Response` runtimes\n */\nexport function createWebHandler(source: HealthEvaluator): (req?: Request) => Promise<Response> {\n\treturn async (_req?: Request): Promise<Response> => {\n\t\ttry {\n\t\t\tconst score = await source.evaluate()\n\t\t\treturn new Response(JSON.stringify(score), {\n\t\t\t\tstatus: 200,\n\t\t\t\theaders: {\n\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t// Cache-Control: never cache. Even 1-second cache on a\n\t\t\t\t\t// CDN edge would mask a fast-moving health collapse.\n\t\t\t\t\t'Cache-Control': 'no-store, no-cache, must-revalidate',\n\t\t\t\t},\n\t\t\t})\n\t\t} catch (err) {\n\t\t\tconst message = err instanceof Error ? err.message : String(err)\n\t\t\treturn new Response(\n\t\t\t\tJSON.stringify({\n\t\t\t\t\terror: 'health_evaluator_failed',\n\t\t\t\t\tmessage,\n\t\t\t\t}),\n\t\t\t\t{\n\t\t\t\t\tstatus: 500,\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t\t'Cache-Control': 'no-store, no-cache, must-revalidate',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t)\n\t\t}\n\t}\n}\n\n/**\n * Build a Node-style `(req, res)` handler — for Express / classic Fastify.\n *\n * Adapter that delegates to `createWebHandler()` so logic stays in one\n * place. Imported lazily by callers that need it; doesn't drag any node\n * types into Web-only code paths.\n */\nexport interface NodeIncoming {\n\tmethod?: string\n\turl?: string\n}\nexport interface NodeOutgoing {\n\tstatusCode: number\n\tsetHeader(name: string, value: string): void\n\tend(body?: string): void\n}\nexport function createNodeHandler(\n\tsource: HealthEvaluator,\n): (req: NodeIncoming, res: NodeOutgoing) => Promise<void> {\n\treturn async (_req, res) => {\n\t\ttry {\n\t\t\tconst score = await source.evaluate()\n\t\t\tres.statusCode = 200\n\t\t\tres.setHeader('Content-Type', 'application/json')\n\t\t\tres.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate')\n\t\t\tres.end(JSON.stringify(score))\n\t\t} catch (err) {\n\t\t\tconst message = err instanceof Error ? err.message : String(err)\n\t\t\tres.statusCode = 500\n\t\t\tres.setHeader('Content-Type', 'application/json')\n\t\t\tres.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate')\n\t\t\tres.end(JSON.stringify({ error: 'health_evaluator_failed', message }))\n\t\t}\n\t}\n}\n","/**\n * Scoring strategies (ADR-111 §4 — three-tier health gate).\n *\n * The default `weightedProduct` strategy is multiplicative — a single bad\n * signal can drag the whole score down (any factor of 0 → score 0). This is\n * the right semantic for **liveness**: one critical subsystem dead means\n * the pod is not ready, regardless of how healthy the rest is.\n *\n * Mathematics:\n * score = ∏ factor_i ^ weight_i (over signals where weight > 0 AND\n * reading is not `unknown`)\n *\n * Where weights are normalised to sum=1 first, so absolute weight values\n * don't matter — only the ratios do. This keeps user expectations sane:\n * `[w=2, w=2]` and `[w=10, w=10]` produce identical scores.\n *\n * Edge cases (deterministic, never throw):\n * - empty input → 1 (perfect health, nothing to penalise)\n * - all weights = 0 → 1 (no active signals)\n * - all readings `unknown` → 1 (we can't see; cardinal-rule fallback\n * lives at the sidecar boundary, not\n * here — ADR-111 §3.2.5)\n * - factor < 0 / NaN → clamped to 0\n * - factor > 1 → clamped to 1\n * - weight < 0 → clamped to 0 (ignored)\n *\n * The clamps make us **safe by construction** — a misconfigured signal\n * cannot push the score outside `[0, 1]`. Tests exercise all branches.\n */\n\nimport type { ScoringStrategy, Signal, SignalReading } from './types'\n\n/** Clamp `x` to `[0, 1]`, mapping NaN to 0. */\nfunction clamp01(x: number): number {\n\tif (!Number.isFinite(x)) return 0\n\tif (x < 0) return 0\n\tif (x > 1) return 1\n\treturn x\n}\n\n/**\n * Weighted geometric mean — the default scoring strategy.\n *\n * @example\n * weightedProduct([\n * { signal: { name: 'lag', weight: 0.4 }, reading: { healthFactor: 0.4 } },\n * { signal: { name: 'q', weight: 0.6 }, reading: { healthFactor: 1.0 } },\n * ])\n * // → 0.4^0.4 × 1.0^0.6 ≈ 0.693 — ADR-111 §4.5 worked example\n */\nexport const weightedProduct: ScoringStrategy = (readings) => {\n\tif (readings.length === 0) return 1\n\n\t// 1. Filter active signals (weight > 0, reading defined, not unknown).\n\tconst active: Array<{ factor: number; weight: number }> = []\n\tfor (const { signal, reading } of readings) {\n\t\tconst w = signal.weight\n\t\tif (!Number.isFinite(w) || w <= 0) continue\n\t\tif (reading.unknown === true) continue\n\t\tactive.push({\n\t\t\tfactor: clamp01(reading.healthFactor),\n\t\t\tweight: w,\n\t\t})\n\t}\n\tif (active.length === 0) return 1\n\n\t// 2. Normalise weights to sum=1.\n\tconst totalWeight = active.reduce((sum, a) => sum + a.weight, 0)\n\tif (totalWeight <= 0 || !Number.isFinite(totalWeight)) return 1\n\n\t// 3. Geometric mean: exp(Σ w_i × ln(f_i)) — exp/log is more numerically\n\t// stable than repeated `Math.pow` × multiplication for small factors.\n\t// ln(0) → -Infinity → exp(-Infinity) → 0, which is exactly what we\n\t// want (any dead signal kills the score).\n\tlet logSum = 0\n\tfor (const { factor, weight } of active) {\n\t\tconst normalisedWeight = weight / totalWeight\n\t\tif (factor <= 0) {\n\t\t\t// Short-circuit: a single zero factor => score 0, regardless of\n\t\t\t// the others. Avoids ln(0) sentinel value plumbing.\n\t\t\treturn 0\n\t\t}\n\t\tlogSum += normalisedWeight * Math.log(factor)\n\t}\n\tconst score = Math.exp(logSum)\n\treturn clamp01(score)\n}\n\n/**\n * Convenience: build a default scoring strategy.\n *\n * Reserved for future: weighted-min, weighted-mean, etc. For now there's\n * one strategy and `weightedProduct` is the only export — this keeps the\n * surface narrow until a concrete second use-case appears.\n */\nexport function defaultScoringStrategy(): ScoringStrategy {\n\treturn weightedProduct\n}\n\n// Re-export the types used by callers writing custom strategies.\nexport type { ScoringStrategy, Signal, SignalReading }\n","/**\n * `eventLoopLagSignal` — main-thread blocking detector (ADR-111 §4.3).\n *\n * Measures Node/Bun's libuv event-loop delay using `monitorEventLoopDelay()`\n * from `node:perf_hooks`. The monitor is a histogram updated by libuv at a\n * configurable resolution (default 10 ms here — every tick measures lag\n * between expected wake and actual wake). We report the **max** observed\n * since the last `read()` then `reset()` — that's the worst-case stall\n * the app suffered in the polling window.\n *\n * Mapping observed-lag-ms → healthFactor ∈ [0, 1]:\n *\n * healthFactor\n * ▲\n * 1.0 ┤━━━━━━━━━┓\n * │ ┃ linear interpolation\n * │ ┃\n * 0.0 ┤ ┗━━━━━━━━━━━━━\n * ┼─────────┼──────────┼──────► observed lag (ms)\n * 0 degradedMs deadMs\n *\n * Below `degradedMs` → factor 1 (healthy). Above `deadMs` → factor 0 (dead).\n * In-between → linear interpolation. ADR-111 §4.3 default thresholds:\n * degradedMs = 5000 (5 s — same order as ADR-110's 10 s probe timeout)\n * deadMs = 30000 (30 s — definitely wedged)\n *\n * Bun-compatibility: Bun ships `monitorEventLoopDelay()` with the same\n * shape as Node 16+. Verified against `bun:1.3` in this repo's CI.\n */\n\nimport { monitorEventLoopDelay } from 'node:perf_hooks'\nimport type { SignalReading, SyncSignal } from '../types'\n\nexport interface EventLoopLagOptions {\n\t/** Lag below this is fully healthy (factor=1). Default 5000 ms. */\n\treadonly degradedMs?: number\n\t/** Lag above this is fully dead (factor=0). Default 30000 ms. */\n\treadonly deadMs?: number\n\t/**\n\t * Histogram resolution (ms). Default 10 ms — the smaller, the more\n\t * accurate the max but the higher the libuv accounting overhead.\n\t * Node defaults are also 10 ms.\n\t */\n\treadonly resolutionMs?: number\n\t/** Weight in the weighted-product score. Default 0.4 (ADR-111 §4.3). */\n\treadonly weight?: number\n\t/**\n\t * Optional injected monitor (tests). Defaults to a fresh\n\t * `monitorEventLoopDelay()` instance.\n\t */\n\treadonly monitor?: ReturnType<typeof monitorEventLoopDelay>\n}\n\n/**\n * Build an `event-loop-lag` signal. The returned object owns a started\n * histogram monitor; call `dispose()` during shutdown / between tests.\n */\nexport function eventLoopLagSignal(opts: EventLoopLagOptions = {}): SyncSignal {\n\tconst degradedMs = opts.degradedMs ?? 5000\n\tconst deadMs = opts.deadMs ?? 30000\n\tconst resolutionMs = opts.resolutionMs ?? 10\n\tconst weight = opts.weight ?? 0.4\n\n\tif (!Number.isFinite(degradedMs) || degradedMs < 0) {\n\t\tthrow new Error(`eventLoopLagSignal: degradedMs must be >= 0, got ${degradedMs}`)\n\t}\n\tif (!Number.isFinite(deadMs) || deadMs <= degradedMs) {\n\t\tthrow new Error(\n\t\t\t`eventLoopLagSignal: deadMs must be > degradedMs (${degradedMs}), got ${deadMs}`,\n\t\t)\n\t}\n\tif (!Number.isFinite(resolutionMs) || resolutionMs < 1) {\n\t\tthrow new Error(`eventLoopLagSignal: resolutionMs must be >= 1, got ${resolutionMs}`)\n\t}\n\n\tconst monitor = opts.monitor ?? monitorEventLoopDelay({ resolution: resolutionMs })\n\tmonitor.enable()\n\n\treturn {\n\t\tname: 'eventLoopLagMs',\n\t\tweight,\n\t\tread(): SignalReading {\n\t\t\t// `max` is in nanoseconds.\n\t\t\tconst maxNs = monitor.max\n\t\t\t// On a brand-new monitor or after `reset()` the histogram can\n\t\t\t// report `max=0` (no samples yet) or, on some Node versions,\n\t\t\t// `Number.MAX_SAFE_INTEGER` as a sentinel. Treat both as\n\t\t\t// \"no reading yet\" → unknown.\n\t\t\tif (!Number.isFinite(maxNs) || maxNs <= 0 || maxNs >= Number.MAX_SAFE_INTEGER) {\n\t\t\t\tmonitor.reset()\n\t\t\t\treturn { value: 0, healthFactor: 1, unknown: true }\n\t\t\t}\n\t\t\tconst observedMs = maxNs / 1_000_000\n\t\t\tmonitor.reset()\n\n\t\t\tlet factor: number\n\t\t\tif (observedMs <= degradedMs) factor = 1\n\t\t\telse if (observedMs >= deadMs) factor = 0\n\t\t\telse {\n\t\t\t\t// Linear interpolation between degraded (1) and dead (0).\n\t\t\t\tconst span = deadMs - degradedMs\n\t\t\t\tfactor = 1 - (observedMs - degradedMs) / span\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tvalue: Math.round(observedMs),\n\t\t\t\thealthFactor: factor,\n\t\t\t}\n\t\t},\n\t\tdispose(): void {\n\t\t\tmonitor.disable()\n\t\t},\n\t}\n}\n","/**\n * Unix-socket server for `@sylphx/sdk/health` (ADR-111 §3.2.4 + §4.2).\n *\n * The sidecar polls the app over a Unix socket — `/var/run/sylphx/health.sock`\n * by default. The shared volume (`emptyDir` mount, see ADR-111 §3.2.2) is\n * provisioned by the reconciler when the sidecar is injected. This server\n * binds Bun's native `Bun.serve({ unix: <path> })` and serves the same\n * JSON the HTTP handler does.\n *\n * Graceful shutdown is essential — the socket file lingers on disk if not\n * cleaned, and the sidecar's first reconnect attempt after a redeploy\n * would hit a stale inode. `shutdown()` calls `server.stop()` AND\n * unlinks the socket file. The default `process.on('SIGTERM')` hook\n * inside `serveUnixSocket()` opt-in via `installSignalHandlers: true`.\n */\n\nimport { unlinkSync } from 'node:fs'\nimport { createWebHandler, type HealthEvaluator } from './handler'\n\n/** Bun-typed minimum for the server we need to control. */\ninterface BunServer {\n\tstop(force?: boolean): void\n\turl?: { href: string } | null\n}\n\nexport interface UnixSocketServerOptions {\n\t/** Absolute path to bind. Defaults to `/var/run/sylphx/health.sock`. */\n\treadonly path?: string\n\t/**\n\t * If `true`, register `SIGTERM` / `SIGINT` handlers that call\n\t * `shutdown()`. Default `false` — apps usually own signal handling\n\t * already; opt in only for standalone health-only processes.\n\t */\n\treadonly installSignalHandlers?: boolean\n\t/**\n\t * Override unlink behavior — useful for tests where the test runner\n\t * already owns the socket cleanup.\n\t */\n\treadonly unlinkOnShutdown?: boolean\n\t/**\n\t * Override the Bun runtime resolver. Used by tests to verify the\n\t * \"no-Bun\" error path without leaving a Bun-only test environment.\n\t * Production code reads `globalThis.Bun` directly.\n\t */\n\treadonly bunRuntime?: { serve: (cfg: unknown) => BunServer } | null\n}\n\nexport interface UnixSocketServerHandle {\n\treadonly path: string\n\treadonly server: BunServer\n\t/** Stop the server AND unlink the socket file (unless `unlinkOnShutdown=false`). */\n\tshutdown(): Promise<void>\n}\n\n/**\n * Start a Bun unix-socket HTTP server bound to `opts.path`. Returns the\n * handle so tests / shutdown handlers can call `shutdown()`.\n *\n * Pre-binds the socket: if a stale file from a prior crash exists at the\n * path, we `unlinkSync` it first so the bind doesn't `EADDRINUSE`. This\n * is safe because the path is operator-controlled (default lives under\n * `/var/run/sylphx/`, owned by the pod's UID 1000).\n */\nexport function startUnixSocketServer(\n\tsource: HealthEvaluator,\n\topts: UnixSocketServerOptions = {},\n): UnixSocketServerHandle {\n\t// Lazy reference to globalThis.Bun — keeps the import free of build-time\n\t// requirements on Bun for non-Bun consumers (server is no-op there).\n\t// Tests inject `opts.bunRuntime: null` to exercise the no-Bun path.\n\tconst bun =\n\t\topts.bunRuntime !== undefined\n\t\t\t? opts.bunRuntime\n\t\t\t: (globalThis as unknown as { Bun?: { serve: (cfg: unknown) => BunServer } }).Bun\n\tif (bun === null || bun === undefined || typeof bun.serve !== 'function') {\n\t\tthrow new Error(\n\t\t\t'startUnixSocketServer: Bun.serve is unavailable — Unix-socket transport requires a Bun runtime',\n\t\t)\n\t}\n\n\tconst path = opts.path ?? '/var/run/sylphx/health.sock'\n\tconst unlinkOnShutdown = opts.unlinkOnShutdown ?? true\n\n\t// Best-effort pre-cleanup of stale socket.\n\ttry {\n\t\tunlinkSync(path)\n\t} catch {\n\t\t// ENOENT is fine; anything else means the user has the wrong path\n\t\t// or perms — the bind below will surface a clear error.\n\t}\n\n\tconst handler = createWebHandler(source)\n\tconst server = bun.serve({\n\t\tunix: path,\n\t\tfetch: async (req: Request): Promise<Response> => handler(req),\n\t}) as BunServer\n\n\tconst handle: UnixSocketServerHandle = {\n\t\tpath,\n\t\tserver,\n\t\tasync shutdown(): Promise<void> {\n\t\t\ttry {\n\t\t\t\tserver.stop(true)\n\t\t\t} catch {\n\t\t\t\t// stopping a server that's already stopped is benign\n\t\t\t}\n\t\t\tif (unlinkOnShutdown) {\n\t\t\t\ttry {\n\t\t\t\t\tunlinkSync(path)\n\t\t\t\t} catch {\n\t\t\t\t\t// ENOENT — already gone, fine\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t}\n\n\tif (opts.installSignalHandlers === true) {\n\t\tconst onSignal = (sig: string): void => {\n\t\t\tvoid handle.shutdown().then(() => {\n\t\t\t\t// 0 = clean exit; signal handlers fire only when the\n\t\t\t\t// owning process is the standalone health server, not\n\t\t\t\t// when embedded in a larger app.\n\t\t\t\tif (typeof process !== 'undefined') process.exit(0)\n\t\t\t})\n\t\t\tvoid sig\n\t\t}\n\t\tprocess.on('SIGTERM', () => onSignal('SIGTERM'))\n\t\tprocess.on('SIGINT', () => onSignal('SIGINT'))\n\t}\n\n\treturn handle\n}\n","/**\n * `errorRateSignal` — request-error-rate over a sliding window (ADR-111 §4.3).\n *\n * Phase B uses an in-memory ring buffer of {timestamp, isError} samples.\n * Phase C will replace this with an OTel collector subscription so the\n * sidecar gets the same data without per-process bookkeeping; for now the\n * in-memory path keeps the SDK self-contained.\n *\n * Mapping observed-rate → healthFactor:\n *\n * healthFactor\n * ▲\n * 1.0 ┤━━━━━━━━━━━━━━━━━━━┓\n * │ ┃ linear interpolation\n * 0.0 ┤ ┗━━━━━━━━━━\n * ┼───────────────────┼────────► error rate (0..1)\n * 0 degradedRate deadRate\n *\n * Default thresholds match ADR-111 §4.3 row 3:\n * degradedRate = 0.05 (5 % errors → degraded)\n * deadRate = 0.50 (50 % errors → dead)\n *\n * `recordSuccess()` / `recordError()` are pushed by the app on each\n * request (or wired into Hono / Express middleware — the SDK provides\n * primitives, not framework integrations). Zero-traffic windows produce\n * `factor=1` (no requests = no errors = healthy by convention).\n */\n\nimport type { SignalReading, SyncSignal } from '../types'\n\nexport interface ErrorRateOptions {\n\t/** Sliding window length. Accepts ms (number) or '5s' / '1m' shorthand. */\n\treadonly window: number | `${number}s` | `${number}m`\n\t/** Rate above this is considered degraded. Default 0.05 (5 %). */\n\treadonly degradedRate?: number\n\t/** Rate at which the signal saturates to dead. Default 0.50 (50 %). */\n\treadonly deadRate?: number\n\t/**\n\t * Soft minimum sample count: until this many samples land, factor=1\n\t * regardless of rate (avoids \"1 error in 1 request → 100 %\" panics).\n\t * Default 10.\n\t */\n\treadonly minSamples?: number\n\t/** Weight in the weighted-product score. Default 0.2 (ADR-111 §4.3). */\n\treadonly weight?: number\n\t/** Optional injected clock for tests. */\n\treadonly now?: () => number\n}\n\nexport interface ErrorRateSignalHandle extends SyncSignal {\n\t/** Record a successful request (call from app middleware). */\n\trecordSuccess(): void\n\t/** Record a failed request (call from app middleware). */\n\trecordError(): void\n\t/** Erase the window. Useful for tests. */\n\treset(): void\n}\n\ninterface Sample {\n\treadonly t: number\n\treadonly e: boolean\n}\n\nfunction parseWindow(w: ErrorRateOptions['window']): number {\n\tif (typeof w === 'number') {\n\t\tif (!Number.isFinite(w) || w <= 0) {\n\t\t\tthrow new Error(`errorRateSignal: window must be > 0, got ${w}`)\n\t\t}\n\t\treturn w\n\t}\n\tconst m = /^(\\d+)(s|m)$/.exec(w)\n\tif (m === null) {\n\t\tthrow new Error(`errorRateSignal: invalid window '${w}', expected '5s' / '1m' / number`)\n\t}\n\tconst n = Number.parseInt(m[1] as string, 10)\n\tconst unit = m[2]\n\tconst ms = unit === 's' ? n * 1000 : n * 60_000\n\tif (ms <= 0) {\n\t\tthrow new Error(`errorRateSignal: parsed window ${ms}ms must be > 0`)\n\t}\n\treturn ms\n}\n\nexport function errorRateSignal(opts: ErrorRateOptions): ErrorRateSignalHandle {\n\tconst windowMs = parseWindow(opts.window)\n\tconst degradedRate = opts.degradedRate ?? 0.05\n\tconst deadRate = opts.deadRate ?? 0.5\n\tconst minSamples = opts.minSamples ?? 10\n\tconst weight = opts.weight ?? 0.2\n\tconst now = opts.now ?? Date.now\n\n\tif (degradedRate < 0 || degradedRate > 1) {\n\t\tthrow new Error(`errorRateSignal: degradedRate must be in [0, 1], got ${degradedRate}`)\n\t}\n\tif (deadRate <= degradedRate || deadRate > 1) {\n\t\tthrow new Error(`errorRateSignal: deadRate must be in (degradedRate, 1], got ${deadRate}`)\n\t}\n\tif (!Number.isFinite(minSamples) || minSamples < 1) {\n\t\tthrow new Error(`errorRateSignal: minSamples must be >= 1, got ${minSamples}`)\n\t}\n\n\t// Linked-list-style ring; we shift the head when entries age out. For\n\t// fleets at 100 req/s with a 5 s window that's ~500 entries — well\n\t// within memory budget. Phase C swaps this for the OTel subscription.\n\tconst samples: Sample[] = []\n\n\tfunction pruneExpired(t: number): void {\n\t\tconst cutoff = t - windowMs\n\t\twhile (samples.length > 0 && (samples[0] as Sample).t < cutoff) {\n\t\t\tsamples.shift()\n\t\t}\n\t}\n\n\treturn {\n\t\tname: `recent${Math.round(windowMs / 1000)}sErrorRate`,\n\t\tweight,\n\t\trecordSuccess(): void {\n\t\t\tsamples.push({ t: now(), e: false })\n\t\t},\n\t\trecordError(): void {\n\t\t\tsamples.push({ t: now(), e: true })\n\t\t},\n\t\treset(): void {\n\t\t\tsamples.length = 0\n\t\t},\n\t\tread(): SignalReading {\n\t\t\tconst t = now()\n\t\t\tpruneExpired(t)\n\t\t\tif (samples.length === 0 || samples.length < minSamples) {\n\t\t\t\t// Insufficient traffic → don't penalise. Return rate=0 +\n\t\t\t\t// factor=1 (visible in JSON for ops triage).\n\t\t\t\treturn { value: 0, healthFactor: 1 }\n\t\t\t}\n\t\t\tlet errors = 0\n\t\t\tfor (const s of samples) {\n\t\t\t\tif (s.e) errors++\n\t\t\t}\n\t\t\tconst rate = errors / samples.length\n\n\t\t\tlet factor: number\n\t\t\tif (rate <= degradedRate) factor = 1\n\t\t\telse if (rate >= deadRate) factor = 0\n\t\t\telse {\n\t\t\t\tconst span = deadRate - degradedRate\n\t\t\t\tfactor = 1 - (rate - degradedRate) / span\n\t\t\t}\n\n\t\t\t// Round to 4 decimal places — wire JSON stays compact and the\n\t\t\t// extra precision is meaningless for ops use.\n\t\t\tconst valueRounded = Math.round(rate * 10_000) / 10_000\n\n\t\t\treturn {\n\t\t\t\tvalue: valueRounded,\n\t\t\t\thealthFactor: factor,\n\t\t\t}\n\t\t},\n\t}\n}\n","/**\n * `memoryPressureSignal` — RSS / cgroup memory.max ratio (ADR-111 §4.3).\n *\n * On Linux containers we read the cgroup v2 memory limit from\n * `/sys/fs/cgroup/memory.max`. Then `pressure = process.memoryUsage().rss /\n * limit`. ADR-111 §4.3 default thresholds:\n * degradedRatio = 0.85 (85 % → degraded)\n * deadRatio = 0.95 (95 % → dead)\n *\n * Mapping observed-pressure → healthFactor:\n *\n * healthFactor\n * ▲\n * 1.0 ┤━━━━━━━━━━━━━━━━━━┓\n * │ ┃\n * 0.0 ┤ ┗━━━━━━━━━\n * ┼──────────────────┼─────────► pressure (0..1)\n * 0 degradedRatio deadRatio\n *\n * Graceful fallback (the cardinal-rule deference):\n * - cgroup file missing → unknown=true (signal ignored)\n * - cgroup file has 'max' → unlimited container, ratio undefined → unknown\n * - file unreadable / parse → unknown=true\n *\n * `unknown=true` makes the scoring strategy ignore the signal — we never\n * pretend to know memory pressure on a host where we can't measure it.\n *\n * cgroup v1 (legacy) lives at `/sys/fs/cgroup/memory/memory.limit_in_bytes`\n * — supported via `cgroupV1Path` for operators on older kernels.\n */\n\nimport { readFileSync } from 'node:fs'\nimport type { SignalReading, SyncSignal } from '../types'\n\nexport interface MemoryPressureOptions {\n\t/** Pressure below this is fully healthy (factor=1). Default 0.85. */\n\treadonly degradedRatio?: number\n\t/** Pressure above this is fully dead (factor=0). Default 0.95. */\n\treadonly deadRatio?: number\n\t/**\n\t * Custom cgroup v2 memory limit path. Default `/sys/fs/cgroup/memory.max`.\n\t */\n\treadonly cgroupV2Path?: string\n\t/**\n\t * Optional cgroup v1 fallback path. Default\n\t * `/sys/fs/cgroup/memory/memory.limit_in_bytes`. Used when v2 path is\n\t * unreadable AND `cgroupV1Path` is set or v2 doesn't exist.\n\t */\n\treadonly cgroupV1Path?: string\n\t/** Weight in the weighted-product score. Default 0.2 (ADR-111 §4.3). */\n\treadonly weight?: number\n\t/**\n\t * Optional injected `process.memoryUsage` for tests.\n\t * Default: `process.memoryUsage`.\n\t */\n\treadonly memoryUsage?: () => { rss: number }\n\t/** Optional injected file reader for tests. */\n\treadonly readFile?: (path: string) => string\n}\n\n/**\n * cgroup v1 reports a sentinel value (`9223372036854771712` =\n * `Number.MAX_SAFE_INTEGER` neighbourhood, well > 2^53) when the limit is\n * unconstrained. cgroup v2 uses the literal string `max`. Both collapse\n * to \"unlimited → unknown\".\n */\nconst CGROUP_V1_UNLIMITED_FLOOR = 1e18\n\nfunction tryRead(reader: (p: string) => string, path: string): string | null {\n\ttry {\n\t\treturn reader(path)\n\t} catch {\n\t\treturn null\n\t}\n}\n\n/**\n * Read the container memory limit in bytes. Returns `null` if unknown\n * (file missing, unparseable, or unlimited).\n *\n * Exposed for direct tests of the cgroup parsing logic.\n */\nexport function readContainerMemoryLimit(\n\treader: (p: string) => string,\n\tv2Path: string,\n\tv1Path: string,\n): number | null {\n\t// 1. Try cgroup v2 first — it's the modern default (Talos / cluster runs\n\t// cgroupv2 hybrid; ADR-111 cluster spec).\n\tconst v2 = tryRead(reader, v2Path)\n\tif (v2 !== null) {\n\t\tconst trimmed = v2.trim()\n\t\tif (trimmed === 'max' || trimmed === '') return null\n\t\tconst n = Number.parseInt(trimmed, 10)\n\t\tif (!Number.isFinite(n) || n <= 0) return null\n\t\t// Some kernels report ridiculous \"max-ish\" sentinel values via v2.\n\t\tif (n > CGROUP_V1_UNLIMITED_FLOOR) return null\n\t\treturn n\n\t}\n\t// 2. Fall back to cgroup v1 if v2 is unreadable.\n\tconst v1 = tryRead(reader, v1Path)\n\tif (v1 === null) return null\n\tconst trimmed = v1.trim()\n\tconst n = Number.parseInt(trimmed, 10)\n\tif (!Number.isFinite(n) || n <= 0) return null\n\tif (n > CGROUP_V1_UNLIMITED_FLOOR) return null\n\treturn n\n}\n\nexport function memoryPressureSignal(opts: MemoryPressureOptions = {}): SyncSignal {\n\tconst degradedRatio = opts.degradedRatio ?? 0.85\n\tconst deadRatio = opts.deadRatio ?? 0.95\n\tconst v2Path = opts.cgroupV2Path ?? '/sys/fs/cgroup/memory.max'\n\tconst v1Path = opts.cgroupV1Path ?? '/sys/fs/cgroup/memory/memory.limit_in_bytes'\n\tconst weight = opts.weight ?? 0.2\n\tconst memoryUsage =\n\t\topts.memoryUsage ??\n\t\t((): { rss: number } => {\n\t\t\tconst m = process.memoryUsage()\n\t\t\treturn { rss: m.rss }\n\t\t})\n\tconst readFile = opts.readFile ?? ((p: string) => readFileSync(p, 'utf8'))\n\n\tif (degradedRatio <= 0 || degradedRatio >= 1) {\n\t\tthrow new Error(`memoryPressureSignal: degradedRatio must be in (0, 1), got ${degradedRatio}`)\n\t}\n\tif (deadRatio <= degradedRatio || deadRatio > 1) {\n\t\tthrow new Error(\n\t\t\t`memoryPressureSignal: deadRatio must be in (degradedRatio, 1], got ${deadRatio}`,\n\t\t)\n\t}\n\n\t// Cache the cgroup limit — kernel doesn't change it at runtime in\n\t// Kubernetes (would require a deployment edit + pod recreate). One\n\t// readSync per process boot is cheap; per poll tick is wasteful.\n\tlet cachedLimit: number | null | undefined\n\tfunction getLimit(): number | null {\n\t\tif (cachedLimit === undefined) {\n\t\t\tcachedLimit = readContainerMemoryLimit(readFile, v2Path, v1Path)\n\t\t}\n\t\treturn cachedLimit\n\t}\n\n\treturn {\n\t\tname: 'memoryPressure',\n\t\tweight,\n\t\tread(): SignalReading {\n\t\t\tconst limit = getLimit()\n\t\t\tif (limit === null) {\n\t\t\t\t// No container limit known — degrade gracefully to \"unknown\"\n\t\t\t\t// rather than report a bogus ratio against host RAM.\n\t\t\t\treturn { value: 0, healthFactor: 1, unknown: true }\n\t\t\t}\n\t\t\tlet rss: number\n\t\t\ttry {\n\t\t\t\trss = memoryUsage().rss\n\t\t\t} catch {\n\t\t\t\treturn { value: 0, healthFactor: 1, unknown: true }\n\t\t\t}\n\t\t\tif (!Number.isFinite(rss) || rss < 0) {\n\t\t\t\treturn { value: 0, healthFactor: 1, unknown: true }\n\t\t\t}\n\t\t\tconst ratio = rss / limit\n\n\t\t\tlet factor: number\n\t\t\tif (ratio <= degradedRatio) factor = 1\n\t\t\telse if (ratio >= deadRatio) factor = 0\n\t\t\telse {\n\t\t\t\tconst span = deadRatio - degradedRatio\n\t\t\t\tfactor = 1 - (ratio - degradedRatio) / span\n\t\t\t}\n\n\t\t\t// 3-decimal precision keeps the wire JSON small.\n\t\t\tconst valueRounded = Math.round(ratio * 1000) / 1000\n\t\t\treturn { value: valueRounded, healthFactor: factor }\n\t\t},\n\t}\n}\n","/**\n * `queueDepthSignal` — backpressure indicator (ADR-111 §4.3).\n *\n * Generic signal: the app provides a `getter` that returns the current\n * length of whatever queue the operator wants probed (BullMQ, RabbitMQ,\n * in-memory work pool, …). The signal does NOT own the queue — the app\n * does. We just measure.\n *\n * Mapping observed-depth → healthFactor:\n *\n * healthFactor\n * ▲\n * 1.0 ┤━━━━━━━━━━━━━━━━━━┓\n * │ ┃ linear interpolation\n * 0.0 ┤ ┗━━━━━━━━━━━━\n * ┼──────────────────┼─────────► depth\n * 0 fullThreshold\n *\n * Below `fullThreshold` → factor 1. At `fullThreshold` → factor 0. Above\n * → factor 0. The implicit \"degraded\" zone is `[0.5 × fullThreshold,\n * fullThreshold]` — depth at half-full produces factor 0.5. Operators\n * tune `fullThreshold` to whatever their queue reasonably hits at peak\n * load; \"100% full\" means \"drain new traffic\", not \"kill the pod\" (the\n * three-tier gate at the sidecar handles the kill decision).\n *\n * `getter` errors are swallowed — a thrown getter produces `unknown=true`\n * (so the scoring strategy ignores this signal, instead of falsely\n * reporting score=0). The app's bug shouldn't masquerade as a sidecar\n * decision.\n */\n\nimport type { AsyncSignal, SignalReading } from '../types'\n\nexport interface QueueDepthOptions {\n\t/**\n\t * Sync or async getter the SDK calls every poll tick. Must return a\n\t * non-negative integer. Throws → reading marked `unknown=true`.\n\t */\n\treadonly getter: () => number | Promise<number>\n\t/**\n\t * Depth at which the queue is considered \"full\" (factor=0). Linear\n\t * interp from 0..fullThreshold. No default — operator-specific.\n\t */\n\treadonly fullThreshold: number\n\t/**\n\t * Optional below-which factor is always 1 (a \"soft floor\"). Default 0\n\t * — the linear interp starts at depth 0.\n\t */\n\treadonly healthyBelow?: number\n\t/** Weight in the weighted-product score. Default 0.2 (ADR-111 §4.3). */\n\treadonly weight?: number\n\t/** Custom signal name. Default `queueDepth`. */\n\treadonly name?: string\n}\n\nexport function queueDepthSignal(opts: QueueDepthOptions): AsyncSignal {\n\tif (typeof opts.getter !== 'function') {\n\t\tthrow new Error('queueDepthSignal: getter must be a function')\n\t}\n\tif (!Number.isFinite(opts.fullThreshold) || opts.fullThreshold <= 0) {\n\t\tthrow new Error(`queueDepthSignal: fullThreshold must be > 0, got ${opts.fullThreshold}`)\n\t}\n\tconst healthyBelow = opts.healthyBelow ?? 0\n\tif (!Number.isFinite(healthyBelow) || healthyBelow < 0 || healthyBelow >= opts.fullThreshold) {\n\t\tthrow new Error(\n\t\t\t`queueDepthSignal: healthyBelow must be in [0, fullThreshold), got ${healthyBelow}`,\n\t\t)\n\t}\n\tconst weight = opts.weight ?? 0.2\n\n\treturn {\n\t\tname: opts.name ?? 'queueDepth',\n\t\tweight,\n\t\tasync read(): Promise<SignalReading> {\n\t\t\tlet raw: unknown\n\t\t\ttry {\n\t\t\t\traw = await opts.getter()\n\t\t\t} catch {\n\t\t\t\t// Cardinal-rule deference: getter blew up → unknown.\n\t\t\t\treturn { value: 0, healthFactor: 1, unknown: true }\n\t\t\t}\n\t\t\tif (typeof raw !== 'number' || !Number.isFinite(raw) || raw < 0) {\n\t\t\t\treturn { value: 0, healthFactor: 1, unknown: true }\n\t\t\t}\n\t\t\tconst depth = raw\n\n\t\t\tlet factor: number\n\t\t\tif (depth <= healthyBelow) factor = 1\n\t\t\telse if (depth >= opts.fullThreshold) factor = 0\n\t\t\telse {\n\t\t\t\tconst span = opts.fullThreshold - healthyBelow\n\t\t\t\tfactor = 1 - (depth - healthyBelow) / span\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tvalue: depth,\n\t\t\t\thealthFactor: factor,\n\t\t\t}\n\t\t},\n\t}\n}\n","/**\n * `@sylphx/sdk/health` — Phase B multi-signal health score (ADR-111 §4).\n *\n * Apps register signals (event-loop lag, queue depth, error rate, memory\n * pressure, …); the SDK folds them into a continuous score in `[0, 1]`;\n * the **sidecar** maps the score to liveness / readiness / drain via the\n * three-tier gate. The SDK's responsibility ends at exposing the score.\n *\n * The wire format the sidecar parses (ADR-111 §3.2.4):\n *\n * ```json\n * {\n * \"score\": 0.92,\n * \"signals\": {\n * \"eventLoopLagMs\": 12,\n * \"queueDepth\": 3,\n * \"recent5sErrorRate\": 0.001,\n * \"memoryPressure\": 0.45\n * },\n * \"lastTickAt\": \"2026-05-03T12:34:56.789Z\"\n * }\n * ```\n *\n * Default signals (if `sylphxHealth()` called with no `signals`):\n * - `eventLoopLagSignal({ degradedMs: 5000, deadMs: 30000 })` (weight 1.0)\n *\n * Three-tier gate (decided by **sidecar**, not SDK; ADR-111 §4.4):\n *\n * | Score | Liveness | Readiness | Effect |\n * | -------------- | -------- | --------- | --------------------- |\n * | > 0.8 | 200 | 200 | Normal traffic |\n * | (0.5, 0.8] | 200 | 503 | Drain new, no kill |\n * | <= 0.5 | 503 | 503 | Kill after threshold |\n *\n * Apps don't need to think about probe semantics — that's the sidecar's\n * job. The app just exposes the score. See `apps/health-agent/` for the\n * sidecar implementation.\n *\n * @example Worked example — OpenClaw under PDF-extract load (ADR-111 §4.5):\n *\n * ```text\n * eventLoopLagMs = 6000 → factor 0.4\n * queueDepth = 12 → factor 1.0\n * errorRate = 0.002 → factor 1.0\n * memoryPressure = 0.55 → factor 1.0\n * score = 0.4^0.4 × 1.0^0.6 ≈ 0.69\n *\n * → falls in [0.5, 0.8] → sidecar drains traffic, doesn't kill.\n * Pod gets to finish PDF extraction.\n * ```\n */\n\nimport { evaluateEffect as buildEvaluateEffect } from './effects'\nimport { createNodeHandler, createWebHandler, type HealthEvaluator } from './handler'\nimport { defaultScoringStrategy } from './scoring'\nimport { eventLoopLagSignal } from './signals/event-loop-lag'\nimport type { HealthScore, Signal, SylphxHealthOptions } from './types'\nimport {\n\tstartUnixSocketServer,\n\ttype UnixSocketServerHandle,\n\ttype UnixSocketServerOptions,\n} from './unix-socket-server'\n\nexport {\n\tcreateNodeHandler,\n\tcreateWebHandler,\n\ttype HealthEvaluator,\n} from './handler'\nexport { defaultScoringStrategy, weightedProduct } from './scoring'\nexport type {\n\tErrorRateOptions,\n\tErrorRateSignalHandle,\n} from './signals/error-rate'\nexport { errorRateSignal } from './signals/error-rate'\nexport type { EventLoopLagOptions } from './signals/event-loop-lag'\nexport { eventLoopLagSignal } from './signals/event-loop-lag'\nexport type { MemoryPressureOptions } from './signals/memory-pressure'\nexport { memoryPressureSignal } from './signals/memory-pressure'\nexport type { QueueDepthOptions } from './signals/queue-depth'\nexport { queueDepthSignal } from './signals/queue-depth'\n// Re-exports — public surface.\nexport type {\n\tAsyncSignal,\n\tHealthScore,\n\tHealthSnapshot,\n\tScoringStrategy,\n\tSignal,\n\tSignalBase,\n\tSignalReading,\n\tSylphxHealthOptions,\n\tSyncSignal,\n} from './types'\nexport { HealthError } from './types'\nexport type {\n\tUnixSocketServerHandle,\n\tUnixSocketServerOptions,\n} from './unix-socket-server'\n\n/**\n * The handle returned by `sylphxHealth()`. Owns the registered signals,\n * exposes evaluation in both Promise + Effect form, and produces an HTTP\n * handler / Unix-socket server.\n *\n * Call `dispose()` during graceful shutdown to release per-signal\n * resources (e.g. the `monitorEventLoopDelay()` histogram).\n */\nexport interface SylphxHealth extends HealthEvaluator {\n\t/** All signals registered (read-only). */\n\treadonly signals: ReadonlyArray<Signal>\n\t/** Snapshot evaluation as a Promise. */\n\tevaluate(): Promise<HealthScore>\n\t/** Snapshot evaluation as an Effect (per Rule 21 / ADR-058 Amendment). */\n\treadonly evaluateEffect: ReturnType<typeof buildEvaluateEffect>\n\t/**\n\t * Web Fetch API HTTP handler — works under Hono, Bun.serve, Next.js\n\t * route.ts, itty-router, Hattip. Always returns 200 + JSON; the\n\t * sidecar applies the three-tier 200/503 gate.\n\t */\n\thandler(): (req?: Request) => Promise<Response>\n\t/** Node.js classic `(req, res)` handler — for Express / classic Fastify. */\n\tnodeHandler(): ReturnType<typeof createNodeHandler>\n\t/**\n\t * Bind a Bun Unix-domain socket and serve the same JSON. The sidecar\n\t * polls `/var/run/sylphx/health.sock` by default (ADR-111 §3.2.4).\n\t */\n\tserveUnixSocket(opts?: UnixSocketServerOptions): UnixSocketServerHandle\n\t/**\n\t * Tear down all registered signals. Idempotent. Call during graceful\n\t * shutdown to release histograms, file watchers, etc.\n\t */\n\tdispose(): void\n}\n\n/**\n * Build a `SylphxHealth` instance.\n *\n * @example Hono integration:\n * ```ts\n * import { Hono } from 'hono'\n * import {\n * sylphxHealth,\n * eventLoopLagSignal,\n * queueDepthSignal,\n * errorRateSignal,\n * memoryPressureSignal,\n * } from '@sylphx/sdk/health'\n *\n * const errors = errorRateSignal({ window: '5s', degradedRate: 0.05 })\n *\n * const health = sylphxHealth({\n * signals: [\n * eventLoopLagSignal({ degradedMs: 5000, deadMs: 30000 }),\n * queueDepthSignal({ getter: () => queue.size, fullThreshold: 1000 }),\n * errors,\n * memoryPressureSignal({ degradedRatio: 0.85 }),\n * ],\n * })\n *\n * const app = new Hono()\n * app.get('/healthz', health.handler())\n *\n * // Track requests for the error-rate signal\n * app.use(async (c, next) => {\n * try { await next(); errors.recordSuccess() }\n * catch (err) { errors.recordError(); throw err }\n * })\n *\n * // Or — primary transport for the sidecar:\n * health.serveUnixSocket() // → /var/run/sylphx/health.sock\n * ```\n *\n * @example Worked example — OpenClaw under PDF-extract load (ADR-111 §4.5):\n *\n * ```text\n * eventLoopLagMs = 6000 → factor 0.4\n * queueDepth = 12 → factor 1.0\n * errorRate = 0.002 → factor 1.0\n * memoryPressure = 0.55 → factor 1.0\n * score = 0.4^0.4 × 1.0^0.6 ≈ 0.69\n *\n * → falls in [0.5, 0.8] → sidecar drains traffic, doesn't kill.\n * Pod gets to finish PDF extraction.\n * ```\n */\nexport function sylphxHealth(opts: SylphxHealthOptions = {}): SylphxHealth {\n\t// ADR-111 §4.6: zero-config default — register a single\n\t// event-loop-lag signal so apps get a meaningful score without any\n\t// boilerplate. Once the app registers richer signals it overrides this.\n\tconst signals: Signal[] =\n\t\topts.signals && opts.signals.length > 0\n\t\t\t? [...opts.signals]\n\t\t\t: [eventLoopLagSignal({ degradedMs: 5000, deadMs: 30000, weight: 1 })]\n\n\tconst scoringStrategy = opts.scoringStrategy ?? defaultScoringStrategy()\n\tconst now = opts.now ?? ((): Date => new Date())\n\n\tlet disposed = false\n\n\tconst evaluate = async (): Promise<HealthScore> => {\n\t\tif (disposed) {\n\t\t\tthrow new Error('sylphxHealth: instance disposed')\n\t\t}\n\t\t// Read all signals in parallel — async-getter signals (queueDepth)\n\t\t// shouldn't serialise behind sync ones. `Promise.all` propagates a\n\t\t// thrown signal directly to the caller; signals are expected to\n\t\t// internally swallow errors and return `unknown=true` instead, so\n\t\t// we never hit this path in normal operation.\n\t\tconst readings = await Promise.all(\n\t\t\tsignals.map(async (signal) => ({\n\t\t\t\tsignal,\n\t\t\t\treading: await signal.read(),\n\t\t\t})),\n\t\t)\n\n\t\tconst score = scoringStrategy(readings)\n\n\t\tconst signalsMap: Record<string, number | string | boolean> = {}\n\t\tfor (const { signal, reading } of readings) {\n\t\t\tsignalsMap[signal.name] = reading.value\n\t\t}\n\n\t\treturn {\n\t\t\tscore,\n\t\t\tsignals: signalsMap,\n\t\t\tlastTickAt: now().toISOString(),\n\t\t}\n\t}\n\n\tconst evaluator: HealthEvaluator = { evaluate }\n\n\tconst evaluateEffect = buildEvaluateEffect(evaluate)\n\n\treturn {\n\t\tsignals,\n\t\tevaluate,\n\t\tevaluateEffect,\n\t\thandler(): (req?: Request) => Promise<Response> {\n\t\t\treturn createWebHandler(evaluator)\n\t\t},\n\t\tnodeHandler(): ReturnType<typeof createNodeHandler> {\n\t\t\treturn createNodeHandler(evaluator)\n\t\t},\n\t\tserveUnixSocket(unixOpts?: UnixSocketServerOptions): UnixSocketServerHandle {\n\t\t\treturn startUnixSocketServer(evaluator, unixOpts)\n\t\t},\n\t\tdispose(): void {\n\t\t\tif (disposed) return\n\t\t\tdisposed = true\n\t\t\tfor (const s of signals) {\n\t\t\t\ttry {\n\t\t\t\t\ts.dispose?.()\n\t\t\t\t} catch {\n\t\t\t\t\t// dispose must not throw — swallow to guarantee idempotency\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t}\n}\n"],"mappings":"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","names":["trimmed","n","evaluateEffect"]}
|
|
1
|
+
{"version":3,"sources":["../../src/health/effects.ts","../../src/health/types.ts","../../src/health/handler.ts","../../src/health/scoring.ts","../../src/health/signals/event-loop-lag.ts","../../src/health/unix-socket-server.ts","../../src/health/signals/error-rate.ts","../../src/health/signals/memory-pressure.ts","../../src/health/signals/queue-depth.ts","../../src/health/index.ts"],"sourcesContent":["/**\n * Effect TS integration for `@sylphx/sdk/health` (Rule 21 / ADR-058 Amendment).\n *\n * Effect-native services should NEVER call `Effect.runPromise` inside\n * business logic — this module exposes `evaluateEffect` so they can fold\n * the health computation into their own fiber graph.\n *\n * `effect` is an OPTIONAL peer dependency — apps that don't import this\n * module never pull it in (sideEffects: false + tree-shaking guarantees\n * this; verified in tests). The `Effect` type is imported from the\n * peer-dep at runtime; consumers either provide it or never reach this\n * code path.\n */\n\nimport { Effect } from 'effect'\nimport type { HealthError, HealthScore } from './types'\nimport { HealthError as HealthErrorClass } from './types'\n\n/**\n * Lift a `() => Promise<HealthScore>` evaluator into an Effect-native value.\n *\n * Errors thrown by the evaluator are tagged `HealthError` so callers can\n * use `Effect.catchTag('HealthError', …)` for typed recovery.\n *\n * @example\n * ```ts\n * import { Effect } from 'effect'\n * import { sylphxHealth } from '@sylphx/sdk/health'\n *\n * const health = sylphxHealth({ signals: [...] })\n *\n * const program = Effect.gen(function* () {\n * const { score, signals } = yield* health.evaluateEffect\n * yield* Effect.log(`health=${score.toFixed(2)} signals=${JSON.stringify(signals)}`)\n * return score\n * })\n *\n * // Only the entry point runs the Effect (Rule 21).\n * const finalScore = await Effect.runPromise(program)\n * ```\n */\nexport function evaluateEffect(\n\tevaluator: () => Promise<HealthScore>,\n): Effect.Effect<HealthScore, HealthError, never> {\n\treturn Effect.tryPromise({\n\t\ttry: () => evaluator(),\n\t\tcatch: (err) => new HealthErrorClass('health evaluation failed', err),\n\t})\n}\n","/**\n * Type SSOT for `@sylphx/sdk/health` (ADR-111 §4).\n *\n * Pure runtime types — framework-free, no schema library required at the\n * SDK boundary. Apps that want Standard Schema validation (per ADR-084)\n * can wrap the wire-format `HealthSnapshot` in their own schema.\n *\n * The wire shape (`HealthSnapshot`) is the **stable contract** the\n * `sylphx-health-agent` sidecar parses (see ADR-111 §3.2.4 +\n * `apps/health-agent/src/app-poll.ts::parseAppHealthBody`). Do not break it.\n */\n\n// ─── Signal — what an app registers ─────────────────────────────────────────\n\n/**\n * One reading produced by a `Signal.read()`. The aggregator turns each\n * reading into a `factor` in `[0, 1]` then folds them into the score via\n * the configured `ScoringStrategy`.\n *\n * `value` is the raw observation (ms, ratio, count, …) — surfaced verbatim\n * in the wire snapshot so operators can debug from JSON without re-running\n * the signal logic.\n *\n * `healthFactor` is the normalised health in `[0, 1]`:\n * - `1` = fully healthy\n * - `0` = dead (or \"we don't know\" if `unknown=true` and the policy says so)\n *\n * `unknown=true` means the signal **could not be measured this tick** (e.g.\n * cgroup file unreadable). Scoring strategies treat unknown signals as\n * `factor=1` (don't penalise an app for our own missing data).\n */\nexport interface SignalReading {\n\treadonly value: number | string | boolean\n\treadonly healthFactor: number\n\treadonly unknown?: boolean\n}\n\n/**\n * A health signal — one named, weighted measurement the score is built from.\n *\n * Two variants:\n * - `SyncSignal` — `read(): SignalReading` (e.g. event-loop lag)\n * - `AsyncSignal` — `read(): Promise<SignalReading>` (e.g. queue depth\n * fetched over IPC)\n *\n * The discriminated `Signal` union accepts both. The aggregator awaits\n * each reading uniformly via `Promise.resolve(signal.read())`.\n *\n * Implementations are pure — no internal mutation outside the closure-\n * captured monitor state (e.g. `monitorEventLoopDelay()`). Stop /\n * cleanup are exposed via `dispose()` for tests and graceful shutdown.\n */\nexport interface SignalBase {\n\t/** Unique stable identifier; appears in the wire `signals.<name>` map. */\n\treadonly name: string\n\t/**\n\t * Weight in the weighted-product score. Strictly `> 0` for active\n\t * signals; `0` is a no-op signal (kept for compatibility with\n\t * conditional registration but skipped in scoring).\n\t */\n\treadonly weight: number\n\t/**\n\t * Tear down any background work (timers, monitors, file watchers).\n\t * Called during graceful shutdown and from test cleanup.\n\t */\n\tdispose?(): void\n}\nexport interface SyncSignal extends SignalBase {\n\tread(): SignalReading\n}\nexport interface AsyncSignal extends SignalBase {\n\tread(): Promise<SignalReading>\n}\nexport type Signal = SyncSignal | AsyncSignal\n\n// ─── Score — what the sidecar reads ─────────────────────────────────────────\n\n/**\n * The complete health score + signal breakdown produced by `health.evaluate()`.\n *\n * `score` is the normalised aggregate in `[0, 1]`. Signal payload is\n * verbatim values from each `SignalReading.value` so operators can\n * cross-reference Grafana dashboards with the JSON.\n */\nexport interface HealthScore {\n\treadonly score: number\n\treadonly signals: Record<string, number | string | boolean>\n\treadonly lastTickAt: string\n}\n\n/**\n * Wire format the sidecar's `app-poll.ts::parseAppHealthBody` consumes.\n *\n * Pinned by ADR-111 §3.2.4 — keep stable. The sidecar tolerates extra\n * keys; do NOT remove existing ones without sequencing the sidecar update\n * first.\n */\nexport type HealthSnapshot = HealthScore\n\n// ─── Errors ─────────────────────────────────────────────────────────────────\n\n/**\n * Tagged error type for the Effect API. Promise consumers see the same\n * `message` via `Error.message` — the tag is for `Effect.catchTag`.\n */\nexport class HealthError extends Error {\n\treadonly _tag = 'HealthError' as const\n\treadonly cause?: unknown\n\tconstructor(message: string, cause?: unknown) {\n\t\tsuper(message)\n\t\tthis.name = 'HealthError'\n\t\tthis.cause = cause\n\t}\n}\n\n// ─── Scoring strategy ───────────────────────────────────────────────────────\n\n/**\n * `ScoringStrategy` collapses N readings → one `score` in `[0, 1]`.\n *\n * Default is `weightedProduct` (see `scoring.ts`). Custom strategies can\n * be plugged in via `sylphxHealth({ scoringStrategy: myStrategy })`.\n */\nexport type ScoringStrategy = (\n\treadings: ReadonlyArray<{ signal: Signal; reading: SignalReading }>,\n) => number\n\n// ─── Public option types ────────────────────────────────────────────────────\n\nexport interface SylphxHealthOptions {\n\t/**\n\t * Signals to register. If omitted, defaults to a single\n\t * `eventLoopLagSignal({ degradedMs: 5000, deadMs: 30000 })` per\n\t * ADR-111 §4.6 (\"sane out-of-box\").\n\t */\n\treadonly signals?: ReadonlyArray<Signal>\n\t/**\n\t * Strategy used to fold readings into a score. Defaults to\n\t * `weightedProduct` from `scoring.ts`.\n\t */\n\treadonly scoringStrategy?: ScoringStrategy\n\t/**\n\t * Optional injected clock — used by tests for deterministic\n\t * `lastTickAt` timestamps.\n\t */\n\treadonly now?: () => Date\n}\n","/**\n * Universal HTTP handler for `@sylphx/sdk/health` (ADR-111 §4.2).\n *\n * Framework-agnostic: returns either a `Web Fetch API` `Response`\n * (Hono / Bun.serve / itty-router / Next.js routes) or, via thin adapters,\n * a Node `(req, res) => void` (Express / Fastify-as-classic-handler).\n *\n * The handler always returns **HTTP 200** unless the SDK itself failed to\n * compute a score (rare; caught and returned as 500 for ops to triage).\n * The three-tier 200 / 503 gate (ADR-111 §4.4) lives in the **sidecar**,\n * not here — the SDK's job ends at exposing the score so the sidecar\n * can decide. This separation is documented in the README (\"apps don't\n * need to think about probe semantics\").\n */\n\nimport type { HealthScore } from './types'\n\n/**\n * Minimal contract a `health` instance must expose for the handler to\n * call. The full `health` object satisfies this implicitly via\n * `evaluate()` from `./index.ts`.\n */\nexport interface HealthEvaluator {\n\tevaluate(): Promise<HealthScore>\n}\n\n/**\n * Build a Web-Fetch-style handler. Suitable for:\n * - Hono: `app.get('/healthz', health.handler())`\n * - Bun.serve: `fetch: health.handler()`\n * - Next.js route.ts: `export const GET = health.handler()`\n * - itty-router / Hattip / standard `(Request) => Response` runtimes\n */\nexport function createWebHandler(source: HealthEvaluator): (req?: Request) => Promise<Response> {\n\treturn async (_req?: Request): Promise<Response> => {\n\t\ttry {\n\t\t\tconst score = await source.evaluate()\n\t\t\treturn new Response(JSON.stringify(score), {\n\t\t\t\tstatus: 200,\n\t\t\t\theaders: {\n\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t// Cache-Control: never cache. Even 1-second cache on a\n\t\t\t\t\t// CDN edge would mask a fast-moving health collapse.\n\t\t\t\t\t'Cache-Control': 'no-store, no-cache, must-revalidate',\n\t\t\t\t},\n\t\t\t})\n\t\t} catch (err) {\n\t\t\tconst message = err instanceof Error ? err.message : String(err)\n\t\t\treturn new Response(\n\t\t\t\tJSON.stringify({\n\t\t\t\t\terror: 'health_evaluator_failed',\n\t\t\t\t\tmessage,\n\t\t\t\t}),\n\t\t\t\t{\n\t\t\t\t\tstatus: 500,\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t\t'Cache-Control': 'no-store, no-cache, must-revalidate',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t)\n\t\t}\n\t}\n}\n\n/**\n * Build a Node-style `(req, res)` handler — for Express / classic Fastify.\n *\n * Adapter that delegates to `createWebHandler()` so logic stays in one\n * place. Imported lazily by callers that need it; doesn't drag any node\n * types into Web-only code paths.\n */\nexport interface NodeIncoming {\n\tmethod?: string\n\turl?: string\n}\nexport interface NodeOutgoing {\n\tstatusCode: number\n\tsetHeader(name: string, value: string): void\n\tend(body?: string): void\n}\nexport function createNodeHandler(\n\tsource: HealthEvaluator,\n): (req: NodeIncoming, res: NodeOutgoing) => Promise<void> {\n\treturn async (_req, res) => {\n\t\ttry {\n\t\t\tconst score = await source.evaluate()\n\t\t\tres.statusCode = 200\n\t\t\tres.setHeader('Content-Type', 'application/json')\n\t\t\tres.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate')\n\t\t\tres.end(JSON.stringify(score))\n\t\t} catch (err) {\n\t\t\tconst message = err instanceof Error ? err.message : String(err)\n\t\t\tres.statusCode = 500\n\t\t\tres.setHeader('Content-Type', 'application/json')\n\t\t\tres.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate')\n\t\t\tres.end(JSON.stringify({ error: 'health_evaluator_failed', message }))\n\t\t}\n\t}\n}\n","/**\n * Scoring strategies (ADR-111 §4 — three-tier health gate).\n *\n * The default `weightedProduct` strategy is multiplicative — a single bad\n * signal can drag the whole score down (any factor of 0 → score 0). This is\n * the right semantic for **liveness**: one critical subsystem dead means\n * the pod is not ready, regardless of how healthy the rest is.\n *\n * Mathematics:\n * score = ∏ factor_i ^ weight_i (over signals where weight > 0 AND\n * reading is not `unknown`)\n *\n * Where weights are normalised to sum=1 first, so absolute weight values\n * don't matter — only the ratios do. This keeps user expectations sane:\n * `[w=2, w=2]` and `[w=10, w=10]` produce identical scores.\n *\n * Edge cases (deterministic, never throw):\n * - empty input → 1 (perfect health, nothing to penalise)\n * - all weights = 0 → 1 (no active signals)\n * - all readings `unknown` → 1 (we can't see; cardinal-rule fallback\n * lives at the sidecar boundary, not\n * here — ADR-111 §3.2.5)\n * - factor < 0 / NaN → clamped to 0\n * - factor > 1 → clamped to 1\n * - weight < 0 → clamped to 0 (ignored)\n *\n * The clamps make us **safe by construction** — a misconfigured signal\n * cannot push the score outside `[0, 1]`. Tests exercise all branches.\n */\n\nimport type { ScoringStrategy, Signal, SignalReading } from './types'\n\n/** Clamp `x` to `[0, 1]`, mapping NaN to 0. */\nfunction clamp01(x: number): number {\n\tif (!Number.isFinite(x)) return 0\n\tif (x < 0) return 0\n\tif (x > 1) return 1\n\treturn x\n}\n\n/**\n * Weighted geometric mean — the default scoring strategy.\n *\n * @example\n * weightedProduct([\n * { signal: { name: 'lag', weight: 0.4 }, reading: { healthFactor: 0.4 } },\n * { signal: { name: 'q', weight: 0.6 }, reading: { healthFactor: 1.0 } },\n * ])\n * // → 0.4^0.4 × 1.0^0.6 ≈ 0.693 — ADR-111 §4.5 worked example\n */\nexport const weightedProduct: ScoringStrategy = (readings) => {\n\tif (readings.length === 0) return 1\n\n\t// 1. Filter active signals (weight > 0, reading defined, not unknown).\n\tconst active: Array<{ factor: number; weight: number }> = []\n\tfor (const { signal, reading } of readings) {\n\t\tconst w = signal.weight\n\t\tif (!Number.isFinite(w) || w <= 0) continue\n\t\tif (reading.unknown === true) continue\n\t\tactive.push({\n\t\t\tfactor: clamp01(reading.healthFactor),\n\t\t\tweight: w,\n\t\t})\n\t}\n\tif (active.length === 0) return 1\n\n\t// 2. Normalise weights to sum=1.\n\tconst totalWeight = active.reduce((sum, a) => sum + a.weight, 0)\n\tif (totalWeight <= 0 || !Number.isFinite(totalWeight)) return 1\n\n\t// 3. Geometric mean: exp(Σ w_i × ln(f_i)) — exp/log is more numerically\n\t// stable than repeated `Math.pow` × multiplication for small factors.\n\t// ln(0) → -Infinity → exp(-Infinity) → 0, which is exactly what we\n\t// want (any dead signal kills the score).\n\tlet logSum = 0\n\tfor (const { factor, weight } of active) {\n\t\tconst normalisedWeight = weight / totalWeight\n\t\tif (factor <= 0) {\n\t\t\t// Short-circuit: a single zero factor => score 0, regardless of\n\t\t\t// the others. Avoids ln(0) sentinel value plumbing.\n\t\t\treturn 0\n\t\t}\n\t\tlogSum += normalisedWeight * Math.log(factor)\n\t}\n\tconst score = Math.exp(logSum)\n\treturn clamp01(score)\n}\n\n/**\n * Convenience: build a default scoring strategy.\n *\n * Reserved for future: weighted-min, weighted-mean, etc. For now there's\n * one strategy and `weightedProduct` is the only export — this keeps the\n * surface narrow until a concrete second use-case appears.\n */\nexport function defaultScoringStrategy(): ScoringStrategy {\n\treturn weightedProduct\n}\n\n// Re-export the types used by callers writing custom strategies.\nexport type { ScoringStrategy, Signal, SignalReading }\n","/**\n * `eventLoopLagSignal` — main-thread blocking detector (ADR-111 §4.3).\n *\n * Measures Node/Bun's libuv event-loop delay using `monitorEventLoopDelay()`\n * from `node:perf_hooks`. The monitor is a histogram updated by libuv at a\n * configurable resolution (default 10 ms here — every tick measures lag\n * between expected wake and actual wake). We report the **max** observed\n * since the last `read()` then `reset()` — that's the worst-case stall\n * the app suffered in the polling window.\n *\n * Mapping observed-lag-ms → healthFactor ∈ [0, 1]:\n *\n * healthFactor\n * ▲\n * 1.0 ┤━━━━━━━━━┓\n * │ ┃ linear interpolation\n * │ ┃\n * 0.0 ┤ ┗━━━━━━━━━━━━━\n * ┼─────────┼──────────┼──────► observed lag (ms)\n * 0 degradedMs deadMs\n *\n * Below `degradedMs` → factor 1 (healthy). Above `deadMs` → factor 0 (dead).\n * In-between → linear interpolation. ADR-111 §4.3 default thresholds:\n * degradedMs = 5000 (5 s — same order as ADR-110's 10 s probe timeout)\n * deadMs = 30000 (30 s — definitely wedged)\n *\n * Bun-compatibility: Bun ships `monitorEventLoopDelay()` with the same\n * shape as Node 16+. Verified against `bun:1.3` in this repo's CI.\n */\n\nimport { monitorEventLoopDelay } from 'node:perf_hooks'\nimport type { SignalReading, SyncSignal } from '../types'\n\nexport interface EventLoopLagOptions {\n\t/** Lag below this is fully healthy (factor=1). Default 5000 ms. */\n\treadonly degradedMs?: number\n\t/** Lag above this is fully dead (factor=0). Default 30000 ms. */\n\treadonly deadMs?: number\n\t/**\n\t * Histogram resolution (ms). Default 10 ms — the smaller, the more\n\t * accurate the max but the higher the libuv accounting overhead.\n\t * Node defaults are also 10 ms.\n\t */\n\treadonly resolutionMs?: number\n\t/** Weight in the weighted-product score. Default 0.4 (ADR-111 §4.3). */\n\treadonly weight?: number\n\t/**\n\t * Optional injected monitor (tests). Defaults to a fresh\n\t * `monitorEventLoopDelay()` instance.\n\t */\n\treadonly monitor?: ReturnType<typeof monitorEventLoopDelay>\n}\n\n/**\n * Build an `event-loop-lag` signal. The returned object owns a started\n * histogram monitor; call `dispose()` during shutdown / between tests.\n */\nexport function eventLoopLagSignal(opts: EventLoopLagOptions = {}): SyncSignal {\n\tconst degradedMs = opts.degradedMs ?? 5000\n\tconst deadMs = opts.deadMs ?? 30000\n\tconst resolutionMs = opts.resolutionMs ?? 10\n\tconst weight = opts.weight ?? 0.4\n\n\tif (!Number.isFinite(degradedMs) || degradedMs < 0) {\n\t\tthrow new Error(`eventLoopLagSignal: degradedMs must be >= 0, got ${degradedMs}`)\n\t}\n\tif (!Number.isFinite(deadMs) || deadMs <= degradedMs) {\n\t\tthrow new Error(\n\t\t\t`eventLoopLagSignal: deadMs must be > degradedMs (${degradedMs}), got ${deadMs}`,\n\t\t)\n\t}\n\tif (!Number.isFinite(resolutionMs) || resolutionMs < 1) {\n\t\tthrow new Error(`eventLoopLagSignal: resolutionMs must be >= 1, got ${resolutionMs}`)\n\t}\n\n\tconst monitor = opts.monitor ?? monitorEventLoopDelay({ resolution: resolutionMs })\n\tmonitor.enable()\n\n\treturn {\n\t\tname: 'eventLoopLagMs',\n\t\tweight,\n\t\tread(): SignalReading {\n\t\t\t// `max` is in nanoseconds.\n\t\t\tconst maxNs = monitor.max\n\t\t\t// On a brand-new monitor or after `reset()` the histogram can\n\t\t\t// report `max=0` (no samples yet) or, on some Node versions,\n\t\t\t// `Number.MAX_SAFE_INTEGER` as a sentinel. Treat both as\n\t\t\t// \"no reading yet\" → unknown.\n\t\t\tif (!Number.isFinite(maxNs) || maxNs <= 0 || maxNs >= Number.MAX_SAFE_INTEGER) {\n\t\t\t\tmonitor.reset()\n\t\t\t\treturn { value: 0, healthFactor: 1, unknown: true }\n\t\t\t}\n\t\t\tconst observedMs = maxNs / 1_000_000\n\t\t\tmonitor.reset()\n\n\t\t\tlet factor: number\n\t\t\tif (observedMs <= degradedMs) factor = 1\n\t\t\telse if (observedMs >= deadMs) factor = 0\n\t\t\telse {\n\t\t\t\t// Linear interpolation between degraded (1) and dead (0).\n\t\t\t\tconst span = deadMs - degradedMs\n\t\t\t\tfactor = 1 - (observedMs - degradedMs) / span\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tvalue: Math.round(observedMs),\n\t\t\t\thealthFactor: factor,\n\t\t\t}\n\t\t},\n\t\tdispose(): void {\n\t\t\tmonitor.disable()\n\t\t},\n\t}\n}\n","/**\n * Unix-socket server for `@sylphx/sdk/health` (ADR-111 §3.2.4 + §4.2).\n *\n * The sidecar polls the app over a Unix socket — `/var/run/sylphx/health.sock`\n * by default. The shared volume (`emptyDir` mount, see ADR-111 §3.2.2) is\n * provisioned by the reconciler when the sidecar is injected. This server\n * binds Bun's native `Bun.serve({ unix: <path> })` and serves the same\n * JSON the HTTP handler does.\n *\n * Graceful shutdown is essential — the socket file lingers on disk if not\n * cleaned, and the sidecar's first reconnect attempt after a redeploy\n * would hit a stale inode. `shutdown()` calls `server.stop()` AND\n * unlinks the socket file. Process signal ownership stays with the app\n * entry point; SDK library code only returns an explicit shutdown handle.\n */\n\nimport { unlinkSync } from 'node:fs'\nimport { createWebHandler, type HealthEvaluator } from './handler'\n\n/** Bun-typed minimum for the server we need to control. */\ninterface BunServer {\n\tstop(force?: boolean): void\n\turl?: { href: string } | null\n}\n\nexport interface UnixSocketServerOptions {\n\t/** Absolute path to bind. Defaults to `/var/run/sylphx/health.sock`. */\n\treadonly path?: string\n\t/**\n\t * Override unlink behavior — useful for tests where the test runner\n\t * already owns the socket cleanup.\n\t */\n\treadonly unlinkOnShutdown?: boolean\n\t/**\n\t * Override the Bun runtime resolver. Used by tests to verify the\n\t * \"no-Bun\" error path without leaving a Bun-only test environment.\n\t * Production code reads `globalThis.Bun` directly.\n\t */\n\treadonly bunRuntime?: { serve: (cfg: unknown) => BunServer } | null\n}\n\nexport interface UnixSocketServerHandle {\n\treadonly path: string\n\treadonly server: BunServer\n\t/** Stop the server AND unlink the socket file (unless `unlinkOnShutdown=false`). */\n\tshutdown(): Promise<void>\n}\n\n/**\n * Start a Bun unix-socket HTTP server bound to `opts.path`. Returns the\n * handle so tests / shutdown handlers can call `shutdown()`.\n *\n * Pre-binds the socket: if a stale file from a prior crash exists at the\n * path, we `unlinkSync` it first so the bind doesn't `EADDRINUSE`. This\n * is safe because the path is operator-controlled (default lives under\n * `/var/run/sylphx/`, owned by the pod's UID 1000).\n */\nexport function startUnixSocketServer(\n\tsource: HealthEvaluator,\n\topts: UnixSocketServerOptions = {},\n): UnixSocketServerHandle {\n\t// Lazy reference to globalThis.Bun — keeps the import free of build-time\n\t// requirements on Bun for non-Bun consumers (server is no-op there).\n\t// Tests inject `opts.bunRuntime: null` to exercise the no-Bun path.\n\tconst bun =\n\t\topts.bunRuntime !== undefined\n\t\t\t? opts.bunRuntime\n\t\t\t: (globalThis as unknown as { Bun?: { serve: (cfg: unknown) => BunServer } }).Bun\n\tif (bun === null || bun === undefined || typeof bun.serve !== 'function') {\n\t\tthrow new Error(\n\t\t\t'startUnixSocketServer: Bun.serve is unavailable — Unix-socket transport requires a Bun runtime',\n\t\t)\n\t}\n\n\tconst path = opts.path ?? '/var/run/sylphx/health.sock'\n\tconst unlinkOnShutdown = opts.unlinkOnShutdown ?? true\n\n\t// Best-effort pre-cleanup of stale socket.\n\ttry {\n\t\tunlinkSync(path)\n\t} catch {\n\t\t// ENOENT is fine; anything else means the user has the wrong path\n\t\t// or perms — the bind below will surface a clear error.\n\t}\n\n\tconst handler = createWebHandler(source)\n\tconst server = bun.serve({\n\t\tunix: path,\n\t\tfetch: async (req: Request): Promise<Response> => handler(req),\n\t}) as BunServer\n\n\tconst handle: UnixSocketServerHandle = {\n\t\tpath,\n\t\tserver,\n\t\tasync shutdown(): Promise<void> {\n\t\t\ttry {\n\t\t\t\tserver.stop(true)\n\t\t\t} catch {\n\t\t\t\t// stopping a server that's already stopped is benign\n\t\t\t}\n\t\t\tif (unlinkOnShutdown) {\n\t\t\t\ttry {\n\t\t\t\t\tunlinkSync(path)\n\t\t\t\t} catch {\n\t\t\t\t\t// ENOENT — already gone, fine\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t}\n\n\treturn handle\n}\n","/**\n * `errorRateSignal` — request-error-rate over a sliding window (ADR-111 §4.3).\n *\n * Phase B uses an in-memory ring buffer of {timestamp, isError} samples.\n * Phase C will replace this with an OTel collector subscription so the\n * sidecar gets the same data without per-process bookkeeping; for now the\n * in-memory path keeps the SDK self-contained.\n *\n * Mapping observed-rate → healthFactor:\n *\n * healthFactor\n * ▲\n * 1.0 ┤━━━━━━━━━━━━━━━━━━━┓\n * │ ┃ linear interpolation\n * 0.0 ┤ ┗━━━━━━━━━━\n * ┼───────────────────┼────────► error rate (0..1)\n * 0 degradedRate deadRate\n *\n * Default thresholds match ADR-111 §4.3 row 3:\n * degradedRate = 0.05 (5 % errors → degraded)\n * deadRate = 0.50 (50 % errors → dead)\n *\n * `recordSuccess()` / `recordError()` are pushed by the app on each\n * request (or wired into Hono / Express middleware — the SDK provides\n * primitives, not framework integrations). Zero-traffic windows produce\n * `factor=1` (no requests = no errors = healthy by convention).\n */\n\nimport type { SignalReading, SyncSignal } from '../types'\n\nexport interface ErrorRateOptions {\n\t/** Sliding window length. Accepts ms (number) or '5s' / '1m' shorthand. */\n\treadonly window: number | `${number}s` | `${number}m`\n\t/** Rate above this is considered degraded. Default 0.05 (5 %). */\n\treadonly degradedRate?: number\n\t/** Rate at which the signal saturates to dead. Default 0.50 (50 %). */\n\treadonly deadRate?: number\n\t/**\n\t * Soft minimum sample count: until this many samples land, factor=1\n\t * regardless of rate (avoids \"1 error in 1 request → 100 %\" panics).\n\t * Default 10.\n\t */\n\treadonly minSamples?: number\n\t/** Weight in the weighted-product score. Default 0.2 (ADR-111 §4.3). */\n\treadonly weight?: number\n\t/** Optional injected clock for tests. */\n\treadonly now?: () => number\n}\n\nexport interface ErrorRateSignalHandle extends SyncSignal {\n\t/** Record a successful request (call from app middleware). */\n\trecordSuccess(): void\n\t/** Record a failed request (call from app middleware). */\n\trecordError(): void\n\t/** Erase the window. Useful for tests. */\n\treset(): void\n}\n\ninterface Sample {\n\treadonly t: number\n\treadonly e: boolean\n}\n\nfunction parseWindow(w: ErrorRateOptions['window']): number {\n\tif (typeof w === 'number') {\n\t\tif (!Number.isFinite(w) || w <= 0) {\n\t\t\tthrow new Error(`errorRateSignal: window must be > 0, got ${w}`)\n\t\t}\n\t\treturn w\n\t}\n\tconst m = /^(\\d+)(s|m)$/.exec(w)\n\tif (m === null) {\n\t\tthrow new Error(`errorRateSignal: invalid window '${w}', expected '5s' / '1m' / number`)\n\t}\n\tconst n = Number.parseInt(m[1] as string, 10)\n\tconst unit = m[2]\n\tconst ms = unit === 's' ? n * 1000 : n * 60_000\n\tif (ms <= 0) {\n\t\tthrow new Error(`errorRateSignal: parsed window ${ms}ms must be > 0`)\n\t}\n\treturn ms\n}\n\nexport function errorRateSignal(opts: ErrorRateOptions): ErrorRateSignalHandle {\n\tconst windowMs = parseWindow(opts.window)\n\tconst degradedRate = opts.degradedRate ?? 0.05\n\tconst deadRate = opts.deadRate ?? 0.5\n\tconst minSamples = opts.minSamples ?? 10\n\tconst weight = opts.weight ?? 0.2\n\tconst now = opts.now ?? Date.now\n\n\tif (degradedRate < 0 || degradedRate > 1) {\n\t\tthrow new Error(`errorRateSignal: degradedRate must be in [0, 1], got ${degradedRate}`)\n\t}\n\tif (deadRate <= degradedRate || deadRate > 1) {\n\t\tthrow new Error(`errorRateSignal: deadRate must be in (degradedRate, 1], got ${deadRate}`)\n\t}\n\tif (!Number.isFinite(minSamples) || minSamples < 1) {\n\t\tthrow new Error(`errorRateSignal: minSamples must be >= 1, got ${minSamples}`)\n\t}\n\n\t// Linked-list-style ring; we shift the head when entries age out. For\n\t// fleets at 100 req/s with a 5 s window that's ~500 entries — well\n\t// within memory budget. Phase C swaps this for the OTel subscription.\n\tconst samples: Sample[] = []\n\n\tfunction pruneExpired(t: number): void {\n\t\tconst cutoff = t - windowMs\n\t\twhile (samples.length > 0 && (samples[0] as Sample).t < cutoff) {\n\t\t\tsamples.shift()\n\t\t}\n\t}\n\n\treturn {\n\t\tname: `recent${Math.round(windowMs / 1000)}sErrorRate`,\n\t\tweight,\n\t\trecordSuccess(): void {\n\t\t\tsamples.push({ t: now(), e: false })\n\t\t},\n\t\trecordError(): void {\n\t\t\tsamples.push({ t: now(), e: true })\n\t\t},\n\t\treset(): void {\n\t\t\tsamples.length = 0\n\t\t},\n\t\tread(): SignalReading {\n\t\t\tconst t = now()\n\t\t\tpruneExpired(t)\n\t\t\tif (samples.length === 0 || samples.length < minSamples) {\n\t\t\t\t// Insufficient traffic → don't penalise. Return rate=0 +\n\t\t\t\t// factor=1 (visible in JSON for ops triage).\n\t\t\t\treturn { value: 0, healthFactor: 1 }\n\t\t\t}\n\t\t\tlet errors = 0\n\t\t\tfor (const s of samples) {\n\t\t\t\tif (s.e) errors++\n\t\t\t}\n\t\t\tconst rate = errors / samples.length\n\n\t\t\tlet factor: number\n\t\t\tif (rate <= degradedRate) factor = 1\n\t\t\telse if (rate >= deadRate) factor = 0\n\t\t\telse {\n\t\t\t\tconst span = deadRate - degradedRate\n\t\t\t\tfactor = 1 - (rate - degradedRate) / span\n\t\t\t}\n\n\t\t\t// Round to 4 decimal places — wire JSON stays compact and the\n\t\t\t// extra precision is meaningless for ops use.\n\t\t\tconst valueRounded = Math.round(rate * 10_000) / 10_000\n\n\t\t\treturn {\n\t\t\t\tvalue: valueRounded,\n\t\t\t\thealthFactor: factor,\n\t\t\t}\n\t\t},\n\t}\n}\n","/**\n * `memoryPressureSignal` — RSS / cgroup memory.max ratio (ADR-111 §4.3).\n *\n * On Linux containers we read the cgroup v2 memory limit from\n * `/sys/fs/cgroup/memory.max`. Then `pressure = process.memoryUsage().rss /\n * limit`. ADR-111 §4.3 default thresholds:\n * degradedRatio = 0.85 (85 % → degraded)\n * deadRatio = 0.95 (95 % → dead)\n *\n * Mapping observed-pressure → healthFactor:\n *\n * healthFactor\n * ▲\n * 1.0 ┤━━━━━━━━━━━━━━━━━━┓\n * │ ┃\n * 0.0 ┤ ┗━━━━━━━━━\n * ┼──────────────────┼─────────► pressure (0..1)\n * 0 degradedRatio deadRatio\n *\n * Graceful fallback (the cardinal-rule deference):\n * - cgroup file missing → unknown=true (signal ignored)\n * - cgroup file has 'max' → unlimited container, ratio undefined → unknown\n * - file unreadable / parse → unknown=true\n *\n * `unknown=true` makes the scoring strategy ignore the signal — we never\n * pretend to know memory pressure on a host where we can't measure it.\n *\n * cgroup v1 (legacy) lives at `/sys/fs/cgroup/memory/memory.limit_in_bytes`\n * — supported via `cgroupV1Path` for operators on older kernels.\n */\n\nimport { readFileSync } from 'node:fs'\nimport type { SignalReading, SyncSignal } from '../types'\n\nexport interface MemoryPressureOptions {\n\t/** Pressure below this is fully healthy (factor=1). Default 0.85. */\n\treadonly degradedRatio?: number\n\t/** Pressure above this is fully dead (factor=0). Default 0.95. */\n\treadonly deadRatio?: number\n\t/**\n\t * Custom cgroup v2 memory limit path. Default `/sys/fs/cgroup/memory.max`.\n\t */\n\treadonly cgroupV2Path?: string\n\t/**\n\t * Optional cgroup v1 fallback path. Default\n\t * `/sys/fs/cgroup/memory/memory.limit_in_bytes`. Used when v2 path is\n\t * unreadable AND `cgroupV1Path` is set or v2 doesn't exist.\n\t */\n\treadonly cgroupV1Path?: string\n\t/** Weight in the weighted-product score. Default 0.2 (ADR-111 §4.3). */\n\treadonly weight?: number\n\t/**\n\t * Optional injected `process.memoryUsage` for tests.\n\t * Default: `process.memoryUsage`.\n\t */\n\treadonly memoryUsage?: () => { rss: number }\n\t/** Optional injected file reader for tests. */\n\treadonly readFile?: (path: string) => string\n}\n\n/**\n * cgroup v1 reports a sentinel value (`9223372036854771712` =\n * `Number.MAX_SAFE_INTEGER` neighbourhood, well > 2^53) when the limit is\n * unconstrained. cgroup v2 uses the literal string `max`. Both collapse\n * to \"unlimited → unknown\".\n */\nconst CGROUP_V1_UNLIMITED_FLOOR = 1e18\n\nfunction tryRead(reader: (p: string) => string, path: string): string | null {\n\ttry {\n\t\treturn reader(path)\n\t} catch {\n\t\treturn null\n\t}\n}\n\n/**\n * Read the container memory limit in bytes. Returns `null` if unknown\n * (file missing, unparseable, or unlimited).\n *\n * Exposed for direct tests of the cgroup parsing logic.\n */\nexport function readContainerMemoryLimit(\n\treader: (p: string) => string,\n\tv2Path: string,\n\tv1Path: string,\n): number | null {\n\t// 1. Try cgroup v2 first — it's the modern default (Talos / cluster runs\n\t// cgroupv2 hybrid; ADR-111 cluster spec).\n\tconst v2 = tryRead(reader, v2Path)\n\tif (v2 !== null) {\n\t\tconst trimmed = v2.trim()\n\t\tif (trimmed === 'max' || trimmed === '') return null\n\t\tconst n = Number.parseInt(trimmed, 10)\n\t\tif (!Number.isFinite(n) || n <= 0) return null\n\t\t// Some kernels report ridiculous \"max-ish\" sentinel values via v2.\n\t\tif (n > CGROUP_V1_UNLIMITED_FLOOR) return null\n\t\treturn n\n\t}\n\t// 2. Fall back to cgroup v1 if v2 is unreadable.\n\tconst v1 = tryRead(reader, v1Path)\n\tif (v1 === null) return null\n\tconst trimmed = v1.trim()\n\tconst n = Number.parseInt(trimmed, 10)\n\tif (!Number.isFinite(n) || n <= 0) return null\n\tif (n > CGROUP_V1_UNLIMITED_FLOOR) return null\n\treturn n\n}\n\nexport function memoryPressureSignal(opts: MemoryPressureOptions = {}): SyncSignal {\n\tconst degradedRatio = opts.degradedRatio ?? 0.85\n\tconst deadRatio = opts.deadRatio ?? 0.95\n\tconst v2Path = opts.cgroupV2Path ?? '/sys/fs/cgroup/memory.max'\n\tconst v1Path = opts.cgroupV1Path ?? '/sys/fs/cgroup/memory/memory.limit_in_bytes'\n\tconst weight = opts.weight ?? 0.2\n\tconst memoryUsage =\n\t\topts.memoryUsage ??\n\t\t((): { rss: number } => {\n\t\t\tconst m = process.memoryUsage()\n\t\t\treturn { rss: m.rss }\n\t\t})\n\tconst readFile = opts.readFile ?? ((p: string) => readFileSync(p, 'utf8'))\n\n\tif (degradedRatio <= 0 || degradedRatio >= 1) {\n\t\tthrow new Error(`memoryPressureSignal: degradedRatio must be in (0, 1), got ${degradedRatio}`)\n\t}\n\tif (deadRatio <= degradedRatio || deadRatio > 1) {\n\t\tthrow new Error(\n\t\t\t`memoryPressureSignal: deadRatio must be in (degradedRatio, 1], got ${deadRatio}`,\n\t\t)\n\t}\n\n\t// Cache the cgroup limit — kernel doesn't change it at runtime in\n\t// Kubernetes (would require a deployment edit + pod recreate). One\n\t// readSync per process boot is cheap; per poll tick is wasteful.\n\tlet cachedLimit: number | null | undefined\n\tfunction getLimit(): number | null {\n\t\tif (cachedLimit === undefined) {\n\t\t\tcachedLimit = readContainerMemoryLimit(readFile, v2Path, v1Path)\n\t\t}\n\t\treturn cachedLimit\n\t}\n\n\treturn {\n\t\tname: 'memoryPressure',\n\t\tweight,\n\t\tread(): SignalReading {\n\t\t\tconst limit = getLimit()\n\t\t\tif (limit === null) {\n\t\t\t\t// No container limit known — degrade gracefully to \"unknown\"\n\t\t\t\t// rather than report a bogus ratio against host RAM.\n\t\t\t\treturn { value: 0, healthFactor: 1, unknown: true }\n\t\t\t}\n\t\t\tlet rss: number\n\t\t\ttry {\n\t\t\t\trss = memoryUsage().rss\n\t\t\t} catch {\n\t\t\t\treturn { value: 0, healthFactor: 1, unknown: true }\n\t\t\t}\n\t\t\tif (!Number.isFinite(rss) || rss < 0) {\n\t\t\t\treturn { value: 0, healthFactor: 1, unknown: true }\n\t\t\t}\n\t\t\tconst ratio = rss / limit\n\n\t\t\tlet factor: number\n\t\t\tif (ratio <= degradedRatio) factor = 1\n\t\t\telse if (ratio >= deadRatio) factor = 0\n\t\t\telse {\n\t\t\t\tconst span = deadRatio - degradedRatio\n\t\t\t\tfactor = 1 - (ratio - degradedRatio) / span\n\t\t\t}\n\n\t\t\t// 3-decimal precision keeps the wire JSON small.\n\t\t\tconst valueRounded = Math.round(ratio * 1000) / 1000\n\t\t\treturn { value: valueRounded, healthFactor: factor }\n\t\t},\n\t}\n}\n","/**\n * `queueDepthSignal` — backpressure indicator (ADR-111 §4.3).\n *\n * Generic signal: the app provides a `getter` that returns the current\n * length of whatever queue the operator wants probed (BullMQ, RabbitMQ,\n * in-memory work pool, …). The signal does NOT own the queue — the app\n * does. We just measure.\n *\n * Mapping observed-depth → healthFactor:\n *\n * healthFactor\n * ▲\n * 1.0 ┤━━━━━━━━━━━━━━━━━━┓\n * │ ┃ linear interpolation\n * 0.0 ┤ ┗━━━━━━━━━━━━\n * ┼──────────────────┼─────────► depth\n * 0 fullThreshold\n *\n * Below `fullThreshold` → factor 1. At `fullThreshold` → factor 0. Above\n * → factor 0. The implicit \"degraded\" zone is `[0.5 × fullThreshold,\n * fullThreshold]` — depth at half-full produces factor 0.5. Operators\n * tune `fullThreshold` to whatever their queue reasonably hits at peak\n * load; \"100% full\" means \"drain new traffic\", not \"kill the pod\" (the\n * three-tier gate at the sidecar handles the kill decision).\n *\n * `getter` errors are swallowed — a thrown getter produces `unknown=true`\n * (so the scoring strategy ignores this signal, instead of falsely\n * reporting score=0). The app's bug shouldn't masquerade as a sidecar\n * decision.\n */\n\nimport type { AsyncSignal, SignalReading } from '../types'\n\nexport interface QueueDepthOptions {\n\t/**\n\t * Sync or async getter the SDK calls every poll tick. Must return a\n\t * non-negative integer. Throws → reading marked `unknown=true`.\n\t */\n\treadonly getter: () => number | Promise<number>\n\t/**\n\t * Depth at which the queue is considered \"full\" (factor=0). Linear\n\t * interp from 0..fullThreshold. No default — operator-specific.\n\t */\n\treadonly fullThreshold: number\n\t/**\n\t * Optional below-which factor is always 1 (a \"soft floor\"). Default 0\n\t * — the linear interp starts at depth 0.\n\t */\n\treadonly healthyBelow?: number\n\t/** Weight in the weighted-product score. Default 0.2 (ADR-111 §4.3). */\n\treadonly weight?: number\n\t/** Custom signal name. Default `queueDepth`. */\n\treadonly name?: string\n}\n\nexport function queueDepthSignal(opts: QueueDepthOptions): AsyncSignal {\n\tif (typeof opts.getter !== 'function') {\n\t\tthrow new Error('queueDepthSignal: getter must be a function')\n\t}\n\tif (!Number.isFinite(opts.fullThreshold) || opts.fullThreshold <= 0) {\n\t\tthrow new Error(`queueDepthSignal: fullThreshold must be > 0, got ${opts.fullThreshold}`)\n\t}\n\tconst healthyBelow = opts.healthyBelow ?? 0\n\tif (!Number.isFinite(healthyBelow) || healthyBelow < 0 || healthyBelow >= opts.fullThreshold) {\n\t\tthrow new Error(\n\t\t\t`queueDepthSignal: healthyBelow must be in [0, fullThreshold), got ${healthyBelow}`,\n\t\t)\n\t}\n\tconst weight = opts.weight ?? 0.2\n\n\treturn {\n\t\tname: opts.name ?? 'queueDepth',\n\t\tweight,\n\t\tasync read(): Promise<SignalReading> {\n\t\t\tlet raw: unknown\n\t\t\ttry {\n\t\t\t\traw = await opts.getter()\n\t\t\t} catch {\n\t\t\t\t// Cardinal-rule deference: getter blew up → unknown.\n\t\t\t\treturn { value: 0, healthFactor: 1, unknown: true }\n\t\t\t}\n\t\t\tif (typeof raw !== 'number' || !Number.isFinite(raw) || raw < 0) {\n\t\t\t\treturn { value: 0, healthFactor: 1, unknown: true }\n\t\t\t}\n\t\t\tconst depth = raw\n\n\t\t\tlet factor: number\n\t\t\tif (depth <= healthyBelow) factor = 1\n\t\t\telse if (depth >= opts.fullThreshold) factor = 0\n\t\t\telse {\n\t\t\t\tconst span = opts.fullThreshold - healthyBelow\n\t\t\t\tfactor = 1 - (depth - healthyBelow) / span\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tvalue: depth,\n\t\t\t\thealthFactor: factor,\n\t\t\t}\n\t\t},\n\t}\n}\n","/**\n * `@sylphx/sdk/health` — Phase B multi-signal health score (ADR-111 §4).\n *\n * Apps register signals (event-loop lag, queue depth, error rate, memory\n * pressure, …); the SDK folds them into a continuous score in `[0, 1]`;\n * the **sidecar** maps the score to liveness / readiness / drain via the\n * three-tier gate. The SDK's responsibility ends at exposing the score.\n *\n * The wire format the sidecar parses (ADR-111 §3.2.4):\n *\n * ```json\n * {\n * \"score\": 0.92,\n * \"signals\": {\n * \"eventLoopLagMs\": 12,\n * \"queueDepth\": 3,\n * \"recent5sErrorRate\": 0.001,\n * \"memoryPressure\": 0.45\n * },\n * \"lastTickAt\": \"2026-05-03T12:34:56.789Z\"\n * }\n * ```\n *\n * Default signals (if `sylphxHealth()` called with no `signals`):\n * - `eventLoopLagSignal({ degradedMs: 5000, deadMs: 30000 })` (weight 1.0)\n *\n * Three-tier gate (decided by **sidecar**, not SDK; ADR-111 §4.4):\n *\n * | Score | Liveness | Readiness | Effect |\n * | -------------- | -------- | --------- | --------------------- |\n * | > 0.8 | 200 | 200 | Normal traffic |\n * | (0.5, 0.8] | 200 | 503 | Drain new, no kill |\n * | <= 0.5 | 503 | 503 | Kill after threshold |\n *\n * Apps don't need to think about probe semantics — that's the sidecar's\n * job. The app just exposes the score. See `apps/health-agent/` for the\n * sidecar implementation.\n *\n * @example Worked example — OpenClaw under PDF-extract load (ADR-111 §4.5):\n *\n * ```text\n * eventLoopLagMs = 6000 → factor 0.4\n * queueDepth = 12 → factor 1.0\n * errorRate = 0.002 → factor 1.0\n * memoryPressure = 0.55 → factor 1.0\n * score = 0.4^0.4 × 1.0^0.6 ≈ 0.69\n *\n * → falls in [0.5, 0.8] → sidecar drains traffic, doesn't kill.\n * Pod gets to finish PDF extraction.\n * ```\n */\n\nimport { evaluateEffect as buildEvaluateEffect } from './effects'\nimport { createNodeHandler, createWebHandler, type HealthEvaluator } from './handler'\nimport { defaultScoringStrategy } from './scoring'\nimport { eventLoopLagSignal } from './signals/event-loop-lag'\nimport type { HealthScore, Signal, SylphxHealthOptions } from './types'\nimport {\n\tstartUnixSocketServer,\n\ttype UnixSocketServerHandle,\n\ttype UnixSocketServerOptions,\n} from './unix-socket-server'\n\nexport {\n\tcreateNodeHandler,\n\tcreateWebHandler,\n\ttype HealthEvaluator,\n} from './handler'\nexport { defaultScoringStrategy, weightedProduct } from './scoring'\nexport type {\n\tErrorRateOptions,\n\tErrorRateSignalHandle,\n} from './signals/error-rate'\nexport { errorRateSignal } from './signals/error-rate'\nexport type { EventLoopLagOptions } from './signals/event-loop-lag'\nexport { eventLoopLagSignal } from './signals/event-loop-lag'\nexport type { MemoryPressureOptions } from './signals/memory-pressure'\nexport { memoryPressureSignal } from './signals/memory-pressure'\nexport type { QueueDepthOptions } from './signals/queue-depth'\nexport { queueDepthSignal } from './signals/queue-depth'\n// Re-exports — public surface.\nexport type {\n\tAsyncSignal,\n\tHealthScore,\n\tHealthSnapshot,\n\tScoringStrategy,\n\tSignal,\n\tSignalBase,\n\tSignalReading,\n\tSylphxHealthOptions,\n\tSyncSignal,\n} from './types'\nexport { HealthError } from './types'\nexport type {\n\tUnixSocketServerHandle,\n\tUnixSocketServerOptions,\n} from './unix-socket-server'\n\n/**\n * The handle returned by `sylphxHealth()`. Owns the registered signals,\n * exposes evaluation in both Promise + Effect form, and produces an HTTP\n * handler / Unix-socket server.\n *\n * Call `dispose()` during graceful shutdown to release per-signal\n * resources (e.g. the `monitorEventLoopDelay()` histogram).\n */\nexport interface SylphxHealth extends HealthEvaluator {\n\t/** All signals registered (read-only). */\n\treadonly signals: ReadonlyArray<Signal>\n\t/** Snapshot evaluation as a Promise. */\n\tevaluate(): Promise<HealthScore>\n\t/** Snapshot evaluation as an Effect (per Rule 21 / ADR-058 Amendment). */\n\treadonly evaluateEffect: ReturnType<typeof buildEvaluateEffect>\n\t/**\n\t * Web Fetch API HTTP handler — works under Hono, Bun.serve, Next.js\n\t * route.ts, itty-router, Hattip. Always returns 200 + JSON; the\n\t * sidecar applies the three-tier 200/503 gate.\n\t */\n\thandler(): (req?: Request) => Promise<Response>\n\t/** Node.js classic `(req, res)` handler — for Express / classic Fastify. */\n\tnodeHandler(): ReturnType<typeof createNodeHandler>\n\t/**\n\t * Bind a Bun Unix-domain socket and serve the same JSON. The sidecar\n\t * polls `/var/run/sylphx/health.sock` by default (ADR-111 §3.2.4).\n\t */\n\tserveUnixSocket(opts?: UnixSocketServerOptions): UnixSocketServerHandle\n\t/**\n\t * Tear down all registered signals. Idempotent. Call during graceful\n\t * shutdown to release histograms, file watchers, etc.\n\t */\n\tdispose(): void\n}\n\n/**\n * Build a `SylphxHealth` instance.\n *\n * @example Hono integration:\n * ```ts\n * import { Hono } from 'hono'\n * import {\n * sylphxHealth,\n * eventLoopLagSignal,\n * queueDepthSignal,\n * errorRateSignal,\n * memoryPressureSignal,\n * } from '@sylphx/sdk/health'\n *\n * const errors = errorRateSignal({ window: '5s', degradedRate: 0.05 })\n *\n * const health = sylphxHealth({\n * signals: [\n * eventLoopLagSignal({ degradedMs: 5000, deadMs: 30000 }),\n * queueDepthSignal({ getter: () => queue.size, fullThreshold: 1000 }),\n * errors,\n * memoryPressureSignal({ degradedRatio: 0.85 }),\n * ],\n * })\n *\n * const app = new Hono()\n * app.get('/healthz', health.handler())\n *\n * // Track requests for the error-rate signal\n * app.use(async (c, next) => {\n * try { await next(); errors.recordSuccess() }\n * catch (err) { errors.recordError(); throw err }\n * })\n *\n * // Or — primary transport for the sidecar:\n * health.serveUnixSocket() // → /var/run/sylphx/health.sock\n * ```\n *\n * @example Worked example — OpenClaw under PDF-extract load (ADR-111 §4.5):\n *\n * ```text\n * eventLoopLagMs = 6000 → factor 0.4\n * queueDepth = 12 → factor 1.0\n * errorRate = 0.002 → factor 1.0\n * memoryPressure = 0.55 → factor 1.0\n * score = 0.4^0.4 × 1.0^0.6 ≈ 0.69\n *\n * → falls in [0.5, 0.8] → sidecar drains traffic, doesn't kill.\n * Pod gets to finish PDF extraction.\n * ```\n */\nexport function sylphxHealth(opts: SylphxHealthOptions = {}): SylphxHealth {\n\t// ADR-111 §4.6: zero-config default — register a single\n\t// event-loop-lag signal so apps get a meaningful score without any\n\t// boilerplate. Once the app registers richer signals it overrides this.\n\tconst signals: Signal[] =\n\t\topts.signals && opts.signals.length > 0\n\t\t\t? [...opts.signals]\n\t\t\t: [eventLoopLagSignal({ degradedMs: 5000, deadMs: 30000, weight: 1 })]\n\n\tconst scoringStrategy = opts.scoringStrategy ?? defaultScoringStrategy()\n\tconst now = opts.now ?? ((): Date => new Date())\n\n\tlet disposed = false\n\n\tconst evaluate = async (): Promise<HealthScore> => {\n\t\tif (disposed) {\n\t\t\tthrow new Error('sylphxHealth: instance disposed')\n\t\t}\n\t\t// Read all signals in parallel — async-getter signals (queueDepth)\n\t\t// shouldn't serialise behind sync ones. `Promise.all` propagates a\n\t\t// thrown signal directly to the caller; signals are expected to\n\t\t// internally swallow errors and return `unknown=true` instead, so\n\t\t// we never hit this path in normal operation.\n\t\tconst readings = await Promise.all(\n\t\t\tsignals.map(async (signal) => ({\n\t\t\t\tsignal,\n\t\t\t\treading: await signal.read(),\n\t\t\t})),\n\t\t)\n\n\t\tconst score = scoringStrategy(readings)\n\n\t\tconst signalsMap: Record<string, number | string | boolean> = {}\n\t\tfor (const { signal, reading } of readings) {\n\t\t\tsignalsMap[signal.name] = reading.value\n\t\t}\n\n\t\treturn {\n\t\t\tscore,\n\t\t\tsignals: signalsMap,\n\t\t\tlastTickAt: now().toISOString(),\n\t\t}\n\t}\n\n\tconst evaluator: HealthEvaluator = { evaluate }\n\n\tconst evaluateEffect = buildEvaluateEffect(evaluate)\n\n\treturn {\n\t\tsignals,\n\t\tevaluate,\n\t\tevaluateEffect,\n\t\thandler(): (req?: Request) => Promise<Response> {\n\t\t\treturn createWebHandler(evaluator)\n\t\t},\n\t\tnodeHandler(): ReturnType<typeof createNodeHandler> {\n\t\t\treturn createNodeHandler(evaluator)\n\t\t},\n\t\tserveUnixSocket(unixOpts?: UnixSocketServerOptions): UnixSocketServerHandle {\n\t\t\treturn startUnixSocketServer(evaluator, unixOpts)\n\t\t},\n\t\tdispose(): void {\n\t\t\tif (disposed) return\n\t\t\tdisposed = true\n\t\t\tfor (const s of signals) {\n\t\t\t\ttry {\n\t\t\t\t\ts.dispose?.()\n\t\t\t\t} catch {\n\t\t\t\t\t// dispose must not throw — swallow to guarantee idempotency\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t}\n}\n"],"mappings":"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","names":["trimmed","n","evaluateEffect"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -82,12 +82,15 @@ interface PlatformRealtimeDeleteChannelResult {
|
|
|
82
82
|
* Implements the canonical connection string format defined in ADR-055 §5.
|
|
83
83
|
* This is a self-contained copy for SDK package independence (no app imports).
|
|
84
84
|
*
|
|
85
|
-
*
|
|
86
|
-
* sylphx://{credential}@{slug}.
|
|
85
|
+
* Hosted format:
|
|
86
|
+
* sylphx://{credential}@{tenant-slug}.api.sylphx.com[:port][/v{version}]
|
|
87
|
+
*
|
|
88
|
+
* Custom/self-hosted domains are also accepted as long as the first DNS label
|
|
89
|
+
* is the tenant slug.
|
|
87
90
|
*
|
|
88
91
|
* Examples:
|
|
89
|
-
* sylphx://pk_prod_f19e5cdc3cc54f7ff81bdc26ec5bfbad@bold-river-a1b2c3.sylphx.com
|
|
90
|
-
* sylphx://sk_prod_5120bfeb5120bfeb5120bfeb5120bfeb@bold-river-a1b2c3.sylphx.com/v1
|
|
92
|
+
* sylphx://pk_prod_f19e5cdc3cc54f7ff81bdc26ec5bfbad@bold-river-a1b2c3.api.sylphx.com
|
|
93
|
+
* sylphx://sk_prod_5120bfeb5120bfeb5120bfeb5120bfeb@bold-river-a1b2c3.api.sylphx.com/v1
|
|
91
94
|
* sylphx://pk_dev_abc12345abc12345abc12345abc12345@calm-peak-z9x4d5.sylphx.dev
|
|
92
95
|
*
|
|
93
96
|
* Invariants:
|
|
@@ -110,9 +113,9 @@ interface ParsedConnectionUrl {
|
|
|
110
113
|
readonly env: ConnectionEnv;
|
|
111
114
|
/** First DNS label of the host — the resource slug (e.g. `bold-river-a1b2c3`) */
|
|
112
115
|
readonly slug: string;
|
|
113
|
-
/** Full host including port when present (e.g. `bold-river-a1b2c3.sylphx.com`) */
|
|
116
|
+
/** Full host including port when present (e.g. `bold-river-a1b2c3.api.sylphx.com`) */
|
|
114
117
|
readonly host: string;
|
|
115
|
-
/** Ready-to-use SDK base URL, always HTTPS (e.g. `https://bold-river-a1b2c3.sylphx.com/v1`) */
|
|
118
|
+
/** Ready-to-use SDK base URL, always HTTPS (e.g. `https://bold-river-a1b2c3.api.sylphx.com/v1`) */
|
|
116
119
|
readonly apiBaseUrl: string;
|
|
117
120
|
}
|
|
118
121
|
declare class InvalidConnectionUrlError extends Error {
|
|
@@ -132,7 +135,7 @@ declare class InvalidConnectionUrlError extends Error {
|
|
|
132
135
|
* import { createClient } from '@sylphx/sdk'
|
|
133
136
|
*
|
|
134
137
|
* const sylphx = createClient(process.env.SYLPHX_URL!)
|
|
135
|
-
* // Parses: sylphx://pk_prod_{hex}@bold-river-a1b2c3.sylphx.com
|
|
138
|
+
* // Parses: sylphx://pk_prod_{hex}@bold-river-a1b2c3.api.sylphx.com
|
|
136
139
|
* ```
|
|
137
140
|
*/
|
|
138
141
|
|
|
@@ -151,7 +154,7 @@ interface SylphxConfig {
|
|
|
151
154
|
readonly env: 'dev' | 'stg' | 'prod' | 'prev';
|
|
152
155
|
/** Resource slug (first DNS label), e.g. 'bold-river-a1b2c3' */
|
|
153
156
|
readonly slug: string;
|
|
154
|
-
/** Pre-computed API base URL, e.g. 'https://bold-river-a1b2c3.sylphx.com/v1' */
|
|
157
|
+
/** Pre-computed API base URL, e.g. 'https://bold-river-a1b2c3.api.sylphx.com/v1' */
|
|
155
158
|
readonly baseUrl: string;
|
|
156
159
|
/** Optional access token for authenticated requests */
|
|
157
160
|
readonly accessToken?: string;
|
|
@@ -204,7 +207,7 @@ interface SylphxClientInput {
|
|
|
204
207
|
* @example Connection URL (recommended)
|
|
205
208
|
* ```typescript
|
|
206
209
|
* const sylphx = createClient(process.env.NEXT_PUBLIC_SYLPHX_URL!)
|
|
207
|
-
* // Parses: sylphx://pk_prod_{hex}@bold-river-a1b2c3.sylphx.com
|
|
210
|
+
* // Parses: sylphx://pk_prod_{hex}@bold-river-a1b2c3.api.sylphx.com
|
|
208
211
|
* ```
|
|
209
212
|
*
|
|
210
213
|
* @example Explicit components
|
|
@@ -225,7 +228,7 @@ declare function createClient(input: string | SylphxClientInput): SylphxConfig;
|
|
|
225
228
|
* @example Connection URL (recommended)
|
|
226
229
|
* ```typescript
|
|
227
230
|
* const sylphx = createServerClient(process.env.SYLPHX_SECRET_URL!)
|
|
228
|
-
* // Parses: sylphx://sk_prod_{hex}@bold-river-a1b2c3.sylphx.com
|
|
231
|
+
* // Parses: sylphx://sk_prod_{hex}@bold-river-a1b2c3.api.sylphx.com
|
|
229
232
|
* ```
|
|
230
233
|
*
|
|
231
234
|
* @example Explicit components
|
|
@@ -2634,6 +2637,28 @@ interface PlatformAccessTokenClaims {
|
|
|
2634
2637
|
readonly org_role?: string;
|
|
2635
2638
|
readonly iat?: number;
|
|
2636
2639
|
readonly exp?: number;
|
|
2640
|
+
/**
|
|
2641
|
+
* RFC 7800 confirmation claim — present when the token is sender-
|
|
2642
|
+
* constrained. Today we emit this for DPoP-bound tokens (RFC 9449)
|
|
2643
|
+
* where `cnf.jkt` is the SHA-256 thumbprint of the client's DPoP
|
|
2644
|
+
* public key.
|
|
2645
|
+
*
|
|
2646
|
+
* Resource servers (e.g. apps/api Management plane) that want to
|
|
2647
|
+
* enforce DPoP MUST:
|
|
2648
|
+
* 1. Look up `oauth_clients.dpop_bound_access_tokens` on the
|
|
2649
|
+
* issuing client to know whether DPoP is required.
|
|
2650
|
+
* 2. If required AND `cnf.jkt` is absent, reject 401.
|
|
2651
|
+
* 3. If `cnf.jkt` is present, verify the inbound `DPoP` header's
|
|
2652
|
+
* proof JWT and assert its public-key thumbprint matches `jkt`.
|
|
2653
|
+
*
|
|
2654
|
+
* Pre-Wave-5.3 this field was stripped from `verifyAccessToken`'s
|
|
2655
|
+
* return value, making resource-side enforcement impossible without
|
|
2656
|
+
* decoding the JWT a second time. Exposing it preserves the wire
|
|
2657
|
+
* format and unlocks the resource-server DPoP middleware.
|
|
2658
|
+
*/
|
|
2659
|
+
readonly cnf?: {
|
|
2660
|
+
readonly jkt?: string;
|
|
2661
|
+
};
|
|
2637
2662
|
}
|
|
2638
2663
|
/**
|
|
2639
2664
|
* `verifyAccessToken` — local JWT verification against cached JWKS.
|
|
@@ -7606,9 +7631,9 @@ declare function captureMessage(config: SylphxConfig, message: string, options?:
|
|
|
7606
7631
|
* ## Usage
|
|
7607
7632
|
*
|
|
7608
7633
|
* ```typescript
|
|
7609
|
-
* import {
|
|
7634
|
+
* import { createServerClient, SandboxClient } from '@sylphx/sdk'
|
|
7610
7635
|
*
|
|
7611
|
-
* const config =
|
|
7636
|
+
* const config = createServerClient(process.env.SYLPHX_URL!)
|
|
7612
7637
|
*
|
|
7613
7638
|
* // Create sandbox (Platform waits for pod ready before returning)
|
|
7614
7639
|
* const sandbox = await SandboxClient.create(config)
|
|
@@ -7955,9 +7980,9 @@ declare class SandboxClient {
|
|
|
7955
7980
|
*
|
|
7956
7981
|
* ### Single worker
|
|
7957
7982
|
* ```typescript
|
|
7958
|
-
* import {
|
|
7983
|
+
* import { createServerClient, RunsClient } from '@sylphx/sdk'
|
|
7959
7984
|
*
|
|
7960
|
-
* const config =
|
|
7985
|
+
* const config = createServerClient(process.env.SYLPHX_URL!)
|
|
7961
7986
|
*
|
|
7962
7987
|
* const run = await RunsClient.create(config, {
|
|
7963
7988
|
* image: 'registry.sylphx.com/sylphx/my-trainer:abc123',
|
|
@@ -8192,7 +8217,7 @@ declare class RunHandle {
|
|
|
8192
8217
|
*
|
|
8193
8218
|
* @example
|
|
8194
8219
|
* ```typescript
|
|
8195
|
-
* const config =
|
|
8220
|
+
* const config = createServerClient(process.env.SYLPHX_URL!)
|
|
8196
8221
|
*
|
|
8197
8222
|
* // Run a worker and wait for completion
|
|
8198
8223
|
* const result = await RunsClient.create(config, { ... }).then(w => w.wait())
|
|
@@ -8336,8 +8361,8 @@ declare const WorkersClient: {
|
|
|
8336
8361
|
*
|
|
8337
8362
|
* ### Cron → Task
|
|
8338
8363
|
* ```typescript
|
|
8339
|
-
* import {
|
|
8340
|
-
* const config =
|
|
8364
|
+
* import { createServerClient, TriggersClient } from '@sylphx/sdk'
|
|
8365
|
+
* const config = createServerClient(process.env.SYLPHX_URL!)
|
|
8341
8366
|
*
|
|
8342
8367
|
* const trigger = await TriggersClient.create(config, {
|
|
8343
8368
|
* name: 'daily-cleanup',
|
package/dist/index.mjs
CHANGED
|
@@ -4878,7 +4878,7 @@ init_constants();
|
|
|
4878
4878
|
init_errors();
|
|
4879
4879
|
var LEGACY_EMBEDDED_REF_PATTERN = /^(pk|sk)_(dev|stg|prod|prev)_[a-z0-9]{12}_[a-f0-9]+$/;
|
|
4880
4880
|
var LEGACY_APP_KEY_PATTERN = /^app_(dev|stg|prod|prev)_/;
|
|
4881
|
-
var MIGRATION_MESSAGE = "API key format has changed. Use a sylphx:// connection URL instead.\n\nNew format: sylphx://pk_prod_{hex}@your-slug.sylphx.com\n\nGenerate new credentials from the Sylphx Console \u2192 Your App \u2192 Environments.\nSee https://docs.sylphx.com/migration for details.";
|
|
4881
|
+
var MIGRATION_MESSAGE = "API key format has changed. Use a sylphx:// connection URL instead.\n\nNew format: sylphx://pk_prod_{hex}@your-slug.api.sylphx.com\n\nGenerate new credentials from the Sylphx Console \u2192 Your App \u2192 Environments.\nSee https://docs.sylphx.com/migration for details.";
|
|
4882
4882
|
function rejectLegacyKeyFormat(input) {
|
|
4883
4883
|
const trimmed = input.trim().toLowerCase();
|
|
4884
4884
|
if (LEGACY_APP_KEY_PATTERN.test(trimmed)) {
|
|
@@ -4921,7 +4921,7 @@ function createServerClient(input) {
|
|
|
4921
4921
|
function createConfigFromUrl(url) {
|
|
4922
4922
|
if (!url || typeof url !== "string") {
|
|
4923
4923
|
throw new SylphxError(
|
|
4924
|
-
"[Sylphx] Connection URL is required. Set SYLPHX_URL or NEXT_PUBLIC_SYLPHX_URL environment variable.\n\nFormat: sylphx://pk_prod_{hex}@your-slug.sylphx.com",
|
|
4924
|
+
"[Sylphx] Connection URL is required. Set SYLPHX_URL or NEXT_PUBLIC_SYLPHX_URL environment variable.\n\nFormat: sylphx://pk_prod_{hex}@your-slug.api.sylphx.com",
|
|
4925
4925
|
{ code: "BAD_REQUEST" }
|
|
4926
4926
|
);
|
|
4927
4927
|
}
|
|
@@ -4930,7 +4930,7 @@ function createConfigFromUrl(url) {
|
|
|
4930
4930
|
if (!trimmed.startsWith("sylphx://")) {
|
|
4931
4931
|
if (CREDENTIAL_REGEX.test(trimmed)) {
|
|
4932
4932
|
throw new SylphxError(
|
|
4933
|
-
"[Sylphx] Received a bare credential instead of a connection URL.\n\nWrap it in a connection URL: sylphx://<credential>@<slug>.sylphx.com\nOr use createClient({ slug, publicKey }) for explicit components.",
|
|
4933
|
+
"[Sylphx] Received a bare credential instead of a connection URL.\n\nWrap it in a connection URL: sylphx://<credential>@<slug>.api.sylphx.com\nOr use createClient({ slug, publicKey }) for explicit components.",
|
|
4934
4934
|
{ code: "BAD_REQUEST" }
|
|
4935
4935
|
);
|
|
4936
4936
|
}
|
|
@@ -6695,6 +6695,8 @@ async function verifyAccessToken(token, opts) {
|
|
|
6695
6695
|
const { payload } = await jwtVerify2(token, jwk, {
|
|
6696
6696
|
audience: opts.audience
|
|
6697
6697
|
});
|
|
6698
|
+
const cnfRaw = payload.cnf;
|
|
6699
|
+
const cnf = cnfRaw && typeof cnfRaw === "object" && "jkt" in cnfRaw ? { jkt: typeof cnfRaw.jkt === "string" ? cnfRaw.jkt : void 0 } : void 0;
|
|
6698
6700
|
return {
|
|
6699
6701
|
sub: payload.sub,
|
|
6700
6702
|
pid: payload.pid,
|
|
@@ -6708,7 +6710,8 @@ async function verifyAccessToken(token, opts) {
|
|
|
6708
6710
|
org_slug: payload.org_slug,
|
|
6709
6711
|
org_role: payload.org_role,
|
|
6710
6712
|
iat: payload.iat,
|
|
6711
|
-
exp: payload.exp
|
|
6713
|
+
exp: payload.exp,
|
|
6714
|
+
...cnf ? { cnf } : {}
|
|
6712
6715
|
};
|
|
6713
6716
|
} catch (err) {
|
|
6714
6717
|
lastError = err;
|