@adonis-agora/durable 1.0.0-rc.0 → 1.0.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +122 -0
- package/dist/assets/spa/assets/index-B2kIfzsc.css +1 -0
- package/dist/assets/spa/assets/index-Bgf_8IJ0.js +16 -0
- package/dist/assets/spa/assets/index-Bgf_8IJ0.js.map +1 -0
- package/dist/assets/spa/index.html +2 -2
- package/dist/commands/runs.d.ts.map +1 -1
- package/dist/commands/runs.js +4 -2
- package/dist/commands/runs.js.map +1 -1
- package/dist/providers/dashboard_provider.d.ts +15 -49
- package/dist/providers/dashboard_provider.d.ts.map +1 -1
- package/dist/providers/dashboard_provider.js +69 -244
- package/dist/providers/dashboard_provider.js.map +1 -1
- package/dist/providers/durable_provider.d.ts.map +1 -1
- package/dist/providers/durable_provider.js +11 -3
- package/dist/providers/durable_provider.js.map +1 -1
- package/dist/src/commands/runs.js +1 -1
- package/dist/src/commands/runs.js.map +1 -1
- package/dist/src/concurrency.d.ts.map +1 -1
- package/dist/src/concurrency.js +1 -0
- package/dist/src/concurrency.js.map +1 -1
- package/dist/src/control-flow-signal.d.ts +5 -4
- package/dist/src/control-flow-signal.d.ts.map +1 -1
- package/dist/src/control-flow-signal.js +5 -4
- package/dist/src/control-flow-signal.js.map +1 -1
- package/dist/src/dashboard/access_denied_page.d.ts +5 -7
- package/dist/src/dashboard/access_denied_page.d.ts.map +1 -1
- package/dist/src/dashboard/access_denied_page.js +2 -6
- package/dist/src/dashboard/access_denied_page.js.map +1 -1
- package/dist/src/dashboard/console_authorize.d.ts +76 -0
- package/dist/src/dashboard/console_authorize.d.ts.map +1 -0
- package/dist/src/dashboard/console_authorize.js +101 -0
- package/dist/src/dashboard/console_authorize.js.map +1 -0
- package/dist/src/dashboard/define_config.d.ts +44 -81
- package/dist/src/dashboard/define_config.d.ts.map +1 -1
- package/dist/src/dashboard/define_config.js +16 -112
- package/dist/src/dashboard/define_config.js.map +1 -1
- package/dist/src/dashboard/handlers.d.ts.map +1 -1
- package/dist/src/dashboard/handlers.js +6 -3
- package/dist/src/dashboard/handlers.js.map +1 -1
- package/dist/src/dashboard/index.d.ts +5 -8
- package/dist/src/dashboard/index.d.ts.map +1 -1
- package/dist/src/dashboard/index.js +4 -6
- package/dist/src/dashboard/index.js.map +1 -1
- package/dist/src/dashboard/openapi.d.ts.map +1 -1
- package/dist/src/dashboard/openapi.js +11 -1
- package/dist/src/dashboard/openapi.js.map +1 -1
- package/dist/src/dashboard/run-filter.d.ts.map +1 -1
- package/dist/src/dashboard/run-filter.js +1 -0
- package/dist/src/dashboard/run-filter.js.map +1 -1
- package/dist/src/define_config.d.ts +6 -0
- package/dist/src/define_config.d.ts.map +1 -1
- package/dist/src/define_config.js.map +1 -1
- package/dist/src/diagnostics/index.d.ts +11 -0
- package/dist/src/diagnostics/index.d.ts.map +1 -0
- package/dist/src/diagnostics/index.js +6 -0
- package/dist/src/diagnostics/index.js.map +1 -0
- package/dist/src/diagnostics/transport.d.ts +71 -0
- package/dist/src/diagnostics/transport.d.ts.map +1 -0
- package/dist/src/diagnostics/transport.js +188 -0
- package/dist/src/diagnostics/transport.js.map +1 -0
- package/dist/src/diagnostics-bridge.d.ts +2 -1
- package/dist/src/diagnostics-bridge.d.ts.map +1 -1
- package/dist/src/diagnostics-bridge.js +7 -1
- package/dist/src/diagnostics-bridge.js.map +1 -1
- package/dist/src/engine.d.ts +30 -8
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +198 -72
- package/dist/src/engine.js.map +1 -1
- package/dist/src/errors.d.ts +9 -0
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +13 -0
- package/dist/src/errors.js.map +1 -1
- package/dist/src/event-trigger-bridge.d.ts +5 -14
- package/dist/src/event-trigger-bridge.d.ts.map +1 -1
- package/dist/src/event-trigger-bridge.js +11 -99
- package/dist/src/event-trigger-bridge.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/interfaces.d.ts +22 -6
- package/dist/src/interfaces.d.ts.map +1 -1
- package/dist/src/interfaces.js +8 -0
- package/dist/src/interfaces.js.map +1 -1
- package/dist/src/origin.js +3 -3
- package/dist/src/scheduler.d.ts.map +1 -1
- package/dist/src/scheduler.js +2 -1
- package/dist/src/scheduler.js.map +1 -1
- package/dist/src/singleton-admission.d.ts.map +1 -1
- package/dist/src/singleton-admission.js +2 -0
- package/dist/src/singleton-admission.js.map +1 -1
- package/dist/src/singleton-gate.d.ts +1 -1
- package/dist/src/singleton-gate.d.ts.map +1 -1
- package/dist/src/singleton-gate.js +4 -3
- package/dist/src/singleton-gate.js.map +1 -1
- package/dist/src/stores/lucid-pg-hot-path.d.ts +6 -2
- package/dist/src/stores/lucid-pg-hot-path.d.ts.map +1 -1
- package/dist/src/stores/lucid-pg-hot-path.js +7 -3
- package/dist/src/stores/lucid-pg-hot-path.js.map +1 -1
- package/dist/src/stores/lucid.d.ts +15 -2
- package/dist/src/stores/lucid.d.ts.map +1 -1
- package/dist/src/stores/lucid.js +191 -49
- package/dist/src/stores/lucid.js.map +1 -1
- package/dist/src/testing/in-memory-state-store.js +3 -3
- package/dist/src/testing/in-memory-state-store.js.map +1 -1
- package/dist/src/testing-kit/state-store-conformance.d.ts.map +1 -1
- package/dist/src/testing-kit/state-store-conformance.js +13 -0
- package/dist/src/testing-kit/state-store-conformance.js.map +1 -1
- package/dist/src/testing-kit/transport-conformance.d.ts.map +1 -1
- package/dist/src/testing-kit/transport-conformance.js +5 -1
- package/dist/src/testing-kit/transport-conformance.js.map +1 -1
- package/dist/src/transports/bullmq/bullmq-transport.d.ts.map +1 -1
- package/dist/src/transports/bullmq/bullmq-transport.js +13 -2
- package/dist/src/transports/bullmq/bullmq-transport.js.map +1 -1
- package/dist/src/transports/db.d.ts.map +1 -1
- package/dist/src/transports/db.js +42 -3
- package/dist/src/transports/db.js.map +1 -1
- package/dist/src/transports/queue.d.ts.map +1 -1
- package/dist/src/transports/queue.js +44 -3
- package/dist/src/transports/queue.js.map +1 -1
- package/dist/src/workflow-discovery.d.ts +1 -1
- package/dist/src/workflow-discovery.d.ts.map +1 -1
- package/dist/src/workflow-discovery.js +4 -7
- package/dist/src/workflow-discovery.js.map +1 -1
- package/dist/src/workflow-events.d.ts +20 -51
- package/dist/src/workflow-events.d.ts.map +1 -1
- package/dist/src/workflow-events.js +12 -30
- package/dist/src/workflow-events.js.map +1 -1
- package/dist/src/workflow-ref.d.ts +1 -1
- package/dist/src/workflow-turn.d.ts.map +1 -1
- package/dist/src/workflow-turn.js +18 -1
- package/dist/src/workflow-turn.js.map +1 -1
- package/dist/stubs/config/durable_dashboard.stub +9 -29
- package/package.json +12 -2
- package/skills/durable-cluster/SKILL.md +8 -4
- package/skills/durable-reliability/SKILL.md +2 -2
- package/skills/durable-setup/SKILL.md +3 -1
- package/dist/assets/spa/assets/index-70hGzOnT.css +0 -1
- package/dist/assets/spa/assets/index-CJjMAUau.js +0 -16
- package/dist/assets/spa/assets/index-CJjMAUau.js.map +0 -1
- package/dist/src/dashboard/auth.d.ts +0 -134
- package/dist/src/dashboard/auth.d.ts.map +0 -1
- package/dist/src/dashboard/auth.js +0 -156
- package/dist/src/dashboard/auth.js.map +0 -1
- package/dist/src/dashboard/login_page.d.ts +0 -36
- package/dist/src/dashboard/login_page.d.ts.map +0 -1
- package/dist/src/dashboard/login_page.js +0 -158
- package/dist/src/dashboard/login_page.js.map +0 -1
- package/dist/src/dashboard/session_cookie.d.ts +0 -45
- package/dist/src/dashboard/session_cookie.d.ts.map +0 -1
- package/dist/src/dashboard/session_cookie.js +0 -97
- package/dist/src/dashboard/session_cookie.js.map +0 -1
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { type DashboardSession, type DashboardSessionUser } from './session_cookie.js';
|
|
2
|
-
/**
|
|
3
|
-
* Framework-light `dashboardAuth` mechanism for the durable dashboard — config resolution, the
|
|
4
|
-
* open-redirect guard, the login decision, and session verification. NO AdonisJS types leak in
|
|
5
|
-
* (mirroring {@link file://./handlers.ts handlers.ts}), so every branch is unit-testable against a
|
|
6
|
-
* plain object with no HTTP server. The provider (`providers/dashboard_provider.ts`) adapts an
|
|
7
|
-
* `HttpContext` to these functions and owns the cookie read/write via AdonisJS's native cookie API.
|
|
8
|
-
*
|
|
9
|
-
* When configured it gates BOTH the dashboard HTML shell (a full-page navigation redirected `302`
|
|
10
|
-
* to a server-rendered login page) and the JSON API (a plain `401`) behind a signed session cookie.
|
|
11
|
-
* When left unconfigured the dashboard behaves exactly as before (no login/logout routes, no
|
|
12
|
-
* session guard).
|
|
13
|
-
*/
|
|
14
|
-
/** Host hook validating submitted credentials from the built-in login page (Mode B — standalone). */
|
|
15
|
-
export type LoginHook = (username: string, password: string) => Promise<DashboardSessionUser | null> | DashboardSessionUser | null;
|
|
16
|
-
/**
|
|
17
|
-
* Host hook validating the HOST APP's own auth off the raw request (Mode A — SSO / "open from your
|
|
18
|
-
* app"). Receives the underlying platform request object (an AdonisJS `Request['request']`, i.e. the
|
|
19
|
-
* raw Node `IncomingMessage`) rather than an AdonisJS type, so this module stays framework-light and
|
|
20
|
-
* unit-testable with no `HttpContext` — the provider passes it through untouched. Mirrors the NestJS
|
|
21
|
-
* sibling console's `SessionHook` byte-for-byte (same two-mode split).
|
|
22
|
-
*/
|
|
23
|
-
export type SessionHook = (request: unknown) => Promise<DashboardSessionUser | null> | DashboardSessionUser | null;
|
|
24
|
-
/**
|
|
25
|
-
* Author-facing `dashboardAuth` option on `config/durable_dashboard.ts`. Gates the dashboard behind
|
|
26
|
-
* a signed session cookie, minted by whichever mode(s) are configured:
|
|
27
|
-
*
|
|
28
|
-
* - Mode B (`login`) — the built-in server-rendered login page (`GET <path>/login`), for a
|
|
29
|
-
* standalone console with its own credentials.
|
|
30
|
-
* - Mode A (`session`) — `POST <path>/session`, for a host app that's already authenticated the
|
|
31
|
-
* operator itself and wants a one-shot "open the console" button (see `client/console-session.ts`).
|
|
32
|
-
*
|
|
33
|
-
* Both may be configured together; at least one is required. The bundled dashboard SPA is unaware of
|
|
34
|
-
* which mode(s) are active — it only reacts to a 401's `{ auth: { modes } }` body (see
|
|
35
|
-
* `durable-client.ts`).
|
|
36
|
-
*
|
|
37
|
-
* Nest needed a `forRootAsync` to reach app services from these hooks; AdonisJS does not — the config
|
|
38
|
-
* module is a plain file and each hook is just a function that may be `async` and close over
|
|
39
|
-
* `app.container`/services (import them at the top of `config/durable_dashboard.ts`), so there is
|
|
40
|
-
* nothing extra to build for the async case.
|
|
41
|
-
*/
|
|
42
|
-
export interface DashboardAuthOptions {
|
|
43
|
-
/** REQUIRED HMAC-SHA256 signing key. Missing/empty => boot error (fail closed). */
|
|
44
|
-
secret: string;
|
|
45
|
-
/** Cookie TTL as a duration string (`'8h'`, `'30m'`, `'7d'`). Default `'8h'`. */
|
|
46
|
-
ttl?: string;
|
|
47
|
-
/** Validates submitted username/password; return the session user, or `null` to deny. Thrown
|
|
48
|
-
* errors are treated as a denial (logged once, never surfaced to the client). May be async. */
|
|
49
|
-
login?: LoginHook;
|
|
50
|
-
/** Validates the host app's own auth off the raw request; return the session user, or `null` to
|
|
51
|
-
* deny. Thrown errors are treated as a denial. May be async. */
|
|
52
|
-
session?: SessionHook;
|
|
53
|
-
}
|
|
54
|
-
/** Which auth surface(s) are active — carried on an API 401's `{ auth: { modes } }` body so the SPA
|
|
55
|
-
* knows whether to offer the login page or just report "not authenticated". */
|
|
56
|
-
export type AuthMode = 'session' | 'login';
|
|
57
|
-
/** Resolved, validated `dashboardAuth` config shared by the guard, the login/session handlers, and
|
|
58
|
-
* the page. */
|
|
59
|
-
export interface ResolvedDashboardAuth {
|
|
60
|
-
secret: string;
|
|
61
|
-
ttlMs: number;
|
|
62
|
-
modes: AuthMode[];
|
|
63
|
-
login?: LoginHook;
|
|
64
|
-
session?: SessionHook;
|
|
65
|
-
}
|
|
66
|
-
/** Cookie name carrying the signed dashboard session. */
|
|
67
|
-
export declare const SESSION_COOKIE_NAME = "durable_dashboard_session";
|
|
68
|
-
/**
|
|
69
|
-
* Validate + resolve the `dashboardAuth` option. Returns `null` when unconfigured (today's
|
|
70
|
-
* unauthenticated behavior, unchanged). Throws at boot (fail closed) when configured but missing a
|
|
71
|
-
* secret, or neither `login` nor `session` is a function — the host learns immediately rather than
|
|
72
|
-
* shipping an un-mintable gate.
|
|
73
|
-
*/
|
|
74
|
-
export declare function resolveDashboardAuth(options: DashboardAuthOptions | undefined): ResolvedDashboardAuth | null;
|
|
75
|
-
/**
|
|
76
|
-
* Guard a client-supplied redirect target against an open redirect: it must be a same-origin,
|
|
77
|
-
* root-relative path (`/durable/foo`), never a protocol-relative (`//evil.com`) or absolute
|
|
78
|
-
* (`https://evil.com`) URL. Falls back to `fallback` (the dashboard's own `basePath`) for anything
|
|
79
|
-
* else, including a missing/non-string value.
|
|
80
|
-
*/
|
|
81
|
-
export declare function sanitizeReturnTo(candidate: unknown, fallback: string): string;
|
|
82
|
-
/**
|
|
83
|
-
* The framework-light outcome of a login attempt, mapped to HTTP by the provider:
|
|
84
|
-
* - `ok` → set `cookieValue` as the session cookie, reply `200 { redirectTo }`.
|
|
85
|
-
* - `bad-request` → `400` (body missing string `username`/`password`).
|
|
86
|
-
* - `unauthorized` → uniform `401 { error }` for EVERY credential failure (unknown user, wrong
|
|
87
|
-
* password, or a throwing hook) — no user-enumeration. `hookError` is set only when the hook
|
|
88
|
-
* threw, so the provider can warn-log it once without changing the client-visible response.
|
|
89
|
-
*/
|
|
90
|
-
export type LoginOutcome = {
|
|
91
|
-
kind: 'ok';
|
|
92
|
-
cookieValue: string;
|
|
93
|
-
redirectTo: string;
|
|
94
|
-
} | {
|
|
95
|
-
kind: 'bad-request';
|
|
96
|
-
message: string;
|
|
97
|
-
} | {
|
|
98
|
-
kind: 'unauthorized';
|
|
99
|
-
message: string;
|
|
100
|
-
hookError?: unknown;
|
|
101
|
-
};
|
|
102
|
-
/**
|
|
103
|
-
* Decide a login attempt end-to-end (validate body → run hook → mint cookie) with no HTTP types.
|
|
104
|
-
* On success returns the signed cookie value to set plus the sanitized `redirectTo`; the password
|
|
105
|
-
* is forwarded to the hook AS-IS (empty string when blank), so the hook alone decides whether a
|
|
106
|
-
* password is required. `404`-worthy (Mode B not configured) is the provider's call, not this
|
|
107
|
-
* function's — it only runs once the provider has confirmed `auth.login` exists.
|
|
108
|
-
*/
|
|
109
|
-
export declare function performLogin(auth: ResolvedDashboardAuth, body: unknown, basePath: string, now?: number): Promise<LoginOutcome>;
|
|
110
|
-
/**
|
|
111
|
-
* The framework-light outcome of a Mode A session-mint attempt (`POST <path>/session`), mapped to
|
|
112
|
-
* HTTP by the provider: `ok` → set the cookie, reply `204`; `unauthorized` → uniform `401` (the
|
|
113
|
-
* `session` hook returned/threw a denial).
|
|
114
|
-
*/
|
|
115
|
-
export type SessionOutcome = {
|
|
116
|
-
kind: 'ok';
|
|
117
|
-
cookieValue: string;
|
|
118
|
-
} | {
|
|
119
|
-
kind: 'unauthorized';
|
|
120
|
-
hookError?: unknown;
|
|
121
|
-
};
|
|
122
|
-
/**
|
|
123
|
-
* Decide a Mode A session-mint attempt end-to-end: run the host's `session` hook against the raw
|
|
124
|
-
* platform request, and mint the cookie on success. No body to validate — the hook reads whatever it
|
|
125
|
-
* needs directly off `request` (its own cookie/JWT/header).
|
|
126
|
-
*/
|
|
127
|
-
export declare function performSession(auth: ResolvedDashboardAuth, request: unknown, now?: number): Promise<SessionOutcome>;
|
|
128
|
-
/**
|
|
129
|
-
* Read + verify a session cookie value, returning the session or `null` when absent, tampered, or
|
|
130
|
-
* expired. Thin wrapper over {@link verifySessionCookie} so the provider never touches crypto.
|
|
131
|
-
*/
|
|
132
|
-
export declare function readSession(auth: ResolvedDashboardAuth, cookieValue: string | undefined, now?: number): DashboardSession | null;
|
|
133
|
-
export type { DashboardSession, DashboardSessionUser } from './session_cookie.js';
|
|
134
|
-
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/dashboard/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EAG1B,MAAM,qBAAqB,CAAC;AAE7B;;;;;;;;;;;GAWG;AAEH,qGAAqG;AACrG,MAAM,MAAM,SAAS,GAAG,CACtB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,KACb,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,GAAG,oBAAoB,GAAG,IAAI,CAAC;AAExE;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GAAG,CACxB,OAAO,EAAE,OAAO,KACb,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,GAAG,oBAAoB,GAAG,IAAI,CAAC;AAExE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,oBAAoB;IACnC,mFAAmF;IACnF,MAAM,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;oGACgG;IAChG,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;qEACiE;IACjE,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED;gFACgF;AAChF,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC;AAE3C;gBACgB;AAChB,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,8BAA8B,CAAC;AAoB/D;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,oBAAoB,GAAG,SAAS,GACxC,qBAAqB,GAAG,IAAI,CA0B9B;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAS7E;AAaD;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GACvD;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAoBnE;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,qBAAqB,EAC3B,IAAI,EAAE,OAAO,EACb,QAAQ,EAAE,MAAM,EAChB,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,YAAY,CAAC,CAuBvB;AAED;;;;GAIG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAElD;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,qBAAqB,EAC3B,OAAO,EAAE,OAAO,EAChB,GAAG,CAAC,EAAE,MAAM,GACX,OAAO,CAAC,cAAc,CAAC,CAezB;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,qBAAqB,EAC3B,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,GAAG,CAAC,EAAE,MAAM,GACX,gBAAgB,GAAG,IAAI,CAMzB;AAED,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { signSessionCookie, verifySessionCookie, } from './session_cookie.js';
|
|
2
|
-
/** Cookie name carrying the signed dashboard session. */
|
|
3
|
-
export const SESSION_COOKIE_NAME = 'durable_dashboard_session';
|
|
4
|
-
const DEFAULT_TTL = '8h';
|
|
5
|
-
const DEFAULT_TTL_MS = 8 * 60 * 60 * 1000;
|
|
6
|
-
const DURATION_UNITS = {
|
|
7
|
-
s: 1000,
|
|
8
|
-
m: 60 * 1000,
|
|
9
|
-
h: 60 * 60 * 1000,
|
|
10
|
-
d: 24 * 60 * 60 * 1000,
|
|
11
|
-
};
|
|
12
|
-
/** Parse a `'<number><s|m|h|d>'` duration to ms; falls back to the 8h default on a bad value. */
|
|
13
|
-
function durationToMs(ttl) {
|
|
14
|
-
const match = /^(\d+)([smhd])$/.exec(ttl.trim());
|
|
15
|
-
if (!match)
|
|
16
|
-
return DEFAULT_TTL_MS;
|
|
17
|
-
const unit = DURATION_UNITS[match[2] ?? ''];
|
|
18
|
-
if (unit === undefined)
|
|
19
|
-
return DEFAULT_TTL_MS;
|
|
20
|
-
return Number(match[1]) * unit;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Validate + resolve the `dashboardAuth` option. Returns `null` when unconfigured (today's
|
|
24
|
-
* unauthenticated behavior, unchanged). Throws at boot (fail closed) when configured but missing a
|
|
25
|
-
* secret, or neither `login` nor `session` is a function — the host learns immediately rather than
|
|
26
|
-
* shipping an un-mintable gate.
|
|
27
|
-
*/
|
|
28
|
-
export function resolveDashboardAuth(options) {
|
|
29
|
-
if (options === undefined)
|
|
30
|
-
return null;
|
|
31
|
-
if (typeof options.secret !== 'string' || options.secret === '') {
|
|
32
|
-
throw new Error('durable_dashboard: dashboardAuth.secret is required and must be a non-empty string ' +
|
|
33
|
-
'(HMAC-SHA256 signing key, 32+ bytes recommended). Failing closed.');
|
|
34
|
-
}
|
|
35
|
-
const hasLogin = typeof options.login === 'function';
|
|
36
|
-
const hasSession = typeof options.session === 'function';
|
|
37
|
-
if (!hasLogin && !hasSession) {
|
|
38
|
-
throw new Error('durable_dashboard: dashboardAuth needs at least one of `login` or `session` (a login hook or ' +
|
|
39
|
-
'a host-session hook).');
|
|
40
|
-
}
|
|
41
|
-
const modes = [];
|
|
42
|
-
if (hasSession)
|
|
43
|
-
modes.push('session');
|
|
44
|
-
if (hasLogin)
|
|
45
|
-
modes.push('login');
|
|
46
|
-
return {
|
|
47
|
-
secret: options.secret,
|
|
48
|
-
ttlMs: durationToMs(options.ttl ?? DEFAULT_TTL),
|
|
49
|
-
modes,
|
|
50
|
-
...(hasLogin ? { login: options.login } : {}),
|
|
51
|
-
...(hasSession ? { session: options.session } : {}),
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Guard a client-supplied redirect target against an open redirect: it must be a same-origin,
|
|
56
|
-
* root-relative path (`/durable/foo`), never a protocol-relative (`//evil.com`) or absolute
|
|
57
|
-
* (`https://evil.com`) URL. Falls back to `fallback` (the dashboard's own `basePath`) for anything
|
|
58
|
-
* else, including a missing/non-string value.
|
|
59
|
-
*/
|
|
60
|
-
export function sanitizeReturnTo(candidate, fallback) {
|
|
61
|
-
if (typeof candidate !== 'string')
|
|
62
|
-
return fallback;
|
|
63
|
-
if (!candidate.startsWith('/'))
|
|
64
|
-
return fallback;
|
|
65
|
-
if (candidate.startsWith('//'))
|
|
66
|
-
return fallback;
|
|
67
|
-
if (candidate.includes('://'))
|
|
68
|
-
return fallback;
|
|
69
|
-
// Browsers normalize backslashes to slashes in URLs, so `/\evil.com` survives every check above
|
|
70
|
-
// yet navigates to `https://evil.com` — reject any backslash outright.
|
|
71
|
-
if (candidate.includes('\\'))
|
|
72
|
-
return fallback;
|
|
73
|
-
return candidate;
|
|
74
|
-
}
|
|
75
|
-
function isString(value) {
|
|
76
|
-
return typeof value === 'string';
|
|
77
|
-
}
|
|
78
|
-
const UNAUTHORIZED_MESSAGE = 'Invalid username or password.';
|
|
79
|
-
/**
|
|
80
|
-
* Run a host hook (`login` or `session`) defensively: a throw is treated as a denial (uniform
|
|
81
|
-
* failure) so a buggy hook never 500s the endpoint into a stack-trace leak. The thrown error is
|
|
82
|
-
* surfaced on the outcome (`hookError`) so the provider can warn-log it once, but it never reaches
|
|
83
|
-
* the client.
|
|
84
|
-
*/
|
|
85
|
-
async function runHook(hook) {
|
|
86
|
-
try {
|
|
87
|
-
return { user: (await hook()) ?? null };
|
|
88
|
-
}
|
|
89
|
-
catch (error) {
|
|
90
|
-
return { user: null, error };
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Decide a login attempt end-to-end (validate body → run hook → mint cookie) with no HTTP types.
|
|
95
|
-
* On success returns the signed cookie value to set plus the sanitized `redirectTo`; the password
|
|
96
|
-
* is forwarded to the hook AS-IS (empty string when blank), so the hook alone decides whether a
|
|
97
|
-
* password is required. `404`-worthy (Mode B not configured) is the provider's call, not this
|
|
98
|
-
* function's — it only runs once the provider has confirmed `auth.login` exists.
|
|
99
|
-
*/
|
|
100
|
-
export async function performLogin(auth, body, basePath, now) {
|
|
101
|
-
const parsed = (body ?? {});
|
|
102
|
-
if (!isString(parsed.username) || !isString(parsed.password)) {
|
|
103
|
-
return { kind: 'bad-request', message: 'Body must include string `username` and `password`.' };
|
|
104
|
-
}
|
|
105
|
-
const login = auth.login;
|
|
106
|
-
if (!login) {
|
|
107
|
-
return { kind: 'unauthorized', message: UNAUTHORIZED_MESSAGE };
|
|
108
|
-
}
|
|
109
|
-
const { user, error } = await runHook(() => login(parsed.username, parsed.password));
|
|
110
|
-
if (!user) {
|
|
111
|
-
return error !== undefined
|
|
112
|
-
? { kind: 'unauthorized', message: UNAUTHORIZED_MESSAGE, hookError: error }
|
|
113
|
-
: { kind: 'unauthorized', message: UNAUTHORIZED_MESSAGE };
|
|
114
|
-
}
|
|
115
|
-
const cookieValue = signSessionCookie(user, {
|
|
116
|
-
secret: auth.secret,
|
|
117
|
-
ttlMs: auth.ttlMs,
|
|
118
|
-
...(now !== undefined ? { now } : {}),
|
|
119
|
-
});
|
|
120
|
-
return { kind: 'ok', cookieValue, redirectTo: sanitizeReturnTo(parsed.returnTo, basePath) };
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Decide a Mode A session-mint attempt end-to-end: run the host's `session` hook against the raw
|
|
124
|
-
* platform request, and mint the cookie on success. No body to validate — the hook reads whatever it
|
|
125
|
-
* needs directly off `request` (its own cookie/JWT/header).
|
|
126
|
-
*/
|
|
127
|
-
export async function performSession(auth, request, now) {
|
|
128
|
-
const session = auth.session;
|
|
129
|
-
if (!session)
|
|
130
|
-
return { kind: 'unauthorized' };
|
|
131
|
-
const { user, error } = await runHook(() => session(request));
|
|
132
|
-
if (!user) {
|
|
133
|
-
return error !== undefined
|
|
134
|
-
? { kind: 'unauthorized', hookError: error }
|
|
135
|
-
: { kind: 'unauthorized' };
|
|
136
|
-
}
|
|
137
|
-
const cookieValue = signSessionCookie(user, {
|
|
138
|
-
secret: auth.secret,
|
|
139
|
-
ttlMs: auth.ttlMs,
|
|
140
|
-
...(now !== undefined ? { now } : {}),
|
|
141
|
-
});
|
|
142
|
-
return { kind: 'ok', cookieValue };
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Read + verify a session cookie value, returning the session or `null` when absent, tampered, or
|
|
146
|
-
* expired. Thin wrapper over {@link verifySessionCookie} so the provider never touches crypto.
|
|
147
|
-
*/
|
|
148
|
-
export function readSession(auth, cookieValue, now) {
|
|
149
|
-
if (cookieValue === undefined || cookieValue === '')
|
|
150
|
-
return null;
|
|
151
|
-
return verifySessionCookie(cookieValue, {
|
|
152
|
-
secret: auth.secret,
|
|
153
|
-
...(now !== undefined ? { now } : {}),
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
//# sourceMappingURL=auth.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/dashboard/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The built-in `dashboardAuth` login page (`GET <basePath>/login`). Deliberately a small,
|
|
3
|
-
* dependency-free, hand-authored HTML page — NOT part of the bundled dashboard SPA — so gating the
|
|
4
|
-
* dashboard shell doesn't require rebuilding or extending the bundled SPA.
|
|
5
|
-
*
|
|
6
|
-
* It works WITHOUT JavaScript: the markup is a classic `<form method="post">` carrying `returnTo`
|
|
7
|
-
* in a hidden field, and the provider answers a form-encoded `POST` with a redirect (to `returnTo`
|
|
8
|
-
* on success, back here with `?error` on failure). The inline `<script>` is progressive
|
|
9
|
-
* enhancement only — it swaps the full-page round trip for a JSON `fetch` and shows the error in
|
|
10
|
-
* place — and it carries the request's CSP nonce when the provider hands one over, so a host
|
|
11
|
-
* running `@adonisjs/shield`'s `script-src 'self' @nonce` keeps the enhancement instead of a
|
|
12
|
-
* dead form. The `<style>` takes the same nonce.
|
|
13
|
-
*
|
|
14
|
-
* The only per-request values interpolated are `returnTo` (already passed through
|
|
15
|
-
* {@link sanitizeReturnTo}, so it is a root-relative path) and the `error` flag; both go through
|
|
16
|
-
* {@link escapeHtml} (and `JSON.stringify` inside the script) so nothing in the query string can
|
|
17
|
-
* break out of an attribute or the script. `basePath` is developer-controlled config.
|
|
18
|
-
*
|
|
19
|
-
* The visual language (dark zinc card, mono type, emerald accent) mirrors the Agora consoles so
|
|
20
|
-
* they feel like one family.
|
|
21
|
-
*
|
|
22
|
-
* The password input has NO `required` attribute and the value is forwarded AS-IS (empty string
|
|
23
|
-
* when blank): the host's `login` hook — not this page — decides whether a password matters, so an
|
|
24
|
-
* email-only host (gate on username, ignore password) works with the same page as a host with real
|
|
25
|
-
* passwords.
|
|
26
|
-
*/
|
|
27
|
-
export interface LoginPageOptions {
|
|
28
|
-
/** The request's CSP nonce, applied to the inline `<style>` and `<script>`. */
|
|
29
|
-
nonce?: string;
|
|
30
|
-
/** Render the "Invalid username or password." notice (the no-JS failure round trip). */
|
|
31
|
-
error?: boolean;
|
|
32
|
-
/** Where to send the operator after a successful sign-in. Sanitized here; default `basePath`. */
|
|
33
|
-
returnTo?: unknown;
|
|
34
|
-
}
|
|
35
|
-
export declare function renderLoginPage(basePath: string, options?: LoginPageOptions): string;
|
|
36
|
-
//# sourceMappingURL=login_page.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"login_page.d.ts","sourceRoot":"","sources":["../../../src/dashboard/login_page.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wFAAwF;IACxF,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,iGAAiG;IACjG,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAkBD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CA2IxF"}
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { escapeHtml } from './access_denied_page.js';
|
|
2
|
-
import { sanitizeReturnTo } from './auth.js';
|
|
3
|
-
/**
|
|
4
|
-
* JSON for embedding INSIDE an inline `<script>`: `JSON.stringify` alone leaves `</script>` (and
|
|
5
|
-
* `<!--`) untouched, and the HTML parser ends the script element on the first `</script>` it sees
|
|
6
|
-
* regardless of JavaScript string boundaries — so a `returnTo` of `/x</script><script>…` would run.
|
|
7
|
-
* Escaping the angle brackets (and `&`, plus the two line terminators JSON allows but JS doesn't)
|
|
8
|
-
* as `\uXXXX` keeps the value byte-identical once parsed and inert to the HTML parser.
|
|
9
|
-
*/
|
|
10
|
-
function jsonForScript(value) {
|
|
11
|
-
return JSON.stringify(value)
|
|
12
|
-
.replaceAll('<', '\\u003c')
|
|
13
|
-
.replaceAll('>', '\\u003e')
|
|
14
|
-
.replaceAll('&', '\\u0026')
|
|
15
|
-
.replaceAll('\u2028', '\\u2028')
|
|
16
|
-
.replaceAll('\u2029', '\\u2029');
|
|
17
|
-
}
|
|
18
|
-
export function renderLoginPage(basePath, options = {}) {
|
|
19
|
-
const loginAction = `${basePath}/login`;
|
|
20
|
-
const defaultReturnTo = basePath === '' ? '/' : basePath;
|
|
21
|
-
const returnTo = sanitizeReturnTo(options.returnTo, defaultReturnTo);
|
|
22
|
-
const nonceAttr = options.nonce !== undefined ? ` nonce="${escapeHtml(options.nonce)}"` : '';
|
|
23
|
-
const errorStyle = options.error ? ' style="display:block"' : '';
|
|
24
|
-
return `<!doctype html>
|
|
25
|
-
<html lang="en">
|
|
26
|
-
<head>
|
|
27
|
-
<meta charset="utf-8" />
|
|
28
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
29
|
-
<meta name="robots" content="noindex, nofollow" />
|
|
30
|
-
<title>Sign in — Durable</title>
|
|
31
|
-
<style${nonceAttr}>
|
|
32
|
-
:root { color-scheme: dark; }
|
|
33
|
-
* { box-sizing: border-box; }
|
|
34
|
-
body {
|
|
35
|
-
margin: 0;
|
|
36
|
-
min-height: 100vh;
|
|
37
|
-
display: flex;
|
|
38
|
-
align-items: center;
|
|
39
|
-
justify-content: center;
|
|
40
|
-
background: #09090b;
|
|
41
|
-
color: #e4e4e7;
|
|
42
|
-
font: 14px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
43
|
-
padding: 16px;
|
|
44
|
-
}
|
|
45
|
-
.card {
|
|
46
|
-
width: 100%;
|
|
47
|
-
max-width: 384px;
|
|
48
|
-
border: 1px solid #27272a;
|
|
49
|
-
background: #18181b;
|
|
50
|
-
border-radius: 8px;
|
|
51
|
-
padding: 32px;
|
|
52
|
-
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.5);
|
|
53
|
-
}
|
|
54
|
-
.brand {
|
|
55
|
-
margin: 0 0 24px;
|
|
56
|
-
text-align: center;
|
|
57
|
-
font-size: 18px;
|
|
58
|
-
font-weight: 600;
|
|
59
|
-
color: #34d399;
|
|
60
|
-
}
|
|
61
|
-
form { display: flex; flex-direction: column; gap: 16px; }
|
|
62
|
-
label { display: flex; flex-direction: column; gap: 6px; }
|
|
63
|
-
.field-label {
|
|
64
|
-
font-size: 11px;
|
|
65
|
-
text-transform: uppercase;
|
|
66
|
-
letter-spacing: 0.05em;
|
|
67
|
-
color: #71717a;
|
|
68
|
-
}
|
|
69
|
-
input {
|
|
70
|
-
border-radius: 4px;
|
|
71
|
-
border: 1px solid #3f3f46;
|
|
72
|
-
background: #09090b;
|
|
73
|
-
color: #f4f4f5;
|
|
74
|
-
padding: 8px 12px;
|
|
75
|
-
font: inherit;
|
|
76
|
-
outline: none;
|
|
77
|
-
}
|
|
78
|
-
input:focus { border-color: rgb(52 211 153 / 0.6); }
|
|
79
|
-
#error {
|
|
80
|
-
display: none;
|
|
81
|
-
margin: 0;
|
|
82
|
-
font-size: 12px;
|
|
83
|
-
color: #fb7185;
|
|
84
|
-
}
|
|
85
|
-
button {
|
|
86
|
-
margin-top: 8px;
|
|
87
|
-
border-radius: 4px;
|
|
88
|
-
border: 1px solid rgb(52 211 153 / 0.4);
|
|
89
|
-
background: rgb(52 211 153 / 0.1);
|
|
90
|
-
color: #6ee7b7;
|
|
91
|
-
padding: 8px 12px;
|
|
92
|
-
font: inherit;
|
|
93
|
-
font-size: 12px;
|
|
94
|
-
font-weight: 500;
|
|
95
|
-
text-transform: uppercase;
|
|
96
|
-
letter-spacing: 0.05em;
|
|
97
|
-
cursor: pointer;
|
|
98
|
-
}
|
|
99
|
-
button:hover:not(:disabled) { background: rgb(52 211 153 / 0.2); }
|
|
100
|
-
button:disabled { opacity: 0.6; cursor: default; }
|
|
101
|
-
</style>
|
|
102
|
-
</head>
|
|
103
|
-
<body>
|
|
104
|
-
<div class="card">
|
|
105
|
-
<p class="brand">Durable</p>
|
|
106
|
-
<form id="login-form" method="post" action="${escapeHtml(loginAction)}" autocomplete="on">
|
|
107
|
-
<input type="hidden" name="returnTo" value="${escapeHtml(returnTo)}" />
|
|
108
|
-
<label>
|
|
109
|
-
<span class="field-label">Username</span>
|
|
110
|
-
<input id="username" name="username" type="text" autocomplete="username" required autofocus />
|
|
111
|
-
</label>
|
|
112
|
-
<label>
|
|
113
|
-
<span class="field-label">Password</span>
|
|
114
|
-
<input id="password" name="password" type="password" autocomplete="current-password" />
|
|
115
|
-
</label>
|
|
116
|
-
<p id="error" role="alert"${errorStyle}>Invalid username or password.</p>
|
|
117
|
-
<button id="submit" type="submit">Sign in</button>
|
|
118
|
-
</form>
|
|
119
|
-
</div>
|
|
120
|
-
<script${nonceAttr}>
|
|
121
|
-
(function () {
|
|
122
|
-
var LOGIN_ACTION = ${jsonForScript(loginAction)};
|
|
123
|
-
var RETURN_TO = ${jsonForScript(returnTo)};
|
|
124
|
-
var errorBox = document.getElementById('error');
|
|
125
|
-
var form = document.getElementById('login-form');
|
|
126
|
-
var submitButton = document.getElementById('submit');
|
|
127
|
-
form.addEventListener('submit', function (event) {
|
|
128
|
-
event.preventDefault();
|
|
129
|
-
submitButton.disabled = true;
|
|
130
|
-
errorBox.style.display = 'none';
|
|
131
|
-
fetch(LOGIN_ACTION, {
|
|
132
|
-
method: 'POST',
|
|
133
|
-
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
134
|
-
credentials: 'same-origin',
|
|
135
|
-
body: JSON.stringify({
|
|
136
|
-
username: document.getElementById('username').value,
|
|
137
|
-
password: document.getElementById('password').value,
|
|
138
|
-
returnTo: RETURN_TO,
|
|
139
|
-
}),
|
|
140
|
-
})
|
|
141
|
-
.then(function (response) {
|
|
142
|
-
if (!response.ok) throw new Error('unauthorized');
|
|
143
|
-
return response.json();
|
|
144
|
-
})
|
|
145
|
-
.then(function (data) {
|
|
146
|
-
window.location.href = data.redirectTo || RETURN_TO;
|
|
147
|
-
})
|
|
148
|
-
.catch(function () {
|
|
149
|
-
errorBox.style.display = 'block';
|
|
150
|
-
submitButton.disabled = false;
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
})();
|
|
154
|
-
</script>
|
|
155
|
-
</body>
|
|
156
|
-
</html>`;
|
|
157
|
-
}
|
|
158
|
-
//# sourceMappingURL=login_page.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"login_page.js","sourceRoot":"","sources":["../../../src/dashboard/login_page.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAqC7C;;;;;;GAMG;AACH,SAAS,aAAa,CAAC,KAAa;IAClC,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;SACzB,UAAU,CAAC,GAAG,EAAE,SAAS,CAAC;SAC1B,UAAU,CAAC,GAAG,EAAE,SAAS,CAAC;SAC1B,UAAU,CAAC,GAAG,EAAE,SAAS,CAAC;SAC1B,UAAU,CAAC,QAAQ,EAAE,SAAS,CAAC;SAC/B,UAAU,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,QAAgB,EAAE,OAAO,GAAqB,EAAE;IAC9E,MAAM,WAAW,GAAG,GAAG,QAAQ,QAAQ,CAAC;IACxC,MAAM,eAAe,GAAG,QAAQ,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC;IACzD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,OAAO,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;IACrE,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7F,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,EAAE,CAAC;IACjE,OAAO;;;;;;;QAOD,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kDA2EiC,UAAU,CAAC,WAAW,CAAC;oDACrB,UAAU,CAAC,QAAQ,CAAC;;;;;;;;;kCAStC,UAAU;;;;SAInC,SAAS;;uBAEK,aAAa,CAAC,WAAW,CAAC;oBAC7B,aAAa,CAAC,QAAQ,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAiCnC,CAAC;AACT,CAAC"}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/** The validated dashboard session attached to a request once its cookie verifies. */
|
|
2
|
-
export interface DashboardSession {
|
|
3
|
-
/** Stable user id (the session user's `id`). */
|
|
4
|
-
sub: string;
|
|
5
|
-
/** Optional display name. */
|
|
6
|
-
name?: string;
|
|
7
|
-
/** Role strings. The dashboard interprets ONE convention on its mutating API routes: a session
|
|
8
|
-
* with a NON-EMPTY roles list must include `operator` (or `admin`) to retry/cancel/bulk/
|
|
9
|
-
* fix-and-replay/signal/etc — everything else is a viewer. An EMPTY list keeps full access
|
|
10
|
-
* (back-compat with host hooks that never set roles). */
|
|
11
|
-
roles: string[];
|
|
12
|
-
/** Issued-at, epoch milliseconds. */
|
|
13
|
-
iat: number;
|
|
14
|
-
/** Expiry, epoch milliseconds. */
|
|
15
|
-
exp: number;
|
|
16
|
-
}
|
|
17
|
-
/** The session user the host `login` hook returns to mint a cookie. */
|
|
18
|
-
export interface DashboardSessionUser {
|
|
19
|
-
id: string;
|
|
20
|
-
name?: string;
|
|
21
|
-
roles?: string[];
|
|
22
|
-
}
|
|
23
|
-
export interface SignOptions {
|
|
24
|
-
secret: string;
|
|
25
|
-
ttlMs: number;
|
|
26
|
-
/** Injectable clock (epoch ms) for deterministic tests. Defaults to `Date.now()`. */
|
|
27
|
-
now?: number;
|
|
28
|
-
}
|
|
29
|
-
export interface VerifyOptions {
|
|
30
|
-
secret: string;
|
|
31
|
-
/** Injectable clock (epoch ms) for deterministic tests. Defaults to `Date.now()`. */
|
|
32
|
-
now?: number;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Sign a session into the cookie value `base64url(payload).base64url(hmac)`. Stateless
|
|
36
|
-
* HMAC-SHA256, no store, `node:crypto` only — no JWT dependency.
|
|
37
|
-
*/
|
|
38
|
-
export declare function signSessionCookie(user: DashboardSessionUser, options: SignOptions): string;
|
|
39
|
-
/**
|
|
40
|
-
* Verify a cookie value and return the session, or `null` for anything that's tampered,
|
|
41
|
-
* malformed, or expired (past `exp` + a 30s grace). Constant-time signature comparison. NEVER
|
|
42
|
-
* throws — any parse failure yields `null`.
|
|
43
|
-
*/
|
|
44
|
-
export declare function verifySessionCookie(value: string, options: VerifyOptions): DashboardSession | null;
|
|
45
|
-
//# sourceMappingURL=session_cookie.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"session_cookie.d.ts","sourceRoot":"","sources":["../../../src/dashboard/session_cookie.ts"],"names":[],"mappings":"AAcA,sFAAsF;AACtF,MAAM,WAAW,gBAAgB;IAC/B,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,6BAA6B;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;8DAG0D;IAC1D,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,qCAAqC;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;CACb;AAED,uEAAuE;AACvE,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,qFAAqF;IACrF,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAcD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,oBAAoB,EAAE,OAAO,EAAE,WAAW,GAAG,MAAM,CAY1F;AAeD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,aAAa,GACrB,gBAAgB,GAAG,IAAI,CA4BzB"}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
-
/**
|
|
3
|
-
* Dependency-free, framework-light signed session cookie for the dashboard's
|
|
4
|
-
* built-in {@link file://./auth.ts dashboardAuth} mechanism. Stateless
|
|
5
|
-
* HMAC-SHA256 over `node:crypto` only (no JWT dependency, no store, no
|
|
6
|
-
* AdonisJS types) — ported verbatim from `@dudousxd/nestjs-durable` so the
|
|
7
|
-
* cookie format is identical across the two dashboards and this stays
|
|
8
|
-
* unit-testable without an `HttpContext`.
|
|
9
|
-
*/
|
|
10
|
-
/** Clock-skew grace applied to `exp` so a marginally-late clock doesn't bounce a valid cookie. */
|
|
11
|
-
const EXP_GRACE_MS = 30_000;
|
|
12
|
-
function base64urlEncode(value) {
|
|
13
|
-
return Buffer.from(value, 'utf8').toString('base64url');
|
|
14
|
-
}
|
|
15
|
-
function base64urlDecode(value) {
|
|
16
|
-
return Buffer.from(value, 'base64url').toString('utf8');
|
|
17
|
-
}
|
|
18
|
-
function hmac(secret, payload) {
|
|
19
|
-
return createHmac('sha256', secret).update(payload).digest('base64url');
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Sign a session into the cookie value `base64url(payload).base64url(hmac)`. Stateless
|
|
23
|
-
* HMAC-SHA256, no store, `node:crypto` only — no JWT dependency.
|
|
24
|
-
*/
|
|
25
|
-
export function signSessionCookie(user, options) {
|
|
26
|
-
const issuedAt = options.now ?? Date.now();
|
|
27
|
-
const session = {
|
|
28
|
-
sub: user.id,
|
|
29
|
-
...(user.name !== undefined ? { name: user.name } : {}),
|
|
30
|
-
roles: user.roles ?? [],
|
|
31
|
-
iat: issuedAt,
|
|
32
|
-
exp: issuedAt + options.ttlMs,
|
|
33
|
-
};
|
|
34
|
-
const encodedPayload = base64urlEncode(JSON.stringify(session));
|
|
35
|
-
const signature = hmac(options.secret, encodedPayload);
|
|
36
|
-
return `${encodedPayload}.${signature}`;
|
|
37
|
-
}
|
|
38
|
-
/** Type guard for the decoded payload shape — defends against tampered/legacy payloads. */
|
|
39
|
-
function isSessionPayload(value) {
|
|
40
|
-
if (typeof value !== 'object' || value === null)
|
|
41
|
-
return false;
|
|
42
|
-
const record = { ...value };
|
|
43
|
-
if (typeof record.sub !== 'string')
|
|
44
|
-
return false;
|
|
45
|
-
if (record.name !== undefined && typeof record.name !== 'string')
|
|
46
|
-
return false;
|
|
47
|
-
if (!Array.isArray(record.roles))
|
|
48
|
-
return false;
|
|
49
|
-
if (!record.roles.every((role) => typeof role === 'string'))
|
|
50
|
-
return false;
|
|
51
|
-
if (typeof record.iat !== 'number' || !Number.isFinite(record.iat))
|
|
52
|
-
return false;
|
|
53
|
-
if (typeof record.exp !== 'number' || !Number.isFinite(record.exp))
|
|
54
|
-
return false;
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Verify a cookie value and return the session, or `null` for anything that's tampered,
|
|
59
|
-
* malformed, or expired (past `exp` + a 30s grace). Constant-time signature comparison. NEVER
|
|
60
|
-
* throws — any parse failure yields `null`.
|
|
61
|
-
*/
|
|
62
|
-
export function verifySessionCookie(value, options) {
|
|
63
|
-
try {
|
|
64
|
-
const dot = value.indexOf('.');
|
|
65
|
-
if (dot <= 0 || dot === value.length - 1)
|
|
66
|
-
return null;
|
|
67
|
-
const encodedPayload = value.slice(0, dot);
|
|
68
|
-
const providedSignature = value.slice(dot + 1);
|
|
69
|
-
const expectedSignature = hmac(options.secret, encodedPayload);
|
|
70
|
-
const provided = Buffer.from(providedSignature, 'base64url');
|
|
71
|
-
const expected = Buffer.from(expectedSignature, 'base64url');
|
|
72
|
-
// timingSafeEqual throws on a length mismatch — guard it so a wrong-length (tampered)
|
|
73
|
-
// signature returns null instead of throwing.
|
|
74
|
-
if (provided.length !== expected.length)
|
|
75
|
-
return null;
|
|
76
|
-
if (!timingSafeEqual(provided, expected))
|
|
77
|
-
return null;
|
|
78
|
-
const decoded = JSON.parse(base64urlDecode(encodedPayload));
|
|
79
|
-
if (!isSessionPayload(decoded))
|
|
80
|
-
return null;
|
|
81
|
-
const now = options.now ?? Date.now();
|
|
82
|
-
if (now > decoded.exp + EXP_GRACE_MS)
|
|
83
|
-
return null;
|
|
84
|
-
const session = {
|
|
85
|
-
sub: decoded.sub,
|
|
86
|
-
...(decoded.name !== undefined ? { name: decoded.name } : {}),
|
|
87
|
-
roles: decoded.roles,
|
|
88
|
-
iat: decoded.iat,
|
|
89
|
-
exp: decoded.exp,
|
|
90
|
-
};
|
|
91
|
-
return session;
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
//# sourceMappingURL=session_cookie.js.map
|