@nifrajs/core 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +2 -2
  2. package/dist/assurance.d.ts +48 -4
  3. package/dist/assurance.d.ts.map +1 -1
  4. package/dist/assurance.js +87 -9
  5. package/dist/assurance.js.map +1 -1
  6. package/dist/binary.d.ts +54 -0
  7. package/dist/binary.d.ts.map +1 -0
  8. package/dist/binary.js +90 -0
  9. package/dist/binary.js.map +1 -0
  10. package/dist/budget.d.ts +1 -1
  11. package/dist/budget.js +1 -1
  12. package/dist/capabilities.d.ts +19 -2
  13. package/dist/capabilities.d.ts.map +1 -1
  14. package/dist/capabilities.js +60 -8
  15. package/dist/capabilities.js.map +1 -1
  16. package/dist/classification.d.ts +1 -1
  17. package/dist/classification.js +1 -1
  18. package/dist/diff.d.ts +6 -6
  19. package/dist/diff.js +6 -6
  20. package/dist/durable-adapters.d.ts.map +1 -1
  21. package/dist/durable-adapters.js +36 -70
  22. package/dist/durable-adapters.js.map +1 -1
  23. package/dist/durable-execution.d.ts +2 -2
  24. package/dist/durable-execution.js +2 -2
  25. package/dist/idempotency.d.ts +3 -3
  26. package/dist/idempotency.js +4 -4
  27. package/dist/index.d.ts +3 -3
  28. package/dist/index.js +3 -3
  29. package/dist/internal/brand.d.ts +1 -1
  30. package/dist/internal/brand.js +1 -1
  31. package/dist/internal/route-catalog.d.ts +58 -0
  32. package/dist/internal/route-catalog.d.ts.map +1 -0
  33. package/dist/internal/route-catalog.js +58 -0
  34. package/dist/internal/route-catalog.js.map +1 -0
  35. package/dist/internal/route-execution.d.ts +75 -0
  36. package/dist/internal/route-execution.d.ts.map +1 -0
  37. package/dist/internal/route-execution.js +2 -0
  38. package/dist/internal/route-execution.js.map +1 -0
  39. package/dist/internal/same-origin.d.ts +29 -0
  40. package/dist/internal/same-origin.d.ts.map +1 -0
  41. package/dist/internal/same-origin.js +47 -0
  42. package/dist/internal/same-origin.js.map +1 -0
  43. package/dist/internal/secret.d.ts +1 -1
  44. package/dist/internal/secret.js +1 -1
  45. package/dist/ledger.d.ts +9 -9
  46. package/dist/ledger.js +5 -5
  47. package/dist/reflection.d.ts +1 -1
  48. package/dist/router/pattern.d.ts +9 -0
  49. package/dist/router/pattern.d.ts.map +1 -1
  50. package/dist/router/pattern.js +13 -4
  51. package/dist/router/pattern.js.map +1 -1
  52. package/dist/router/router.js +3 -3
  53. package/dist/schema/standard.d.ts +2 -2
  54. package/dist/schema/standard.js +2 -2
  55. package/dist/seo.d.ts +3 -3
  56. package/dist/seo.js +4 -4
  57. package/dist/server/body.d.ts +2 -2
  58. package/dist/server/body.js +2 -2
  59. package/dist/server/context.d.ts +18 -18
  60. package/dist/server/contract.d.ts +7 -7
  61. package/dist/server/contract.js +3 -3
  62. package/dist/server/cookies.d.ts +4 -4
  63. package/dist/server/cookies.js +8 -8
  64. package/dist/server/edge.d.ts +83 -0
  65. package/dist/server/edge.d.ts.map +1 -0
  66. package/dist/server/edge.js +71 -0
  67. package/dist/server/edge.js.map +1 -0
  68. package/dist/server/install.d.ts +2 -0
  69. package/dist/server/install.d.ts.map +1 -1
  70. package/dist/server/install.js +2 -0
  71. package/dist/server/install.js.map +1 -1
  72. package/dist/server/logger.d.ts +7 -7
  73. package/dist/server/logger.js +4 -4
  74. package/dist/server/registry.d.ts +5 -5
  75. package/dist/server/response-contract-lane.d.ts +91 -0
  76. package/dist/server/response-contract-lane.d.ts.map +1 -0
  77. package/dist/server/response-contract-lane.js +81 -0
  78. package/dist/server/response-contract-lane.js.map +1 -0
  79. package/dist/server/server.d.ts +52 -91
  80. package/dist/server/server.d.ts.map +1 -1
  81. package/dist/server/server.js +130 -186
  82. package/dist/server/server.js.map +1 -1
  83. package/dist/server/sse.d.ts +4 -4
  84. package/dist/server/sse.js +7 -7
  85. package/dist/server/webhook.d.ts +1 -1
  86. package/dist/server/webhook.js +5 -5
  87. package/dist/server/websocket.d.ts +19 -19
  88. package/dist/server/websocket.js +9 -9
  89. package/dist/server/ws-bun.d.ts +4 -4
  90. package/dist/server/ws-bun.js +3 -3
  91. package/dist/server.d.ts +3 -1
  92. package/dist/server.d.ts.map +1 -1
  93. package/dist/server.js +3 -1
  94. package/dist/server.js.map +1 -1
  95. package/dist/transport-codec.d.ts +12 -0
  96. package/dist/transport-codec.d.ts.map +1 -1
  97. package/dist/transport-codec.js +19 -4
  98. package/dist/transport-codec.js.map +1 -1
  99. package/package.json +12 -2
  100. package/src/assurance.ts +154 -8
  101. package/src/binary.ts +128 -0
  102. package/src/budget.ts +1 -1
  103. package/src/capabilities.ts +71 -9
  104. package/src/classification.ts +1 -1
  105. package/src/diff.ts +8 -8
  106. package/src/durable-adapters.ts +40 -70
  107. package/src/durable-execution.ts +2 -2
  108. package/src/idempotency.ts +4 -4
  109. package/src/index.ts +3 -3
  110. package/src/internal/brand.ts +1 -1
  111. package/src/internal/route-catalog.ts +94 -0
  112. package/src/internal/route-execution.ts +106 -0
  113. package/src/internal/same-origin.ts +43 -0
  114. package/src/internal/secret.ts +1 -1
  115. package/src/ledger.ts +11 -11
  116. package/src/reflection.ts +1 -1
  117. package/src/router/pattern.ts +16 -4
  118. package/src/router/router.ts +5 -5
  119. package/src/schema/standard.ts +2 -2
  120. package/src/seo.ts +6 -6
  121. package/src/server/body.ts +2 -2
  122. package/src/server/context.ts +18 -18
  123. package/src/server/contract.ts +13 -13
  124. package/src/server/cookies.ts +8 -8
  125. package/src/server/edge.ts +150 -0
  126. package/src/server/install.ts +5 -0
  127. package/src/server/logger.ts +8 -8
  128. package/src/server/registry.ts +5 -5
  129. package/src/server/response-contract-lane.ts +164 -0
  130. package/src/server/server.ts +159 -366
  131. package/src/server/sse.ts +11 -11
  132. package/src/server/webhook.ts +5 -5
  133. package/src/server/websocket.ts +22 -22
  134. package/src/server/ws-bun.ts +4 -4
  135. package/src/server.ts +8 -5
  136. package/src/transport-codec.ts +24 -5
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Cookie primitives — parse a request `Cookie` header, serialize a `Set-Cookie`, and sign/verify a
2
+ * Cookie primitives - parse a request `Cookie` header, serialize a `Set-Cookie`, and sign/verify a
3
3
  * value with HMAC-SHA256 via **WebCrypto** (`crypto.subtle`), so they're portable across Bun, Node,
