@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.
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/dist/app.d.ts +174 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +684 -0
- package/dist/app.js.map +1 -0
- package/dist/commentary/audit-bridge.d.ts +57 -0
- package/dist/commentary/audit-bridge.d.ts.map +1 -0
- package/dist/commentary/audit-bridge.js +92 -0
- package/dist/commentary/audit-bridge.js.map +1 -0
- package/dist/commentary/built-in-patterns.d.ts +24 -0
- package/dist/commentary/built-in-patterns.d.ts.map +1 -0
- package/dist/commentary/built-in-patterns.js +62 -0
- package/dist/commentary/built-in-patterns.js.map +1 -0
- package/dist/commentary/index.d.ts +5 -0
- package/dist/commentary/index.js +5 -0
- package/dist/commentary/sanitizer.d.ts +37 -0
- package/dist/commentary/sanitizer.d.ts.map +1 -0
- package/dist/commentary/sanitizer.js +182 -0
- package/dist/commentary/sanitizer.js.map +1 -0
- package/dist/commentary/types.d.ts +120 -0
- package/dist/commentary/types.d.ts.map +1 -0
- package/dist/config.d.ts +760 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +217 -0
- package/dist/config.js.map +1 -0
- package/dist/consolidator/daemon.d.ts +88 -0
- package/dist/consolidator/daemon.d.ts.map +1 -0
- package/dist/consolidator/daemon.js +54 -0
- package/dist/consolidator/daemon.js.map +1 -0
- package/dist/consolidator/index.d.ts +2 -0
- package/dist/consolidator/index.js +3 -0
- package/dist/errors/index.d.ts +104 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +131 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/health/checks.d.ts +120 -0
- package/dist/health/checks.d.ts.map +1 -0
- package/dist/health/checks.js +127 -0
- package/dist/health/checks.js.map +1 -0
- package/dist/health/index.d.ts +3 -0
- package/dist/health/index.js +4 -0
- package/dist/health/routes.d.ts +66 -0
- package/dist/health/routes.d.ts.map +1 -0
- package/dist/health/routes.js +96 -0
- package/dist/health/routes.js.map +1 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/context.d.ts +66 -0
- package/dist/internal/context.d.ts.map +1 -0
- package/dist/internal/ids.js +21 -0
- package/dist/internal/ids.js.map +1 -0
- package/dist/internal/json.js +46 -0
- package/dist/internal/json.js.map +1 -0
- package/dist/lifecycle/hooks.d.ts +57 -0
- package/dist/lifecycle/hooks.d.ts.map +1 -0
- package/dist/lifecycle/index.d.ts +2 -0
- package/dist/lifecycle/index.js +3 -0
- package/dist/lifecycle/pre-bind.d.ts +38 -0
- package/dist/lifecycle/pre-bind.d.ts.map +1 -0
- package/dist/lifecycle/pre-bind.js +62 -0
- package/dist/lifecycle/pre-bind.js.map +1 -0
- package/dist/metrics/catalog.d.ts +34 -0
- package/dist/metrics/catalog.d.ts.map +1 -0
- package/dist/metrics/catalog.js +61 -0
- package/dist/metrics/catalog.js.map +1 -0
- package/dist/metrics/index.d.ts +3 -0
- package/dist/metrics/index.js +4 -0
- package/dist/metrics/registry.d.ts +63 -0
- package/dist/metrics/registry.d.ts.map +1 -0
- package/dist/metrics/registry.js +222 -0
- package/dist/metrics/registry.js.map +1 -0
- package/dist/middleware/audit.d.ts +47 -0
- package/dist/middleware/audit.d.ts.map +1 -0
- package/dist/middleware/audit.js +71 -0
- package/dist/middleware/audit.js.map +1 -0
- package/dist/middleware/auth.d.ts +50 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +164 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/cors.d.ts +15 -0
- package/dist/middleware/cors.d.ts.map +1 -0
- package/dist/middleware/cors.js +45 -0
- package/dist/middleware/cors.js.map +1 -0
- package/dist/middleware/csrf.d.ts +15 -0
- package/dist/middleware/csrf.d.ts.map +1 -0
- package/dist/middleware/csrf.js +77 -0
- package/dist/middleware/csrf.js.map +1 -0
- package/dist/middleware/idempotency.d.ts +41 -0
- package/dist/middleware/idempotency.d.ts.map +1 -0
- package/dist/middleware/idempotency.js +198 -0
- package/dist/middleware/idempotency.js.map +1 -0
- package/dist/middleware/index.d.ts +9 -0
- package/dist/middleware/index.js +10 -0
- package/dist/middleware/rate-limit.d.ts +17 -0
- package/dist/middleware/rate-limit.d.ts.map +1 -0
- package/dist/middleware/rate-limit.js +47 -0
- package/dist/middleware/rate-limit.js.map +1 -0
- package/dist/middleware/request-state.d.ts +27 -0
- package/dist/middleware/request-state.d.ts.map +1 -0
- package/dist/middleware/request-state.js +42 -0
- package/dist/middleware/request-state.js.map +1 -0
- package/dist/middleware/scope.d.ts +24 -0
- package/dist/middleware/scope.d.ts.map +1 -0
- package/dist/middleware/scope.js +50 -0
- package/dist/middleware/scope.js.map +1 -0
- package/dist/registry/index.d.ts +135 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +96 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/replay/index.d.ts +2 -0
- package/dist/replay/index.js +3 -0
- package/dist/replay/routes.d.ts +46 -0
- package/dist/replay/routes.d.ts.map +1 -0
- package/dist/replay/routes.js +189 -0
- package/dist/replay/routes.js.map +1 -0
- package/dist/routes/agents.d.ts +41 -0
- package/dist/routes/agents.d.ts.map +1 -0
- package/dist/routes/agents.js +213 -0
- package/dist/routes/agents.js.map +1 -0
- package/dist/routes/audit.d.ts +57 -0
- package/dist/routes/audit.d.ts.map +1 -0
- package/dist/routes/audit.js +116 -0
- package/dist/routes/audit.js.map +1 -0
- package/dist/routes/auth.d.ts +26 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +54 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/health.d.ts +22 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +33 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/index.d.ts +10 -0
- package/dist/routes/index.js +12 -0
- package/dist/routes/mcp.d.ts +32 -0
- package/dist/routes/mcp.d.ts.map +1 -0
- package/dist/routes/mcp.js +61 -0
- package/dist/routes/mcp.js.map +1 -0
- package/dist/routes/memory.d.ts +141 -0
- package/dist/routes/memory.d.ts.map +1 -0
- package/dist/routes/memory.js +102 -0
- package/dist/routes/memory.js.map +1 -0
- package/dist/routes/sessions.d.ts +54 -0
- package/dist/routes/sessions.d.ts.map +1 -0
- package/dist/routes/sessions.js +135 -0
- package/dist/routes/sessions.js.map +1 -0
- package/dist/routes/skills.d.ts +34 -0
- package/dist/routes/skills.d.ts.map +1 -0
- package/dist/routes/skills.js +54 -0
- package/dist/routes/skills.js.map +1 -0
- package/dist/routes/tokens.d.ts +25 -0
- package/dist/routes/tokens.d.ts.map +1 -0
- package/dist/routes/tokens.js +87 -0
- package/dist/routes/tokens.js.map +1 -0
- package/dist/routes/workflows.d.ts +27 -0
- package/dist/routes/workflows.d.ts.map +1 -0
- package/dist/routes/workflows.js +220 -0
- package/dist/routes/workflows.js.map +1 -0
- package/dist/runtime/run-state.d.ts +158 -0
- package/dist/runtime/run-state.d.ts.map +1 -0
- package/dist/runtime/run-state.js +182 -0
- package/dist/runtime/run-state.js.map +1 -0
- package/dist/sse/events.d.ts +44 -0
- package/dist/sse/events.d.ts.map +1 -0
- package/dist/sse/events.js +200 -0
- package/dist/sse/events.js.map +1 -0
- package/dist/sse/index.d.ts +2 -0
- package/dist/sse/index.js +3 -0
- package/dist/triggers/daemon.d.ts +74 -0
- package/dist/triggers/daemon.d.ts.map +1 -0
- package/dist/triggers/daemon.js +114 -0
- package/dist/triggers/daemon.js.map +1 -0
- package/dist/triggers/index.d.ts +3 -0
- package/dist/triggers/index.js +4 -0
- package/dist/triggers/routes.d.ts +21 -0
- package/dist/triggers/routes.d.ts.map +1 -0
- package/dist/triggers/routes.js +117 -0
- package/dist/triggers/routes.js.map +1 -0
- package/dist/ws/dispatcher.d.ts +169 -0
- package/dist/ws/dispatcher.d.ts.map +1 -0
- package/dist/ws/dispatcher.js +303 -0
- package/dist/ws/dispatcher.js.map +1 -0
- package/dist/ws/index.d.ts +6 -0
- package/dist/ws/index.js +7 -0
- package/dist/ws/replay-buffer.d.ts +47 -0
- package/dist/ws/replay-buffer.d.ts.map +1 -0
- package/dist/ws/replay-buffer.js +88 -0
- package/dist/ws/replay-buffer.js.map +1 -0
- package/dist/ws/subjects.d.ts +71 -0
- package/dist/ws/subjects.d.ts.map +1 -0
- package/dist/ws/subjects.js +112 -0
- package/dist/ws/subjects.js.map +1 -0
- package/dist/ws/ticket.d.ts +74 -0
- package/dist/ws/ticket.d.ts.map +1 -0
- package/dist/ws/ticket.js +112 -0
- package/dist/ws/ticket.js.map +1 -0
- package/dist/ws/upgrade.d.ts +63 -0
- package/dist/ws/upgrade.d.ts.map +1 -0
- package/dist/ws/upgrade.js +324 -0
- package/dist/ws/upgrade.js.map +1 -0
- 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"}
|