@adonis-agora/authkit-server 1.0.0-rc.1 → 1.0.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +94 -0
- package/README.md +4 -5
- package/build/commands/agora_doctor.d.ts +22 -0
- package/build/commands/agora_doctor.js +109 -0
- package/build/commands/clients_create.d.ts +4 -0
- package/build/commands/clients_create.js +97 -0
- package/build/commands/commands.json +78 -0
- package/build/commands/doctor.js +8 -74
- package/build/host/views/account/apps.edge +1 -5
- package/build/host/views/account/extension.edge +77 -0
- package/build/host/views/account/mfa.edge +2 -0
- package/build/host/views/account/partials/extension_actions.edge +14 -0
- package/build/host/views/account/partials/extension_block.edge +197 -0
- package/build/host/views/account/partials/extension_value.edge +8 -0
- package/build/host/views/account/partials/nav.edge +14 -0
- package/build/host/views/account/security.edge +1 -5
- package/build/host/views/account/tokens.edge +2 -2
- package/build/host/views/agents/consent.edge +11 -2
- package/build/host/views/agents/done.edge +1 -1
- package/build/host/views/partials/styles.edge +1 -1
- package/build/index.d.ts +12 -12
- package/build/index.js +7 -9
- package/build/providers/authkit_server_provider.js +28 -8
- package/build/src/account/extension.d.ts +10 -0
- package/build/src/account/extension.js +9 -0
- package/build/src/account/http.d.ts +77 -0
- package/build/src/account/http.js +381 -0
- package/build/src/account/mount.d.ts +40 -0
- package/build/src/account/mount.js +171 -0
- package/build/src/account/registry.d.ts +41 -0
- package/build/src/account/registry.js +81 -0
- package/build/src/account/remote.d.ts +72 -0
- package/build/src/account/remote.js +406 -0
- package/build/src/account/runtime.d.ts +289 -0
- package/build/src/account/runtime.js +641 -0
- package/build/src/accounts/account_store.d.ts +6 -0
- package/build/src/accounts/lucid_store/organizations.js +6 -0
- package/build/src/agents/config.d.ts +9 -13
- package/build/src/agents/config.js +17 -11
- package/build/src/agents/delegation_service.js +3 -3
- package/build/src/agents/delegation_store.d.ts +3 -2
- package/build/src/agents/delegation_store.js +6 -5
- package/build/src/agents/extension.d.ts +261 -0
- package/build/src/agents/extension.js +40 -0
- package/build/src/agents/middleware.js +1 -1
- package/build/src/agents/protocol.d.ts +4 -4
- package/build/src/agents/registry_store.d.ts +3 -2
- package/build/src/agents/registry_store.js +1 -1
- package/build/src/agents/runtime.d.ts +26 -7
- package/build/src/agents/runtime.js +110 -75
- package/build/src/audit/audit_sink.d.ts +1 -1
- package/build/src/audit/audit_sink.js +10 -1
- package/build/src/console_switcher.d.ts +132 -0
- package/build/src/console_switcher.js +206 -0
- package/build/src/define_config.d.ts +34 -12
- package/build/src/define_config.js +15 -7
- package/build/src/dev_tools/contract.d.ts +72 -0
- package/build/src/dev_tools/contract.js +10 -0
- package/build/src/dev_tools/gate.d.ts +17 -0
- package/build/src/dev_tools/gate.js +69 -0
- package/build/src/dev_tools/register.d.ts +11 -0
- package/build/src/dev_tools/register.js +25 -0
- package/build/src/dev_tools/tools.d.ts +24 -0
- package/build/src/dev_tools/tools.js +384 -0
- package/build/src/doctor/agora_checks.d.ts +15 -0
- package/build/src/doctor/agora_checks.js +150 -0
- package/build/src/doctor/agora_doctor.d.ts +115 -0
- package/build/src/doctor/agora_doctor.js +692 -0
- package/build/src/doctor/doctor_input.d.ts +27 -0
- package/build/src/doctor/doctor_input.js +86 -0
- package/build/src/doctor/index.d.ts +5 -0
- package/build/src/doctor/index.js +4 -0
- package/build/src/host/access_token_verifier.js +7 -0
- package/build/src/host/account_api/account_api_controller.js +8 -6
- package/build/src/host/account_session_sign_in.d.ts +12 -0
- package/build/src/host/account_session_sign_in.js +19 -0
- package/build/src/host/admin_api/admin_users_service.d.ts +5 -2
- package/build/src/host/admin_api/api_clients_controller.d.ts +37 -0
- package/build/src/host/admin_api/api_clients_controller.js +31 -1
- package/build/src/host/admin_api/dto.d.ts +3 -1
- package/build/src/host/admin_api/dto.js +2 -0
- package/build/src/host/admin_authorize.d.ts +8 -0
- package/build/src/host/admin_authorize.js +25 -0
- package/build/src/host/admin_clients_service.d.ts +20 -0
- package/build/src/host/admin_clients_service.js +55 -0
- package/build/src/host/admin_console/admin_console_entry.d.ts +14 -0
- package/build/src/host/admin_console/admin_console_entry.js +80 -0
- package/build/src/host/admin_console/admin_shell_controller.js +8 -0
- package/build/src/host/admin_console/console_clients_controller.d.ts +41 -0
- package/build/src/host/admin_console/console_clients_controller.js +34 -1
- package/build/src/host/admin_console/console_personal_agents_controller.d.ts +1 -6
- package/build/src/host/admin_console/console_personal_agents_controller.js +50 -30
- package/build/src/host/admin_console/console_switcher_controller.d.ts +11 -0
- package/build/src/host/admin_console/console_switcher_controller.js +23 -0
- package/build/src/host/admin_sessions_service.js +8 -5
- package/build/src/host/admin_validators.d.ts +29 -0
- package/build/src/host/admin_validators.js +30 -0
- package/build/src/host/auth_host_config.d.ts +9 -2
- package/build/src/host/controllers/account_apps_controller.js +15 -11
- package/build/src/host/csrf.d.ts +3 -3
- package/build/src/host/csrf.js +3 -13
- package/build/src/host/default_mailer.d.ts +11 -6
- package/build/src/host/default_mailer.js +16 -12
- package/build/src/host/i18n.d.ts +41 -14
- package/build/src/host/i18n.js +55 -13
- package/build/src/host/rate_limit.d.ts +8 -0
- package/build/src/host/rate_limit.js +9 -0
- package/build/src/host/register_auth_host.d.ts +14 -0
- package/build/src/host/register_auth_host.js +167 -106
- package/build/src/host/renderers/edge_renderer.js +5 -0
- package/build/src/host/renderers/inertia_renderer.js +7 -1
- package/build/src/host/screen_props.d.ts +39 -8
- package/build/src/host/ui-dist/assets/index-Dq3dJVBv.js +67 -0
- package/build/src/host/ui-dist/assets/{index-DTSmD4RU.css → index-WNIyCEPA.css} +1 -1
- package/build/src/host/ui-dist/index.html +2 -2
- package/build/src/provider/build_provider.js +37 -6
- package/build/src/provider/oidc_service.d.ts +1 -1
- package/build/src/provider/oidc_service.js +1 -1
- package/build/src/schema/ensure.d.ts +14 -0
- package/build/src/schema/ensure.js +43 -150
- package/build/src/service_tenants/client_metadata.d.ts +39 -0
- package/build/src/service_tenants/client_metadata.js +80 -0
- package/build/src/service_tenants/config.d.ts +44 -0
- package/build/src/service_tenants/config.js +44 -0
- package/build/src/service_tenants/provider_hooks.d.ts +52 -0
- package/build/src/service_tenants/provider_hooks.js +124 -0
- package/build/src/service_tenants/tenant_verifier.d.ts +73 -0
- package/build/src/service_tenants/tenant_verifier.js +104 -0
- package/build/src/tenancy/tenant_labels.d.ts +30 -0
- package/build/src/tenancy/tenant_labels.js +64 -0
- package/build/types.d.ts +3 -1
- package/package.json +9 -6
- package/build/src/agents/poppy/client_registry.d.ts +0 -50
- package/build/src/agents/poppy/client_registry.js +0 -260
- package/build/src/agents/poppy/config.d.ts +0 -396
- package/build/src/agents/poppy/config.js +0 -228
- package/build/src/agents/poppy/dpop.d.ts +0 -61
- package/build/src/agents/poppy/dpop.js +0 -158
- package/build/src/agents/poppy/email_code_sign_in.d.ts +0 -27
- package/build/src/agents/poppy/email_code_sign_in.js +0 -140
- package/build/src/agents/poppy/errors.d.ts +0 -25
- package/build/src/agents/poppy/errors.js +0 -25
- package/build/src/agents/poppy/replay.d.ts +0 -25
- package/build/src/agents/poppy/replay.js +0 -106
- package/build/src/agents/poppy/resource.d.ts +0 -100
- package/build/src/agents/poppy/resource.js +0 -129
- package/build/src/agents/poppy/safe_fetch.d.ts +0 -48
- package/build/src/agents/poppy/safe_fetch.js +0 -262
- package/build/src/agents/poppy/service.d.ts +0 -293
- package/build/src/agents/poppy/service.js +0 -1458
- package/build/src/agents/poppy/session_jwt.d.ts +0 -30
- package/build/src/agents/poppy/session_jwt.js +0 -133
- package/build/src/agents/poppy/store.d.ts +0 -106
- package/build/src/agents/poppy/store.js +0 -261
- package/build/src/agents/poppy/web_session.d.ts +0 -39
- package/build/src/agents/poppy/web_session.js +0 -122
- package/build/src/host/controllers/poppy_consent_controller.d.ts +0 -19
- package/build/src/host/controllers/poppy_consent_controller.js +0 -199
- package/build/src/host/controllers/poppy_controller.d.ts +0 -33
- package/build/src/host/controllers/poppy_controller.js +0 -219
- package/build/src/host/poppy_session_key.d.ts +0 -9
- package/build/src/host/poppy_session_key.js +0 -9
- package/build/src/host/ui-dist/assets/index-D0EIeyKK.js +0 -67
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { ACCOUNT_EXTENSION_CONTRACT, ACCOUNT_EXTENSIONS_MANIFEST_SUBJECT, ACCOUNT_EXTENSIONS_SCOPE, ACCOUNT_EXTENSIONS_TOKEN_TYP, ACCOUNT_EXTENSIONS_WELL_KNOWN, ACCOUNT_EXTENSIONS_WIRE_VERSION, ACCOUNT_RESERVED_SEGMENTS, ACCOUNT_SEGMENT, accountExtensionsBodyHash, validateAccountExtension, } from '@adonis-agora/authkit-core';
|
|
2
|
+
import { importJWK, SignJWT } from 'jose';
|
|
3
|
+
const SIGNING_ALGS = new Set([
|
|
4
|
+
'RS256',
|
|
5
|
+
'RS384',
|
|
6
|
+
'RS512',
|
|
7
|
+
'PS256',
|
|
8
|
+
'PS384',
|
|
9
|
+
'PS512',
|
|
10
|
+
'ES256',
|
|
11
|
+
'ES384',
|
|
12
|
+
'ES512',
|
|
13
|
+
'EdDSA',
|
|
14
|
+
]);
|
|
15
|
+
function algOf(jwk) {
|
|
16
|
+
if (typeof jwk.alg === 'string')
|
|
17
|
+
return SIGNING_ALGS.has(jwk.alg) ? jwk.alg : null;
|
|
18
|
+
if (jwk.kty === 'RSA')
|
|
19
|
+
return 'RS256';
|
|
20
|
+
if (jwk.kty === 'EC' && jwk.crv === 'P-256')
|
|
21
|
+
return 'ES256';
|
|
22
|
+
if (jwk.kty === 'EC' && jwk.crv === 'P-384')
|
|
23
|
+
return 'ES384';
|
|
24
|
+
if (jwk.kty === 'OKP')
|
|
25
|
+
return 'EdDSA';
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
/** Signs a remote-extension token with the IdP's current signing key. */
|
|
29
|
+
export async function signRemoteExtensionToken(signing, claims) {
|
|
30
|
+
const jwk = signing.signingJwks.keys.find((key) => algOf(key) !== null && key.d !== undefined);
|
|
31
|
+
if (!jwk)
|
|
32
|
+
throw new Error('authkit: no signing key can sign remote account extension tokens.');
|
|
33
|
+
const alg = algOf(jwk);
|
|
34
|
+
const key = await importJWK(jwk, alg);
|
|
35
|
+
const iat = Math.floor((claims.now ?? Date.now()) / 1000);
|
|
36
|
+
const payload = {
|
|
37
|
+
scope: ACCOUNT_EXTENSIONS_SCOPE,
|
|
38
|
+
jti: crypto.randomUUID(),
|
|
39
|
+
};
|
|
40
|
+
if (claims.bodyHash !== undefined)
|
|
41
|
+
payload.bh = claims.bodyHash;
|
|
42
|
+
return new SignJWT(payload)
|
|
43
|
+
.setProtectedHeader({
|
|
44
|
+
alg,
|
|
45
|
+
typ: ACCOUNT_EXTENSIONS_TOKEN_TYP,
|
|
46
|
+
...(typeof jwk.kid === 'string' ? { kid: jwk.kid } : {}),
|
|
47
|
+
})
|
|
48
|
+
.setIssuer(signing.issuer)
|
|
49
|
+
.setAudience(claims.audience)
|
|
50
|
+
.setSubject(claims.subject)
|
|
51
|
+
.setIssuedAt(iat)
|
|
52
|
+
.setExpirationTime(iat + (claims.ttlSec ?? 60))
|
|
53
|
+
.sign(key);
|
|
54
|
+
}
|
|
55
|
+
/** Thrown when an app answers a call with an error or cannot be reached. */
|
|
56
|
+
export class RemoteAccountExtensionError extends Error {
|
|
57
|
+
status;
|
|
58
|
+
constructor(message, status) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.status = status;
|
|
61
|
+
this.name = 'RemoteAccountExtensionError';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function baseUrl(source) {
|
|
65
|
+
return `${source.url.replace(/\/+$/, '')}${ACCOUNT_EXTENSIONS_WELL_KNOWN}`;
|
|
66
|
+
}
|
|
67
|
+
function validSource(source, production) {
|
|
68
|
+
if (!source || typeof source.clientId !== 'string' || source.clientId === '') {
|
|
69
|
+
return 'needs a clientId';
|
|
70
|
+
}
|
|
71
|
+
let url;
|
|
72
|
+
try {
|
|
73
|
+
url = new URL(source.url);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return `has an invalid url "${String(source.url)}"`;
|
|
77
|
+
}
|
|
78
|
+
if (url.protocol !== 'https:' && (production || url.protocol !== 'http:')) {
|
|
79
|
+
return `url "${source.url}" must be https`;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The remote account extensions of this IdP: fetches each app's manifest (cached for `ttlMs`,
|
|
85
|
+
* refreshed in the background), turns it into extensions whose `visible`, `load` and actions call
|
|
86
|
+
* the app, and serves their pages next to the in-process ones. An app that is down keeps its last
|
|
87
|
+
* manifest (its pages then show the load-failure notice) or, never reached, contributes nothing.
|
|
88
|
+
*/
|
|
89
|
+
export class RemoteAccountExtensions {
|
|
90
|
+
#entries;
|
|
91
|
+
#deps;
|
|
92
|
+
#visible = new WeakMap();
|
|
93
|
+
#warned = new Set();
|
|
94
|
+
#pagesKey = '';
|
|
95
|
+
#pages = [];
|
|
96
|
+
constructor(sources, deps) {
|
|
97
|
+
this.#deps = deps;
|
|
98
|
+
const seen = new Set();
|
|
99
|
+
this.#entries = [];
|
|
100
|
+
for (const source of sources) {
|
|
101
|
+
const problem = validSource(source, deps.production === true);
|
|
102
|
+
if (problem) {
|
|
103
|
+
throw new Error(`authkit: accountConsole.remote entry ${problem}.`);
|
|
104
|
+
}
|
|
105
|
+
if (seen.has(source.clientId)) {
|
|
106
|
+
throw new Error(`authkit: accountConsole.remote lists the client "${source.clientId}" twice.`);
|
|
107
|
+
}
|
|
108
|
+
seen.add(source.clientId);
|
|
109
|
+
this.#entries.push({
|
|
110
|
+
source,
|
|
111
|
+
extensions: [],
|
|
112
|
+
fetchedAt: 0,
|
|
113
|
+
version: 0,
|
|
114
|
+
inflight: null,
|
|
115
|
+
retryAt: 0,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
get size() {
|
|
120
|
+
return this.#entries.length;
|
|
121
|
+
}
|
|
122
|
+
#now() {
|
|
123
|
+
return this.#deps.now?.() ?? Date.now();
|
|
124
|
+
}
|
|
125
|
+
#log(message) {
|
|
126
|
+
(this.#deps.log ?? ((m) => console.error(m)))(message);
|
|
127
|
+
}
|
|
128
|
+
/** Fetches every manifest now; resolves when all settled or after `timeoutMs`. */
|
|
129
|
+
async start(timeoutMs = 3000) {
|
|
130
|
+
let timer;
|
|
131
|
+
await Promise.race([
|
|
132
|
+
Promise.allSettled(this.#entries.map((entry) => this.#refresh(entry))),
|
|
133
|
+
new Promise((resolve) => {
|
|
134
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
135
|
+
}),
|
|
136
|
+
]);
|
|
137
|
+
if (timer !== undefined)
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
}
|
|
140
|
+
/** Starts a background refresh of every stale manifest (never awaits, never throws). */
|
|
141
|
+
#refreshStale() {
|
|
142
|
+
const now = this.#now();
|
|
143
|
+
for (const entry of this.#entries) {
|
|
144
|
+
const ttl = entry.source.ttlMs ?? 300_000;
|
|
145
|
+
if (entry.inflight || now < entry.retryAt)
|
|
146
|
+
continue;
|
|
147
|
+
if (entry.fetchedAt !== 0 && now - entry.fetchedAt < ttl)
|
|
148
|
+
continue;
|
|
149
|
+
void this.#refresh(entry);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
#refresh(entry) {
|
|
153
|
+
if (entry.inflight)
|
|
154
|
+
return entry.inflight;
|
|
155
|
+
entry.inflight = (async () => {
|
|
156
|
+
try {
|
|
157
|
+
const manifest = await this.#fetchManifest(entry.source);
|
|
158
|
+
entry.extensions = this.#toExtensions(entry.source, manifest);
|
|
159
|
+
entry.fetchedAt = this.#now();
|
|
160
|
+
entry.version += 1;
|
|
161
|
+
entry.retryAt = 0;
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
// Keep the last good manifest; try again in 30 s.
|
|
165
|
+
entry.retryAt = this.#now() + 30_000;
|
|
166
|
+
this.#log(`authkit: remote account extensions of "${entry.source.clientId}" could not be fetched: ${error.message}`);
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
entry.inflight = null;
|
|
170
|
+
}
|
|
171
|
+
})();
|
|
172
|
+
return entry.inflight;
|
|
173
|
+
}
|
|
174
|
+
async #request(source, init) {
|
|
175
|
+
const signing = await this.#deps.signing();
|
|
176
|
+
const token = await signRemoteExtensionToken(signing, {
|
|
177
|
+
audience: source.clientId,
|
|
178
|
+
subject: init.subject,
|
|
179
|
+
...(init.body !== undefined ? { bodyHash: accountExtensionsBodyHash(init.body) } : {}),
|
|
180
|
+
now: this.#now(),
|
|
181
|
+
});
|
|
182
|
+
const doFetch = this.#deps.fetch ?? fetch;
|
|
183
|
+
const response = await doFetch(baseUrl(source), {
|
|
184
|
+
method: init.body === undefined ? 'GET' : 'POST',
|
|
185
|
+
headers: {
|
|
186
|
+
authorization: `Bearer ${token}`,
|
|
187
|
+
accept: 'application/json',
|
|
188
|
+
...(init.body !== undefined ? { 'content-type': 'application/json' } : {}),
|
|
189
|
+
},
|
|
190
|
+
...(init.body !== undefined ? { body: init.body } : {}),
|
|
191
|
+
redirect: 'error',
|
|
192
|
+
signal: AbortSignal.timeout(init.timeoutMs),
|
|
193
|
+
});
|
|
194
|
+
let payload = null;
|
|
195
|
+
try {
|
|
196
|
+
payload = await response.json();
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
payload = null;
|
|
200
|
+
}
|
|
201
|
+
if (!response.ok) {
|
|
202
|
+
const message = typeof payload?.error?.message === 'string'
|
|
203
|
+
? payload.error.message
|
|
204
|
+
: `HTTP ${response.status}`;
|
|
205
|
+
throw new RemoteAccountExtensionError(message, response.status);
|
|
206
|
+
}
|
|
207
|
+
return payload;
|
|
208
|
+
}
|
|
209
|
+
async #fetchManifest(source) {
|
|
210
|
+
const payload = (await this.#request(source, {
|
|
211
|
+
subject: ACCOUNT_EXTENSIONS_MANIFEST_SUBJECT,
|
|
212
|
+
timeoutMs: source.timeoutMs ?? 5000,
|
|
213
|
+
}));
|
|
214
|
+
if (!payload ||
|
|
215
|
+
payload.version !== ACCOUNT_EXTENSIONS_WIRE_VERSION ||
|
|
216
|
+
!Array.isArray(payload.extensions)) {
|
|
217
|
+
throw new Error('the manifest is not a version 1 account extensions manifest');
|
|
218
|
+
}
|
|
219
|
+
return payload;
|
|
220
|
+
}
|
|
221
|
+
/** Builds proxy extensions from a manifest; an invalid extension is skipped and logged. */
|
|
222
|
+
#toExtensions(source, manifest) {
|
|
223
|
+
const out = [];
|
|
224
|
+
for (const remote of manifest.extensions) {
|
|
225
|
+
const extension = {
|
|
226
|
+
id: remote?.id,
|
|
227
|
+
contract: Math.min(remote?.contract ?? 1, ACCOUNT_EXTENSION_CONTRACT),
|
|
228
|
+
messages: remote?.messages ?? {},
|
|
229
|
+
scriptOrigins: Array.isArray(remote?.scriptOrigins) ? remote.scriptOrigins : [],
|
|
230
|
+
pages: (Array.isArray(remote?.pages) ? remote.pages : []).map((page) => this.#toPage(source, page)),
|
|
231
|
+
};
|
|
232
|
+
try {
|
|
233
|
+
if (typeof remote?.contract === 'number' && remote.contract > ACCOUNT_EXTENSION_CONTRACT) {
|
|
234
|
+
throw new Error(`"${remote.id}" was written against contract ${remote.contract}; this authkit implements ${ACCOUNT_EXTENSION_CONTRACT}.`);
|
|
235
|
+
}
|
|
236
|
+
validateAccountExtension(extension);
|
|
237
|
+
out.push(extension);
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
this.#log(`authkit: remote account extension from "${source.clientId}" skipped: ${error.message}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
#call(source, call, timeoutMs) {
|
|
246
|
+
const body = JSON.stringify(call);
|
|
247
|
+
return this.#request(source, { subject: call.account.id, body, timeoutMs });
|
|
248
|
+
}
|
|
249
|
+
#base(source, ctx) {
|
|
250
|
+
const urls = {};
|
|
251
|
+
for (const page of this.#pages) {
|
|
252
|
+
if (page.remote === source.clientId)
|
|
253
|
+
urls[page.page.id] = this.#deps.pathOf(page.segment);
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
account: {
|
|
257
|
+
id: ctx.account.id,
|
|
258
|
+
email: ctx.account.email,
|
|
259
|
+
name: ctx.account.name,
|
|
260
|
+
roles: [...ctx.account.roles],
|
|
261
|
+
},
|
|
262
|
+
activeOrg: ctx.activeOrg,
|
|
263
|
+
tenant: ctx.tenant ?? null,
|
|
264
|
+
impersonatedBy: ctx.impersonatedBy,
|
|
265
|
+
locale: ctx.locale,
|
|
266
|
+
urls,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/** The visible conditional pages of `source` for this account, once per request. */
|
|
270
|
+
#visibleSet(source, ctx) {
|
|
271
|
+
const key = ctx.account;
|
|
272
|
+
let perRequest = this.#visible.get(key);
|
|
273
|
+
if (!perRequest) {
|
|
274
|
+
perRequest = new Map();
|
|
275
|
+
this.#visible.set(key, perRequest);
|
|
276
|
+
}
|
|
277
|
+
let decided = perRequest.get(source.clientId);
|
|
278
|
+
if (!decided) {
|
|
279
|
+
const pageIds = this.#pages
|
|
280
|
+
.filter((page) => page.remote === source.clientId && page.page.visible)
|
|
281
|
+
.map((page) => page.page.id);
|
|
282
|
+
decided = (async () => {
|
|
283
|
+
try {
|
|
284
|
+
const payload = (await this.#call(source, { ...this.#base(source, ctx), op: 'visible', pageIds }, source.timeoutMs ?? 5000));
|
|
285
|
+
return new Set(Array.isArray(payload?.visible)
|
|
286
|
+
? payload.visible.filter((id) => typeof id === 'string')
|
|
287
|
+
: []);
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
// The app cannot answer: keep its pages listed, so each shows the "could not be
|
|
291
|
+
// loaded" notice instead of vanishing (a page body never loads without the app).
|
|
292
|
+
this.#log(`authkit: remote account extensions of "${source.clientId}" did not answer visible(): ${error.message}`);
|
|
293
|
+
return new Set(pageIds);
|
|
294
|
+
}
|
|
295
|
+
})();
|
|
296
|
+
perRequest.set(source.clientId, decided);
|
|
297
|
+
}
|
|
298
|
+
return decided;
|
|
299
|
+
}
|
|
300
|
+
#toPage(source, remote) {
|
|
301
|
+
const pageId = String(remote?.id);
|
|
302
|
+
const actions = {};
|
|
303
|
+
for (const [name, action] of Object.entries((remote?.actions ?? {}))) {
|
|
304
|
+
actions[name] = {
|
|
305
|
+
label: action?.label ?? name,
|
|
306
|
+
tone: action?.tone === 'danger' ? 'danger' : 'default',
|
|
307
|
+
...(action?.confirm ? { confirm: action.confirm } : {}),
|
|
308
|
+
sudo: action?.sudo === true,
|
|
309
|
+
fields: Array.isArray(action?.fields) ? action.fields : [],
|
|
310
|
+
handle: async (ctx, input) => {
|
|
311
|
+
const payload = (await this.#call(source, {
|
|
312
|
+
...this.#base(source, ctx),
|
|
313
|
+
op: 'action',
|
|
314
|
+
pageId,
|
|
315
|
+
action: name,
|
|
316
|
+
query: ctx.query,
|
|
317
|
+
input,
|
|
318
|
+
}, this.#deps.actionTimeoutMs ?? 15_000));
|
|
319
|
+
if (!payload?.result || typeof payload.result.ok !== 'boolean') {
|
|
320
|
+
throw new Error('the app answered the action without a result');
|
|
321
|
+
}
|
|
322
|
+
return payload.result;
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
id: pageId,
|
|
328
|
+
segment: String(remote?.segment),
|
|
329
|
+
title: remote?.title ?? pageId,
|
|
330
|
+
nav: remote?.nav === false ? false : (remote?.nav ?? {}),
|
|
331
|
+
...(remote?.conditional === true
|
|
332
|
+
? {
|
|
333
|
+
visible: async (ctx) => (await this.#visibleSet(source, ctx)).has(pageId),
|
|
334
|
+
}
|
|
335
|
+
: {}),
|
|
336
|
+
load: async (ctx) => {
|
|
337
|
+
const payload = (await this.#call(source, { ...this.#base(source, ctx), op: 'load', pageId, query: ctx.query }, source.timeoutMs ?? 5000));
|
|
338
|
+
if (!payload?.view || !Array.isArray(payload.view.blocks)) {
|
|
339
|
+
throw new Error('the app answered the page without a view');
|
|
340
|
+
}
|
|
341
|
+
return payload.view;
|
|
342
|
+
},
|
|
343
|
+
actions,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/** Every remote extension currently known (for the message catalogs). */
|
|
347
|
+
extensions() {
|
|
348
|
+
return this.#entries.flatMap((entry) => entry.extensions);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* The remote pages to serve now, after host overrides, minus any whose segment collides with a
|
|
352
|
+
* built-in screen, an in-process page or an earlier app (skipped and logged once).
|
|
353
|
+
*/
|
|
354
|
+
pages(context) {
|
|
355
|
+
this.#refreshStale();
|
|
356
|
+
const key = this.#entries.map((e) => e.version).join(',');
|
|
357
|
+
if (key === this.#pagesKey)
|
|
358
|
+
return this.#pages;
|
|
359
|
+
const used = new Set(context.taken);
|
|
360
|
+
const usedIds = new Set(context.takenIds);
|
|
361
|
+
const out = [];
|
|
362
|
+
for (const entry of this.#entries) {
|
|
363
|
+
for (const extension of entry.extensions) {
|
|
364
|
+
const override = context.overrides?.[extension.id];
|
|
365
|
+
if (override === false)
|
|
366
|
+
continue;
|
|
367
|
+
if (usedIds.has(extension.id)) {
|
|
368
|
+
this.#warnOnce(`${entry.source.clientId}:${extension.id}`, `authkit: remote account extension "${extension.id}" from "${entry.source.clientId}" is skipped: the id is already used.`);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
usedIds.add(extension.id);
|
|
372
|
+
for (const page of extension.pages) {
|
|
373
|
+
const pageOverride = override ? override.pages?.[page.id] : undefined;
|
|
374
|
+
if (pageOverride === false)
|
|
375
|
+
continue;
|
|
376
|
+
const segment = pageOverride?.segment ?? page.segment;
|
|
377
|
+
if (!ACCOUNT_SEGMENT.test(segment) ||
|
|
378
|
+
ACCOUNT_RESERVED_SEGMENTS.has(segment) ||
|
|
379
|
+
used.has(segment)) {
|
|
380
|
+
this.#warnOnce(`${entry.source.clientId}:${page.id}:${segment}`, `authkit: remote account page "${page.id}" from "${entry.source.clientId}" is skipped: the segment "${segment}" is reserved or already used.`);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
used.add(segment);
|
|
384
|
+
const nav = page.nav === false ? undefined : page.nav;
|
|
385
|
+
out.push({
|
|
386
|
+
extensionId: extension.id,
|
|
387
|
+
page,
|
|
388
|
+
segment,
|
|
389
|
+
order: pageOverride?.order ?? nav?.order ?? 100,
|
|
390
|
+
scriptOrigins: extension.scriptOrigins ?? [],
|
|
391
|
+
remote: entry.source.clientId,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
this.#pagesKey = key;
|
|
397
|
+
this.#pages = out;
|
|
398
|
+
return out;
|
|
399
|
+
}
|
|
400
|
+
#warnOnce(key, message) {
|
|
401
|
+
if (this.#warned.has(key))
|
|
402
|
+
return;
|
|
403
|
+
this.#warned.add(key);
|
|
404
|
+
this.#log(message);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
import type { AccountActiveOrg, AccountExtensionContext, AccountExtensionOverride, AccountPageView, AccountSubject, AccountTenantRef, ActionRef, Block, Text, Value } from './extension.js';
|
|
3
|
+
import type { AccountExtensionRegistryImpl, RegisteredAccountPage } from './registry.js';
|
|
4
|
+
import type { RemoteAccountExtensionSource, RemoteAccountExtensions } from './remote.js';
|
|
5
|
+
/** `accountConsole` in `config/authkit.ts`. */
|
|
6
|
+
export interface AccountConsoleConfig {
|
|
7
|
+
/**
|
|
8
|
+
* Maps the signed-in account to the app's subject (what billing, media and agents key on).
|
|
9
|
+
* Default `{ type: 'User', id: account.id }`. Every extension receives it as `ctx.subject`.
|
|
10
|
+
*/
|
|
11
|
+
subject?: (account: {
|
|
12
|
+
id: string;
|
|
13
|
+
email: string | null;
|
|
14
|
+
name: string | null;
|
|
15
|
+
roles: readonly string[];
|
|
16
|
+
}, context: {
|
|
17
|
+
activeOrg: AccountActiveOrg | null;
|
|
18
|
+
tenant: AccountTenantRef | null;
|
|
19
|
+
}) => AccountSubject | Promise<AccountSubject>;
|
|
20
|
+
/** Turn off, rename or reorder extension pages, by extension id. */
|
|
21
|
+
extensions?: Record<string, AccountExtensionOverride>;
|
|
22
|
+
/** Budget of one page's `load()`, in ms. Default 5000. */
|
|
23
|
+
loadTimeoutMs?: number;
|
|
24
|
+
/** Budget of one action's `handle()`, in ms. Default 15000. */
|
|
25
|
+
actionTimeoutMs?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Apps that sign in through this IdP and serve their own pages (billing, files…) for this
|
|
28
|
+
* console. Each must turn on `accountConsole.remote` in its `config/authkit_client.ts`.
|
|
29
|
+
*/
|
|
30
|
+
remote?: RemoteAccountExtensionSource[];
|
|
31
|
+
}
|
|
32
|
+
/** Built-in screens in the navigation, with their fixed order. */
|
|
33
|
+
export declare const BUILTIN_NAV: readonly [{
|
|
34
|
+
readonly id: 'security';
|
|
35
|
+
readonly order: 10;
|
|
36
|
+
readonly label: 'account.security.title';
|
|
37
|
+
}, {
|
|
38
|
+
readonly id: 'mfa';
|
|
39
|
+
readonly order: 20;
|
|
40
|
+
readonly label: 'account.mfa.title';
|
|
41
|
+
}, {
|
|
42
|
+
readonly id: 'tokens';
|
|
43
|
+
readonly order: 30;
|
|
44
|
+
readonly label: 'account.tokens.title';
|
|
45
|
+
}, {
|
|
46
|
+
readonly id: 'apps';
|
|
47
|
+
readonly order: 40;
|
|
48
|
+
readonly label: 'account.apps.title';
|
|
49
|
+
}, {
|
|
50
|
+
readonly id: 'orgs';
|
|
51
|
+
readonly order: 50;
|
|
52
|
+
readonly label: 'account.orgs.title';
|
|
53
|
+
}];
|
|
54
|
+
export type BuiltinNavId = (typeof BUILTIN_NAV)[number]['id'];
|
|
55
|
+
interface ConsoleState {
|
|
56
|
+
pages: readonly RegisteredAccountPage[];
|
|
57
|
+
builtins: Partial<Record<BuiltinNavId, string>>;
|
|
58
|
+
registry: AccountExtensionRegistryImpl | null;
|
|
59
|
+
remote: RemoteAccountExtensions | null;
|
|
60
|
+
/** Segments of the mounted built-in screens. */
|
|
61
|
+
builtinSegments: readonly string[];
|
|
62
|
+
overrides?: Readonly<Record<string, AccountExtensionOverride>>;
|
|
63
|
+
}
|
|
64
|
+
/** Called by `registerAuthHost` with the mounted screens and the frozen pages. */
|
|
65
|
+
export declare function setAccountConsoleState(next: Partial<ConsoleState>): void;
|
|
66
|
+
/** The served extension pages. */
|
|
67
|
+
export declare function accountExtensionPages(): readonly RegisteredAccountPage[];
|
|
68
|
+
/** The remote pages currently served (for the catch-all routes). */
|
|
69
|
+
export declare function remoteAccountPageBySegment(segment: string): RegisteredAccountPage | undefined;
|
|
70
|
+
/** Resets the process state (tests). */
|
|
71
|
+
export declare function resetAccountConsoleState(): void;
|
|
72
|
+
export declare function findPageById(pageId: string): RegisteredAccountPage | undefined;
|
|
73
|
+
export declare function findPageBySegment(segment: string): RegisteredAccountPage | undefined;
|
|
74
|
+
/** Same-origin path of an extension page. */
|
|
75
|
+
export declare function pageHref(page: RegisteredAccountPage, query?: Record<string, string>): string;
|
|
76
|
+
/** Looks up a message: host catalog (overrides win), then the extension's own catalogs. */
|
|
77
|
+
export declare function makeTranslator(hostMessages: Record<string, string>, locale: string, catalogs: readonly (Readonly<Record<string, Readonly<Record<string, string>>>> | undefined)[]): (text: Text | undefined | null) => string;
|
|
78
|
+
/** A value ready to show: `text` is formatted for the locale. */
|
|
79
|
+
export interface ResolvedValue {
|
|
80
|
+
type: Value['type'];
|
|
81
|
+
text: string;
|
|
82
|
+
tone?: 'neutral' | 'success' | 'warning' | 'danger';
|
|
83
|
+
/** ISO timestamp of a date value. */
|
|
84
|
+
iso?: string;
|
|
85
|
+
amount?: number;
|
|
86
|
+
currency?: string;
|
|
87
|
+
value?: number;
|
|
88
|
+
}
|
|
89
|
+
export declare function formatValue(value: Value, t: (text: Text | undefined) => string, locale: string, now: Date): ResolvedValue;
|
|
90
|
+
/**
|
|
91
|
+
* A safe href: an https URL when `external`, else a same-origin path (`/…`, never `//…` nor
|
|
92
|
+
* `/\…`). Anything else (`javascript:`, `data:`, protocol-relative) is `null`.
|
|
93
|
+
*/
|
|
94
|
+
export declare function safeHref(href: unknown, external?: boolean): string | null;
|
|
95
|
+
/** A script `src` a component block may load: same-origin, or https on an allowed origin. */
|
|
96
|
+
export declare function safeScriptSrc(src: unknown, origins: readonly string[]): string | null;
|
|
97
|
+
/** A button for an {@link ActionRef}. */
|
|
98
|
+
export interface ResolvedAction {
|
|
99
|
+
action: string;
|
|
100
|
+
target: string | null;
|
|
101
|
+
label: string;
|
|
102
|
+
tone: 'default' | 'danger';
|
|
103
|
+
sudo: boolean;
|
|
104
|
+
/** `confirm` or `fields`: the button opens the confirmation step (`href`, GET). */
|
|
105
|
+
step: boolean;
|
|
106
|
+
/** POST target (no step) or the confirmation step (step). */
|
|
107
|
+
href: string;
|
|
108
|
+
/** Always the POST target of the action (forms that collect the fields themselves). */
|
|
109
|
+
postHref: string;
|
|
110
|
+
/** JSON endpoint for this action. */
|
|
111
|
+
apiHref: string;
|
|
112
|
+
}
|
|
113
|
+
export type ResolvedBlock = {
|
|
114
|
+
kind: 'notice';
|
|
115
|
+
tone: 'info' | 'success' | 'warning' | 'danger';
|
|
116
|
+
text: string;
|
|
117
|
+
} | {
|
|
118
|
+
kind: 'keyValue';
|
|
119
|
+
title: string | null;
|
|
120
|
+
rows: Array<{
|
|
121
|
+
label: string;
|
|
122
|
+
value: ResolvedValue;
|
|
123
|
+
}>;
|
|
124
|
+
actions: ResolvedAction[];
|
|
125
|
+
} | {
|
|
126
|
+
kind: 'list';
|
|
127
|
+
title: string | null;
|
|
128
|
+
empty: string;
|
|
129
|
+
items: Array<{
|
|
130
|
+
title: string;
|
|
131
|
+
subtitle: string | null;
|
|
132
|
+
meta: ResolvedValue[];
|
|
133
|
+
badge: ResolvedValue | null;
|
|
134
|
+
href: string | null;
|
|
135
|
+
external: boolean;
|
|
136
|
+
actions: ResolvedAction[];
|
|
137
|
+
}>;
|
|
138
|
+
nextHref: string | null;
|
|
139
|
+
nextCursor: string | null;
|
|
140
|
+
} | {
|
|
141
|
+
kind: 'table';
|
|
142
|
+
title: string | null;
|
|
143
|
+
empty: string;
|
|
144
|
+
columns: Array<{
|
|
145
|
+
id: string;
|
|
146
|
+
label: string;
|
|
147
|
+
}>;
|
|
148
|
+
rows: Array<{
|
|
149
|
+
cells: ResolvedValue[];
|
|
150
|
+
actions: ResolvedAction[];
|
|
151
|
+
}>;
|
|
152
|
+
hasActions: boolean;
|
|
153
|
+
nextHref: string | null;
|
|
154
|
+
nextCursor: string | null;
|
|
155
|
+
} | {
|
|
156
|
+
kind: 'meter';
|
|
157
|
+
label: string;
|
|
158
|
+
used: number;
|
|
159
|
+
limit: number | null;
|
|
160
|
+
percent: number | null;
|
|
161
|
+
text: string;
|
|
162
|
+
resetsAt: ResolvedValue | null;
|
|
163
|
+
} | {
|
|
164
|
+
kind: 'actions';
|
|
165
|
+
items: ResolvedAction[];
|
|
166
|
+
} | {
|
|
167
|
+
kind: 'link';
|
|
168
|
+
label: string;
|
|
169
|
+
href: string;
|
|
170
|
+
external: boolean;
|
|
171
|
+
} | {
|
|
172
|
+
kind: 'toolPreview';
|
|
173
|
+
title: string;
|
|
174
|
+
tool: string | null;
|
|
175
|
+
subtitle: string | null;
|
|
176
|
+
args: Array<{
|
|
177
|
+
label: string;
|
|
178
|
+
value: ResolvedValue;
|
|
179
|
+
}>;
|
|
180
|
+
body: {
|
|
181
|
+
format: 'json' | 'text';
|
|
182
|
+
content: string;
|
|
183
|
+
} | null;
|
|
184
|
+
actions: ResolvedAction[];
|
|
185
|
+
} | {
|
|
186
|
+
kind: 'component';
|
|
187
|
+
provider: string;
|
|
188
|
+
mountId: string;
|
|
189
|
+
scripts: Array<{
|
|
190
|
+
src: string;
|
|
191
|
+
integrity: string | null;
|
|
192
|
+
module: boolean;
|
|
193
|
+
}>;
|
|
194
|
+
/** The props, serialized for a `<script type="application/json">` (`<` escaped). */
|
|
195
|
+
propsJson: string;
|
|
196
|
+
props: Record<string, unknown>;
|
|
197
|
+
submit: ResolvedAction | null;
|
|
198
|
+
fallback: {
|
|
199
|
+
label: string;
|
|
200
|
+
href: string;
|
|
201
|
+
external: boolean;
|
|
202
|
+
} | null;
|
|
203
|
+
};
|
|
204
|
+
export interface ResolvedAccountPage {
|
|
205
|
+
id: string;
|
|
206
|
+
extension: string;
|
|
207
|
+
title: string;
|
|
208
|
+
href: string;
|
|
209
|
+
tabs: Array<{
|
|
210
|
+
id: string;
|
|
211
|
+
label: string;
|
|
212
|
+
href: string;
|
|
213
|
+
active: boolean;
|
|
214
|
+
}>;
|
|
215
|
+
blocks: ResolvedBlock[];
|
|
216
|
+
}
|
|
217
|
+
/** JSON for a `<script type="application/json">`: never closes the tag, never runs. */
|
|
218
|
+
export declare function jsonForDataBlock(value: unknown): string;
|
|
219
|
+
export interface ResolveInput {
|
|
220
|
+
page: RegisteredAccountPage;
|
|
221
|
+
view: AccountPageView;
|
|
222
|
+
t: (text: Text | undefined | null) => string;
|
|
223
|
+
locale: string;
|
|
224
|
+
now: Date;
|
|
225
|
+
query: {
|
|
226
|
+
cursor?: string;
|
|
227
|
+
tab?: string;
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/** The button for `ref`, or `null` when the page has no such action. */
|
|
231
|
+
export declare function resolveAction(page: RegisteredAccountPage, ref: ActionRef, t: (text: Text | undefined | null) => string): ResolvedAction | null;
|
|
232
|
+
/** Resolves one block; an unknown or malformed block becomes a warning notice. */
|
|
233
|
+
export declare function resolveBlock(input: ResolveInput, block: Block): ResolvedBlock;
|
|
234
|
+
export declare function resolvePageView(input: ResolveInput): ResolvedAccountPage;
|
|
235
|
+
export interface ConsoleRequest {
|
|
236
|
+
/** The base context (no `query`); each page call gets its own. */
|
|
237
|
+
base: Omit<AccountExtensionContext, 'query'>;
|
|
238
|
+
hostMessages: Record<string, string>;
|
|
239
|
+
locale: string;
|
|
240
|
+
config: AccountConsoleConfig;
|
|
241
|
+
cfg: any;
|
|
242
|
+
/** Memoized `visible()` per page id. */
|
|
243
|
+
visible: Map<string, Promise<boolean>>;
|
|
244
|
+
}
|
|
245
|
+
/** The console context of this request, built once. `null` without a signed-in account. */
|
|
246
|
+
export declare function consoleRequest(ctx: HttpContext): Promise<ConsoleRequest | null>;
|
|
247
|
+
/** Validated query: `cursor` and `tab` only. */
|
|
248
|
+
export declare function validatedQuery(raw: Record<string, unknown>): {
|
|
249
|
+
cursor?: string;
|
|
250
|
+
tab?: string;
|
|
251
|
+
};
|
|
252
|
+
export declare function extensionContext(request: ConsoleRequest, query: {
|
|
253
|
+
cursor?: string;
|
|
254
|
+
tab?: string;
|
|
255
|
+
}): AccountExtensionContext;
|
|
256
|
+
/** The translator for one page (its extension's catalogs). */
|
|
257
|
+
export declare function translatorFor(request: ConsoleRequest, page: RegisteredAccountPage | null): (text: Text | undefined | null) => string;
|
|
258
|
+
/** `page.visible(ctx)`, once per request; a throw hides the page. */
|
|
259
|
+
export declare function isPageVisible(request: ConsoleRequest, page: RegisteredAccountPage): Promise<boolean>;
|
|
260
|
+
export declare function logExtensionError(page: RegisteredAccountPage, where: string, error: unknown): void;
|
|
261
|
+
/** Runs `fn` with a time budget. */
|
|
262
|
+
export declare function withTimeout<T>(work: Promise<T>, ms: number, what: string): Promise<T>;
|
|
263
|
+
/** Loads and resolves a page; a throw or timeout becomes a danger notice. */
|
|
264
|
+
export declare function loadResolvedPage(request: ConsoleRequest, page: RegisteredAccountPage, query: {
|
|
265
|
+
cursor?: string;
|
|
266
|
+
tab?: string;
|
|
267
|
+
}): Promise<ResolvedAccountPage>;
|
|
268
|
+
/** One navigation entry of the account console. */
|
|
269
|
+
export interface AccountNavEntry {
|
|
270
|
+
id: string;
|
|
271
|
+
label: string;
|
|
272
|
+
href: string;
|
|
273
|
+
active: boolean;
|
|
274
|
+
group: 'account' | 'billing' | 'ai' | 'content';
|
|
275
|
+
order: number;
|
|
276
|
+
}
|
|
277
|
+
/** Mounted built-in screens (id → path). */
|
|
278
|
+
export declare function mountedBuiltins(): Partial<Record<BuiltinNavId, string>>;
|
|
279
|
+
/**
|
|
280
|
+
* The account console navigation: mounted built-in screens plus the extension pages visible to
|
|
281
|
+
* this account, sorted by `(group, order, id)`.
|
|
282
|
+
*/
|
|
283
|
+
export declare function buildAccountNav(ctx: HttpContext, activeId: string | null): Promise<AccountNavEntry[]>;
|
|
284
|
+
/**
|
|
285
|
+
* The `accountNav` prop of an account screen (`null` for other views). Never throws: a broken
|
|
286
|
+
* extension leaves the built-in entries.
|
|
287
|
+
*/
|
|
288
|
+
export declare function accountNavFor(ctx: HttpContext, view: string, props: Record<string, unknown>): Promise<AccountNavEntry[] | null>;
|
|
289
|
+
export {};
|