@graphorin/server 0.5.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 (204) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +116 -0
  4. package/dist/app.d.ts +174 -0
  5. package/dist/app.d.ts.map +1 -0
  6. package/dist/app.js +684 -0
  7. package/dist/app.js.map +1 -0
  8. package/dist/commentary/audit-bridge.d.ts +57 -0
  9. package/dist/commentary/audit-bridge.d.ts.map +1 -0
  10. package/dist/commentary/audit-bridge.js +92 -0
  11. package/dist/commentary/audit-bridge.js.map +1 -0
  12. package/dist/commentary/built-in-patterns.d.ts +24 -0
  13. package/dist/commentary/built-in-patterns.d.ts.map +1 -0
  14. package/dist/commentary/built-in-patterns.js +62 -0
  15. package/dist/commentary/built-in-patterns.js.map +1 -0
  16. package/dist/commentary/index.d.ts +5 -0
  17. package/dist/commentary/index.js +5 -0
  18. package/dist/commentary/sanitizer.d.ts +37 -0
  19. package/dist/commentary/sanitizer.d.ts.map +1 -0
  20. package/dist/commentary/sanitizer.js +182 -0
  21. package/dist/commentary/sanitizer.js.map +1 -0
  22. package/dist/commentary/types.d.ts +120 -0
  23. package/dist/commentary/types.d.ts.map +1 -0
  24. package/dist/config.d.ts +760 -0
  25. package/dist/config.d.ts.map +1 -0
  26. package/dist/config.js +217 -0
  27. package/dist/config.js.map +1 -0
  28. package/dist/consolidator/daemon.d.ts +88 -0
  29. package/dist/consolidator/daemon.d.ts.map +1 -0
  30. package/dist/consolidator/daemon.js +54 -0
  31. package/dist/consolidator/daemon.js.map +1 -0
  32. package/dist/consolidator/index.d.ts +2 -0
  33. package/dist/consolidator/index.js +3 -0
  34. package/dist/errors/index.d.ts +104 -0
  35. package/dist/errors/index.d.ts.map +1 -0
  36. package/dist/errors/index.js +131 -0
  37. package/dist/errors/index.js.map +1 -0
  38. package/dist/health/checks.d.ts +120 -0
  39. package/dist/health/checks.d.ts.map +1 -0
  40. package/dist/health/checks.js +127 -0
  41. package/dist/health/checks.js.map +1 -0
  42. package/dist/health/index.d.ts +3 -0
  43. package/dist/health/index.js +4 -0
  44. package/dist/health/routes.d.ts +66 -0
  45. package/dist/health/routes.d.ts.map +1 -0
  46. package/dist/health/routes.js +96 -0
  47. package/dist/health/routes.js.map +1 -0
  48. package/dist/index.d.ts +88 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +86 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/internal/context.d.ts +66 -0
  53. package/dist/internal/context.d.ts.map +1 -0
  54. package/dist/internal/ids.js +21 -0
  55. package/dist/internal/ids.js.map +1 -0
  56. package/dist/internal/json.js +46 -0
  57. package/dist/internal/json.js.map +1 -0
  58. package/dist/lifecycle/hooks.d.ts +57 -0
  59. package/dist/lifecycle/hooks.d.ts.map +1 -0
  60. package/dist/lifecycle/index.d.ts +2 -0
  61. package/dist/lifecycle/index.js +3 -0
  62. package/dist/lifecycle/pre-bind.d.ts +38 -0
  63. package/dist/lifecycle/pre-bind.d.ts.map +1 -0
  64. package/dist/lifecycle/pre-bind.js +62 -0
  65. package/dist/lifecycle/pre-bind.js.map +1 -0
  66. package/dist/metrics/catalog.d.ts +34 -0
  67. package/dist/metrics/catalog.d.ts.map +1 -0
  68. package/dist/metrics/catalog.js +61 -0
  69. package/dist/metrics/catalog.js.map +1 -0
  70. package/dist/metrics/index.d.ts +3 -0
  71. package/dist/metrics/index.js +4 -0
  72. package/dist/metrics/registry.d.ts +63 -0
  73. package/dist/metrics/registry.d.ts.map +1 -0
  74. package/dist/metrics/registry.js +222 -0
  75. package/dist/metrics/registry.js.map +1 -0
  76. package/dist/middleware/audit.d.ts +47 -0
  77. package/dist/middleware/audit.d.ts.map +1 -0
  78. package/dist/middleware/audit.js +71 -0
  79. package/dist/middleware/audit.js.map +1 -0
  80. package/dist/middleware/auth.d.ts +50 -0
  81. package/dist/middleware/auth.d.ts.map +1 -0
  82. package/dist/middleware/auth.js +164 -0
  83. package/dist/middleware/auth.js.map +1 -0
  84. package/dist/middleware/cors.d.ts +15 -0
  85. package/dist/middleware/cors.d.ts.map +1 -0
  86. package/dist/middleware/cors.js +45 -0
  87. package/dist/middleware/cors.js.map +1 -0
  88. package/dist/middleware/csrf.d.ts +15 -0
  89. package/dist/middleware/csrf.d.ts.map +1 -0
  90. package/dist/middleware/csrf.js +77 -0
  91. package/dist/middleware/csrf.js.map +1 -0
  92. package/dist/middleware/idempotency.d.ts +41 -0
  93. package/dist/middleware/idempotency.d.ts.map +1 -0
  94. package/dist/middleware/idempotency.js +198 -0
  95. package/dist/middleware/idempotency.js.map +1 -0
  96. package/dist/middleware/index.d.ts +9 -0
  97. package/dist/middleware/index.js +10 -0
  98. package/dist/middleware/rate-limit.d.ts +17 -0
  99. package/dist/middleware/rate-limit.d.ts.map +1 -0
  100. package/dist/middleware/rate-limit.js +47 -0
  101. package/dist/middleware/rate-limit.js.map +1 -0
  102. package/dist/middleware/request-state.d.ts +27 -0
  103. package/dist/middleware/request-state.d.ts.map +1 -0
  104. package/dist/middleware/request-state.js +42 -0
  105. package/dist/middleware/request-state.js.map +1 -0
  106. package/dist/middleware/scope.d.ts +24 -0
  107. package/dist/middleware/scope.d.ts.map +1 -0
  108. package/dist/middleware/scope.js +50 -0
  109. package/dist/middleware/scope.js.map +1 -0
  110. package/dist/registry/index.d.ts +135 -0
  111. package/dist/registry/index.d.ts.map +1 -0
  112. package/dist/registry/index.js +96 -0
  113. package/dist/registry/index.js.map +1 -0
  114. package/dist/replay/index.d.ts +2 -0
  115. package/dist/replay/index.js +3 -0
  116. package/dist/replay/routes.d.ts +46 -0
  117. package/dist/replay/routes.d.ts.map +1 -0
  118. package/dist/replay/routes.js +189 -0
  119. package/dist/replay/routes.js.map +1 -0
  120. package/dist/routes/agents.d.ts +41 -0
  121. package/dist/routes/agents.d.ts.map +1 -0
  122. package/dist/routes/agents.js +213 -0
  123. package/dist/routes/agents.js.map +1 -0
  124. package/dist/routes/audit.d.ts +57 -0
  125. package/dist/routes/audit.d.ts.map +1 -0
  126. package/dist/routes/audit.js +116 -0
  127. package/dist/routes/audit.js.map +1 -0
  128. package/dist/routes/auth.d.ts +26 -0
  129. package/dist/routes/auth.d.ts.map +1 -0
  130. package/dist/routes/auth.js +54 -0
  131. package/dist/routes/auth.js.map +1 -0
  132. package/dist/routes/health.d.ts +22 -0
  133. package/dist/routes/health.d.ts.map +1 -0
  134. package/dist/routes/health.js +33 -0
  135. package/dist/routes/health.js.map +1 -0
  136. package/dist/routes/index.d.ts +10 -0
  137. package/dist/routes/index.js +12 -0
  138. package/dist/routes/mcp.d.ts +32 -0
  139. package/dist/routes/mcp.d.ts.map +1 -0
  140. package/dist/routes/mcp.js +61 -0
  141. package/dist/routes/mcp.js.map +1 -0
  142. package/dist/routes/memory.d.ts +141 -0
  143. package/dist/routes/memory.d.ts.map +1 -0
  144. package/dist/routes/memory.js +102 -0
  145. package/dist/routes/memory.js.map +1 -0
  146. package/dist/routes/sessions.d.ts +54 -0
  147. package/dist/routes/sessions.d.ts.map +1 -0
  148. package/dist/routes/sessions.js +135 -0
  149. package/dist/routes/sessions.js.map +1 -0
  150. package/dist/routes/skills.d.ts +34 -0
  151. package/dist/routes/skills.d.ts.map +1 -0
  152. package/dist/routes/skills.js +54 -0
  153. package/dist/routes/skills.js.map +1 -0
  154. package/dist/routes/tokens.d.ts +25 -0
  155. package/dist/routes/tokens.d.ts.map +1 -0
  156. package/dist/routes/tokens.js +87 -0
  157. package/dist/routes/tokens.js.map +1 -0
  158. package/dist/routes/workflows.d.ts +27 -0
  159. package/dist/routes/workflows.d.ts.map +1 -0
  160. package/dist/routes/workflows.js +220 -0
  161. package/dist/routes/workflows.js.map +1 -0
  162. package/dist/runtime/run-state.d.ts +158 -0
  163. package/dist/runtime/run-state.d.ts.map +1 -0
  164. package/dist/runtime/run-state.js +182 -0
  165. package/dist/runtime/run-state.js.map +1 -0
  166. package/dist/sse/events.d.ts +44 -0
  167. package/dist/sse/events.d.ts.map +1 -0
  168. package/dist/sse/events.js +200 -0
  169. package/dist/sse/events.js.map +1 -0
  170. package/dist/sse/index.d.ts +2 -0
  171. package/dist/sse/index.js +3 -0
  172. package/dist/triggers/daemon.d.ts +74 -0
  173. package/dist/triggers/daemon.d.ts.map +1 -0
  174. package/dist/triggers/daemon.js +114 -0
  175. package/dist/triggers/daemon.js.map +1 -0
  176. package/dist/triggers/index.d.ts +3 -0
  177. package/dist/triggers/index.js +4 -0
  178. package/dist/triggers/routes.d.ts +21 -0
  179. package/dist/triggers/routes.d.ts.map +1 -0
  180. package/dist/triggers/routes.js +117 -0
  181. package/dist/triggers/routes.js.map +1 -0
  182. package/dist/ws/dispatcher.d.ts +169 -0
  183. package/dist/ws/dispatcher.d.ts.map +1 -0
  184. package/dist/ws/dispatcher.js +303 -0
  185. package/dist/ws/dispatcher.js.map +1 -0
  186. package/dist/ws/index.d.ts +6 -0
  187. package/dist/ws/index.js +7 -0
  188. package/dist/ws/replay-buffer.d.ts +47 -0
  189. package/dist/ws/replay-buffer.d.ts.map +1 -0
  190. package/dist/ws/replay-buffer.js +88 -0
  191. package/dist/ws/replay-buffer.js.map +1 -0
  192. package/dist/ws/subjects.d.ts +71 -0
  193. package/dist/ws/subjects.d.ts.map +1 -0
  194. package/dist/ws/subjects.js +112 -0
  195. package/dist/ws/subjects.js.map +1 -0
  196. package/dist/ws/ticket.d.ts +74 -0
  197. package/dist/ws/ticket.d.ts.map +1 -0
  198. package/dist/ws/ticket.js +112 -0
  199. package/dist/ws/ticket.js.map +1 -0
  200. package/dist/ws/upgrade.d.ts +63 -0
  201. package/dist/ws/upgrade.d.ts.map +1 -0
  202. package/dist/ws/upgrade.js +324 -0
  203. package/dist/ws/upgrade.js.map +1 -0
  204. package/package.json +156 -0
