@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.
Files changed (163) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +4 -5
  3. package/build/commands/agora_doctor.d.ts +22 -0
  4. package/build/commands/agora_doctor.js +109 -0
  5. package/build/commands/clients_create.d.ts +4 -0
  6. package/build/commands/clients_create.js +97 -0
  7. package/build/commands/commands.json +78 -0
  8. package/build/commands/doctor.js +8 -74
  9. package/build/host/views/account/apps.edge +1 -5
  10. package/build/host/views/account/extension.edge +77 -0
  11. package/build/host/views/account/mfa.edge +2 -0
  12. package/build/host/views/account/partials/extension_actions.edge +14 -0
  13. package/build/host/views/account/partials/extension_block.edge +197 -0
  14. package/build/host/views/account/partials/extension_value.edge +8 -0
  15. package/build/host/views/account/partials/nav.edge +14 -0
  16. package/build/host/views/account/security.edge +1 -5
  17. package/build/host/views/account/tokens.edge +2 -2
  18. package/build/host/views/agents/consent.edge +11 -2
  19. package/build/host/views/agents/done.edge +1 -1
  20. package/build/host/views/partials/styles.edge +1 -1
  21. package/build/index.d.ts +12 -12
  22. package/build/index.js +7 -9
  23. package/build/providers/authkit_server_provider.js +28 -8
  24. package/build/src/account/extension.d.ts +10 -0
  25. package/build/src/account/extension.js +9 -0
  26. package/build/src/account/http.d.ts +77 -0
  27. package/build/src/account/http.js +381 -0
  28. package/build/src/account/mount.d.ts +40 -0
  29. package/build/src/account/mount.js +171 -0
  30. package/build/src/account/registry.d.ts +41 -0
  31. package/build/src/account/registry.js +81 -0
  32. package/build/src/account/remote.d.ts +72 -0
  33. package/build/src/account/remote.js +406 -0
  34. package/build/src/account/runtime.d.ts +289 -0
  35. package/build/src/account/runtime.js +641 -0
  36. package/build/src/accounts/account_store.d.ts +6 -0
  37. package/build/src/accounts/lucid_store/organizations.js +6 -0
  38. package/build/src/agents/config.d.ts +9 -13
  39. package/build/src/agents/config.js +17 -11
  40. package/build/src/agents/delegation_service.js +3 -3
  41. package/build/src/agents/delegation_store.d.ts +3 -2
  42. package/build/src/agents/delegation_store.js +6 -5
  43. package/build/src/agents/extension.d.ts +261 -0
  44. package/build/src/agents/extension.js +40 -0
  45. package/build/src/agents/middleware.js +1 -1
  46. package/build/src/agents/protocol.d.ts +4 -4
  47. package/build/src/agents/registry_store.d.ts +3 -2
  48. package/build/src/agents/registry_store.js +1 -1
  49. package/build/src/agents/runtime.d.ts +26 -7
  50. package/build/src/agents/runtime.js +110 -75
  51. package/build/src/audit/audit_sink.d.ts +1 -1
  52. package/build/src/audit/audit_sink.js +10 -1
  53. package/build/src/console_switcher.d.ts +132 -0
  54. package/build/src/console_switcher.js +206 -0
  55. package/build/src/define_config.d.ts +34 -12
  56. package/build/src/define_config.js +15 -7
  57. package/build/src/dev_tools/contract.d.ts +72 -0
  58. package/build/src/dev_tools/contract.js +10 -0
  59. package/build/src/dev_tools/gate.d.ts +17 -0
  60. package/build/src/dev_tools/gate.js +69 -0
  61. package/build/src/dev_tools/register.d.ts +11 -0
  62. package/build/src/dev_tools/register.js +25 -0
  63. package/build/src/dev_tools/tools.d.ts +24 -0
  64. package/build/src/dev_tools/tools.js +384 -0
  65. package/build/src/doctor/agora_checks.d.ts +15 -0
  66. package/build/src/doctor/agora_checks.js +150 -0
  67. package/build/src/doctor/agora_doctor.d.ts +115 -0
  68. package/build/src/doctor/agora_doctor.js +692 -0
  69. package/build/src/doctor/doctor_input.d.ts +27 -0
  70. package/build/src/doctor/doctor_input.js +86 -0
  71. package/build/src/doctor/index.d.ts +5 -0
  72. package/build/src/doctor/index.js +4 -0
  73. package/build/src/host/access_token_verifier.js +7 -0
  74. package/build/src/host/account_api/account_api_controller.js +8 -6
  75. package/build/src/host/account_session_sign_in.d.ts +12 -0
  76. package/build/src/host/account_session_sign_in.js +19 -0
  77. package/build/src/host/admin_api/admin_users_service.d.ts +5 -2
  78. package/build/src/host/admin_api/api_clients_controller.d.ts +37 -0
  79. package/build/src/host/admin_api/api_clients_controller.js +31 -1
  80. package/build/src/host/admin_api/dto.d.ts +3 -1
  81. package/build/src/host/admin_api/dto.js +2 -0
  82. package/build/src/host/admin_authorize.d.ts +8 -0
  83. package/build/src/host/admin_authorize.js +25 -0
  84. package/build/src/host/admin_clients_service.d.ts +20 -0
  85. package/build/src/host/admin_clients_service.js +55 -0
  86. package/build/src/host/admin_console/admin_console_entry.d.ts +14 -0
  87. package/build/src/host/admin_console/admin_console_entry.js +80 -0
  88. package/build/src/host/admin_console/admin_shell_controller.js +8 -0
  89. package/build/src/host/admin_console/console_clients_controller.d.ts +41 -0
  90. package/build/src/host/admin_console/console_clients_controller.js +34 -1
  91. package/build/src/host/admin_console/console_personal_agents_controller.d.ts +1 -6
  92. package/build/src/host/admin_console/console_personal_agents_controller.js +50 -30
  93. package/build/src/host/admin_console/console_switcher_controller.d.ts +11 -0
  94. package/build/src/host/admin_console/console_switcher_controller.js +23 -0
  95. package/build/src/host/admin_sessions_service.js +8 -5
  96. package/build/src/host/admin_validators.d.ts +29 -0
  97. package/build/src/host/admin_validators.js +30 -0
  98. package/build/src/host/auth_host_config.d.ts +9 -2
  99. package/build/src/host/controllers/account_apps_controller.js +15 -11
  100. package/build/src/host/csrf.d.ts +3 -3
  101. package/build/src/host/csrf.js +3 -13
  102. package/build/src/host/default_mailer.d.ts +11 -6
  103. package/build/src/host/default_mailer.js +16 -12
  104. package/build/src/host/i18n.d.ts +41 -14
  105. package/build/src/host/i18n.js +55 -13
  106. package/build/src/host/rate_limit.d.ts +8 -0
  107. package/build/src/host/rate_limit.js +9 -0
  108. package/build/src/host/register_auth_host.d.ts +14 -0
  109. package/build/src/host/register_auth_host.js +167 -106
  110. package/build/src/host/renderers/edge_renderer.js +5 -0
  111. package/build/src/host/renderers/inertia_renderer.js +7 -1
  112. package/build/src/host/screen_props.d.ts +39 -8
  113. package/build/src/host/ui-dist/assets/index-Dq3dJVBv.js +67 -0
  114. package/build/src/host/ui-dist/assets/{index-DTSmD4RU.css → index-WNIyCEPA.css} +1 -1
  115. package/build/src/host/ui-dist/index.html +2 -2
  116. package/build/src/provider/build_provider.js +37 -6
  117. package/build/src/provider/oidc_service.d.ts +1 -1
  118. package/build/src/provider/oidc_service.js +1 -1
  119. package/build/src/schema/ensure.d.ts +14 -0
  120. package/build/src/schema/ensure.js +43 -150
  121. package/build/src/service_tenants/client_metadata.d.ts +39 -0
  122. package/build/src/service_tenants/client_metadata.js +80 -0
  123. package/build/src/service_tenants/config.d.ts +44 -0
  124. package/build/src/service_tenants/config.js +44 -0
  125. package/build/src/service_tenants/provider_hooks.d.ts +52 -0
  126. package/build/src/service_tenants/provider_hooks.js +124 -0
  127. package/build/src/service_tenants/tenant_verifier.d.ts +73 -0
  128. package/build/src/service_tenants/tenant_verifier.js +104 -0
  129. package/build/src/tenancy/tenant_labels.d.ts +30 -0
  130. package/build/src/tenancy/tenant_labels.js +64 -0
  131. package/build/types.d.ts +3 -1
  132. package/package.json +9 -6
  133. package/build/src/agents/poppy/client_registry.d.ts +0 -50
  134. package/build/src/agents/poppy/client_registry.js +0 -260
  135. package/build/src/agents/poppy/config.d.ts +0 -396
  136. package/build/src/agents/poppy/config.js +0 -228
  137. package/build/src/agents/poppy/dpop.d.ts +0 -61
  138. package/build/src/agents/poppy/dpop.js +0 -158
  139. package/build/src/agents/poppy/email_code_sign_in.d.ts +0 -27
  140. package/build/src/agents/poppy/email_code_sign_in.js +0 -140
  141. package/build/src/agents/poppy/errors.d.ts +0 -25
  142. package/build/src/agents/poppy/errors.js +0 -25
  143. package/build/src/agents/poppy/replay.d.ts +0 -25
  144. package/build/src/agents/poppy/replay.js +0 -106
  145. package/build/src/agents/poppy/resource.d.ts +0 -100
  146. package/build/src/agents/poppy/resource.js +0 -129
  147. package/build/src/agents/poppy/safe_fetch.d.ts +0 -48
  148. package/build/src/agents/poppy/safe_fetch.js +0 -262
  149. package/build/src/agents/poppy/service.d.ts +0 -293
  150. package/build/src/agents/poppy/service.js +0 -1458
  151. package/build/src/agents/poppy/session_jwt.d.ts +0 -30
  152. package/build/src/agents/poppy/session_jwt.js +0 -133
  153. package/build/src/agents/poppy/store.d.ts +0 -106
  154. package/build/src/agents/poppy/store.js +0 -261
  155. package/build/src/agents/poppy/web_session.d.ts +0 -39
  156. package/build/src/agents/poppy/web_session.js +0 -122
  157. package/build/src/host/controllers/poppy_consent_controller.d.ts +0 -19
  158. package/build/src/host/controllers/poppy_consent_controller.js +0 -199
  159. package/build/src/host/controllers/poppy_controller.d.ts +0 -33
  160. package/build/src/host/controllers/poppy_controller.js +0 -219
  161. package/build/src/host/poppy_session_key.d.ts +0 -9
  162. package/build/src/host/poppy_session_key.js +0 -9
  163. package/build/src/host/ui-dist/assets/index-D0EIeyKK.js +0 -67
