@hansenexus/hud 0.3.0 → 0.4.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 (54) hide show
  1. package/README.md +186 -4
  2. package/dist/access-CNvJOwKM.d.ts +54 -0
  3. package/dist/capabilities-CjulXFQX.js +189 -0
  4. package/dist/{define-hud-BHQR8Lbv.d.ts → define-hud-BBzsSl8b.d.ts} +6 -1
  5. package/dist/env-DfxpPfs9.js +28 -0
  6. package/dist/env-vEjyJfPW.d.ts +17 -0
  7. package/dist/force-state-BaqWsmrf.js +153 -0
  8. package/dist/force-state-CCqoTfVB.d.ts +23 -0
  9. package/dist/handler-WmE4Lu7l.d.ts +39 -0
  10. package/dist/index-BYsdS-7d.d.ts +55 -0
  11. package/dist/index.d.ts +6 -36
  12. package/dist/index.js +119 -202
  13. package/dist/marker-C66LxOhJ.d.ts +9 -0
  14. package/dist/marker-CMrvxPmS.d.ts +12 -0
  15. package/dist/marker-DjeGvL-F.js +9 -0
  16. package/dist/marker-uZYPOJ4M.js +12 -0
  17. package/dist/markers.d.ts +3 -0
  18. package/dist/markers.js +3 -0
  19. package/dist/next.d.ts +3 -18
  20. package/dist/next.js +3 -28
  21. package/dist/payload-CLKDVZWl.d.ts +68 -0
  22. package/dist/plugins/access/server.d.ts +122 -0
  23. package/dist/plugins/access/server.js +339 -0
  24. package/dist/plugins/access.d.ts +41 -0
  25. package/dist/plugins/access.js +250 -0
  26. package/dist/plugins/agent/server.d.ts +129 -0
  27. package/dist/plugins/agent/server.js +429 -0
  28. package/dist/plugins/agent.d.ts +37 -0
  29. package/dist/plugins/agent.js +411 -0
  30. package/dist/plugins/design.d.ts +125 -0
  31. package/dist/plugins/design.js +619 -0
  32. package/dist/plugins/observe.d.ts +64 -0
  33. package/dist/plugins/observe.js +617 -0
  34. package/dist/plugins/ops/server.d.ts +82 -0
  35. package/dist/plugins/ops/server.js +380 -0
  36. package/dist/plugins/ops.d.ts +26 -0
  37. package/dist/plugins/ops.js +362 -0
  38. package/dist/plugins/vitals.js +3 -86
  39. package/dist/{plugins-1QmTmZyL.d.ts → plugins-CQ_0w1ot.d.ts} +39 -1
  40. package/dist/{plugins-CzUiTn_r.js → plugins-DVy_vaYr.js} +3 -1
  41. package/dist/review.d.ts +181 -0
  42. package/dist/review.js +1017 -0
  43. package/dist/selector-NSYSe7AZ.js +28 -0
  44. package/dist/server.d.ts +3 -39
  45. package/dist/server.js +1 -1
  46. package/dist/shortcut-U6ftVY06.js +222 -0
  47. package/dist/state-boundary.d.ts +26 -0
  48. package/dist/state-boundary.js +61 -0
  49. package/dist/styles--slk86ZQ.js +332 -0
  50. package/dist/types-DADBRiGk.d.ts +50 -0
  51. package/dist/types-QJUajwWq.js +35 -0
  52. package/dist/types-f3jlNxzw.d.ts +92 -0
  53. package/dist/vitals-panel-Dvc5dnR-.js +87 -0
  54. package/package.json +23 -3
package/dist/next.d.ts CHANGED
@@ -1,21 +1,6 @@
1
- import { n as HudMountProps } from "./define-hud-BHQR8Lbv.js";
1
+ import { n as HudMountProps } from "./define-hud-BBzsSl8b.js";
2
+ import { n as hudEnvLabel, r as resolveHudEnv, t as HudEnv } from "./env-vEjyJfPW.js";
2
3
  import { ComponentType } from "react";
3
- //#region src/next/env.d.ts
4
- /**
5
- * Where the HUD believes it runs. An explicit `NEXT_PUBLIC_HUD_ENV` wins over
6
- * heuristics because k3s deploys have no `VERCEL_ENV`.
7
- */
8
- type HudEnv = "development" | "preview" | "staging" | "production" | "disabled";
9
- /**
10
- * Env ladder: kill switch → `NODE_ENV` (inlined by Next, so production
11
- * bundles fold the development branch) → `NEXT_PUBLIC_HUD_ENV` (k8s overlays)
12
- * → `VERCEL_ENV` → production.
13
- *
14
- * Each read is a literal `process.env.X` so Next can inline it in client code.
15
- */
16
- export declare function resolveHudEnv(): HudEnv;
17
- export declare function hudEnvLabel(env?: HudEnv): string;
18
- //#endregion
19
4
  //#region src/next/loader.d.ts
