@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
@@ -1,13 +1,9 @@
1
+ import { getBootedApp } from '../../services/booted_app.js';
1
2
  import { supportsAccountStatus } from '../accounts/account_store.js';
2
3
  import { findMcpResource } from '../mcp/mcp_oauth.js';
3
4
  import { PersonalAgentVerifier } from './agent_identity.js';
4
5
  import { PersonalAgentDelegation } from './delegation_service.js';
5
6
  import { DelegationStore } from './delegation_store.js';
6
- import { PoppyClientRegistry } from './poppy/client_registry.js';
7
- import { hmacNonceSource } from './poppy/dpop.js';
8
- import { databaseReplayStore, memoryLimiter, memoryReplayStore, redisReplayStore, } from './poppy/replay.js';
9
- import { PoppyService, poppyUrls } from './poppy/service.js';
10
- import { PoppyStore } from './poppy/store.js';
11
7
  import { AgentRegistryStore } from './registry_store.js';
12
8
  import { keystoreSigner } from './signer.js';
13
9
  /**
@@ -27,9 +23,10 @@ export function personalAgentUrls(oidcIssuer, prefix) {
27
23
  };
28
24
  }
29
25
  const runtimes = new WeakMap();
30
- /** Ponte Poppy → oidc-provider: Bearer de MCP de Sessions logadas viram access tokens dele. */
31
- function mcpBridge(service, poppyApis) {
26
+ /** Access tokens do oidc-provider para resources MCP (ver {@link PersonalAgentAccessTokens}). */
27
+ function accessTokens(service) {
32
28
  return {
29
+ mcpEnabled: !!service.config.mcp?.enabled,
33
30
  isMcpResource(url) {
34
31
  try {
35
32
  return findMcpResource(url, service.config.issuer, service.config.mcp) !== null;
@@ -42,11 +39,6 @@ function mcpBridge(service, poppyApis) {
42
39
  const provider = service.provider;
43
40
  if (!provider?.AccessToken || !provider?.Grant)
44
41
  return null;
45
- // Só faz sentido quando a integração MCP do authkit está ligada, ou o MCP é de `apis`.
46
- if (!service.config.mcp?.enabled &&
47
- !poppyApis.some((a) => a.type === 'mcp' && a.url.replace(/\/+$/, '') === input.resource.replace(/\/+$/, ''))) {
48
- return null;
49
- }
50
42
  const remaining = Math.max(60, Math.floor((input.grantExpiresAt.getTime() - Date.now()) / 1000));
51
43
  let grant = await provider.Grant.find(input.grantId);
52
44
  if (!grant) {
@@ -63,13 +55,13 @@ function mcpBridge(service, poppyApis) {
63
55
  clientId: input.clientId,
64
56
  grantId: input.grantId,
65
57
  scope: input.scope,
66
- gty: 'poppy',
58
+ gty: input.grantType,
67
59
  expiresIn: input.expiresIn,
68
60
  });
69
61
  at.aud = input.resource;
70
62
  return at.save();
71
63
  },
72
- async revoke(grantId) {
64
+ async revokeGrant(grantId) {
73
65
  const provider = service.provider;
74
66
  if (!provider?.Grant)
75
67
  return;
@@ -80,18 +72,32 @@ function mcpBridge(service, poppyApis) {
80
72
  },
81
73
  };
82
74
  }
83
- async function resolveReplay(replay, conn, make) {
84
- if (replay === 'memory')
85
- return memoryReplayStore();
86
- if (replay === 'database')
87
- return databaseReplayStore(conn);
88
- if (typeof replay.claim === 'function')
89
- return replay;
90
- const name = replay.redis;
91
- if (!make)
92
- throw new Error('authkit: personalAgents.poppy.replay { redis } exige o container.');
93
- const redis = await make('redis');
94
- return redisReplayStore(redis.connection(name));
75
+ /** O ledger de grants visto por uma extension: só os ids com o prefixo dela. */
76
+ function scopedLedger(store, prefix) {
77
+ const mine = (id) => typeof id === 'string' && id.startsWith(prefix);
78
+ return {
79
+ async insertGrant(grant) {
80
+ if (!mine(grant.id)) {
81
+ throw new Error(`authkit: grant "${grant.id}" fora do prefixo "${prefix}" da extension.`);
82
+ }
83
+ await store.insertGrant(grant);
84
+ },
85
+ async findGrant(id) {
86
+ return mine(id) ? store.findGrant(id) : null;
87
+ },
88
+ async listGrants(accountId) {
89
+ return (await store.listGrants(accountId)).filter((g) => mine(g.id));
90
+ },
91
+ async unrevokedGrantIdsOfClient(clientId) {
92
+ return (await store.unrevokedGrantIdsOfClient(clientId)).filter(mine);
93
+ },
94
+ async revokeGrantById(id, at) {
95
+ if (mine(id))
96
+ await store.revokeGrantById(id, at);
97
+ },
98
+ insertRefreshToken: (token) => store.insertRefreshToken(token),
99
+ findRefreshToken: (tokenHash) => store.findRefreshToken(tokenHash),
100
+ };
95
101
  }
96
102
  /**
97
103
  * Config com o registro do console por cima: um agente BLOQUEADO no console é
@@ -112,21 +118,7 @@ function withRegistry(config, registry) {
112
118
  }
113
119
  return null;
114
120
  };
115
- const poppy = config.poppy;
116
- return {
117
- ...config,
118
- resolveAgent,
119
- poppy: poppy && {
120
- ...poppy,
121
- clients: {
122
- ...poppy.clients,
123
- isBlocked: async (id) => (await poppy.clients.isBlocked(id)) ||
124
- (await registry.find('poppy', id))?.status === 'blocked',
125
- isRegistered: async (id) => (await poppy.clients.isRegistered(id)) ||
126
- (await registry.find('poppy', id))?.status === 'allowed',
127
- },
128
- },
129
- };
121
+ return { ...config, resolveAgent };
130
122
  }
131
123
  /**
132
124
  * Monta (uma vez por `OidcService`) o runtime de personal agents. `null` quando
@@ -149,7 +141,7 @@ export function buildPersonalAgentsRuntime(service, makeDb, options = {}) {
149
141
  };
150
142
  let conn = null;
151
143
  const connection = service.config.schema.connection;
152
- if (config.delegation || config.poppy) {
144
+ if (config.delegation || (config.extensions ?? []).length > 0) {
153
145
  const db = await makeDb();
154
146
  conn = () => (connection ? db.connection(connection) : db.connection());
155
147
  }
@@ -177,39 +169,53 @@ export function buildPersonalAgentsRuntime(service, makeDb, options = {}) {
177
169
  isAccountActive,
178
170
  });
179
171
  }
180
- let poppy = null;
181
- if (effective.poppy && store && conn) {
182
- const cfg = effective.poppy;
183
- const limiter = cfg.rateLimit === false
184
- ? null
185
- : typeof cfg.rateLimit === 'function'
186
- ? cfg.rateLimit
187
- : memoryLimiter(cfg.rateLimit);
188
- const secret = service.deriveSecret('poppy-dpop-nonce');
189
- poppy = new PoppyService({
190
- cfg,
191
- urls: poppyUrls(service.config.issuer, cfg.prefix),
192
- store: new PoppyStore(conn),
193
- grants: store,
194
- registry: new PoppyClientRegistry(cfg, { fetchJson: options.fetchJson }),
195
- replay: await resolveReplay(cfg.replay, conn, options.make),
196
- limiter,
197
- nonces: hmacNonceSource(secret),
198
- isAccountActive,
199
- mcp: mcpBridge(service, cfg.apis),
200
- now: options.now,
201
- });
172
+ const extensions = new Map();
173
+ const now = () => options.now?.() ?? new Date();
174
+ if (store && conn && registry) {
175
+ const db = conn;
176
+ const tokens = accessTokens(service);
177
+ for (const extension of config.extensions ?? []) {
178
+ const runtime = await extension.setup({
179
+ protocol: extension.id,
180
+ issuer: service.config.issuer,
181
+ db: () => db(),
182
+ grants: scopedLedger(store, extension.grantIdPrefix),
183
+ registryStatus: async (identifier) => (await registry.find(extension.id, identifier))?.status ?? null,
184
+ deriveSecret: (label) => service.deriveSecret(label),
185
+ isAccountActive,
186
+ make: async (binding) => {
187
+ if (!options.make) {
188
+ throw new Error(`authkit: o binding "${binding}" exige o container do app.`);
189
+ }
190
+ return options.make(binding);
191
+ },
192
+ accessTokens: tokens,
193
+ now,
194
+ });
195
+ extensions.set(extension.id, { extension, runtime });
196
+ }
202
197
  }
198
+ /** A extension dona de um grant, pelo prefixo do id. */
199
+ const ownerOf = (grantId) => {
200
+ for (const entry of extensions.values()) {
201
+ if (grantId.startsWith(entry.extension.grantIdPrefix))
202
+ return entry;
203
+ }
204
+ return null;
205
+ };
206
+ const isExtensionGrant = (grantId) => (config.extensions ?? []).some((e) => grantId.startsWith(e.grantIdPrefix));
203
207
  const revokeAgentGrant = async (accountId, grantId) => {
204
- if (grantId.startsWith('pgrant_')) {
205
- return poppy ? poppy.revokeAccountGrant(accountId, grantId) : false;
208
+ if (isExtensionGrant(grantId)) {
209
+ const owner = ownerOf(grantId);
210
+ return owner ? owner.runtime.revokeAccountGrant(accountId, grantId) : false;
206
211
  }
207
212
  return delegation ? delegation.revokeGrant(accountId, grantId) : false;
208
213
  };
209
214
  const revokeAllAgentGrants = async (accountId) => {
210
215
  let count = 0;
211
- if (poppy)
212
- count += await poppy.revokeAllAccountGrants(accountId);
216
+ for (const { runtime } of extensions.values()) {
217
+ count += await runtime.revokeAllAccountGrants(accountId);
218
+ }
213
219
  if (delegation)
214
220
  count += await delegation.revokeAllGrants(accountId);
215
221
  return count;
@@ -222,10 +228,11 @@ export function buildPersonalAgentsRuntime(service, makeDb, options = {}) {
222
228
  // Expirado já não vale: revogar não muda nada (e não vira evento de auditoria).
223
229
  if (!grant || grant.revokedAt || grant.expiresAt <= now)
224
230
  return null;
225
- if (grantId.startsWith('pgrant_')) {
226
- if (!poppy)
231
+ if (isExtensionGrant(grantId)) {
232
+ const owner = ownerOf(grantId);
233
+ if (!owner)
227
234
  return null;
228
- await poppy.revokeGrant(grantId);
235
+ await owner.runtime.revokeGrant(grantId);
229
236
  }
230
237
  else {
231
238
  await store.revokeGrantById(grantId, now);
@@ -235,9 +242,11 @@ export function buildPersonalAgentsRuntime(service, makeDb, options = {}) {
235
242
  const disconnectAgent = async (protocol, identifier) => {
236
243
  if (!store)
237
244
  return 0;
238
- if (protocol === 'poppy')
239
- return poppy ? poppy.disconnectClient(identifier) : 0;
240
- const ids = (await store.unrevokedGrantIdsOfClient(identifier)).filter((id) => !id.startsWith('pgrant_'));
245
+ if (protocol !== 'pact') {
246
+ const entry = extensions.get(protocol);
247
+ return entry ? entry.runtime.disconnectAgent(identifier) : 0;
248
+ }
249
+ const ids = (await store.unrevokedGrantIdsOfClient(identifier)).filter((id) => !isExtensionGrant(id));
241
250
  const at = options.now?.() ?? new Date();
242
251
  for (const id of ids)
243
252
  await store.revokeGrantById(id, at);
@@ -248,7 +257,7 @@ export function buildPersonalAgentsRuntime(service, makeDb, options = {}) {
248
257
  urls,
249
258
  verifier: new PersonalAgentVerifier(effective),
250
259
  delegation,
251
- poppy,
260
+ extensions,
252
261
  revokeAgentGrant,
253
262
  revokeAllAgentGrants,
254
263
  registry,
@@ -270,3 +279,29 @@ export async function personalAgentsFor(ctx) {
270
279
  make: (binding) => ctx.containerResolver.make(binding),
271
280
  });
272
281
  }
282
+ /**
283
+ * The runtime an extension's `setup` built (`personalAgents.extensions`), by protocol id. `null`
284
+ * when the extension is not configured (or there is no database). Takes the request, an explicit
285
+ * source (tests, scripts) or nothing (the booted app).
286
+ *
287
+ * Stable for 1.x — see `extension.ts`.
288
+ */
289
+ export async function personalAgentExtensionRuntime(id, from) {
290
+ let runtime;
291
+ if (from && 'containerResolver' in from) {
292
+ runtime = await personalAgentsFor(from);
293
+ }
294
+ else if (from) {
295
+ const source = from;
296
+ runtime = await buildPersonalAgentsRuntime(source.service, () => source.db(), {
297
+ make: source.make ? (binding) => source.make(binding) : undefined,
298
+ now: source.now ? () => source.now() : undefined,
299
+ });
300
+ }
301
+ else {
302
+ const app = getBootedApp();
303
+ const service = (await app.container.make('authkit.server'));
304
+ runtime = await buildPersonalAgentsRuntime(service, () => app.container.make('lucid.db'), { make: (binding) => app.container.make(binding) });
305
+ }
306
+ return runtime?.extensions.get(id)?.runtime ?? null;
307
+ }
@@ -8,7 +8,7 @@
8
8
  * varre os `audit.record({ type: ... })` do `src/` e falha quando um evento
9
9
  * emitido não está aqui.
10
10
  */
11
- export declare const AUDIT_EVENT_TYPES: readonly ['login.success', 'login.failure', 'signup', 'password_reset.issued', 'password_reset.consumed', 'pat.issued', 'pat.revoked', 'pat.used', 'impersonation', 'impersonation.panel_viewed', 'impersonation.stopped', 'mfa.enabled', 'mfa.disabled', 'mfa.recovery_codes_regenerated', 'account.locked', 'passkey.registered', 'passkey.removed', 'email_verification.issued', 'email_verification.consumed', 'client.created', 'client.updated', 'client.deleted', 'session.revoked_all', 'password.changed', 'password.rehashed', 'email.change_requested', 'email.changed', 'login.new_ip_notified', 'login.new_device', 'login.otp_sent', 'login.otp_verified', 'login.otp_failed', 'login.otp_invalidated', 'bot_protection.rejected', 'grant.revoked_by_user', 'user.created', 'user.password_reset_sent', 'user.disabled', 'user.enabled', 'user.deleted', 'profile.updated', 'account.deleted', 'account.exported', 'keys.rotated', 'organization.created', 'organization.updated', 'organization.deleted', 'organization.member_added', 'organization.member_removed', 'organization.member_role_changed', 'organization.member_role_updated', 'organization.switched', 'organization.deactivated', 'organization.invitation_sent', 'organization.invitation_accepted', 'organization.invitation_revoked', 'email_change.requested', 'email_change.confirmed', 'email_change.cancelled', 'security_notice.sent', 'settings.updated', 'maintenance.enabled', 'maintenance.disabled', 'trusted_device.revoked', 'password.expired_change_forced', 'otp.locked', 'otp.unlocked', 'otp.unlock_failed', 'sudo.confirmed', 'session.single_enforced', 'account.expired_login_blocked', 'account.expiration_warned', 'login.magic_link_sent', 'session.revoked', 'account.signed_out_all', 'client.secret_regenerated', 'roles_catalog.updated', 'agent.delegation_approved', 'agent.delegation_denied', 'agent.grant_revoked', 'agent.grant_revoked_by_admin', 'agent.allowed', 'agent.blocked', 'agent.registry_removed'];
11
+ export declare const AUDIT_EVENT_TYPES: readonly ['login.success', 'login.failure', 'signup', 'password_reset.issued', 'password_reset.consumed', 'pat.issued', 'pat.revoked', 'pat.used', 'impersonation', 'impersonation.panel_viewed', 'impersonation.stopped', 'mfa.enabled', 'mfa.disabled', 'mfa.recovery_codes_regenerated', 'account.locked', 'passkey.registered', 'passkey.removed', 'email_verification.issued', 'email_verification.consumed', 'client.created', 'client.updated', 'client.deleted', 'session.revoked_all', 'password.changed', 'password.rehashed', 'email.change_requested', 'email.changed', 'login.new_ip_notified', 'login.new_device', 'login.otp_sent', 'login.otp_verified', 'login.otp_failed', 'login.otp_invalidated', 'bot_protection.rejected', 'grant.revoked_by_user', 'user.created', 'user.password_reset_sent', 'user.disabled', 'user.enabled', 'user.deleted', 'profile.updated', 'account.deleted', 'account.exported', 'keys.rotated', 'organization.created', 'organization.updated', 'organization.deleted', 'organization.member_added', 'organization.member_removed', 'organization.member_role_changed', 'organization.member_role_updated', 'organization.switched', 'organization.deactivated', 'organization.invitation_sent', 'organization.invitation_accepted', 'organization.invitation_revoked', 'email_change.requested', 'email_change.confirmed', 'email_change.cancelled', 'security_notice.sent', 'settings.updated', 'maintenance.enabled', 'maintenance.disabled', 'trusted_device.revoked', 'password.expired_change_forced', 'otp.locked', 'otp.unlocked', 'otp.unlock_failed', 'sudo.confirmed', 'session.single_enforced', 'account.expired_login_blocked', 'account.expiration_warned', 'login.magic_link_sent', 'session.revoked', 'account.signed_out_all', 'client.secret_regenerated', 'client.disabled', 'client.enabled', 'service_tenant.token_issued', 'roles_catalog.updated', 'agent.delegation_approved', 'agent.delegation_denied', 'agent.grant_revoked', 'agent.grant_revoked_by_admin', 'agent.allowed', 'agent.blocked', 'agent.registry_removed', 'account.extension.redirect', 'account.extension.action'];
12
12
  /** Tipos de eventos de auditoria relevantes para segurança emitidos pelo IdP. */
13
13
  export type AuditEventType = (typeof AUDIT_EVENT_TYPES)[number];
14
14
  /**
@@ -107,6 +107,11 @@ export const AUDIT_EVENT_TYPES = [
107
107
  'session.revoked',
108
108
  'account.signed_out_all',
109
109
  'client.secret_regenerated',
110
+ // A client was disabled (no new tokens) or enabled again.
111
+ 'client.disabled',
112
+ 'client.enabled',
113
+ // A service tenant got a token (`partition`, `resource`, `jti` in the metadata).
114
+ 'service_tenant.token_issued',
110
115
  'roles_catalog.updated',
111
116
  // Personal agents (PACT §5): o usuário aprovou/negou a delegação pedida por um
112
117
  // agente, ou revogou um grant no console. `clientId` = issuer do agente.
@@ -114,9 +119,13 @@ export const AUDIT_EVENT_TYPES = [
114
119
  'agent.delegation_denied',
115
120
  'agent.grant_revoked',
116
121
  // Console admin: um admin revogou a conexão de um usuário, ou liberou/bloqueou um
117
- // agente (PACT pelo `iss`, Poppy pelo `client_id`).
122
+ // agente (PACT pelo `iss`; uma extension pelo identificador dela).
118
123
  'agent.grant_revoked_by_admin',
119
124
  'agent.allowed',
120
125
  'agent.blocked',
121
126
  'agent.registry_removed',
127
+ // Console de conta: uma action de extension mandou o usuário para fora do app
128
+ // (`redirectTo.external`), ou uma action sensível (sudo/danger) rodou.
129
+ 'account.extension.redirect',
130
+ 'account.extension.action',
122
131
  ];
@@ -0,0 +1,132 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ /**
3
+ * The console switcher contract, version 1.
4
+ *
5
+ * Every `@adonis-agora` console registers itself into ONE data-only registry bound as a value at
6
+ * `'agora.consoles'` (created by whichever lib boots first). Each console then shows a switcher to
7
+ * the other consoles the viewer may open. The types are declared locally in every lib (TypeScript is
8
+ * structural), so no lib imports another.
9
+ */
10
+ /** The container binding the registry lives at. */
11
+ export declare const AGORA_CONSOLES_BINDING = "agora.consoles";
12
+ /** Bound as a VALUE under `'agora.consoles'`. Data only, so every lib's copy is identical. */
13
+ export interface AgoraConsoleRegistry {
14
+ readonly version: 1;
15
+ readonly entries: AgoraConsoleEntry[];
16
+ }
17
+ export type AgoraConsoleIcon = 'workflow' | 'telescope' | 'media' | 'payments' | 'agent' | 'identity' | 'generic';
18
+ /** The decision a console's gate returns. Never throws. */
19
+ export interface AgoraConsoleDecision {
20
+ allowed: boolean;
21
+ reason?: string;
22
+ redirectTo?: string;
23
+ }
24
+ /** One search hit. `href` is a same-origin absolute path. */
25
+ export interface AgoraConsoleSearchHit {
26
+ title: string;
27
+ subtitle?: string;
28
+ href: string;
29
+ }
30
+ export interface AgoraConsoleEntry {
31
+ /** `/^[a-z][a-z0-9-]*$/`: `'durable'`, `'telescope'`, `'authkit'`, … */
32
+ id: string;
33
+ /** Product title (`'Identity'`). */
34
+ title: string;
35
+ /** Package name, shown as secondary text (`'@adonis-agora/authkit-server'`). */
36
+ package?: string;
37
+ /** Unknown values render as `'generic'`. */
38
+ icon: AgoraConsoleIcon;
39
+ /** Same-origin absolute path of the console shell (`'/admin'`). */
40
+ path: string;
41
+ /** Lower first. durable 10, telescope 20, agent 30, payments 40, media 50, authkit 90. */
42
+ order: number;
43
+ /**
44
+ * The console's resolved gate (the one its own routes run). While a switcher runs it for another
45
+ * console, ``ctx.agoraProbe` is `true` (see `isAgoraProbe`), so an authorizer that writes an audit entry per call can skip.
46
+ */
47
+ authorize(ctx: HttpContext): Promise<AgoraConsoleDecision>;
48
+ /** Global search provider. Only called after `authorize` allowed the viewer. */
49
+ search?(ctx: HttpContext, query: string, limit: number): Promise<AgoraConsoleSearchHit[]>;
50
+ }
51
+ /** What `GET <console api>/consoles` answers. */
52
+ export interface VisibleConsoles {
53
+ version: 1;
54
+ current: string;
55
+ consoles: Array<{
56
+ id: string;
57
+ title: string;
58
+ package?: string;
59
+ icon: AgoraConsoleIcon;
60
+ href: string;
61
+ }>;
62
+ }
63
+ /** What `GET <console api>/consoles/search?q=` answers. */
64
+ export interface ConsoleSearchResults {
65
+ version: 1;
66
+ query: string;
67
+ groups: Array<{
68
+ id: string;
69
+ title: string;
70
+ icon: AgoraConsoleIcon;
71
+ hits: AgoraConsoleSearchHit[];
72
+ }>;
73
+ }
74
+ /** Default time each console's `authorize` (and `search`) gets before it counts as hidden. */
75
+ export declare const CONSOLE_PROBE_TIMEOUT_MS = 300;
76
+ /** Hits returned per console by the global search. */
77
+ export declare const CONSOLE_SEARCH_LIMIT = 5;
78
+ interface ContainerLike {
79
+ hasBinding(binding: string): boolean;
80
+ bindValue(binding: string, value: unknown): void;
81
+ make(binding: string): Promise<unknown>;
82
+ }
83
+ interface AppLike {
84
+ container: ContainerLike;
85
+ }
86
+ interface LoggerLike {
87
+ warn(message: string): void;
88
+ debug?(message: string): void;
89
+ }
90
+ /** A same-origin absolute path: starts with `/`, not `//`, no backslash, no control characters. */
91
+ export declare function isSameOriginPath(value: unknown): value is string;
92
+ /**
93
+ * Add a console to the shared `'agora.consoles'` registry, creating it when absent. Skips (never
94
+ * throws) a registry of another version; warns and skips a duplicate id. Throws on an invalid `id`
95
+ * or `path`: that is a programming error. Apps can call it to register a console they mount by hand.
96
+ *
97
+ * ```ts
98
+ * await registerAgoraConsole(app, { id: 'reports', title: 'Reports', icon: 'generic', path: '/reports',
99
+ * order: 100, authorize: async (ctx) => ({ allowed: await isStaff(ctx) }) })
100
+ * ```
101
+ */
102
+ export declare function registerAgoraConsole(app: AppLike, entry: AgoraConsoleEntry, logger?: LoggerLike): Promise<boolean>;
103
+ /** The registry, when one is bound and is version 1. */
104
+ export declare function readAgoraConsoles(app: AppLike): Promise<AgoraConsoleRegistry | null>;
105
+ /** How many consoles besides `selfId` are registered (the shell's `switcher.others`). */
106
+ export declare function countOtherConsoles(registry: AgoraConsoleRegistry | null, selfId: string): number;
107
+ /**
108
+ * Whether a console switcher is running this `authorize` on behalf of another console (it only asks
109
+ * whether to list it). The marker is `ctx.agoraProbe === true`, set structurally by every
110
+ * `@adonis-agora` console; an authorizer that audits every call can skip then.
111
+ */
112
+ export declare function isAgoraProbe(ctx: unknown): boolean;
113
+ /**
114
+ * The consoles (other than `selfId`) the viewer may open: every `authorize` runs in parallel, each
115
+ * raced against 300 ms; a timeout, a rejection or anything but `allowed: true` hides the console.
116
+ */
117
+ export declare function listVisibleConsoles(registry: AgoraConsoleRegistry | null, selfId: string, ctx: HttpContext, options?: {
118
+ timeoutMs?: number;
119
+ logger?: LoggerLike;
120
+ }): Promise<VisibleConsoles>;
121
+ /**
122
+ * Global search across the consoles the viewer may open. The current console (`selfId`, already
123
+ * authorized by its own route guard) is searched without re-running its gate; the others only after
124
+ * their `authorize` allowed. Each `search` gets `timeoutMs` and returns at most `limit` hits; a hit
125
+ * whose `href` is not a same-origin path is dropped.
126
+ */
127
+ export declare function searchConsoles(registry: AgoraConsoleRegistry | null, selfId: string, ctx: HttpContext, query: string, options?: {
128
+ timeoutMs?: number;
129
+ limit?: number;
130
+ logger?: LoggerLike;
131
+ }): Promise<ConsoleSearchResults>;
132
+ export {};
@@ -0,0 +1,206 @@
1
+ /**
2
+ * The console switcher contract, version 1.
3
+ *
4
+ * Every `@adonis-agora` console registers itself into ONE data-only registry bound as a value at
5
+ * `'agora.consoles'` (created by whichever lib boots first). Each console then shows a switcher to
6
+ * the other consoles the viewer may open. The types are declared locally in every lib (TypeScript is
7
+ * structural), so no lib imports another.
8
+ */
9
+ /** The container binding the registry lives at. */
10
+ export const AGORA_CONSOLES_BINDING = 'agora.consoles';
11
+ const CONSOLE_ID = /^[a-z][a-z0-9-]*$/;
12
+ const ICONS = [
13
+ 'workflow',
14
+ 'telescope',
15
+ 'media',
16
+ 'payments',
17
+ 'agent',
18
+ 'identity',
19
+ 'generic',
20
+ ];
21
+ /** Default time each console's `authorize` (and `search`) gets before it counts as hidden. */
22
+ export const CONSOLE_PROBE_TIMEOUT_MS = 300;
23
+ /** Hits returned per console by the global search. */
24
+ export const CONSOLE_SEARCH_LIMIT = 5;
25
+ /** A same-origin absolute path: starts with `/`, not `//`, no backslash, no control characters. */
26
+ export function isSameOriginPath(value) {
27
+ return (typeof value === 'string' &&
28
+ value.startsWith('/') &&
29
+ !value.startsWith('//') &&
30
+ !value.includes('\\') &&
31
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: rejecting control characters is the point
32
+ !/[\u0000-\u001f\u007f]/.test(value));
33
+ }
34
+ /**
35
+ * Add a console to the shared `'agora.consoles'` registry, creating it when absent. Skips (never
36
+ * throws) a registry of another version; warns and skips a duplicate id. Throws on an invalid `id`
37
+ * or `path`: that is a programming error. Apps can call it to register a console they mount by hand.
38
+ *
39
+ * ```ts
40
+ * await registerAgoraConsole(app, { id: 'reports', title: 'Reports', icon: 'generic', path: '/reports',
41
+ * order: 100, authorize: async (ctx) => ({ allowed: await isStaff(ctx) }) })
42
+ * ```
43
+ */
44
+ export async function registerAgoraConsole(app, entry, logger) {
45
+ if (typeof entry?.id !== 'string' || !CONSOLE_ID.test(entry.id)) {
46
+ throw new Error(`registerAgoraConsole: console id ${JSON.stringify(entry?.id)} must match /^[a-z][a-z0-9-]*$/`);
47
+ }
48
+ if (!isSameOriginPath(entry.path)) {
49
+ throw new Error(`registerAgoraConsole: console "${entry.id}" path ${JSON.stringify(entry.path)} must be a same-origin absolute path`);
50
+ }
51
+ if (typeof entry.authorize !== 'function') {
52
+ throw new Error(`registerAgoraConsole: console "${entry.id}" needs an authorize(ctx)`);
53
+ }
54
+ const container = app.container;
55
+ if (!container.hasBinding(AGORA_CONSOLES_BINDING)) {
56
+ container.bindValue(AGORA_CONSOLES_BINDING, { version: 1, entries: [] });
57
+ }
58
+ const registry = (await container.make(AGORA_CONSOLES_BINDING));
59
+ if (registry?.version !== 1 || !Array.isArray(registry.entries))
60
+ return false;
61
+ if (registry.entries.some((e) => e.id === entry.id)) {
62
+ logger?.warn(`[@adonis-agora/authkit-server] console id "${entry.id}" already registered`);
63
+ return false;
64
+ }
65
+ registry.entries.push(entry);
66
+ return true;
67
+ }
68
+ /** The registry, when one is bound and is version 1. */
69
+ export async function readAgoraConsoles(app) {
70
+ if (!app.container.hasBinding(AGORA_CONSOLES_BINDING))
71
+ return null;
72
+ const registry = (await app.container.make(AGORA_CONSOLES_BINDING));
73
+ if (registry?.version !== 1 || !Array.isArray(registry.entries))
74
+ return null;
75
+ return registry;
76
+ }
77
+ /** How many consoles besides `selfId` are registered (the shell's `switcher.others`). */
78
+ export function countOtherConsoles(registry, selfId) {
79
+ return registry ? registry.entries.filter((e) => e.id !== selfId).length : 0;
80
+ }
81
+ function sortEntries(entries) {
82
+ return [...entries].sort((a, b) => a.order - b.order || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
83
+ }
84
+ function iconOf(icon) {
85
+ return ICONS.includes(icon) ? icon : 'generic';
86
+ }
87
+ const TIMED_OUT = Symbol('timed out');
88
+ /** Resolves to `TIMED_OUT` when `promise` takes longer than `ms`, or `undefined` when it rejects. */
89
+ async function race(promise, ms) {
90
+ let timer;
91
+ try {
92
+ return await Promise.race([
93
+ Promise.resolve().then(promise),
94
+ new Promise((resolve) => {
95
+ timer = setTimeout(() => resolve(TIMED_OUT), ms);
96
+ }),
97
+ ]);
98
+ }
99
+ catch {
100
+ return undefined;
101
+ }
102
+ finally {
103
+ if (timer)
104
+ clearTimeout(timer);
105
+ }
106
+ }
107
+ /**
108
+ * Whether a console switcher is running this `authorize` on behalf of another console (it only asks
109
+ * whether to list it). The marker is `ctx.agoraProbe === true`, set structurally by every
110
+ * `@adonis-agora` console; an authorizer that audits every call can skip then.
111
+ */
112
+ export function isAgoraProbe(ctx) {
113
+ return ctx?.agoraProbe === true;
114
+ }
115
+ /** Runs `fn` with `ctx.agoraProbe = true`, restoring the previous value afterwards. */
116
+ async function probing(ctx, fn) {
117
+ const holder = ctx;
118
+ const previous = holder.agoraProbe;
119
+ holder.agoraProbe = true;
120
+ try {
121
+ return await fn();
122
+ }
123
+ finally {
124
+ if (previous === undefined)
125
+ delete holder.agoraProbe;
126
+ else
127
+ holder.agoraProbe = previous;
128
+ }
129
+ }
130
+ /** Runs every other console's gate in parallel (each raced against `timeoutMs`); keeps the allowed. */
131
+ async function allowedEntries(entries, ctx, timeoutMs, logger) {
132
+ return probing(ctx, async () => {
133
+ const decisions = await Promise.all(entries.map(async (entry) => {
134
+ const decision = await race(() => entry.authorize(ctx), timeoutMs);
135
+ if (decision === TIMED_OUT || decision === undefined) {
136
+ logger?.debug?.(`[@adonis-agora/authkit-server] console "${entry.id}" hidden: authorize ${decision === TIMED_OUT ? 'timed out' : 'failed'}`);
137
+ return false;
138
+ }
139
+ return decision?.allowed === true;
140
+ }));
141
+ return entries.filter((_, i) => decisions[i]);
142
+ });
143
+ }
144
+ /**
145
+ * The consoles (other than `selfId`) the viewer may open: every `authorize` runs in parallel, each
146
+ * raced against 300 ms; a timeout, a rejection or anything but `allowed: true` hides the console.
147
+ */
148
+ export async function listVisibleConsoles(registry, selfId, ctx, options = {}) {
149
+ const others = sortEntries((registry?.entries ?? []).filter((e) => e.id !== selfId));
150
+ const allowed = await allowedEntries(others, ctx, options.timeoutMs ?? CONSOLE_PROBE_TIMEOUT_MS, options.logger);
151
+ return {
152
+ version: 1,
153
+ current: selfId,
154
+ consoles: allowed
155
+ .filter((e) => isSameOriginPath(e.path))
156
+ .map((e) => ({
157
+ id: e.id,
158
+ title: e.title,
159
+ ...(typeof e.package === 'string' ? { package: e.package } : {}),
160
+ icon: iconOf(e.icon),
161
+ href: e.path,
162
+ })),
163
+ };
164
+ }
165
+ /**
166
+ * Global search across the consoles the viewer may open. The current console (`selfId`, already
167
+ * authorized by its own route guard) is searched without re-running its gate; the others only after
168
+ * their `authorize` allowed. Each `search` gets `timeoutMs` and returns at most `limit` hits; a hit
169
+ * whose `href` is not a same-origin path is dropped.
170
+ */
171
+ export async function searchConsoles(registry, selfId, ctx, query, options = {}) {
172
+ const q = query.trim().slice(0, 200);
173
+ const result = { version: 1, query: q, groups: [] };
174
+ if (q.length === 0 || !registry)
175
+ return result;
176
+ const timeoutMs = options.timeoutMs ?? CONSOLE_PROBE_TIMEOUT_MS;
177
+ const limit = options.limit ?? CONSOLE_SEARCH_LIMIT;
178
+ const searchable = sortEntries(registry.entries.filter((e) => typeof e.search === 'function'));
179
+ const self = searchable.filter((e) => e.id === selfId);
180
+ const others = await allowedEntries(searchable.filter((e) => e.id !== selfId), ctx, timeoutMs, options.logger);
181
+ const targets = sortEntries([...self, ...others]);
182
+ // Real searches by the viewer: run without the probe marker.
183
+ const hits = await Promise.all(targets.map((entry) => race(() => entry.search(ctx, q, limit), timeoutMs)));
184
+ targets.forEach((entry, i) => {
185
+ const found = hits[i];
186
+ if (!Array.isArray(found))
187
+ return;
188
+ const safe = found
189
+ .filter((h) => h && typeof h.title === 'string' && isSameOriginPath(h.href))
190
+ .slice(0, limit)
191
+ .map((h) => ({
192
+ title: h.title,
193
+ ...(typeof h.subtitle === 'string' ? { subtitle: h.subtitle } : {}),
194
+ href: h.href,
195
+ }));
196
+ if (safe.length > 0) {
197
+ result.groups.push({
198
+ id: entry.id,
199
+ title: entry.title,
200
+ icon: iconOf(entry.icon),
201
+ hits: safe,
202
+ });
203
+ }
204
+ });
205
+ return result;
206
+ }