@adonis-agora/agent 0.25.5 → 0.26.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 +21 -0
- package/dist/providers/dashboard_provider.d.ts.map +1 -1
- package/dist/providers/dashboard_provider.js +16 -12
- package/dist/providers/dashboard_provider.js.map +1 -1
- package/dist/src/dashboard/access_denied_page.d.ts +102 -0
- package/dist/src/dashboard/access_denied_page.d.ts.map +1 -0
- package/dist/src/dashboard/access_denied_page.js +266 -0
- package/dist/src/dashboard/access_denied_page.js.map +1 -0
- package/dist/src/dashboard/define_config.d.ts +22 -0
- package/dist/src/dashboard/define_config.d.ts.map +1 -1
- package/dist/src/dashboard/define_config.js +2 -0
- package/dist/src/dashboard/define_config.js.map +1 -1
- package/dist/src/dashboard/gate.d.ts +23 -1
- package/dist/src/dashboard/gate.d.ts.map +1 -1
- package/dist/src/dashboard/gate.js +55 -0
- package/dist/src/dashboard/gate.js.map +1 -1
- package/dist/src/dashboard/index.d.ts +4 -2
- package/dist/src/dashboard/index.d.ts.map +1 -1
- package/dist/src/dashboard/index.js +3 -17
- package/dist/src/dashboard/index.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @adonis-agora/agent
|
|
2
2
|
|
|
3
|
+
## 0.26.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#90](https://github.com/DavideCarvalho/adonis-agora-agent/pull/90) [`c540ed5`](https://github.com/DavideCarvalho/adonis-agora-agent/commit/c540ed5884d8dc2ad4a22c0f29f9ec9c68ace6be) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Governance console: a refused request now gets a real page instead of `{"error":"forbidden"}`.
|
|
8
|
+
|
|
9
|
+
Opening the console without permission used to answer the browser with JSON. Both providers
|
|
10
|
+
(the embedded `@adonis-agora/agent/dashboard_provider` and the standalone
|
|
11
|
+
`@adonis-agora/agent-dashboard`) now serve a built-in access-denied page in the console's own
|
|
12
|
+
visual language — the status, a sentence explaining the refusal and a "Back to app" link.
|
|
13
|
+
Statuses are unchanged (`401` when no actor resolves, `403` when `authorize` denies), and a
|
|
14
|
+
redirect written by `onUnauthenticated`/`authorize` still wins.
|
|
15
|
+
|
|
16
|
+
The page carries no inline `<script>`, so a nonce'd `script-src` CSP cannot break it; its inline
|
|
17
|
+
`<style>` takes `@adonisjs/shield`'s request nonce when one exists.
|
|
18
|
+
|
|
19
|
+
New `dashboard.accessDenied` option on `config/agent.ts` to customise it — an object (`brand`,
|
|
20
|
+
`title`, `message`, `homeHref`, `accent`, labels) to tweak the built-in page, or a function
|
|
21
|
+
`(info, ctx) => html | void` to render it yourself or redirect. `@adonis-agora/agent/dashboard`
|
|
22
|
+
exports the shared `answerDashboardDenial` + `renderAccessDeniedPage`.
|
|
23
|
+
|
|
3
24
|
## 0.25.5
|
|
4
25
|
|
|
5
26
|
### Patch Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard_provider.d.ts","sourceRoot":"","sources":["../../providers/dashboard_provider.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"dashboard_provider.d.ts","sourceRoot":"","sources":["../../providers/dashboard_provider.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAmB/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,MAAM,CAAC,OAAO,OAAO,iBAAiB;;IACxB,SAAS,CAAC,GAAG,EAAE,kBAAkB;IAA7C,YAAsB,GAAG,EAAE,kBAAkB,EAAI;IAc3C,IAAI,kBA0ET;CAiEF"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { resolve as resolvePath } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { apiBaseFor, contentTypeFor, decideDashboardMount, evaluateDashboardGate, injectApiBase, injectBaseHref, mountPathFor, resolveDashboardConfig, safeAssetSegments, } from '../src/dashboard/index.js';
|
|
4
|
+
import { answerDashboardDenial, apiBaseFor, contentTypeFor, decideDashboardMount, evaluateDashboardGate, injectApiBase, injectBaseHref, mountPathFor, resolveDashboardConfig, safeAssetSegments, } from '../src/dashboard/index.js';
|
|
5
5
|
/**
|
|
6
6
|
* Serves the `@adonis-agora/agent-dashboard` governance SPA **from this package's own build** — no
|
|
7
7
|
* separate install, no separate provider registration. `node ace configure @adonis-agora/agent`
|
|
@@ -88,20 +88,25 @@ export default class DashboardProvider {
|
|
|
88
88
|
const mount = mountPathFor(agentPath, dashboardConfig.path);
|
|
89
89
|
const actorResolver = agentConfig.actorResolver;
|
|
90
90
|
const authorize = dashboardConfig.authorize;
|
|
91
|
+
// What a refused browser sees — the built-in access-denied page unless the host customised it.
|
|
92
|
+
const denial = {
|
|
93
|
+
basePath: mount,
|
|
94
|
+
accessDenied: dashboardConfig.accessDenied,
|
|
95
|
+
};
|
|
91
96
|
// The SPA shell, served at the bare mount. We do NOT redirect to a trailing-slash variant: the
|
|
92
97
|
// AdonisJS router normalizes trailing slashes, so `mount` and `${mount}/` are the SAME route
|
|
93
98
|
// pattern — registering both throws "Duplicate route". Instead `sendIndex` injects a
|
|
94
99
|
// `<base href="${mount}/">`, so the SPA's relative `./assets/*` URLs resolve against the mount
|
|
95
100
|
// directory regardless of whether the browser's URL carries a trailing slash.
|
|
96
101
|
router.get(mount, async (ctx) => {
|
|
97
|
-
if (!(await this.#gate(ctx, actorResolver, authorize, dashboardConfig.onUnauthenticated)))
|
|
102
|
+
if (!(await this.#gate(ctx, actorResolver, authorize, dashboardConfig.onUnauthenticated, denial)))
|
|
98
103
|
return;
|
|
99
104
|
await this.#sendIndex(ctx, apiBase, mount);
|
|
100
105
|
});
|
|
101
106
|
// Built assets (JS/CSS/fonts/...), with an index fallback for any unmatched path so the
|
|
102
107
|
// client-rendered console still boots on a deep link.
|
|
103
108
|
router.get(`${mount}/*`, async (ctx) => {
|
|
104
|
-
if (!(await this.#gate(ctx, actorResolver, authorize, dashboardConfig.onUnauthenticated)))
|
|
109
|
+
if (!(await this.#gate(ctx, actorResolver, authorize, dashboardConfig.onUnauthenticated, denial)))
|
|
105
110
|
return;
|
|
106
111
|
const segments = safeAssetSegments(ctx.params['*']);
|
|
107
112
|
if (segments === null) {
|
|
@@ -114,18 +119,17 @@ export default class DashboardProvider {
|
|
|
114
119
|
* Resolve the actor through the agent config's resolver, mirroring the governance routes, then run
|
|
115
120
|
* the optional `authorize` gate. Returns `true` to proceed; replies `401` on a missing/failed
|
|
116
121
|
* resolver and `403` when `authorize` denies the resolved actor. The decision itself lives in the
|
|
117
|
-
* router-free {@link evaluateDashboardGate} so it can be unit tested; this only writes the response
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
+
* router-free {@link evaluateDashboardGate} so it can be unit tested; this only writes the response
|
|
123
|
+
* — the access-denied PAGE (a browser is what hits these routes), via the shared
|
|
124
|
+
* {@link answerDashboardDenial} — and, mirroring `@adonis-agora/durable`'s dashboard, ONLY when
|
|
125
|
+
* neither `onUnauthenticated` nor `authorize` already answered the request themselves: if either
|
|
126
|
+
* set a `location` header (typically via `ctx.response.redirect(...)`, e.g. to the app's own login
|
|
127
|
+
* page), that redirect stands and this skips writing its own `401`/`403` page on top of it.
|
|
122
128
|
*/
|
|
123
|
-
async #gate(ctx, actorResolver, authorize, onUnauthenticated) {
|
|
129
|
+
async #gate(ctx, actorResolver, authorize, onUnauthenticated, denial) {
|
|
124
130
|
const verdict = await evaluateDashboardGate(ctx, actorResolver, authorize, !this.app.inProduction, onUnauthenticated);
|
|
125
131
|
if (!verdict.ok) {
|
|
126
|
-
|
|
127
|
-
return false;
|
|
128
|
-
ctx.response.status(verdict.status).json({ error: verdict.error });
|
|
132
|
+
await answerDashboardDenial(ctx, verdict, denial);
|
|
129
133
|
return false;
|
|
130
134
|
}
|
|
131
135
|
return true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard_provider.js","sourceRoot":"","sources":["../../providers/dashboard_provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGzC,OAAO,EAGL,UAAU,EACV,cAAc,
|
|
1
|
+
{"version":3,"file":"dashboard_provider.js","sourceRoot":"","sources":["../../providers/dashboard_provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGzC,OAAO,EAGL,qBAAqB,EACrB,UAAU,EACV,cAAc,EAEd,oBAAoB,EACpB,qBAAqB,EACrB,aAAa,EACb,cAAc,EACd,YAAY,EACZ,sBAAsB,EACtB,iBAAiB,GAClB,MAAM,2BAA2B,CAAC;AAInC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,MAAM,CAAC,OAAO,OAAO,iBAAiB;IACd,GAAG;IAAzB,YAAsB,GAAuB;mBAAvB,GAAG;IAAuB,CAAC;IAEjD;;;;;OAKG;IACH,OAAO,CAAqB;IAC5B,aAAa;QACX,IAAI,CAAC,OAAO,KAAK,aAAa,CAAC,IAAI,GAAG,CAAC,gBAAgB,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QAC3E,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,KAAK,CAAC,IAAI;QACR,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAc,OAAO,EAAE,EAAiB,CAAC,CAAC;QACjF,8FAA8F;QAC9F,mGAAmG;QACnG,MAAM,eAAe,GAAG,sBAAsB,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;QAEtE,yFAAyF;QACzF,mGAAmG;QACnG,mGAAmG;QACnG,MAAM,QAAQ,GAAG,oBAAoB,CACnC,eAAe,CAAC,OAAO,EACvB,WAAW,CAAC,mBAAmB,EAC/B,WAAW,CAAC,iBAAiB,CAC9B,CAAC;QACF,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;YACpB,IAAI,QAAQ,CAAC,MAAM,KAAK,UAAU;gBAAE,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;YACnE,OAAO;QACT,CAAC;QAED,mGAAmG;QACnG,8FAA8F;QAC9F,oGAAoG;QACpG,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAEvD,MAAM,SAAS,GAAG,WAAW,CAAC,IAAI,IAAI,OAAO,CAAC;QAC9C,MAAM,OAAO,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;QACtC,MAAM,KAAK,GAAG,YAAY,CAAC,SAAS,EAAE,eAAe,CAAC,IAAI,CAAC,CAAC;QAC5D,MAAM,aAAa,GAAG,WAAW,CAAC,aAAa,CAAC;QAEhD,MAAM,SAAS,GAAG,eAAe,CAAC,SAAS,CAAC;QAC5C,+FAA+F;QAC/F,MAAM,MAAM,GAA2B;YACrC,QAAQ,EAAE,KAAK;YACf,YAAY,EAAE,eAAe,CAAC,YAAY;SAC3C,CAAC;QAEF,+FAA+F;QAC/F,6FAA6F;QAC7F,qFAAqF;QACrF,+FAA+F;QAC/F,8EAA8E;QAC9E,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YAC9B,IACE,CAAC,CAAC,MAAM,IAAI,CAAC,KAAK,CAChB,GAAG,EACH,aAAa,EACb,SAAS,EACT,eAAe,CAAC,iBAAiB,EACjC,MAAM,CACP,CAAC;gBAEF,OAAO;YACT,MAAM,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;QAC7C,CAAC,CAAC,CAAC;QAEH,wFAAwF;QACxF,sDAAsD;QACtD,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YACrC,IACE,CAAC,CAAC,MAAM,IAAI,CAAC,KAAK,CAChB,GAAG,EACH,aAAa,EACb,SAAS,EACT,eAAe,CAAC,iBAAiB,EACjC,MAAM,CACP,CAAC;gBAEF,OAAO;YACT,MAAM,QAAQ,GAAG,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YACpD,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;gBACtB,OAAO,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAC;YACpE,CAAC;YACD,MAAM,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;QACvD,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;;;;OAUG;IACH,KAAK,CAAC,KAAK,CACT,GAAgB,EAChB,aAAwC,EACxC,SAA8C,EAC9C,iBAAgE,EAChE,MAA8B;QAE9B,MAAM,OAAO,GAAG,MAAM,qBAAqB,CACzC,GAAG,EACH,aAAa,EACb,SAAS,EACT,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,EACtB,iBAAiB,CAClB,CAAC;QACF,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;YAChB,MAAM,qBAAqB,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;YAClD,OAAO,KAAK,CAAC;QACf,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,UAAU,CAAC,GAAgB,EAAE,OAAe,EAAE,KAAa;QAC/D,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,YAAY,CAAC,EAAE,MAAM,CAAC,CAAC;QACrF,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,cAAc,EAAE,0BAA0B,CAAC,CAAC;QAChE,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,eAAe,EAAE,2BAA2B,CAAC,CAAC;QAClE,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,aAAa,CAAC,cAAc,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;IACzE,CAAC;IAED,4FAA4F;IAC5F,KAAK,CAAC,UAAU,CACd,GAAgB,EAChB,QAAkB,EAClB,OAAe,EACf,KAAa;QAEb,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,YAAY,CAAC;QAC/D,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAC;YAC9E,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,cAAc,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC;YAC9D,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC5B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACvD,MAAM,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;gBAC3C,OAAO;YACT,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in "access denied" page a BROWSER sees when the dashboard guard refuses a page
|
|
3
|
+
* navigation — the SPA shell, its assets, or (Mode A only) a session-less visit. Before this page
|
|
4
|
+
* existed the guard answered every denial with the JSON it answers the API with
|
|
5
|
+
* (`{ "error": "forbidden" }`), which is the right thing for the SPA's `fetch` calls and the wrong
|
|
6
|
+
* thing for a human staring at a browser tab.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately a small, dependency-free, hand-authored HTML page (like `login_page.ts`) — NOT part
|
|
9
|
+
* of the bundled SPA, so a denial never needs the SPA to load first. It carries NO `<script>` at all,
|
|
10
|
+
* so a host running a nonce'd `script-src` CSP (the `@adonisjs/shield` default) cannot break it; the
|
|
11
|
+
* one inline `<style>` takes the request's CSP nonce when the provider hands one over.
|
|
12
|
+
*
|
|
13
|
+
* Every string the host can supply ({@link AccessDeniedPageOptions}) is HTML-escaped on the way in.
|
|
14
|
+
* They are developer-controlled config values, not user input — but escaping them costs nothing and
|
|
15
|
+
* removes the one class of bug a "just interpolate it" template would invite.
|
|
16
|
+
*
|
|
17
|
+
* The visual language (Aviary neutrals, the console's own accent, mono eyebrow over a sans body)
|
|
18
|
+
* mirrors the agent SPA so the refusal reads as part of the same product, not a server default.
|
|
19
|
+
*
|
|
20
|
+
* Ported from `@adonis-agora/payments` (also in `@adonis-agora/durable`, `@adonis-agora/media`, `@adonis-agora/agent` and
|
|
21
|
+
* `@adonis-agora/telescope` with only {@link CONSOLE} changed, so the Agora consoles share ONE
|
|
22
|
+
* refusal story rather than each inventing its own.
|
|
23
|
+
*/
|
|
24
|
+
/** Why the guard refused. Drives the default title/message and which buttons make sense. */
|
|
25
|
+
export type AccessDeniedReason =
|
|
26
|
+
/** Nobody is signed in (a `401`): offer the login page when one exists. */
|
|
27
|
+
'unauthenticated'
|
|
28
|
+
/** Somebody is signed in but `authorize` said no (a `403`). */
|
|
29
|
+
| 'forbidden'
|
|
30
|
+
/** `dashboardAuth` runs in Mode A only and there is no session — the host app must mint one. */
|
|
31
|
+
| 'session-required';
|
|
32
|
+
/** What the provider knows about the refusal — handed to a custom renderer and to the default page. */
|
|
33
|
+
export interface AccessDeniedInfo {
|
|
34
|
+
/** The HTTP status the page is served with. */
|
|
35
|
+
status: 401 | 403;
|
|
36
|
+
reason: AccessDeniedReason;
|
|
37
|
+
/** The console's mount path (`/agent/dashboard`; `''` when mounted at the root). */
|
|
38
|
+
basePath: string;
|
|
39
|
+
/** The built-in login page, present only when `dashboardAuth.login` is configured. */
|
|
40
|
+
loginHref?: string;
|
|
41
|
+
/** A developer-facing detail (e.g. a thrown hook's message). Providers set it OUTSIDE production only. */
|
|
42
|
+
detail?: string;
|
|
43
|
+
/** The request's CSP nonce, applied to the inline `<style>` so a nonce'd `style-src` keeps the page styled. */
|
|
44
|
+
nonce?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Knobs for the built-in page — the `accessDenied` config option in its OBJECT form. Every field is
|
|
48
|
+
* optional; omit the option entirely for the defaults.
|
|
49
|
+
*/
|
|
50
|
+
export interface AccessDeniedPageOptions {
|
|
51
|
+
/** Console name in the eyebrow and `<title>`. Default: the console's own name (`Agent`). */
|
|
52
|
+
brand?: string;
|
|
53
|
+
/** Heading. Default depends on {@link AccessDeniedInfo.reason}. */
|
|
54
|
+
title?: string;
|
|
55
|
+
/** The sentence under the heading. Default depends on the reason. */
|
|
56
|
+
message?: string;
|
|
57
|
+
/** Where "Back to app" points. Default `/`; `false` hides the button. */
|
|
58
|
+
homeHref?: string | false;
|
|
59
|
+
/** Label of the "Back to app" button. */
|
|
60
|
+
homeLabel?: string;
|
|
61
|
+
/** Where "Sign in" points. Default: the built-in login page when one exists; `false` hides it. */
|
|
62
|
+
loginHref?: string | false;
|
|
63
|
+
/** Label of the "Sign in" button. */
|
|
64
|
+
loginLabel?: string;
|
|
65
|
+
/** Accent colour (any CSS colour). Default: the console's own accent. */
|
|
66
|
+
accent?: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The `accessDenied` config option in its FUNCTION form: render the page yourself. Return an HTML
|
|
70
|
+
* string and the provider serves it (with the refusal's status, `text/html`, `no-store`). Return
|
|
71
|
+
* nothing after answering the request yourself — a redirect to your own login page, most commonly —
|
|
72
|
+
* and the provider stands down. Return nothing WITHOUT answering and the default page is served, so
|
|
73
|
+
* the hook is safe to use for side effects (logging) alone.
|
|
74
|
+
*
|
|
75
|
+
* `Ctx` is the host framework's HTTP context — the provider passes AdonisJS's `HttpContext` through
|
|
76
|
+
* untouched; it is generic here so this module stays framework-light and unit-testable.
|
|
77
|
+
*/
|
|
78
|
+
export type AccessDeniedRenderer<Ctx = unknown> = (info: AccessDeniedInfo, ctx: Ctx) => string | undefined | null | void | Promise<string | undefined | null | void>;
|
|
79
|
+
/** The `accessDenied` config option: tweak the built-in page, or replace it. */
|
|
80
|
+
export type AccessDeniedOption<Ctx = unknown> = AccessDeniedPageOptions | AccessDeniedRenderer<Ctx>;
|
|
81
|
+
/** This console's identity — the ONLY thing that differs between the Agora ports of this file. */
|
|
82
|
+
export declare const CONSOLE: {
|
|
83
|
+
readonly brand: 'Agent';
|
|
84
|
+
readonly accent: '#a78bfa';
|
|
85
|
+
readonly packageName: '@adonis-agora/agent';
|
|
86
|
+
};
|
|
87
|
+
/** Escape the five characters that matter inside HTML text and double-quoted attributes. */
|
|
88
|
+
export declare function escapeHtml(value: string): string;
|
|
89
|
+
/**
|
|
90
|
+
* Render the built-in page. Pure: same `info` + `options` in, same HTML out — so it is trivially
|
|
91
|
+
* snapshot/regex testable and safe to call from anywhere (the provider, a test, a host that wants
|
|
92
|
+
* to embed the default page inside its own layout).
|
|
93
|
+
*/
|
|
94
|
+
export declare function renderAccessDeniedPage(info: AccessDeniedInfo, options?: AccessDeniedPageOptions): string;
|
|
95
|
+
/**
|
|
96
|
+
* Resolve WHAT to serve for a refusal, honouring the host's `accessDenied` option in either form.
|
|
97
|
+
* Returns the HTML to send, or `null` when a custom renderer already answered the request itself
|
|
98
|
+
* (`answered()` reports whether it did — the provider wires that to "a `location` header or a body
|
|
99
|
+
* is already set"). Framework-light: knows nothing about the HTTP context beyond passing it through.
|
|
100
|
+
*/
|
|
101
|
+
export declare function resolveAccessDeniedPage<Ctx>(info: AccessDeniedInfo, option: AccessDeniedOption<Ctx> | null | undefined, ctx: Ctx, answered: () => boolean): Promise<string | null>;
|
|
102
|
+
//# sourceMappingURL=access_denied_page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access_denied_page.d.ts","sourceRoot":"","sources":["../../../src/dashboard/access_denied_page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,4FAA4F;AAC5F,MAAM,MAAM,kBAAkB;AAC5B,2EAA2E;AACzE,iBAAiB;AACnB,+DAA+D;GAC7D,WAAW;AACb,gGAAgG;GAC9F,kBAAkB,CAAC;AAEvB,uGAAuG;AACvG,MAAM,WAAW,gBAAgB;IAC/B,+CAA+C;IAC/C,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;IAClB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,oFAAoF;IACpF,QAAQ,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0GAA0G;IAC1G,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+GAA+G;IAC/G,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACtC,4FAA4F;IAC5F,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC1B,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kGAAkG;IAClG,SAAS,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC3B,qCAAqC;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,oBAAoB,CAAC,GAAG,GAAG,OAAO,IAAI,CAChD,IAAI,EAAE,gBAAgB,EACtB,GAAG,EAAE,GAAG,KAEL,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC;AAElF,gFAAgF;AAChF,MAAM,MAAM,kBAAkB,CAAC,GAAG,GAAG,OAAO,IAAI,uBAAuB,GAAG,oBAAoB,CAAC,GAAG,CAAC,CAAC;AAEpG,kGAAkG;AAClG,eAAO,MAAM,OAAO;aAClB,KAAK,EAAE,OAAO;aACd,MAAM,EAAE,SAAS;aACjB,WAAW,EAAE,qBAAqB;CAC1B,CAAC;AAmBX,4FAA4F;AAC5F,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOhD;AAyBD;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,gBAAgB,EACtB,OAAO,GAAE,uBAA4B,GACpC,MAAM,CAqLR;AAED;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAAC,GAAG,EAC/C,IAAI,EAAE,gBAAgB,EACtB,MAAM,EAAE,kBAAkB,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,SAAS,EAClD,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,OAAO,GACtB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAQxB"}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in "access denied" page a BROWSER sees when the dashboard guard refuses a page
|
|
3
|
+
* navigation — the SPA shell, its assets, or (Mode A only) a session-less visit. Before this page
|
|
4
|
+
* existed the guard answered every denial with the JSON it answers the API with
|
|
5
|
+
* (`{ "error": "forbidden" }`), which is the right thing for the SPA's `fetch` calls and the wrong
|
|
6
|
+
* thing for a human staring at a browser tab.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately a small, dependency-free, hand-authored HTML page (like `login_page.ts`) — NOT part
|
|
9
|
+
* of the bundled SPA, so a denial never needs the SPA to load first. It carries NO `<script>` at all,
|
|
10
|
+
* so a host running a nonce'd `script-src` CSP (the `@adonisjs/shield` default) cannot break it; the
|
|
11
|
+
* one inline `<style>` takes the request's CSP nonce when the provider hands one over.
|
|
12
|
+
*
|
|
13
|
+
* Every string the host can supply ({@link AccessDeniedPageOptions}) is HTML-escaped on the way in.
|
|
14
|
+
* They are developer-controlled config values, not user input — but escaping them costs nothing and
|
|
15
|
+
* removes the one class of bug a "just interpolate it" template would invite.
|
|
16
|
+
*
|
|
17
|
+
* The visual language (Aviary neutrals, the console's own accent, mono eyebrow over a sans body)
|
|
18
|
+
* mirrors the agent SPA so the refusal reads as part of the same product, not a server default.
|
|
19
|
+
*
|
|
20
|
+
* Ported from `@adonis-agora/payments` (also in `@adonis-agora/durable`, `@adonis-agora/media`, `@adonis-agora/agent` and
|
|
21
|
+
* `@adonis-agora/telescope` with only {@link CONSOLE} changed, so the Agora consoles share ONE
|
|
22
|
+
* refusal story rather than each inventing its own.
|
|
23
|
+
*/
|
|
24
|
+
/** This console's identity — the ONLY thing that differs between the Agora ports of this file. */
|
|
25
|
+
export const CONSOLE = {
|
|
26
|
+
brand: 'Agent',
|
|
27
|
+
accent: '#a78bfa',
|
|
28
|
+
packageName: '@adonis-agora/agent',
|
|
29
|
+
};
|
|
30
|
+
const DEFAULT_COPY = {
|
|
31
|
+
unauthenticated: {
|
|
32
|
+
title: 'Sign in required',
|
|
33
|
+
message: 'You need to be signed in to open this console.',
|
|
34
|
+
},
|
|
35
|
+
forbidden: {
|
|
36
|
+
title: 'Access denied',
|
|
37
|
+
message: 'Your account is not allowed to open this console. If that looks wrong, ask an administrator to grant you access.',
|
|
38
|
+
},
|
|
39
|
+
'session-required': {
|
|
40
|
+
title: 'Open this console from your app',
|
|
41
|
+
message: 'This console only accepts a session minted by your application. Go back to the app and open the console from there.',
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
/** Escape the five characters that matter inside HTML text and double-quoted attributes. */
|
|
45
|
+
export function escapeHtml(value) {
|
|
46
|
+
return value
|
|
47
|
+
.replaceAll('&', '&')
|
|
48
|
+
.replaceAll('<', '<')
|
|
49
|
+
.replaceAll('>', '>')
|
|
50
|
+
.replaceAll('"', '"')
|
|
51
|
+
.replaceAll("'", ''');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Accept only a same-origin, root-relative path for a button `href` — the same open-redirect rule
|
|
55
|
+
* `sanitizeReturnTo` applies to `returnTo`. A host-supplied absolute URL is still a developer's
|
|
56
|
+
* choice, so it is allowed; what is refused is anything that could smuggle a `javascript:` scheme.
|
|
57
|
+
*/
|
|
58
|
+
function safeHref(candidate) {
|
|
59
|
+
const trimmed = candidate.trim();
|
|
60
|
+
if (trimmed === '')
|
|
61
|
+
return null;
|
|
62
|
+
if (/^(https?:)?\/\//i.test(trimmed))
|
|
63
|
+
return trimmed;
|
|
64
|
+
if (trimmed.startsWith('/'))
|
|
65
|
+
return trimmed;
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
/** Only a plausible CSS colour token reaches the stylesheet; anything else falls back to the accent. */
|
|
69
|
+
function safeColor(candidate) {
|
|
70
|
+
return /^[#a-zA-Z0-9(),.%\s-]+$/.test(candidate) ? candidate : CONSOLE.accent;
|
|
71
|
+
}
|
|
72
|
+
const LOCK_ICON = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>';
|
|
73
|
+
const KEY_ICON = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="7.5" cy="15.5" r="4.5"/><path d="m21 2-9.6 9.6"/><path d="m15.5 7.5 3 3L22 7l-3-3"/></svg>';
|
|
74
|
+
/**
|
|
75
|
+
* Render the built-in page. Pure: same `info` + `options` in, same HTML out — so it is trivially
|
|
76
|
+
* snapshot/regex testable and safe to call from anywhere (the provider, a test, a host that wants
|
|
77
|
+
* to embed the default page inside its own layout).
|
|
78
|
+
*/
|
|
79
|
+
export function renderAccessDeniedPage(info, options = {}) {
|
|
80
|
+
const copy = DEFAULT_COPY[info.reason];
|
|
81
|
+
const brand = escapeHtml(options.brand ?? CONSOLE.brand);
|
|
82
|
+
const title = escapeHtml(options.title ?? copy.title);
|
|
83
|
+
const message = escapeHtml(options.message ?? copy.message);
|
|
84
|
+
const accent = safeColor(options.accent ?? CONSOLE.accent);
|
|
85
|
+
const nonceAttr = info.nonce !== undefined ? ` nonce="${escapeHtml(info.nonce)}"` : '';
|
|
86
|
+
const loginHref = options.loginHref === false
|
|
87
|
+
? null
|
|
88
|
+
: (safeHref(options.loginHref ?? info.loginHref ?? '') ?? null);
|
|
89
|
+
const homeHref = options.homeHref === false ? null : safeHref(options.homeHref ?? '/');
|
|
90
|
+
const buttons = [];
|
|
91
|
+
if (loginHref !== null && info.reason !== 'forbidden') {
|
|
92
|
+
buttons.push(`<a class="btn primary" href="${escapeHtml(loginHref)}">${KEY_ICON}${escapeHtml(options.loginLabel ?? 'Sign in')}</a>`);
|
|
93
|
+
}
|
|
94
|
+
if (homeHref !== null) {
|
|
95
|
+
buttons.push(`<a class="btn${buttons.length === 0 ? ' primary' : ''}" href="${escapeHtml(homeHref)}">${escapeHtml(options.homeLabel ?? 'Back to app')}</a>`);
|
|
96
|
+
}
|
|
97
|
+
if (loginHref !== null && info.reason === 'forbidden') {
|
|
98
|
+
// A forbidden user might simply be the wrong user: offer a way to switch accounts, second.
|
|
99
|
+
buttons.push(`<a class="btn" href="${escapeHtml(loginHref)}">${escapeHtml(options.loginLabel ?? 'Sign in as someone else')}</a>`);
|
|
100
|
+
}
|
|
101
|
+
const detail = info.detail !== undefined && info.detail !== ''
|
|
102
|
+
? `\n <p class="detail">${escapeHtml(info.detail)}</p>`
|
|
103
|
+
: '';
|
|
104
|
+
const actions = buttons.length > 0 ? `\n <div class="actions">${buttons.join('')}</div>` : '';
|
|
105
|
+
return `<!doctype html>
|
|
106
|
+
<html lang="en">
|
|
107
|
+
<head>
|
|
108
|
+
<meta charset="utf-8" />
|
|
109
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
110
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
111
|
+
<title>${title} — ${brand}</title>
|
|
112
|
+
<style${nonceAttr}>
|
|
113
|
+
:root {
|
|
114
|
+
color-scheme: dark;
|
|
115
|
+
--bg: #09090b;
|
|
116
|
+
--panel: #0c0c0f;
|
|
117
|
+
--panel-2: #101017;
|
|
118
|
+
--line: #1c1c22;
|
|
119
|
+
--text: #e7e7ea;
|
|
120
|
+
--muted: #76767f;
|
|
121
|
+
--bad: #f87171;
|
|
122
|
+
--accent: ${accent};
|
|
123
|
+
}
|
|
124
|
+
* { box-sizing: border-box; }
|
|
125
|
+
body {
|
|
126
|
+
margin: 0;
|
|
127
|
+
min-height: 100vh;
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
justify-content: center;
|
|
131
|
+
padding: 24px;
|
|
132
|
+
background:
|
|
133
|
+
radial-gradient(900px 480px at 50% -10%, color-mix(in srgb, var(--accent) 9%, transparent), transparent 70%),
|
|
134
|
+
var(--bg);
|
|
135
|
+
color: var(--text);
|
|
136
|
+
font: 15px/1.6 "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
137
|
+
-webkit-font-smoothing: antialiased;
|
|
138
|
+
}
|
|
139
|
+
.card {
|
|
140
|
+
position: relative;
|
|
141
|
+
overflow: hidden;
|
|
142
|
+
width: 100%;
|
|
143
|
+
max-width: 440px;
|
|
144
|
+
padding: 36px 32px 28px;
|
|
145
|
+
background: var(--panel);
|
|
146
|
+
border: 1px solid var(--line);
|
|
147
|
+
border-radius: 12px;
|
|
148
|
+
box-shadow: 0 30px 60px -24px rgb(0 0 0 / 0.7);
|
|
149
|
+
}
|
|
150
|
+
.card::before {
|
|
151
|
+
content: "";
|
|
152
|
+
position: absolute;
|
|
153
|
+
inset: 0 0 auto 0;
|
|
154
|
+
height: 2px;
|
|
155
|
+
background: linear-gradient(90deg, var(--accent), transparent 80%);
|
|
156
|
+
}
|
|
157
|
+
.eyebrow {
|
|
158
|
+
display: flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
gap: 10px;
|
|
161
|
+
margin: 0 0 22px;
|
|
162
|
+
font: 11px/1 "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
163
|
+
letter-spacing: 0.12em;
|
|
164
|
+
text-transform: uppercase;
|
|
165
|
+
color: var(--muted);
|
|
166
|
+
}
|
|
167
|
+
.eyebrow .brand { color: var(--accent); }
|
|
168
|
+
.eyebrow .status {
|
|
169
|
+
margin-left: auto;
|
|
170
|
+
padding: 4px 7px;
|
|
171
|
+
border: 1px solid var(--line);
|
|
172
|
+
border-radius: 4px;
|
|
173
|
+
color: var(--bad);
|
|
174
|
+
letter-spacing: 0.08em;
|
|
175
|
+
}
|
|
176
|
+
.icon {
|
|
177
|
+
display: grid;
|
|
178
|
+
place-items: center;
|
|
179
|
+
width: 40px;
|
|
180
|
+
height: 40px;
|
|
181
|
+
margin-bottom: 18px;
|
|
182
|
+
border-radius: 10px;
|
|
183
|
+
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
|
184
|
+
color: var(--accent);
|
|
185
|
+
}
|
|
186
|
+
h1 {
|
|
187
|
+
margin: 0 0 8px;
|
|
188
|
+
font-size: 22px;
|
|
189
|
+
font-weight: 600;
|
|
190
|
+
letter-spacing: -0.01em;
|
|
191
|
+
}
|
|
192
|
+
p { margin: 0; color: var(--muted); }
|
|
193
|
+
.detail {
|
|
194
|
+
margin-top: 14px;
|
|
195
|
+
padding: 10px 12px;
|
|
196
|
+
border: 1px solid var(--line);
|
|
197
|
+
border-radius: 6px;
|
|
198
|
+
background: var(--panel-2);
|
|
199
|
+
font: 12px/1.5 "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
200
|
+
color: var(--text);
|
|
201
|
+
overflow-wrap: anywhere;
|
|
202
|
+
}
|
|
203
|
+
.actions {
|
|
204
|
+
display: flex;
|
|
205
|
+
flex-wrap: wrap;
|
|
206
|
+
gap: 10px;
|
|
207
|
+
margin-top: 26px;
|
|
208
|
+
}
|
|
209
|
+
.btn {
|
|
210
|
+
display: inline-flex;
|
|
211
|
+
align-items: center;
|
|
212
|
+
gap: 8px;
|
|
213
|
+
padding: 9px 14px;
|
|
214
|
+
border: 1px solid var(--line);
|
|
215
|
+
border-radius: 6px;
|
|
216
|
+
background: var(--panel-2);
|
|
217
|
+
color: var(--text);
|
|
218
|
+
font-size: 13px;
|
|
219
|
+
font-weight: 500;
|
|
220
|
+
text-decoration: none;
|
|
221
|
+
}
|
|
222
|
+
.btn:hover { border-color: color-mix(in srgb, var(--accent) 50%, var(--line)); }
|
|
223
|
+
.btn.primary {
|
|
224
|
+
background: color-mix(in srgb, var(--accent) 14%, transparent);
|
|
225
|
+
border-color: color-mix(in srgb, var(--accent) 45%, transparent);
|
|
226
|
+
color: var(--accent);
|
|
227
|
+
}
|
|
228
|
+
.btn.primary:hover { background: color-mix(in srgb, var(--accent) 22%, transparent); }
|
|
229
|
+
.foot {
|
|
230
|
+
margin-top: 26px;
|
|
231
|
+
font: 11px/1 "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
232
|
+
letter-spacing: 0.04em;
|
|
233
|
+
color: var(--muted);
|
|
234
|
+
opacity: 0.7;
|
|
235
|
+
}
|
|
236
|
+
</style>
|
|
237
|
+
</head>
|
|
238
|
+
<body>
|
|
239
|
+
<main class="card">
|
|
240
|
+
<p class="eyebrow"><span class="brand">${brand}</span><span>console</span><span class="status">${info.status}</span></p>
|
|
241
|
+
<div class="icon" aria-hidden="true">${info.reason === 'forbidden' ? LOCK_ICON : KEY_ICON}</div>
|
|
242
|
+
<h1>${title}</h1>
|
|
243
|
+
<p>${message}</p>${detail}${actions}
|
|
244
|
+
<p class="foot">${escapeHtml(CONSOLE.packageName)}</p>
|
|
245
|
+
</main>
|
|
246
|
+
</body>
|
|
247
|
+
</html>`;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Resolve WHAT to serve for a refusal, honouring the host's `accessDenied` option in either form.
|
|
251
|
+
* Returns the HTML to send, or `null` when a custom renderer already answered the request itself
|
|
252
|
+
* (`answered()` reports whether it did — the provider wires that to "a `location` header or a body
|
|
253
|
+
* is already set"). Framework-light: knows nothing about the HTTP context beyond passing it through.
|
|
254
|
+
*/
|
|
255
|
+
export async function resolveAccessDeniedPage(info, option, ctx, answered) {
|
|
256
|
+
if (typeof option === 'function') {
|
|
257
|
+
const rendered = await option(info, ctx);
|
|
258
|
+
if (typeof rendered === 'string')
|
|
259
|
+
return rendered;
|
|
260
|
+
if (answered())
|
|
261
|
+
return null;
|
|
262
|
+
return renderAccessDeniedPage(info);
|
|
263
|
+
}
|
|
264
|
+
return renderAccessDeniedPage(info, option ?? {});
|
|
265
|
+
}
|
|
266
|
+
//# sourceMappingURL=access_denied_page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access_denied_page.js","sourceRoot":"","sources":["../../../src/dashboard/access_denied_page.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
2
|
import type { AgentGovernanceAuthorize } from '../governance-gate.js';
|
|
3
3
|
import type { Actor } from '../types.js';
|
|
4
|
+
import type { AccessDeniedOption as GenericAccessDeniedOption, AccessDeniedRenderer as GenericAccessDeniedRenderer } from './access_denied_page.js';
|
|
5
|
+
/**
|
|
6
|
+
* The function form of {@link AgentDashboardConfig.accessDenied}: render (or answer) a refused
|
|
7
|
+
* page navigation yourself. Receives the refusal ({@link AccessDeniedInfo}) and the AdonisJS
|
|
8
|
+
* {@link HttpContext}. Return an HTML string to have it served; answer the request yourself (a
|
|
9
|
+
* redirect, most commonly) and return nothing to make the provider stand down; return nothing
|
|
10
|
+
* WITHOUT answering and the built-in page is served.
|
|
11
|
+
*/
|
|
12
|
+
export type AccessDeniedRenderer = GenericAccessDeniedRenderer<HttpContext>;
|
|
13
|
+
/** `accessDenied` in either form — an options object for the built-in page, or a renderer. */
|
|
14
|
+
export type AccessDeniedOption = GenericAccessDeniedOption<HttpContext>;
|
|
4
15
|
/**
|
|
5
16
|
* An extra authorization gate for the dashboard, run AFTER the agent config's `actorResolver` has
|
|
6
17
|
* resolved the caller. Return `false` (or throw) to deny — the request gets `403`. The governance
|
|
@@ -55,12 +66,23 @@ export interface AgentDashboardConfig {
|
|
|
55
66
|
* `401 { error }` JSON with a custom page/redirect. See {@link AgentDashboardUnauthenticatedHook}.
|
|
56
67
|
*/
|
|
57
68
|
onUnauthenticated?: AgentDashboardUnauthenticatedHook;
|
|
69
|
+
/**
|
|
70
|
+
* What a BROWSER sees when the gate refuses — the SPA shell or its assets (the console has no
|
|
71
|
+
* JSON routes of its own; its data comes from the agent's `/agent/governance/*` routes, which are
|
|
72
|
+
* unaffected). Omit it for the built-in page — a dark card in the console's own visual language
|
|
73
|
+
* with the status, a sentence explaining the refusal and a "Back to app" link. Pass an object to
|
|
74
|
+
* tweak that page (`brand`, `title`, `message`, `homeHref`, `accent`, …), or a function to
|
|
75
|
+
* render/answer it yourself — see {@link AccessDeniedRenderer}. A redirect written by
|
|
76
|
+
* `onUnauthenticated` or `authorize` still wins: the provider never overwrites a `location` header.
|
|
77
|
+
*/
|
|
78
|
+
accessDenied?: AccessDeniedOption;
|
|
58
79
|
}
|
|
59
80
|
export interface ResolvedAgentDashboardConfig {
|
|
60
81
|
enabled: boolean;
|
|
61
82
|
path?: string;
|
|
62
83
|
authorize?: AgentDashboardAuthorize;
|
|
63
84
|
onUnauthenticated?: AgentDashboardUnauthenticatedHook;
|
|
85
|
+
accessDenied?: AccessDeniedOption;
|
|
64
86
|
}
|
|
65
87
|
/** Fill defaults for the optional dashboard config block. */
|
|
66
88
|
export declare function resolveDashboardConfig(config: AgentDashboardConfig | undefined): ResolvedAgentDashboardConfig;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define_config.d.ts","sourceRoot":"","sources":["../../../src/dashboard/define_config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACtE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"define_config.d.ts","sourceRoot":"","sources":["../../../src/dashboard/define_config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACtE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,KAAK,EACV,kBAAkB,IAAI,yBAAyB,EAC/C,oBAAoB,IAAI,2BAA2B,EACpD,MAAM,yBAAyB,CAAC;AAEjC;;;;;;GAMG;AACH,MAAM,MAAM,oBAAoB,GAAG,2BAA2B,CAAC,WAAW,CAAC,CAAC;AAE5E,8FAA8F;AAC9F,MAAM,MAAM,kBAAkB,GAAG,yBAAyB,CAAC,WAAW,CAAC,CAAC;AAExE;;;;;GAKG;AACH,MAAM,MAAM,uBAAuB,GAAG,CACpC,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,WAAW,KACb,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,iCAAiC,GAAG,CAAC,GAAG,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE3F;;;;;;;;;GASG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,SAAS,CAAC,EAAE,uBAAuB,CAAC;IACpC;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iCAAiC,CAAC;IACtD;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,kBAAkB,CAAC;CACnC;AAED,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,uBAAuB,CAAC;IACpC,iBAAiB,CAAC,EAAE,iCAAiC,CAAC;IACtD,YAAY,CAAC,EAAE,kBAAkB,CAAC;CACnC;AAED,6DAA6D;AAC7D,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,oBAAoB,GAAG,SAAS,GACvC,4BAA4B,CAQ9B;AAED,uGAAuG;AACvG,MAAM,MAAM,sBAAsB,GAC9B;IAAE,KAAK,EAAE,IAAI,CAAA;CAAE,GACf;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,UAAU,CAAA;CAAE,GACpC;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC/D;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,uBAAuB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAoBvE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,OAAO,EAChB,mBAAmB,EAAE,wBAAwB,GAAG,SAAS,EACzD,iBAAiB,CAAC,EAAE,OAAO,GAC1B,sBAAsB,CAgBxB"}
|
|
@@ -7,6 +7,8 @@ export function resolveDashboardConfig(config) {
|
|
|
7
7
|
resolved.authorize = config.authorize;
|
|
8
8
|
if (config?.onUnauthenticated !== undefined)
|
|
9
9
|
resolved.onUnauthenticated = config.onUnauthenticated;
|
|
10
|
+
if (config?.accessDenied !== undefined)
|
|
11
|
+
resolved.accessDenied = config.accessDenied;
|
|
10
12
|
return resolved;
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define_config.js","sourceRoot":"","sources":["../../../src/dashboard/define_config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"define_config.js","sourceRoot":"","sources":["../../../src/dashboard/define_config.ts"],"names":[],"mappings":"AAmGA,6DAA6D;AAC7D,MAAM,UAAU,sBAAsB,CACpC,MAAwC;IAExC,MAAM,QAAQ,GAAiC,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,IAAI,IAAI,EAAE,CAAC;IACpF,IAAI,MAAM,EAAE,IAAI,KAAK,SAAS;QAAE,QAAQ,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;IAC5D,IAAI,MAAM,EAAE,SAAS,KAAK,SAAS;QAAE,QAAQ,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;IAC3E,IAAI,MAAM,EAAE,iBAAiB,KAAK,SAAS;QACzC,QAAQ,CAAC,iBAAiB,GAAG,MAAM,CAAC,iBAAiB,CAAC;IACxD,IAAI,MAAM,EAAE,YAAY,KAAK,SAAS;QAAE,QAAQ,CAAC,YAAY,GAAG,MAAM,CAAC,YAAY,CAAC;IACpF,OAAO,QAAQ,CAAC;AAClB,CAAC;AASD;;;;GAIG;AACH,MAAM,0BAA0B,GAC9B,0sBAA0sB,CAAC;AAE7sB;;;;;;GAMG;AACH,MAAM,6BAA6B,GACjC,ipBAAipB,CAAC;AAEppB;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAAgB,EAChB,mBAAyD,EACzD,iBAA2B;IAE3B,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAC1D,IAAI,mBAAmB,KAAK,SAAS,EAAE,CAAC;QACtC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,EAAE,0BAA0B,EAAE,CAAC;IAC7F,CAAC;IACD,mGAAmG;IACnG,iGAAiG;IACjG,0FAA0F;IAC1F,IAAI,iBAAiB,KAAK,KAAK,EAAE,CAAC;QAChC,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE,uBAAuB;YAC/B,OAAO,EAAE,6BAA6B;SACvC,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AACzB,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
2
|
import type { ActorResolver } from '../spi/actor-resolver.js';
|
|
3
|
-
import type { AgentDashboardAuthorize, AgentDashboardUnauthenticatedHook } from './define_config.js';
|
|
3
|
+
import type { AccessDeniedOption, AgentDashboardAuthorize, AgentDashboardUnauthenticatedHook } from './define_config.js';
|
|
4
4
|
/** The outcome of the dashboard access gate: proceed, or deny with an HTTP status + message. */
|
|
5
5
|
export type DashboardGateVerdict = {
|
|
6
6
|
ok: true;
|
|
@@ -47,4 +47,26 @@ export type DashboardGateVerdict = {
|
|
|
47
47
|
* this function only guarantees the hook runs before the verdict is handed back.
|
|
48
48
|
*/
|
|
49
49
|
export declare function evaluateDashboardGate(ctx: HttpContext, actorResolver: ActorResolver | undefined, authorize?: AgentDashboardAuthorize, debug?: boolean, onUnauthenticated?: AgentDashboardUnauthenticatedHook): Promise<DashboardGateVerdict>;
|
|
50
|
+
/** What {@link answerDashboardDenial} needs to serve (or delegate) the access-denied page. */
|
|
51
|
+
export interface DashboardDenialOptions {
|
|
52
|
+
/** The console's mount (`/agent/dashboard`) — reported to a custom renderer as `basePath`. */
|
|
53
|
+
basePath: string;
|
|
54
|
+
/** The host's `dashboard.accessDenied` option — tweak the built-in page, or render it. */
|
|
55
|
+
accessDenied?: AccessDeniedOption | undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Write a refused {@link DashboardGateVerdict} to the response — shared by BOTH dashboard providers
|
|
59
|
+
* (embedded and standalone) so they refuse identically. A browser is what hits these routes (the
|
|
60
|
+
* SPA shell and its assets), so the body is the built-in access-denied page (or the host's
|
|
61
|
+
* `accessDenied` customisation of it), not the JSON the governance API answers with.
|
|
62
|
+
*
|
|
63
|
+
* Stands down when the request is already answered: `onUnauthenticated`/`authorize` (or the
|
|
64
|
+
* renderer) writing a redirect keeps it — the `location`-header rule the providers have always
|
|
65
|
+
* honoured. The verdict's `error` is shown on the page as a developer detail only when it is more
|
|
66
|
+
* than the generic word (i.e. the `debug` message outside production, or the static "no actor
|
|
67
|
+
* resolver configured" operator hint).
|
|
68
|
+
*/
|
|
69
|
+
export declare function answerDashboardDenial(ctx: HttpContext, verdict: Extract<DashboardGateVerdict, {
|
|
70
|
+
ok: false;
|
|
71
|
+
}>, options: DashboardDenialOptions): Promise<void>;
|
|
50
72
|
//# sourceMappingURL=gate.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../../src/dashboard/gate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../../src/dashboard/gate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D,OAAO,KAAK,EACV,kBAAkB,EAClB,uBAAuB,EACvB,iCAAiC,EAClC,MAAM,oBAAoB,CAAC;AAE5B,gGAAgG;AAChG,MAAM,MAAM,oBAAoB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAElG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,WAAW,EAChB,aAAa,EAAE,aAAa,GAAG,SAAS,EACxC,SAAS,CAAC,EAAE,uBAAuB,EACnC,KAAK,UAAQ,EACb,iBAAiB,CAAC,EAAE,iCAAiC,GACpD,OAAO,CAAC,oBAAoB,CAAC,CA8B/B;AAED,8FAA8F;AAC9F,MAAM,WAAW,sBAAsB;IACrC,8FAA8F;IAC9F,QAAQ,EAAE,MAAM,CAAC;IACjB,0FAA0F;IAC1F,YAAY,CAAC,EAAE,kBAAkB,GAAG,SAAS,CAAC;CAC/C;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,WAAW,EAChB,OAAO,EAAE,OAAO,CAAC,oBAAoB,EAAE;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,CAAC,EACrD,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAmBf"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveAccessDeniedPage } from './access_denied_page.js';
|
|
1
2
|
/**
|
|
2
3
|
* Decide whether a request may reach the dashboard, WITHOUT touching the response — so it is unit
|
|
3
4
|
* testable free of the AdonisJS router/app: resolve the actor through the agent config's resolver
|
|
@@ -68,4 +69,58 @@ export async function evaluateDashboardGate(ctx, actorResolver, authorize, debug
|
|
|
68
69
|
}
|
|
69
70
|
return { ok: true };
|
|
70
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Write a refused {@link DashboardGateVerdict} to the response — shared by BOTH dashboard providers
|
|
74
|
+
* (embedded and standalone) so they refuse identically. A browser is what hits these routes (the
|
|
75
|
+
* SPA shell and its assets), so the body is the built-in access-denied page (or the host's
|
|
76
|
+
* `accessDenied` customisation of it), not the JSON the governance API answers with.
|
|
77
|
+
*
|
|
78
|
+
* Stands down when the request is already answered: `onUnauthenticated`/`authorize` (or the
|
|
79
|
+
* renderer) writing a redirect keeps it — the `location`-header rule the providers have always
|
|
80
|
+
* honoured. The verdict's `error` is shown on the page as a developer detail only when it is more
|
|
81
|
+
* than the generic word (i.e. the `debug` message outside production, or the static "no actor
|
|
82
|
+
* resolver configured" operator hint).
|
|
83
|
+
*/
|
|
84
|
+
export async function answerDashboardDenial(ctx, verdict, options) {
|
|
85
|
+
const answered = () => responseAnswered(ctx);
|
|
86
|
+
if (answered())
|
|
87
|
+
return;
|
|
88
|
+
const nonce = cspNonce(ctx);
|
|
89
|
+
const generic = verdict.error === 'unauthorized' || verdict.error === 'forbidden';
|
|
90
|
+
const info = {
|
|
91
|
+
status: verdict.status,
|
|
92
|
+
reason: verdict.status === 401 ? 'unauthenticated' : 'forbidden',
|
|
93
|
+
basePath: options.basePath,
|
|
94
|
+
...(generic ? {} : { detail: verdict.error }),
|
|
95
|
+
...(nonce !== undefined ? { nonce } : {}),
|
|
96
|
+
};
|
|
97
|
+
const html = await resolveAccessDeniedPage(info, options.accessDenied, ctx, answered);
|
|
98
|
+
if (html === null)
|
|
99
|
+
return;
|
|
100
|
+
ctx.response
|
|
101
|
+
.status(info.status)
|
|
102
|
+
.header('content-type', 'text/html; charset=utf-8')
|
|
103
|
+
.header('cache-control', 'no-store, must-revalidate')
|
|
104
|
+
.send(html);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Whether something already answered this request: a redirect (`location` header) or a body queued
|
|
108
|
+
* on the response. The body check reads AdonisJS's `response.hasLazyBody` structurally so a
|
|
109
|
+
* plain-object `ctx` double in a unit test (which has neither) still works.
|
|
110
|
+
*/
|
|
111
|
+
function responseAnswered(ctx) {
|
|
112
|
+
if (ctx.response.getHeader('location'))
|
|
113
|
+
return true;
|
|
114
|
+
const response = ctx.response;
|
|
115
|
+
return response.hasLazyBody === true || response.headersSent === true;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The request's CSP nonce when the host runs `@adonisjs/shield` with `@nonce` in its policy (shield
|
|
119
|
+
* exposes it as `response.nonce`). Read structurally: this package neither depends on shield nor
|
|
120
|
+
* cares which middleware minted the nonce — only that the page's inline `<style>` carries it.
|
|
121
|
+
*/
|
|
122
|
+
function cspNonce(ctx) {
|
|
123
|
+
const nonce = ctx.response.nonce;
|
|
124
|
+
return typeof nonce === 'string' && nonce !== '' ? nonce : undefined;
|
|
125
|
+
}
|
|
71
126
|
//# sourceMappingURL=gate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate.js","sourceRoot":"","sources":["../../../src/dashboard/gate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"gate.js","sourceRoot":"","sources":["../../../src/dashboard/gate.ts"],"names":[],"mappings":"AAEA,OAAO,EAAyB,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAUzF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,GAAgB,EAChB,aAAwC,EACxC,SAAmC,EACnC,KAAK,GAAG,KAAK,EACb,iBAAqD;IAErD,IAAI,aAAa,KAAK,SAAS,EAAE,CAAC;QAChC,MAAM,iBAAiB,EAAE,CAAC,GAAG,CAAC,CAAC;QAC/B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,8BAA8B,EAAE,CAAC;IAC3E,CAAC;IACD,IAAI,KAAoD,CAAC;IACzD,IAAI,CAAC;QACH,KAAK,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC3C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,iBAAiB,EAAE,CAAC,GAAG,CAAC,CAAC;QAC/B,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,GAAG;YACX,KAAK,EAAE,KAAK,IAAI,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,cAAc;SACxE,CAAC;IACJ,CAAC;IACD,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,IAAI,CAAC;YACH,IAAI,CAAC,CAAC,MAAM,SAAS,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC;gBACnC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;YACxD,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,MAAM,EAAE,GAAG;gBACX,KAAK,EAAE,KAAK,IAAI,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW;aACrE,CAAC;QACJ,CAAC;IACH,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC;AAUD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,GAAgB,EAChB,OAAqD,EACrD,OAA+B;IAE/B,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;IAC7C,IAAI,QAAQ,EAAE;QAAE,OAAO;IACvB,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC5B,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,KAAK,cAAc,IAAI,OAAO,CAAC,KAAK,KAAK,WAAW,CAAC;IAClF,MAAM,IAAI,GAAqB;QAC7B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM,EAAE,OAAO,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,WAAW;QAChE,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;QAC7C,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC1C,CAAC;IACF,MAAM,IAAI,GAAG,MAAM,uBAAuB,CAAC,IAAI,EAAE,OAAO,CAAC,YAAY,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC;IACtF,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO;IAC1B,GAAG,CAAC,QAAQ;SACT,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;SACnB,MAAM,CAAC,cAAc,EAAE,0BAA0B,CAAC;SAClD,MAAM,CAAC,eAAe,EAAE,2BAA2B,CAAC;SACpD,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAED;;;;GAIG;AACH,SAAS,gBAAgB,CAAC,GAAgB;IACxC,IAAI,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IACpD,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAuE,CAAC;IAC7F,OAAO,QAAQ,CAAC,WAAW,KAAK,IAAI,IAAI,QAAQ,CAAC,WAAW,KAAK,IAAI,CAAC;AACxE,CAAC;AAED;;;;GAIG;AACH,SAAS,QAAQ,CAAC,GAAgB;IAChC,MAAM,KAAK,GAAI,GAAG,CAAC,QAA2C,CAAC,KAAK,CAAC;IACrE,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACvE,CAAC"}
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
* so a package.json edge back from here to there would be circular. See
|
|
15
15
|
* `providers/dashboard_provider.ts`'s module doc for the full story.
|
|
16
16
|
*/
|
|
17
|
-
export
|
|
18
|
-
export {
|
|
17
|
+
export type { AccessDeniedInfo, AccessDeniedPageOptions, AccessDeniedReason, } from './access_denied_page.js';
|
|
18
|
+
export { CONSOLE as ACCESS_DENIED_CONSOLE, escapeHtml, renderAccessDeniedPage, resolveAccessDeniedPage, } from './access_denied_page.js';
|
|
19
|
+
export { type AccessDeniedOption, type AccessDeniedRenderer, type AgentDashboardAuthorize, type AgentDashboardConfig, type AgentDashboardUnauthenticatedHook, type DashboardMountDecision, decideDashboardMount, type ResolvedAgentDashboardConfig, resolveDashboardConfig, } from './define_config.js';
|
|
20
|
+
export { answerDashboardDenial, type DashboardDenialOptions, type DashboardGateVerdict, evaluateDashboardGate, } from './gate.js';
|
|
19
21
|
export { apiBaseFor, CONFIG_ELEMENT_ID, contentTypeFor, injectApiBase, injectBaseHref, mountPathFor, safeAssetSegments, trimSlashes, } from './paths.js';
|
|
20
22
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/dashboard/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EACzB,KAAK,iCAAiC,EACtC,KAAK,sBAAsB,EAC3B,oBAAoB,EACpB,KAAK,4BAA4B,EACjC,sBAAsB,GACvB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/dashboard/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,YAAY,EACV,gBAAgB,EAChB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,OAAO,IAAI,qBAAqB,EAChC,UAAU,EACV,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EACzB,KAAK,iCAAiC,EACtC,KAAK,sBAAsB,EAC3B,oBAAoB,EACpB,KAAK,4BAA4B,EACjC,sBAAsB,GACvB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,qBAAqB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,qBAAqB,GACtB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,UAAU,EACV,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,cAAc,EACd,YAAY,EACZ,iBAAiB,EACjB,WAAW,GACZ,MAAM,YAAY,CAAC"}
|
|
@@ -1,20 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* mount/gate decisions, and the pure path/asset helpers. Framework-light (no AdonisJS router/app
|
|
4
|
-
* dependency), so they're unit-testable in isolation and reusable by BOTH dashboard providers:
|
|
5
|
-
*
|
|
6
|
-
* - `@adonis-agora/agent/dashboard_provider` (this package, `providers/dashboard_provider.ts`) —
|
|
7
|
-
* embedded: registered automatically by `node ace configure @adonis-agora/agent`, serves the SPA
|
|
8
|
-
* bundle copied into THIS package's own `dist/assets/spa` at build time.
|
|
9
|
-
* - `@adonis-agora/agent-dashboard`'s own provider (`agent_dashboard_provider.ts`) — standalone: an
|
|
10
|
-
* independently installable/registrable package some apps already depend on directly.
|
|
11
|
-
*
|
|
12
|
-
* Living here (rather than in `@adonis-agora/agent-dashboard`) keeps the dependency direction
|
|
13
|
-
* one-way: the dashboard package already depends on this one for `AgentConfig`/`ActorResolver`/etc.,
|
|
14
|
-
* so a package.json edge back from here to there would be circular. See
|
|
15
|
-
* `providers/dashboard_provider.ts`'s module doc for the full story.
|
|
16
|
-
*/
|
|
1
|
+
// The built-in "access denied" page (what a browser sees on a refused page navigation).
|
|
2
|
+
export { CONSOLE as ACCESS_DENIED_CONSOLE, escapeHtml, renderAccessDeniedPage, resolveAccessDeniedPage, } from './access_denied_page.js';
|
|
17
3
|
export { decideDashboardMount, resolveDashboardConfig, } from './define_config.js';
|
|
18
|
-
export { evaluateDashboardGate } from './gate.js';
|
|
4
|
+
export { answerDashboardDenial, evaluateDashboardGate, } from './gate.js';
|
|
19
5
|
export { apiBaseFor, CONFIG_ELEMENT_ID, contentTypeFor, injectApiBase, injectBaseHref, mountPathFor, safeAssetSegments, trimSlashes, } from './paths.js';
|
|
20
6
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/dashboard/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/dashboard/index.ts"],"names":[],"mappings":"AAqBA,wFAAwF;AACxF,OAAO,EACL,OAAO,IAAI,qBAAqB,EAChC,UAAU,EACV,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAOL,oBAAoB,EAEpB,sBAAsB,GACvB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,qBAAqB,EAGrB,qBAAqB,GACtB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,UAAU,EACV,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,cAAc,EACd,YAAY,EACZ,iBAAiB,EACjB,WAAW,GACZ,MAAM,YAAY,CAAC"}
|
package/dist/src/index.d.ts
CHANGED
package/dist/src/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.26.0';
|
|
2
2
|
// Re-export the configure hook from the package root so `node ace configure` finds it.
|
|
3
3
|
// AdonisJS imports the package MAIN and reads `configure` off the module namespace —
|
|
4
4
|
// the `./configure` subpath alone is never consulted.
|
package/package.json
CHANGED