@adonis-agora/durable 0.21.1 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/dist/assets/spa/assets/index-BpH1yTMa.js +81 -0
- package/dist/assets/spa/assets/index-BpH1yTMa.js.map +1 -0
- package/dist/assets/spa/assets/index-CTsYsi6p.css +1 -0
- package/dist/assets/spa/index.html +19 -0
- package/dist/providers/dashboard_provider.d.ts +51 -52
- package/dist/providers/dashboard_provider.d.ts.map +1 -1
- package/dist/providers/dashboard_provider.js +234 -121
- package/dist/providers/dashboard_provider.js.map +1 -1
- package/dist/src/dashboard/attr-filter.d.ts +8 -0
- package/dist/src/dashboard/attr-filter.d.ts.map +1 -0
- package/dist/src/dashboard/attr-filter.js +35 -0
- package/dist/src/dashboard/attr-filter.js.map +1 -0
- package/dist/src/dashboard/auth.d.ts +55 -10
- package/dist/src/dashboard/auth.d.ts.map +1 -1
- package/dist/src/dashboard/auth.js +50 -11
- package/dist/src/dashboard/auth.js.map +1 -1
- package/dist/src/dashboard/gateway-adapter.d.ts +52 -5
- package/dist/src/dashboard/gateway-adapter.d.ts.map +1 -1
- package/dist/src/dashboard/gateway-adapter.js +48 -6
- package/dist/src/dashboard/gateway-adapter.js.map +1 -1
- package/dist/src/dashboard/handlers.d.ts +56 -8
- package/dist/src/dashboard/handlers.d.ts.map +1 -1
- package/dist/src/dashboard/handlers.js +116 -14
- package/dist/src/dashboard/handlers.js.map +1 -1
- package/dist/src/dashboard/index.d.ts +4 -4
- package/dist/src/dashboard/index.d.ts.map +1 -1
- package/dist/src/dashboard/index.js +3 -3
- package/dist/src/dashboard/index.js.map +1 -1
- package/dist/src/dashboard/spa.d.ts +22 -0
- package/dist/src/dashboard/spa.d.ts.map +1 -0
- package/dist/src/dashboard/spa.js +43 -0
- package/dist/src/dashboard/spa.js.map +1 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/transports/queue-mock-adapter.d.ts +1 -0
- package/dist/src/transports/queue-mock-adapter.d.ts.map +1 -1
- package/dist/src/transports/queue-mock-adapter.js +1 -0
- package/dist/src/transports/queue-mock-adapter.js.map +1 -1
- package/dist/stubs/config/durable_dashboard.stub +19 -0
- package/package.json +18 -17
|
@@ -11,15 +11,31 @@ import { type DashboardSession, type DashboardSessionUser } from './session_cook
|
|
|
11
11
|
* When left unconfigured the dashboard behaves exactly as before (no login/logout routes, no
|
|
12
12
|
* session guard).
|
|
13
13
|
*/
|
|
14
|
-
/** Host hook validating submitted credentials from the built-in login page. */
|
|
14
|
+
/** Host hook validating submitted credentials from the built-in login page (Mode B — standalone). */
|
|
15
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;
|
|
16
24
|
/**
|
|
17
25
|
* Author-facing `dashboardAuth` option on `config/durable_dashboard.ts`. Gates the dashboard behind
|
|
18
|
-
* a signed session cookie
|
|
19
|
-
*
|
|
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`).
|
|
20
36
|
*
|
|
21
|
-
* Nest needed a `forRootAsync` to reach app services from
|
|
22
|
-
*
|
|
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
|
|
23
39
|
* `app.container`/services (import them at the top of `config/durable_dashboard.ts`), so there is
|
|
24
40
|
* nothing extra to build for the async case.
|
|
25
41
|
*/
|
|
@@ -30,20 +46,30 @@ export interface DashboardAuthOptions {
|
|
|
30
46
|
ttl?: string;
|
|
31
47
|
/** Validates submitted username/password; return the session user, or `null` to deny. Thrown
|
|
32
48
|
* errors are treated as a denial (logged once, never surfaced to the client). May be async. */
|
|
33
|
-
login
|
|
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;
|
|
34
53
|
}
|
|
35
|
-
/**
|
|
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. */
|
|
36
59
|
export interface ResolvedDashboardAuth {
|
|
37
60
|
secret: string;
|
|
38
61
|
ttlMs: number;
|
|
39
|
-
|
|
62
|
+
modes: AuthMode[];
|
|
63
|
+
login?: LoginHook;
|
|
64
|
+
session?: SessionHook;
|
|
40
65
|
}
|
|
41
66
|
/** Cookie name carrying the signed dashboard session. */
|
|
42
67
|
export declare const SESSION_COOKIE_NAME = "durable_dashboard_session";
|
|
43
68
|
/**
|
|
44
69
|
* Validate + resolve the `dashboardAuth` option. Returns `null` when unconfigured (today's
|
|
45
70
|
* unauthenticated behavior, unchanged). Throws at boot (fail closed) when configured but missing a
|
|
46
|
-
* secret or
|
|
71
|
+
* secret, or neither `login` nor `session` is a function — the host learns immediately rather than
|
|
72
|
+
* shipping an un-mintable gate.
|
|
47
73
|
*/
|
|
48
74
|
export declare function resolveDashboardAuth(options: DashboardAuthOptions | undefined): ResolvedDashboardAuth | null;
|
|
49
75
|
/**
|
|
@@ -77,9 +103,28 @@ export type LoginOutcome = {
|
|
|
77
103
|
* Decide a login attempt end-to-end (validate body → run hook → mint cookie) with no HTTP types.
|
|
78
104
|
* On success returns the signed cookie value to set plus the sanitized `redirectTo`; the password
|
|
79
105
|
* is forwarded to the hook AS-IS (empty string when blank), so the hook alone decides whether a
|
|
80
|
-
* password is required.
|
|
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.
|
|
81
108
|
*/
|
|
82
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>;
|
|
83
128
|
/**
|
|
84
129
|
* Read + verify a session cookie value, returning the session or `null` when absent, tampered, or
|
|
85
130
|
* expired. Thin wrapper over {@link verifySessionCookie} so the provider never touches crypto.
|
|
@@ -1 +1 @@
|
|
|
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
|
|
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,CAM7E;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"}
|
|
@@ -22,7 +22,8 @@ function durationToMs(ttl) {
|
|
|
22
22
|
/**
|
|
23
23
|
* Validate + resolve the `dashboardAuth` option. Returns `null` when unconfigured (today's
|
|
24
24
|
* unauthenticated behavior, unchanged). Throws at boot (fail closed) when configured but missing a
|
|
25
|
-
* secret or
|
|
25
|
+
* secret, or neither `login` nor `session` is a function — the host learns immediately rather than
|
|
26
|
+
* shipping an un-mintable gate.
|
|
26
27
|
*/
|
|
27
28
|
export function resolveDashboardAuth(options) {
|
|
28
29
|
if (options === undefined)
|
|
@@ -31,13 +32,23 @@ export function resolveDashboardAuth(options) {
|
|
|
31
32
|
throw new Error('durable_dashboard: dashboardAuth.secret is required and must be a non-empty string ' +
|
|
32
33
|
'(HMAC-SHA256 signing key, 32+ bytes recommended). Failing closed.');
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
|
|
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).');
|
|
36
40
|
}
|
|
41
|
+
const modes = [];
|
|
42
|
+
if (hasSession)
|
|
43
|
+
modes.push('session');
|
|
44
|
+
if (hasLogin)
|
|
45
|
+
modes.push('login');
|
|
37
46
|
return {
|
|
38
47
|
secret: options.secret,
|
|
39
48
|
ttlMs: durationToMs(options.ttl ?? DEFAULT_TTL),
|
|
40
|
-
|
|
49
|
+
modes,
|
|
50
|
+
...(hasLogin ? { login: options.login } : {}),
|
|
51
|
+
...(hasSession ? { session: options.session } : {}),
|
|
41
52
|
};
|
|
42
53
|
}
|
|
43
54
|
/**
|
|
@@ -62,13 +73,14 @@ function isString(value) {
|
|
|
62
73
|
}
|
|
63
74
|
const UNAUTHORIZED_MESSAGE = 'Invalid username or password.';
|
|
64
75
|
/**
|
|
65
|
-
* Run
|
|
66
|
-
* buggy hook never 500s the endpoint into a stack-trace leak. The thrown error is
|
|
67
|
-
* outcome (`hookError`) so the provider can warn-log it once, but it never reaches
|
|
76
|
+
* Run a host hook (`login` or `session`) defensively: a throw is treated as a denial (uniform
|
|
77
|
+
* failure) so a buggy hook never 500s the endpoint into a stack-trace leak. The thrown error is
|
|
78
|
+
* surfaced on the outcome (`hookError`) so the provider can warn-log it once, but it never reaches
|
|
79
|
+
* the client.
|
|
68
80
|
*/
|
|
69
|
-
async function
|
|
81
|
+
async function runHook(hook) {
|
|
70
82
|
try {
|
|
71
|
-
return { user: (await
|
|
83
|
+
return { user: (await hook()) ?? null };
|
|
72
84
|
}
|
|
73
85
|
catch (error) {
|
|
74
86
|
return { user: null, error };
|
|
@@ -78,14 +90,19 @@ async function runLoginHook(auth, username, password) {
|
|
|
78
90
|
* Decide a login attempt end-to-end (validate body → run hook → mint cookie) with no HTTP types.
|
|
79
91
|
* On success returns the signed cookie value to set plus the sanitized `redirectTo`; the password
|
|
80
92
|
* is forwarded to the hook AS-IS (empty string when blank), so the hook alone decides whether a
|
|
81
|
-
* password is required.
|
|
93
|
+
* password is required. `404`-worthy (Mode B not configured) is the provider's call, not this
|
|
94
|
+
* function's — it only runs once the provider has confirmed `auth.login` exists.
|
|
82
95
|
*/
|
|
83
96
|
export async function performLogin(auth, body, basePath, now) {
|
|
84
97
|
const parsed = (body ?? {});
|
|
85
98
|
if (!isString(parsed.username) || !isString(parsed.password)) {
|
|
86
99
|
return { kind: 'bad-request', message: 'Body must include string `username` and `password`.' };
|
|
87
100
|
}
|
|
88
|
-
const
|
|
101
|
+
const login = auth.login;
|
|
102
|
+
if (!login) {
|
|
103
|
+
return { kind: 'unauthorized', message: UNAUTHORIZED_MESSAGE };
|
|
104
|
+
}
|
|
105
|
+
const { user, error } = await runHook(() => login(parsed.username, parsed.password));
|
|
89
106
|
if (!user) {
|
|
90
107
|
return error !== undefined
|
|
91
108
|
? { kind: 'unauthorized', message: UNAUTHORIZED_MESSAGE, hookError: error }
|
|
@@ -98,6 +115,28 @@ export async function performLogin(auth, body, basePath, now) {
|
|
|
98
115
|
});
|
|
99
116
|
return { kind: 'ok', cookieValue, redirectTo: sanitizeReturnTo(parsed.returnTo, basePath) };
|
|
100
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Decide a Mode A session-mint attempt end-to-end: run the host's `session` hook against the raw
|
|
120
|
+
* platform request, and mint the cookie on success. No body to validate — the hook reads whatever it
|
|
121
|
+
* needs directly off `request` (its own cookie/JWT/header).
|
|
122
|
+
*/
|
|
123
|
+
export async function performSession(auth, request, now) {
|
|
124
|
+
const session = auth.session;
|
|
125
|
+
if (!session)
|
|
126
|
+
return { kind: 'unauthorized' };
|
|
127
|
+
const { user, error } = await runHook(() => session(request));
|
|
128
|
+
if (!user) {
|
|
129
|
+
return error !== undefined
|
|
130
|
+
? { kind: 'unauthorized', hookError: error }
|
|
131
|
+
: { kind: 'unauthorized' };
|
|
132
|
+
}
|
|
133
|
+
const cookieValue = signSessionCookie(user, {
|
|
134
|
+
secret: auth.secret,
|
|
135
|
+
ttlMs: auth.ttlMs,
|
|
136
|
+
...(now !== undefined ? { now } : {}),
|
|
137
|
+
});
|
|
138
|
+
return { kind: 'ok', cookieValue };
|
|
139
|
+
}
|
|
101
140
|
/**
|
|
102
141
|
* Read + verify a session cookie value, returning the session or `null` when absent, tampered, or
|
|
103
142
|
* expired. Thin wrapper over {@link verifySessionCookie} so the provider never touches crypto.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/dashboard/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/dashboard/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,19 +1,65 @@
|
|
|
1
|
+
import type { EngineEvent, RunResult } from '../interfaces.js';
|
|
1
2
|
import type { RunGateway } from '../run-gateway/interface.js';
|
|
2
3
|
import type { DashboardEngine } from './handlers.js';
|
|
4
|
+
/**
|
|
5
|
+
* The slice of {@link import('../engine.js').WorkflowEngine} a store role's {@link
|
|
6
|
+
* storeDashboardEngine} delegates to. Declared structurally (not by importing the concrete class) so
|
|
7
|
+
* this module depends only on the verbs it forwards — mirrors `run-gateway/store-run-gateway.ts`'s
|
|
8
|
+
* `RunGatewayEngine` pattern. The real `WorkflowEngine` is structurally assignable to this (every
|
|
9
|
+
* method here — including `retryWithInput`, `continue`, and the global `subscribe` — already exists
|
|
10
|
+
* on it).
|
|
11
|
+
*/
|
|
12
|
+
export interface StoreEngineLike {
|
|
13
|
+
getRun: DashboardEngine['getRun'];
|
|
14
|
+
listRuns: DashboardEngine['listRuns'];
|
|
15
|
+
listCheckpoints: DashboardEngine['listCheckpoints'];
|
|
16
|
+
getRunChildren: DashboardEngine['getRunChildren'];
|
|
17
|
+
requeue: DashboardEngine['requeue'];
|
|
18
|
+
redispatchPending?: DashboardEngine['redispatchPending'];
|
|
19
|
+
cancel: DashboardEngine['cancel'];
|
|
20
|
+
workerHealth: DashboardEngine['workerHealth'];
|
|
21
|
+
retryWithInput(runId: string, input: unknown, newRunId?: string): Promise<{
|
|
22
|
+
runId: string;
|
|
23
|
+
} | null>;
|
|
24
|
+
continue(runId: string): Promise<RunResult | null>;
|
|
25
|
+
/** The engine's GLOBAL listener (every run) — {@link storeDashboardEngine} filters it to one run,
|
|
26
|
+
* same as `StoreRunGateway.subscribe` does. */
|
|
27
|
+
subscribe(listener: (event: EngineEvent) => void): () => void;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Adapt a store-backed {@link import('../engine.js').WorkflowEngine} to the {@link DashboardEngine}
|
|
31
|
+
* port — an explicit wrapper (not a raw `as DashboardEngine` cast) because two verbs need shape
|
|
32
|
+
* bridging, not just a type assertion:
|
|
33
|
+
* - `subscribe(runId, onEvent)` — the engine's `subscribe` is a GLOBAL listener with no `runId`
|
|
34
|
+
* filter; this narrows it to one run's events, byte-for-byte the same filter `StoreRunGateway`
|
|
35
|
+
* already applies.
|
|
36
|
+
* - `redispatchPending` — degrades to `null` when the engine lacks it (kept optional for the same
|
|
37
|
+
* reason `RunGatewayEngine.redispatchPending` is: not every engine build has it yet).
|
|
38
|
+
* Every other verb (including `retryWithInput`/`continue`, which the AdonisJS engine already
|
|
39
|
+
* implements) forwards 1:1 — behavior unchanged from a plain cast for those.
|
|
40
|
+
*/
|
|
41
|
+
export declare function storeDashboardEngine(engine: StoreEngineLike): DashboardEngine;
|
|
3
42
|
/**
|
|
4
43
|
* Adapt a {@link RunGateway} to the {@link DashboardEngine} port the JSON handlers drive, so a
|
|
5
44
|
* **store-less `tenant` pod** serves the SAME dashboard over the wire (design §8) — every read/control
|
|
6
45
|
* verb round-trips through the pod's `ProxyRunGateway` to the control plane, with NO local store or
|
|
7
46
|
* engine. Store presence stays invisible above the handler line.
|
|
8
47
|
*
|
|
9
|
-
* The gateway is a near-superset of the port;
|
|
48
|
+
* The gateway is a near-superset of the port; three verbs need bridging:
|
|
10
49
|
* - `listCheckpoints` → the gateway's `getCheckpoints` (same shape, §8 naming);
|
|
11
50
|
* - `requeue` → the gateway's `redispatchPending` (the operator recovery verb; the extra
|
|
12
|
-
* `redispatched` count on the result is a harmless superset of `RunResult`)
|
|
51
|
+
* `redispatched` count on the result is a harmless superset of `RunResult`);
|
|
52
|
+
* - `subscribe(runId, onEvent)` → the gateway's own per-run `subscribe` (already scoped, no filtering
|
|
53
|
+
* needed here — unlike the store-role adapter above).
|
|
13
54
|
*
|
|
14
|
-
* `getRunChildren` (parent→child fan-out)
|
|
55
|
+
* `getRunChildren` (parent→child fan-out) rides the P4 read surface too (a `getRunChildren`
|
|
15
56
|
* `RunRequestKind` verb), so a store-less tenant pod's run detail view lists children round-tripped over
|
|
16
57
|
* the wire — the responder enforces the tenant-ownership check before answering (anti-IDOR).
|
|
58
|
+
*
|
|
59
|
+
* `retryWithInput`/`continue` have NO gateway verb yet (design §8 didn't need fix-and-replay or
|
|
60
|
+
* breakpoint-continue over the wire) — they degrade to `null`, same convention as
|
|
61
|
+
* `RunGatewayEngine.redispatchPending`'s optional degrade. A tenant pod's dashboard therefore shows
|
|
62
|
+
* these actions as unavailable (the handlers 404) until the wire protocol grows the verbs.
|
|
17
63
|
*/
|
|
18
64
|
export declare function gatewayDashboardEngine(gateway: RunGateway): DashboardEngine;
|
|
19
65
|
/** The minimal container surface {@link dashboardEngineForRole} resolves bindings through. */
|
|
@@ -27,8 +73,9 @@ export interface DashboardContainer {
|
|
|
27
73
|
* - **`tenant`** (store-less): resolve the {@link DURABLE_RUN_GATEWAY} token (a `ProxyRunGateway`) and
|
|
28
74
|
* adapt it — the engine binding is deliberately ABSENT on a tenant pod (structural isolation), so we
|
|
29
75
|
* MUST NOT reach for `WorkflowEngine` here;
|
|
30
|
-
* - **store roles** (`standalone`/`control-plane`): resolve the concrete `WorkflowEngine`
|
|
31
|
-
* {@link
|
|
76
|
+
* - **store roles** (`standalone`/`control-plane`): resolve the concrete `WorkflowEngine` and adapt it
|
|
77
|
+
* via {@link storeDashboardEngine} — every verb still forwards 1:1, just through an explicit wrapper
|
|
78
|
+
* instead of a raw cast (see that function's doc for why).
|
|
32
79
|
*
|
|
33
80
|
* `engineToken` is injected (rather than imported) so the module doesn't hard-depend on the engine class
|
|
34
81
|
* for the tenant path — the tenant branch never touches it.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway-adapter.d.ts","sourceRoot":"","sources":["../../../src/dashboard/gateway-adapter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"gateway-adapter.d.ts","sourceRoot":"","sources":["../../../src/dashboard/gateway-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAC;IAClC,QAAQ,EAAE,eAAe,CAAC,UAAU,CAAC,CAAC;IACtC,eAAe,EAAE,eAAe,CAAC,iBAAiB,CAAC,CAAC;IACpD,cAAc,EAAE,eAAe,CAAC,gBAAgB,CAAC,CAAC;IAClD,OAAO,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC;IACpC,iBAAiB,CAAC,EAAE,eAAe,CAAC,mBAAmB,CAAC,CAAC;IACzD,MAAM,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAC;IAClC,YAAY,EAAE,eAAe,CAAC,cAAc,CAAC,CAAC;IAC9C,cAAc,CACZ,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,OAAO,EACd,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IACrC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IACnD;oDACgD;IAChD,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CAC/D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,eAAe,GAAG,eAAe,CAiB7E;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,UAAU,GAAG,eAAe,CAc3E;AAED,8FAA8F;AAC9F,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,YAAY,GAAG,eAAe,GAAG,QAAQ,EAC/C,SAAS,EAAE,kBAAkB,EAC7B,WAAW,EAAE,OAAO,GACnB,OAAO,CAAC,eAAe,CAAC,CAO1B"}
|
|
@@ -1,18 +1,55 @@
|
|
|
1
1
|
import { DURABLE_RUN_GATEWAY } from '../role_bindings.js';
|
|
2
|
+
/**
|
|
3
|
+
* Adapt a store-backed {@link import('../engine.js').WorkflowEngine} to the {@link DashboardEngine}
|
|
4
|
+
* port — an explicit wrapper (not a raw `as DashboardEngine` cast) because two verbs need shape
|
|
5
|
+
* bridging, not just a type assertion:
|
|
6
|
+
* - `subscribe(runId, onEvent)` — the engine's `subscribe` is a GLOBAL listener with no `runId`
|
|
7
|
+
* filter; this narrows it to one run's events, byte-for-byte the same filter `StoreRunGateway`
|
|
8
|
+
* already applies.
|
|
9
|
+
* - `redispatchPending` — degrades to `null` when the engine lacks it (kept optional for the same
|
|
10
|
+
* reason `RunGatewayEngine.redispatchPending` is: not every engine build has it yet).
|
|
11
|
+
* Every other verb (including `retryWithInput`/`continue`, which the AdonisJS engine already
|
|
12
|
+
* implements) forwards 1:1 — behavior unchanged from a plain cast for those.
|
|
13
|
+
*/
|
|
14
|
+
export function storeDashboardEngine(engine) {
|
|
15
|
+
return {
|
|
16
|
+
getRun: (runId) => engine.getRun(runId),
|
|
17
|
+
listRuns: (query) => engine.listRuns(query),
|
|
18
|
+
listCheckpoints: (runId) => engine.listCheckpoints(runId),
|
|
19
|
+
getRunChildren: (runId) => engine.getRunChildren(runId),
|
|
20
|
+
requeue: (runId) => engine.requeue(runId),
|
|
21
|
+
redispatchPending: (runId) => engine.redispatchPending?.(runId) ?? Promise.resolve(null),
|
|
22
|
+
cancel: (runId, opts) => engine.cancel(runId, opts),
|
|
23
|
+
workerHealth: (extra) => engine.workerHealth(extra),
|
|
24
|
+
retryWithInput: (runId, input) => engine.retryWithInput(runId, input),
|
|
25
|
+
continue: (runId) => engine.continue(runId),
|
|
26
|
+
subscribe: (runId, onEvent) => engine.subscribe((event) => {
|
|
27
|
+
if (event.runId === runId)
|
|
28
|
+
onEvent(event);
|
|
29
|
+
}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
2
32
|
/**
|
|
3
33
|
* Adapt a {@link RunGateway} to the {@link DashboardEngine} port the JSON handlers drive, so a
|
|
4
34
|
* **store-less `tenant` pod** serves the SAME dashboard over the wire (design §8) — every read/control
|
|
5
35
|
* verb round-trips through the pod's `ProxyRunGateway` to the control plane, with NO local store or
|
|
6
36
|
* engine. Store presence stays invisible above the handler line.
|
|
7
37
|
*
|
|
8
|
-
* The gateway is a near-superset of the port;
|
|
38
|
+
* The gateway is a near-superset of the port; three verbs need bridging:
|
|
9
39
|
* - `listCheckpoints` → the gateway's `getCheckpoints` (same shape, §8 naming);
|
|
10
40
|
* - `requeue` → the gateway's `redispatchPending` (the operator recovery verb; the extra
|
|
11
|
-
* `redispatched` count on the result is a harmless superset of `RunResult`)
|
|
41
|
+
* `redispatched` count on the result is a harmless superset of `RunResult`);
|
|
42
|
+
* - `subscribe(runId, onEvent)` → the gateway's own per-run `subscribe` (already scoped, no filtering
|
|
43
|
+
* needed here — unlike the store-role adapter above).
|
|
12
44
|
*
|
|
13
|
-
* `getRunChildren` (parent→child fan-out)
|
|
45
|
+
* `getRunChildren` (parent→child fan-out) rides the P4 read surface too (a `getRunChildren`
|
|
14
46
|
* `RunRequestKind` verb), so a store-less tenant pod's run detail view lists children round-tripped over
|
|
15
47
|
* the wire — the responder enforces the tenant-ownership check before answering (anti-IDOR).
|
|
48
|
+
*
|
|
49
|
+
* `retryWithInput`/`continue` have NO gateway verb yet (design §8 didn't need fix-and-replay or
|
|
50
|
+
* breakpoint-continue over the wire) — they degrade to `null`, same convention as
|
|
51
|
+
* `RunGatewayEngine.redispatchPending`'s optional degrade. A tenant pod's dashboard therefore shows
|
|
52
|
+
* these actions as unavailable (the handlers 404) until the wire protocol grows the verbs.
|
|
16
53
|
*/
|
|
17
54
|
export function gatewayDashboardEngine(gateway) {
|
|
18
55
|
return {
|
|
@@ -24,6 +61,9 @@ export function gatewayDashboardEngine(gateway) {
|
|
|
24
61
|
redispatchPending: (runId) => gateway.redispatchPending(runId),
|
|
25
62
|
cancel: (runId, opts) => gateway.cancel(runId, opts),
|
|
26
63
|
workerHealth: () => gateway.workerHealth(),
|
|
64
|
+
retryWithInput: () => Promise.resolve(null),
|
|
65
|
+
continue: () => Promise.resolve(null),
|
|
66
|
+
subscribe: (runId, onEvent) => gateway.subscribe(runId, onEvent),
|
|
27
67
|
};
|
|
28
68
|
}
|
|
29
69
|
/**
|
|
@@ -33,8 +73,9 @@ export function gatewayDashboardEngine(gateway) {
|
|
|
33
73
|
* - **`tenant`** (store-less): resolve the {@link DURABLE_RUN_GATEWAY} token (a `ProxyRunGateway`) and
|
|
34
74
|
* adapt it — the engine binding is deliberately ABSENT on a tenant pod (structural isolation), so we
|
|
35
75
|
* MUST NOT reach for `WorkflowEngine` here;
|
|
36
|
-
* - **store roles** (`standalone`/`control-plane`): resolve the concrete `WorkflowEngine`
|
|
37
|
-
* {@link
|
|
76
|
+
* - **store roles** (`standalone`/`control-plane`): resolve the concrete `WorkflowEngine` and adapt it
|
|
77
|
+
* via {@link storeDashboardEngine} — every verb still forwards 1:1, just through an explicit wrapper
|
|
78
|
+
* instead of a raw cast (see that function's doc for why).
|
|
38
79
|
*
|
|
39
80
|
* `engineToken` is injected (rather than imported) so the module doesn't hard-depend on the engine class
|
|
40
81
|
* for the tenant path — the tenant branch never touches it.
|
|
@@ -44,6 +85,7 @@ export async function dashboardEngineForRole(role, container, engineToken) {
|
|
|
44
85
|
const gateway = (await container.make(DURABLE_RUN_GATEWAY));
|
|
45
86
|
return gatewayDashboardEngine(gateway);
|
|
46
87
|
}
|
|
47
|
-
|
|
88
|
+
const engine = (await container.make(engineToken));
|
|
89
|
+
return storeDashboardEngine(engine);
|
|
48
90
|
}
|
|
49
91
|
//# sourceMappingURL=gateway-adapter.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway-adapter.js","sourceRoot":"","sources":["../../../src/dashboard/gateway-adapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"gateway-adapter.js","sourceRoot":"","sources":["../../../src/dashboard/gateway-adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GroupHealth, RunQuery, RunResult, StepCheckpoint, WorkflowRun } from '../index.js';
|
|
1
|
+
import type { EngineEvent, GroupHealth, RunQuery, RunResult, StepCheckpoint, WorkflowRun } from '../index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Framework-light JSON handlers over a {@link DashboardEngine}.
|
|
4
4
|
*
|
|
@@ -11,15 +11,24 @@ import type { GroupHealth, RunQuery, RunResult, StepCheckpoint, WorkflowRun } fr
|
|
|
11
11
|
* leak in, so the handlers are unit-testable against a real in-memory engine
|
|
12
12
|
* with no HTTP server. The provider adapts an AdonisJS `HttpContext` to these
|
|
13
13
|
* shapes.
|
|
14
|
+
*
|
|
15
|
+
* `listRuns`/`getRun`/`retryRun`/`redispatchPendingRun`/`cancelRun`/`health` are the original
|
|
16
|
+
* handlers this file always had (kept response-shape compatible with the legacy static
|
|
17
|
+
* `assets/dashboard.html`, still served as a compat fallback — see `compat.ts`/`compat-view.ts`).
|
|
18
|
+
* `workers`, `topology`, `retryWithInputRun`, `continueRun`, and `bulkAction` are additions that give
|
|
19
|
+
* the new `@adonis-agora/durable-dashboard` React SPA parity with `@dudousxd/nestjs-durable-dashboard`'s
|
|
20
|
+
* `DurableApiController` (fix-and-replay, bulk retry/cancel, breakpoint continue, full worker
|
|
21
|
+
* heartbeats, topology badge). The SSE `runs/:id/stream` route is wired directly in
|
|
22
|
+
* `providers/dashboard_provider.ts` since streaming needs the raw HTTP response, not a JSON `ApiResponse`.
|
|
14
23
|
*/
|
|
15
24
|
/**
|
|
16
25
|
* The bounded read/control surface the JSON handlers drive — declared STRUCTURALLY (a port), not by
|
|
17
26
|
* importing the concrete `WorkflowEngine` class, so the same handlers serve BOTH durable topologies
|
|
18
|
-
* (design §8): a store role passes the real {@link
|
|
19
|
-
*
|
|
20
|
-
* {@link import('../run-gateway/interface.js').RunGateway} (
|
|
21
|
-
* is therefore invisible to the handlers. Mirrors the `RunGatewayEngine` port pattern
|
|
22
|
-
* `StoreRunGateway`.
|
|
27
|
+
* (design §8): a store role passes an adapter over the real {@link
|
|
28
|
+
* import('../engine.js').WorkflowEngine} (`storeDashboardEngine`); a store-less `tenant` pod passes an
|
|
29
|
+
* adapter over its {@link import('../run-gateway/interface.js').RunGateway} (`gatewayDashboardEngine`).
|
|
30
|
+
* Store presence is therefore invisible to the handlers. Mirrors the `RunGatewayEngine` port pattern
|
|
31
|
+
* already used by `StoreRunGateway`.
|
|
23
32
|
*/
|
|
24
33
|
export interface DashboardEngine {
|
|
25
34
|
getRun(runId: string): Promise<WorkflowRun | null>;
|
|
@@ -32,6 +41,17 @@ export interface DashboardEngine {
|
|
|
32
41
|
compensate?: boolean;
|
|
33
42
|
}): Promise<RunResult | null>;
|
|
34
43
|
workerHealth(extra?: string[]): Promise<GroupHealth[]>;
|
|
44
|
+
/** Fix-and-replay: start a fresh linked run from `runId`'s workflow with a corrected `input`. Returns
|
|
45
|
+
* the new run's id, or `null` if `runId` is unknown. Degrades to `null` on a topology that can't
|
|
46
|
+
* perform it yet (a store-less `tenant` pod — see `gateway-adapter.ts`). */
|
|
47
|
+
retryWithInput(runId: string, input: unknown): Promise<{
|
|
48
|
+
runId: string;
|
|
49
|
+
} | null>;
|
|
50
|
+
/** Resume a run paused at a `ctx.breakpoint()`. Returns `null` if the run isn't paused at one, or on
|
|
51
|
+
* a topology that can't perform it yet (see `retryWithInput`). */
|
|
52
|
+
continue(runId: string): Promise<RunResult | null>;
|
|
53
|
+
/** Live lifecycle events for ONE run; returns an unsubscribe fn. */
|
|
54
|
+
subscribe(runId: string, onEvent: (event: EngineEvent) => void): () => void;
|
|
35
55
|
}
|
|
36
56
|
/** The read/control port the handlers operate over (a store engine or a tenant gateway adapter). */
|
|
37
57
|
export interface Deps {
|
|
@@ -53,7 +73,7 @@ export interface ApiResponse {
|
|
|
53
73
|
}
|
|
54
74
|
/** A `200 OK` JSON response. Exported so sibling handlers (e.g. `compat`) share one convention. */
|
|
55
75
|
export declare const ok: (body: unknown) => ApiResponse;
|
|
56
|
-
/** `GET /runs` — list runs filtered by status/workflow/tag, paginated. */
|
|
76
|
+
/** `GET /runs` — list runs filtered by status/workflow/tag/namespace/search-attributes, paginated. */
|
|
57
77
|
export declare function listRuns(deps: Deps, req: ApiRequest): Promise<ApiResponse>;
|
|
58
78
|
/** `GET /runs/:id` — a run's detail: the run, its step timeline, and child run ids. */
|
|
59
79
|
export declare function getRun(deps: Deps, req: ApiRequest): Promise<ApiResponse>;
|
|
@@ -72,6 +92,34 @@ export declare function retryRun(deps: Deps, req: ApiRequest): Promise<ApiRespon
|
|
|
72
92
|
export declare function redispatchPendingRun(deps: Deps, req: ApiRequest): Promise<ApiResponse>;
|
|
73
93
|
/** `POST /runs/:id/cancel` — cancel a run. Pass `{ compensate: true }` to undo the saga first. */
|
|
74
94
|
export declare function cancelRun(deps: Deps, req: ApiRequest): Promise<ApiResponse>;
|
|
75
|
-
/**
|
|
95
|
+
/**
|
|
96
|
+
* `POST /runs/:id/retry-with-input` — fix-and-replay: start a fresh linked run of `runId`'s workflow
|
|
97
|
+
* with a corrected `input`. Body: `{ input: unknown }`. Returns the new run's id; the ORIGINAL run is
|
|
98
|
+
* left untouched (mirrors `@dudousxd/nestjs-durable-dashboard`'s `retryWithInput`).
|
|
99
|
+
*/
|
|
100
|
+
export declare function retryWithInputRun(deps: Deps, req: ApiRequest): Promise<ApiResponse>;
|
|
101
|
+
/**
|
|
102
|
+
* `POST /runs/:id/continue` — resume a run paused at a {@link import('../interfaces.js').WorkflowCtx}
|
|
103
|
+
* breakpoint (the dashboard's "continue" button). `404` if the run isn't paused at one.
|
|
104
|
+
*/
|
|
105
|
+
export declare function continueRun(deps: Deps, req: ApiRequest): Promise<ApiResponse>;
|
|
106
|
+
/**
|
|
107
|
+
* `POST /bulk/:action` (`action` = `retry`|`cancel`) — apply an action to every run matching the same
|
|
108
|
+
* filter `listRuns` accepts (status/workflow/tag/namespace/attr), capped at 500 matches. Skips (does
|
|
109
|
+
* not abort on) a run that can't take the action (e.g. already terminal). Mirrors
|
|
110
|
+
* `@dudousxd/nestjs-durable-dashboard`'s `DashboardService.bulk`.
|
|
111
|
+
*/
|
|
112
|
+
export declare function bulkAction(deps: Deps, req: ApiRequest): Promise<ApiResponse>;
|
|
113
|
+
/** `GET /health` — per-group worker health (queue backlog + live worker heartbeats), reduced to a
|
|
114
|
+
* compact shape. Kept for the legacy static dashboard (`assets/dashboard.html`, compat fallback). */
|
|
76
115
|
export declare function health(deps: Deps): Promise<ApiResponse>;
|
|
116
|
+
/** `GET /workers` — full per-group worker health (every live worker's heartbeat, not just a count),
|
|
117
|
+
* for the new SPA's Workers panel (`pivot-by-worker.ts`/`group-by-partition.ts` need per-instance
|
|
118
|
+
* `instanceId`/`lastBeatAt`). Raw `GroupHealth[]`, unwrapped — matches
|
|
119
|
+
* `@dudousxd/nestjs-durable-dashboard`'s `GET /workers` shape byte for byte (minus the `status`
|
|
120
|
+
* telemetry field, which the AdonisJS engine's heartbeat doesn't carry — see `durable-client.ts`). */
|
|
121
|
+
export declare function workers(deps: Deps): Promise<ApiResponse>;
|
|
122
|
+
/** `GET /topology` — this deployment's durable role, for the header badge. Pure/local (no engine
|
|
123
|
+
* round-trip): the role is already known by `dashboard_provider.ts` from `config/durable.ts`. */
|
|
124
|
+
export declare function topology(role: 'standalone' | 'control-plane' | 'tenant', tenant?: string): ApiResponse;
|
|
77
125
|
//# sourceMappingURL=handlers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../../src/dashboard/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,QAAQ,EACR,SAAS,EAET,cAAc,EACd,WAAW,EACZ,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../../src/dashboard/handlers.ts"],"names":[],"mappings":"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"}
|