4
4
  * Deno, and workerd with no `node:crypto` dependency. Pure + runtime-agnostic: `c.cookies` (read) and
5
5
  * `c.set.cookie` (write) build on these, and `@nifrajs/auth` builds sessions on top.
@@ -7,8 +7,8 @@
7
7
  import { requireSecretBytes } from "../internal/secret.js";
8
8
  // RFC 6265 cookie-name token: visible ASCII minus separators/whitespace/controls.
9
9
  const COOKIE_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
10
- const MAX_COOKIE_BYTES = 4096; // browsers cap a cookie near 4 KB — reject oversized before the wire
11
- /** True if `v` contains a character illegal in a cookie attribute (control char or `;` separator) —
10
+ const MAX_COOKIE_BYTES = 4096; // browsers cap a cookie near 4 KB - reject oversized before the wire
11
+ /** True if `v` contains a character illegal in a cookie attribute (control char or `;` separator) -
12
12
  * the header-injection guard for dev-supplied `Path`/`Domain`. */
13
13
  const hasIllegalChar = (v) => {
14
14
  for (let i = 0; i < v.length; i++) {
@@ -19,7 +19,7 @@ const hasIllegalChar = (v) => {
19
19
  return false;
20
20
  };
21
21
  const safeDecode = (v) => {
22
- // No `%` ⇒ nothing to decode — skip the call AND its try frame (profiled: cookie values are
22
+ // No `%` ⇒ nothing to decode - skip the call AND its try frame (profiled: cookie values are
23
23
  // overwhelmingly plain; this was ~3% of a realistic request).
24
24
  if (!v.includes("%"))
25
25
  return v;
@@ -31,7 +31,7 @@ const safeDecode = (v) => {
31
31
  }
32
32
  };
33
33
  /** Parse a request `Cookie` header into a name→value map (values URL-decoded). Unparseable pairs are
34
- * skipped rather than throwing — a junk `Cookie` header shouldn't fail the request.
34
+ * skipped rather than throwing - a junk `Cookie` header shouldn't fail the request.
35
35
  *
36
36
  * Hand-rolled index walk instead of `split(";")` + `trim()` chains: those allocated an array + up
37
37
  * to three substrings per pair and showed up at ~6% of a realistic (auth + cookie) request. Same
@@ -86,9 +86,9 @@ export function parseCookies(header) {
86
86
  return out;
87
87
  }
88
88
  /**
89
- * Serialize a `Set-Cookie` header value. Pure — applies **no** security defaults (the caller, e.g.
89
+ * Serialize a `Set-Cookie` header value. Pure - applies **no** security defaults (the caller, e.g.
90
90
  * `c.set.cookie`, layers `HttpOnly`/`Secure`/`SameSite` on). Throws on an invalid cookie name, a
91
- * header-injecting `Path`/`Domain`, a non-integer `maxAge`, or an oversized result — a serialization
91
+ * header-injecting `Path`/`Domain`, a non-integer `maxAge`, or an oversized result - a serialization
92
92
  * bug should fail loudly, not silently emit a cookie the browser drops.
93
93
  */
94
94
  export function serializeCookie(name, value, options = {}) {
@@ -152,7 +152,7 @@ const fromBase64Url = (s) => {
152
152
  return bytes;
153
153
  }
154
154
  catch {
155
- return null; // not valid base64 — treat as a bad signature
155
+ return null; // not valid base64 - treat as a bad signature
156
156
  }
157
157
  };
158
158
  /** Append an HMAC-SHA256 signature to a value → `value.signature` (base64url). For signed cookies. */
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The edge adapter: a nifra app presented as a Cloudflare-Workers-shaped `ExportedHandler`.
3
+ *
4
+ * ## Why this is not in the server kernel
5
+ *
6
+ * `toFetchHandler` never touches the `Server` class. It takes a STRUCTURAL app - anything with `fetch`, and
7
+ * optionally `resolveWebSocketUpgrade` - which is what lets it wrap an app it has no access to the
8
+ * internals of, and what makes it a separate concern from the kernel rather than a method on it. It sat
9
+ * in `server.ts` for no reason other than that both were written the same week.
10
+ *
11
+ * The types here are all structural too (`ExecutionContext`, `ScheduledController`,
12
+ * `DurableObjectNamespaceLike`), so `@nifrajs/core` declares the Workers surface it needs instead of
13
+ * depending on `@cloudflare/workers-types`. The real Workers types satisfy them.
14
+ *
15
+ * The `MaybePromise` import is type-only and erased, so there is no runtime cycle back into the kernel.
16
+ */
17
+ import type { Platform } from "./context.js";
18
+ import type { MaybePromise } from "./server.js";
19
+ import type { WebSocketUpgradeOutcome } from "./websocket.js";
20
+ /** A Cloudflare Workers-style execution context (the `fetch` 3rd arg). Structural - only
21
+ * `waitUntil` is used; declared here so `@nifrajs/core` needs no Workers type dependency. */
22
+ export interface ExecutionContext {
23
+ waitUntil(promise: Promise<unknown>): void;
24
+ }
25
+ /** A Cloudflare Workers-style scheduled (cron) controller. Structural - no Workers type dependency. */
26
+ export interface ScheduledController {
27
+ /** Epoch ms the run was scheduled for. */
28
+ readonly scheduledTime: number;
29
+ /** The matching cron expression from `wrangler.toml` `[triggers]`. */
30
+ readonly cron: string;
31
+ /** Tell the platform not to retry this run on failure. */
32
+ noRetry(): void;
33
+ }
34
+ /** A nifra cron handler: the platform controller + the same typed `env`/`waitUntil` nifra threads into
35
+ * request handlers. Schedule background work with `waitUntil` so it outlives the trigger. */
36
+ export type ScheduledHandler<Env = unknown> = (controller: ScheduledController, context: {
37
+ readonly env: Env;
38
+ waitUntil(promise: Promise<unknown>): void;
39
+ }) => MaybePromise<void>;
40
+ /** Structural view of a Cloudflare Durable Object namespace binding - keeps `@cloudflare/workers-types`
41
+ * out of `@nifrajs/core`. The real `DurableObjectNamespace` satisfies it. */
42
+ export interface DurableObjectNamespaceLike {
43
+ idFromName(name: string): unknown;
44
+ get(id: unknown): {
45
+ fetch(request: Request): Promise<Response>;
46
+ };
47
+ }
48
+ /**
49
+ * Adapt a nifra app to an edge "ExportedHandler" - use it as a Cloudflare Workers (or any
50
+ * `fetch(request, env, ctx)` runtime) default export. It threads `env` + `ctx.waitUntil` into the
51
+ * nifra Context, so handlers read `c.env` and schedule background work via `c.waitUntil`:
52
+ *
53
+ * export default toFetchHandler(app)
54
+ *
55
+ * Pass `{ scheduled }` to also export a Workers cron handler (for a `[triggers]` schedule) - it
56
+ * receives the platform controller plus the same typed `env`/`waitUntil`:
57
+ *
58
+ * export default toFetchHandler(app, {
59
+ * scheduled: (controller, { env, waitUntil }) =>
60
+ * waitUntil(env.KV.put("last-run", String(controller.scheduledTime))),
61
+ * })
62
+ *
63
+ * No Workers-only deps - `app.fetch` stays a portable Web-standard handler; this is the thin
64
+ * shim from the platform's 3-arg `fetch`/`scheduled` to it.
65
+ */
66
+ export declare function toFetchHandler<Env = unknown>(app: {
67
+ fetch(request: Request, platform?: Platform<Env>): MaybePromise<Response>;
68
+ resolveWebSocketUpgrade?(request: Request, platform?: Platform<Env>): MaybePromise<WebSocketUpgradeOutcome>;
69
+ }, options?: {
70
+ scheduled?: ScheduledHandler<Env>;
71
+ /**
72
+ * Route WebSocket upgrades to a Durable Object that holds the connections and runs the app's
73
+ * pub/sub - enabling cross-connection broadcast (`app.publish`) on Cloudflare Workers, where a
74
+ * stateless isolate can't. Pass the DO namespace binding from `env`; pair with `@nifrajs/workers`'
75
+ * `createWebSocketHub(app)` (the DO class) + a `wrangler.toml` binding. Without this, WS upgrades use
76
+ * a per-connection `WebSocketPair` (no broadcast).
77
+ */
78
+ webSocketHub?: (env: Env) => DurableObjectNamespaceLike;
79
+ }): {
80
+ fetch(request: Request, env: Env, ctx: ExecutionContext): MaybePromise<Response>;
81
+ scheduled?(controller: ScheduledController, env: Env, ctx: ExecutionContext): MaybePromise<void>;
82
+ };
83
+ //# sourceMappingURL=edge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edge.d.ts","sourceRoot":"","sources":["../../src/server/edge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,KAAK,EAAqB,uBAAuB,EAAE,MAAM,gBAAgB,CAAA;AAEhF;6FAC6F;AAC7F,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAA;CAC3C;AAED,uGAAuG;AACvG,MAAM,WAAW,mBAAmB;IAClC,0CAA0C;IAC1C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,0DAA0D;IAC1D,OAAO,IAAI,IAAI,CAAA;CAChB;AAED;6FAC6F;AAC7F,MAAM,MAAM,gBAAgB,CAAC,GAAG,GAAG,OAAO,IAAI,CAC5C,UAAU,EAAE,mBAAmB,EAC/B,OAAO,EAAE;IAAE,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC;IAAC,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAA;CAAE,KACvE,YAAY,CAAC,IAAI,CAAC,CAAA;AAQvB;6EAC6E;AAC7E,MAAM,WAAW,0BAA0B;IACzC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;IACjC,GAAG,CAAC,EAAE,EAAE,OAAO,GAAG;QAAE,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;KAAE,CAAA;CACjE;AAKD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAAC,GAAG,GAAG,OAAO,EAC1C,GAAG,EAAE;IACH,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC,GAAG,CAAC,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAA;IACzE,uBAAuB,CAAC,CACtB,OAAO,EAAE,OAAO,EAChB,QAAQ,CAAC,EAAE,QAAQ,CAAC,GAAG,CAAC,GACvB,YAAY,CAAC,uBAAuB,CAAC,CAAA;CACzC,EACD,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,gBAAgB,CAAC,GAAG,CAAC,CAAA;IACjC;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,0BAA0B,CAAA;CACxD,GACA;IACD,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,gBAAgB,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAA;IAChF,SAAS,CAAC,CAAC,UAAU,EAAE,mBAAmB,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,gBAAgB,GAAG,YAAY,CAAC,IAAI,CAAC,CAAA;CACjG,CAkDA"}
@@ -0,0 +1,71 @@
1
+ /** The single hub Durable Object id nifra routes WS upgrades to (one hub per app - see `@nifrajs/workers`). */
2
+ const NIFRA_WS_HUB_ID = "nifra-ws-hub";
3
+ /**
4
+ * Adapt a nifra app to an edge "ExportedHandler" - use it as a Cloudflare Workers (or any
5
+ * `fetch(request, env, ctx)` runtime) default export. It threads `env` + `ctx.waitUntil` into the
6
+ * nifra Context, so handlers read `c.env` and schedule background work via `c.waitUntil`:
7
+ *
8
+ * export default toFetchHandler(app)
9
+ *
10
+ * Pass `{ scheduled }` to also export a Workers cron handler (for a `[triggers]` schedule) - it
11
+ * receives the platform controller plus the same typed `env`/`waitUntil`:
12
+ *
13
+ * export default toFetchHandler(app, {
14
+ * scheduled: (controller, { env, waitUntil }) =>
15
+ * waitUntil(env.KV.put("last-run", String(controller.scheduledTime))),
16
+ * })
17
+ *
18
+ * No Workers-only deps - `app.fetch` stays a portable Web-standard handler; this is the thin
19
+ * shim from the platform's 3-arg `fetch`/`scheduled` to it.
20
+ */
21
+ export function toFetchHandler(app, options) {
22
+ const scheduled = options?.scheduled;
23
+ const webSocketHub = options?.webSocketHub;
24
+ const resolveWs = app.resolveWebSocketUpgrade?.bind(app);
25
+ return {
26
+ fetch: (request, env, ctx) => {
27
+ const platform = { env, waitUntil: (promise) => ctx.waitUntil(promise) };
28
+ // WebSocket broadcast on Workers: route upgrades to the hub Durable Object (it holds the
29
+ // connections + runs the app's pub/sub, so `app.publish` reaches every client). The hub itself
30
+ // resolves the route and rejects non-WS paths. See `@nifrajs/workers`' `createWebSocketHub`.
31
+ if (webSocketHub !== undefined &&
32
+ request.headers.get("upgrade")?.toLowerCase() === "websocket") {
33
+ const ns = webSocketHub(env);
34
+ return ns.get(ns.idFromName(NIFRA_WS_HUB_ID)).fetch(request);
35
+ }
36
+ // Workers WebSockets: a WS upgrade for a registered route becomes a `WebSocketPair` + a 101.
37
+ // Feature-detected, so non-Workers edge runtimes (which lack `WebSocketPair` - e.g. Deno Deploy
38
+ // uses `@nifrajs/deno`'s `Deno.upgradeWebSocket`) simply fall through to the normal `fetch`.
39
+ const Pair = globalThis.WebSocketPair;
40
+ if (resolveWs !== undefined && Pair !== undefined) {
41
+ const accept = (outcome) => {
42
+ if (outcome.kind === "pass")
43
+ return app.fetch(request, platform);
44
+ if (outcome.kind === "reject")
45
+ return outcome.response;
46
+ const pair = new Pair();
47
+ const server = pair[1];
48
+ server.accept();
49
+ // The outcome carries the installed runtime's `attach`, so `toFetchHandler` (a standalone
50
+ // function, no access to the app's private runtime) wires the socket without a static WS import.
51
+ outcome.attach(server, outcome.handler, outcome.data, {
52
+ openNow: true,
53
+ pubsub: outcome.pubsub,
54
+ });
55
+ // `webSocket` is a Workers-only `ResponseInit` field (absent from the standard type), and a
56
+ // 101 status is only valid on the Workers runtime - both gated by the `Pair` feature check.
57
+ return new Response(null, { status: 101, webSocket: pair[0] });
58
+ };
59
+ const out = resolveWs(request, platform);
60
+ return out instanceof Promise ? out.then(accept) : accept(out);
61
+ }
62
+ return app.fetch(request, platform);
63
+ },
64
+ ...(scheduled !== undefined
65
+ ? {
66
+ scheduled: (controller, env, ctx) => scheduled(controller, { env, waitUntil: (promise) => ctx.waitUntil(promise) }),
67
+ }
68
+ : {}),
69
+ };
70
+ }
71
+ //# sourceMappingURL=edge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edge.js","sourceRoot":"","sources":["../../src/server/edge.ts"],"names":[],"mappings":"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"}
@@ -18,4 +18,6 @@ export declare const INSTALL_SSE: unique symbol;
18
18
  export declare const INSTALL_NODE_DIRECT: unique symbol;
19
19
  /** @internal Install the WebSocket runtime on a server (called by the `websocket()` plugin). */
20
20
  export declare const INSTALL_WS: unique symbol;
21
+ /** @internal Install the response-contract runtime on a server (called by the `responseContract()` plugin). */
22
+ export declare const INSTALL_RESPONSE_CONTRACT: unique symbol;
21
23
  //# sourceMappingURL=install.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/server/install.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,oGAAoG;AACpG,eAAO,MAAM,mBAAmB,EAAE,OAAO,MAAwD,CAAA;AAEjG,uGAAuG;AACvG,eAAO,MAAM,qBAAqB,EAAE,OAAO,MAE1C,CAAA;AAED,oFAAoF;AACpF,eAAO,MAAM,WAAW,EAAE,OAAO,MAAgD,CAAA;AAEjF,8FAA8F;AAC9F,eAAO,MAAM,WAAW,EAAE,OAAO,MAAgD,CAAA;AAEjF,oGAAoG;AACpG,eAAO,MAAM,mBAAmB,EAAE,OAAO,MAAwD,CAAA;AAEjG,gGAAgG;AAChG,eAAO,MAAM,UAAU,EAAE,OAAO,MAA+C,CAAA"}
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/server/install.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,oGAAoG;AACpG,eAAO,MAAM,mBAAmB,EAAE,OAAO,MAAwD,CAAA;AAEjG,uGAAuG;AACvG,eAAO,MAAM,qBAAqB,EAAE,OAAO,MAE1C,CAAA;AAED,oFAAoF;AACpF,eAAO,MAAM,WAAW,EAAE,OAAO,MAAgD,CAAA;AAEjF,8FAA8F;AAC9F,eAAO,MAAM,WAAW,EAAE,OAAO,MAAgD,CAAA;AAEjF,oGAAoG;AACpG,eAAO,MAAM,mBAAmB,EAAE,OAAO,MAAwD,CAAA;AAEjG,gGAAgG;AAChG,eAAO,MAAM,UAAU,EAAE,OAAO,MAA+C,CAAA;AAE/E,+GAA+G;AAC/G,eAAO,MAAM,yBAAyB,EAAE,OAAO,MAE9C,CAAA"}
@@ -18,4 +18,6 @@ export const INSTALL_SSE = Symbol.for("@nifrajs/core/install-sse");
18
18
  export const INSTALL_NODE_DIRECT = Symbol.for("@nifrajs/core/install-node-direct");
19
19
  /** @internal Install the WebSocket runtime on a server (called by the `websocket()` plugin). */
20
20
  export const INSTALL_WS = Symbol.for("@nifrajs/core/install-ws");
21
+ /** @internal Install the response-contract runtime on a server (called by the `responseContract()` plugin). */
22
+ export const INSTALL_RESPONSE_CONTRACT = Symbol.for("@nifrajs/core/install-response-contract");
21
23
  //# sourceMappingURL=install.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"install.js","sourceRoot":"","sources":["../../src/server/install.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,oGAAoG;AACpG,MAAM,CAAC,MAAM,mBAAmB,GAAkB,MAAM,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAA;AAEjG,uGAAuG;AACvG,MAAM,CAAC,MAAM,qBAAqB,GAAkB,MAAM,CAAC,GAAG,CAC5D,qCAAqC,CACtC,CAAA;AAED,oFAAoF;AACpF,MAAM,CAAC,MAAM,WAAW,GAAkB,MAAM,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAA;AAEjF,8FAA8F;AAC9F,MAAM,CAAC,MAAM,WAAW,GAAkB,MAAM,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAA;AAEjF,oGAAoG;AACpG,MAAM,CAAC,MAAM,mBAAmB,GAAkB,MAAM,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAA;AAEjG,gGAAgG;AAChG,MAAM,CAAC,MAAM,UAAU,GAAkB,MAAM,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAA"}
1
+ {"version":3,"file":"install.js","sourceRoot":"","sources":["../../src/server/install.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,oGAAoG;AACpG,MAAM,CAAC,MAAM,mBAAmB,GAAkB,MAAM,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAA;AAEjG,uGAAuG;AACvG,MAAM,CAAC,MAAM,qBAAqB,GAAkB,MAAM,CAAC,GAAG,CAC5D,qCAAqC,CACtC,CAAA;AAED,oFAAoF;AACpF,MAAM,CAAC,MAAM,WAAW,GAAkB,MAAM,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAA;AAEjF,8FAA8F;AAC9F,MAAM,CAAC,MAAM,WAAW,GAAkB,MAAM,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAA;AAEjF,oGAAoG;AACpG,MAAM,CAAC,MAAM,mBAAmB,GAAkB,MAAM,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAA;AAEjG,gGAAgG;AAChG,MAAM,CAAC,MAAM,UAAU,GAAkB,MAAM,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAA;AAE/E,+GAA+G;AAC/G,MAAM,CAAC,MAAM,yBAAyB,GAAkB,MAAM,CAAC,GAAG,CAChE,yCAAyC,CAC1C,CAAA"}
@@ -12,12 +12,12 @@ export interface Logger {
12
12
  }
13
13
  /**
14
14
  * Tunes redaction. Key-name redaction always runs; the rest is **opt-in**:
15
- * - `keyParts` — extra case-insensitive key fragments, added to the built-in denylist.
16
- * - `valuePatterns` — regexes matched against string **values** *and* the log message; each match is
15
+ * - `keyParts` - extra case-insensitive key fragments, added to the built-in denylist.
16
+ * - `valuePatterns` - regexes matched against string **values** *and* the log message; each match is
17
17
  * replaced with the placeholder. This is the value-scanning hook for secrets that land in a value or
18
- * message (e.g. `err.message`), which key-name redaction can't catch. Off unless provided — the
18
+ * message (e.g. `err.message`), which key-name redaction can't catch. Off unless provided - the
19
19
  * default path does no value scanning, so it stays allocation-light. See {@link commonSecretPatterns}.
20
- * - `placeholder` — the replacement string (default `[REDACTED]`).
20
+ * - `placeholder` - the replacement string (default `[REDACTED]`).
21
21
  */
22
22
  export interface RedactOptions {
23
23
  readonly keyParts?: readonly string[];
@@ -25,7 +25,7 @@ export interface RedactOptions {
25
25
  readonly placeholder?: string;
26
26
  }
27
27
  /**
28
- * A conservative, high-signal set of patterns for {@link RedactOptions.valuePatterns} — opt in by
28
+ * A conservative, high-signal set of patterns for {@link RedactOptions.valuePatterns} - opt in by
29
29
  * passing it (or a subset) to `jsonLogger`/`redactLogFields`. Covers bearer tokens, JWTs, emails, and a
30
30
  * few well-known key formats (Stripe, GitHub, AWS access-key ids). Chosen to minimize false positives;
31
31
  * add your own (e.g. internal id formats) as needed. Every pattern is global so all matches are scrubbed.
@@ -39,11 +39,11 @@ export declare const commonSecretPatterns: ReadonlyArray<RegExp>;
39
39
  export declare function redactLogFields(fields: LogFields, options?: RedactOptions): LogFields;
40
40
  /**
41
41
  * The default logger: one redacted JSON object per line. `write` is injectable for tests or
42
- * alternative sinks (defaults to stderr). `options` tunes redaction — pass `valuePatterns` (e.g.
42
+ * alternative sinks (defaults to stderr). `options` tunes redaction - pass `valuePatterns` (e.g.
43
43
  * {@link commonSecretPatterns}) to also scrub secrets embedded in values + the message. Framework keys
44
44
  * (`level`, `message`, `time`) always win over user fields of the same name.
45
45
  */
46
46
  export declare function jsonLogger(write?: (line: string) => void, options?: RedactOptions): Logger;
47
- /** Discards everything — for tests, or when log output is handled elsewhere. */
47
+ /** Discards everything - for tests, or when log output is handled elsewhere. */
48
48
  export declare const silentLogger: Logger;
49
49
  //# sourceMappingURL=logger.d.ts.map
@@ -22,7 +22,7 @@ const SENSITIVE_KEY_PARTS = [
22
22
  "ssn",
23
23
  ];
24
24
  /**
25
- * A conservative, high-signal set of patterns for {@link RedactOptions.valuePatterns} — opt in by
25
+ * A conservative, high-signal set of patterns for {@link RedactOptions.valuePatterns} - opt in by
26
26
  * passing it (or a subset) to `jsonLogger`/`redactLogFields`. Covers bearer tokens, JWTs, emails, and a
27
27
  * few well-known key formats (Stripe, GitHub, AWS access-key ids). Chosen to minimize false positives;
28
28
  * add your own (e.g. internal id formats) as needed. Every pattern is global so all matches are scrubbed.
@@ -72,7 +72,7 @@ function redactValue(value, seen, config) {
72
72
  return value.map((item) => redactValue(item, seen, config));
73
73
  const out = {};
74
74
  for (const [key, val] of Object.entries(value)) {
75
- // A sensitive key wins outright (don't scan it) — the whole value is replaced.
75
+ // A sensitive key wins outright (don't scan it) - the whole value is replaced.
76
76
  out[key] = isSensitiveKey(key, config.keyParts)
77
77
  ? config.placeholder
78
78
  : redactValue(val, seen, config);
@@ -92,7 +92,7 @@ function writeToStderr(line) {
92
92
  }
93
93
  /**
94
94
  * The default logger: one redacted JSON object per line. `write` is injectable for tests or
95
- * alternative sinks (defaults to stderr). `options` tunes redaction — pass `valuePatterns` (e.g.
95
+ * alternative sinks (defaults to stderr). `options` tunes redaction - pass `valuePatterns` (e.g.
96
96
  * {@link commonSecretPatterns}) to also scrub secrets embedded in values + the message. Framework keys
97
97
  * (`level`, `message`, `time`) always win over user fields of the same name.
98
98
  */
@@ -115,7 +115,7 @@ export function jsonLogger(write = writeToStderr, options) {
115
115
  };
116
116
  }
117
117
  const noop = () => undefined;
118
- /** Discards everything — for tests, or when log output is handled elsewhere. */
118
+ /** Discards everything - for tests, or when log output is handled elsewhere. */
119
119
  export const silentLogger = {
120
120
  debug: noop,
121
121
  info: noop,
@@ -83,20 +83,20 @@ export type WsRouteInfoFor<Path extends string, In extends StandardSchemaV1 | un
83
83
  /**
84
84
  * Merge a new route into the registry, combining methods that share a path.
85
85
  *
86
- * SCALING CEILING (measured — see many-routes.test-d.ts and the isolation study below): a single
87
- * fluent chain hits TS2589 at ~95-100 routes. This intersection is NOT the cause — in isolation it
86
+ * SCALING CEILING (measured - see many-routes.test-d.ts and the isolation study below): a single
87
+ * fluent chain hits TS2589 at ~95-100 routes. This intersection is NOT the cause - in isolation it
88
88
  * accumulates 1000+ routes cleanly, and heavy per-route `Params<Path>`/schema inference alone
89
89
  * reaches 600+. The wall is an INTERACTION unique to the fluent builder: each `.get(path, handler)`
90
90
  * both (a) computes the handler's context type from the path (`c.params.id` inferred from `:id`)
91
91
  * AND (b) returns `Server<AddRoute<growing-R, …>, Ctx>`. Neither alone strains the compiler; the
92
- * PRODUCT — recomputing the handler context while re-threading the ever-larger registry at each of
93
- * N steps — exhausts TypeScript's per-expression instantiation budget around N≈95. It is therefore
92
+ * PRODUCT - recomputing the handler context while re-threading the ever-larger registry at each of
93
+ * N steps - exhausts TypeScript's per-expression instantiation budget around N≈95. It is therefore
94
94
  * O(N) and inherent to any builder that infers handler context AND accumulates a typed route
95
95
  * registry (Elysia/tRPC/hono's typed clients cap the same way); it is not fixable by reshaping
96
96
  * AddRoute. Past ~90 routes, use a path that DOESN'T form the product: split into domain groups and
97
97
  * `.merge()` them (each group is a short chain; a merge is one `R & R2` intersection with no
98
98
  * per-call context work), or contract-first `implement()` (the registry is one object type declared
99
- * upfront — no grow-R-per-call, so no ceiling at all).
99
+ * upfront - no grow-R-per-call, so no ceiling at all).
100
100
  */
101
101
  export type AddRoute<R extends Registry, Method extends string, Path extends string, Info extends RouteInfo> = R & {
102
102
  [P in Path]: {
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Making a route's declared `response` schema mean something at runtime.
3
+ *
4
+ * ## The gap this closes
5
+ *
6
+ * A `response` schema is a LOWER bound, not an upper one. It says "at least these fields, of these
7
+ * types"; it never said "only these". TypeScript does not close the gap either - excess-property
8
+ * checking fires on a fresh object literal in an annotated position, and a handler is neither: it is a
9
+ * contextually-typed argument that usually returns a variable. So this compiles, and ships every field:
10
+ *
11
+ * app.get("/me", { response: PublicUser }, async () => {
12
+ * const user = await db.users.find(id) // { id, name, email, passwordHash, ... }
13
+ * return user // all of it goes on the wire
14
+ * })
15
+ *
16
+ * The client's type says `{ id, name }`, so the leak is invisible from both ends. And it can appear
17
+ * with no code change at all: add a column to `users`, and the next deploy ships it to browsers.
18
+ *
19
+ * ## Why the behaviour follows the schema, not this module
20
+ *
21
+ * Standard Schema exposes exactly one operation - `validate` - and no way to enumerate a schema's
22
+ * declared keys. So a projection of "just the declared fields" cannot be written generically here;
23
+ * the only clean value available is whatever `validate()` returns. That is deliberate on the spec's
24
+ * part, and it means enforcement inherits each validator's own semantics:
25
+ *
26
+ * - a STRIPPING schema (zod, valibot) returns a cleaned value -> the undeclared fields are removed
27
+ * - a STRICT schema (`@nifrajs/schema`'s `t.object`) reports issues -> the response is a violation
28
+ *
29
+ * That is not an inconsistency to paper over: a strict schema has already declared that extra fields
30
+ * are an error, and a stripping one has declared they are ignorable. Enforcement honours what the
31
+ * author wrote rather than overriding it.
32
+ */
33
+ import type { StandardSchemaV1 } from "../schema/standard.js";
34
+ import type { IdentityPlugin } from "./plugin.js";
35
+ /**
36
+ * How hard a declared `response` schema is held.
37
+ *
38
+ * - `warn` - check and log; the response is sent UNCHANGED, so enabling it cannot break anything.
39
+ * - `enforce` - serialize the validated value, so undeclared data cannot reach the wire.
40
+ *
41
+ * There is no `off`: not installing the plugin IS off, and that is what keeps this module out of an
42
+ * app's bundle entirely rather than shipping a disabled branch to everyone.
43
+ */
44
+ export type ResponseContractMode = "warn" | "enforce";
45
+ /** The outcome of checking one handler result against its declared response schema. */
46
+ export type ResponseContractOutcome =
47
+ /** Nothing to report. `value` is what should be serialized (the original result, or the cleaned one). */
48
+ {
49
+ readonly kind: "ok";
50
+ readonly value: unknown;
51
+ }
52
+ /** `warn` only: the payload differed from the contract. The ORIGINAL result is still served. */
53
+ | {
54
+ readonly kind: "warn";
55
+ readonly value: unknown;
56
+ readonly message: string;
57
+ }
58
+ /** `enforce` only: the payload cannot be reconciled with the contract, so it must not be sent. */
59
+ | {
60
+ readonly kind: "violation";
61
+ readonly message: string;
62
+ };
63
+ /**
64
+ * Check one result against the route's declared response schema.
65
+ *
66
+ * Returns synchronously when the schema does (the common case), so an app that opts in does not pay a
67
+ * microtask per response for a check that had no work to do.
68
+ */
69
+ export declare function checkResponseContract(schema: StandardSchemaV1, result: unknown, mode: "warn" | "enforce"): ResponseContractOutcome | Promise<ResponseContractOutcome>;
70
+ /**
71
+ * What the server holds when the plugin is installed. The kernel calls `check` through this object and
72
+ * never imports the implementation, so an app that does not install the plugin does not carry it.
73
+ */
74
+ export interface ResponseContractRuntime {
75
+ readonly mode: ResponseContractMode;
76
+ check(schema: StandardSchemaV1, result: unknown): ResponseContractOutcome | Promise<ResponseContractOutcome>;
77
+ }
78
+ /**
79
+ * Hold every route's declared `response` schema to what the handler actually returned.
80
+ *
81
+ * app.use(responseContract("enforce"))
82
+ *
83
+ * Install it before the routes it should cover - like `idempotency()`, the decision is made per route
84
+ * at registration, so routes registered earlier are not retroactively covered.
85
+ *
86
+ * A contracted route leaves the fused and native fast paths, because the check needs the handler's
87
+ * value before it becomes bytes. That is the same trade an idempotent route makes, and the reason this
88
+ * is opt-in rather than always on.
89
+ */
90
+ export declare function responseContract(mode?: ResponseContractMode): IdentityPlugin;
91
+ //# sourceMappingURL=response-contract-lane.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response-contract-lane.d.ts","sourceRoot":"","sources":["../../src/server/response-contract-lane.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,KAAK,EAAiC,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAE5F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAGjD;;;;;;;;GAQG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,SAAS,CAAA;AAErD,uFAAuF;AACvF,MAAM,MAAM,uBAAuB;AACjC,yGAAyG;AACvG;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CAAE;AAClD,gGAAgG;GAC9F;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AAC9E,kGAAkG;GAChG;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AA0B5D;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,OAAO,EACf,IAAI,EAAE,MAAM,GAAG,SAAS,GACvB,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAQ5D;AA0BD;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAA;IACnC,KAAK,CACH,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,OAAO,GACd,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAA;CAC9D;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,GAAE,oBAA6B,GAAG,cAAc,CAUpF"}
@@ -0,0 +1,81 @@
1
+ import { INSTALL_RESPONSE_CONTRACT } from "./install.js";
2
+ const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
3
+ /** Top-level keys present in the handler's result but absent from the validated value. */
4
+ function droppedKeys(result, value) {
5
+ if (!isPlainObject(result) || !isPlainObject(value))
6
+ return [];
7
+ return Object.keys(result).filter((k) => !(k in value));
8
+ }
9
+ function describeIssues(issues) {
10
+ return issues
11
+ .map((issue) => {
12
+ const path = Array.isArray(issue.path)
13
+ ? issue.path
14
+ .map((seg) => String(typeof seg === "object" && seg !== null ? seg.key : seg))
15
+ .join(".")
16
+ : "";
17
+ return path === "" ? issue.message : `${path}: ${issue.message}`;
18
+ })
19
+ .join("; ");
20
+ }
21
+ /**
22
+ * Check one result against the route's declared response schema.
23
+ *
24
+ * Returns synchronously when the schema does (the common case), so an app that opts in does not pay a
25
+ * microtask per response for a check that had no work to do.
26
+ */
27
+ export function checkResponseContract(schema, result, mode) {
28
+ // A handler may return a raw Response as deliberate control flow (a redirect, a stream). There is no
29
+ // JSON payload to hold to the contract, and re-serializing one would corrupt it.
30
+ if (result instanceof Response || result === undefined)
31
+ return { kind: "ok", value: result };
32
+ const settled = schema["~standard"].validate(result);
33
+ return settled instanceof Promise
34
+ ? settled.then((r) => interpret(r, result, mode))
35
+ : interpret(settled, result, mode);
36
+ }
37
+ function interpret(settled, result, mode) {
38
+ if (settled.issues !== undefined) {
39
+ const message = `response does not satisfy its declared contract: ${describeIssues(settled.issues)}`;
40
+ // `warn` never changes what is served - it reports and gets out of the way, so switching it on can
41
+ // never be the thing that broke production.
42
+ return mode === "warn"
43
+ ? { kind: "warn", value: result, message }
44
+ : { kind: "violation", message };
45
+ }
46
+ const dropped = droppedKeys(result, settled.value);
47
+ if (mode === "enforce")
48
+ return { kind: "ok", value: settled.value };
49
+ return dropped.length === 0
50
+ ? { kind: "ok", value: result }
51
+ : {
52
+ kind: "warn",
53
+ value: result,
54
+ message: `response carries fields its contract does not declare: ${dropped.join(", ")}. They are being sent; \`responseContract: "enforce"\` would strip them.`,
55
+ };
56
+ }
57
+ /**
58
+ * Hold every route's declared `response` schema to what the handler actually returned.
59
+ *
60
+ * app.use(responseContract("enforce"))
61
+ *
62
+ * Install it before the routes it should cover - like `idempotency()`, the decision is made per route
63
+ * at registration, so routes registered earlier are not retroactively covered.
64
+ *
65
+ * A contracted route leaves the fused and native fast paths, because the check needs the handler's
66
+ * value before it becomes bytes. That is the same trade an idempotent route makes, and the reason this
67
+ * is opt-in rather than always on.
68
+ */
69
+ export function responseContract(mode = "warn") {
70
+ const runtime = {
71
+ mode,
72
+ check: (schema, result) => checkResponseContract(schema, result, mode),
73
+ };
74
+ const apply = (app) => {
75
+ ;
76
+ app[INSTALL_RESPONSE_CONTRACT](runtime);
77
+ return app;
78
+ };
79
+ return Object.assign(apply, { pluginName: "nifra:response-contract" });
80
+ }
81
+ //# sourceMappingURL=response-contract-lane.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response-contract-lane.js","sourceRoot":"","sources":["../../src/server/response-contract-lane.ts"],"names":[],"mappings":"AAiCA,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAA;AAwBxD,MAAM,aAAa,GAAG,CAAC,CAAU,EAAgC,EAAE,CACjE,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;AAE1D,0FAA0F;AAC1F,SAAS,WAAW,CAAC,MAAe,EAAE,KAAc;IAClD,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IAC9D,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAA;AACzD,CAAC;AAED,SAAS,cAAc,CAAC,MAAoC;IAC1D,OAAO,MAAM;SACV,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QACb,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC;YACpC,CAAC,CAAC,KAAK,CAAC,IAAI;iBACP,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CACX,MAAM,CAAC,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,CAAC,CAAC,CAAE,GAAwB,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CACtF;iBACA,IAAI,CAAC,GAAG,CAAC;YACd,CAAC,CAAC,EAAE,CAAA;QACN,OAAO,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,CAAA;IAClE,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAA;AACf,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CACnC,MAAwB,EACxB,MAAe,EACf,IAAwB;IAExB,qGAAqG;IACrG,iFAAiF;IACjF,IAAI,MAAM,YAAY,QAAQ,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,CAAA;IAC5F,MAAM,OAAO,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;IACpD,OAAO,OAAO,YAAY,OAAO;QAC/B,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;QACjD,CAAC,CAAC,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,CAAA;AACtC,CAAC;AAED,SAAS,SAAS,CAChB,OAAgC,EAChC,MAAe,EACf,IAAwB;IAExB,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QACjC,MAAM,OAAO,GAAG,oDAAoD,cAAc,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAA;QACpG,mGAAmG;QACnG,4CAA4C;QAC5C,OAAO,IAAI,KAAK,MAAM;YACpB,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE;YAC1C,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,CAAA;IACpC,CAAC;IACD,MAAM,OAAO,GAAG,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,CAAA;IAClD,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAA;IACnE,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC;QACzB,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE;QAC/B,CAAC,CAAC;YACE,IAAI,EAAE,MAAM;YACZ,KAAK,EAAE,MAAM;YACb,OAAO,EAAE,0DAA0D,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,0EAA0E;SAChK,CAAA;AACP,CAAC;AAkBD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAA6B,MAAM;IAClE,MAAM,OAAO,GAA4B;QACvC,IAAI;QACJ,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC;KACvE,CAAA;IACD,MAAM,KAAK,GAAG,CAAsB,GAAM,EAAK,EAAE;QAC/C,CAAC;QAAC,GAA8C,CAAC,yBAAyB,CAAC,CAAC,OAAO,CAAC,CAAA;QACpF,OAAO,GAAG,CAAA;IACZ,CAAC,CAAA;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,UAAU,EAAE,yBAAyB,EAAE,CAAmB,CAAA;AAC1F,CAAC"}