@knpkv/jcf-web 0.2.0 → 0.3.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/dist/main.js +7 -5
- package/dist/main.js.map +1 -1
- package/dist/server/Api.d.ts +6 -6
- package/dist/server/HttpApplication.d.ts +2 -1
- package/dist/server/HttpApplication.js +3 -2
- package/dist/server/HttpApplication.js.map +1 -1
- package/dist/server/OwnerSession.d.ts +15 -60
- package/dist/server/OwnerSession.js +17 -209
- package/dist/server/OwnerSession.js.map +1 -1
- package/dist/server/Server.d.ts +4 -3
- package/dist/server/Server.js +4 -3
- package/dist/server/Server.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
package/dist/main.js
CHANGED
|
@@ -5,25 +5,27 @@
|
|
|
5
5
|
* @module
|
|
6
6
|
*/
|
|
7
7
|
import { NodeRuntime, NodeServices } from "@effect/platform-node";
|
|
8
|
+
import { loopbackOrigin } from "@knpkv/browser-pairing/owner-session";
|
|
8
9
|
import { Deferred, Effect, Fiber, Layer, Option } from "effect";
|
|
9
10
|
import * as Stdio from "effect/Stdio";
|
|
10
11
|
import * as Stream from "effect/Stream";
|
|
11
|
-
import {
|
|
12
|
+
import { makeOwnerSession } from "./server/OwnerSession.js";
|
|
12
13
|
import { makeServer, Port, PublicOrigin } from "./server/Server.js";
|
|
13
14
|
const run = Effect.gen(function* () {
|
|
14
15
|
const stdio = yield* Stdio.Stdio;
|
|
15
16
|
const port = yield* Port.pipe(Effect.orDie);
|
|
16
17
|
const configuredOrigin = yield* PublicOrigin.pipe(Effect.orDie);
|
|
17
|
-
const authorityOrigin =
|
|
18
|
+
const authorityOrigin = loopbackOrigin("127.0.0.1", port);
|
|
18
19
|
// One origin decision: the URL that gets printed is the same origin every request is checked
|
|
19
20
|
// against, so a dev proxy in front of the server cannot be advertised and then rejected.
|
|
20
|
-
const security = yield*
|
|
21
|
+
const security = yield* makeOwnerSession(authorityOrigin, Option.getOrUndefined(configuredOrigin)).pipe(Effect.orDie);
|
|
21
22
|
const ready = yield* Deferred.make();
|
|
22
23
|
const server = yield* Layer.launch(makeServer({ port, ready, security })).pipe(Effect.forkChild({ startImmediately: true }));
|
|
23
|
-
|
|
24
|
+
// The server only ends this race by failing; until it is listening there is no link to print.
|
|
25
|
+
const url = yield* Effect.raceFirst(Deferred.await(ready), Fiber.join(server).pipe(Effect.andThen(Effect.never)));
|
|
24
26
|
// On stdout and nowhere else: this line is the credential. It is not logged, so it cannot end up
|
|
25
27
|
// in a log file that outlives the process that minted it.
|
|
26
|
-
yield* Stream.make(`jcf week view: ${
|
|
28
|
+
yield* Stream.make(`jcf week view: ${url}\n`).pipe(Stream.run(stdio.stdout()));
|
|
27
29
|
return yield* Fiber.join(server);
|
|
28
30
|
});
|
|
29
31
|
// Executable entry point: the host platform is provided once for this process.
|
package/dist/main.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AACA;;;;GAIG;AACH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACjE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/D,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AACA;;;;GAIG;AACH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,sCAAsC,CAAA;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/D,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAC3D,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEnE,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAC9B,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,CAAA;IAChC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IAC3C,MAAM,gBAAgB,GAAG,KAAK,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IAC/D,MAAM,eAAe,GAAG,cAAc,CAAC,WAAW,EAAE,IAAI,CAAC,CAAA;IAEzD,6FAA6F;IAC7F,yFAAyF;IACzF,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,gBAAgB,CACtC,eAAe,EACf,MAAM,CAAC,cAAc,CAAC,gBAAgB,CAAC,CACxC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IAEpB,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAU,CAAA;IAC5C,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CAC5E,MAAM,CAAC,SAAS,CAAC,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC,CAC7C,CAAA;IACD,8FAA8F;IAC9F,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IACjH,iGAAiG;IACjG,0DAA0D;IAC1D,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC,IAAI,CAChD,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAC3B,CAAA;IACD,OAAO,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;AAClC,CAAC,CAAC,CAAA;AAEF,+EAA+E;AAC/E,wDAAwD;AACxD,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA"}
|
package/dist/server/Api.d.ts
CHANGED
|
@@ -414,7 +414,11 @@ declare const JcfWebApi_base: HttpApi.HttpApi<"JcfWebApi", HttpApiGroup.HttpApiG
|
|
|
414
414
|
readonly provider: Schema.Literal<"codex">;
|
|
415
415
|
readonly model: Schema.NullOr<Schema.String>;
|
|
416
416
|
readonly effort: Schema.NullOr<Schema.Literals<readonly ["minimal", "low", "medium", "high", "xhigh"]>>;
|
|
417
|
-
}>]>>, never, OwnerSessionAuth, never> | HttpApiEndpoint.HttpApiEndpoint<"
|
|
417
|
+
}>]>>, never, OwnerSessionAuth, never> | HttpApiEndpoint.HttpApiEndpoint<"mine", "POST", "/api/config/mine", never, never, Schema.toCodecJson<Schema.Struct<{
|
|
418
|
+
readonly ticketKey: Schema.String;
|
|
419
|
+
}>>, never, Schema.toCodecJson<Schema.Struct<{
|
|
420
|
+
readonly ownershipOverrides: Schema.$Array<Schema.String>;
|
|
421
|
+
}>>, Schema.toCodecJson<typeof ApiError | typeof ProposalRejectedError>, OwnerSessionAuth, never> | HttpApiEndpoint.HttpApiEndpoint<"saveAgent", "POST", "/api/config/agent", never, never, Schema.toCodecJson<Schema.Union<readonly [Schema.Struct<{
|
|
418
422
|
readonly provider: Schema.Literal<"claude">;
|
|
419
423
|
readonly model: Schema.NullOr<Schema.String>;
|
|
420
424
|
readonly effort: Schema.NullOr<Schema.Literals<readonly ["low", "medium", "high", "xhigh", "max"]>>;
|
|
@@ -430,11 +434,7 @@ declare const JcfWebApi_base: HttpApi.HttpApi<"JcfWebApi", HttpApiGroup.HttpApiG
|
|
|
430
434
|
readonly provider: Schema.Literal<"codex">;
|
|
431
435
|
readonly model: Schema.NullOr<Schema.String>;
|
|
432
436
|
readonly effort: Schema.NullOr<Schema.Literals<readonly ["minimal", "low", "medium", "high", "xhigh"]>>;
|
|
433
|
-
}>]>>, Schema.toCodecJson<typeof ApiError>, OwnerSessionAuth, never> | HttpApiEndpoint.HttpApiEndpoint<"
|
|
434
|
-
readonly ticketKey: Schema.String;
|
|
435
|
-
}>>, never, Schema.toCodecJson<Schema.Struct<{
|
|
436
|
-
readonly ownershipOverrides: Schema.$Array<Schema.String>;
|
|
437
|
-
}>>, Schema.toCodecJson<typeof ApiError | typeof ProposalRejectedError>, OwnerSessionAuth, never> | HttpApiEndpoint.HttpApiEndpoint<"standing", "POST", "/api/config/standing", never, never, Schema.toCodecJson<Schema.Struct<{
|
|
437
|
+
}>]>>, Schema.toCodecJson<typeof ApiError>, OwnerSessionAuth, never> | HttpApiEndpoint.HttpApiEndpoint<"standing", "POST", "/api/config/standing", never, never, Schema.toCodecJson<Schema.Struct<{
|
|
438
438
|
readonly cwd: Schema.String;
|
|
439
439
|
readonly ticketKey: Schema.String;
|
|
440
440
|
}>>, never, Schema.toCodecJson<Schema.Struct<{
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** The same authenticated routes, bootstrap exchange and static client in production and tests. */
|
|
2
|
+
import * as OwnerSession from "@knpkv/browser-pairing/owner-session";
|
|
2
3
|
import { FileSystem, Layer, Path } from "effect";
|
|
3
4
|
import { HttpRouter } from "effect/http";
|
|
4
5
|
/** True only when `candidate` is `root` itself or one of its path-segment descendants. */
|
|
5
6
|
export declare const isWithinDirectory: (path: Path.Path, root: string, candidate: string) => boolean;
|
|
6
7
|
/** Supply engine services and owner secrets; listener and platform belong to the executable. */
|
|
7
|
-
export declare const application: Layer.Layer<never, never, import("@knpkv/jira-clockify/services/ConfigService.js").ConfigService | import("effect/Crypto").Crypto | FileSystem.FileSystem | import("effect/http/Etag").Generator | import("effect/http/HttpPlatform").HttpPlatform | HttpRouter.HttpRouter | import("@knpkv/jira-clockify/services/IssueFacts.js").IssueFacts | import("@knpkv/jira-api-client").JiraApiClient | import("@knpkv/jira-cli/JiraAuth").JiraAuth |
|
|
8
|
+
export declare const application: Layer.Layer<never, never, import("@knpkv/jira-clockify/services/ConfigService.js").ConfigService | import("effect/Crypto").Crypto | FileSystem.FileSystem | import("effect/http/Etag").Generator | import("effect/http/HttpPlatform").HttpPlatform | HttpRouter.HttpRouter | import("@knpkv/jira-clockify/services/IssueFacts.js").IssueFacts | import("@knpkv/jira-api-client").JiraApiClient | import("@knpkv/jira-cli/JiraAuth").JiraAuth | OwnerSession.OwnerSession | Path.Path | import("@knpkv/jira-clockify/services/ReconcileService.js").ReconcileService | HttpRouter.Request<"Error", import("effect/PlatformError").BadArgument> | HttpRouter.Request<"Error", import("effect/http/HttpBody").HttpBodyError> | HttpRouter.Request<"Error", import("effect/PlatformError").PlatformError> | HttpRouter.Request<"Requires", FileSystem.FileSystem> | HttpRouter.Request<"Requires", Path.Path> | import("@knpkv/jira-clockify/services/SavedEntries.js").SavedEntries | import("@knpkv/jira-clockify/services/SessionAttributor.js").SessionAttributor>;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/** The same authenticated routes, bootstrap exchange and static client in production and tests. */
|
|
2
|
+
import * as OwnerSession from "@knpkv/browser-pairing/owner-session";
|
|
2
3
|
import { Effect, FileSystem, Layer, Path } from "effect";
|
|
3
4
|
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
|
|
4
5
|
import { HttpApiBuilder } from "effect/http-api";
|
|
5
6
|
import { JcfWebApi } from "./Api.js";
|
|
6
7
|
import { ConfigLive, EntriesLive, RowsLive, WeekLive } from "./Handlers.js";
|
|
7
|
-
import { ownerSessionAuthLayer
|
|
8
|
+
import { ownerSessionAuthLayer } from "./OwnerSession.js";
|
|
8
9
|
import { layer as weekPlansLayer } from "./WeekPlans.js";
|
|
9
10
|
const mimeTypes = new Map([
|
|
10
11
|
[".css", "text/css"],
|
|
@@ -58,5 +59,5 @@ const serveStatic = Effect.gen(function* () {
|
|
|
58
59
|
const StaticRouter = HttpRouter.use((router) => router.add("GET", "/*", serveStatic));
|
|
59
60
|
const ApiRoutes = HttpApiBuilder.layer(JcfWebApi).pipe(Layer.provide(Layer.mergeAll(WeekLive, RowsLive, ConfigLive, EntriesLive)), Layer.provide(ownerSessionAuthLayer), Layer.provide(weekPlansLayer));
|
|
60
61
|
/** Supply engine services and owner secrets; listener and platform belong to the executable. */
|
|
61
|
-
export const application = Layer.mergeAll(ApiRoutes,
|
|
62
|
+
export const application = Layer.mergeAll(ApiRoutes, OwnerSession.BootstrapRouter, StaticRouter).pipe(Layer.orDie);
|
|
62
63
|
//# sourceMappingURL=HttpApplication.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HttpApplication.js","sourceRoot":"","sources":["../../src/server/HttpApplication.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AACxD,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAC/E,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AACpC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAC3E,OAAO,EAAE,qBAAqB,EAAE,
|
|
1
|
+
{"version":3,"file":"HttpApplication.js","sourceRoot":"","sources":["../../src/server/HttpApplication.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,62 +1,17 @@
|
|
|
1
|
-
import type { CsrfToken, PairingCode, SessionToken } from "@knpkv/browser-pairing/schema";
|
|
2
|
-
import { Context, Effect, Layer, Redacted, Ref, Schema } from "effect";
|
|
3
|
-
import { HttpRouter } from "effect/http";
|
|
4
|
-
import { ForbiddenApiError, OwnerSessionAuth, UnauthorizedApiError } from "./Api.js";
|
|
5
|
-
export interface BootstrapAttemptState {
|
|
6
|
-
readonly failedAttempts: number;
|
|
7
|
-
readonly inFlight: number;
|
|
8
|
-
}
|
|
9
|
-
export interface OwnerSessionSecretsContract {
|
|
10
|
-
/** The origin this server considers itself to be. A request's Host header is never authoritative. */
|
|
11
|
-
readonly authorityOrigin: string;
|
|
12
|
-
/**
|
|
13
|
-
* The origin the browser is expected to present. Same as the authority in production; the Vite
|
|
14
|
-
* dev server when it proxies in front of it, because that is the origin the page runs on.
|
|
15
|
-
*/
|
|
16
|
-
readonly browserOrigin: string;
|
|
17
|
-
readonly bootstrapAvailable: Ref.Ref<boolean>;
|
|
18
|
-
readonly bootstrapAttemptState: Ref.Ref<BootstrapAttemptState>;
|
|
19
|
-
readonly bootstrapExpiresAtMillis: Ref.Ref<number | undefined>;
|
|
20
|
-
readonly bootstrapToken: Redacted.Redacted<PairingCode>;
|
|
21
|
-
readonly csrfToken: Redacted.Redacted<CsrfToken>;
|
|
22
|
-
readonly ownerToken: Redacted.Redacted<SessionToken>;
|
|
23
|
-
}
|
|
24
|
-
declare const OwnerSessionSecrets_base: Context.ServiceClass<OwnerSessionSecrets, "@knpkv/jcf-web/OwnerSessionSecrets", OwnerSessionSecretsContract>;
|
|
25
|
-
export declare class OwnerSessionSecrets extends OwnerSessionSecrets_base {
|
|
26
|
-
}
|
|
27
|
-
declare const UnsafeServerOriginError_base: Schema.Class<UnsafeServerOriginError, Schema.TaggedStruct<"UnsafeServerOriginError", {
|
|
28
|
-
readonly message: Schema.String;
|
|
29
|
-
readonly origin: Schema.String;
|
|
30
|
-
}>, import("effect/Cause").YieldableError>;
|
|
31
|
-
export declare class UnsafeServerOriginError extends UnsafeServerOriginError_base {
|
|
32
|
-
}
|
|
33
|
-
export declare const isLoopbackHostname: (hostname: string) => boolean;
|
|
34
|
-
export declare const ownerSessionOrigin: (hostname: string, port: number) => string;
|
|
35
|
-
export declare const requireLoopbackOrigin: (origin: string) => Effect.Effect<string, UnsafeServerOriginError, never>;
|
|
36
|
-
/** The advertised origin: the bound server, or the dev server that proxies to it. */
|
|
37
|
-
export declare const resolvePublicOrigin: (configuredOrigin: string | undefined, authorityOrigin: string) => Effect.Effect<string, UnsafeServerOriginError, never>;
|
|
38
|
-
export declare const makeOwnerSessionSecrets: (authorityOrigin: string, configuredPublicOrigin?: string | undefined) => Effect.Effect<OwnerSessionSecretsContract, import("@knpkv/browser-pairing").BrowserPairingError | UnsafeServerOriginError, import("effect/Crypto").Crypto>;
|
|
39
|
-
/** Start the bootstrap clock. Called once the server is actually listening, never before. */
|
|
40
|
-
export declare const activateOwnerSessionBootstrap: (secrets: OwnerSessionSecretsContract) => Effect.Effect<void, import("@knpkv/browser-pairing").BrowserPairingError, never>;
|
|
41
1
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
2
|
+
* jcf-web's Owner Session: the shared `@knpkv/browser-pairing/owner-session` policy, with writes.
|
|
3
|
+
*
|
|
4
|
+
* The page saves timesheet entries, so writes need the CSRF token the bootstrap exchange returns. The
|
|
5
|
+
* Vite dev server keeps its own Origin when it proxies, so the configured public origin is also the
|
|
6
|
+
* origin every browser request is checked against: the URL that gets printed cannot be advertised and
|
|
7
|
+
* then rejected.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
44
10
|
*/
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
readonly origin: string | undefined;
|
|
53
|
-
}
|
|
54
|
-
export declare const authorizeOwnerRequest: (request: OwnerRequest, secrets: OwnerSessionSecretsContract) => Effect.Effect<undefined, ForbiddenApiError | UnauthorizedApiError, never>;
|
|
55
|
-
export declare const authorizeBootstrapRequest: (request: {
|
|
56
|
-
readonly authorization: string | undefined;
|
|
57
|
-
readonly origin: string | undefined;
|
|
58
|
-
}, secrets: OwnerSessionSecretsContract) => Effect.Effect<undefined, ForbiddenApiError | UnauthorizedApiError, never>;
|
|
59
|
-
export declare const ownerSessionAuthLayer: Layer.Layer<OwnerSessionAuth, never, OwnerSessionSecrets>;
|
|
60
|
-
/** The one route outside the authenticated API: spending the printed code for a session. */
|
|
61
|
-
export declare const OwnerSessionBootstrapRouter: Layer.Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Error", import("effect/http/HttpBody").HttpBodyError> | HttpRouter.Request<"Requires", OwnerSessionSecrets>>;
|
|
62
|
-
export {};
|
|
11
|
+
import type { BrowserPairingError, CredentialCookieError } from "@knpkv/browser-pairing";
|
|
12
|
+
import * as OwnerSession from "@knpkv/browser-pairing/owner-session";
|
|
13
|
+
import { type Crypto, Effect, Layer } from "effect";
|
|
14
|
+
import { OwnerSessionAuth } from "./Api.js";
|
|
15
|
+
/** Build the session for a server bound at `authorityOrigin`, advertised at `configuredPublicOrigin`. */
|
|
16
|
+
export declare const makeOwnerSession: (authorityOrigin: string, configuredPublicOrigin?: string | undefined) => Effect.Effect<OwnerSession.OwnerSessionService, BrowserPairingError | CredentialCookieError | OwnerSession.UnsafeLoopbackAddressError, Crypto.Crypto>;
|
|
17
|
+
export declare const ownerSessionAuthLayer: Layer.Layer<OwnerSessionAuth, never, OwnerSession.OwnerSession>;
|
|
@@ -1,219 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* **Mental model**
|
|
5
|
-
*
|
|
6
|
-
* - **One process, one operator.** The server binds a loopback address and mints a session token for
|
|
7
|
-
* itself at startup. There is no user table and no login: the person who can read the terminal is
|
|
8
|
-
* the person who gets in, which is exactly the authority a tool that writes to their own timesheet
|
|
9
|
-
* should have.
|
|
10
|
-
* - **The URL is the handshake.** The bootstrap code is printed once, in a fragment the browser
|
|
11
|
-
* never sends upstream, and is spent the first time it is exchanged for the session cookie.
|
|
12
|
-
* - **A read needs the cookie and must not be a browser cross-origin request; a write also needs the
|
|
13
|
-
* expected origin and CSRF header.** Another page in the same browser can make the browser send a
|
|
14
|
-
* cookie, but Fetch Metadata keeps that page from starting agent-backed reads without an Origin.
|
|
15
|
-
*
|
|
16
|
-
* The rules are those of `@knpkv/codecommit-web`'s owner session, over the same
|
|
17
|
-
* `@knpkv/browser-pairing` credential primitives. They are re-stated here rather than imported
|
|
18
|
-
* because that module is private to that application; the shared home for them is a later job than
|
|
19
|
-
* this one, and duplicating the *policy* while sharing the *primitives* is the smaller of the two
|
|
20
|
-
* mistakes available.
|
|
21
|
-
*
|
|
22
|
-
* @module
|
|
23
|
-
*/
|
|
24
|
-
import { credentialValuesEqual, decideOneTimeCredential, expiresAt, issueCsrfToken, issuePairingCode, issueSessionToken, serializeCredentialCookie } from "@knpkv/browser-pairing";
|
|
25
|
-
import { Clock, Context, Effect, Layer, Redacted, Ref, Schema } from "effect";
|
|
26
|
-
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
|
|
1
|
+
import * as OwnerSession from "@knpkv/browser-pairing/owner-session";
|
|
2
|
+
import { Effect, Layer, Redacted } from "effect";
|
|
27
3
|
import { ForbiddenApiError, OwnerSessionAuth, UnauthorizedApiError } from "./Api.js";
|
|
28
|
-
/**
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
*
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
/** Methods that only read. They need the session, but no origin and no CSRF token. */
|
|
38
|
-
const safeMethods = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
39
|
-
/**
|
|
40
|
-
* The one origin other than the bound server that may hold a session: the Vite dev server, which
|
|
41
|
-
* proxies both the API and the bootstrap exchange, so the browser stays on a single origin.
|
|
42
|
-
*/
|
|
43
|
-
const devPublicOrigin = "http://localhost:5173";
|
|
44
|
-
export class OwnerSessionSecrets extends Context.Service()("@knpkv/jcf-web/OwnerSessionSecrets") {
|
|
45
|
-
}
|
|
46
|
-
export class UnsafeServerOriginError extends Schema.TaggedError()("UnsafeServerOriginError", { message: Schema.String, origin: Schema.String }) {
|
|
47
|
-
}
|
|
48
|
-
export const isLoopbackHostname = (hostname) => hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1" || hostname === "[::1]";
|
|
49
|
-
export const ownerSessionOrigin = (hostname, port) => `http://${hostname === "::1" ? "[::1]" : hostname}:${port}`;
|
|
50
|
-
export const requireLoopbackOrigin = Effect.fn("OwnerSession.requireLoopbackOrigin")(function* (origin) {
|
|
51
|
-
const url = yield* Effect.try({
|
|
52
|
-
catch: () => new UnsafeServerOriginError({ message: "jcf-web needs a valid HTTP origin", origin }),
|
|
53
|
-
try: () => new URL(origin)
|
|
4
|
+
/** Build the session for a server bound at `authorityOrigin`, advertised at `configuredPublicOrigin`. */
|
|
5
|
+
export const makeOwnerSession = Effect.fn("JcfWeb.makeOwnerSession")(function* (authorityOrigin, configuredPublicOrigin) {
|
|
6
|
+
return yield* OwnerSession.make({
|
|
7
|
+
authorityOrigin,
|
|
8
|
+
browserOrigin: yield* OwnerSession.resolvePublicOrigin(configuredPublicOrigin, authorityOrigin),
|
|
9
|
+
cookieName: "jcf_owner",
|
|
10
|
+
freshUrlHint: "restart jcf-web for a fresh URL",
|
|
11
|
+
product: "jcf-web",
|
|
12
|
+
writes: "csrf"
|
|
54
13
|
});
|
|
55
|
-
if (url.protocol !== "http:" || !isLoopbackHostname(url.hostname) || url.search !== "" || url.hash !== "") {
|
|
56
|
-
return yield* new UnsafeServerOriginError({
|
|
57
|
-
message: "jcf-web may only be reached over HTTP on a loopback host",
|
|
58
|
-
origin
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
return url.origin;
|
|
62
|
-
});
|
|
63
|
-
/** The advertised origin: the bound server, or the dev server that proxies to it. */
|
|
64
|
-
export const resolvePublicOrigin = Effect.fn("OwnerSession.resolvePublicOrigin")(function* (configuredOrigin, authorityOrigin) {
|
|
65
|
-
const authority = yield* requireLoopbackOrigin(authorityOrigin);
|
|
66
|
-
if (configuredOrigin === undefined)
|
|
67
|
-
return authority;
|
|
68
|
-
const configured = yield* requireLoopbackOrigin(configuredOrigin);
|
|
69
|
-
if (configured !== authority && configured !== devPublicOrigin) {
|
|
70
|
-
return yield* new UnsafeServerOriginError({
|
|
71
|
-
message: "jcf-web's public origin must be the bound server or its dev proxy",
|
|
72
|
-
origin: configuredOrigin
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
return configured;
|
|
76
|
-
});
|
|
77
|
-
export const makeOwnerSessionSecrets = Effect.fn("OwnerSession.makeSecrets")(function* (authorityOrigin, configuredPublicOrigin) {
|
|
78
|
-
const validated = yield* requireLoopbackOrigin(authorityOrigin);
|
|
79
|
-
const browserOrigin = yield* resolvePublicOrigin(configuredPublicOrigin, validated);
|
|
80
|
-
const [ownerToken, csrfToken, bootstrapToken] = yield* Effect.all([
|
|
81
|
-
issueSessionToken(),
|
|
82
|
-
issueCsrfToken(),
|
|
83
|
-
issuePairingCode()
|
|
84
|
-
]);
|
|
85
|
-
return OwnerSessionSecrets.of({
|
|
86
|
-
authorityOrigin: validated,
|
|
87
|
-
bootstrapAttemptState: yield* Ref.make({ failedAttempts: 0, inFlight: 0 }),
|
|
88
|
-
bootstrapAvailable: yield* Ref.make(true),
|
|
89
|
-
bootstrapExpiresAtMillis: yield* Ref.make(undefined),
|
|
90
|
-
bootstrapToken,
|
|
91
|
-
browserOrigin,
|
|
92
|
-
csrfToken,
|
|
93
|
-
ownerToken
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
/** Start the bootstrap clock. Called once the server is actually listening, never before. */
|
|
97
|
-
export const activateOwnerSessionBootstrap = Effect.fn("OwnerSession.activateBootstrap")(function* (secrets) {
|
|
98
|
-
const now = yield* Clock.currentTimeMillis;
|
|
99
|
-
yield* Ref.set(secrets.bootstrapExpiresAtMillis, yield* expiresAt(now, BOOTSTRAP_LIFETIME_MILLIS));
|
|
100
|
-
});
|
|
101
|
-
/**
|
|
102
|
-
* The URL to open. The code rides in the fragment, which browsers do not send to servers and
|
|
103
|
-
* proxies do not log, and the page strips it from the address bar once it has been spent.
|
|
104
|
-
*/
|
|
105
|
-
export const ownerSessionUrl = (publicOrigin, secrets) => `${publicOrigin.replace(/\/+$/u, "")}/#bootstrap_token=${encodeURIComponent(Redacted.value(secrets.bootstrapToken))}`;
|
|
106
|
-
export const ownerSessionCookie = (secrets) => serializeCredentialCookie(secrets.ownerToken, {
|
|
107
|
-
httpOnly: true,
|
|
108
|
-
name: "jcf_owner",
|
|
109
|
-
path: "/api",
|
|
110
|
-
sameSite: "strict",
|
|
111
|
-
secure: false
|
|
112
|
-
});
|
|
113
|
-
const fetchSites = new Set(["cross-site", "none", "same-origin", "same-site"]);
|
|
114
|
-
export const authorizeOwnerRequest = Effect.fn("OwnerSession.authorizeRequest")(function* (request, secrets) {
|
|
115
|
-
if (credentialValuesEqual(request.credential, Redacted.value(secrets.ownerToken)) !== true) {
|
|
116
|
-
return yield* new UnauthorizedApiError({ message: "Missing or invalid owner session" });
|
|
117
|
-
}
|
|
118
|
-
const sameOrigin = request.origin !== undefined && request.origin === secrets.browserOrigin;
|
|
119
|
-
if (request.origin !== undefined && !sameOrigin) {
|
|
120
|
-
return yield* new ForbiddenApiError({ message: "Request origin does not match this jcf-web server" });
|
|
121
|
-
}
|
|
122
|
-
if (request.fetchSite !== undefined && !fetchSites.has(request.fetchSite)) {
|
|
123
|
-
return yield* new ForbiddenApiError({ message: "Request carries invalid browser site metadata" });
|
|
124
|
-
}
|
|
125
|
-
if (request.fetchSite === "cross-site" ||
|
|
126
|
-
(!sameOrigin && (request.fetchSite === "same-site" || request.fetchSite === "none"))) {
|
|
127
|
-
return yield* new ForbiddenApiError({ message: "Browser request is not from this jcf-web page" });
|
|
128
|
-
}
|
|
129
|
-
// An explicit client such as curl carries neither Origin nor Fetch Metadata and remains allowed:
|
|
130
|
-
// it still has to present the process-scoped session cookie. Browser-marked cross-origin reads
|
|
131
|
-
// are rejected before their handler can start provider or agent work.
|
|
132
|
-
if (safeMethods.has(request.method.toUpperCase()))
|
|
133
|
-
return;
|
|
134
|
-
if (!sameOrigin) {
|
|
135
|
-
return yield* new ForbiddenApiError({ message: "Mutation origin does not match this jcf-web server" });
|
|
136
|
-
}
|
|
137
|
-
const csrfMatches = request.csrfToken !== undefined &&
|
|
138
|
-
credentialValuesEqual(request.csrfToken, Redacted.value(secrets.csrfToken)) === true;
|
|
139
|
-
if (!csrfMatches)
|
|
140
|
-
return yield* new ForbiddenApiError({ message: "Missing or invalid CSRF token" });
|
|
141
|
-
});
|
|
142
|
-
const bearerToken = (authorization) => authorization !== undefined && authorization.startsWith("Bearer ")
|
|
143
|
-
? authorization.slice("Bearer ".length)
|
|
144
|
-
: undefined;
|
|
145
|
-
export const authorizeBootstrapRequest = Effect.fn("OwnerSession.authorizeBootstrap")(function* (request, secrets) {
|
|
146
|
-
if (request.origin !== secrets.browserOrigin) {
|
|
147
|
-
return yield* new ForbiddenApiError({ message: "Bootstrap origin does not match this jcf-web server" });
|
|
148
|
-
}
|
|
149
|
-
const supplied = bearerToken(request.authorization);
|
|
150
|
-
// Counted before the code is even compared, so a burst of parallel guesses cannot all pass the
|
|
151
|
-
// check together and then be counted afterwards.
|
|
152
|
-
const admit = Ref.modify(secrets.bootstrapAttemptState, (state) => {
|
|
153
|
-
if (state.failedAttempts + state.inFlight >= MAX_BOOTSTRAP_FAILURES)
|
|
154
|
-
return ["unavailable", state];
|
|
155
|
-
const matches = supplied !== undefined &&
|
|
156
|
-
credentialValuesEqual(supplied, Redacted.value(secrets.bootstrapToken)) === true;
|
|
157
|
-
if (!matches) {
|
|
158
|
-
return ["invalid", {
|
|
159
|
-
failedAttempts: Math.min(MAX_BOOTSTRAP_FAILURES, state.failedAttempts + 1),
|
|
160
|
-
inFlight: state.inFlight
|
|
161
|
-
}];
|
|
162
|
-
}
|
|
163
|
-
return ["accepted", { ...state, inFlight: state.inFlight + 1 }];
|
|
164
|
-
}).pipe(Effect.flatMap((admission) => admission === "unavailable"
|
|
165
|
-
? Effect.fail(new UnauthorizedApiError({ message: "Bootstrap is closed for this process" }))
|
|
166
|
-
: admission === "invalid"
|
|
167
|
-
? Effect.fail(new UnauthorizedApiError({ message: "Missing or invalid bootstrap token" }))
|
|
168
|
-
: Effect.void));
|
|
169
|
-
return yield* Effect.acquireUseRelease(admit, () => Effect.gen(function* () {
|
|
170
|
-
const expiry = yield* Ref.get(secrets.bootstrapExpiresAtMillis);
|
|
171
|
-
if (expiry === undefined)
|
|
172
|
-
return yield* new UnauthorizedApiError({ message: "Bootstrap is not active yet" });
|
|
173
|
-
const now = yield* Clock.currentTimeMillis;
|
|
174
|
-
const decision = yield* Ref.modify(secrets.bootstrapAvailable, (available) => {
|
|
175
|
-
const next = decideOneTimeCredential({ consumedAt: available ? null : 0, expiresAt: expiry, revokedAt: null }, now);
|
|
176
|
-
return [next, next === "accepted" ? false : available];
|
|
177
|
-
});
|
|
178
|
-
if (decision === "accepted")
|
|
179
|
-
return;
|
|
180
|
-
const message = decision === "expired"
|
|
181
|
-
? "Bootstrap token has expired — restart jcf-web for a fresh URL"
|
|
182
|
-
: decision === "consumed"
|
|
183
|
-
? "Bootstrap token has already been used"
|
|
184
|
-
: "Bootstrap token state is invalid";
|
|
185
|
-
return yield* new UnauthorizedApiError({ message });
|
|
186
|
-
}), () => Ref.update(secrets.bootstrapAttemptState, (state) => ({ ...state, inFlight: Math.max(0, state.inFlight - 1) })));
|
|
187
14
|
});
|
|
188
15
|
export const ownerSessionAuthLayer = Layer.effect(OwnerSessionAuth, Effect.gen(function* () {
|
|
189
|
-
const
|
|
16
|
+
const session = yield* OwnerSession.OwnerSession;
|
|
190
17
|
return OwnerSessionAuth.of({
|
|
191
|
-
ownerCookie: Effect.fn("
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
fetchSite: request.headers["sec-fetch-site"],
|
|
197
|
-
method: request.method,
|
|
198
|
-
origin: request.headers.origin
|
|
199
|
-
}, secrets);
|
|
18
|
+
ownerCookie: Effect.fn("JcfWeb.ownerCookie")(function* (httpEffect, { credential }) {
|
|
19
|
+
yield* session.authorizeHttp(Redacted.value(credential)).pipe(Effect.catchTags({
|
|
20
|
+
OwnerSessionForbiddenError: ({ message }) => Effect.fail(new ForbiddenApiError({ message })),
|
|
21
|
+
OwnerSessionUnauthorizedError: ({ message }) => Effect.fail(new UnauthorizedApiError({ message }))
|
|
22
|
+
}));
|
|
200
23
|
return yield* httpEffect;
|
|
201
24
|
})
|
|
202
25
|
});
|
|
203
26
|
}));
|
|
204
|
-
/** The one route outside the authenticated API: spending the printed code for a session. */
|
|
205
|
-
export const OwnerSessionBootstrapRouter = HttpRouter.use((router) => router.add("POST", "/auth/bootstrap", Effect.gen(function* () {
|
|
206
|
-
const secrets = yield* OwnerSessionSecrets;
|
|
207
|
-
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
208
|
-
const result = yield* Effect.result(authorizeBootstrapRequest({ authorization: request.headers.authorization, origin: request.headers.origin }, secrets));
|
|
209
|
-
if (result._tag === "Failure") {
|
|
210
|
-
return HttpServerResponse.text(result.failure.message, {
|
|
211
|
-
status: result.failure._tag === "UnauthorizedApiError" ? 401 : 403
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
return yield* HttpServerResponse.json({ csrfToken: Redacted.value(secrets.csrfToken) }, {
|
|
215
|
-
headers: { "cache-control": "no-store", "set-cookie": ownerSessionCookie(secrets) },
|
|
216
|
-
status: 200
|
|
217
|
-
});
|
|
218
|
-
})));
|
|
219
27
|
//# sourceMappingURL=OwnerSession.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OwnerSession.js","sourceRoot":"","sources":["../../src/server/OwnerSession.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"OwnerSession.js","sourceRoot":"","sources":["../../src/server/OwnerSession.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,YAAY,MAAM,sCAAsC,CAAA;AACpE,OAAO,EAAe,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAA;AAC7D,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAEpF,yGAAyG;AACzG,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC,EAAE,CAAC,yBAAyB,CAAC,CAClE,QAAQ,CAAC,EACP,eAAuB,EACvB,sBAA+B;IAM/B,OAAO,KAAK,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC;QAC9B,eAAe;QACf,aAAa,EAAE,KAAK,CAAC,CAAC,YAAY,CAAC,mBAAmB,CAAC,sBAAsB,EAAE,eAAe,CAAC;QAC/F,UAAU,EAAE,WAAW;QACvB,YAAY,EAAE,iCAAiC;QAC/C,OAAO,EAAE,SAAS;QAClB,MAAM,EAAE,MAAM;KACf,CAAC,CAAA;AACJ,CAAC,CACF,CAAA;AAED,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,CAAC,MAAM,CAC/C,gBAAgB,EAChB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,YAAY,CAAC,YAAY,CAAA;IAChD,OAAO,gBAAgB,CAAC,EAAE,CAAC;QACzB,WAAW,EAAE,MAAM,CAAC,EAAE,CAAC,oBAAoB,CAAC,CAAC,QAAQ,CAAC,EAAC,UAAU,EAAE,EAAE,UAAU,EAAE;YAC/E,KAAK,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAC3D,MAAM,CAAC,SAAS,CAAC;gBACf,0BAA0B,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,iBAAiB,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;gBAC5F,6BAA6B,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,oBAAoB,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;aACnG,CAAC,CACH,CAAA;YACD,OAAO,KAAK,CAAC,CAAC,UAAU,CAAA;QAC1B,CAAC,CAAC;KACH,CAAC,CAAA;AACJ,CAAC,CAAC,CACH,CAAA"}
|
package/dist/server/Server.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import * as OwnerSession from "@knpkv/browser-pairing/owner-session";
|
|
1
2
|
import { Config, Deferred, Layer, Schema } from "effect";
|
|
2
|
-
import { type OwnerSessionSecretsContract } from "./OwnerSession.js";
|
|
3
3
|
export interface JcfWebServerOptions {
|
|
4
4
|
readonly hostname?: string;
|
|
5
5
|
readonly port: number;
|
|
6
|
-
|
|
7
|
-
readonly
|
|
6
|
+
/** Completed with the bootstrap URL once the server is listening. */
|
|
7
|
+
readonly ready?: Deferred.Deferred<string>;
|
|
8
|
+
readonly security: OwnerSession.OwnerSessionService;
|
|
8
9
|
}
|
|
9
10
|
export declare const makeServer: (options: JcfWebServerOptions) => Layer.Layer<never, import("@knpkv/browser-pairing").BrowserPairingError | Config.ConfigError | Schema.SchemaError | import("effect/http/HttpServerError").ServeError, never>;
|
|
10
11
|
/** The port to bind. Deliberately not 3000: the CodeCommit web app already lives there. */
|
package/dist/server/Server.js
CHANGED
|
@@ -13,15 +13,16 @@
|
|
|
13
13
|
* @module
|
|
14
14
|
*/
|
|
15
15
|
import { NodeHttpServer, NodeServices } from "@effect/platform-node";
|
|
16
|
+
import * as OwnerSession from "@knpkv/browser-pairing/owner-session";
|
|
16
17
|
import { Layers } from "@knpkv/jira-clockify";
|
|
17
18
|
import { Config, Deferred, Effect, Layer, Schema } from "effect";
|
|
18
19
|
import { Etag, HttpPlatform, HttpRouter } from "effect/http";
|
|
19
20
|
import { createServer } from "node:http";
|
|
20
21
|
import { application } from "./HttpApplication.js";
|
|
21
|
-
import { activateOwnerSessionBootstrap, OwnerSessionSecrets } from "./OwnerSession.js";
|
|
22
22
|
const HttpPlatformLive = HttpPlatform.layer.pipe(Layer.provide(NodeServices.layer));
|
|
23
|
-
const
|
|
24
|
-
|
|
23
|
+
export const makeServer = (options) => Layer.unwrap(Schema.decodeUnknownEffect(OwnerSession.LoopbackHostname)(options.hostname ?? "127.0.0.1").pipe(Effect.map((hostname) => HttpRouter.serve(application.pipe(Layer.provide(Layers.HeadlessLayer))).pipe(Layer.provide(NodeHttpServer.layerServer(createServer, { host: hostname, port: options.port })), Layer.provide(Etag.layer), Layer.provide(HttpPlatformLive), Layer.provide(NodeServices.layer), Layer.provide(Layer.succeed(OwnerSession.OwnerSession, options.security)), Layer.tap(() =>
|
|
24
|
+
// The startup link: minted only once the server is listening, handed to whoever prints it.
|
|
25
|
+
options.security.mintBootstrapCode.pipe(Effect.map((code) => OwnerSession.bootstrapUrl(options.security.browserOrigin, code)), Effect.flatMap((url) => options.ready === undefined ? Effect.void : Deferred.succeed(options.ready, url))))))));
|
|
25
26
|
/** The port to bind. Deliberately not 3000: the CodeCommit web app already lives there. */
|
|
26
27
|
export const Port = Config.Int("PORT").pipe(Config.withDefault(3111));
|
|
27
28
|
/** Set by `pnpm dev` so the printed URL points at the Vite dev server that proxies here. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Server.js","sourceRoot":"","sources":["../../src/server/Server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACpE,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAC7C,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAChE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"Server.js","sourceRoot":"","sources":["../../src/server/Server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACpE,OAAO,KAAK,YAAY,MAAM,sCAAsC,CAAA;AACpE,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAC7C,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAChE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAElD,MAAM,gBAAgB,GAAG,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAA;AAUnF,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,OAA4B,EAAE,EAAE,CACzD,KAAK,CAAC,MAAM,CACV,MAAM,CAAC,mBAAmB,CAAC,YAAY,CAAC,gBAAgB,CAAC,CAAC,OAAO,CAAC,QAAQ,IAAI,WAAW,CAAC,CAAC,IAAI,CAC7F,MAAM,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CACtB,UAAU,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,IAAI,CAC1E,KAAK,CAAC,OAAO,CACX,cAAc,CAAC,WAAW,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CACjF,EACD,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EACzB,KAAK,CAAC,OAAO,CAAC,gBAAgB,CAAC,EAC/B,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,EACjC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,EACzE,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE;AACb,2FAA2F;AAC3F,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC,IAAI,CACrC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,YAAY,CAAC,OAAO,CAAC,QAAQ,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC,EACrF,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAC1G,CACF,CACF,CACF,CACF,CACF,CAAA;AAEH,2FAA2F;AAC3F,MAAM,CAAC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAA;AAErE,4FAA4F;AAC5F,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,uBAAuB,CAAC,CAAC,CAAA"}
|