@@ -0,0 +1,182 @@
1
+ import { DEFAULT_APPLY_TO_EVENTS, DEFAULT_DELIVERY_COMMENTARY_PATTERNS } from "./built-in-patterns.js";
2
+ import { createHash } from "node:crypto";
3
+
4
+ //#region src/commentary/sanitizer.ts
5
+ /**
6
+ * Stateless delivery-layer commentary-phase trace sanitizer. Walks
7
+ * outbound `ServerMessage` event frames and applies the configured
8
+ * pattern catalogue to the JSON-stringified payload; matched
9
+ * fragments are wrapped in the configured envelope (default) or
10
+ * stripped (per the `'strip'` policy) or passed through (per the
11
+ * `'pass-through'` policy — operator opt-out for trusted
12
+ * deployments).
13
+ *
14
+ * The sanitizer runs AFTER the `ServerMessage` Zod-validation pass
15
+ * in `@graphorin/protocol` and BEFORE the message hits the wire (so
16
+ * the protocol schema is unchanged; the sanitization is a pure
17
+ * transform applied to validated payloads). It is bytes-equal across
18
+ * the WS / SSE / REST transports — the same input + same policy
19
+ * produces the same output regardless of which transport invoked
20
+ * the sanitizer.
21
+ *
22
+ * @packageDocumentation
23
+ */
24
+ const DEFAULT_OPEN = "<<<commentary>>>";
25
+ const DEFAULT_CLOSE = "<<</commentary>>>";
26
+ /**
27
+ * Build a stateless delivery-layer sanitizer. Tests can swap the
28
+ * `sink` for an in-memory recorder; production wires the
29
+ * `@graphorin/security/audit` `appendAudit` helper.
30
+ *
31
+ * @stable
32
+ */
33
+ function createDeliveryCommentarySanitizer(config = {}) {
34
+ const policy = config.policy ?? "wrap";
35
+ const applyToEvents = config.applyToEvents ?? DEFAULT_APPLY_TO_EVENTS;
36
+ const applyToSet = new Set(applyToEvents);
37
+ const patterns = config.patterns ?? DEFAULT_DELIVERY_COMMENTARY_PATTERNS;
38
+ const open = config.wrapOpen ?? DEFAULT_OPEN;
39
+ const close = config.wrapClose ?? DEFAULT_CLOSE;
40
+ const sink = config.sink;
41
+ function sanitize(frame, transport) {
42
+ if (policy === "pass-through") return frame;
43
+ if (!applyToSet.has(frame.type)) return frame;
44
+ const before = JSON.stringify(frame.payload ?? null);
45
+ const allReasons = /* @__PURE__ */ new Set();
46
+ let mutated = false;
47
+ const next = walkAndSanitize(frame.payload, (text) => {
48
+ const out = applyToText(text, patterns, policy, open, close);
49
+ for (const reason of out.reasons) allReasons.add(reason);
50
+ if (out.transformed !== text) mutated = true;
51
+ return out.transformed;
52
+ });
53
+ if (!mutated) return frame;
54
+ const after = JSON.stringify(next ?? null);
55
+ const decision = {
56
+ transport,
57
+ boundary: "event-emission",
58
+ policy,
59
+ applied: true,
60
+ reasons: [...allReasons],
61
+ matchedPattern: [...allReasons][0],
62
+ sha256OfBefore: sha256(before),
63
+ sha256OfAfter: sha256(after),
64
+ eventType: frame.type
65
+ };
66
+ try {
67
+ sink?.onDecision(decision);
68
+ } catch {}
69
+ return Object.freeze({
70
+ ...frame,
71
+ payload: next
72
+ });
73
+ }
74
+ return Object.freeze({
75
+ policy,
76
+ applyToEvents,
77
+ patterns,
78
+ sanitize
79
+ });
80
+ }
81
+ /**
82
+ * Recursively walks any JSON-shaped value (`null`, primitive, array,
83
+ * object) and applies the supplied transform to every string leaf.
84
+ * Returns a structurally-cloned copy when at least one leaf was
85
+ * rewritten; returns the original input by reference otherwise.
86
+ */
87
+ function walkAndSanitize(value, transform) {
88
+ if (typeof value === "string") {
89
+ const next = transform(value);
90
+ return next === value ? value : next;
91
+ }
92
+ if (Array.isArray(value)) {
93
+ let mutated = false;
94
+ const next = new Array(value.length);
95
+ for (let i = 0; i < value.length; i += 1) {
96
+ const child = walkAndSanitize(value[i], transform);
97
+ if (child !== value[i]) mutated = true;
98
+ next[i] = child;
99
+ }
100
+ return mutated ? next : value;
101
+ }
102
+ if (value !== null && typeof value === "object") {
103
+ let mutated = false;
104
+ const next = {};
105
+ for (const [key, val] of Object.entries(value)) {
106
+ const child = walkAndSanitize(val, transform);
107
+ if (child !== val) mutated = true;
108
+ next[key] = child;
109
+ }
110
+ return mutated ? next : value;
111
+ }
112
+ return value;
113
+ }
114
+ function applyToText(text, patterns, policy, open, close) {
115
+ const segments = splitByWrapEnvelope(text, open, close);
116
+ const matchedReasons = /* @__PURE__ */ new Set();
117
+ const next = [];
118
+ for (const segment of segments) {
119
+ if (segment.kind === "wrapped") {
120
+ next.push(segment.text);
121
+ continue;
122
+ }
123
+ let segText = segment.text;
124
+ for (const pattern of patterns) {
125
+ const regex = freshRegex(pattern.regex);
126
+ let didMatch = false;
127
+ segText = segText.replace(regex, (match) => {
128
+ didMatch = true;
129
+ if (policy === "strip") return "";
130
+ return `${open}${match}${close}`;
131
+ });
132
+ if (didMatch) matchedReasons.add(pattern.reason);
133
+ }
134
+ next.push(segText);
135
+ }
136
+ return {
137
+ transformed: next.join(""),
138
+ reasons: [...matchedReasons]
139
+ };
140
+ }
141
+ function freshRegex(re) {
142
+ return new RegExp(re.source, re.flags);
143
+ }
144
+ function splitByWrapEnvelope(text, open, close) {
145
+ const out = [];
146
+ let cursor = 0;
147
+ while (cursor < text.length) {
148
+ const openAt = text.indexOf(open, cursor);
149
+ if (openAt < 0) {
150
+ out.push({
151
+ kind: "plain",
152
+ text: text.slice(cursor)
153
+ });
154
+ break;
155
+ }
156
+ if (openAt > cursor) out.push({
157
+ kind: "plain",
158
+ text: text.slice(cursor, openAt)
159
+ });
160
+ const closeAt = text.indexOf(close, openAt + open.length);
161
+ if (closeAt < 0) {
162
+ out.push({
163
+ kind: "plain",
164
+ text: text.slice(openAt)
165
+ });
166
+ break;
167
+ }
168
+ out.push({
169
+ kind: "wrapped",
170
+ text: text.slice(openAt, closeAt + close.length)
171
+ });
172
+ cursor = closeAt + close.length;
173
+ }
174
+ return out;
175
+ }
176
+ function sha256(value) {
177
+ return createHash("sha256").update(value, "utf8").digest("hex");
178
+ }
179
+
180
+ //#endregion
181
+ export { createDeliveryCommentarySanitizer };
182
+ //# sourceMappingURL=sanitizer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitizer.js","names":["policy: DeliveryCommentaryPolicy","applyToEvents: ReadonlyArray<string>","patterns: ReadonlyArray<DeliveryCommentaryPattern>","sink: DeliveryCommentarySink | undefined","decision: DeliveryCommentaryDecision","next: unknown[]","next: Record<string, unknown>","next: string[]","out: { kind: 'plain' | 'wrapped'; text: string }[]"],"sources":["../../src/commentary/sanitizer.ts"],"sourcesContent":["/**\n * Stateless delivery-layer commentary-phase trace sanitizer. Walks\n * outbound `ServerMessage` event frames and applies the configured\n * pattern catalogue to the JSON-stringified payload; matched\n * fragments are wrapped in the configured envelope (default) or\n * stripped (per the `'strip'` policy) or passed through (per the\n * `'pass-through'` policy — operator opt-out for trusted\n * deployments).\n *\n * The sanitizer runs AFTER the `ServerMessage` Zod-validation pass\n * in `@graphorin/protocol` and BEFORE the message hits the wire (so\n * the protocol schema is unchanged; the sanitization is a pure\n * transform applied to validated payloads). It is bytes-equal across\n * the WS / SSE / REST transports — the same input + same policy\n * produces the same output regardless of which transport invoked\n * the sanitizer.\n *\n * @packageDocumentation\n */\n\nimport { createHash } from 'node:crypto';\nimport type { ServerEventFrame } from '@graphorin/protocol';\n\nimport {\n DEFAULT_APPLY_TO_EVENTS,\n DEFAULT_DELIVERY_COMMENTARY_PATTERNS,\n} from './built-in-patterns.js';\nimport type {\n DeliveryCommentaryConfig,\n DeliveryCommentaryDecision,\n DeliveryCommentaryPattern,\n DeliveryCommentaryPolicy,\n DeliveryCommentaryReason,\n DeliveryCommentarySink,\n DeliveryCommentaryTransport,\n} from './types.js';\n\nconst DEFAULT_OPEN = '<<<commentary>>>';\nconst DEFAULT_CLOSE = '<<</commentary>>>';\n\n/**\n * Public surface returned by {@link createDeliveryCommentarySanitizer}.\n *\n * @stable\n */\nexport interface DeliveryCommentarySanitizer {\n readonly policy: DeliveryCommentaryPolicy;\n readonly applyToEvents: ReadonlyArray<string>;\n readonly patterns: ReadonlyArray<DeliveryCommentaryPattern>;\n /**\n * Sanitize the payload of a single `event` frame. Returns the\n * (possibly-replaced) frame; emits an audit decision via the\n * configured sink when the sanitizer mutated the payload.\n *\n * The frame is returned unchanged when:\n * - the policy is `'pass-through'`,\n * - the event type is not in `applyToEvents`,\n * - or no pattern matched the JSON-stringified payload.\n */\n sanitize(frame: ServerEventFrame, transport: DeliveryCommentaryTransport): ServerEventFrame;\n}\n\n/**\n * Build a stateless delivery-layer sanitizer. Tests can swap the\n * `sink` for an in-memory recorder; production wires the\n * `@graphorin/security/audit` `appendAudit` helper.\n *\n * @stable\n */\nexport function createDeliveryCommentarySanitizer(\n config: DeliveryCommentaryConfig = {},\n): DeliveryCommentarySanitizer {\n const policy: DeliveryCommentaryPolicy = config.policy ?? 'wrap';\n const applyToEvents: ReadonlyArray<string> = config.applyToEvents ?? DEFAULT_APPLY_TO_EVENTS;\n const applyToSet = new Set(applyToEvents);\n const patterns: ReadonlyArray<DeliveryCommentaryPattern> =\n config.patterns ?? DEFAULT_DELIVERY_COMMENTARY_PATTERNS;\n const open = config.wrapOpen ?? DEFAULT_OPEN;\n const close = config.wrapClose ?? DEFAULT_CLOSE;\n const sink: DeliveryCommentarySink | undefined = config.sink;\n\n function sanitize(\n frame: ServerEventFrame,\n transport: DeliveryCommentaryTransport,\n ): ServerEventFrame {\n if (policy === 'pass-through') return frame;\n if (!applyToSet.has(frame.type)) return frame;\n const before = JSON.stringify(frame.payload ?? null);\n const allReasons = new Set<DeliveryCommentaryReason>();\n let mutated = false;\n const next = walkAndSanitize(frame.payload, (text) => {\n const out = applyToText(text, patterns, policy, open, close);\n for (const reason of out.reasons) allReasons.add(reason);\n if (out.transformed !== text) mutated = true;\n return out.transformed;\n });\n if (!mutated) return frame;\n const after = JSON.stringify(next ?? null);\n const decision: DeliveryCommentaryDecision = {\n transport,\n boundary: 'event-emission',\n policy,\n applied: true,\n reasons: [...allReasons],\n matchedPattern: [...allReasons][0],\n sha256OfBefore: sha256(before),\n sha256OfAfter: sha256(after),\n eventType: frame.type,\n };\n try {\n sink?.onDecision(decision);\n } catch {\n // Sink errors must never block the wire — swallow + continue.\n }\n return Object.freeze({ ...frame, payload: next });\n }\n\n return Object.freeze({\n policy,\n applyToEvents,\n patterns,\n sanitize,\n });\n}\n\ninterface ApplyResult {\n readonly transformed: string;\n readonly reasons: ReadonlyArray<DeliveryCommentaryReason>;\n}\n\n/**\n * Recursively walks any JSON-shaped value (`null`, primitive, array,\n * object) and applies the supplied transform to every string leaf.\n * Returns a structurally-cloned copy when at least one leaf was\n * rewritten; returns the original input by reference otherwise.\n */\nfunction walkAndSanitize(value: unknown, transform: (s: string) => string): unknown {\n if (typeof value === 'string') {\n const next = transform(value);\n return next === value ? value : next;\n }\n if (Array.isArray(value)) {\n let mutated = false;\n const next: unknown[] = new Array(value.length);\n for (let i = 0; i < value.length; i += 1) {\n const child = walkAndSanitize(value[i], transform);\n if (child !== value[i]) mutated = true;\n next[i] = child;\n }\n return mutated ? next : value;\n }\n if (value !== null && typeof value === 'object') {\n let mutated = false;\n const next: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(value as Record<string, unknown>)) {\n const child = walkAndSanitize(val, transform);\n if (child !== val) mutated = true;\n next[key] = child;\n }\n return mutated ? next : value;\n }\n return value;\n}\n\nfunction applyToText(\n text: string,\n patterns: ReadonlyArray<DeliveryCommentaryPattern>,\n policy: DeliveryCommentaryPolicy,\n open: string,\n close: string,\n): ApplyResult {\n // Split the body into already-wrapped + plain segments so we never\n // re-scan inside an existing wrap envelope. This makes the sanitizer\n // idempotent on the same payload (a second pass produces bytes-equal\n // output, satisfying the cross-cut composition contract with\n // Phase 11 session-output sanitization + Phase 11 replay\n // sanitization).\n const segments = splitByWrapEnvelope(text, open, close);\n const matchedReasons = new Set<DeliveryCommentaryReason>();\n const next: string[] = [];\n for (const segment of segments) {\n if (segment.kind === 'wrapped') {\n next.push(segment.text);\n continue;\n }\n let segText = segment.text;\n for (const pattern of patterns) {\n const regex = freshRegex(pattern.regex);\n let didMatch = false;\n segText = segText.replace(regex, (match) => {\n didMatch = true;\n if (policy === 'strip') return '';\n return `${open}${match}${close}`;\n });\n if (didMatch) matchedReasons.add(pattern.reason);\n }\n next.push(segText);\n }\n return {\n transformed: next.join(''),\n reasons: [...matchedReasons],\n };\n}\n\nfunction freshRegex(re: RegExp): RegExp {\n return new RegExp(re.source, re.flags);\n}\n\nfunction splitByWrapEnvelope(\n text: string,\n open: string,\n close: string,\n): ReadonlyArray<{ readonly kind: 'plain' | 'wrapped'; readonly text: string }> {\n const out: { kind: 'plain' | 'wrapped'; text: string }[] = [];\n let cursor = 0;\n while (cursor < text.length) {\n const openAt = text.indexOf(open, cursor);\n if (openAt < 0) {\n out.push({ kind: 'plain', text: text.slice(cursor) });\n break;\n }\n if (openAt > cursor) {\n out.push({ kind: 'plain', text: text.slice(cursor, openAt) });\n }\n const closeAt = text.indexOf(close, openAt + open.length);\n if (closeAt < 0) {\n out.push({ kind: 'plain', text: text.slice(openAt) });\n break;\n }\n out.push({ kind: 'wrapped', text: text.slice(openAt, closeAt + close.length) });\n cursor = closeAt + close.length;\n }\n return out;\n}\n\nfunction sha256(value: string): string {\n return createHash('sha256').update(value, 'utf8').digest('hex');\n}\n"],"mappings":"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"}
@@ -0,0 +1,120 @@
1
+ //#region src/commentary/types.d.ts
2
+ /**
3
+ * Types for the delivery-layer commentary-phase trace sanitization
4
+ * applied by the WebSocket dispatcher (`@graphorin/server/ws`) and
5
+ * the SSE event-emission boundary (`@graphorin/server/sse`).
6
+ *
7
+ * The catalogue + per-policy enum + audit row shape is intentionally
8
+ * structural so the server's delivery layer stays self-contained. A
9
+ * deployment that wants to share patterns with the session-output
10
+ * boundary in `@graphorin/sessions` can pass that package's
11
+ * `BUILT_IN_COMMENTARY_PATTERNS` through {@link DeliveryCommentaryConfig}.patterns;
12
+ * the shape is bytes-equal across the two layers (the
13
+ * defense-in-depth posture is the load-bearing property — see
14
+ * Phase 11 sanitizer for the storage-write boundary, this module
15
+ * for the wire-emission boundary).
16
+ *
17
+ * @packageDocumentation
18
+ */
19
+ /**
20
+ * Operator-facing policy. Identical semantics to the session-output
21
+ * sanitizer in `@graphorin/sessions/commentary` so the two layers
22
+ * are bytes-equal on idempotent re-application.
23
+ *
24
+ * - `'wrap'` (default) — wraps the matched fragment in a
25
+ * `<<<commentary>>>...<<</commentary>>>` envelope so downstream
26
+ * consumers can choose to render or hide based on context.
27
+ * - `'strip'` — removes the matched fragment entirely.
28
+ * - `'pass-through'` — disables the sanitization (operator opt-in
29
+ * for trusted deployments).
30
+ *
31
+ * @stable
32
+ */
33
+ type DeliveryCommentaryPolicy = 'wrap' | 'strip' | 'pass-through';
34
+ /**
35
+ * Discriminator surfaced on audit rows + counter labels for the
36
+ * transport that produced the sanitization decision. The cardinality
37
+ * is bounded (3-valued).
38
+ *
39
+ * @stable
40
+ */
41
+ type DeliveryCommentaryTransport = 'ws' | 'sse' | 'rest';
42
+ /**
43
+ * Stable label for each detection pattern. Surfaced in the audit row
44
+ * + the counter label cardinality is bounded.
45
+ *
46
+ * @stable
47
+ */
48
+ type DeliveryCommentaryReason = 'tool.call.start-payload-signature' | 'tool.call.delta-payload-signature' | 'tool.call.end-payload-signature' | 'tool.execute.end-payload-signature' | 'agent.fanout-event-signature' | 'context.compacted-event-signature' | 'agent.model.fellback-event-signature';
49
+ /**
50
+ * Single pattern entry in the {@link DEFAULT_DELIVERY_COMMENTARY_PATTERNS}
51
+ * catalogue. The `regex` is matched against the JSON payload of the
52
+ * `event` frame (after `JSON.stringify(payload)`); deployments that
53
+ * want to match against the wire-format string instead can supply
54
+ * their own catalogue.
55
+ *
56
+ * @stable
57
+ */
58
+ interface DeliveryCommentaryPattern {
59
+ readonly reason: DeliveryCommentaryReason;
60
+ readonly regex: RegExp;
61
+ readonly description: string;
62
+ }
63
+ /**
64
+ * Per-emission decision recorded by the sanitizer. Mirrored on the
65
+ * audit row + the counter increment.
66
+ *
67
+ * @stable
68
+ */
69
+ interface DeliveryCommentaryDecision {
70
+ readonly transport: DeliveryCommentaryTransport;
71
+ readonly boundary: 'event-emission';
72
+ readonly policy: DeliveryCommentaryPolicy;
73
+ readonly applied: boolean;
74
+ readonly reasons: ReadonlyArray<DeliveryCommentaryReason>;
75
+ readonly matchedPattern: string | undefined;
76
+ readonly sha256OfBefore: string;
77
+ readonly sha256OfAfter: string;
78
+ readonly eventType: string;
79
+ }
80
+ /**
81
+ * Single audit + counter sink consumed by the sanitizer when a
82
+ * decision fires. Wiring is optional — operators that do not need
83
+ * audit telemetry can skip the sink and the sanitizer becomes a
84
+ * pure transform.
85
+ *
86
+ * @stable
87
+ */
88
+ interface DeliveryCommentarySink {
89
+ /**
90
+ * Called once per applied decision. Implementations should be
91
+ * non-throwing; the sanitizer wraps the call in `try/catch` so a
92
+ * misbehaving sink never blocks the wire.
93
+ */
94
+ onDecision(decision: DeliveryCommentaryDecision): void;
95
+ }
96
+ /**
97
+ * Public configuration accepted by the WS / SSE / REST event-
98
+ * emission sanitizer. Shape mirrors the per-server
99
+ * `WsConfig.commentarySanitization` field documented in the runtime
100
+ * spec.
101
+ *
102
+ * @stable
103
+ */
104
+ interface DeliveryCommentaryConfig {
105
+ readonly policy?: DeliveryCommentaryPolicy;
106
+ /**
107
+ * Whitelist of `event.type` literals to sanitize. The default
108
+ * covers the user-visible commentary surface (`text.delta` plus
109
+ * the two tool-result variants); operators extend the list as
110
+ * their UI rendering boundary expands.
111
+ */
112
+ readonly applyToEvents?: ReadonlyArray<string>;
113
+ readonly patterns?: ReadonlyArray<DeliveryCommentaryPattern>;
114
+ readonly wrapOpen?: string;
115
+ readonly wrapClose?: string;
116
+ readonly sink?: DeliveryCommentarySink;
117
+ }
118
+ //#endregion
119
+ export { DeliveryCommentaryConfig, DeliveryCommentaryDecision, DeliveryCommentaryPattern, DeliveryCommentaryPolicy, DeliveryCommentaryReason, DeliveryCommentarySink, DeliveryCommentaryTransport };
120
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../../src/commentary/types.ts"],"sourcesContent":[],"mappings":";;AAgCA;AASA;AAQA;AAkBA;AAYA;;;;;;AAoBA;AAiBA;;;;;;;;;;;;;;;;;;;KApFY,wBAAA;;;;;;;;KASA,2BAAA;;;;;;;KAQA,wBAAA;;;;;;;;;;UAkBK,yBAAA;mBACE;kBACD;;;;;;;;;UAUD,0BAAA;sBACK;;mBAEH;;oBAEC,cAAc;;;;;;;;;;;;;;UAejB,sBAAA;;;;;;uBAMM;;;;;;;;;;UAWN,wBAAA;oBACG;;;;;;;2BAOO;sBACL,cAAc;;;kBAGlB"}