@adonis-agora/authkit-server 1.0.0-rc.7 → 1.0.0-rc.9
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 +19 -0
- 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/partials/styles.edge +1 -1
- package/build/index.d.ts +2 -0
- package/build/index.js +1 -0
- package/build/providers/authkit_server_provider.js +5 -0
- package/build/src/account/extension.d.ts +323 -0
- package/build/src/account/extension.js +36 -0
- package/build/src/account/http.d.ts +71 -0
- package/build/src/account/http.js +369 -0
- package/build/src/account/mount.d.ts +35 -0
- package/build/src/account/mount.js +99 -0
- package/build/src/account/registry.d.ts +48 -0
- package/build/src/account/registry.js +229 -0
- package/build/src/account/runtime.d.ts +277 -0
- package/build/src/account/runtime.js +623 -0
- package/build/src/audit/audit_sink.d.ts +1 -1
- package/build/src/audit/audit_sink.js +4 -0
- package/build/src/console_switcher.d.ts +132 -0
- package/build/src/console_switcher.js +206 -0
- package/build/src/define_config.d.ts +18 -0
- package/build/src/define_config.js +4 -0
- package/build/src/host/admin_api/dto.d.ts +1 -1
- 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_switcher_controller.d.ts +11 -0
- package/build/src/host/admin_console/console_switcher_controller.js +23 -0
- package/build/src/host/auth_host_config.d.ts +3 -0
- package/build/src/host/i18n.d.ts +36 -0
- package/build/src/host/i18n.js +38 -0
- 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 +140 -29
- 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 +28 -0
- 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/types.d.ts +3 -1
- package/package.json +3 -2
- package/build/src/host/ui-dist/assets/index-u0bMxkLg.js +0 -67
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { ACCOUNT_BLOCK_KINDS, ACCOUNT_EXTENSION_CONTRACT, } from './extension.js';
|
|
2
|
+
const EXTENSION_ID = /^[a-z0-9-]{2,16}$/;
|
|
3
|
+
const PAGE_LOCAL_ID = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
4
|
+
const SEGMENT = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
5
|
+
const ACTION_NAME = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
|
|
6
|
+
const FIELD_NAME = /^[a-zA-Z][a-zA-Z0-9_]{0,63}$/;
|
|
7
|
+
/** Segments authkit itself uses under the account prefix, whatever the host renames. */
|
|
8
|
+
const RESERVED_SEGMENTS = new Set([
|
|
9
|
+
'api',
|
|
10
|
+
'login',
|
|
11
|
+
'logout',
|
|
12
|
+
'security',
|
|
13
|
+
'mfa',
|
|
14
|
+
'confirm',
|
|
15
|
+
'tokens',
|
|
16
|
+
'apps',
|
|
17
|
+
'orgs',
|
|
18
|
+
'email',
|
|
19
|
+
'x',
|
|
20
|
+
'assets',
|
|
21
|
+
]);
|
|
22
|
+
/** Built-in screen keys (an extension id may not shadow one). */
|
|
23
|
+
const BUILTIN_IDS = new Set([
|
|
24
|
+
'login',
|
|
25
|
+
'logout',
|
|
26
|
+
'security',
|
|
27
|
+
'mfa',
|
|
28
|
+
'confirm',
|
|
29
|
+
'tokens',
|
|
30
|
+
'apps',
|
|
31
|
+
'orgs',
|
|
32
|
+
'account',
|
|
33
|
+
'authkit',
|
|
34
|
+
]);
|
|
35
|
+
/** Thrown for an invalid extension; the message names it. */
|
|
36
|
+
export class AccountExtensionError extends Error {
|
|
37
|
+
}
|
|
38
|
+
function fail(id, message) {
|
|
39
|
+
throw new AccountExtensionError(`authkit: account extension "${id}" — ${message}`);
|
|
40
|
+
}
|
|
41
|
+
function validateAction(id, pageId, name, action) {
|
|
42
|
+
if (!ACTION_NAME.test(name))
|
|
43
|
+
fail(id, `page "${pageId}" has an invalid action name "${name}".`);
|
|
44
|
+
if (typeof action?.handle !== 'function') {
|
|
45
|
+
fail(id, `action "${pageId}.${name}" has no handle().`);
|
|
46
|
+
}
|
|
47
|
+
for (const field of action.fields ?? []) {
|
|
48
|
+
if (!FIELD_NAME.test(field?.name ?? '')) {
|
|
49
|
+
fail(id, `action "${pageId}.${name}" has an invalid field name "${String(field?.name)}".`);
|
|
50
|
+
}
|
|
51
|
+
if (!['text', 'select', 'textarea'].includes(field.kind)) {
|
|
52
|
+
fail(id, `field "${field.name}" of "${pageId}.${name}" has an unknown kind.`);
|
|
53
|
+
}
|
|
54
|
+
if (field.kind === 'select' && !(field.options && field.options.length > 0)) {
|
|
55
|
+
fail(id, `select field "${field.name}" of "${pageId}.${name}" needs options.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Validates an extension's own shape (no knowledge of the other extensions). */
|
|
60
|
+
export function validateAccountExtension(extension) {
|
|
61
|
+
const id = String(extension?.id);
|
|
62
|
+
if (!EXTENSION_ID.test(id) || BUILTIN_IDS.has(id)) {
|
|
63
|
+
fail(id, 'invalid id (2–16 of a-z, 0-9, -; not a built-in screen).');
|
|
64
|
+
}
|
|
65
|
+
if (typeof extension.contract !== 'number' || !Number.isInteger(extension.contract)) {
|
|
66
|
+
fail(id, 'contract must be the major it was written against (1).');
|
|
67
|
+
}
|
|
68
|
+
if (extension.contract > ACCOUNT_EXTENSION_CONTRACT) {
|
|
69
|
+
fail(id, `written against contract ${extension.contract}; this authkit implements ${ACCOUNT_EXTENSION_CONTRACT}. Upgrade @adonis-agora/authkit-server.`);
|
|
70
|
+
}
|
|
71
|
+
for (const [locale, catalog] of Object.entries(extension.messages ?? {})) {
|
|
72
|
+
for (const key of Object.keys(catalog ?? {})) {
|
|
73
|
+
if (!key.startsWith(`${id}.`)) {
|
|
74
|
+
fail(id, `message key "${key}" (${locale}) must start with "${id}.".`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const origin of extension.scriptOrigins ?? []) {
|
|
79
|
+
let parsed = null;
|
|
80
|
+
try {
|
|
81
|
+
parsed = new URL(origin);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
parsed = null;
|
|
85
|
+
}
|
|
86
|
+
if (!parsed || parsed.protocol !== 'https:' || parsed.origin !== origin) {
|
|
87
|
+
fail(id, `scriptOrigins entry "${origin}" must be an https origin (no path).`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!Array.isArray(extension.pages) || extension.pages.length === 0) {
|
|
91
|
+
fail(id, 'needs at least one page.');
|
|
92
|
+
}
|
|
93
|
+
const pageIds = new Set();
|
|
94
|
+
const segments = new Set();
|
|
95
|
+
for (const page of extension.pages) {
|
|
96
|
+
const pageId = String(page?.id);
|
|
97
|
+
const [prefix, local, ...rest] = pageId.split('.');
|
|
98
|
+
if (prefix !== id || !local || rest.length > 0 || !PAGE_LOCAL_ID.test(local)) {
|
|
99
|
+
fail(id, `page id "${pageId}" must be "${id}.<page>" (a-z, 0-9, -).`);
|
|
100
|
+
}
|
|
101
|
+
if (pageIds.has(pageId))
|
|
102
|
+
fail(id, `page "${pageId}" is listed twice.`);
|
|
103
|
+
pageIds.add(pageId);
|
|
104
|
+
if (!SEGMENT.test(String(page.segment)) || RESERVED_SEGMENTS.has(page.segment)) {
|
|
105
|
+
fail(id, `page "${pageId}" has an invalid or reserved segment "${String(page.segment)}".`);
|
|
106
|
+
}
|
|
107
|
+
if (segments.has(page.segment))
|
|
108
|
+
fail(id, `segment "${page.segment}" is used twice.`);
|
|
109
|
+
segments.add(page.segment);
|
|
110
|
+
if (typeof page.load !== 'function')
|
|
111
|
+
fail(id, `page "${pageId}" has no load().`);
|
|
112
|
+
for (const [name, action] of Object.entries(page.actions ?? {})) {
|
|
113
|
+
validateAction(id, pageId, name, action);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The registry bound at `authkit.accountExtensions`. Contributors call `register()` from their
|
|
119
|
+
* provider's `boot()`; authkit freezes it when it mounts the console routes.
|
|
120
|
+
*/
|
|
121
|
+
export class AccountExtensionRegistryImpl {
|
|
122
|
+
contract = ACCOUNT_EXTENSION_CONTRACT;
|
|
123
|
+
blockKinds = ACCOUNT_BLOCK_KINDS;
|
|
124
|
+
#extensions = [];
|
|
125
|
+
#pages = null;
|
|
126
|
+
#production;
|
|
127
|
+
#log;
|
|
128
|
+
constructor(options = {}) {
|
|
129
|
+
this.#production = options.production ?? (() => false);
|
|
130
|
+
this.#log = options.log ?? ((message) => console.error(message));
|
|
131
|
+
}
|
|
132
|
+
/** Adds an extension. Throws outside production on an invalid one; skips it in production. */
|
|
133
|
+
register(extension) {
|
|
134
|
+
if (this.#pages !== null) {
|
|
135
|
+
throw new AccountExtensionError(`authkit: account extension "${String(extension?.id)}" registered after the account console routes were mounted. Register it from your provider's boot(), not from a preload.`);
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
validateAccountExtension(extension);
|
|
139
|
+
if (this.#extensions.some((e) => e.id === extension.id)) {
|
|
140
|
+
fail(extension.id, 'is registered twice.');
|
|
141
|
+
}
|
|
142
|
+
for (const other of this.#extensions) {
|
|
143
|
+
for (const page of extension.pages) {
|
|
144
|
+
if (other.pages.some((p) => p.segment === page.segment)) {
|
|
145
|
+
fail(extension.id, `segment "${page.segment}" is already used by "${other.id}".`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
if (!this.#production())
|
|
152
|
+
throw error;
|
|
153
|
+
this.#log(`${error.message} The extension is skipped.`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
this.#extensions.push(extension);
|
|
157
|
+
}
|
|
158
|
+
get frozen() {
|
|
159
|
+
return this.#pages !== null;
|
|
160
|
+
}
|
|
161
|
+
/** The registered extensions, in registration order. */
|
|
162
|
+
extensions() {
|
|
163
|
+
return this.#extensions;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Applies the host overrides, checks the segments against the mounted screens and freezes the
|
|
167
|
+
* registry. Returns the pages to serve. Idempotent.
|
|
168
|
+
*/
|
|
169
|
+
freeze(options) {
|
|
170
|
+
if (this.#pages !== null)
|
|
171
|
+
return this.#pages;
|
|
172
|
+
const log = options.log ?? this.#log;
|
|
173
|
+
const builtin = new Set(options.builtinSegments ?? []);
|
|
174
|
+
const used = new Map();
|
|
175
|
+
const out = [];
|
|
176
|
+
for (const extension of this.#extensions) {
|
|
177
|
+
const override = options.overrides?.[extension.id];
|
|
178
|
+
if (override === false)
|
|
179
|
+
continue;
|
|
180
|
+
const pages = [];
|
|
181
|
+
try {
|
|
182
|
+
for (const page of extension.pages) {
|
|
183
|
+
const pageOverride = override ? override.pages?.[page.id] : undefined;
|
|
184
|
+
if (pageOverride === false)
|
|
185
|
+
continue;
|
|
186
|
+
const segment = pageOverride?.segment ?? page.segment;
|
|
187
|
+
if (!SEGMENT.test(segment) || RESERVED_SEGMENTS.has(segment)) {
|
|
188
|
+
fail(extension.id, `accountConsole.extensions renames "${page.id}" to an invalid segment "${segment}".`);
|
|
189
|
+
}
|
|
190
|
+
if (builtin.has(segment)) {
|
|
191
|
+
fail(extension.id, `segment "${segment}" of "${page.id}" collides with a built-in screen.`);
|
|
192
|
+
}
|
|
193
|
+
const owner = used.get(segment);
|
|
194
|
+
if (owner)
|
|
195
|
+
fail(extension.id, `segment "${segment}" of "${page.id}" is already used by "${owner}".`);
|
|
196
|
+
used.set(segment, page.id);
|
|
197
|
+
const nav = page.nav === false ? undefined : page.nav;
|
|
198
|
+
pages.push({
|
|
199
|
+
extensionId: extension.id,
|
|
200
|
+
page,
|
|
201
|
+
segment,
|
|
202
|
+
order: pageOverride?.order ?? nav?.order ?? 100,
|
|
203
|
+
scriptOrigins: extension.scriptOrigins ?? [],
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
for (const page of pages)
|
|
209
|
+
used.delete(page.segment);
|
|
210
|
+
if (!options.production)
|
|
211
|
+
throw error;
|
|
212
|
+
log(`${error.message} The extension is skipped.`);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
out.push(...pages);
|
|
216
|
+
}
|
|
217
|
+
for (const id of Object.keys(options.overrides ?? {})) {
|
|
218
|
+
if (!this.#extensions.some((e) => e.id === id)) {
|
|
219
|
+
log(`authkit: accountConsole.extensions names "${id}", which no installed package registered.`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
this.#pages = out;
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
/** The served pages (empty until frozen). */
|
|
226
|
+
pages() {
|
|
227
|
+
return this.#pages ?? [];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
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
|
+
/** `accountConsole` in `config/authkit.ts`. */
|
|
5
|
+
export interface AccountConsoleConfig {
|
|
6
|
+
/**
|
|
7
|
+
* Maps the signed-in account to the app's subject (what billing, media and agents key on).
|
|
8
|
+
* Default `{ type: 'User', id: account.id }`. Every extension receives it as `ctx.subject`.
|
|
9
|
+
*/
|
|
10
|
+
subject?: (account: {
|
|
11
|
+
id: string;
|
|
12
|
+
email: string | null;
|
|
13
|
+
name: string | null;
|
|
14
|
+
roles: readonly string[];
|
|
15
|
+
}, context: {
|
|
16
|
+
activeOrg: AccountActiveOrg | null;
|
|
17
|
+
tenant: AccountTenantRef | null;
|
|
18
|
+
}) => AccountSubject | Promise<AccountSubject>;
|
|
19
|
+
/** Turn off, rename or reorder extension pages, by extension id. */
|
|
20
|
+
extensions?: Record<string, AccountExtensionOverride>;
|
|
21
|
+
/** Budget of one page's `load()`, in ms. Default 5000. */
|
|
22
|
+
loadTimeoutMs?: number;
|
|
23
|
+
/** Budget of one action's `handle()`, in ms. Default 15000. */
|
|
24
|
+
actionTimeoutMs?: number;
|
|
25
|
+
}
|
|
26
|
+
/** Built-in screens in the navigation, with their fixed order. */
|
|
27
|
+
export declare const BUILTIN_NAV: readonly [{
|
|
28
|
+
readonly id: 'security';
|
|
29
|
+
readonly order: 10;
|
|
30
|
+
readonly label: 'account.security.title';
|
|
31
|
+
}, {
|
|
32
|
+
readonly id: 'mfa';
|
|
33
|
+
readonly order: 20;
|
|
34
|
+
readonly label: 'account.mfa.title';
|
|
35
|
+
}, {
|
|
36
|
+
readonly id: 'tokens';
|
|
37
|
+
readonly order: 30;
|
|
38
|
+
readonly label: 'account.tokens.title';
|
|
39
|
+
}, {
|
|
40
|
+
readonly id: 'apps';
|
|
41
|
+
readonly order: 40;
|
|
42
|
+
readonly label: 'account.apps.title';
|
|
43
|
+
}, {
|
|
44
|
+
readonly id: 'orgs';
|
|
45
|
+
readonly order: 50;
|
|
46
|
+
readonly label: 'account.orgs.title';
|
|
47
|
+
}];
|
|
48
|
+
export type BuiltinNavId = (typeof BUILTIN_NAV)[number]['id'];
|
|
49
|
+
interface ConsoleState {
|
|
50
|
+
pages: readonly RegisteredAccountPage[];
|
|
51
|
+
builtins: Partial<Record<BuiltinNavId, string>>;
|
|
52
|
+
registry: AccountExtensionRegistryImpl | null;
|
|
53
|
+
}
|
|
54
|
+
/** Called by `registerAuthHost` with the mounted screens and the frozen pages. */
|
|
55
|
+
export declare function setAccountConsoleState(next: Partial<ConsoleState>): void;
|
|
56
|
+
/** The served extension pages. */
|
|
57
|
+
export declare function accountExtensionPages(): readonly RegisteredAccountPage[];
|
|
58
|
+
/** Resets the process state (tests). */
|
|
59
|
+
export declare function resetAccountConsoleState(): void;
|
|
60
|
+
export declare function findPageById(pageId: string): RegisteredAccountPage | undefined;
|
|
61
|
+
export declare function findPageBySegment(segment: string): RegisteredAccountPage | undefined;
|
|
62
|
+
/** Same-origin path of an extension page. */
|
|
63
|
+
export declare function pageHref(page: RegisteredAccountPage, query?: Record<string, string>): string;
|
|
64
|
+
/** Looks up a message: host catalog (overrides win), then the extension's own catalogs. */
|
|
65
|
+
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;
|
|
66
|
+
/** A value ready to show: `text` is formatted for the locale. */
|
|
67
|
+
export interface ResolvedValue {
|
|
68
|
+
type: Value['type'];
|
|
69
|
+
text: string;
|
|
70
|
+
tone?: 'neutral' | 'success' | 'warning' | 'danger';
|
|
71
|
+
/** ISO timestamp of a date value. */
|
|
72
|
+
iso?: string;
|
|
73
|
+
amount?: number;
|
|
74
|
+
currency?: string;
|
|
75
|
+
value?: number;
|
|
76
|
+
}
|
|
77
|
+
export declare function formatValue(value: Value, t: (text: Text | undefined) => string, locale: string, now: Date): ResolvedValue;
|
|
78
|
+
/**
|
|
79
|
+
* A safe href: an https URL when `external`, else a same-origin path (`/…`, never `//…` nor
|
|
80
|
+
* `/\…`). Anything else (`javascript:`, `data:`, protocol-relative) is `null`.
|
|
81
|
+
*/
|
|
82
|
+
export declare function safeHref(href: unknown, external?: boolean): string | null;
|
|
83
|
+
/** A script `src` a component block may load: same-origin, or https on an allowed origin. */
|
|
84
|
+
export declare function safeScriptSrc(src: unknown, origins: readonly string[]): string | null;
|
|
85
|
+
/** A button for an {@link ActionRef}. */
|
|
86
|
+
export interface ResolvedAction {
|
|
87
|
+
action: string;
|
|
88
|
+
target: string | null;
|
|
89
|
+
label: string;
|
|
90
|
+
tone: 'default' | 'danger';
|
|
91
|
+
sudo: boolean;
|
|
92
|
+
/** `confirm` or `fields`: the button opens the confirmation step (`href`, GET). */
|
|
93
|
+
step: boolean;
|
|
94
|
+
/** POST target (no step) or the confirmation step (step). */
|
|
95
|
+
href: string;
|
|
96
|
+
/** Always the POST target of the action (forms that collect the fields themselves). */
|
|
97
|
+
postHref: string;
|
|
98
|
+
/** JSON endpoint for this action. */
|
|
99
|
+
apiHref: string;
|
|
100
|
+
}
|
|
101
|
+
export type ResolvedBlock = {
|
|
102
|
+
kind: 'notice';
|
|
103
|
+
tone: 'info' | 'success' | 'warning' | 'danger';
|
|
104
|
+
text: string;
|
|
105
|
+
} | {
|
|
106
|
+
kind: 'keyValue';
|
|
107
|
+
title: string | null;
|
|
108
|
+
rows: Array<{
|
|
109
|
+
label: string;
|
|
110
|
+
value: ResolvedValue;
|
|
111
|
+
}>;
|
|
112
|
+
actions: ResolvedAction[];
|
|
113
|
+
} | {
|
|
114
|
+
kind: 'list';
|
|
115
|
+
title: string | null;
|
|
116
|
+
empty: string;
|
|
117
|
+
items: Array<{
|
|
118
|
+
title: string;
|
|
119
|
+
subtitle: string | null;
|
|
120
|
+
meta: ResolvedValue[];
|
|
121
|
+
badge: ResolvedValue | null;
|
|
122
|
+
href: string | null;
|
|
123
|
+
external: boolean;
|
|
124
|
+
actions: ResolvedAction[];
|
|
125
|
+
}>;
|
|
126
|
+
nextHref: string | null;
|
|
127
|
+
nextCursor: string | null;
|
|
128
|
+
} | {
|
|
129
|
+
kind: 'table';
|
|
130
|
+
title: string | null;
|
|
131
|
+
empty: string;
|
|
132
|
+
columns: Array<{
|
|
133
|
+
id: string;
|
|
134
|
+
label: string;
|
|
135
|
+
}>;
|
|
136
|
+
rows: Array<{
|
|
137
|
+
cells: ResolvedValue[];
|
|
138
|
+
actions: ResolvedAction[];
|
|
139
|
+
}>;
|
|
140
|
+
hasActions: boolean;
|
|
141
|
+
nextHref: string | null;
|
|
142
|
+
nextCursor: string | null;
|
|
143
|
+
} | {
|
|
144
|
+
kind: 'meter';
|
|
145
|
+
label: string;
|
|
146
|
+
used: number;
|
|
147
|
+
limit: number | null;
|
|
148
|
+
percent: number | null;
|
|
149
|
+
text: string;
|
|
150
|
+
resetsAt: ResolvedValue | null;
|
|
151
|
+
} | {
|
|
152
|
+
kind: 'actions';
|
|
153
|
+
items: ResolvedAction[];
|
|
154
|
+
} | {
|
|
155
|
+
kind: 'link';
|
|
156
|
+
label: string;
|
|
157
|
+
href: string;
|
|
158
|
+
external: boolean;
|
|
159
|
+
} | {
|
|
160
|
+
kind: 'toolPreview';
|
|
161
|
+
title: string;
|
|
162
|
+
tool: string | null;
|
|
163
|
+
subtitle: string | null;
|
|
164
|
+
args: Array<{
|
|
165
|
+
label: string;
|
|
166
|
+
value: ResolvedValue;
|
|
167
|
+
}>;
|
|
168
|
+
body: {
|
|
169
|
+
format: 'json' | 'text';
|
|
170
|
+
content: string;
|
|
171
|
+
} | null;
|
|
172
|
+
actions: ResolvedAction[];
|
|
173
|
+
} | {
|
|
174
|
+
kind: 'component';
|
|
175
|
+
provider: string;
|
|
176
|
+
mountId: string;
|
|
177
|
+
scripts: Array<{
|
|
178
|
+
src: string;
|
|
179
|
+
integrity: string | null;
|
|
180
|
+
module: boolean;
|
|
181
|
+
}>;
|
|
182
|
+
/** The props, serialized for a `<script type="application/json">` (`<` escaped). */
|
|
183
|
+
propsJson: string;
|
|
184
|
+
props: Record<string, unknown>;
|
|
185
|
+
submit: ResolvedAction | null;
|
|
186
|
+
fallback: {
|
|
187
|
+
label: string;
|
|
188
|
+
href: string;
|
|
189
|
+
external: boolean;
|
|
190
|
+
} | null;
|
|
191
|
+
};
|
|
192
|
+
export interface ResolvedAccountPage {
|
|
193
|
+
id: string;
|
|
194
|
+
extension: string;
|
|
195
|
+
title: string;
|
|
196
|
+
href: string;
|
|
197
|
+
tabs: Array<{
|
|
198
|
+
id: string;
|
|
199
|
+
label: string;
|
|
200
|
+
href: string;
|
|
201
|
+
active: boolean;
|
|
202
|
+
}>;
|
|
203
|
+
blocks: ResolvedBlock[];
|
|
204
|
+
}
|
|
205
|
+
/** JSON for a `<script type="application/json">`: never closes the tag, never runs. */
|
|
206
|
+
export declare function jsonForDataBlock(value: unknown): string;
|
|
207
|
+
export interface ResolveInput {
|
|
208
|
+
page: RegisteredAccountPage;
|
|
209
|
+
view: AccountPageView;
|
|
210
|
+
t: (text: Text | undefined | null) => string;
|
|
211
|
+
locale: string;
|
|
212
|
+
now: Date;
|
|
213
|
+
query: {
|
|
214
|
+
cursor?: string;
|
|
215
|
+
tab?: string;
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/** The button for `ref`, or `null` when the page has no such action. */
|
|
219
|
+
export declare function resolveAction(page: RegisteredAccountPage, ref: ActionRef, t: (text: Text | undefined | null) => string): ResolvedAction | null;
|
|
220
|
+
/** Resolves one block; an unknown or malformed block becomes a warning notice. */
|
|
221
|
+
export declare function resolveBlock(input: ResolveInput, block: Block): ResolvedBlock;
|
|
222
|
+
export declare function resolvePageView(input: ResolveInput): ResolvedAccountPage;
|
|
223
|
+
export interface ConsoleRequest {
|
|
224
|
+
/** The base context (no `query`); each page call gets its own. */
|
|
225
|
+
base: Omit<AccountExtensionContext, 'query'>;
|
|
226
|
+
hostMessages: Record<string, string>;
|
|
227
|
+
locale: string;
|
|
228
|
+
config: AccountConsoleConfig;
|
|
229
|
+
cfg: any;
|
|
230
|
+
/** Memoized `visible()` per page id. */
|
|
231
|
+
visible: Map<string, Promise<boolean>>;
|
|
232
|
+
}
|
|
233
|
+
/** The console context of this request, built once. `null` without a signed-in account. */
|
|
234
|
+
export declare function consoleRequest(ctx: HttpContext): Promise<ConsoleRequest | null>;
|
|
235
|
+
/** Validated query: `cursor` and `tab` only. */
|
|
236
|
+
export declare function validatedQuery(raw: Record<string, unknown>): {
|
|
237
|
+
cursor?: string;
|
|
238
|
+
tab?: string;
|
|
239
|
+
};
|
|
240
|
+
export declare function extensionContext(request: ConsoleRequest, query: {
|
|
241
|
+
cursor?: string;
|
|
242
|
+
tab?: string;
|
|
243
|
+
}): AccountExtensionContext;
|
|
244
|
+
/** The translator for one page (its extension's catalogs). */
|
|
245
|
+
export declare function translatorFor(request: ConsoleRequest, page: RegisteredAccountPage | null): (text: Text | undefined | null) => string;
|
|
246
|
+
/** `page.visible(ctx)`, once per request; a throw hides the page. */
|
|
247
|
+
export declare function isPageVisible(request: ConsoleRequest, page: RegisteredAccountPage): Promise<boolean>;
|
|
248
|
+
export declare function logExtensionError(page: RegisteredAccountPage, where: string, error: unknown): void;
|
|
249
|
+
/** Runs `fn` with a time budget. */
|
|
250
|
+
export declare function withTimeout<T>(work: Promise<T>, ms: number, what: string): Promise<T>;
|
|
251
|
+
/** Loads and resolves a page; a throw or timeout becomes a danger notice. */
|
|
252
|
+
export declare function loadResolvedPage(request: ConsoleRequest, page: RegisteredAccountPage, query: {
|
|
253
|
+
cursor?: string;
|
|
254
|
+
tab?: string;
|
|
255
|
+
}): Promise<ResolvedAccountPage>;
|
|
256
|
+
/** One navigation entry of the account console. */
|
|
257
|
+
export interface AccountNavEntry {
|
|
258
|
+
id: string;
|
|
259
|
+
label: string;
|
|
260
|
+
href: string;
|
|
261
|
+
active: boolean;
|
|
262
|
+
group: 'account' | 'billing' | 'ai' | 'content';
|
|
263
|
+
order: number;
|
|
264
|
+
}
|
|
265
|
+
/** Mounted built-in screens (id → path). */
|
|
266
|
+
export declare function mountedBuiltins(): Partial<Record<BuiltinNavId, string>>;
|
|
267
|
+
/**
|
|
268
|
+
* The account console navigation: mounted built-in screens plus the extension pages visible to
|
|
269
|
+
* this account, sorted by `(group, order, id)`.
|
|
270
|
+
*/
|
|
271
|
+
export declare function buildAccountNav(ctx: HttpContext, activeId: string | null): Promise<AccountNavEntry[]>;
|
|
272
|
+
/**
|
|
273
|
+
* The `accountNav` prop of an account screen (`null` for other views). Never throws: a broken
|
|
274
|
+
* extension leaves the built-in entries.
|
|
275
|
+
*/
|
|
276
|
+
export declare function accountNavFor(ctx: HttpContext, view: string, props: Record<string, unknown>): Promise<AccountNavEntry[] | null>;
|
|
277
|
+
export {};
|