@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,112 @@
1
+ import { parseScope, scopeMatches } from "@graphorin/security/auth";
2
+
3
+ //#region src/ws/subjects.ts
4
+ const SESSION_EVENTS = /^session:([A-Za-z0-9_-]+)\/events$/;
5
+ const SESSION_RUN_EVENTS = /^session:([A-Za-z0-9_-]+)\/runs\/([A-Za-z0-9_-]+)\/events$/;
6
+ const AGENT_RUN_EVENTS = /^agent:([A-Za-z0-9_-]+)\/runs\/([A-Za-z0-9_-]+)\/events$/;
7
+ const WORKFLOW_EVENTS = /^workflow:([A-Za-z0-9_-]+)\/events$/;
8
+ const WORKFLOW_RUN_EVENTS = /^workflow:([A-Za-z0-9_-]+)\/runs\/([A-Za-z0-9_-]+)\/events$/;
9
+ /**
10
+ * Parse a subject string into the {@link ParsedSubject} discriminator.
11
+ *
12
+ * @stable
13
+ */
14
+ function tryParseSubject(raw) {
15
+ if (typeof raw !== "string" || raw.length === 0) return {
16
+ ok: false,
17
+ reason: "malformed"
18
+ };
19
+ if (raw.includes("*") || raw.includes("#")) return {
20
+ ok: false,
21
+ reason: "wildcard-not-supported"
22
+ };
23
+ if (raw === "memory/conflicts") return {
24
+ ok: true,
25
+ subject: { kind: "memory-conflicts" }
26
+ };
27
+ if (raw === "audit/events") return {
28
+ ok: true,
29
+ subject: { kind: "audit-events" }
30
+ };
31
+ let m = raw.match(SESSION_RUN_EVENTS);
32
+ if (m !== null) return {
33
+ ok: true,
34
+ subject: {
35
+ kind: "session-run-events",
36
+ sessionId: m[1] ?? "",
37
+ runId: m[2] ?? ""
38
+ }
39
+ };
40
+ m = raw.match(AGENT_RUN_EVENTS);
41
+ if (m !== null) return {
42
+ ok: true,
43
+ subject: {
44
+ kind: "agent-run-events",
45
+ agentId: m[1] ?? "",
46
+ runId: m[2] ?? ""
47
+ }
48
+ };
49
+ m = raw.match(SESSION_EVENTS);
50
+ if (m !== null) return {
51
+ ok: true,
52
+ subject: {
53
+ kind: "session-events",
54
+ sessionId: m[1] ?? ""
55
+ }
56
+ };
57
+ m = raw.match(WORKFLOW_RUN_EVENTS);
58
+ if (m !== null) return {
59
+ ok: true,
60
+ subject: {
61
+ kind: "workflow-run-events",
62
+ workflowId: m[1] ?? "",
63
+ runId: m[2] ?? ""
64
+ }
65
+ };
66
+ m = raw.match(WORKFLOW_EVENTS);
67
+ if (m !== null) return {
68
+ ok: true,
69
+ subject: {
70
+ kind: "workflow-events",
71
+ workflowId: m[1] ?? ""
72
+ }
73
+ };
74
+ return {
75
+ ok: false,
76
+ reason: "unknown-subject"
77
+ };
78
+ }
79
+ /**
80
+ * Required scope literal for every subject kind, expressed as a
81
+ * `ParsedScope`. The matcher `scopeMatches(granted, required)` uses
82
+ * the standard wildcard rules from `@graphorin/security/auth`
83
+ * (e.g. `agents:*` matches `agents:invoke:foo`).
84
+ *
85
+ * @stable
86
+ */
87
+ function requiredScopeFor(subject) {
88
+ switch (subject.kind) {
89
+ case "session-events": return parseScope(`agents:invoke:${subject.sessionId}`);
90
+ case "session-run-events": return parseScope(`agents:invoke:${subject.sessionId}`);
91
+ case "agent-run-events": return parseScope(`agents:read:${subject.agentId}`);
92
+ case "workflow-events": return parseScope(`workflows:read:${subject.workflowId}`);
93
+ case "workflow-run-events": return parseScope(`workflows:read:${subject.workflowId}`);
94
+ case "memory-conflicts": return parseScope("memory:read");
95
+ case "audit-events": return parseScope("audit:read");
96
+ }
97
+ }
98
+ /**
99
+ * Compatibility shim — re-exports `scopeMatches` so consumers don't
100
+ * have to learn the security package's surface.
101
+ *
102
+ * @stable
103
+ */
104
+ function isSubjectAllowed(granted, subject) {
105
+ const required = requiredScopeFor(subject);
106
+ for (const scope of granted) if (scopeMatches(scope, required)) return true;
107
+ return false;
108
+ }
109
+
110
+ //#endregion
111
+ export { isSubjectAllowed, requiredScopeFor, tryParseSubject };
112
+ //# sourceMappingURL=subjects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subjects.js","names":[],"sources":["../../src/ws/subjects.ts"],"sourcesContent":["/**\n * Strict subject grammar enforced by the WS dispatcher. Subjects are\n * the multiplexing primitive over a single WS connection: every\n * subscription is keyed by a subject, every event carries the\n * subject it was emitted into, and the per-subject scope check\n * happens at subscription time.\n *\n * Wildcards are deferred to v0.2+ — the dispatcher rejects any\n * subject containing `'*'` or `'#'` so the ACL surface stays\n * tractable.\n *\n * @packageDocumentation\n */\n\nimport type { ParsedScope } from '@graphorin/security/auth';\nimport { parseScope, scopeMatches } from '@graphorin/security/auth';\n\n/**\n * Discriminated union of every recognised subject form. Surfaced on\n * audit log entries + diagnostics; the wire still carries the raw\n * string.\n *\n * @stable\n */\nexport type ParsedSubject =\n | { readonly kind: 'session-events'; readonly sessionId: string }\n | {\n readonly kind: 'session-run-events';\n readonly sessionId: string;\n readonly runId: string;\n }\n | {\n readonly kind: 'agent-run-events';\n readonly agentId: string;\n readonly runId: string;\n }\n | { readonly kind: 'workflow-events'; readonly workflowId: string }\n | {\n readonly kind: 'workflow-run-events';\n readonly workflowId: string;\n readonly runId: string;\n }\n | { readonly kind: 'memory-conflicts' }\n | { readonly kind: 'audit-events' };\n\n/**\n * Result of {@link tryParseSubject}.\n *\n * @stable\n */\nexport type ParseSubjectResult =\n | { readonly ok: true; readonly subject: ParsedSubject }\n | {\n readonly ok: false;\n readonly reason: 'wildcard-not-supported' | 'unknown-subject' | 'malformed';\n };\n\nconst SESSION_EVENTS = /^session:([A-Za-z0-9_-]+)\\/events$/;\nconst SESSION_RUN_EVENTS = /^session:([A-Za-z0-9_-]+)\\/runs\\/([A-Za-z0-9_-]+)\\/events$/;\nconst AGENT_RUN_EVENTS = /^agent:([A-Za-z0-9_-]+)\\/runs\\/([A-Za-z0-9_-]+)\\/events$/;\nconst WORKFLOW_EVENTS = /^workflow:([A-Za-z0-9_-]+)\\/events$/;\nconst WORKFLOW_RUN_EVENTS = /^workflow:([A-Za-z0-9_-]+)\\/runs\\/([A-Za-z0-9_-]+)\\/events$/;\n\n/**\n * Parse a subject string into the {@link ParsedSubject} discriminator.\n *\n * @stable\n */\nexport function tryParseSubject(raw: string): ParseSubjectResult {\n if (typeof raw !== 'string' || raw.length === 0) {\n return { ok: false, reason: 'malformed' };\n }\n if (raw.includes('*') || raw.includes('#')) {\n return { ok: false, reason: 'wildcard-not-supported' };\n }\n if (raw === 'memory/conflicts') {\n return { ok: true, subject: { kind: 'memory-conflicts' } };\n }\n if (raw === 'audit/events') {\n return { ok: true, subject: { kind: 'audit-events' } };\n }\n let m = raw.match(SESSION_RUN_EVENTS);\n if (m !== null) {\n return {\n ok: true,\n subject: { kind: 'session-run-events', sessionId: m[1] ?? '', runId: m[2] ?? '' },\n };\n }\n m = raw.match(AGENT_RUN_EVENTS);\n if (m !== null) {\n return {\n ok: true,\n subject: { kind: 'agent-run-events', agentId: m[1] ?? '', runId: m[2] ?? '' },\n };\n }\n m = raw.match(SESSION_EVENTS);\n if (m !== null) {\n return { ok: true, subject: { kind: 'session-events', sessionId: m[1] ?? '' } };\n }\n m = raw.match(WORKFLOW_RUN_EVENTS);\n if (m !== null) {\n return {\n ok: true,\n subject: { kind: 'workflow-run-events', workflowId: m[1] ?? '', runId: m[2] ?? '' },\n };\n }\n m = raw.match(WORKFLOW_EVENTS);\n if (m !== null) {\n return { ok: true, subject: { kind: 'workflow-events', workflowId: m[1] ?? '' } };\n }\n return { ok: false, reason: 'unknown-subject' };\n}\n\n/**\n * Required scope literal for every subject kind, expressed as a\n * `ParsedScope`. The matcher `scopeMatches(granted, required)` uses\n * the standard wildcard rules from `@graphorin/security/auth`\n * (e.g. `agents:*` matches `agents:invoke:foo`).\n *\n * @stable\n */\nexport function requiredScopeFor(subject: ParsedSubject): ParsedScope {\n switch (subject.kind) {\n case 'session-events':\n return parseScope(`agents:invoke:${subject.sessionId}`);\n case 'session-run-events':\n return parseScope(`agents:invoke:${subject.sessionId}`);\n case 'agent-run-events':\n return parseScope(`agents:read:${subject.agentId}`);\n case 'workflow-events':\n return parseScope(`workflows:read:${subject.workflowId}`);\n case 'workflow-run-events':\n return parseScope(`workflows:read:${subject.workflowId}`);\n case 'memory-conflicts':\n return parseScope('memory:read');\n case 'audit-events':\n return parseScope('audit:read');\n }\n}\n\n/**\n * Compatibility shim — re-exports `scopeMatches` so consumers don't\n * have to learn the security package's surface.\n *\n * @stable\n */\nexport function isSubjectAllowed(\n granted: ReadonlyArray<ParsedScope>,\n subject: ParsedSubject,\n): boolean {\n const required = requiredScopeFor(subject);\n for (const scope of granted) {\n if (scopeMatches(scope, required)) return true;\n }\n return false;\n}\n"],"mappings":";;;AAyDA,MAAM,iBAAiB;AACvB,MAAM,qBAAqB;AAC3B,MAAM,mBAAmB;AACzB,MAAM,kBAAkB;AACxB,MAAM,sBAAsB;;;;;;AAO5B,SAAgB,gBAAgB,KAAiC;AAC/D,KAAI,OAAO,QAAQ,YAAY,IAAI,WAAW,EAC5C,QAAO;EAAE,IAAI;EAAO,QAAQ;EAAa;AAE3C,KAAI,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,IAAI,CACxC,QAAO;EAAE,IAAI;EAAO,QAAQ;EAA0B;AAExD,KAAI,QAAQ,mBACV,QAAO;EAAE,IAAI;EAAM,SAAS,EAAE,MAAM,oBAAoB;EAAE;AAE5D,KAAI,QAAQ,eACV,QAAO;EAAE,IAAI;EAAM,SAAS,EAAE,MAAM,gBAAgB;EAAE;CAExD,IAAI,IAAI,IAAI,MAAM,mBAAmB;AACrC,KAAI,MAAM,KACR,QAAO;EACL,IAAI;EACJ,SAAS;GAAE,MAAM;GAAsB,WAAW,EAAE,MAAM;GAAI,OAAO,EAAE,MAAM;GAAI;EAClF;AAEH,KAAI,IAAI,MAAM,iBAAiB;AAC/B,KAAI,MAAM,KACR,QAAO;EACL,IAAI;EACJ,SAAS;GAAE,MAAM;GAAoB,SAAS,EAAE,MAAM;GAAI,OAAO,EAAE,MAAM;GAAI;EAC9E;AAEH,KAAI,IAAI,MAAM,eAAe;AAC7B,KAAI,MAAM,KACR,QAAO;EAAE,IAAI;EAAM,SAAS;GAAE,MAAM;GAAkB,WAAW,EAAE,MAAM;GAAI;EAAE;AAEjF,KAAI,IAAI,MAAM,oBAAoB;AAClC,KAAI,MAAM,KACR,QAAO;EACL,IAAI;EACJ,SAAS;GAAE,MAAM;GAAuB,YAAY,EAAE,MAAM;GAAI,OAAO,EAAE,MAAM;GAAI;EACpF;AAEH,KAAI,IAAI,MAAM,gBAAgB;AAC9B,KAAI,MAAM,KACR,QAAO;EAAE,IAAI;EAAM,SAAS;GAAE,MAAM;GAAmB,YAAY,EAAE,MAAM;GAAI;EAAE;AAEnF,QAAO;EAAE,IAAI;EAAO,QAAQ;EAAmB;;;;;;;;;;AAWjD,SAAgB,iBAAiB,SAAqC;AACpE,SAAQ,QAAQ,MAAhB;EACE,KAAK,iBACH,QAAO,WAAW,iBAAiB,QAAQ,YAAY;EACzD,KAAK,qBACH,QAAO,WAAW,iBAAiB,QAAQ,YAAY;EACzD,KAAK,mBACH,QAAO,WAAW,eAAe,QAAQ,UAAU;EACrD,KAAK,kBACH,QAAO,WAAW,kBAAkB,QAAQ,aAAa;EAC3D,KAAK,sBACH,QAAO,WAAW,kBAAkB,QAAQ,aAAa;EAC3D,KAAK,mBACH,QAAO,WAAW,cAAc;EAClC,KAAK,eACH,QAAO,WAAW,aAAa;;;;;;;;;AAUrC,SAAgB,iBACd,SACA,SACS;CACT,MAAM,WAAW,iBAAiB,QAAQ;AAC1C,MAAK,MAAM,SAAS,QAClB,KAAI,aAAa,OAAO,SAAS,CAAE,QAAO;AAE5C,QAAO"}
@@ -0,0 +1,74 @@
1
+ import { ParsedScope } from "@graphorin/security/auth";
2
+
3
+ //#region src/ws/ticket.d.ts
4
+
5
+ /**
6
+ * Stable shape returned by {@link WsTicketStore.issue}.
7
+ *
8
+ * @stable
9
+ */
10
+ interface WsTicket {
11
+ readonly value: string;
12
+ readonly expiresAt: number;
13
+ readonly issuedAt: number;
14
+ readonly tokenId: string;
15
+ readonly scopes: ReadonlyArray<ParsedScope>;
16
+ }
17
+ /**
18
+ * Stable result of {@link WsTicketStore.consume}.
19
+ *
20
+ * @stable
21
+ */
22
+ type WsTicketConsumeResult = {
23
+ readonly ok: true;
24
+ readonly ticket: WsTicket;
25
+ } | {
26
+ readonly ok: false;
27
+ readonly reason: 'unknown' | 'consumed' | 'expired';
28
+ };
29
+ /**
30
+ * Options accepted by {@link createWsTicketStore}.
31
+ *
32
+ * @stable
33
+ */
34
+ interface WsTicketStoreOptions {
35
+ /** Default `300_000` (5 min). */
36
+ readonly ttlMs?: number;
37
+ /** Cap on the number of unconsumed tickets retained. Default `1_000`. */
38
+ readonly maxOutstanding?: number;
39
+ readonly now?: () => number;
40
+ /**
41
+ * Random-bytes generator. Tests pass a deterministic source so
42
+ * ticket values are reproducible.
43
+ */
44
+ readonly randomBytes?: (length: number) => Uint8Array;
45
+ }
46
+ /**
47
+ * Pluggable in-memory ticket store used by the WS upgrade handler +
48
+ * the `POST /v1/session/ws-ticket` route.
49
+ *
50
+ * @stable
51
+ */
52
+ interface WsTicketStore {
53
+ readonly ttlMs: number;
54
+ issue(input: {
55
+ readonly tokenId: string;
56
+ readonly scopes: ReadonlyArray<ParsedScope>;
57
+ }): WsTicket;
58
+ consume(value: string): WsTicketConsumeResult;
59
+ /** Drop expired entries; called on every `consume()`. */
60
+ prune(): number;
61
+ size(): number;
62
+ }
63
+ /**
64
+ * Build the default in-memory ticket store. Production deployments
65
+ * use exactly one store per process (multiple processes would each
66
+ * issue their own tickets — there is no shared state because the
67
+ * single-user-per-process default applies).
68
+ *
69
+ * @stable
70
+ */
71
+ declare function createWsTicketStore(options?: WsTicketStoreOptions): WsTicketStore;
72
+ //#endregion
73
+ export { WsTicket, WsTicketConsumeResult, WsTicketStore, WsTicketStoreOptions, createWsTicketStore };
74
+ //# sourceMappingURL=ticket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ticket.d.ts","names":[],"sources":["../../src/ws/ticket.ts"],"sourcesContent":[],"mappings":";;;;;;;;;UA2BiB,QAAA;;;;;mBAKE,cAAc;;;;;;;KAQrB,qBAAA;;mBAC8B;;;;;;;;;;UAQzB,oBAAA;;;;;;;;;;6CAU4B;;;;;;;;UAS5B,aAAA;;;;qBAE2C,cAAc;MAAiB;0BACjE;;;;;;;;;;;;;iBAmBV,mBAAA,WAA6B,uBAA4B"}
@@ -0,0 +1,112 @@
1
+ import { randomBytes } from "node:crypto";
2
+
3
+ //#region src/ws/ticket.ts
4
+ /**
5
+ * In-memory single-use ticket store for browser WebSocket clients.
6
+ * The browser WebSocket API does not allow custom headers, so the
7
+ * server issues a short-lived ticket via `POST /v1/session/ws-ticket`
8
+ * (HTTP-authenticated); the client then attaches the value as a
9
+ * second `Sec-WebSocket-Protocol` token (`ticket.<value>`) on the
10
+ * upgrade request.
11
+ *
12
+ * Tickets:
13
+ * - default 5-minute TTL,
14
+ * - single-use (the first valid `consume()` call marks the ticket
15
+ * consumed; every subsequent attempt rejects),
16
+ * - in-memory only (server restart invalidates everything; CLI /
17
+ * SDK clients use HTTP bearer instead so the lost ticket window
18
+ * does not affect them).
19
+ *
20
+ * @packageDocumentation
21
+ */
22
+ /**
23
+ * Build the default in-memory ticket store. Production deployments
24
+ * use exactly one store per process (multiple processes would each
25
+ * issue their own tickets — there is no shared state because the
26
+ * single-user-per-process default applies).
27
+ *
28
+ * @stable
29
+ */
30
+ function createWsTicketStore(options = {}) {
31
+ const ttlMs = options.ttlMs ?? 5 * 6e4;
32
+ const maxOutstanding = options.maxOutstanding ?? 1e3;
33
+ const now = options.now ?? Date.now;
34
+ const random = options.randomBytes ?? ((n) => randomBytes(n));
35
+ const entries = /* @__PURE__ */ new Map();
36
+ function evictOldest() {
37
+ const oldest = entries.keys().next().value;
38
+ if (oldest !== void 0) entries.delete(oldest);
39
+ }
40
+ function prune() {
41
+ const cutoff = now();
42
+ let removed = 0;
43
+ for (const [value, entry] of entries) if (entry.ticket.expiresAt <= cutoff) {
44
+ entries.delete(value);
45
+ removed += 1;
46
+ }
47
+ return removed;
48
+ }
49
+ function issue(input) {
50
+ prune();
51
+ while (entries.size >= maxOutstanding) evictOldest();
52
+ const issuedAt = now();
53
+ const value = encodeTicket(random(24));
54
+ const ticket = {
55
+ value,
56
+ issuedAt,
57
+ expiresAt: issuedAt + ttlMs,
58
+ tokenId: input.tokenId,
59
+ scopes: Object.freeze(input.scopes.slice())
60
+ };
61
+ entries.set(value, {
62
+ ticket,
63
+ consumed: false
64
+ });
65
+ return ticket;
66
+ }
67
+ function consume(value) {
68
+ const entry = entries.get(value);
69
+ if (entry === void 0) {
70
+ prune();
71
+ return {
72
+ ok: false,
73
+ reason: "unknown"
74
+ };
75
+ }
76
+ if (entry.ticket.expiresAt <= now()) {
77
+ entries.delete(value);
78
+ prune();
79
+ return {
80
+ ok: false,
81
+ reason: "expired"
82
+ };
83
+ }
84
+ if (entry.consumed) {
85
+ prune();
86
+ return {
87
+ ok: false,
88
+ reason: "consumed"
89
+ };
90
+ }
91
+ entry.consumed = true;
92
+ prune();
93
+ return {
94
+ ok: true,
95
+ ticket: entry.ticket
96
+ };
97
+ }
98
+ return Object.freeze({
99
+ ttlMs,
100
+ issue,
101
+ consume,
102
+ prune,
103
+ size: () => entries.size
104
+ });
105
+ }
106
+ function encodeTicket(bytes) {
107
+ return Buffer.from(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
108
+ }
109
+
110
+ //#endregion
111
+ export { createWsTicketStore };
112
+ //# sourceMappingURL=ticket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ticket.js","names":["ticket: WsTicket"],"sources":["../../src/ws/ticket.ts"],"sourcesContent":["/**\n * In-memory single-use ticket store for browser WebSocket clients.\n * The browser WebSocket API does not allow custom headers, so the\n * server issues a short-lived ticket via `POST /v1/session/ws-ticket`\n * (HTTP-authenticated); the client then attaches the value as a\n * second `Sec-WebSocket-Protocol` token (`ticket.<value>`) on the\n * upgrade request.\n *\n * Tickets:\n * - default 5-minute TTL,\n * - single-use (the first valid `consume()` call marks the ticket\n * consumed; every subsequent attempt rejects),\n * - in-memory only (server restart invalidates everything; CLI /\n * SDK clients use HTTP bearer instead so the lost ticket window\n * does not affect them).\n *\n * @packageDocumentation\n */\n\nimport { randomBytes } from 'node:crypto';\nimport type { ParsedScope } from '@graphorin/security/auth';\n\n/**\n * Stable shape returned by {@link WsTicketStore.issue}.\n *\n * @stable\n */\nexport interface WsTicket {\n readonly value: string;\n readonly expiresAt: number;\n readonly issuedAt: number;\n readonly tokenId: string;\n readonly scopes: ReadonlyArray<ParsedScope>;\n}\n\n/**\n * Stable result of {@link WsTicketStore.consume}.\n *\n * @stable\n */\nexport type WsTicketConsumeResult =\n | { readonly ok: true; readonly ticket: WsTicket }\n | { readonly ok: false; readonly reason: 'unknown' | 'consumed' | 'expired' };\n\n/**\n * Options accepted by {@link createWsTicketStore}.\n *\n * @stable\n */\nexport interface WsTicketStoreOptions {\n /** Default `300_000` (5 min). */\n readonly ttlMs?: number;\n /** Cap on the number of unconsumed tickets retained. Default `1_000`. */\n readonly maxOutstanding?: number;\n readonly now?: () => number;\n /**\n * Random-bytes generator. Tests pass a deterministic source so\n * ticket values are reproducible.\n */\n readonly randomBytes?: (length: number) => Uint8Array;\n}\n\n/**\n * Pluggable in-memory ticket store used by the WS upgrade handler +\n * the `POST /v1/session/ws-ticket` route.\n *\n * @stable\n */\nexport interface WsTicketStore {\n readonly ttlMs: number;\n issue(input: { readonly tokenId: string; readonly scopes: ReadonlyArray<ParsedScope> }): WsTicket;\n consume(value: string): WsTicketConsumeResult;\n /** Drop expired entries; called on every `consume()`. */\n prune(): number;\n size(): number;\n}\n\ninterface InternalEntry {\n readonly ticket: WsTicket;\n consumed: boolean;\n}\n\n/**\n * Build the default in-memory ticket store. Production deployments\n * use exactly one store per process (multiple processes would each\n * issue their own tickets — there is no shared state because the\n * single-user-per-process default applies).\n *\n * @stable\n */\nexport function createWsTicketStore(options: WsTicketStoreOptions = {}): WsTicketStore {\n const ttlMs = options.ttlMs ?? 5 * 60_000;\n const maxOutstanding = options.maxOutstanding ?? 1_000;\n const now = options.now ?? Date.now;\n const random = options.randomBytes ?? ((n: number) => randomBytes(n));\n const entries = new Map<string, InternalEntry>();\n\n function evictOldest(): void {\n const oldest = entries.keys().next().value;\n if (oldest !== undefined) entries.delete(oldest);\n }\n\n function prune(): number {\n const cutoff = now();\n let removed = 0;\n for (const [value, entry] of entries) {\n if (entry.ticket.expiresAt <= cutoff) {\n entries.delete(value);\n removed += 1;\n }\n }\n return removed;\n }\n\n function issue(input: {\n readonly tokenId: string;\n readonly scopes: ReadonlyArray<ParsedScope>;\n }): WsTicket {\n prune();\n while (entries.size >= maxOutstanding) {\n evictOldest();\n }\n const issuedAt = now();\n const value = encodeTicket(random(24));\n const ticket: WsTicket = {\n value,\n issuedAt,\n expiresAt: issuedAt + ttlMs,\n tokenId: input.tokenId,\n scopes: Object.freeze(input.scopes.slice()),\n };\n entries.set(value, { ticket, consumed: false });\n return ticket;\n }\n\n function consume(value: string): WsTicketConsumeResult {\n const entry = entries.get(value);\n if (entry === undefined) {\n prune();\n return { ok: false, reason: 'unknown' };\n }\n if (entry.ticket.expiresAt <= now()) {\n entries.delete(value);\n prune();\n return { ok: false, reason: 'expired' };\n }\n if (entry.consumed) {\n prune();\n return { ok: false, reason: 'consumed' };\n }\n entry.consumed = true;\n prune();\n return { ok: true, ticket: entry.ticket };\n }\n\n return Object.freeze({\n ttlMs,\n issue,\n consume,\n prune,\n size: () => entries.size,\n });\n}\n\nfunction encodeTicket(bytes: Uint8Array): string {\n // URL-safe base64 — Buffer.from is available in every supported\n // Node runtime; the implementation never touches the value once\n // encoded so the choice is purely cosmetic.\n return Buffer.from(bytes)\n .toString('base64')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/g, '');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0FA,SAAgB,oBAAoB,UAAgC,EAAE,EAAiB;CACrF,MAAM,QAAQ,QAAQ,SAAS,IAAI;CACnC,MAAM,iBAAiB,QAAQ,kBAAkB;CACjD,MAAM,MAAM,QAAQ,OAAO,KAAK;CAChC,MAAM,SAAS,QAAQ,iBAAiB,MAAc,YAAY,EAAE;CACpE,MAAM,0BAAU,IAAI,KAA4B;CAEhD,SAAS,cAAoB;EAC3B,MAAM,SAAS,QAAQ,MAAM,CAAC,MAAM,CAAC;AACrC,MAAI,WAAW,OAAW,SAAQ,OAAO,OAAO;;CAGlD,SAAS,QAAgB;EACvB,MAAM,SAAS,KAAK;EACpB,IAAI,UAAU;AACd,OAAK,MAAM,CAAC,OAAO,UAAU,QAC3B,KAAI,MAAM,OAAO,aAAa,QAAQ;AACpC,WAAQ,OAAO,MAAM;AACrB,cAAW;;AAGf,SAAO;;CAGT,SAAS,MAAM,OAGF;AACX,SAAO;AACP,SAAO,QAAQ,QAAQ,eACrB,cAAa;EAEf,MAAM,WAAW,KAAK;EACtB,MAAM,QAAQ,aAAa,OAAO,GAAG,CAAC;EACtC,MAAMA,SAAmB;GACvB;GACA;GACA,WAAW,WAAW;GACtB,SAAS,MAAM;GACf,QAAQ,OAAO,OAAO,MAAM,OAAO,OAAO,CAAC;GAC5C;AACD,UAAQ,IAAI,OAAO;GAAE;GAAQ,UAAU;GAAO,CAAC;AAC/C,SAAO;;CAGT,SAAS,QAAQ,OAAsC;EACrD,MAAM,QAAQ,QAAQ,IAAI,MAAM;AAChC,MAAI,UAAU,QAAW;AACvB,UAAO;AACP,UAAO;IAAE,IAAI;IAAO,QAAQ;IAAW;;AAEzC,MAAI,MAAM,OAAO,aAAa,KAAK,EAAE;AACnC,WAAQ,OAAO,MAAM;AACrB,UAAO;AACP,UAAO;IAAE,IAAI;IAAO,QAAQ;IAAW;;AAEzC,MAAI,MAAM,UAAU;AAClB,UAAO;AACP,UAAO;IAAE,IAAI;IAAO,QAAQ;IAAY;;AAE1C,QAAM,WAAW;AACjB,SAAO;AACP,SAAO;GAAE,IAAI;GAAM,QAAQ,MAAM;GAAQ;;AAG3C,QAAO,OAAO,OAAO;EACnB;EACA;EACA;EACA;EACA,YAAY,QAAQ;EACrB,CAAC;;AAGJ,SAAS,aAAa,OAA2B;AAI/C,QAAO,OAAO,KAAK,MAAM,CACtB,SAAS,SAAS,CAClB,QAAQ,OAAO,IAAI,CACnB,QAAQ,OAAO,IAAI,CACnB,QAAQ,QAAQ,GAAG"}
@@ -0,0 +1,63 @@
1
+ import { ServerVariables } from "../internal/context.js";
2
+ import { WsDispatcher } from "./dispatcher.js";
3
+ import { RunStateTracker } from "../runtime/run-state.js";
4
+ import { WsTicketStore } from "./ticket.js";
5
+ import { Context } from "hono";
6
+ import { TokenVerifier } from "@graphorin/security/auth";
7
+ import { WSEvents } from "hono/ws";
8
+
9
+ //#region src/ws/upgrade.d.ts
10
+
11
+ /**
12
+ * Public configuration accepted by {@link createWsUpgradeEvents}.
13
+ *
14
+ * @stable
15
+ */
16
+ interface WsUpgradeOptions {
17
+ readonly dispatcher: WsDispatcher;
18
+ readonly tickets: WsTicketStore;
19
+ /**
20
+ * Token verifier for bearer / ticket upgrades. Optional only in the
21
+ * IP-13 no-auth loopback mode (`anonymous: true`), where there is no
22
+ * verifier to construct and every upgrade is accepted.
23
+ */
24
+ readonly verifier?: TokenVerifier;
25
+ /**
26
+ * IP-13: authentication is disabled server-wide (`auth.kind='none'`).
27
+ * Accept the upgrade unconditionally with a full (`admin:*`) scope grant
28
+ * instead of silently refusing to mount the WS route. Trusted-loopback
29
+ * / single-operator mode only.
30
+ */
31
+ readonly anonymous?: boolean;
32
+ readonly runs?: RunStateTracker;
33
+ readonly now?: () => number;
34
+ /**
35
+ * Subprotocol the server advertises. Defaults to
36
+ * {@link SUBPROTOCOL_NAME}; tests can override to exercise the
37
+ * mismatch path.
38
+ */
39
+ readonly serverSubprotocol?: string;
40
+ readonly newSubscriptionId?: () => string;
41
+ readonly newSubscriberId?: () => string;
42
+ }
43
+ /**
44
+ * Build the `WSEvents` callback bag the Hono helper consumes. The
45
+ * function takes the request `Context` so the upgrade can read the
46
+ * `Authorization` header / `Sec-WebSocket-Protocol` ticket directly.
47
+ *
48
+ * Production wiring on `@hono/node-ws`:
49
+ *
50
+ * ```ts
51
+ * const { upgradeWebSocket, injectWebSocket } = createNodeWebSocket({ app });
52
+ * app.get('/v1/ws', upgradeWebSocket((c) => createWsUpgradeEvents(c, deps)));
53
+ * injectWebSocket(serve(...));
54
+ * ```
55
+ *
56
+ * @stable
57
+ */
58
+ declare function createWsUpgradeEvents(c: Context<{
59
+ Variables: ServerVariables;
60
+ }>, options: WsUpgradeOptions): Promise<WSEvents>;
61
+ //#endregion
62
+ export { WsUpgradeOptions, createWsUpgradeEvents };
63
+ //# sourceMappingURL=upgrade.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upgrade.d.ts","names":[],"sources":["../../src/ws/upgrade.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;UA0DiB,gBAAA;uBACM;oBACH;;;;;;sBAME;;;;;;;;kBAQJ;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2BI,qBAAA,IACjB;aAAqB;aACf,mBACR,QAAQ"}