@@ -0,0 +1,641 @@
1
+ import { joinAccountPath } from '../host/account_paths.js';
2
+ import { resolveAccountRoles } from '../host/account_roles.js';
3
+ import { ACCOUNT_SESSION_KEY } from '../host/account_session_key.js';
4
+ import { readActiveOrgFromHostCtx, verifyActiveOrgMembership } from '../host/active_org_cookie.js';
5
+ import { impersonationState } from '../host/impersonation_session.js';
6
+ // ── process state (set when the console routes are mounted) ──────────────────────────────────────
7
+ /** Built-in screens in the navigation, with their fixed order. */
8
+ export const BUILTIN_NAV = [
9
+ { id: 'security', order: 10, label: 'account.security.title' },
10
+ { id: 'mfa', order: 20, label: 'account.mfa.title' },
11
+ { id: 'tokens', order: 30, label: 'account.tokens.title' },
12
+ { id: 'apps', order: 40, label: 'account.apps.title' },
13
+ { id: 'orgs', order: 50, label: 'account.orgs.title' },
14
+ ];
15
+ let state = {
16
+ pages: [],
17
+ builtins: {},
18
+ registry: null,
19
+ remote: null,
20
+ builtinSegments: [],
21
+ };
22
+ /** Called by `registerAuthHost` with the mounted screens and the frozen pages. */
23
+ export function setAccountConsoleState(next) {
24
+ state = { ...state, ...next };
25
+ }
26
+ /** The served extension pages. */
27
+ export function accountExtensionPages() {
28
+ if (!state.remote)
29
+ return state.pages;
30
+ const remote = state.remote.pages({
31
+ taken: new Set([...state.builtinSegments, ...state.pages.map((p) => p.segment)]),
32
+ takenIds: new Set(state.pages.map((p) => p.extensionId)),
33
+ ...(state.overrides ? { overrides: state.overrides } : {}),
34
+ });
35
+ return remote.length === 0 ? state.pages : [...state.pages, ...remote];
36
+ }
37
+ /** The remote pages currently served (for the catch-all routes). */
38
+ export function remoteAccountPageBySegment(segment) {
39
+ return accountExtensionPages().find((p) => p.remote !== undefined && p.segment === segment);
40
+ }
41
+ /** Resets the process state (tests). */
42
+ export function resetAccountConsoleState() {
43
+ state = { pages: [], builtins: {}, registry: null, remote: null, builtinSegments: [] };
44
+ }
45
+ export function findPageById(pageId) {
46
+ return accountExtensionPages().find((p) => p.page.id === pageId);
47
+ }
48
+ export function findPageBySegment(segment) {
49
+ return accountExtensionPages().find((p) => p.segment === segment);
50
+ }
51
+ /** Same-origin path of an extension page. */
52
+ export function pageHref(page, query) {
53
+ const base = joinAccountPath(page.segment);
54
+ const qs = query ? new URLSearchParams(Object.entries(query).filter(([, v]) => v !== '')) : null;
55
+ const s = qs?.toString();
56
+ return s ? `${base}?${s}` : base;
57
+ }
58
+ // ── text ──────────────────────────────────────────────────────────────────────────────────────────
59
+ /** Looks up a message: host catalog (overrides win), then the extension's own catalogs. */
60
+ export function makeTranslator(hostMessages, locale, catalogs) {
61
+ const lookup = (key) => {
62
+ if (typeof hostMessages[key] === 'string')
63
+ return hostMessages[key];
64
+ for (const catalog of catalogs) {
65
+ const hit = catalog?.[locale]?.[key] ?? catalog?.[locale.split('-')[0] ?? '']?.[key];
66
+ if (typeof hit === 'string')
67
+ return hit;
68
+ }
69
+ for (const catalog of catalogs) {
70
+ const hit = catalog?.en?.[key];
71
+ if (typeof hit === 'string')
72
+ return hit;
73
+ }
74
+ return key;
75
+ };
76
+ return (text) => {
77
+ if (text === undefined || text === null)
78
+ return '';
79
+ if (typeof text === 'string')
80
+ return text;
81
+ if (typeof text !== 'object' || typeof text.key !== 'string')
82
+ return '';
83
+ const template = lookup(text.key);
84
+ const params = text.params;
85
+ if (!params)
86
+ return template;
87
+ return template.replace(/\{(\w+)\}/g, (match, name) => params[name] === undefined ? match : String(params[name]));
88
+ };
89
+ }
90
+ function fractionDigits(currency) {
91
+ try {
92
+ return (new Intl.NumberFormat('en', { style: 'currency', currency }).resolvedOptions()
93
+ .maximumFractionDigits ?? 2);
94
+ }
95
+ catch {
96
+ return 2;
97
+ }
98
+ }
99
+ function toDate(at) {
100
+ const d = at instanceof Date ? at : new Date(at);
101
+ return Number.isNaN(d.getTime()) ? null : d;
102
+ }
103
+ function relative(date, now, locale) {
104
+ const seconds = Math.round((date.getTime() - now.getTime()) / 1000);
105
+ const units = [
106
+ ['year', 31_536_000],
107
+ ['month', 2_592_000],
108
+ ['day', 86_400],
109
+ ['hour', 3600],
110
+ ['minute', 60],
111
+ ];
112
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
113
+ for (const [unit, size] of units) {
114
+ if (Math.abs(seconds) >= size)
115
+ return rtf.format(Math.round(seconds / size), unit);
116
+ }
117
+ return rtf.format(seconds, 'second');
118
+ }
119
+ export function formatValue(value, t, locale, now) {
120
+ try {
121
+ switch (value?.type) {
122
+ case 'text':
123
+ return { type: 'text', text: t(value.text) };
124
+ case 'badge':
125
+ return { type: 'badge', text: t(value.text), tone: value.tone ?? 'neutral' };
126
+ case 'money': {
127
+ const digits = fractionDigits(value.currency);
128
+ const major = value.amount / 10 ** digits;
129
+ return {
130
+ type: 'money',
131
+ text: new Intl.NumberFormat(locale, {
132
+ style: 'currency',
133
+ currency: value.currency,
134
+ }).format(major),
135
+ amount: value.amount,
136
+ currency: value.currency,
137
+ };
138
+ }
139
+ case 'number': {
140
+ const n = new Intl.NumberFormat(locale).format(value.value);
141
+ const unit = value.unit ? t(value.unit) : '';
142
+ return { type: 'number', text: unit ? `${n} ${unit}` : n, value: value.value };
143
+ }
144
+ case 'date': {
145
+ const date = toDate(value.at);
146
+ if (!date)
147
+ return { type: 'date', text: '' };
148
+ const style = value.style ?? 'date';
149
+ const text = style === 'relative'
150
+ ? relative(date, now, locale)
151
+ : new Intl.DateTimeFormat(locale, {
152
+ dateStyle: 'medium',
153
+ ...(style === 'datetime' ? { timeStyle: 'short' } : {}),
154
+ }).format(date);
155
+ return { type: 'date', text, iso: date.toISOString() };
156
+ }
157
+ default:
158
+ return { type: 'text', text: '' };
159
+ }
160
+ }
161
+ catch {
162
+ return { type: 'text', text: '' };
163
+ }
164
+ }
165
+ // ── hrefs ─────────────────────────────────────────────────────────────────────────────────────────
166
+ /**
167
+ * A safe href: an https URL when `external`, else a same-origin path (`/…`, never `//…` nor
168
+ * `/\…`). Anything else (`javascript:`, `data:`, protocol-relative) is `null`.
169
+ */
170
+ export function safeHref(href, external) {
171
+ if (typeof href !== 'string' || href === '')
172
+ return null;
173
+ if (external) {
174
+ try {
175
+ const url = new URL(href);
176
+ return url.protocol === 'https:' ? url.toString() : null;
177
+ }
178
+ catch {
179
+ return null;
180
+ }
181
+ }
182
+ if (!href.startsWith('/') || href.startsWith('//') || href.startsWith('/\\'))
183
+ return null;
184
+ // Whitespace or control characters (code point < 0x20) never belong in a path.
185
+ if ([...href].some((ch) => ch.charCodeAt(0) < 0x20 || /\s/.test(ch)))
186
+ return null;
187
+ return href;
188
+ }
189
+ /** A script `src` a component block may load: same-origin, or https on an allowed origin. */
190
+ export function safeScriptSrc(src, origins) {
191
+ const local = safeHref(src, false);
192
+ if (local)
193
+ return local;
194
+ if (typeof src !== 'string')
195
+ return null;
196
+ try {
197
+ const url = new URL(src);
198
+ return url.protocol === 'https:' && origins.includes(url.origin) ? url.toString() : null;
199
+ }
200
+ catch {
201
+ return null;
202
+ }
203
+ }
204
+ /** JSON for a `<script type="application/json">`: never closes the tag, never runs. */
205
+ export function jsonForDataBlock(value) {
206
+ return JSON.stringify(value ?? {})
207
+ .replace(/</g, '\\u003c')
208
+ .replace(/>/g, '\\u003e')
209
+ .replace(/&/g, '\\u0026')
210
+ .replace(/\u2028/g, '\\u2028')
211
+ .replace(/\u2029/g, '\\u2029');
212
+ }
213
+ function resolveActions(input, refs) {
214
+ const out = [];
215
+ for (const ref of refs ?? []) {
216
+ const r = resolveAction(input.page, ref, input.t);
217
+ if (r)
218
+ out.push(r);
219
+ }
220
+ return out;
221
+ }
222
+ /** The button for `ref`, or `null` when the page has no such action. */
223
+ export function resolveAction(page, ref, t) {
224
+ const action = page.page.actions?.[ref?.action];
225
+ if (!action || !Object.hasOwn(page.page.actions ?? {}, ref.action))
226
+ return null;
227
+ const target = typeof ref.target === 'string' ? ref.target : null;
228
+ const step = action.confirm !== undefined || (action.fields?.length ?? 0) > 0;
229
+ const base = `${joinAccountPath(page.segment)}/actions/${encodeURIComponent(ref.action)}`;
230
+ const qs = target !== null ? `?target=${encodeURIComponent(target)}` : '';
231
+ return {
232
+ action: ref.action,
233
+ target,
234
+ label: t(action.label),
235
+ tone: action.tone === 'danger' ? 'danger' : 'default',
236
+ sudo: action.sudo === true,
237
+ step,
238
+ href: step ? `${base}/confirm${qs}` : base,
239
+ postHref: base,
240
+ apiHref: `${joinAccountPath('api')}/x/${encodeURIComponent(page.page.id)}/actions/${encodeURIComponent(ref.action)}`,
241
+ };
242
+ }
243
+ function nextLink(input, cursor) {
244
+ if (typeof cursor !== 'string' || cursor === '')
245
+ return null;
246
+ return pageHref(input.page, {
247
+ cursor,
248
+ ...(input.query.tab ? { tab: input.query.tab } : {}),
249
+ });
250
+ }
251
+ let mountCounter = 0;
252
+ /** Resolves one block; an unknown or malformed block becomes a warning notice. */
253
+ export function resolveBlock(input, block) {
254
+ const { t, locale, now } = input;
255
+ const value = (v) => formatValue(v, t, locale, now);
256
+ const fallback = (why) => ({ kind: 'notice', tone: 'warning', text: why });
257
+ try {
258
+ switch (block?.kind) {
259
+ case 'notice':
260
+ return {
261
+ kind: 'notice',
262
+ tone: ['info', 'success', 'warning', 'danger'].includes(block.tone) ? block.tone : 'info',
263
+ text: t(block.text),
264
+ };
265
+ case 'keyValue':
266
+ return {
267
+ kind: 'keyValue',
268
+ title: block.title ? t(block.title) : null,
269
+ rows: (block.rows ?? []).map((row) => ({ label: t(row.label), value: value(row.value) })),
270
+ actions: resolveActions(input, block.actions),
271
+ };
272
+ case 'list':
273
+ return {
274
+ kind: 'list',
275
+ title: block.title ? t(block.title) : null,
276
+ empty: t(block.empty),
277
+ items: (block.items ?? []).map((item) => ({
278
+ title: t(item.title),
279
+ subtitle: item.subtitle ? t(item.subtitle) : null,
280
+ meta: (item.meta ?? []).map(value),
281
+ badge: item.badge ? value(item.badge) : null,
282
+ href: safeHref(item.href, item.external),
283
+ external: item.external === true,
284
+ actions: resolveActions(input, item.actions),
285
+ })),
286
+ nextHref: nextLink(input, block.nextCursor),
287
+ nextCursor: block.nextCursor ?? null,
288
+ };
289
+ case 'table': {
290
+ const columns = (block.columns ?? []).map((c) => ({ id: c.id, label: t(c.label) }));
291
+ const rows = (block.rows ?? []).map((row) => ({
292
+ cells: columns.map((c) => row.cells?.[c.id]
293
+ ? value(row.cells[c.id])
294
+ : { type: 'text', text: '' }),
295
+ actions: resolveActions(input, row.actions),
296
+ }));
297
+ return {
298
+ kind: 'table',
299
+ title: block.title ? t(block.title) : null,
300
+ empty: t(block.empty),
301
+ columns,
302
+ rows,
303
+ hasActions: rows.some((r) => r.actions.length > 0),
304
+ nextHref: nextLink(input, block.nextCursor),
305
+ nextCursor: block.nextCursor ?? null,
306
+ };
307
+ }
308
+ case 'meter': {
309
+ const used = Number(block.used) || 0;
310
+ const limit = typeof block.limit === 'number' ? block.limit : null;
311
+ const unit = block.unit ? ` ${t(block.unit)}` : '';
312
+ const fmt = new Intl.NumberFormat(locale);
313
+ return {
314
+ kind: 'meter',
315
+ label: t(block.label),
316
+ used,
317
+ limit,
318
+ percent: limit !== null && limit > 0 ? Math.min(100, Math.round((used / limit) * 100)) : null,
319
+ text: limit !== null
320
+ ? `${fmt.format(used)} / ${fmt.format(limit)}${unit}`
321
+ : `${fmt.format(used)}${unit}`,
322
+ resetsAt: block.resetsAt
323
+ ? value({ type: 'date', at: block.resetsAt, style: 'relative' })
324
+ : null,
325
+ };
326
+ }
327
+ case 'actions':
328
+ return { kind: 'actions', items: resolveActions(input, block.items) };
329
+ case 'link': {
330
+ const href = safeHref(block.href, block.external);
331
+ if (!href)
332
+ return fallback(t({ key: 'account.extension.unsafe_link' }));
333
+ return { kind: 'link', label: t(block.label), href, external: block.external === true };
334
+ }
335
+ case 'toolPreview':
336
+ return {
337
+ kind: 'toolPreview',
338
+ title: t(block.title),
339
+ tool: typeof block.tool === 'string' ? block.tool : null,
340
+ subtitle: block.subtitle ? t(block.subtitle) : null,
341
+ args: (block.args ?? []).map((a) => ({ label: t(a.label), value: value(a.value) })),
342
+ body: block.body && typeof block.body.content === 'string'
343
+ ? {
344
+ format: block.body.format === 'json' ? 'json' : 'text',
345
+ content: block.body.content,
346
+ }
347
+ : null,
348
+ actions: resolveActions(input, block.actions),
349
+ };
350
+ case 'component': {
351
+ const provider = String(block.provider ?? '');
352
+ if (!/^[a-z0-9][a-z0-9-]{0,31}$/.test(provider)) {
353
+ return fallback(t({ key: 'account.extension.unknown_block' }));
354
+ }
355
+ const scripts = [];
356
+ for (const script of (block.scripts ?? [])) {
357
+ const src = safeScriptSrc(script?.src, input.page.scriptOrigins);
358
+ if (!src)
359
+ return fallback(t({ key: 'account.extension.unsafe_script' }));
360
+ scripts.push({
361
+ src,
362
+ integrity: typeof script.integrity === 'string' ? script.integrity : null,
363
+ module: script.module === true,
364
+ });
365
+ }
366
+ const fb = block.fallback ? safeHref(block.fallback.href, block.fallback.external) : null;
367
+ mountCounter = (mountCounter + 1) % 1_000_000;
368
+ const props = (block.props ?? {});
369
+ return {
370
+ kind: 'component',
371
+ provider,
372
+ mountId: `authkit-component-${input.page.page.id.replace(/\./g, '-')}-${mountCounter}`,
373
+ scripts,
374
+ propsJson: jsonForDataBlock(props),
375
+ props,
376
+ submit: block.submit ? resolveAction(input.page, block.submit, t) : null,
377
+ fallback: fb && block.fallback
378
+ ? {
379
+ label: t(block.fallback.label),
380
+ href: fb,
381
+ external: block.fallback.external === true,
382
+ }
383
+ : null,
384
+ };
385
+ }
386
+ default:
387
+ return fallback(t({ key: 'account.extension.unknown_block' }));
388
+ }
389
+ }
390
+ catch {
391
+ return fallback(t({ key: 'account.extension.unknown_block' }));
392
+ }
393
+ }
394
+ export function resolvePageView(input) {
395
+ const tabs = (input.view.tabs ?? []).map((tab, index) => ({
396
+ id: tab.id,
397
+ label: input.t(tab.label),
398
+ href: pageHref(input.page, { tab: tab.id }),
399
+ active: input.query.tab ? input.query.tab === tab.id : index === 0,
400
+ }));
401
+ return {
402
+ id: input.page.page.id,
403
+ extension: input.page.extensionId,
404
+ title: input.t(input.page.page.title),
405
+ href: pageHref(input.page),
406
+ tabs,
407
+ blocks: (Array.isArray(input.view.blocks) ? input.view.blocks : []).map((b) => resolveBlock(input, b)),
408
+ };
409
+ }
410
+ const perRequest = new WeakMap();
411
+ function tenantFrom(activeOrg) {
412
+ if (!activeOrg)
413
+ return null;
414
+ return {
415
+ id: activeOrg.id,
416
+ kind: 'organization',
417
+ source: 'authkit',
418
+ slug: activeOrg.slug,
419
+ role: activeOrg.role,
420
+ };
421
+ }
422
+ async function readActiveOrg(ctx, cfg, accountId) {
423
+ try {
424
+ const claimed = readActiveOrgFromHostCtx(ctx, { appKey: cfg.appKey });
425
+ const verified = await verifyActiveOrgMembership(cfg.accountStore, accountId, claimed);
426
+ return verified ? { id: verified.orgId, slug: verified.orgSlug, role: verified.orgRole } : null;
427
+ }
428
+ catch {
429
+ return null;
430
+ }
431
+ }
432
+ /** The console context of this request, built once. `null` without a signed-in account. */
433
+ export function consoleRequest(ctx) {
434
+ const key = ctx;
435
+ const existing = perRequest.get(key);
436
+ if (existing)
437
+ return existing;
438
+ const built = (async () => {
439
+ const accountId = ctx.session?.get(ACCOUNT_SESSION_KEY);
440
+ if (!accountId)
441
+ return null;
442
+ const service = await ctx.containerResolver.make('authkit.server');
443
+ const cfg = service.config;
444
+ const account = (await cfg.accountStore.findById(accountId));
445
+ if (!account)
446
+ return null;
447
+ const roles = await resolveAccountRoles(cfg, account);
448
+ const activeOrg = await readActiveOrg(ctx, cfg, accountId);
449
+ const tenant = tenantFrom(activeOrg);
450
+ const config = cfg.accountConsole ?? {};
451
+ const summary = {
452
+ id: String(account.id),
453
+ email: account.email ?? null,
454
+ name: account.name ?? null,
455
+ roles,
456
+ };
457
+ const subject = config.subject
458
+ ? await config.subject(summary, { activeOrg, tenant })
459
+ : { type: 'User', id: summary.id };
460
+ const impersonation = impersonationState(ctx);
461
+ const locale = typeof cfg.locale === 'string' ? cfg.locale : 'en';
462
+ const base = {
463
+ account: summary,
464
+ activeOrg,
465
+ tenant,
466
+ subject,
467
+ impersonatedBy: impersonation.active ? (impersonation.impersonatorId ?? 'unknown') : null,
468
+ locale,
469
+ url(pageId, query) {
470
+ const page = findPageById(pageId);
471
+ return page ? pageHref(page, query) : joinAccountPath('');
472
+ },
473
+ make: (binding) => ctx.containerResolver.make(binding),
474
+ now: () => new Date(),
475
+ };
476
+ return {
477
+ base,
478
+ hostMessages: (cfg.messages ?? {}),
479
+ locale,
480
+ config,
481
+ cfg,
482
+ visible: new Map(),
483
+ };
484
+ })();
485
+ perRequest.set(key, built);
486
+ return built;
487
+ }
488
+ /** Validated query: `cursor` and `tab` only. */
489
+ export function validatedQuery(raw) {
490
+ const out = {};
491
+ const cursor = raw.cursor;
492
+ if (typeof cursor === 'string' && cursor.length > 0 && cursor.length <= 512)
493
+ out.cursor = cursor;
494
+ const tab = raw.tab;
495
+ if (typeof tab === 'string' && /^[a-zA-Z0-9_-]{1,64}$/.test(tab))
496
+ out.tab = tab;
497
+ return out;
498
+ }
499
+ export function extensionContext(request, query) {
500
+ return { ...request.base, query };
501
+ }
502
+ /** The translator for one page (its extension's catalogs). */
503
+ export function translatorFor(request, page) {
504
+ const all = [...(state.registry?.extensions() ?? []), ...(state.remote?.extensions() ?? [])];
505
+ const catalogs = page
506
+ ? [all.find((e) => e.id === page.extensionId)?.messages]
507
+ : all.map((e) => e.messages);
508
+ return makeTranslator(request.hostMessages, request.locale, catalogs);
509
+ }
510
+ /** `page.visible(ctx)`, once per request; a throw hides the page. */
511
+ export function isPageVisible(request, page) {
512
+ const cached = request.visible.get(page.page.id);
513
+ if (cached)
514
+ return cached;
515
+ const decided = (async () => {
516
+ if (typeof page.page.visible !== 'function')
517
+ return true;
518
+ try {
519
+ return (await page.page.visible(extensionContext(request, {}))) !== false;
520
+ }
521
+ catch (error) {
522
+ logExtensionError(page, 'visible', error);
523
+ return false;
524
+ }
525
+ })();
526
+ request.visible.set(page.page.id, decided);
527
+ return decided;
528
+ }
529
+ export function logExtensionError(page, where, error) {
530
+ const message = error instanceof Error ? error.message : String(error);
531
+ console.error(`authkit: account extension "${page.extensionId}" page "${page.page.id}" ${where}() failed: ${message}`);
532
+ }
533
+ /** Runs `fn` with a time budget. */
534
+ export async function withTimeout(work, ms, what) {
535
+ let timer;
536
+ try {
537
+ return await Promise.race([
538
+ work,
539
+ new Promise((_, reject) => {
540
+ timer = setTimeout(() => reject(new Error(`${what} timed out after ${ms} ms`)), ms);
541
+ }),
542
+ ]);
543
+ }
544
+ finally {
545
+ if (timer !== undefined)
546
+ clearTimeout(timer);
547
+ }
548
+ }
549
+ /** Loads and resolves a page; a throw or timeout becomes a danger notice. */
550
+ export async function loadResolvedPage(request, page, query) {
551
+ const t = translatorFor(request, page);
552
+ let view;
553
+ try {
554
+ const loaded = await withTimeout(page.page.load(extensionContext(request, query)), request.config.loadTimeoutMs ?? 5000, 'load()');
555
+ if (!loaded || typeof loaded !== 'object' || !Array.isArray(loaded.blocks)) {
556
+ throw new Error('load() must return { blocks: [...] }');
557
+ }
558
+ view = loaded;
559
+ }
560
+ catch (error) {
561
+ logExtensionError(page, 'load', error);
562
+ view = {
563
+ blocks: [{ kind: 'notice', tone: 'danger', text: { key: 'account.extension.load_failed' } }],
564
+ };
565
+ }
566
+ return resolvePageView({ page, view, t, locale: request.locale, now: request.base.now(), query });
567
+ }
568
+ const GROUP_ORDER = ['account', 'billing', 'ai', 'content'];
569
+ /** Mounted built-in screens (id → path). */
570
+ export function mountedBuiltins() {
571
+ return state.builtins;
572
+ }
573
+ /**
574
+ * The account console navigation: mounted built-in screens plus the extension pages visible to
575
+ * this account, sorted by `(group, order, id)`.
576
+ */
577
+ export async function buildAccountNav(ctx, activeId) {
578
+ const request = await consoleRequest(ctx).catch(() => null);
579
+ const t = request
580
+ ? translatorFor(request, null)
581
+ : (text) => (typeof text === 'string' ? text : (text?.key ?? ''));
582
+ const host = (key) => (request ? (request.hostMessages[key] ?? key) : key);
583
+ const entries = [];
584
+ for (const builtin of BUILTIN_NAV) {
585
+ const href = state.builtins[builtin.id];
586
+ if (!href)
587
+ continue;
588
+ entries.push({
589
+ id: builtin.id,
590
+ label: host(builtin.label),
591
+ href,
592
+ active: activeId === builtin.id,
593
+ group: 'account',
594
+ order: builtin.order,
595
+ });
596
+ }
597
+ if (request) {
598
+ const pages = accountExtensionPages();
599
+ const visible = await Promise.all(pages.map((p) => isPageVisible(request, p)));
600
+ pages.forEach((page, index) => {
601
+ if (!visible[index] || page.page.nav === false)
602
+ return;
603
+ const nav = page.page.nav ?? {};
604
+ entries.push({
605
+ id: page.page.id,
606
+ label: t(nav.label ?? page.page.title),
607
+ href: pageHref(page),
608
+ active: activeId === page.page.id,
609
+ group: nav.group ?? 'account',
610
+ order: page.order,
611
+ });
612
+ });
613
+ }
614
+ return entries.sort((a, b) => GROUP_ORDER.indexOf(a.group) - GROUP_ORDER.indexOf(b.group) ||
615
+ a.order - b.order ||
616
+ (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
617
+ }
618
+ const NAV_VIEWS = {
619
+ 'account/security': 'security',
620
+ 'account/mfa': 'mfa',
621
+ 'account/tokens': 'tokens',
622
+ 'account/apps': 'apps',
623
+ 'account/orgs': 'orgs',
624
+ 'account/extension': 'extension',
625
+ };
626
+ /**
627
+ * The `accountNav` prop of an account screen (`null` for other views). Never throws: a broken
628
+ * extension leaves the built-in entries.
629
+ */
630
+ export async function accountNavFor(ctx, view, props) {
631
+ const active = NAV_VIEWS[view];
632
+ if (!active)
633
+ return null;
634
+ const activeId = active === 'extension' ? (props.page?.id ?? null) : active;
635
+ try {
636
+ return await buildAccountNav(ctx, activeId);
637
+ }
638
+ catch {
639
+ return [];
640
+ }
641
+ }
@@ -605,6 +605,12 @@ export interface OrganizationsCapability {
605
605
  }): Promise<OrgSummary>;
606
606
  findOrgById(orgId: string): Promise<OrgSummary | null>;
607
607
  findOrgBySlug(slug: string): Promise<OrgSummary | null>;
608
+ /**
609
+ * OPTIONAL: the orgs among `orgIds` that exist, in a bounded number of queries
610
+ * (any order; unknown ids are left out). Absent → callers fall back to
611
+ * `findOrgById` per id.
612
+ */
613
+ findOrgsByIds?(orgIds: readonly string[]): Promise<OrgSummary[]>;
608
614
  listOrgsForAccount(accountId: string): Promise<Array<OrgSummary & {
609
615
  role: string;
610
616
  }>>;
@@ -137,6 +137,12 @@ export function buildOrganizations(ctx) {
137
137
  const row = await OrgModel.find(orgId);
138
138
  return row ? toOrgSummary(row) : null;
139
139
  },
140
+ async findOrgsByIds(orgIds) {
141
+ const ids = [...new Set(orgIds)];
142
+ if (ids.length === 0)
143
+ return [];
144
+ return (await findManyChunked(OrgModel, ids)).map(toOrgSummary);
145
+ },
140
146
  async findOrgBySlug(slug) {
141
147
  const row = await OrgModel.query().where('slug', slug).first();
142
148
  return row ? toOrgSummary(row) : null;