20
5
  type HudModuleLoader = () => Promise<{
21
6
  default: ComponentType<HudMountProps>;
@@ -45,4 +30,4 @@ type HudModuleLoader = () => Promise<{
45
30
  */
46
31
  export declare function createHudLoader(load: HudModuleLoader | null): ComponentType<HudMountProps>;
47
32
  //#endregion
48
- export type { HudEnv, HudModuleLoader, HudMountProps };
33
+ export { type HudEnv, type HudModuleLoader, type HudMountProps, hudEnvLabel, resolveHudEnv };
package/dist/next.js CHANGED
@@ -1,32 +1,6 @@
1
+ import { n as resolveHudEnv, t as hudEnvLabel } from "./env-DfxpPfs9.js";
1
2
  import { Suspense, lazy, useEffect, useState } from "react";
2
3
  import { jsx } from "react/jsx-runtime";
3
- //#region src/next/env.ts
4
- /**
5
- * Env ladder: kill switch → `NODE_ENV` (inlined by Next, so production
6
- * bundles fold the development branch) → `NEXT_PUBLIC_HUD_ENV` (k8s overlays)
7
- * → `VERCEL_ENV` → production.
8
- *
9
- * Each read is a literal `process.env.X` so Next can inline it in client code.
10
- */
11
- function resolveHudEnv() {
12
- if (process.env.NEXT_PUBLIC_HUD_DISABLED === "true") return "disabled";
13
- if (process.env.NODE_ENV === "development") return "development";
14
- const explicit = process.env.NEXT_PUBLIC_HUD_ENV;
15
- if (explicit === "staging" || explicit === "preview" || explicit === "production") return explicit;
16
- if (process.env.VERCEL_ENV === "preview") return "preview";
17
- return "production";
18
- }
19
- const LABELS = {
20
- development: "DEV",
21
- preview: "PREVIEW",
22
- staging: "STAGING",
23
- production: "PROD",
24
- disabled: "OFF"
25
- };
26
- function hudEnvLabel(env = resolveHudEnv()) {
27
- return LABELS[env];
28
- }
29
- //#endregion
30
4
  //#region src/next/loader.tsx
31
5
  /**
32
6
  * Mounts the app's `defineHud` module, lazily and client side only.
@@ -64,7 +38,8 @@ function createHudLoader(load) {
64
38
  fallback: null,
65
39
  children: /* @__PURE__ */ jsx(LazyHud, {
66
40
  envLabel: hudEnvLabel(env),
67
- ...props
41
+ ...props,
42
+ env
68
43
  })
69
44
  });
70
45
  }
