@danypops/jittor 0.10.0 → 0.11.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/README.md +8 -5
- package/extension/src/benchmark-tui.ts +4 -0
- package/extension/src/capabilities/codex-recovery.ts +127 -0
- package/extension/src/capabilities/http-headers.ts +5 -0
- package/extension/src/capabilities/local-run-telemetry.ts +104 -0
- package/extension/src/capabilities/provider-response-telemetry.ts +96 -0
- package/extension/src/footer.ts +10 -53
- package/extension/src/index.ts +92 -272
- package/extension/src/session-identity.ts +20 -0
- package/extension/src/tui.ts +20 -11
- package/package.json +1 -1
- package/src/adapters/sqlite-metric-store.ts +11 -2
- package/src/adapters/sqlite-session-identity-store.ts +45 -0
- package/src/cli-commands/benchmarks.ts +140 -0
- package/src/cli-commands/compaction.ts +17 -0
- package/src/cli-commands/context.ts +49 -0
- package/src/cli-commands/metrics.ts +296 -0
- package/src/cli-commands/op.ts +40 -0
- package/src/cli-commands/route-args.ts +15 -0
- package/src/cli-commands/router.ts +207 -0
- package/src/cli-commands/service-daemon.ts +72 -0
- package/src/cli-commands/session.ts +42 -0
- package/src/cli-commands/support.ts +33 -0
- package/src/cli.ts +42 -769
- package/src/constants.ts +7 -0
- package/src/daemon.ts +13 -3
- package/src/db.ts +15 -1
- package/src/operations/benchmark-operations.ts +12 -0
- package/src/operations/context-operations.ts +30 -0
- package/src/operations/metrics-operations.ts +77 -0
- package/src/operations/model-ranking-operations.ts +16 -0
- package/src/operations/router-operations.ts +19 -0
- package/src/operations/session-identity-operations.ts +15 -0
- package/src/operations/session-scope.ts +31 -0
- package/src/operations/types.ts +3 -0
- package/src/ports/metric-store.ts +2 -0
- package/src/ports/router-controller.ts +9 -9
- package/src/ports/session-identity-store.ts +5 -0
- package/src/providers/telemetry-sources.ts +2 -1
- package/src/router.ts +124 -67
- package/src/service.ts +60 -118
- package/src/session-identity-service.ts +55 -0
package/src/service.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { errorResponse, healthResponse, readyResponse, requireBearerToken } from "@danypops/daemon-kit/http";
|
|
2
|
-
import {
|
|
2
|
+
import { SERVICE_MAX_BODY_BYTES, SERVICE_MAX_RESPONSE_BYTES } from "./constants.ts";
|
|
3
|
+
import { InvalidSessionSecretError, SessionIdentity, type RegisterSessionIdentityResult } from "./session-identity-service.ts";
|
|
3
4
|
import { VERSION } from "./version.ts";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import type { MetricObservation, MetricQuery, StoredMetricObservation } from "./domain/metric.ts";
|
|
6
|
+
import type { CompactionDurationEstimate, ContextAssessment } from "./domain/context-telemetry.ts";
|
|
7
|
+
import type { TaskCostSummary } from "./domain/task-cost.ts";
|
|
7
8
|
import type { UsageAggregateRow } from "./domain/usage.ts";
|
|
8
9
|
import type { BenchmarkQuery, BenchmarkQueryResult, BenchmarkRefreshResult } from "./domain/benchmark.ts";
|
|
9
10
|
import type { ModelRanker, ModelRecommendationInput } from "./domain/model-ranking-service.ts";
|
|
@@ -12,9 +13,18 @@ import type { BenchmarkController } from "./ports/benchmark-controller.ts";
|
|
|
12
13
|
import type { MetricStore } from "./ports/metric-store.ts";
|
|
13
14
|
import type { RouteOverride, RouterController, RouterStatus, TelemetryPollResult } from "./ports/router-controller.ts";
|
|
14
15
|
import type { PolicyDecision, Route } from "./policy.ts";
|
|
16
|
+
import { metricsOperations } from "./operations/metrics-operations.ts";
|
|
17
|
+
import { benchmarkOperations } from "./operations/benchmark-operations.ts";
|
|
18
|
+
import { contextOperations } from "./operations/context-operations.ts";
|
|
19
|
+
import { routerOperations } from "./operations/router-operations.ts";
|
|
20
|
+
import { modelRankingOperations } from "./operations/model-ranking-operations.ts";
|
|
21
|
+
import { sessionIdentityOperations } from "./operations/session-identity-operations.ts";
|
|
22
|
+
import { routerMutationAuthorizer } from "./operations/session-scope.ts";
|
|
23
|
+
import type { OperationHandlerMap } from "./operations/types.ts";
|
|
15
24
|
|
|
16
25
|
export const EXPECTED_OPERATION_NAMES = [
|
|
17
26
|
"metrics.record",
|
|
27
|
+
"metrics.record_batch",
|
|
18
28
|
"metrics.query",
|
|
19
29
|
"metrics.distinct_scopes",
|
|
20
30
|
"metrics.usage_series",
|
|
@@ -23,6 +33,8 @@ export const EXPECTED_OPERATION_NAMES = [
|
|
|
23
33
|
"benchmark.refresh",
|
|
24
34
|
"benchmark.status",
|
|
25
35
|
"benchmark.query",
|
|
36
|
+
"session.register",
|
|
37
|
+
"session.release",
|
|
26
38
|
"models.rank",
|
|
27
39
|
"context.assess",
|
|
28
40
|
"compaction.estimate",
|
|
@@ -39,8 +51,12 @@ export const EXPECTED_OPERATION_NAMES = [
|
|
|
39
51
|
] as const;
|
|
40
52
|
|
|
41
53
|
export type OperationName = typeof EXPECTED_OPERATION_NAMES[number];
|
|
54
|
+
interface RouterScopeInput { session_id?: string; session_secret?: string }
|
|
42
55
|
export interface OperationInputs {
|
|
56
|
+
"session.register": { session_id: string };
|
|
57
|
+
"session.release": { session_id: string; session_secret?: string };
|
|
43
58
|
"metrics.record": MetricObservation;
|
|
59
|
+
"metrics.record_batch": { observations: MetricObservation[] };
|
|
44
60
|
"metrics.query": MetricQuery;
|
|
45
61
|
"metrics.distinct_scopes": { source: string; since: number; until: number; limit?: number };
|
|
46
62
|
"metrics.usage_series": { source: string; since: number; until: number; bucketSizeMs: number; bucketCount: number; scopeLimit?: number };
|
|
@@ -49,22 +65,25 @@ export interface OperationInputs {
|
|
|
49
65
|
"benchmark.refresh": { force?: boolean };
|
|
50
66
|
"benchmark.status": Record<string, never>;
|
|
51
67
|
"benchmark.query": BenchmarkQuery;
|
|
52
|
-
"models.rank": ModelRecommendationInput;
|
|
68
|
+
"models.rank": ModelRecommendationInput & RouterScopeInput;
|
|
53
69
|
"context.assess": { since?: number; until?: number };
|
|
54
70
|
"compaction.estimate": Record<string, never>;
|
|
55
71
|
"service.checkpoint": Record<string, never>;
|
|
56
72
|
"telemetry.poll": Record<string, never>;
|
|
57
|
-
"router.status":
|
|
58
|
-
"router.decide":
|
|
59
|
-
"router.pause":
|
|
60
|
-
"router.resume":
|
|
61
|
-
"router.override": RouteOverride;
|
|
62
|
-
"router.clear_override":
|
|
63
|
-
"router.current_route": Route;
|
|
64
|
-
"router.available_routes": { routes: Route[] };
|
|
73
|
+
"router.status": RouterScopeInput;
|
|
74
|
+
"router.decide": RouterScopeInput;
|
|
75
|
+
"router.pause": RouterScopeInput;
|
|
76
|
+
"router.resume": RouterScopeInput;
|
|
77
|
+
"router.override": RouteOverride & RouterScopeInput;
|
|
78
|
+
"router.clear_override": RouterScopeInput;
|
|
79
|
+
"router.current_route": Route & RouterScopeInput;
|
|
80
|
+
"router.available_routes": { routes: Route[] } & RouterScopeInput;
|
|
65
81
|
}
|
|
66
82
|
export interface OperationOutputs {
|
|
83
|
+
"session.register": RegisterSessionIdentityResult;
|
|
84
|
+
"session.release": { released: boolean };
|
|
67
85
|
"metrics.record": StoredMetricObservation;
|
|
86
|
+
"metrics.record_batch": StoredMetricObservation[];
|
|
68
87
|
"metrics.query": StoredMetricObservation[];
|
|
69
88
|
"metrics.distinct_scopes": string[];
|
|
70
89
|
"metrics.usage_series": { rows: UsageAggregateRow[]; truncated: boolean };
|
|
@@ -89,6 +108,7 @@ export interface OperationOutputs {
|
|
|
89
108
|
}
|
|
90
109
|
|
|
91
110
|
export class UnknownOperationError extends Error {}
|
|
111
|
+
export { InvalidSessionSecretError };
|
|
92
112
|
|
|
93
113
|
class UnavailableModelRanker implements ModelRanker {
|
|
94
114
|
rank(): ModelRankingResult { throw new Error("model ranking is not configured"); }
|
|
@@ -114,12 +134,30 @@ class UnavailableRouter implements RouterController {
|
|
|
114
134
|
}
|
|
115
135
|
|
|
116
136
|
export class JittorService {
|
|
137
|
+
private readonly router: RouterController;
|
|
138
|
+
private readonly operations: OperationHandlerMap;
|
|
139
|
+
|
|
117
140
|
constructor(
|
|
118
141
|
private readonly metrics: MetricStore,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
142
|
+
router: RouterController = new UnavailableRouter(),
|
|
143
|
+
benchmarks: BenchmarkController = new UnavailableBenchmarkController(),
|
|
144
|
+
modelRanker: ModelRanker = new UnavailableModelRanker(),
|
|
145
|
+
sessionIdentity?: SessionIdentity,
|
|
146
|
+
) {
|
|
147
|
+
this.router = router;
|
|
148
|
+
const authorize = routerMutationAuthorizer(sessionIdentity);
|
|
149
|
+
// Each capability module owns a disjoint, bounded slice of EXPECTED_OPERATION_NAMES and only
|
|
150
|
+
// the collaborators it needs -- adding a new operation domain means adding a new module here,
|
|
151
|
+
// not another switch case in a single responsibility magnet.
|
|
152
|
+
this.operations = {
|
|
153
|
+
...metricsOperations(metrics),
|
|
154
|
+
...benchmarkOperations(benchmarks),
|
|
155
|
+
...contextOperations(metrics),
|
|
156
|
+
...routerOperations(router, authorize),
|
|
157
|
+
...modelRankingOperations(modelRanker, router, authorize),
|
|
158
|
+
...sessionIdentityOperations(sessionIdentity),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
123
161
|
|
|
124
162
|
operationNames(): OperationName[] {
|
|
125
163
|
return [...EXPECTED_OPERATION_NAMES];
|
|
@@ -128,110 +166,13 @@ export class JittorService {
|
|
|
128
166
|
async execute<Name extends OperationName>(operation: Name, input: OperationInputs[Name]): Promise<OperationOutputs[Name]>;
|
|
129
167
|
async execute(operation: string, input: Record<string, unknown>): Promise<unknown>;
|
|
130
168
|
async execute(operation: string, input: Record<string, unknown> = {}): Promise<unknown> {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
case "metrics.distinct_scopes": {
|
|
135
|
-
const source = input["source"];
|
|
136
|
-
const since = input["since"];
|
|
137
|
-
const until = input["until"];
|
|
138
|
-
if (typeof source !== "string" || source.length === 0) throw new Error("source is required");
|
|
139
|
-
if (!Number.isSafeInteger(since) || !Number.isSafeInteger(until) || (since as number) < 0 || (until as number) < (since as number)) {
|
|
140
|
-
throw new Error("distinct scopes requires non-negative ordered integer bounds");
|
|
141
|
-
}
|
|
142
|
-
const requestedLimit = input["limit"];
|
|
143
|
-
const limit = Number.isFinite(requestedLimit) ? Math.max(1, Math.min(USAGE_MAX_DISTINCT_SCOPES, Math.floor(requestedLimit as number))) : USAGE_MAX_DISTINCT_SCOPES;
|
|
144
|
-
return this.metrics.distinctScopes({ source, since: since as number, until: until as number, limit });
|
|
145
|
-
}
|
|
146
|
-
case "metrics.usage_series": {
|
|
147
|
-
const source = input["source"];
|
|
148
|
-
const since = input["since"];
|
|
149
|
-
const until = input["until"];
|
|
150
|
-
const bucketSizeMs = input["bucketSizeMs"];
|
|
151
|
-
const bucketCount = input["bucketCount"];
|
|
152
|
-
if (typeof source !== "string" || source.length === 0) throw new Error("source is required");
|
|
153
|
-
if (!Number.isSafeInteger(since) || !Number.isSafeInteger(until) || (since as number) < 0 || (until as number) < (since as number)) {
|
|
154
|
-
throw new Error("usage series requires non-negative ordered integer bounds");
|
|
155
|
-
}
|
|
156
|
-
if (typeof bucketSizeMs !== "number" || !Number.isFinite(bucketSizeMs) || bucketSizeMs <= 0) throw new Error("bucketSizeMs must be a positive number");
|
|
157
|
-
if (!Number.isInteger(bucketCount) || (bucketCount as number) <= 0 || (bucketCount as number) > MAX_USAGE_BUCKETS) {
|
|
158
|
-
throw new Error(`bucketCount must be a positive integer up to ${MAX_USAGE_BUCKETS}`);
|
|
159
|
-
}
|
|
160
|
-
const requestedScopeLimit = input["scopeLimit"];
|
|
161
|
-
const scopeLimit = Number.isFinite(requestedScopeLimit) ? Math.max(1, Math.min(USAGE_MAX_DISTINCT_SCOPES, Math.floor(requestedScopeLimit as number))) : USAGE_MAX_DISTINCT_SCOPES;
|
|
162
|
-
const scopes = this.metrics.distinctScopes({ source, since: since as number, until: until as number, limit: scopeLimit });
|
|
163
|
-
// More distinct scopes may exist beyond this bounded list -- that is the only remaining
|
|
164
|
-
// truncation risk once aggregation replaces a per-scope raw-row fetch (see aggregateUsage's
|
|
165
|
-
// own doc comment for the incident this was built to stop repeating).
|
|
166
|
-
const truncated = scopes.length >= scopeLimit;
|
|
167
|
-
const rows = scopes.length === 0 ? [] : this.metrics.aggregateUsage({
|
|
168
|
-
source, scopes, since: since as number, until: until as number, bucketSizeMs, bucketCount: bucketCount as number,
|
|
169
|
-
});
|
|
170
|
-
return { rows, truncated };
|
|
171
|
-
}
|
|
172
|
-
case "metrics.cost_by_task": {
|
|
173
|
-
const since = input["since"];
|
|
174
|
-
const until = input["until"];
|
|
175
|
-
if (!Number.isSafeInteger(since) || !Number.isSafeInteger(until) || (since as number) < 0 || (until as number) < (since as number)) {
|
|
176
|
-
throw new Error("cost by task requires non-negative ordered integer bounds");
|
|
177
|
-
}
|
|
178
|
-
const rows = this.metrics.query({ source: "pi", since: since as number, until: until as number, order: "desc", limit: TASK_COST_QUERY_LIMIT });
|
|
179
|
-
return buildTaskCostSummary(rows, { since: since as number, until: until as number, truncated: rows.length >= TASK_COST_QUERY_LIMIT });
|
|
180
|
-
}
|
|
181
|
-
case "metrics.prune": {
|
|
182
|
-
const before = input["before"];
|
|
183
|
-
if (typeof before !== "number") throw new Error("before is required");
|
|
184
|
-
const force = input["force"] === true;
|
|
185
|
-
const minCutoff = Date.now() - PRUNE_MIN_AGE_MS;
|
|
186
|
-
if (!force && before > minCutoff) {
|
|
187
|
-
throw new Error(`refusing to prune metrics newer than ${new Date(minCutoff).toISOString()} without force: true (this looked like it could delete recent or live data)`);
|
|
188
|
-
}
|
|
189
|
-
return { deleted: this.metrics.pruneBefore(before) };
|
|
190
|
-
}
|
|
191
|
-
case "benchmark.refresh": return this.benchmarks.refresh(input["force"] === true);
|
|
192
|
-
case "benchmark.status": return this.benchmarks.status();
|
|
193
|
-
case "benchmark.query": return this.benchmarks.query(input as unknown as BenchmarkQuery);
|
|
194
|
-
case "models.rank": {
|
|
195
|
-
const result = this.modelRanker.rank(input as unknown as ModelRecommendationInput);
|
|
196
|
-
if (result.automaticSelection && this.router.applyModelRanking) this.router.applyModelRanking(result.ranked.map((item) => item.candidate));
|
|
197
|
-
return result;
|
|
198
|
-
}
|
|
199
|
-
case "context.assess": {
|
|
200
|
-
const until = input["until"] === undefined ? Date.now() : input["until"];
|
|
201
|
-
const since = input["since"] === undefined && typeof until === "number" ? Math.max(0, until - CONTEXT_ASSESSMENT_DEFAULT_WINDOW_MS) : input["since"];
|
|
202
|
-
if (!Number.isSafeInteger(since) || !Number.isSafeInteger(until) || (since as number) < 0 || (until as number) < (since as number)) throw new Error("context assessment requires non-negative ordered integer bounds");
|
|
203
|
-
const query = { since: since as number, until: until as number, order: "asc" as const, limit: CONTEXT_ASSESSMENT_QUERY_LIMIT };
|
|
204
|
-
const injections = this.metrics.query({ ...query, source: "papyrus-context", metric: "injected-characters" });
|
|
205
|
-
const compactions = this.metrics.query({ ...query, source: "pi-context" });
|
|
206
|
-
return assessContextTelemetry(injections, compactions, {
|
|
207
|
-
since: since as number,
|
|
208
|
-
until: until as number,
|
|
209
|
-
truncated: injections.length >= CONTEXT_ASSESSMENT_QUERY_LIMIT || compactions.length >= CONTEXT_ASSESSMENT_QUERY_LIMIT,
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
case "compaction.estimate": {
|
|
213
|
-
const rows = this.metrics.query({
|
|
214
|
-
source: "pi-context", scope: "compaction", metric: "compaction-duration",
|
|
215
|
-
order: "desc", limit: COMPACTION_DURATION_ESTIMATE_MAX_SAMPLES,
|
|
216
|
-
});
|
|
217
|
-
return estimateCompactionDuration(rows);
|
|
218
|
-
}
|
|
219
|
-
case "service.checkpoint": this.metrics.checkpoint(); return { ok: true };
|
|
220
|
-
case "telemetry.poll": return this.router.poll();
|
|
221
|
-
case "router.status": return this.router.status();
|
|
222
|
-
case "router.decide": return this.router.decide();
|
|
223
|
-
case "router.pause": return this.router.pause();
|
|
224
|
-
case "router.resume": return this.router.resume();
|
|
225
|
-
case "router.override": return this.router.setOverride(input as unknown as RouteOverride);
|
|
226
|
-
case "router.clear_override": return this.router.clearOverride();
|
|
227
|
-
case "router.current_route": return this.router.setCurrentRoute(input as unknown as Route);
|
|
228
|
-
case "router.available_routes": return this.router.setAvailableRoutes(Array.isArray(input["routes"]) ? input["routes"] as Route[] : []);
|
|
229
|
-
default: throw new UnknownOperationError(`unknown operation: ${operation}`);
|
|
230
|
-
}
|
|
169
|
+
const handler = this.operations[operation];
|
|
170
|
+
if (!handler) throw new UnknownOperationError(`unknown operation: ${operation}`);
|
|
171
|
+
return handler(input);
|
|
231
172
|
}
|
|
232
173
|
|
|
233
174
|
ready(): boolean {
|
|
234
|
-
return this.router.status().ready;
|
|
175
|
+
return this.router.status(undefined).ready;
|
|
235
176
|
}
|
|
236
177
|
|
|
237
178
|
close(): void {
|
|
@@ -284,6 +225,7 @@ export function createApp(options: JittorAppOptions): { fetch(request: Request):
|
|
|
284
225
|
return json({ result: await options.service.execute(body.op, input) });
|
|
285
226
|
} catch (error) {
|
|
286
227
|
if (error instanceof UnknownOperationError) return json({ error: error.message }, 404);
|
|
228
|
+
if (error instanceof InvalidSessionSecretError) return json({ error: error.message }, 403);
|
|
287
229
|
return json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
288
230
|
}
|
|
289
231
|
},
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { isSessionRegistered, registerSessionIdentity, releaseSessionIdentity, verifySessionSecret } from "@danypops/daemon-kit/session-identity";
|
|
2
|
+
import type { SessionIdentityStore } from "./ports/session-identity-store.ts";
|
|
3
|
+
|
|
4
|
+
export interface RegisterSessionIdentityResult {
|
|
5
|
+
sessionId: string;
|
|
6
|
+
secret: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Thrown when a session_id has a registered identity but the caller did not present a matching session_secret. Mapped to HTTP 403 in service.ts, separate from generic validation's 400. */
|
|
10
|
+
export class InvalidSessionSecretError extends Error {}
|
|
11
|
+
|
|
12
|
+
function assertValidSessionId(sessionId: string): void {
|
|
13
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) throw new Error("session_id is required");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Wraps daemon-kit's storage-agnostic session-identity primitive against Jittor's own
|
|
18
|
+
* SQLite-backed store, and enforces it at the one place a caller-supplied session_id is
|
|
19
|
+
* behavior-affecting: mutating router.* operations (see ports/router-controller.ts).
|
|
20
|
+
*
|
|
21
|
+
* Opt-in armor, not a breaking migration: a session_id that was never registered mutates
|
|
22
|
+
* exactly as before (undefined sessionId included, since that maps to the router's legacy
|
|
23
|
+
* "global" scope). Only once a sessionId is registered does a matching session_secret become
|
|
24
|
+
* mandatory. Every real Pi session becomes armored automatically once its extension fires
|
|
25
|
+
* session_start and registers.
|
|
26
|
+
*/
|
|
27
|
+
export class SessionIdentity {
|
|
28
|
+
constructor(private readonly store: SessionIdentityStore) {}
|
|
29
|
+
|
|
30
|
+
register(sessionId: string): RegisterSessionIdentityResult {
|
|
31
|
+
assertValidSessionId(sessionId);
|
|
32
|
+
return registerSessionIdentity(this.store, sessionId);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
release(sessionId: string, secret: string | undefined): { released: boolean } {
|
|
36
|
+
assertValidSessionId(sessionId);
|
|
37
|
+
const wasRegistered = isSessionRegistered(this.store, sessionId);
|
|
38
|
+
releaseSessionIdentity(this.store, sessionId, secret);
|
|
39
|
+
return { released: wasRegistered && !isSessionRegistered(this.store, sessionId) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
isRegistered(sessionId: string): boolean {
|
|
43
|
+
return isSessionRegistered(this.store, sessionId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
verify(sessionId: string, secret: string | undefined): boolean {
|
|
47
|
+
return verifySessionSecret(this.store, sessionId, secret);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
assertAuthorized(sessionId: string | undefined, secret: string | undefined): void {
|
|
51
|
+
if (sessionId === undefined) return;
|
|
52
|
+
if (!this.isRegistered(sessionId)) return;
|
|
53
|
+
if (!this.verify(sessionId, secret)) throw new InvalidSessionSecretError(`session "${sessionId}" is registered; a valid session_secret is required to mutate its router state`);
|
|
54
|
+
}
|
|
55
|
+
}
|