@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,228 +0,0 @@
1
- /** Parâmetros que um campo do Mediated Sign-In não pode usar (extensão, §2). */
2
- const RESERVED_FIELDS = new Set([
3
- 'grant_type',
4
- 'scope',
5
- 'resource',
6
- 'client_id',
7
- 'client_secret',
8
- 'client_assertion',
9
- 'client_assertion_type',
10
- 'sign_in_id',
11
- 'one_time_code',
12
- ]);
13
- const SCOPE_ID = /^[\x21\x23-\x5B\x5D-\x7E]+$/; // RFC 6749 §3.3 scope-token
14
- const DOMAIN = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/;
15
- function fail(message) {
16
- throw new Error(`authkit: personalAgents.poppy.${message}`);
17
- }
18
- function positive(value, fallback, what) {
19
- if (value === undefined)
20
- return fallback;
21
- if (!Number.isFinite(value) || value <= 0)
22
- fail(`${what} precisa ser > 0.`);
23
- return Math.floor(value);
24
- }
25
- function absoluteUrl(value, what) {
26
- let url;
27
- try {
28
- url = new URL(value);
29
- }
30
- catch {
31
- fail(`${what} precisa ser uma URL absoluta (recebeu "${value}").`);
32
- }
33
- if (url.protocol !== 'https:' && url.protocol !== 'http:') {
34
- fail(`${what} precisa ser http(s) (recebeu "${value}").`);
35
- }
36
- }
37
- export function normalizePoppyPrefix(prefix) {
38
- const trimmed = (prefix ?? '/poppy').trim().replace(/\/+$/, '');
39
- if (!trimmed)
40
- return '/poppy';
41
- return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
42
- }
43
- function membership(value) {
44
- if (typeof value === 'function')
45
- return async (id) => (await value(id)) === true;
46
- const set = new Set(value ?? []);
47
- return async (id) => set.has(id);
48
- }
49
- function listOf(value) {
50
- if (typeof value === 'function')
51
- return null;
52
- return [...new Set(value ?? [])];
53
- }
54
- function typeScopes(input, all, what) {
55
- if (input === false)
56
- return null;
57
- const scopes = typeof input === 'object' && input.scopes ? [...new Set(input.scopes)] : all;
58
- for (const id of scopes) {
59
- if (!all.includes(id))
60
- fail(`signIn.${what}.scopes — scope desconhecido "${id}".`);
61
- }
62
- if (scopes.length === 0)
63
- fail(`signIn.${what}.scopes precisa de pelo menos um scope.`);
64
- return { scopes };
65
- }
66
- export function resolvePoppyConfig(input, fallbackScopes) {
67
- if (!input)
68
- return undefined;
69
- const org = input.organization;
70
- if (!org?.name?.trim())
71
- fail('organization.name é obrigatório.');
72
- // Domínios comparados só ignorando caixa, sem nenhuma outra normalização (§3.1).
73
- const domains = [org.domain, ...(org.aliases ?? [])].map((d) => (d ?? '').toLowerCase().replace(/\.$/, ''));
74
- for (const d of domains) {
75
- if (!DOMAIN.test(d))
76
- fail(`organization — domínio inválido "${d}" (só o host, sem esquema).`);
77
- }
78
- const scopes = { ...(input.scopes ?? fallbackScopes ?? {}) };
79
- for (const id of Object.keys(scopes)) {
80
- if (!SCOPE_ID.test(id))
81
- fail(`scopes — id inválido "${id}" (sem espaços/aspas).`);
82
- }
83
- const all = Object.keys(scopes);
84
- const scopeHints = { ...(input.scopeHints ?? {}) };
85
- for (const [id, hint] of Object.entries(scopeHints)) {
86
- if (!['none', 'reversible', 'irreversible'].includes(hint?.changes)) {
87
- fail(`scopeHints.${id}.changes precisa ser none, reversible ou irreversible.`);
88
- }
89
- if (typeof hint.personal_data !== 'boolean') {
90
- fail(`scopeHints.${id}.personal_data é obrigatório (boolean).`);
91
- }
92
- }
93
- const signInInput = input.signIn ?? {};
94
- let mediated = null;
95
- if (signInInput.mediated) {
96
- const m = signInInput.mediated;
97
- if (!Array.isArray(m.fields) || m.fields.length === 0) {
98
- fail('signIn.mediated.fields precisa de pelo menos um campo.');
99
- }
100
- const names = new Set();
101
- for (const f of m.fields) {
102
- if (!f?.name || !f.label || typeof f.secret !== 'boolean') {
103
- fail('signIn.mediated.fields — cada campo precisa de name, label e secret.');
104
- }
105
- if (RESERVED_FIELDS.has(f.name)) {
106
- fail(`signIn.mediated.fields — "${f.name}" é um parâmetro reservado.`);
107
- }
108
- if (names.has(f.name))
109
- fail(`signIn.mediated.fields — campo duplicado "${f.name}".`);
110
- names.add(f.name);
111
- }
112
- if (typeof m.verify !== 'function')
113
- fail('signIn.mediated.verify é obrigatório.');
114
- if (!Array.isArray(m.scopes))
115
- fail('signIn.mediated.scopes é obrigatório.');
116
- const mediatedScopes = typeScopes({ scopes: m.scopes }, all, 'mediated').scopes;
117
- mediated = {
118
- fields: m.fields.map((f) => ({ name: f.name, label: f.label, secret: f.secret })),
119
- scopes: mediatedScopes,
120
- verify: m.verify,
121
- verifyCode: m.verifyCode,
122
- codeTtl: positive(m.codeTtl, 600, 'signIn.mediated.codeTtl'),
123
- maxCodeAttempts: positive(m.maxCodeAttempts, 5, 'signIn.mediated.maxCodeAttempts'),
124
- };
125
- }
126
- if (all.length === 0 && (signInInput.direct !== false || signInInput.device !== false)) {
127
- fail('nenhum scope: declare `scopes` ou desligue signIn.direct e signIn.device.');
128
- }
129
- for (const entry of input.agent?.protocols ?? []) {
130
- if (!entry.type)
131
- fail('agent.protocols[].type é obrigatório.');
132
- absoluteUrl(entry.endpoint, 'agent.protocols[].endpoint');
133
- if (entry.resource)
134
- absoluteUrl(entry.resource, 'agent.protocols[].resource');
135
- if (entry.access && entry.access !== 'session' && entry.access !== 'signed_in') {
136
- fail('agent.protocols[].access precisa ser session ou signed_in.');
137
- }
138
- }
139
- for (const api of input.apis ?? []) {
140
- if (!api.type)
141
- fail('apis[].type é obrigatório.');
142
- absoluteUrl(api.url, 'apis[].url');
143
- if (api.resource)
144
- absoluteUrl(api.resource, 'apis[].resource');
145
- if (typeof api.description !== 'string')
146
- fail('apis[].description é obrigatório.');
147
- }
148
- if (input.feedback)
149
- absoluteUrl(input.feedback.endpoint, 'feedback.endpoint');
150
- if (input.excludedTasks)
151
- absoluteUrl(input.excludedTasks.url, 'excludedTasks.url');
152
- const redirectTo = input.web ? (input.web.redirectTo ?? '/') : '/';
153
- if (!redirectTo.startsWith('/') || redirectTo.startsWith('//')) {
154
- fail('web.redirectTo precisa ser um path do site.');
155
- }
156
- for (const name of Object.keys(input.extensions ?? {})) {
157
- if (typeof input.extensions[name]?.version !== 'string') {
158
- fail(`extensions.${name}.version é obrigatório (string).`);
159
- }
160
- }
161
- const dpopAlgs = input.dpop?.algorithms ?? ['ES256'];
162
- if (dpopAlgs.length === 0 || dpopAlgs.some((a) => a === 'none' || a.startsWith('HS'))) {
163
- fail('dpop.algorithms precisa de algoritmos assimétricos.');
164
- }
165
- const allowInsecureClientIds = input.allowInsecureClientIds === true;
166
- if (allowInsecureClientIds && process.env.NODE_ENV === 'production') {
167
- fail('allowInsecureClientIds é só para desenvolvimento/teste e não pode ser ligado com NODE_ENV=production.');
168
- }
169
- let rateLimit;
170
- if (input.rateLimit === false)
171
- rateLimit = false;
172
- else if (typeof input.rateLimit === 'function')
173
- rateLimit = input.rateLimit;
174
- else {
175
- rateLimit = {
176
- sessionsPerMinute: positive(input.rateLimit?.sessionsPerMinute, 120, 'rateLimit.sessionsPerMinute'),
177
- mediatedPerHour: positive(input.rateLimit?.mediatedPerHour, 10, 'rateLimit.mediatedPerHour'),
178
- };
179
- }
180
- return {
181
- organization: { name: org.name, domain: domains[0], aliases: domains.slice(1) },
182
- prefix: normalizePoppyPrefix(input.prefix),
183
- scopes,
184
- scopeHints,
185
- signIn: {
186
- direct: typeScopes(signInInput.direct, all, 'direct'),
187
- device: typeScopes(signInInput.device, all, 'device'),
188
- mediated,
189
- },
190
- agent: input.agent?.protocols?.length ? { protocols: [...input.agent.protocols] } : null,
191
- apis: [...(input.apis ?? [])],
192
- feedback: input.feedback ?? null,
193
- excludedTasks: input.excludedTasks ?? null,
194
- web: input.web === false
195
- ? null
196
- : {
197
- browserSession: input.web?.browserSession !== false,
198
- login: input.web?.login,
199
- redirectTo,
200
- },
201
- extensions: { ...(input.extensions ?? {}) },
202
- clients: {
203
- requireRegistration: input.clients?.requireRegistration === true,
204
- isRegistered: membership(input.clients?.registered),
205
- isBlocked: membership(input.clients?.blocked),
206
- registeredList: listOf(input.clients?.registered),
207
- blockedList: listOf(input.clients?.blocked),
208
- },
209
- allowInsecureClientIds,
210
- accessTokenTtl: positive(input.accessTokenTtl, 3600, 'accessTokenTtl'),
211
- refreshTokenTtl: positive(input.refreshTokenTtl, 30 * 24 * 3600, 'refreshTokenTtl'),
212
- sessionTtl: positive(input.sessionTtl, 30 * 24 * 3600, 'sessionTtl'),
213
- authorizationCodeTtl: positive(input.authorizationCodeTtl, 60, 'authorizationCodeTtl'),
214
- deviceCodeTtl: positive(input.deviceCodeTtl, 600, 'deviceCodeTtl'),
215
- pollInterval: positive(input.pollInterval, 5, 'pollInterval'),
216
- assertionMaxLifetime: positive(input.assertionMaxLifetime, 300, 'assertionMaxLifetime'),
217
- sessionJwt: { maxAge: positive(input.sessionJwt?.maxAge, 60, 'sessionJwt.maxAge') },
218
- dpop: {
219
- required: input.dpop?.required === true,
220
- requireNonce: input.dpop?.requireNonce === true,
221
- maxAge: positive(input.dpop?.maxAge, 60, 'dpop.maxAge'),
222
- algorithms: [...dpopAlgs],
223
- },
224
- replay: input.replay ?? 'database',
225
- rateLimit,
226
- toActor: input.toActor,
227
- };
228
- }
@@ -1,61 +0,0 @@
1
- import type { PoppyReplayStore } from './config.js';
2
- export type DpopFailure = {
3
- ok: false;
4
- error: 'invalid_dpop_proof' | 'use_dpop_nonce';
5
- description: string;
6
- };
7
- export type DpopResult = {
8
- ok: true;
9
- jkt: string;
10
- jti: string;
11
- } | DpopFailure;
12
- export interface DpopNonceSource {
13
- /** Nonce novo para o header `DPoP-Nonce`. */
14
- issue(): string;
15
- /** O nonce ainda vale? */
16
- valid(nonce: string): boolean;
17
- }
18
- /**
19
- * Nonces sem estado: `base64url(ts).base64url(hmac(ts))`, válidos por
20
- * `ttlSeconds` (default 300). Qualquer instância com o mesmo segredo valida.
21
- *
22
- * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
23
- * de forma incompatível fora de majors enquanto ela for draft.
24
- */
25
- export declare function hmacNonceSource(secret: Buffer | string, options?: {
26
- ttlSeconds?: number;
27
- now?: () => number;
28
- }): DpopNonceSource;
29
- /** `ath`: base64url(SHA-256(access token)). */
30
- export declare function accessTokenHash(token: string): string;
31
- /**
32
- * `htu` normalizado para comparação: esquema e host minúsculos, sem porta
33
- * default, sem query nem fragmento. `null` se não for URL http(s).
34
- */
35
- export declare function normalizeHtu(value: string): string | null;
36
- export interface VerifyDpopInput {
37
- /** Valor do(s) header(s) `DPoP`. Mais de um = inválido. */
38
- proof: string | string[] | undefined | null;
39
- method: string;
40
- /** URL da request (a query é ignorada). */
41
- url: string;
42
- /** Access token apresentado junto (resource server) — exige `ath`. */
43
- accessToken?: string;
44
- /** Thumbprint (RFC 7638) a que o token está preso. */
45
- expectedJkt?: string;
46
- algorithms: string[];
47
- /** Janela do `iat`, em segundos. */
48
- maxAge: number;
49
- /** Tolerância de relógio para `iat` no futuro, em segundos. Default: 5. */
50
- clockSkew?: number;
51
- replay: PoppyReplayStore;
52
- nonce?: {
53
- required: boolean;
54
- source: DpopNonceSource;
55
- };
56
- now?: () => number;
57
- }
58
- /**
59
- * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — pode mudar de forma incompatível enquanto a spec for draft.
60
- */
61
- export declare function verifyDpopProof(input: VerifyDpopInput): Promise<DpopResult>;
@@ -1,158 +0,0 @@
1
- /**
2
- * Verificação de provas DPoP (RFC 9449) — utilitário genérico para o token
3
- * endpoint e para resource servers (§4.5 do Poppy).
4
- *
5
- * Confere: um único header `DPoP`; `typ` `dpop+jwt`; algoritmo permitido; `jwk`
6
- * público no header e a assinatura com ele; `jti`, `htm`, `htu` (sem query nem
7
- * fragmento), `iat` dentro da janela; `ath` = SHA-256 do access token; o
8
- * `nonce` quando exigido; replay do `jti` na janela; e, num resource server, que
9
- * a chave é a do token (`cnf.jkt`).
10
- */
11
- import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
12
- import { calculateJwkThumbprint, decodeProtectedHeader, EmbeddedJWK, jwtVerify, } from 'jose';
13
- import { replayKey } from './replay.js';
14
- /**
15
- * Nonces sem estado: `base64url(ts).base64url(hmac(ts))`, válidos por
16
- * `ttlSeconds` (default 300). Qualquer instância com o mesmo segredo valida.
17
- *
18
- * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — acompanha a spec em desenvolvimento e PODE MUDAR
19
- * de forma incompatível fora de majors enquanto ela for draft.
20
- */
21
- export function hmacNonceSource(secret, options = {}) {
22
- const ttl = (options.ttlSeconds ?? 300) * 1000;
23
- const now = options.now ?? Date.now;
24
- const mac = (ts) => createHmac('sha256', secret).update(`dpop-nonce:${ts}`).digest('base64url');
25
- return {
26
- issue() {
27
- const ts = Buffer.from(String(now())).toString('base64url');
28
- return `${ts}.${mac(ts)}`;
29
- },
30
- valid(nonce) {
31
- const [ts, sig] = nonce.split('.');
32
- if (!ts || !sig)
33
- return false;
34
- const expected = Buffer.from(mac(ts));
35
- const given = Buffer.from(sig);
36
- if (expected.length !== given.length || !timingSafeEqual(expected, given))
37
- return false;
38
- const issuedAt = Number(Buffer.from(ts, 'base64url').toString());
39
- const t = now();
40
- return Number.isFinite(issuedAt) && issuedAt <= t + 5000 && t - issuedAt <= ttl;
41
- },
42
- };
43
- }
44
- /** `ath`: base64url(SHA-256(access token)). */
45
- export function accessTokenHash(token) {
46
- return createHash('sha256').update(token).digest('base64url');
47
- }
48
- /**
49
- * `htu` normalizado para comparação: esquema e host minúsculos, sem porta
50
- * default, sem query nem fragmento. `null` se não for URL http(s).
51
- */
52
- export function normalizeHtu(value) {
53
- try {
54
- const url = new URL(value);
55
- if (url.protocol !== 'https:' && url.protocol !== 'http:')
56
- return null;
57
- url.hash = '';
58
- url.search = '';
59
- return url.toString();
60
- }
61
- catch {
62
- return null;
63
- }
64
- }
65
- function failure(error, description) {
66
- return { ok: false, error, description };
67
- }
68
- /**
69
- * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — pode mudar de forma incompatível enquanto a spec for draft.
70
- */
71
- export async function verifyDpopProof(input) {
72
- const raw = input.proof;
73
- if (raw === undefined || raw === null || raw === '') {
74
- return failure('invalid_dpop_proof', 'DPoP proof is missing');
75
- }
76
- if (Array.isArray(raw) ? raw.length !== 1 : raw.includes(',')) {
77
- return failure('invalid_dpop_proof', 'Exactly one DPoP proof is allowed');
78
- }
79
- const proof = Array.isArray(raw) ? raw[0] : raw;
80
- let header;
81
- try {
82
- header = decodeProtectedHeader(proof);
83
- }
84
- catch {
85
- return failure('invalid_dpop_proof', 'Malformed DPoP proof');
86
- }
87
- if (header.typ !== 'dpop+jwt')
88
- return failure('invalid_dpop_proof', 'typ must be dpop+jwt');
89
- if (!header.alg || !input.algorithms.includes(header.alg)) {
90
- return failure('invalid_dpop_proof', 'Unsupported DPoP algorithm');
91
- }
92
- const jwk = header.jwk;
93
- if (!jwk || typeof jwk !== 'object')
94
- return failure('invalid_dpop_proof', 'jwk header is required');
95
- if ('d' in jwk || jwk.kty === 'oct') {
96
- return failure('invalid_dpop_proof', 'jwk must be a public key');
97
- }
98
- const nowMs = (input.now ?? Date.now)();
99
- let payload;
100
- try {
101
- ({ payload } = await jwtVerify(proof, EmbeddedJWK, {
102
- algorithms: input.algorithms,
103
- typ: 'dpop+jwt',
104
- currentDate: new Date(nowMs),
105
- }));
106
- }
107
- catch {
108
- return failure('invalid_dpop_proof', 'DPoP proof signature is invalid');
109
- }
110
- const { jti, htm, htu, iat, ath, nonce } = payload;
111
- if (typeof jti !== 'string' || jti.length < 8 || jti.length > 256) {
112
- return failure('invalid_dpop_proof', 'jti is required');
113
- }
114
- if (typeof htm !== 'string' || htm !== input.method.toUpperCase()) {
115
- return failure('invalid_dpop_proof', 'htm does not match the request method');
116
- }
117
- const expectedHtu = normalizeHtu(input.url);
118
- if (typeof htu !== 'string' || !expectedHtu || normalizeHtu(htu) !== expectedHtu) {
119
- return failure('invalid_dpop_proof', 'htu does not match the request URL');
120
- }
121
- const nowS = Math.floor(nowMs / 1000);
122
- const skew = input.clockSkew ?? 5;
123
- if (typeof iat !== 'number' || iat > nowS + skew || iat < nowS - input.maxAge) {
124
- return failure('invalid_dpop_proof', 'iat is outside the acceptance window');
125
- }
126
- if (input.accessToken !== undefined) {
127
- if (typeof ath !== 'string' || ath !== accessTokenHash(input.accessToken)) {
128
- return failure('invalid_dpop_proof', 'ath does not match the access token');
129
- }
130
- }
131
- let jkt;
132
- try {
133
- jkt = await calculateJwkThumbprint(jwk, 'sha256');
134
- }
135
- catch {
136
- return failure('invalid_dpop_proof', 'Invalid jwk');
137
- }
138
- if (input.expectedJkt !== undefined && jkt !== input.expectedJkt) {
139
- return failure('invalid_dpop_proof', 'DPoP key does not match the token binding');
140
- }
141
- if (input.nonce?.required) {
142
- if (typeof nonce !== 'string' || !input.nonce.source.valid(nonce)) {
143
- return failure('use_dpop_nonce', 'A DPoP nonce issued by the server is required');
144
- }
145
- }
146
- else if (nonce !== undefined && input.nonce && typeof nonce === 'string') {
147
- // Nonce mandado sem ser exigido: se vier, tem de ser um nosso e válido.
148
- if (!input.nonce.source.valid(nonce)) {
149
- return failure('use_dpop_nonce', 'The DPoP nonce is invalid or expired');
150
- }
151
- }
152
- // Replay por último: uma prova inválida não queima o `jti`.
153
- const expiresAt = new Date((iat + input.maxAge + skew) * 1000);
154
- if (!(await input.replay.claim(replayKey('dpop', jkt, jti), expiresAt))) {
155
- return failure('invalid_dpop_proof', 'DPoP proof was already used');
156
- }
157
- return { ok: true, jkt, jti };
158
- }
@@ -1,27 +0,0 @@
1
- import { type AuthAccount } from '../../accounts/account_store.js';
2
- import type { PoppyMediatedConfigInput } from './config.js';
3
- export interface PoppyEmailCodeSignInOptions {
4
- /** Scopes que este sign-in pode conceder. */
5
- scopes: string[];
6
- /** Validade do código, em segundos. Default: 600. */
7
- codeTtl?: number;
8
- /** Tentativas de código antes de `invalid_grant`. Default: 5. */
9
- maxCodeAttempts?: number;
10
- /**
11
- * Quem pode entrar por aqui (ex.: só pacientes, não profissionais). `false` = recebe a
12
- * resposta de conta inexistente. Default: toda conta ativa.
13
- */
14
- eligible?: (account: AuthAccount) => boolean | Promise<boolean>;
15
- /**
16
- * Códigos por e-mail por hora, nesta instância — impede um agente de lotar a caixa de
17
- * alguém. Acima disso a resposta é a mesma, sem e-mail. Default: 5.
18
- */
19
- maxPerHour?: number;
20
- /** Rótulo do campo que o agente mostra ao usuário. Default: `'Email address'`. */
21
- label?: string;
22
- /** Texto de `sentTo` a partir do e-mail mascarado. Default: `` `Email to ${masked}` ``. */
23
- sentTo?: (masked: string) => string;
24
- }
25
- /** `ana.souza@gmail.com` → `a***@gmail.com`: o bastante para a pessoa reconhecer. */
26
- export declare function maskEmail(email: string): string;
27
- export declare function poppyEmailCodeSignIn(options: PoppyEmailCodeSignInOptions): PoppyMediatedConfigInput;
@@ -1,140 +0,0 @@
1
- /**
2
- * @experimental Poppy (Personal Agent Protocol) Draft 0.3 — pode mudar de forma incompatível enquanto a spec for draft.
3
- *
4
- * Mediated Sign-In pronto (extensão `mediated-sign-in`): o agente pede o e-mail da conta, o authkit manda
5
- * um código de 6 dígitos para ele e o agente devolve o código. Usa as contas
6
- * (`accountStore`) e o e-mail (hook `mail.onPoppySignInCode` ou o mailer default)
7
- * do próprio authkit — o app só liga:
8
- *
9
- * ```ts
10
- * personalAgents: {
11
- * poppy: { signIn: { mediated: poppyEmailCodeSignIn({ scopes: ['orders:read'] }) } },
12
- * }
13
- * ```
14
- *
15
- * Sem enumeração de contas: e-mail sem conta, conta desativada, conta recusada
16
- * por `eligible` ou e-mail acima do limite recebem a MESMA resposta, sem código
17
- * (`accountId: null`) — a lib recusa qualquer código como errado. O envio não é
18
- * esperado, então o tempo de resposta também não denuncia.
19
- */
20
- import { randomInt } from 'node:crypto';
21
- import { supportsAccountStatus } from '../../accounts/account_store.js';
22
- import { sendPoppySignInCodeEmail } from '../../host/default_mailer.js';
23
- import { personalAgentsFor } from '../runtime.js';
24
- /** `ana.souza@gmail.com` → `a***@gmail.com`: o bastante para a pessoa reconhecer. */
25
- export function maskEmail(email) {
26
- const at = email.lastIndexOf('@');
27
- const user = at > 0 ? email.slice(0, at) : '';
28
- const domain = at > 0 ? email.slice(at + 1) : '';
29
- if (!user || !domain)
30
- return '***';
31
- return `${user[0]}***@${domain}`;
32
- }
33
- const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
34
- const HOUR_MS = 3600 * 1000;
35
- const MAX_TRACKED = 10_000;
36
- export function poppyEmailCodeSignIn(options) {
37
- const positive = (value, fallback, what) => {
38
- if (value === undefined)
39
- return fallback;
40
- if (!Number.isFinite(value) || value < 1) {
41
- throw new Error(`authkit: poppyEmailCodeSignIn ${what} precisa ser um número >= 1.`);
42
- }
43
- return Math.floor(value);
44
- };
45
- const codeTtl = positive(options.codeTtl, 600, 'codeTtl');
46
- const maxPerHour = positive(options.maxPerHour, 5, 'maxPerHour');
47
- const maxCodeAttempts = positive(options.maxCodeAttempts, 5, 'maxCodeAttempts');
48
- const sentToText = options.sentTo ?? ((masked) => `Email to ${masked}`);
49
- const sends = new Map();
50
- /** `true` = ainda cabe um envio para este e-mail na última hora (e conta este). */
51
- const claim = (email) => {
52
- const now = Date.now();
53
- const recent = (sends.get(email) ?? []).filter((t) => now - t < HOUR_MS);
54
- if (recent.length >= maxPerHour) {
55
- sends.set(email, recent);
56
- return false;
57
- }
58
- recent.push(now);
59
- sends.delete(email);
60
- sends.set(email, recent);
61
- if (sends.size > MAX_TRACKED)
62
- sends.delete(sends.keys().next().value);
63
- return true;
64
- };
65
- const eligibleAccount = async (ctx, email) => {
66
- const service = await ctx.containerResolver.make('authkit.server');
67
- const store = service.config.accountStore;
68
- const account = await store.findByEmail(email);
69
- if (!account)
70
- return null;
71
- if (supportsAccountStatus(store) && (await store.isDisabled(account.id)))
72
- return null;
73
- if (options.eligible) {
74
- // Um `eligible` que lança vira "não elegível": um 500 só para algumas contas as denunciaria.
75
- try {
76
- if (!(await options.eligible(account)))
77
- return null;
78
- }
79
- catch (error) {
80
- ctx.logger.error({ err: error }, 'authkit: poppyEmailCodeSignIn eligible lançou');
81
- return null;
82
- }
83
- }
84
- return account;
85
- };
86
- return {
87
- fields: [{ name: 'email', label: options.label ?? 'Email address', secret: false }],
88
- scopes: options.scopes,
89
- codeTtl,
90
- maxCodeAttempts,
91
- async verify({ credentials, clientId, ctx }) {
92
- const email = (credentials.email ?? '').trim().toLowerCase();
93
- if (!EMAIL.test(email))
94
- return { status: 'failed' };
95
- const sentTo = sentToText(maskEmail(email));
96
- const decoy = { status: 'code_required', accountId: null, sentTo };
97
- // O limite conta para qualquer e-mail — com ou sem conta, a resposta não muda.
98
- if (!claim(email))
99
- return decoy;
100
- const account = await eligibleAccount(ctx, email);
101
- if (!account)
102
- return decoy;
103
- const code = String(randomInt(0, 1_000_000)).padStart(6, '0');
104
- // Sem `await`: o caminho com conta não pode demorar mais que o sem conta.
105
- void deliver(ctx, { email, code, clientId, expiresInMinutes: Math.ceil(codeTtl / 60) });
106
- return { status: 'code_required', accountId: account.id, sentTo, code };
107
- },
108
- };
109
- }
110
- function hostOf(clientId) {
111
- try {
112
- return new URL(clientId).host;
113
- }
114
- catch {
115
- return clientId;
116
- }
117
- }
118
- async function deliver(ctx, data) {
119
- try {
120
- const runtime = await personalAgentsFor(ctx);
121
- const agentName = runtime?.poppy
122
- ? await runtime.poppy.registry.displayName(data.clientId)
123
- : hostOf(data.clientId);
124
- const service = await ctx.containerResolver.make('authkit.server');
125
- const hook = service.config.mail?.onPoppySignInCode;
126
- const input = {
127
- email: data.email,
128
- code: data.code,
129
- agentName,
130
- expiresInMinutes: data.expiresInMinutes,
131
- };
132
- if (hook)
133
- await hook(input);
134
- else
135
- await sendPoppySignInCodeEmail(ctx, input);
136
- }
137
- catch (error) {
138
- ctx.logger.error({ err: error }, 'authkit: o código do Poppy Mediated Sign-In não saiu');
139
- }
140
- }
@@ -1,25 +0,0 @@
1
- /**
2
- * Erros OAuth do Poppy (§4.7, RFC 6749 §5.2, RFC 8628 §3.5, RFC 9449, extensão Mediated Sign-In)
3
- * — viram `{ error, error_description }` com o status da spec.
4
- */
5
- export type PoppyErrorCode = 'invalid_request' | 'invalid_client' | 'invalid_grant' | 'invalid_session' | 'invalid_dpop_proof' | 'use_dpop_nonce'
6
- /** Interno: HTTP 429 com `Retry-After`, sem corpo (§5.6). */
7
- | 'too_many_requests' | 'code_required' | 'invalid_scope' | 'invalid_target' | 'unsupported_grant_type' | 'unsupported_token_type' | 'unauthorized_client' | 'authorization_pending' | 'slow_down' | 'access_denied' | 'expired_token';
8
- export declare class PoppyError extends Error {
9
- readonly code: PoppyErrorCode;
10
- readonly description: string;
11
- readonly status: number;
12
- /** Headers extras da resposta (`DPoP-Nonce`, `Retry-After`). */
13
- readonly headers: Record<string, string>;
14
- /** Campos extras do corpo (`sign_in_id`, `sent_to`, `expires_in` do `code_required`). */
15
- readonly extra: Record<string, unknown>;
16
- constructor(code: PoppyErrorCode, description: string, options?: {
17
- status?: number;
18
- headers?: Record<string, string>;
19
- extra?: Record<string, unknown>;
20
- });
21
- toJSON(): {
22
- error: PoppyErrorCode;
23
- error_description: string;
24
- };
25
- }
@@ -1,25 +0,0 @@
1
- const STATUS = {
2
- invalid_client: 401,
3
- too_many_requests: 429,
4
- };
5
- export class PoppyError extends Error {
6
- code;
7
- description;
8
- status;
9
- /** Headers extras da resposta (`DPoP-Nonce`, `Retry-After`). */
10
- headers;
11
- /** Campos extras do corpo (`sign_in_id`, `sent_to`, `expires_in` do `code_required`). */
12
- extra;
13
- constructor(code, description, options = {}) {
14
- super(description);
15
- this.code = code;
16
- this.description = description;
17
- this.name = 'PoppyError';
18
- this.status = options.status ?? STATUS[code] ?? 400;
19
- this.headers = options.headers ?? {};
20
- this.extra = options.extra ?? {};
21
- }
22
- toJSON() {
23
- return { error: this.code, error_description: this.description, ...this.extra };
24
- }
25
- }