@knpkv/agent-usage 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/dist/client/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  4. package/dist/client/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  5. package/dist/client/assets/index-CUuLXODk.css +1 -0
  6. package/dist/client/assets/index-DB-VXWqy.js +55 -0
  7. package/dist/client/index.html +15 -0
  8. package/dist/core/Attribution.d.ts +59 -0
  9. package/dist/core/Attribution.js +108 -0
  10. package/dist/core/Attribution.js.map +1 -0
  11. package/dist/core/Balances.d.ts +27 -0
  12. package/dist/core/Balances.js +63 -0
  13. package/dist/core/Balances.js.map +1 -0
  14. package/dist/core/ClaudeLimits.d.ts +58 -0
  15. package/dist/core/ClaudeLimits.js +125 -0
  16. package/dist/core/ClaudeLimits.js.map +1 -0
  17. package/dist/core/ClaudeLimitsLive.d.ts +18 -0
  18. package/dist/core/ClaudeLimitsLive.js +36 -0
  19. package/dist/core/ClaudeLimitsLive.js.map +1 -0
  20. package/dist/core/ClaudeReader.d.ts +27 -0
  21. package/dist/core/ClaudeReader.js +154 -0
  22. package/dist/core/ClaudeReader.js.map +1 -0
  23. package/dist/core/CodexReader.d.ts +91 -0
  24. package/dist/core/CodexReader.js +365 -0
  25. package/dist/core/CodexReader.js.map +1 -0
  26. package/dist/core/Database.d.ts +4 -0
  27. package/dist/core/Database.js +65 -0
  28. package/dist/core/Database.js.map +1 -0
  29. package/dist/core/Ingest.d.ts +55 -0
  30. package/dist/core/Ingest.js +267 -0
  31. package/dist/core/Ingest.js.map +1 -0
  32. package/dist/core/Model.d.ts +160 -0
  33. package/dist/core/Model.js +125 -0
  34. package/dist/core/Model.js.map +1 -0
  35. package/dist/core/Pricing.d.ts +42 -0
  36. package/dist/core/Pricing.js +126 -0
  37. package/dist/core/Pricing.js.map +1 -0
  38. package/dist/core/Readers.d.ts +42 -0
  39. package/dist/core/Readers.js +18 -0
  40. package/dist/core/Readers.js.map +1 -0
  41. package/dist/core/Report.d.ts +53 -0
  42. package/dist/core/Report.js +270 -0
  43. package/dist/core/Report.js.map +1 -0
  44. package/dist/core/Store.d.ts +94 -0
  45. package/dist/core/Store.js +376 -0
  46. package/dist/core/Store.js.map +1 -0
  47. package/dist/core/Tickets.d.ts +42 -0
  48. package/dist/core/Tickets.js +125 -0
  49. package/dist/core/Tickets.js.map +1 -0
  50. package/dist/index.d.ts +14 -0
  51. package/dist/index.js +15 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/main.d.ts +2 -0
  54. package/dist/main.js +65 -0
  55. package/dist/main.js.map +1 -0
  56. package/dist/server/Api.d.ts +304 -0
  57. package/dist/server/Api.js +20 -0
  58. package/dist/server/Api.js.map +1 -0
  59. package/dist/server/Config.d.ts +27 -0
  60. package/dist/server/Config.js +49 -0
  61. package/dist/server/Config.js.map +1 -0
  62. package/dist/server/Handlers.d.ts +5 -0
  63. package/dist/server/Handlers.js +48 -0
  64. package/dist/server/Handlers.js.map +1 -0
  65. package/dist/server/HttpApplication.d.ts +7 -0
  66. package/dist/server/HttpApplication.js +70 -0
  67. package/dist/server/HttpApplication.js.map +1 -0
  68. package/dist/server/OwnerSession.d.ts +60 -0
  69. package/dist/server/OwnerSession.js +206 -0
  70. package/dist/server/OwnerSession.js.map +1 -0
  71. package/dist/server/Runtime.d.ts +57 -0
  72. package/dist/server/Runtime.js +85 -0
  73. package/dist/server/Runtime.js.map +1 -0
  74. package/dist/server/Server.d.ts +15 -0
  75. package/dist/server/Server.js +39 -0
  76. package/dist/server/Server.js.map +1 -0
  77. package/dist/shared/contracts.d.ts +309 -0
  78. package/dist/shared/contracts.js +105 -0
  79. package/dist/shared/contracts.js.map +1 -0
  80. package/dist/tsconfig.tsbuildinfo +1 -0
  81. package/package.json +68 -0