@@ -0,0 +1,68 @@
1
+ //#region src/core/grabber/inspect.d.ts
2
+ /**
3
+ * Which React component rendered a DOM element, read from the fiber React
4
+ * attaches to it in development. Best effort by design: fibers are React
5
+ * internals, so every field is probed rather than trusted, and a page that is
6
+ * not React (or a production build) simply yields `null`.
7
+ */
8
+ interface GrabComponent {
9
+ /** Nearest named component above the element, e.g. `HeroCta`. */
10
+ name: string;
11
+ /** Named components further up, nearest first, at most five. */
12
+ owners: string[];
13
+ /**
14
+ * Where the element's JSX was written, e.g. `src/site.tsx:12:5`. A hint, not
15
+ * a resolution: the file is reliable, the line is the position in the
16
+ * dev server's transformed module and can sit a few lines below the original.
17
+ */
18
+ source?: string;
19
+ }
20
+ //#endregion
21
+ //#region src/core/grabber/payload.d.ts
22
+ /** Box in viewport percentages, measured at grab time. */
23
+ interface GrabRect {
24
+ x: number;
25
+ y: number;
26
+ width: number;
27
+ height: number;
28
+ }
29
+ interface GrabAnchor {
30
+ /** `nth-of-type` path from `<body>`. */
31
+ selector: string;
32
+ /** Human label, e.g. `button “Termin buchen”`. */
33
+ label: string;
34
+ tag: string;
35
+ /**
36
+ * Viewport-relative, so correct only for a grab that is acted on at once.
37
+ * Anything that persists a position (review pins) needs document-relative
38
+ * coordinates instead.
39
+ */
40
+ rect: GrabRect;
41
+ }
42
+ /** One entry of an app's component manifest: a name and the file it lives in. */
43
+ interface ComponentHint {
44
+ name: string;
45
+ path: string;
46
+ }
47
+ /** What a completed grab hands to plugins (`onGrab`, `ctx.grab`). */
48
+ interface GrabPayload {
49
+ app: string;
50
+ url: string;
51
+ route: string;
52
+ anchor: GrabAnchor;
53
+ /** The React component that rendered the element, when the page is React in development. */
54
+ component: GrabComponent | null;
55
+ viewport: {
56
+ width: number;
57
+ height: number;
58
+ };
59
+ grabbedAt: string;
60
+ }
61
+ /**
62
+ * Scores manifest entries against the route and anchor label. Pure token
63
+ * overlap: a hint for the agent, not a resolution. Ported from the old HUD's
64
+ * `grabber/payload.ts`.
65
+ */
66
+ declare function matchComponentHints(hints: readonly ComponentHint[] | undefined, route: string, label: string, max?: number): ComponentHint[];
67
+ //#endregion
68
+ export { GrabComponent as a, matchComponentHints as i, GrabAnchor as n, GrabPayload as r, ComponentHint as t };
@@ -0,0 +1,122 @@
1
+ import { r as HudCapability } from "../../plugins-CQ_0w1ot.js";
2
+ import { t as HudEnv } from "../../env-vEjyJfPW.js";
3
+ import { a as signHudUnlockToken, c as CAPABILITY_MATRIX, d as hasCapability, f as isHudVisible, i as resolveHudAccess, l as CapabilityMatrixOverride, n as HudAccess, o as verifyHudUnlockToken, r as hudRequiresUnlock, s as ACCESS_UNLOCK_CAPABILITY, t as HUD_UNLOCK_COOKIE, u as HUD_CAPABILITIES } from "../../access-CNvJOwKM.js";
4
+ import { t as HudGuard } from "../../handler-WmE4Lu7l.js";
5
+ //#region src/plugins/access/server/settings.d.ts
6
+ /** What the server half reads from its environment, per request. */
7
+ interface AccessSettings {
8
+ env: HudEnv;
9
+ /** `NEXT_PUBLIC_HUD_ENABLED=true`: the deployed tier opted in. */
10
+ enabled: boolean;
11
+ /** `HUD_ACCESS_PASSWORD`. Server only: never pass it to client code. */
12
+ password: string | undefined;
13
+ /**
14
+ * `HUD_ACCESS_SECRET`: optional, server only. When set, the unlock cookie is
15
+ * signed with it instead of the password, so a stolen cookie gives nothing
16
+ * to guess the password against.
17
+ */
18
+ secret?: string | undefined;
19
+ }
20
+ interface AccessServerOptions {
21
+ /** Path `createHudHandler` is mounted at, for the guard's unlock exemption. Default `/api/hud`. */
22
+ basePath?: string;
23
+ /** Replaces the default matrix's list for a deployed tier. Keep it equal to the client half's. */
24
+ matrix?: CapabilityMatrixOverride;
25
+ /** Default: `readAccessSettings`, i.e. `process.env`. A seam for tests and the playground. */
26
+ settings?: () => AccessSettings;
27
+ }
28
+ /**
29
+ * Reads the env ladder, the opt-in flag and the secrets, at call time.
30
+ * `HUD_DISABLED=true` is a runtime kill switch: unlike the `NEXT_PUBLIC_*`
31
+ * flags, which are inlined at build time, it takes effect on a restart
32
+ * without a rebuild, and turns every route into a 404.
33
+ */
34
+ export declare function readAccessSettings(): AccessSettings;
35
+ //#endregion
36
+ //#region src/plugins/access/server/guard.d.ts
37
+ interface HudGuardRequest {
38
+ headers: {
39
+ get(name: string): string | null;
40
+ };
41
+ }
42
+ type HudGuardResult = {
43
+ ok: true;
44
+ env: HudEnv;
45
+ } | {
46
+ ok: false;
47
+ status: 401 | 403 | 404;
48
+ error: string;
49
+ };
50
+ /**
51
+ * Whether this request may exercise `capability` right now.
52
+ *
53
+ * Order matters: a HUD that is not enabled answers 404 (nothing here), a
54
+ * denied capability answers 403 before any password is considered (the
55
+ * password must never be a way to widen the matrix), and only a permitted
56
+ * capability gets as far as the unlock check. A route without a capability
57
+ * is development-only: default-deny means undeclared is denied.
58
+ */
59
+ export declare function checkHudRequest(request: HudGuardRequest, capability: HudCapability | undefined, options?: AccessServerOptions): Promise<HudGuardResult>;
60
+ /** Whether the request already carries a valid unlock (the status route). */
61
+ export declare function isHudRequestUnlocked(request: HudGuardRequest, options?: AccessServerOptions): Promise<boolean>;
62
+ //#endregion
63
+ //#region src/plugins/access/server/unlock.d.ts
64
+ interface HudUnlockRequest extends HudGuardRequest {
65
+ json(): Promise<unknown>;
66
+ }
67
+ interface HudUnlockResult {
68
+ status: number;
69
+ body: Record<string, unknown>;
70
+ /** `Set-Cookie` value, when the attempt changed the unlock state. */
71
+ setCookie?: string;
72
+ }
73
+ /**
74
+ * GET: what the client needs to decide between the panels and the lock, and
75
+ * nothing more. The route is reachable while locked, so it names no tier.
76
+ */
77
+ export declare function handleHudUnlockStatus(request: HudGuardRequest, options?: AccessServerOptions): Promise<HudUnlockResult>;
78
+ /** POST: exchanges the password for a signed, expiring unlock cookie. */
79
+ export declare function handleHudUnlockAttempt(request: HudUnlockRequest, options?: AccessServerOptions): Promise<HudUnlockResult>;
80
+ /** Test seam: the attempt budget is process state, not request state. */
81
+ export declare function resetHudUnlockAttempts(): void;
82
+ /** Test seam: how many clients the attempt budget tracks. */
83
+ export declare function trackedHudUnlockClients(): number;
84
+ //#endregion
85
+ //#region src/plugins/access/server.d.ts
86
+ /**
87
+ * The guard for `createHudHandler({ guard })` on deployed tiers. It answers
88
+ * exactly like the old `checkHudRequest`: 404 while the tier has not opted in,
89
+ * 403 for a capability the tier's matrix denies (before any password is
90
+ * considered), 401 until the request carries a valid unlock cookie. In
91
+ * development it lets everything through, like `developmentOnly`.
92
+ *
93
+ * The unlock route itself is the one exemption, so a locked HUD can unlock.
94
+ * Pass the handler's `basePath` when it is not `/api/hud`.
95
+ */
96
+ export declare function accessGuard(options?: AccessServerOptions): HudGuard;
97
+ /**
98
+ * Server half of the access plugin: `GET|POST <basePath>/access/unlock`.
99
+ * GET reports `{ unlocked, configured }`; POST trades `{ password }` for an
100
+ * httpOnly cookie. Wrong passwords are budgeted: ten per client per ten
101
+ * minutes, and past a hundred overall every client with failures of its own
102
+ * is throttled; a throttled client gets 429 even for the right password.
103
+ *
104
+ * ```ts
105
+ * const options = {};
106
+ * export const { GET, POST } = createHudHandler([accessServer(options), ...], {
107
+ * guard: accessGuard(options),
108
+ * });
109
+ * ```
110
+ */
111
+ export declare function accessServer(options?: AccessServerOptions): {
112
+ readonly id: "access";
113
+ readonly capability: "access.unlock";
114
+ readonly routes: {
115
+ readonly "/unlock": {
116
+ readonly GET: (request: Request) => Promise<Response>;
117
+ readonly POST: (request: Request) => Promise<Response>;
118
+ };
119
+ };
120
+ };
121
+ //#endregion
122
+ export { ACCESS_UNLOCK_CAPABILITY, type AccessServerOptions, type AccessSettings, CAPABILITY_MATRIX, type CapabilityMatrixOverride, HUD_CAPABILITIES, HUD_UNLOCK_COOKIE, type HudAccess, type HudGuardResult, hasCapability, hudRequiresUnlock, isHudVisible, resolveHudAccess, signHudUnlockToken, verifyHudUnlockToken };
@@ -0,0 +1,339 @@
1
+ import { i as definePlugin } from "../../plugins-DVy_vaYr.js";
2
+ import { n as resolveHudEnv } from "../../env-DfxpPfs9.js";
3
+ import { a as isHudVisible, c as HUD_UNLOCK_COOKIE, d as hudUnlockTtlMs, f as readCookie, g as verifyHudUnlockToken, h as timingSafeEqual, i as hasCapability, l as hudRequiresUnlock, m as signHudUnlockToken, n as CAPABILITY_MATRIX, o as resolveMatrix, p as resolveHudAccess, r as HUD_CAPABILITIES, t as ACCESS_UNLOCK_CAPABILITY, u as hudUnlockCookieString } from "../../capabilities-CjulXFQX.js";
4
+ //#region src/plugins/access/server/settings.ts
5
+ /** The key material unlock cookies are signed with. */
6
+ function signingSecret({ secret, password }) {
7
+ return secret || password;
8
+ }
9
+ /**
10
+ * Reads the env ladder, the opt-in flag and the secrets, at call time.
11
+ * `HUD_DISABLED=true` is a runtime kill switch: unlike the `NEXT_PUBLIC_*`
12
+ * flags, which are inlined at build time, it takes effect on a restart
13
+ * without a rebuild, and turns every route into a 404.
14
+ */
15
+ function readAccessSettings() {
16
+ return {
17
+ env: process.env.HUD_DISABLED === "true" ? "disabled" : resolveHudEnv(),
18
+ enabled: process.env.NEXT_PUBLIC_HUD_ENABLED === "true",
19
+ password: process.env.HUD_ACCESS_PASSWORD || void 0,
20
+ secret: process.env.HUD_ACCESS_SECRET || void 0
21
+ };
22
+ }
23
+ //#endregion
24
+ //#region src/plugins/access/server/guard.ts
25
+ /**
26
+ * Whether this request may exercise `capability` right now.
27
+ *
28
+ * Order matters: a HUD that is not enabled answers 404 (nothing here), a
29
+ * denied capability answers 403 before any password is considered (the
30
+ * password must never be a way to widen the matrix), and only a permitted
31
+ * capability gets as far as the unlock check. A route without a capability
32
+ * is development-only: default-deny means undeclared is denied.
33
+ */
34
+ async function checkHudRequest(request, capability, options = {}) {
35
+ const settings = (options.settings ?? readAccessSettings)();
36
+ const { env, enabled, password } = settings;
37
+ if (!isHudVisible(env, enabled)) return {
38
+ ok: false,
39
+ status: 404,
40
+ error: "not found"
41
+ };
42
+ if (env !== "development" && (capability === void 0 || !hasCapability(env, capability, resolveMatrix(options.matrix)))) return {
43
+ ok: false,
44
+ status: 403,
45
+ error: "forbidden"
46
+ };
47
+ const access = resolveHudAccess(env);
48
+ if (!hudRequiresUnlock(access)) return {
49
+ ok: true,
50
+ env
51
+ };
52
+ if (!password) return {
53
+ ok: false,
54
+ status: 401,
55
+ error: "hud unlock is not configured"
56
+ };
57
+ const token = readCookie(request.headers.get("cookie"), HUD_UNLOCK_COOKIE);
58
+ if (!await verifyHudUnlockToken(token, signingSecret(settings), env, { ttlMs: hudUnlockTtlMs(access) })) return {
59
+ ok: false,
60
+ status: 401,
61
+ error: "hud is locked"
62
+ };
63
+ return {
64
+ ok: true,
65
+ env
66
+ };
67
+ }
68
+ /** Whether the request already carries a valid unlock (the status route). */
69
+ async function isHudRequestUnlocked(request, options = {}) {
70
+ const settings = (options.settings ?? readAccessSettings)();
71
+ const access = resolveHudAccess(settings.env);
72
+ if (access === "denied") return false;
73
+ if (!hudRequiresUnlock(access)) return true;
74
+ if (!settings.password) return false;
75
+ const token = readCookie(request.headers.get("cookie"), HUD_UNLOCK_COOKIE);
76
+ return verifyHudUnlockToken(token, signingSecret(settings), settings.env, { ttlMs: hudUnlockTtlMs(access) });
77
+ }
78
+ //#endregion
79
+ //#region src/plugins/access/server/unlock.ts
80
+ /**
81
+ * The unlock route, `<basePath>/access/unlock`. Verification is an HMAC over
82
+ * the tier and the token's own issue time (see ../access.ts), so nothing is
83
+ * stored server side apart from the failed-attempt budget.
84
+ */
85
+ /**
86
+ * Failed-attempt budget, per process. The password is the only barrier in
87
+ * front of a deployed HUD, and a pod lives long enough that unlimited
88
+ * guessing is a real risk. Deliberately crude: an in-memory counter is
89
+ * enough friction for an operator-facing surface, and it cannot fail open
90
+ * the way a shared store can when it is unreachable.
91
+ *
92
+ * The throttle is checked before the password, so a throttled client gets
93
+ * 429 even for the right one; a guess that lands while throttled is worth
94
+ * nothing. Two budgets:
95
+ *
96
+ * - ten failures per client;
97
+ * - a ceiling of a hundred across all clients, against address rotation.
98
+ * It blocks only clients with recent failures of their own, so an
99
+ * operator who has not mistyped still unlocks during an attack.
100
+ */
101
+ const ATTEMPT_WINDOW_MS = 6e5;
102
+ const MAX_FAILED_ATTEMPTS = 10;
103
+ const MAX_FAILED_GLOBAL = 100;
104
+ /** Most clients tracked at once. */
105
+ const MAX_TRACKED = 1e3;
106
+ /**
107
+ * Requests without client headers share this key. It has no per-client
108
+ * limit (it would let anyone lock out everyone), only the global ceiling.
109
+ */
110
+ const UNKNOWN_CLIENT = "unknown";
111
+ const failures = /* @__PURE__ */ new Map();
112
+ let globalFailures = {
113
+ count: 0,
114
+ firstAt: 0
115
+ };
116
+ function isLive(bucket, now) {
117
+ return now - bucket.firstAt <= ATTEMPT_WINDOW_MS;
118
+ }
119
+ /**
120
+ * `x-real-ip` when the proxy sets it, else the last `x-forwarded-for` entry:
121
+ * the one the trusted proxy appended. Entries before it are whatever the
122
+ * client sent, so keying on them would let a client pick its own bucket.
123
+ */
124
+ function clientKey(request) {
125
+ const realIp = request.headers.get("x-real-ip")?.trim();
126
+ if (realIp) return realIp;
127
+ return request.headers.get("x-forwarded-for")?.split(",").at(-1)?.trim() || UNKNOWN_CLIENT;
128
+ }
129
+ function isThrottled(key, now) {
130
+ const entry = failures.get(key);
131
+ if (!entry || !isLive(entry, now)) return false;
132
+ if (key !== UNKNOWN_CLIENT && entry.count >= MAX_FAILED_ATTEMPTS) return true;
133
+ return isLive(globalFailures, now) && globalFailures.count >= MAX_FAILED_GLOBAL;
134
+ }
135
+ /** Makes room for one more client without resetting a bucket close to its limit. */
136
+ function makeRoom(now) {
137
+ if (failures.size < MAX_TRACKED) return true;
138
+ for (const [key, tracked] of failures) if (!isLive(tracked, now)) failures.delete(key);
139
+ if (failures.size < MAX_TRACKED) return true;
140
+ for (const [key, tracked] of failures) if (tracked.count < MAX_FAILED_ATTEMPTS / 2) {
141
+ failures.delete(key);
142
+ return true;
143
+ }
144
+ return false;
145
+ }
146
+ function recordFailure(key, now) {
147
+ if (isLive(globalFailures, now)) globalFailures.count += 1;
148
+ else globalFailures = {
149
+ count: 1,
150
+ firstAt: now
151
+ };
152
+ const entry = failures.get(key);
153
+ if (entry && isLive(entry, now)) {
154
+ entry.count += 1;
155
+ return;
156
+ }
157
+ failures.delete(key);
158
+ if (makeRoom(now)) failures.set(key, {
159
+ count: 1,
160
+ firstAt: now
161
+ });
162
+ }
163
+ /**
164
+ * GET: what the client needs to decide between the panels and the lock, and
165
+ * nothing more. The route is reachable while locked, so it names no tier.
166
+ */
167
+ async function handleHudUnlockStatus(request, options = {}) {
168
+ const settings = (options.settings ?? readAccessSettings)();
169
+ const { env, enabled, password } = settings;
170
+ if (!isHudVisible(env, enabled)) return {
171
+ status: 404,
172
+ body: { error: "not found" }
173
+ };
174
+ const required = hudRequiresUnlock(resolveHudAccess(env));
175
+ return {
176
+ status: 200,
177
+ body: {
178
+ unlocked: await isHudRequestUnlocked(request, { settings: () => settings }),
179
+ configured: !required || Boolean(password)
180
+ }
181
+ };
182
+ }
183
+ /** POST: exchanges the password for a signed, expiring unlock cookie. */
184
+ async function handleHudUnlockAttempt(request, options = {}) {
185
+ const settings = (options.settings ?? readAccessSettings)();
186
+ const { env, enabled, password: expected } = settings;
187
+ if (!isHudVisible(env, enabled)) return {
188
+ status: 404,
189
+ body: { error: "not found" }
190
+ };
191
+ const access = resolveHudAccess(env);
192
+ if (!hudRequiresUnlock(access)) return {
193
+ status: 200,
194
+ body: {
195
+ unlocked: true,
196
+ required: false
197
+ }
198
+ };
199
+ const secret = signingSecret(settings);
200
+ if (!expected || !secret) return {
201
+ status: 503,
202
+ body: {
203
+ unlocked: false,
204
+ configured: false,
205
+ error: "hud unlock is not configured"
206
+ }
207
+ };
208
+ const now = Date.now();
209
+ const key = clientKey(request);
210
+ if (isThrottled(key, now)) return {
211
+ status: 429,
212
+ body: {
213
+ unlocked: false,
214
+ error: "too many attempts, try again later"
215
+ }
216
+ };
217
+ let submitted = "";
218
+ try {
219
+ const payload = await request.json();
220
+ if (typeof payload?.password === "string") submitted = payload.password;
221
+ } catch {}
222
+ if (!submitted || !timingSafeEqual(submitted, expected)) {
223
+ recordFailure(key, now);
224
+ return {
225
+ status: 401,
226
+ body: {
227
+ unlocked: false,
228
+ error: "wrong password"
229
+ }
230
+ };
231
+ }
232
+ failures.delete(key);
233
+ const ttlMs = hudUnlockTtlMs(access);
234
+ return {
235
+ status: 200,
236
+ body: {
237
+ unlocked: true,
238
+ expiresInMs: ttlMs
239
+ },
240
+ setCookie: hudUnlockCookieString(await signHudUnlockToken(secret, env, now), ttlMs)
241
+ };
242
+ }
243
+ /** Test seam: the attempt budget is process state, not request state. */
244
+ function resetHudUnlockAttempts() {
245
+ failures.clear();
246
+ globalFailures = {
247
+ count: 0,
248
+ firstAt: 0
249
+ };
250
+ }
251
+ /** Test seam: how many clients the attempt budget tracks. */
252
+ function trackedHudUnlockClients() {
253
+ return failures.size;
254
+ }
255
+ //#endregion
256
+ //#region src/plugins/access/server.ts
257
+ function toResponse({ status, body, setCookie }) {
258
+ const res = Response.json(body, {
259
+ status,
260
+ headers: { "cache-control": "no-store" }
261
+ });
262
+ if (setCookie) res.headers.append("set-cookie", setCookie);
263
+ return res;
264
+ }
265
+ function splitPath(path) {
266
+ return path.split("/").filter(Boolean);
267
+ }
268
+ /**
269
+ * The guard for `createHudHandler({ guard })` on deployed tiers. It answers
270
+ * exactly like the old `checkHudRequest`: 404 while the tier has not opted in,
271
+ * 403 for a capability the tier's matrix denies (before any password is
272
+ * considered), 401 until the request carries a valid unlock cookie. In
273
+ * development it lets everything through, like `developmentOnly`.
274
+ *
275
+ * The unlock route itself is the one exemption, so a locked HUD can unlock.
276
+ * Pass the handler's `basePath` when it is not `/api/hud`.
277
+ */
278
+ function accessGuard(options = {}) {
279
+ const settings = options.settings ?? readAccessSettings;
280
+ const unlockPath = [
281
+ ...splitPath(options.basePath ?? "/api/hud"),
282
+ "access",
283
+ "unlock"
284
+ ].join("/");
285
+ return async (request, capability) => {
286
+ const path = splitPath(new URL(request.url).pathname).join("/");
287
+ if (capability === "access.unlock" && path === unlockPath) {
288
+ const { env, enabled } = settings();
289
+ return isHudVisible(env, enabled) ? null : Response.json({
290
+ ok: false,
291
+ error: "not found"
292
+ }, { status: 404 });
293
+ }
294
+ const result = await checkHudRequest(request, capability, {
295
+ ...options,
296
+ settings
297
+ });
298
+ if (result.ok) return null;
299
+ return Response.json({
300
+ ok: false,
301
+ error: result.error
302
+ }, {
303
+ status: result.status,
304
+ headers: { "cache-control": "no-store" }
305
+ });
306
+ };
307
+ }
308
+ /**
309
+ * Server half of the access plugin: `GET|POST <basePath>/access/unlock`.
310
+ * GET reports `{ unlocked, configured }`; POST trades `{ password }` for an
311
+ * httpOnly cookie. Wrong passwords are budgeted: ten per client per ten
312
+ * minutes, and past a hundred overall every client with failures of its own
313
+ * is throttled; a throttled client gets 429 even for the right password.
314
+ *
315
+ * ```ts
316
+ * const options = {};
317
+ * export const { GET, POST } = createHudHandler([accessServer(options), ...], {
318
+ * guard: accessGuard(options),
319
+ * });
320
+ * ```
321
+ */
322
+ function accessServer(options = {}) {
323
+ return definePlugin({
324
+ id: "access",
325
+ capability: ACCESS_UNLOCK_CAPABILITY,
326
+ routes: { "/unlock": {
327
+ GET: async (request) => request.method === "GET" ? toResponse(await handleHudUnlockStatus(request, options)) : Response.json({
328
+ ok: false,
329
+ error: "method not allowed"
330
+ }, {
331
+ status: 405,
332
+ headers: { allow: "GET, POST" }
333
+ }),
334
+ POST: async (request) => toResponse(await handleHudUnlockAttempt(request, options))
335
+ } }
336
+ });
337
+ }
338
+ //#endregion
339
+ export { ACCESS_UNLOCK_CAPABILITY, CAPABILITY_MATRIX, HUD_CAPABILITIES, HUD_UNLOCK_COOKIE, accessGuard, accessServer, checkHudRequest, handleHudUnlockAttempt, handleHudUnlockStatus, hasCapability, hudRequiresUnlock, isHudRequestUnlocked, isHudVisible, readAccessSettings, resetHudUnlockAttempts, resolveHudAccess, signHudUnlockToken, trackedHudUnlockClients, verifyHudUnlockToken };
@@ -0,0 +1,41 @@
1
+ import { c as HudPluginProps, r as HudCapability } from "../plugins-CQ_0w1ot.js";
2
+ import "../index-BYsdS-7d.js";
3
+ import { c as CAPABILITY_MATRIX, d as hasCapability, i as resolveHudAccess, l as CapabilityMatrixOverride, n as HudAccess, r as hudRequiresUnlock, s as ACCESS_UNLOCK_CAPABILITY, u as HUD_CAPABILITIES } from "../access-CNvJOwKM.js";
4
+ //#region src/plugins/access/access-panel.d.ts
5
+ declare function AccessPill(): import("react").JSX.Element;
6
+ //#endregion
7
+ //#region src/plugins/access/unlock-store.d.ts
8
+ type UnlockStatus = "checking" | "locked" | "unlocked" | "unconfigured";
9
+ /**
10
+ * `HudAccessControl.useUnlocked`: development is open, a disabled or unknown
11
+ * tier never unlocks, every deployed tier asks the server once per mount.
12
+ */
13
+ declare function useUnlocked(envName: string, apiBase: string): boolean;
14
+ //#endregion
15
+ //#region src/plugins/access/index.d.ts
16
+ export interface AccessOptions {
17
+ /** Replaces the default matrix's list for a deployed tier. Keep it equal to the server half's. */
18
+ matrix?: CapabilityMatrixOverride;
19
+ }
20
+ /**
21
+ * Opt-in for deployed tiers. Without it `defineHud` renders nothing outside
22
+ * development. With it the HUD follows the env ladder: a tier's capability
23
+ * matrix narrows the plugins, and until the operator unlocks with the
24
+ * server's password only this plugin's lock shows.
25
+ *
26
+ * Client half; the password check lives in `@hansenexus/hud/plugins/access/server`
27
+ * (`accessServer()` + `accessGuard()`), and the password never reaches the browser.
28
+ */
29
+ export declare function access(options?: AccessOptions): {
30
+ readonly id: "access";
31
+ readonly title: "Access";
32
+ readonly capability: "access.unlock";
33
+ readonly pill: typeof AccessPill;
34
+ readonly panel: ({ ctx }: HudPluginProps) => import("react").JSX.Element;
35
+ readonly access: {
36
+ readonly can: (env: string, capability: HudCapability) => boolean;
37
+ readonly useUnlocked: typeof useUnlocked;
38
+ };
39
+ };
40
+ //#endregion
41
+ export { ACCESS_UNLOCK_CAPABILITY, CAPABILITY_MATRIX, type CapabilityMatrixOverride, HUD_CAPABILITIES, type HudAccess, type UnlockStatus, hasCapability, hudRequiresUnlock, resolveHudAccess };