@@ -0,0 +1,70 @@
1
+ /** The same authenticated routes, bootstrap exchange and static client in production and tests. */
2
+ import { Effect, FileSystem, Layer, Path } from "effect";
3
+ import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
4
+ import { HttpApiBuilder } from "effect/http-api";
5
+ import { AgentUsageApi } from "./Api.js";
6
+ import { UsageLive } from "./Handlers.js";
7
+ import { ownerSessionAuthLayer, OwnerSessionBootstrapRouter } from "./OwnerSession.js";
8
+ const mimeTypes = new Map([
9
+ [".css", "text/css"],
10
+ [".html", "text/html"],
11
+ [".ico", "image/x-icon"],
12
+ [".js", "application/javascript"],
13
+ [".json", "application/json"],
14
+ [".map", "application/json"],
15
+ [".svg", "image/svg+xml"],
16
+ [".woff2", "font/woff2"]
17
+ ]);
18
+ /** True only when `candidate` is `root` itself or one of its path-segment descendants. */
19
+ export const isWithinDirectory = (path, root, candidate) => {
20
+ const relative = path.relative(root, candidate);
21
+ return relative === "" || (!path.isAbsolute(relative) && relative !== ".." &&
22
+ !relative.startsWith(`..${path.sep}`));
23
+ };
24
+ /** The built client, served from the same origin so the session cookie applies to both. */
25
+ const serveStatic = Effect.gen(function* () {
26
+ const request = yield* HttpServerRequest.HttpServerRequest;
27
+ const fileSystem = yield* FileSystem.FileSystem;
28
+ const path = yield* Path.Path;
29
+ const decoded = yield* Effect.result(Effect.try({
30
+ try: () => decodeURIComponent(new URL(request.url, "http://localhost").pathname),
31
+ catch: () => "malformed"
32
+ }));
33
+ // A path whose percent-encoding does not decode is a bad request, not a server failure.
34
+ if (decoded._tag === "Failure")
35
+ return HttpServerResponse.text("Bad Request", { status: 400 });
36
+ const requested = decoded.success.replace(/^\/+/u, "");
37
+ const here = yield* path.fromFileUrl(new URL(".", import.meta.url));
38
+ const staticDirectory = path.resolve(here, "../../dist/client");
39
+ const resolved = path.resolve(staticDirectory, requested === "" ? "index.html" : requested);
40
+ // A resolved path that escaped the directory is a traversal attempt, not a missing file.
41
+ if (!isWithinDirectory(path, staticDirectory, resolved)) {
42
+ return HttpServerResponse.text("Forbidden", { status: 403 });
43
+ }
44
+ if (yield* fileSystem.exists(resolved)) {
45
+ const info = yield* fileSystem.stat(resolved);
46
+ if (info.type === "File") {
47
+ return HttpServerResponse.uint8Array(yield* fileSystem.readFile(resolved), {
48
+ headers: { "content-type": mimeTypes.get(path.extname(resolved)) ?? "application/octet-stream" },
49
+ status: 200
50
+ });
51
+ }
52
+ }
53
+ const index = path.join(staticDirectory, "index.html");
54
+ if (yield* fileSystem.exists(index)) {
55
+ return HttpServerResponse.uint8Array(yield* fileSystem.readFile(index), {
56
+ headers: { "content-type": "text/html" },
57
+ status: 200
58
+ });
59
+ }
60
+ return HttpServerResponse.text("The client has not been built. Run: pnpm --filter @knpkv/agent-usage build", {
61
+ status: 404
62
+ });
63
+ });
64
+ const StaticRouter = HttpRouter.use((router) => router.add("GET", "/*", serveStatic));
65
+ /** Usage, tickets and paths are private to the owner: no API response may be kept by a cache. */
66
+ const NoStore = HttpRouter.middleware((httpEffect) => Effect.map(httpEffect, HttpServerResponse.setHeader("cache-control", "private, no-store")));
67
+ const ApiRoutes = HttpApiBuilder.layer(AgentUsageApi).pipe(Layer.provide(UsageLive), Layer.provide(ownerSessionAuthLayer), Layer.provide(NoStore.layer));
68
+ /** Supply the store, runtime state and owner secrets; listener and platform belong to the executable. */
69
+ export const application = Layer.mergeAll(ApiRoutes, OwnerSessionBootstrapRouter, StaticRouter).pipe(Layer.orDie);
70
+ //# sourceMappingURL=HttpApplication.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HttpApplication.js","sourceRoot":"","sources":["../../src/server/HttpApplication.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ import type { 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 ownerToken: Redacted.Redacted<SessionToken>;
22
+ }
23
+ declare const OwnerSessionSecrets_base: Context.ServiceClass<OwnerSessionSecrets, "@knpkv/agent-usage/OwnerSessionSecrets", OwnerSessionSecretsContract>;
24
+ export declare class OwnerSessionSecrets extends OwnerSessionSecrets_base {
25
+ }
26
+ declare const UnsafeServerOriginError_base: Schema.Class<UnsafeServerOriginError, Schema.TaggedStruct<"UnsafeServerOriginError", {
27
+ readonly message: Schema.String;
28
+ readonly origin: Schema.String;
29
+ }>, import("effect/Cause").YieldableError>;
30
+ export declare class UnsafeServerOriginError extends UnsafeServerOriginError_base {
31
+ }
32
+ export declare const isLoopbackHostname: (hostname: string) => boolean;
33
+ export declare const ownerSessionOrigin: (hostname: string, port: number) => string;
34
+ export declare const requireLoopbackOrigin: (origin: string) => Effect.Effect<string, UnsafeServerOriginError, never>;
35
+ /** The advertised origin: the bound server, or the dev server that proxies to it. */
36
+ export declare const resolvePublicOrigin: (configuredOrigin: string | undefined, authorityOrigin: string) => Effect.Effect<string, UnsafeServerOriginError, never>;
37
+ export declare const makeOwnerSessionSecrets: (authorityOrigin: string, configuredPublicOrigin?: string | undefined) => Effect.Effect<OwnerSessionSecretsContract, import("@knpkv/browser-pairing").BrowserPairingError | UnsafeServerOriginError, import("effect/Crypto").Crypto>;
38
+ /** Start the bootstrap clock. Called once the server is actually listening, never before. */
39
+ export declare const activateOwnerSessionBootstrap: (secrets: OwnerSessionSecretsContract) => Effect.Effect<void, import("@knpkv/browser-pairing").BrowserPairingError, never>;
40
+ /**
41
+ * The URL to open. The code rides in the fragment, which browsers do not send to servers and
42
+ * proxies do not log, and the page strips it from the address bar once it has been spent.
43
+ */
44
+ export declare const ownerSessionUrl: (publicOrigin: string, secrets: Pick<OwnerSessionSecretsContract, "bootstrapToken">) => string;
45
+ export declare const ownerSessionCookie: (secrets: Pick<OwnerSessionSecretsContract, "ownerToken">) => string;
46
+ interface OwnerRequest {
47
+ readonly credential: string;
48
+ readonly fetchSite: string | undefined;
49
+ readonly method: string;
50
+ readonly origin: string | undefined;
51
+ }
52
+ export declare const authorizeOwnerRequest: (request: OwnerRequest, secrets: OwnerSessionSecretsContract) => Effect.Effect<undefined, ForbiddenApiError | UnauthorizedApiError, never>;
53
+ export declare const authorizeBootstrapRequest: (request: {
54
+ readonly authorization: string | undefined;
55
+ readonly origin: string | undefined;
56
+ }, secrets: OwnerSessionSecretsContract) => Effect.Effect<undefined, ForbiddenApiError | UnauthorizedApiError, never>;
57
+ export declare const ownerSessionAuthLayer: Layer.Layer<OwnerSessionAuth, never, OwnerSessionSecrets>;
58
+ /** The one route outside the authenticated API: spending the printed code for a session. */
59
+ export declare const OwnerSessionBootstrapRouter: Layer.Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Error", import("effect/http/HttpBody").HttpBodyError> | HttpRouter.Request<"Requires", OwnerSessionSecrets>>;
60
+ export {};
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Who may talk to this server.
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 the authority a page showing their own agents' usage, ticket
9
+ * keys and working directories should demand.
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
+ * - **Every route only reads, and a read needs the cookie and must not be a browser cross-origin
13
+ * request.** Another page in the same browser can make the browser send a cookie, but Fetch
14
+ * Metadata keeps that page from reading usage without an Origin. With no writes there is no CSRF
15
+ * token to issue.
16
+ *
17
+ * The rules are `@knpkv/jcf-web`'s owner session minus its write path, over the same
18
+ * `@knpkv/browser-pairing` credential primitives. They are re-stated rather than imported because
19
+ * that module is private to that application; duplicating the *policy* while sharing the
20
+ * *primitives* is the smaller of the two mistakes available.
21
+ *
22
+ * @module
23
+ */
24
+ import { credentialValuesEqual, decideOneTimeCredential, expiresAt, 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";
27
+ import { ForbiddenApiError, OwnerSessionAuth, UnauthorizedApiError } from "./Api.js";
28
+ /** How long the printed bootstrap URL stays usable. Long enough to click, short enough to forget. */
29
+ const BOOTSTRAP_LIFETIME_MILLIS = 60_000;
30
+ /**
31
+ * Failed bootstrap exchanges before the exchange closes for the life of the process.
32
+ *
33
+ * The code is high-entropy, so this is not what makes guessing hard — it is what stops a process
34
+ * that is being guessed at from staying open all afternoon.
35
+ */
36
+ const MAX_BOOTSTRAP_FAILURES = 5;
37
+ /** Methods that only read: the only ones this server answers on its API. */
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/agent-usage/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: "agent-usage needs a valid HTTP origin", origin }),
53
+ try: () => new URL(origin)
54
+ });
55
+ if (url.protocol !== "http:" || !isLoopbackHostname(url.hostname) || url.search !== "" || url.hash !== "") {
56
+ return yield* new UnsafeServerOriginError({
57
+ message: "agent-usage 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: "agent-usage'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, bootstrapToken] = yield* Effect.all([issueSessionToken(), issuePairingCode()]);
81
+ return OwnerSessionSecrets.of({
82
+ authorityOrigin: validated,
83
+ bootstrapAttemptState: yield* Ref.make({ failedAttempts: 0, inFlight: 0 }),
84
+ bootstrapAvailable: yield* Ref.make(true),
85
+ bootstrapExpiresAtMillis: yield* Ref.make(undefined),
86
+ bootstrapToken,
87
+ browserOrigin,
88
+ ownerToken
89
+ });
90
+ });
91
+ /** Start the bootstrap clock. Called once the server is actually listening, never before. */
92
+ export const activateOwnerSessionBootstrap = Effect.fn("OwnerSession.activateBootstrap")(function* (secrets) {
93
+ const now = yield* Clock.currentTimeMillis;
94
+ yield* Ref.set(secrets.bootstrapExpiresAtMillis, yield* expiresAt(now, BOOTSTRAP_LIFETIME_MILLIS));
95
+ });
96
+ /**
97
+ * The URL to open. The code rides in the fragment, which browsers do not send to servers and
98
+ * proxies do not log, and the page strips it from the address bar once it has been spent.
99
+ */
100
+ export const ownerSessionUrl = (publicOrigin, secrets) => `${publicOrigin.replace(/\/+$/u, "")}/#bootstrap_token=${encodeURIComponent(Redacted.value(secrets.bootstrapToken))}`;
101
+ export const ownerSessionCookie = (secrets) => serializeCredentialCookie(secrets.ownerToken, {
102
+ httpOnly: true,
103
+ name: "agent_usage_owner",
104
+ path: "/api",
105
+ sameSite: "strict",
106
+ secure: false
107
+ });
108
+ const fetchSites = new Set(["cross-site", "none", "same-origin", "same-site"]);
109
+ export const authorizeOwnerRequest = Effect.fn("OwnerSession.authorizeRequest")(function* (request, secrets) {
110
+ if (credentialValuesEqual(request.credential, Redacted.value(secrets.ownerToken)) !== true) {
111
+ return yield* new UnauthorizedApiError({ message: "Missing or invalid owner session" });
112
+ }
113
+ const sameOrigin = request.origin !== undefined && request.origin === secrets.browserOrigin;
114
+ if (request.origin !== undefined && !sameOrigin) {
115
+ return yield* new ForbiddenApiError({ message: "Request origin does not match this agent-usage server" });
116
+ }
117
+ if (request.fetchSite !== undefined && !fetchSites.has(request.fetchSite)) {
118
+ return yield* new ForbiddenApiError({ message: "Request carries invalid browser site metadata" });
119
+ }
120
+ if (request.fetchSite === "cross-site" ||
121
+ (!sameOrigin && (request.fetchSite === "same-site" || request.fetchSite === "none"))) {
122
+ return yield* new ForbiddenApiError({ message: "Browser request is not from this agent-usage page" });
123
+ }
124
+ // An explicit client such as curl carries neither Origin nor Fetch Metadata and remains allowed:
125
+ // it still has to present the process-scoped session cookie.
126
+ if (!safeMethods.has(request.method.toUpperCase())) {
127
+ return yield* new ForbiddenApiError({ message: "This server only answers reads" });
128
+ }
129
+ });
130
+ const bearerToken = (authorization) => authorization !== undefined && authorization.startsWith("Bearer ")
131
+ ? authorization.slice("Bearer ".length)
132
+ : undefined;
133
+ export const authorizeBootstrapRequest = Effect.fn("OwnerSession.authorizeBootstrap")(function* (request, secrets) {
134
+ if (request.origin !== secrets.browserOrigin) {
135
+ return yield* new ForbiddenApiError({ message: "Bootstrap origin does not match this agent-usage server" });
136
+ }
137
+ const supplied = bearerToken(request.authorization);
138
+ // Counted before the code is even compared, so a burst of parallel guesses cannot all pass the
139
+ // check together and then be counted afterwards.
140
+ const admit = Ref.modify(secrets.bootstrapAttemptState, (state) => {
141
+ if (state.failedAttempts + state.inFlight >= MAX_BOOTSTRAP_FAILURES)
142
+ return ["unavailable", state];
143
+ const matches = supplied !== undefined &&
144
+ credentialValuesEqual(supplied, Redacted.value(secrets.bootstrapToken)) === true;
145
+ if (!matches) {
146
+ return ["invalid", {
147
+ failedAttempts: Math.min(MAX_BOOTSTRAP_FAILURES, state.failedAttempts + 1),
148
+ inFlight: state.inFlight
149
+ }];
150
+ }
151
+ return ["accepted", { ...state, inFlight: state.inFlight + 1 }];
152
+ }).pipe(Effect.flatMap((admission) => admission === "unavailable"
153
+ ? Effect.fail(new UnauthorizedApiError({ message: "Bootstrap is closed for this process" }))
154
+ : admission === "invalid"
155
+ ? Effect.fail(new UnauthorizedApiError({ message: "Missing or invalid bootstrap token" }))
156
+ : Effect.void));
157
+ return yield* Effect.acquireUseRelease(admit, () => Effect.gen(function* () {
158
+ const expiry = yield* Ref.get(secrets.bootstrapExpiresAtMillis);
159
+ if (expiry === undefined)
160
+ return yield* new UnauthorizedApiError({ message: "Bootstrap is not active yet" });
161
+ const now = yield* Clock.currentTimeMillis;
162
+ const decision = yield* Ref.modify(secrets.bootstrapAvailable, (available) => {
163
+ const next = decideOneTimeCredential({ consumedAt: available ? null : 0, expiresAt: expiry, revokedAt: null }, now);
164
+ return [next, next === "accepted" ? false : available];
165
+ });
166
+ if (decision === "accepted")
167
+ return;
168
+ const message = decision === "expired"
169
+ ? "Bootstrap token has expired — restart agent-usage for a fresh URL"
170
+ : decision === "consumed"
171
+ ? "Bootstrap token has already been used"
172
+ : "Bootstrap token state is invalid";
173
+ return yield* new UnauthorizedApiError({ message });
174
+ }), () => Ref.update(secrets.bootstrapAttemptState, (state) => ({ ...state, inFlight: Math.max(0, state.inFlight - 1) })));
175
+ });
176
+ export const ownerSessionAuthLayer = Layer.effect(OwnerSessionAuth, Effect.gen(function* () {
177
+ const secrets = yield* OwnerSessionSecrets;
178
+ return OwnerSessionAuth.of({
179
+ ownerCookie: Effect.fn("OwnerSession.ownerCookie")(function* (httpEffect, { credential }) {
180
+ const request = yield* HttpServerRequest.HttpServerRequest;
181
+ yield* authorizeOwnerRequest({
182
+ credential: Redacted.value(credential),
183
+ fetchSite: request.headers["sec-fetch-site"],
184
+ method: request.method,
185
+ origin: request.headers.origin
186
+ }, secrets);
187
+ return yield* httpEffect;
188
+ })
189
+ });
190
+ }));
191
+ /** The one route outside the authenticated API: spending the printed code for a session. */
192
+ export const OwnerSessionBootstrapRouter = HttpRouter.use((router) => router.add("POST", "/auth/bootstrap", Effect.gen(function* () {
193
+ const secrets = yield* OwnerSessionSecrets;
194
+ const request = yield* HttpServerRequest.HttpServerRequest;
195
+ const result = yield* Effect.result(authorizeBootstrapRequest({ authorization: request.headers.authorization, origin: request.headers.origin }, secrets));
196
+ if (result._tag === "Failure") {
197
+ return HttpServerResponse.text(result.failure.message, {
198
+ status: result.failure._tag === "UnauthorizedApiError" ? 401 : 403
199
+ });
200
+ }
201
+ return yield* HttpServerResponse.json({}, {
202
+ headers: { "cache-control": "no-store", "set-cookie": ownerSessionCookie(secrets) },
203
+ status: 200
204
+ });
205
+ })));
206
+ //# sourceMappingURL=OwnerSession.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OwnerSession.js","sourceRoot":"","sources":["../../src/server/OwnerSession.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,SAAS,EACT,gBAAgB,EAChB,iBAAiB,EACjB,yBAAyB,EAC1B,MAAM,wBAAwB,CAAA;AAE/B,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC7E,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAC/E,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAEpF,qGAAqG;AACrG,MAAM,yBAAyB,GAAG,MAAM,CAAA;AAExC;;;;;GAKG;AACH,MAAM,sBAAsB,GAAG,CAAC,CAAA;AAEhC,4EAA4E;AAC5E,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAA;AAEvD;;;GAGG;AACH,MAAM,eAAe,GAAG,uBAAuB,CAAA;AAsB/C,MAAM,OAAO,mBAAoB,SAAQ,OAAO,CAAC,OAAO,EAGrD,CAAC,wCAAwC,CAAC;CAAG;AAEhD,MAAM,OAAO,uBAAwB,SAAQ,MAAM,CAAC,WAAW,EAA2B,CACxF,yBAAyB,EACzB,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAClD;CAAG;AAEJ,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,QAAgB,EAAW,EAAE,CAC9D,QAAQ,KAAK,WAAW,IAAI,QAAQ,KAAK,WAAW,IAAI,QAAQ,KAAK,KAAK,IAAI,QAAQ,KAAK,OAAO,CAAA;AAEpG,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,QAAgB,EAAE,IAAY,EAAU,EAAE,CAC3E,UAAU,QAAQ,KAAK,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,IAAI,IAAI,EAAE,CAAA;AAE7D,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC,EAAE,CAAC,oCAAoC,CAAC,CAClF,QAAQ,CAAC,EAAC,MAAc;IACtB,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;QAC5B,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,uBAAuB,CAAC,EAAE,OAAO,EAAE,uCAAuC,EAAE,MAAM,EAAE,CAAC;QACtG,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC;KAC3B,CAAC,CAAA;IACF,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,MAAM,KAAK,EAAE,IAAI,GAAG,CAAC,IAAI,KAAK,EAAE,EAAE,CAAC;QAC1G,OAAO,KAAK,CAAC,CAAC,IAAI,uBAAuB,CAAC;YACxC,OAAO,EAAE,8DAA8D;YACvE,MAAM;SACP,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,GAAG,CAAC,MAAM,CAAA;AACnB,CAAC,CACF,CAAA;AAED,qFAAqF;AACrF,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,EAAE,CAAC,kCAAkC,CAAC,CAC9E,QAAQ,CAAC,EAAC,gBAAoC,EAAE,eAAuB;IACrE,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,qBAAqB,CAAC,eAAe,CAAC,CAAA;IAC/D,IAAI,gBAAgB,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IACpD,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,qBAAqB,CAAC,gBAAgB,CAAC,CAAA;IACjE,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,eAAe,EAAE,CAAC;QAC/D,OAAO,KAAK,CAAC,CAAC,IAAI,uBAAuB,CAAC;YACxC,OAAO,EAAE,uEAAuE;YAChF,MAAM,EAAE,gBAAgB;SACzB,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,UAAU,CAAA;AACnB,CAAC,CACF,CAAA;AAED,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC,EAAE,CAAC,0BAA0B,CAAC,CAC1E,QAAQ,CAAC,EAAC,eAAuB,EAAE,sBAA+B;IAChE,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,qBAAqB,CAAC,eAAe,CAAC,CAAA;IAC/D,MAAM,aAAa,GAAG,KAAK,CAAC,CAAC,mBAAmB,CAAC,sBAAsB,EAAE,SAAS,CAAC,CAAA;IACnF,MAAM,CAAC,UAAU,EAAE,cAAc,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,iBAAiB,EAAE,EAAE,gBAAgB,EAAE,CAAC,CAAC,CAAA;IACjG,OAAO,mBAAmB,CAAC,EAAE,CAAC;QAC5B,eAAe,EAAE,SAAS;QAC1B,qBAAqB,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,CAAwB,EAAE,cAAc,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;QACjG,kBAAkB,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC;QACzC,wBAAwB,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,CAAqB,SAAS,CAAC;QACxE,cAAc;QACd,aAAa;QACb,UAAU;KACX,CAAC,CAAA;AACJ,CAAC,CACF,CAAA;AAED,6FAA6F;AAC7F,MAAM,CAAC,MAAM,6BAA6B,GAAG,MAAM,CAAC,EAAE,CAAC,gCAAgC,CAAC,CACtF,QAAQ,CAAC,EAAC,OAAoC;IAC5C,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAA;IAC1C,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,wBAAwB,EAAE,KAAK,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE,yBAAyB,CAAC,CAAC,CAAA;AACpG,CAAC,CACF,CAAA;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAC7B,YAAoB,EACpB,OAA4D,EACpD,EAAE,CACV,GAAG,YAAY,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,qBAAqB,kBAAkB,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,EAAE,CAAA;AAEvH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,OAAwD,EAAU,EAAE,CACrG,yBAAyB,CAAC,OAAO,CAAC,UAAU,EAAE;IAC5C,QAAQ,EAAE,IAAI;IACd,IAAI,EAAE,mBAAmB;IACzB,IAAI,EAAE,MAAM;IACZ,QAAQ,EAAE,QAAQ;IAClB,MAAM,EAAE,KAAK;CACd,CAAC,CAAA;AASJ,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,WAAW,CAAC,CAAC,CAAA;AAE9E,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC,EAAE,CAAC,+BAA+B,CAAC,CAC7E,QAAQ,CAAC,EAAC,OAAqB,EAAE,OAAoC;IACnE,IAAI,qBAAqB,CAAC,OAAO,CAAC,UAAU,EAAE,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC3F,OAAO,KAAK,CAAC,CAAC,IAAI,oBAAoB,CAAC,EAAE,OAAO,EAAE,kCAAkC,EAAE,CAAC,CAAA;IACzF,CAAC;IACD,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,KAAK,SAAS,IAAI,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC,aAAa,CAAA;IAC3F,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,UAAU,EAAE,CAAC;QAChD,OAAO,KAAK,CAAC,CAAC,IAAI,iBAAiB,CAAC,EAAE,OAAO,EAAE,uDAAuD,EAAE,CAAC,CAAA;IAC3G,CAAC;IACD,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QAC1E,OAAO,KAAK,CAAC,CAAC,IAAI,iBAAiB,CAAC,EAAE,OAAO,EAAE,+CAA+C,EAAE,CAAC,CAAA;IACnG,CAAC;IACD,IACE,OAAO,CAAC,SAAS,KAAK,YAAY;QAClC,CAAC,CAAC,UAAU,IAAI,CAAC,OAAO,CAAC,SAAS,KAAK,WAAW,IAAI,OAAO,CAAC,SAAS,KAAK,MAAM,CAAC,CAAC,EACpF,CAAC;QACD,OAAO,KAAK,CAAC,CAAC,IAAI,iBAAiB,CAAC,EAAE,OAAO,EAAE,mDAAmD,EAAE,CAAC,CAAA;IACvG,CAAC;IACD,iGAAiG;IACjG,6DAA6D;IAC7D,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;QACnD,OAAO,KAAK,CAAC,CAAC,IAAI,iBAAiB,CAAC,EAAE,OAAO,EAAE,gCAAgC,EAAE,CAAC,CAAA;IACpF,CAAC;AACH,CAAC,CACF,CAAA;AAID,MAAM,WAAW,GAAG,CAAC,aAAiC,EAAsB,EAAE,CAC5E,aAAa,KAAK,SAAS,IAAI,aAAa,CAAC,UAAU,CAAC,SAAS,CAAC;IAChE,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC;IACvC,CAAC,CAAC,SAAS,CAAA;AAEf,MAAM,CAAC,MAAM,yBAAyB,GAAG,MAAM,CAAC,EAAE,CAAC,iCAAiC,CAAC,CACnF,QAAQ,CAAC,EACP,OAA4F,EAC5F,OAAoC;IAEpC,IAAI,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC,aAAa,EAAE,CAAC;QAC7C,OAAO,KAAK,CAAC,CAAC,IAAI,iBAAiB,CAAC,EAAE,OAAO,EAAE,yDAAyD,EAAE,CAAC,CAAA;IAC7G,CAAC;IACD,MAAM,QAAQ,GAAG,WAAW,CAAC,OAAO,CAAC,aAAa,CAAC,CAAA;IACnD,+FAA+F;IAC/F,iDAAiD;IACjD,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CACtB,OAAO,CAAC,qBAAqB,EAC7B,CAAC,KAAK,EAAwD,EAAE;QAC9D,IAAI,KAAK,CAAC,cAAc,GAAG,KAAK,CAAC,QAAQ,IAAI,sBAAsB;YAAE,OAAO,CAAC,aAAa,EAAE,KAAK,CAAC,CAAA;QAClG,MAAM,OAAO,GAAG,QAAQ,KAAK,SAAS;YACpC,qBAAqB,CAAC,QAAQ,EAAE,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,KAAK,IAAI,CAAA;QAClF,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,CAAC,SAAS,EAAE;oBACjB,cAAc,EAAE,IAAI,CAAC,GAAG,CAAC,sBAAsB,EAAE,KAAK,CAAC,cAAc,GAAG,CAAC,CAAC;oBAC1E,QAAQ,EAAE,KAAK,CAAC,QAAQ;iBACzB,CAAC,CAAA;QACJ,CAAC;QACD,OAAO,CAAC,UAAU,EAAE,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,GAAG,CAAC,EAAE,CAAC,CAAA;IACjE,CAAC,CACF,CAAC,IAAI,CACJ,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,EAAE,EAAE,CAC3B,SAAS,KAAK,aAAa;QACzB,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,oBAAoB,CAAC,EAAE,OAAO,EAAE,sCAAsC,EAAE,CAAC,CAAC;QAC5F,CAAC,CAAC,SAAS,KAAK,SAAS;YACzB,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,oBAAoB,CAAC,EAAE,OAAO,EAAE,oCAAoC,EAAE,CAAC,CAAC;YAC1F,CAAC,CAAC,MAAM,CAAC,IAAI,CAChB,CACF,CAAA;IACD,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,iBAAiB,CACpC,KAAK,EACL,GAAG,EAAE,CACH,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAClB,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,wBAAwB,CAAC,CAAA;QAC/D,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC,CAAC,IAAI,oBAAoB,CAAC,EAAE,OAAO,EAAE,6BAA6B,EAAE,CAAC,CAAA;QAC5G,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAA;QAC1C,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAC,SAAS,EAAE,EAAE;YAC3E,MAAM,IAAI,GAAG,uBAAuB,CAClC,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,EACxE,GAAG,CACJ,CAAA;YACD,OAAO,CAAC,IAAI,EAAE,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;QACxD,CAAC,CAAC,CAAA;QACF,IAAI,QAAQ,KAAK,UAAU;YAAE,OAAM;QACnC,MAAM,OAAO,GAAG,QAAQ,KAAK,SAAS;YACpC,CAAC,CAAC,mEAAmE;YACrE,CAAC,CAAC,QAAQ,KAAK,UAAU;gBACzB,CAAC,CAAC,uCAAuC;gBACzC,CAAC,CAAC,kCAAkC,CAAA;QACtC,OAAO,KAAK,CAAC,CAAC,IAAI,oBAAoB,CAAC,EAAE,OAAO,EAAE,CAAC,CAAA;IACrD,CAAC,CAAC,EACJ,GAAG,EAAE,CACH,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,qBAAqB,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAClH,CAAA;AACH,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,mBAAmB,CAAA;IAC1C,OAAO,gBAAgB,CAAC,EAAE,CAAC;QACzB,WAAW,EAAE,MAAM,CAAC,EAAE,CAAC,0BAA0B,CAAC,CAChD,QAAQ,CAAC,EAAC,UAAU,EAAE,EAAE,UAAU,EAAE;YAClC,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,iBAAiB,CAAC,iBAAiB,CAAA;YAC1D,KAAK,CAAC,CAAC,qBAAqB,CAC1B;gBACE,UAAU,EAAE,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC;gBACtC,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,gBAAgB,CAAC;gBAC5C,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM;aAC/B,EACD,OAAO,CACR,CAAA;YACD,OAAO,KAAK,CAAC,CAAC,UAAU,CAAA;QAC1B,CAAC,CACF;KACF,CAAC,CAAA;AACJ,CAAC,CAAC,CACH,CAAA;AAED,4FAA4F;AAC5F,MAAM,CAAC,MAAM,2BAA2B,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CACnE,MAAM,CAAC,GAAG,CACR,MAAM,EACN,iBAAiB,EACjB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,mBAAmB,CAAA;IAC1C,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,iBAAiB,CAAC,iBAAiB,CAAA;IAC1D,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CACjC,yBAAyB,CACvB,EAAE,aAAa,EAAE,OAAO,CAAC,OAAO,CAAC,aAAa,EAAE,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAChF,OAAO,CACR,CACF,CAAA;IACD,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE;YACrD,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,IAAI,KAAK,sBAAsB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;SACnE,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,KAAK,CAAC,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE,EAAE;QACxC,OAAO,EAAE,EAAE,eAAe,EAAE,UAAU,EAAE,YAAY,EAAE,kBAAkB,CAAC,OAAO,CAAC,EAAE;QACnF,MAAM,EAAE,GAAG;KACZ,CAAC,CAAA;AACJ,CAAC,CAAC,CACH,CACF,CAAA"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The long-running half of `agent-usage serve`: an ingest pass every minute, a ticket-title refresh
3
+ * on its own minute schedule, and a Claude limit poll every five minutes, all feeding the store the
4
+ * API reads.
5
+ *
6
+ * **Mental model**
7
+ *
8
+ * - **Polling, not watching.** Byte cursors make a pass over unchanged files cheap, so a fixed
9
+ * interval is simpler and as current as the graphs need; no filesystem watchers.
10
+ * - **A failed pass is a status, not a crash.** It is recorded for the status strip and the next
11
+ * interval tries again; the server keeps serving what the store already holds.
12
+ * - **Ingest Status lives in memory.** Cursors are the only persisted ingest state; a restart
13
+ * recomputes the status on its first pass.
14
+ *
15
+ * @module
16
+ */
17
+ import type { FileSystem, Path } from "effect";
18
+ import { Context, Duration, Effect, Layer, SubscriptionRef } from "effect";
19
+ import { type ClaudeUsageDeps } from "../core/ClaudeLimits.js";
20
+ import { type SourceRoots } from "../core/Ingest.js";
21
+ import { UsageStore } from "../core/Store.js";
22
+ import { type TicketSearch } from "../core/Tickets.js";
23
+ import type { ServerStatus } from "../shared/contracts.js";
24
+ export declare const INGEST_INTERVAL: Duration.Duration;
25
+ export declare const CLAUDE_POLL_INTERVAL: Duration.Duration;
26
+ declare const RuntimeState_base: Context.ServiceClass<RuntimeState, "@knpkv/agent-usage/server/Runtime/RuntimeState", {
27
+ readonly machine: string;
28
+ /** Known Projects from configuration. */
29
+ readonly projects: ReadonlyArray<string>;
30
+ /** The status the page shows; a SubscriptionRef so its changes can be followed. */
31
+ readonly status: SubscriptionRef.SubscriptionRef<ServerStatus>;
32
+ }>;
33
+ export declare class RuntimeState extends RuntimeState_base {
34
+ static readonly layer: (machine: string, projects?: ReadonlyArray<string>) => Layer.Layer<RuntimeState, never, never>;
35
+ }
36
+ /** The Known Projects now: those the store's branches and paths name, plus the configured ones. */
37
+ export declare const currentKnownProjects: Effect.Effect<ReadonlySet<string>, import("../core/Store.js").StoreError, RuntimeState | UsageStore>;
38
+ export interface BackgroundOptions {
39
+ readonly roots: SourceRoots;
40
+ readonly claude: ClaudeUsageDeps;
41
+ readonly ticketSearch: TicketSearch;
42
+ }
43
+ /** One ingest pass, its outcome written to the status. */
44
+ export declare const ingestCycle: (options: BackgroundOptions) => Effect.Effect<void, never, FileSystem.FileSystem | Path.Path | RuntimeState | UsageStore>;
45
+ /**
46
+ * One title refresh for the tickets booked recently. It runs on its own schedule: a slow or hung
47
+ * `acli` delays titles, never the next ingest pass.
48
+ */
49
+ export declare const titleCycle: (options: BackgroundOptions) => Effect.Effect<void, never, RuntimeState | UsageStore>;
50
+ /**
51
+ * One Claude limit poll, recorded. Failing to store it is reported apart from ingest, so a good
52
+ * ingest pass cannot clear it; the next stored poll does.
53
+ */
54
+ export declare const claudePollCycle: (options: BackgroundOptions) => Effect.Effect<void, never, RuntimeState | UsageStore>;
55
+ /** Runs both cycles on their intervals for the life of the layer's scope. */
56
+ export declare const backgroundLayer: (options: BackgroundOptions) => Layer.Layer<never, never, RuntimeState | UsageStore | FileSystem.FileSystem | Path.Path>;
57
+ export {};
@@ -0,0 +1,85 @@
1
+ import { Clock, Context, Duration, Effect, Layer, Schedule, SubscriptionRef } from "effect";
2
+ import { attribute, knownProjects } from "../core/Attribution.js";
3
+ import { pollClaudeLimits } from "../core/ClaudeLimits.js";
4
+ import { ingestOnce } from "../core/Ingest.js";
5
+ import { UsageStore } from "../core/Store.js";
6
+ import { refreshTicketTitles } from "../core/Tickets.js";
7
+ export const INGEST_INTERVAL = Duration.seconds(60);
8
+ export const CLAUDE_POLL_INTERVAL = Duration.minutes(5);
9
+ /** How far back ticket titles are kept fresh. */
10
+ const TITLE_HORIZON_MILLIS = 90 * 24 * 60 * 60 * 1000;
11
+ export class RuntimeState extends Context.Service()("@knpkv/agent-usage/server/Runtime/RuntimeState") {
12
+ static layer = (machine, projects = []) => Layer.effect(RuntimeState, Effect.map(SubscriptionRef.make({
13
+ machine,
14
+ ingest: null,
15
+ ingestFailure: null,
16
+ limitsFailure: null,
17
+ ticketLookupFailures: []
18
+ }), (status) => RuntimeState.of({ machine, projects, status })));
19
+ }
20
+ /** The Known Projects now: those the store's branches and paths name, plus the configured ones. */
21
+ export const currentKnownProjects = Effect.gen(function* () {
22
+ const store = yield* UsageStore;
23
+ const state = yield* RuntimeState;
24
+ return knownProjects(yield* store.places(state.machine), state.projects);
25
+ });
26
+ /** Ticket keys booked in the recent past, whose titles the page will ask for. */
27
+ const recentTicketKeys = Effect.gen(function* () {
28
+ const store = yield* UsageStore;
29
+ const now = yield* Clock.currentTimeMillis;
30
+ const state = yield* RuntimeState;
31
+ const groups = yield* store.usageGroups({ from: now - TITLE_HORIZON_MILLIS, to: now + 1, machine: state.machine });
32
+ const projects = yield* currentKnownProjects;
33
+ const keys = new Set();
34
+ for (const group of groups) {
35
+ const { booking } = attribute(group.attribution, projects);
36
+ if (booking._tag === "Ticket")
37
+ keys.add(booking.key);
38
+ }
39
+ return [...keys];
40
+ });
41
+ /** One ingest pass, its outcome written to the status. */
42
+ export const ingestCycle = (options) => Effect.gen(function* () {
43
+ const state = yield* RuntimeState;
44
+ const outcome = yield* Effect.result(ingestOnce(options.roots));
45
+ if (outcome._tag === "Failure") {
46
+ yield* SubscriptionRef.update(state.status, (status) => ({
47
+ ...status,
48
+ ingestFailure: `${outcome.failure.operation}: the store could not be written`
49
+ }));
50
+ return;
51
+ }
52
+ yield* SubscriptionRef.update(state.status, (status) => ({ ...status, ingest: outcome.success, ingestFailure: null }));
53
+ });
54
+ /**
55
+ * One title refresh for the tickets booked recently. It runs on its own schedule: a slow or hung
56
+ * `acli` delays titles, never the next ingest pass.
57
+ */
58
+ export const titleCycle = (options) => Effect.gen(function* () {
59
+ const state = yield* RuntimeState;
60
+ const failures = yield* recentTicketKeys.pipe(Effect.flatMap((keys) => refreshTicketTitles(keys, options.ticketSearch)), Effect.map((lookups) => lookups.map((failure) => failure.reason)), Effect.catchTag("StoreError", (error) => Effect.succeed([`ticket titles: ${error.operation} failed`])));
61
+ yield* SubscriptionRef.update(state.status, (status) => ({ ...status, ticketLookupFailures: [...new Set(failures)] }));
62
+ });
63
+ /**
64
+ * One Claude limit poll, recorded. Failing to store it is reported apart from ingest, so a good
65
+ * ingest pass cannot clear it; the next stored poll does.
66
+ */
67
+ export const claudePollCycle = (options) => Effect.gen(function* () {
68
+ const state = yield* RuntimeState;
69
+ const store = yield* UsageStore;
70
+ const observations = yield* pollClaudeLimits(options.claude, state.machine);
71
+ const stored = yield* Effect.result(store.recordObservations(observations.snapshots, observations.balances));
72
+ yield* SubscriptionRef.update(state.status, (status) => ({
73
+ ...status,
74
+ limitsFailure: stored._tag === "Failure"
75
+ ? `${stored.failure.operation}: Claude limits could not be stored`
76
+ : null
77
+ }));
78
+ });
79
+ /** Runs both cycles on their intervals for the life of the layer's scope. */
80
+ export const backgroundLayer = (options) => Layer.effectDiscard(Effect.gen(function* () {
81
+ yield* Effect.forkScoped(Effect.repeat(ingestCycle(options), Schedule.spaced(INGEST_INTERVAL)));
82
+ yield* Effect.forkScoped(Effect.repeat(titleCycle(options), Schedule.spaced(INGEST_INTERVAL)));
83
+ yield* Effect.forkScoped(Effect.repeat(claudePollCycle(options), Schedule.spaced(CLAUDE_POLL_INTERVAL)));
84
+ }));
85
+ //# sourceMappingURL=Runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Runtime.js","sourceRoot":"","sources":["../../src/server/Runtime.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { Config, Deferred, Layer, Schema } from "effect";
2
+ import type { AgentUsageConfig } from "./Config.js";
3
+ import { type OwnerSessionSecretsContract } from "./OwnerSession.js";
4
+ export interface AgentUsageServerOptions {
5
+ readonly config: AgentUsageConfig;
6
+ readonly hostname?: string;
7
+ readonly port: number;
8
+ readonly ready?: Deferred.Deferred<void>;
9
+ readonly security: OwnerSessionSecretsContract;
10
+ }
11
+ export declare const makeServer: (options: AgentUsageServerOptions) => Layer.Layer<never, import("@knpkv/browser-pairing").BrowserPairingError | Schema.SchemaError | import("effect/http/HttpServerError").ServeError | import("../core/Store.js").StoreError, never>;
12
+ /** The port to bind: next to jcf-web's 3111. */
13
+ export declare const Port: Config.Config<number>;
14
+ /** Set by `pnpm dev` so the printed URL points at the Vite dev server that proxies here. */
15
+ export declare const PublicOrigin: Config.Config<import("effect/Option").Option<string>>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Binding the usage view to a loopback port, with the store and the background work behind it.
3
+ *
4
+ * **Mental model**
5
+ *
6
+ * - **One process, one operator, one origin.** The server listens on loopback, mints its own session
7
+ * at startup, and prints the URL that spends it.
8
+ * - **The store opens before the port does.** A store that cannot be opened privately stops the
9
+ * server from starting rather than serving an empty page.
10
+ *
11
+ * @module
12
+ */
13
+ import { NodeHttpServer, NodeServices } from "@effect/platform-node";
14
+ import { Config, Deferred, Effect, Layer, Schema } from "effect";
15
+ import { Etag, FetchHttpClient, HttpPlatform, HttpRouter } from "effect/http";
16
+ import { createServer } from "node:http";
17
+ import { liveClaudeUsageDeps } from "../core/ClaudeLimitsLive.js";
18
+ import { databaseLayer } from "../core/Database.js";
19
+ import { acliTicketSearch } from "../core/Tickets.js";
20
+ import { application } from "./HttpApplication.js";
21
+ import { activateOwnerSessionBootstrap, OwnerSessionSecrets } from "./OwnerSession.js";
22
+ import { backgroundLayer, RuntimeState } from "./Runtime.js";
23
+ const HttpPlatformLive = HttpPlatform.layer.pipe(Layer.provide(NodeServices.layer));
24
+ const LoopbackHostname = Schema.Literals(["127.0.0.1", "localhost", "::1"]);
25
+ /** The background ingest and polling, with their live dependencies. */
26
+ const background = (config) => Layer.unwrap(Effect.gen(function* () {
27
+ const claude = yield* liveClaudeUsageDeps(config.claudeConfigDir);
28
+ const ticketSearch = yield* acliTicketSearch;
29
+ return backgroundLayer({ roots: config.roots, claude, ticketSearch });
30
+ }));
31
+ export const makeServer = (options) => Layer.unwrap(Schema.decodeUnknownEffect(LoopbackHostname)(options.hostname ?? "127.0.0.1").pipe(Effect.map((hostname) => {
32
+ const services = Layer.mergeAll(databaseLayer(options.config.storeDirectory), RuntimeState.layer(options.config.roots.machine, options.config.projects));
33
+ return Layer.mergeAll(HttpRouter.serve(application), background(options.config)).pipe(Layer.provide(services), Layer.provide(NodeHttpServer.layerServer(createServer, { host: hostname, port: options.port })), Layer.provide(Etag.layer), Layer.provide(HttpPlatformLive), Layer.provide(FetchHttpClient.layer), Layer.provide(NodeServices.layer), Layer.provide(Layer.succeed(OwnerSessionSecrets, options.security)), Layer.tap(() => activateOwnerSessionBootstrap(options.security).pipe(Effect.andThen(options.ready === undefined ? Effect.void : Deferred.succeed(options.ready, undefined)))));
34
+ })));
35
+ /** The port to bind: next to jcf-web's 3111. */
36
+ export const Port = Config.Int("PORT").pipe(Config.withDefault(3112));
37
+ /** Set by `pnpm dev` so the printed URL points at the Vite dev server that proxies here. */
38
+ export const PublicOrigin = Config.option(Config.String("AGENT_USAGE_PUBLIC_ORIGIN"));
39
+ //# sourceMappingURL=Server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Server.js","sourceRoot":"","sources":["../../src/server/Server.ts"],"names":[],"mappings":"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"}