@adonis-agora/authkit-server 0.76.0 → 0.78.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -0
- package/build/host/views/agents/consent.edge +13 -2
- package/build/host/views/agents/done.edge +3 -0
- package/build/host/views/mfa-challenge.edge +42 -3
- package/build/index.d.ts +19 -1
- package/build/index.js +13 -1
- package/build/providers/authkit_server_provider.js +15 -1
- package/build/src/accounts/account_store.d.ts +2 -0
- package/build/src/accounts/lucid_account_store.js +2 -0
- package/build/src/adapters/adapter_contract.d.ts +2 -0
- package/build/src/adapters/database_adapter.d.ts +1 -0
- package/build/src/adapters/database_adapter.js +26 -0
- package/build/src/adapters/redis_adapter.d.ts +1 -0
- package/build/src/adapters/redis_adapter.js +30 -0
- package/build/src/agents/config.d.ts +20 -3
- package/build/src/agents/config.js +19 -4
- package/build/src/agents/delegation_service.js +18 -8
- package/build/src/agents/middleware.js +2 -1
- package/build/src/agents/poppy/client_registry.d.ts +45 -0
- package/build/src/agents/poppy/client_registry.js +226 -0
- package/build/src/agents/poppy/config.d.ts +362 -0
- package/build/src/agents/poppy/config.js +201 -0
- package/build/src/agents/poppy/dpop.d.ts +61 -0
- package/build/src/agents/poppy/dpop.js +158 -0
- package/build/src/agents/poppy/errors.d.ts +20 -0
- package/build/src/agents/poppy/errors.js +22 -0
- package/build/src/agents/poppy/replay.d.ts +25 -0
- package/build/src/agents/poppy/replay.js +106 -0
- package/build/src/agents/poppy/resource.d.ts +95 -0
- package/build/src/agents/poppy/resource.js +125 -0
- package/build/src/agents/poppy/safe_fetch.d.ts +48 -0
- package/build/src/agents/poppy/safe_fetch.js +256 -0
- package/build/src/agents/poppy/service.d.ts +280 -0
- package/build/src/agents/poppy/service.js +1452 -0
- package/build/src/agents/poppy/store.d.ts +109 -0
- package/build/src/agents/poppy/store.js +260 -0
- package/build/src/agents/poppy/web_session.d.ts +32 -0
- package/build/src/agents/poppy/web_session.js +104 -0
- package/build/src/agents/protocol.d.ts +8 -4
- package/build/src/agents/runtime.d.ts +17 -1
- package/build/src/agents/runtime.js +138 -13
- package/build/src/define_config.d.ts +24 -0
- package/build/src/define_config.js +14 -0
- package/build/src/host/account_api/account_api_controller.d.ts +12 -1
- package/build/src/host/account_api/account_api_controller.js +10 -5
- package/build/src/host/admin_sessions_service.js +8 -5
- package/build/src/host/auth_host_config.d.ts +2 -1
- package/build/src/host/controllers/account_apps_controller.js +13 -3
- package/build/src/host/controllers/account_security_controller.js +8 -4
- package/build/src/host/controllers/interaction_controller.d.ts +13 -1
- package/build/src/host/controllers/interaction_controller.js +352 -24
- package/build/src/host/controllers/poppy_consent_controller.d.ts +19 -0
- package/build/src/host/controllers/poppy_consent_controller.js +173 -0
- package/build/src/host/controllers/poppy_controller.d.ts +25 -0
- package/build/src/host/controllers/poppy_controller.js +194 -0
- package/build/src/host/csrf.d.ts +7 -0
- package/build/src/host/csrf.js +16 -1
- package/build/src/host/custom_login.d.ts +36 -0
- package/build/src/host/custom_login.js +76 -0
- package/build/src/host/custom_login_completion.d.ts +4 -0
- package/build/src/host/custom_login_completion.js +33 -0
- package/build/src/host/custom_mfa.d.ts +77 -0
- package/build/src/host/custom_mfa.js +268 -0
- package/build/src/host/i18n.d.ts +8 -0
- package/build/src/host/i18n.js +14 -6
- package/build/src/host/idp_session_bridge.d.ts +5 -3
- package/build/src/host/idp_session_bridge.js +5 -3
- package/build/src/host/oidc_rp_guard.d.ts +5 -1
- package/build/src/host/oidc_rp_guard.js +15 -2
- package/build/src/host/persistent_rp_session.d.ts +8 -0
- package/build/src/host/persistent_rp_session.js +74 -0
- package/build/src/host/poppy_session_key.d.ts +9 -0
- package/build/src/host/poppy_session_key.js +9 -0
- package/build/src/host/register_auth_host.js +72 -0
- package/build/src/host/whatsapp_code_sender.d.ts +20 -0
- package/build/src/host/whatsapp_code_sender.js +9 -0
- package/build/src/host/whatsapp_senders/http.d.ts +5 -0
- package/build/src/host/whatsapp_senders/http.js +33 -0
- package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.d.ts +20 -0
- package/build/src/host/whatsapp_senders/meta_whatsapp_code_sender.js +51 -0
- package/build/src/host/whatsapp_senders/whatsmiau_code_sender.d.ts +14 -0
- package/build/src/host/whatsapp_senders/whatsmiau_code_sender.js +36 -0
- package/build/src/provider/oidc_service.d.ts +5 -0
- package/build/src/provider/oidc_service.js +15 -4
- package/build/src/schema/ensure.js +124 -0
- package/build/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
- package/package.json +2 -1
- package/stubs/ui/react/pages/mfa-challenge.tsx +207 -37
|
@@ -8,6 +8,7 @@ import { adminApiGuard } from './admin_api/admin_api_guard.js';
|
|
|
8
8
|
import { normalizeAdminApiPrefix, normalizeAdminPrefix, setAdminApiPrefix, setAdminPrefix, } from './admin_prefix.js';
|
|
9
9
|
import { getAuthHostConfig, markAuthHostAutoMounted, wasAuthHostAutoMounted, } from './auth_host_config.js';
|
|
10
10
|
import { ensureConsoleSession } from './idp_session_bridge.js';
|
|
11
|
+
import { POPPY_WEB_SESSION_KEY } from './poppy_session_key.js';
|
|
11
12
|
import { createAuthThrottles } from './rate_limit.js';
|
|
12
13
|
import { redirectExact } from './redirect_exact.js';
|
|
13
14
|
import { requestPathWithQuery } from './request_url.js';
|
|
@@ -113,7 +114,16 @@ function buildLoginRedirect(ctx, extra) {
|
|
|
113
114
|
* classe NÃO era aplicada em runtime num grupo, deixando /account/tokens e
|
|
114
115
|
* /account/mfa acessíveis sem sessão.
|
|
115
116
|
*/
|
|
117
|
+
/** A sessão é do navegador de um personal agent Poppy (§5)? */
|
|
118
|
+
function isPoppyBrowser(ctx) {
|
|
119
|
+
const marker = ctx.session?.get?.(POPPY_WEB_SESSION_KEY);
|
|
120
|
+
return typeof marker === 'object' && marker !== null && typeof marker.sessionId === 'string';
|
|
121
|
+
}
|
|
116
122
|
const accountGuard = async (ctx, next) => {
|
|
123
|
+
// O navegador de um personal agent Poppy (§5) não entra no console de conta nem
|
|
124
|
+
// nas telas de consentimento: a sessão é do agente, não do usuário.
|
|
125
|
+
if (isPoppyBrowser(ctx))
|
|
126
|
+
return ctx.response.status(403).send('');
|
|
117
127
|
// Sessão do console — ou, com `accountSession.acceptIdpSession`, a do IdP (SSO).
|
|
118
128
|
if (!(await ensureConsoleSession(ctx))) {
|
|
119
129
|
return redirectExact(ctx.response, buildLoginRedirect(ctx));
|
|
@@ -145,6 +155,8 @@ export const adminGuard = async (ctx, next) => {
|
|
|
145
155
|
if (!cfg.admin.enabled) {
|
|
146
156
|
return ctx.response.notFound();
|
|
147
157
|
}
|
|
158
|
+
if (isPoppyBrowser(ctx))
|
|
159
|
+
return ctx.response.status(403).send('');
|
|
148
160
|
// Com `accountSession.acceptIdpSession`, a sessão do IdP também abre o console.
|
|
149
161
|
await ensureConsoleSession(ctx);
|
|
150
162
|
const accountId = ctx.session?.get(ACCOUNT_SESSION_KEY);
|
|
@@ -187,6 +199,8 @@ const C = {
|
|
|
187
199
|
accountConfirm: () => import('./controllers/account_confirm_controller.js'),
|
|
188
200
|
agentOAuth: () => import('./controllers/agent_oauth_controller.js'),
|
|
189
201
|
agentConsent: () => import('./controllers/agent_consent_controller.js'),
|
|
202
|
+
poppy: () => import('./controllers/poppy_controller.js'),
|
|
203
|
+
poppyConsent: () => import('./controllers/poppy_consent_controller.js'),
|
|
190
204
|
webauthnAsset: () => import('./controllers/webauthn_asset_controller.js'),
|
|
191
205
|
logoutAsset: () => import('./controllers/logout_asset_controller.js'),
|
|
192
206
|
passkeyAutofillAsset: () => import('./controllers/passkey_autofill_asset_controller.js'),
|
|
@@ -441,6 +455,18 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
441
455
|
router.post('/auth/interaction/:uid/identifier', [C.interaction, 'identifier']);
|
|
442
456
|
withLogin(router.post('/auth/interaction/:uid/login', [C.interaction, 'login']));
|
|
443
457
|
withLogin(router.post('/auth/interaction/:uid/mfa', [C.interaction, 'mfaVerify']));
|
|
458
|
+
const customMfaBeginRoute = router.post('/auth/interaction/:uid/mfa/custom/:method/begin', [
|
|
459
|
+
C.interaction,
|
|
460
|
+
'customMfaBegin',
|
|
461
|
+
]);
|
|
462
|
+
withLogin(customMfaBeginRoute);
|
|
463
|
+
customMfaBeginRoute.as('authkit.mfa.custom.begin');
|
|
464
|
+
const customMfaVerifyRoute = router.post('/auth/interaction/:uid/mfa/custom/:method/verify', [
|
|
465
|
+
C.interaction,
|
|
466
|
+
'customMfaVerify',
|
|
467
|
+
]);
|
|
468
|
+
withLogin(customMfaVerifyRoute);
|
|
469
|
+
customMfaVerifyRoute.as('authkit.mfa.custom.verify');
|
|
444
470
|
// Troca de senha obrigatória quando a senha expirou (password expiration gate).
|
|
445
471
|
withLogin(router.post('/auth/interaction/:uid/password-expired', [
|
|
446
472
|
C.interaction,
|
|
@@ -496,6 +522,52 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
496
522
|
})
|
|
497
523
|
.use([accountGuard]);
|
|
498
524
|
}
|
|
525
|
+
// Personal Agent Protocol (Poppy): descoberta, authorization server (token,
|
|
526
|
+
// revogação, device authorization), Mediated Sign-In e sessão de navegador —
|
|
527
|
+
// server-to-server ou POST cross-site do navegador do agente (sem CSRF, ver
|
|
528
|
+
// `authkitCsrfExceptions`). As telas do usuário (authorize/device) exigem a
|
|
529
|
+
// sessão de conta e mantêm o CSRF.
|
|
530
|
+
const poppyPrefix = hostCfg?.personalAgents?.poppyPrefix;
|
|
531
|
+
if (poppyPrefix) {
|
|
532
|
+
// Nomes explícitos: o Router nomeia pela dupla controller.método, e as duas URLs da metadata
|
|
533
|
+
// (RFC 8414 na raiz e debaixo do issuer) usam o mesmo — "poppy.metadata already exists" no boot.
|
|
534
|
+
router.get('/.well-known/poppy.json', [C.poppy, 'discovery']).as('authkit.poppy.discovery');
|
|
535
|
+
router
|
|
536
|
+
.get(`/.well-known/oauth-authorization-server${poppyPrefix}`, [C.poppy, 'metadata'])
|
|
537
|
+
.as('authkit.poppy.metadata');
|
|
538
|
+
router
|
|
539
|
+
.get(`${poppyPrefix}/.well-known/oauth-authorization-server`, [C.poppy, 'metadata'])
|
|
540
|
+
.as('authkit.poppy.metadata_under_issuer');
|
|
541
|
+
router.post(`${poppyPrefix}/oauth/token`, [C.poppy, 'token']).as('authkit.poppy.token');
|
|
542
|
+
router.post(`${poppyPrefix}/oauth/revoke`, [C.poppy, 'revoke']).as('authkit.poppy.revoke');
|
|
543
|
+
router
|
|
544
|
+
.post(`${poppyPrefix}/oauth/device`, [C.poppy, 'deviceAuthorization'])
|
|
545
|
+
.as('authkit.poppy.device_authorization');
|
|
546
|
+
// Credenciais e códigos de uso único: throttle por IP (§4.7 exige rate limit).
|
|
547
|
+
withLogin(router.post(`${poppyPrefix}/sign-in`, [C.poppy, 'mediatedStart']).as('authkit.poppy.sign_in'));
|
|
548
|
+
withOtpLogin(router
|
|
549
|
+
.post(`${poppyPrefix}/sign-in/:id`, [C.poppy, 'mediatedCode'])
|
|
550
|
+
.as('authkit.poppy.sign_in_code'));
|
|
551
|
+
router
|
|
552
|
+
.post(`${poppyPrefix}/browser-session`, [C.poppy, 'browserSession'])
|
|
553
|
+
.as('authkit.poppy.browser_session');
|
|
554
|
+
router
|
|
555
|
+
.group(() => {
|
|
556
|
+
router
|
|
557
|
+
.get(`${poppyPrefix}/oauth/authorize`, [C.poppyConsent, 'authorize'])
|
|
558
|
+
.as('authkit.poppy.authorize');
|
|
559
|
+
router
|
|
560
|
+
.post(`${poppyPrefix}/oauth/authorize`, [C.poppyConsent, 'decideAuthorization'])
|
|
561
|
+
.as('authkit.poppy.authorize_decide');
|
|
562
|
+
withOtpLogin(router
|
|
563
|
+
.get(`${poppyPrefix}/device`, [C.poppyConsent, 'device'])
|
|
564
|
+
.as('authkit.poppy.device'));
|
|
565
|
+
withOtpLogin(router
|
|
566
|
+
.post(`${poppyPrefix}/device`, [C.poppyConsent, 'decideDevice'])
|
|
567
|
+
.as('authkit.poppy.device_decide'));
|
|
568
|
+
})
|
|
569
|
+
.use([accountGuard]);
|
|
570
|
+
}
|
|
499
571
|
// Paths do console de conta (configuráveis/localizáveis via `accountRoutes`).
|
|
500
572
|
// As TELAS vêm de `accountPath(key)` (prefixo + segmento configurável); os
|
|
501
573
|
// action-subpaths concatenados (`/password`, `/enroll`, ...) são FIXOS —
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
/** Provider-neutral delivery input. Only the host generates and verifies the OTP. */
|
|
3
|
+
export interface WhatsappCodeInput {
|
|
4
|
+
phone: string;
|
|
5
|
+
code: string;
|
|
6
|
+
locale: string;
|
|
7
|
+
expiresInSeconds: number;
|
|
8
|
+
/** Optional host-localized copy for transports that send free-form messages. */
|
|
9
|
+
text?: string;
|
|
10
|
+
}
|
|
11
|
+
/** Implement this interface to support any WhatsApp provider. Reject on failed delivery. */
|
|
12
|
+
export interface WhatsappCodeSender {
|
|
13
|
+
sendCode(input: WhatsappCodeInput): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
/** A configured instance, or an injectable class resolved by the request container. */
|
|
16
|
+
export type WhatsappCodeSenderConstructor = new (...args: never[]) => WhatsappCodeSender;
|
|
17
|
+
export type WhatsappCodeSenderBinding = WhatsappCodeSender | WhatsappCodeSenderConstructor;
|
|
18
|
+
export declare function resolveWhatsappCodeSender(resolver: Pick<HttpContext['containerResolver'], 'make'>, binding: WhatsappCodeSenderBinding): Promise<WhatsappCodeSender>;
|
|
19
|
+
export { MetaWhatsappCodeSender, type MetaWhatsappCodeSenderOptions, } from './whatsapp_senders/meta_whatsapp_code_sender.js';
|
|
20
|
+
export { WhatsmiauCodeSender, type WhatsmiauCodeSenderOptions, } from './whatsapp_senders/whatsmiau_code_sender.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export async function resolveWhatsappCodeSender(resolver, binding) {
|
|
2
|
+
const sender = typeof binding === 'function' ? await resolver.make(binding) : binding;
|
|
3
|
+
if (!sender || typeof sender.sendCode !== 'function') {
|
|
4
|
+
throw new Error('Invalid WhatsApp code sender: expected sendCode(input)');
|
|
5
|
+
}
|
|
6
|
+
return sender;
|
|
7
|
+
}
|
|
8
|
+
export { MetaWhatsappCodeSender, } from './whatsapp_senders/meta_whatsapp_code_sender.js';
|
|
9
|
+
export { WhatsmiauCodeSender, } from './whatsapp_senders/whatsmiau_code_sender.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Never include configuration values in validation errors. */
|
|
2
|
+
export declare function requireString(value: string, field: string): void;
|
|
3
|
+
export declare function recipientPhone(phone: string): string;
|
|
4
|
+
/** Response bodies and fetch causes can contain credentials and OTPs: discard both. */
|
|
5
|
+
export declare function postJson(provider: string, url: URL, headers: Record<string, string>, body: unknown): Promise<void>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Never include configuration values in validation errors. */
|
|
2
|
+
export function requireString(value, field) {
|
|
3
|
+
if (typeof value !== 'string' || !value.trim() || /[\r\n]/.test(value)) {
|
|
4
|
+
throw new Error(`Invalid WhatsApp sender ${field}`);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function recipientPhone(phone) {
|
|
8
|
+
if (typeof phone !== 'string' || !/^\+?[1-9]\d{6,14}$/.test(phone)) {
|
|
9
|
+
throw new Error('Invalid WhatsApp recipient phone');
|
|
10
|
+
}
|
|
11
|
+
return phone.replace(/^\+/, '');
|
|
12
|
+
}
|
|
13
|
+
/** Response bodies and fetch causes can contain credentials and OTPs: discard both. */
|
|
14
|
+
export async function postJson(provider, url, headers, body) {
|
|
15
|
+
let response;
|
|
16
|
+
try {
|
|
17
|
+
response = await fetch(url, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
20
|
+
body: JSON.stringify(body),
|
|
21
|
+
signal: AbortSignal.timeout(15_000),
|
|
22
|
+
redirect: 'error',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw new Error(`WhatsApp code delivery failed (${provider})`);
|
|
27
|
+
}
|
|
28
|
+
// Do not parse or log the body, even when a provider echoes the submitted code.
|
|
29
|
+
void response.body?.cancel().catch(() => { });
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(`WhatsApp code delivery failed (${provider}, HTTP ${response.status})`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { WhatsappCodeInput, WhatsappCodeSender } from '../whatsapp_code_sender.js';
|
|
2
|
+
export interface MetaWhatsappCodeSenderOptions {
|
|
3
|
+
accessToken: string;
|
|
4
|
+
phoneNumberId: string;
|
|
5
|
+
/** Explicit supported Graph API version, for example v23.0. */
|
|
6
|
+
apiVersion: string;
|
|
7
|
+
/** An approved authentication template with an OTP URL button. */
|
|
8
|
+
templateName: string;
|
|
9
|
+
/** Overrides the host locale when the template uses a different language. */
|
|
10
|
+
languageCode?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Meta Cloud API authentication template transport (copy-code / OTP URL button). */
|
|
13
|
+
export declare class MetaWhatsappCodeSender implements WhatsappCodeSender {
|
|
14
|
+
private readonly accessToken;
|
|
15
|
+
private readonly endpoint;
|
|
16
|
+
private readonly templateName;
|
|
17
|
+
private readonly languageCode?;
|
|
18
|
+
constructor(options: MetaWhatsappCodeSenderOptions);
|
|
19
|
+
sendCode(input: WhatsappCodeInput): Promise<void>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { postJson, recipientPhone, requireString } from './http.js';
|
|
2
|
+
const LANGUAGE_CODES = { 'pt-BR': 'pt_BR', en: 'en_US', es: 'es' };
|
|
3
|
+
/** Meta Cloud API authentication template transport (copy-code / OTP URL button). */
|
|
4
|
+
export class MetaWhatsappCodeSender {
|
|
5
|
+
accessToken;
|
|
6
|
+
endpoint;
|
|
7
|
+
templateName;
|
|
8
|
+
languageCode;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
requireString(options.accessToken, 'accessToken');
|
|
11
|
+
if (typeof options.apiVersion !== 'string' || !/^v\d+\.\d+$/.test(options.apiVersion)) {
|
|
12
|
+
throw new Error('Invalid WhatsApp sender apiVersion');
|
|
13
|
+
}
|
|
14
|
+
if (typeof options.phoneNumberId !== 'string' || !/^\d+$/.test(options.phoneNumberId)) {
|
|
15
|
+
throw new Error('Invalid WhatsApp sender phoneNumberId');
|
|
16
|
+
}
|
|
17
|
+
if (typeof options.templateName !== 'string' || !/^[a-z0-9_]+$/.test(options.templateName)) {
|
|
18
|
+
throw new Error('Invalid WhatsApp sender templateName');
|
|
19
|
+
}
|
|
20
|
+
if (options.languageCode !== undefined) {
|
|
21
|
+
requireString(options.languageCode, 'languageCode');
|
|
22
|
+
}
|
|
23
|
+
this.endpoint = new URL(`https://graph.facebook.com/${options.apiVersion}/${options.phoneNumberId}/messages`);
|
|
24
|
+
this.accessToken = options.accessToken;
|
|
25
|
+
this.templateName = options.templateName;
|
|
26
|
+
this.languageCode = options.languageCode;
|
|
27
|
+
}
|
|
28
|
+
async sendCode(input) {
|
|
29
|
+
await postJson('Meta', this.endpoint, { Authorization: `Bearer ${this.accessToken}` }, {
|
|
30
|
+
messaging_product: 'whatsapp',
|
|
31
|
+
recipient_type: 'individual',
|
|
32
|
+
to: recipientPhone(input.phone),
|
|
33
|
+
type: 'template',
|
|
34
|
+
template: {
|
|
35
|
+
name: this.templateName,
|
|
36
|
+
language: {
|
|
37
|
+
code: this.languageCode ?? LANGUAGE_CODES[input.locale] ?? input.locale.replace(/-/g, '_'),
|
|
38
|
+
},
|
|
39
|
+
components: [
|
|
40
|
+
{ type: 'body', parameters: [{ type: 'text', text: input.code }] },
|
|
41
|
+
{
|
|
42
|
+
type: 'button',
|
|
43
|
+
sub_type: 'url',
|
|
44
|
+
index: '0',
|
|
45
|
+
parameters: [{ type: 'text', text: input.code }],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { WhatsappCodeInput, WhatsappCodeSender } from '../whatsapp_code_sender.js';
|
|
2
|
+
export interface WhatsmiauCodeSenderOptions {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
instanceName: string;
|
|
5
|
+
/** Includes the API version path. Defaults to Whatsmiau Cloud's /v2 endpoint. */
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Whatsmiau Cloud v2 / Evolution-compatible free-form text transport. */
|
|
9
|
+
export declare class WhatsmiauCodeSender implements WhatsappCodeSender {
|
|
10
|
+
private readonly apiKey;
|
|
11
|
+
private readonly endpoint;
|
|
12
|
+
constructor(options: WhatsmiauCodeSenderOptions);
|
|
13
|
+
sendCode(input: WhatsappCodeInput): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { postJson, recipientPhone, requireString } from './http.js';
|
|
2
|
+
/** Whatsmiau Cloud v2 / Evolution-compatible free-form text transport. */
|
|
3
|
+
export class WhatsmiauCodeSender {
|
|
4
|
+
apiKey;
|
|
5
|
+
endpoint;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
requireString(options.apiKey, 'apiKey');
|
|
8
|
+
if (typeof options.instanceName !== 'string' ||
|
|
9
|
+
!/^[A-Za-z0-9_-]+$/.test(options.instanceName)) {
|
|
10
|
+
throw new Error('Invalid WhatsApp sender instanceName');
|
|
11
|
+
}
|
|
12
|
+
let baseUrl;
|
|
13
|
+
try {
|
|
14
|
+
baseUrl = new URL(options.baseUrl ?? 'https://api.whatsmiau.dev/v2');
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new Error('Invalid WhatsApp sender baseUrl');
|
|
18
|
+
}
|
|
19
|
+
if (!['http:', 'https:'].includes(baseUrl.protocol) ||
|
|
20
|
+
baseUrl.username ||
|
|
21
|
+
baseUrl.password ||
|
|
22
|
+
baseUrl.search ||
|
|
23
|
+
baseUrl.hash) {
|
|
24
|
+
throw new Error('Invalid WhatsApp sender baseUrl');
|
|
25
|
+
}
|
|
26
|
+
baseUrl.pathname = `${baseUrl.pathname.replace(/\/+$/, '')}/message/sendText/${options.instanceName}`;
|
|
27
|
+
this.endpoint = baseUrl;
|
|
28
|
+
this.apiKey = options.apiKey;
|
|
29
|
+
}
|
|
30
|
+
async sendCode(input) {
|
|
31
|
+
await postJson('Whatsmiau', this.endpoint, { apikey: this.apiKey }, {
|
|
32
|
+
number: recipientPhone(input.phone),
|
|
33
|
+
text: input.text ?? input.code,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -25,6 +25,11 @@ export declare class OidcService {
|
|
|
25
25
|
get signingJwks(): {
|
|
26
26
|
keys: Record<string, any>[];
|
|
27
27
|
};
|
|
28
|
+
/**
|
|
29
|
+
* @internal Segredo derivado da app key para um uso (`label`) — HMAC dos nonces
|
|
30
|
+
* DPoP do Poppy, por exemplo. Igual em todas as instâncias, sem expor a chave.
|
|
31
|
+
*/
|
|
32
|
+
deriveSecret(label: string): Buffer;
|
|
28
33
|
/** Pathname do issuer sem barra final (ex.: `/oidc`). Vazio quando montado na raiz. */
|
|
29
34
|
readonly mountPath: string;
|
|
30
35
|
readonly recorder: MetricsRecorder;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { timingSafeEqual } from 'node:crypto';
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import { NoopRecorder } from '@adonis-agora/authkit-core';
|
|
3
3
|
import Koa from 'koa';
|
|
4
4
|
import mount from 'koa-mount';
|
|
@@ -41,6 +41,13 @@ export class OidcService {
|
|
|
41
41
|
get signingJwks() {
|
|
42
42
|
return this.#signingJwks;
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* @internal Segredo derivado da app key para um uso (`label`) — HMAC dos nonces
|
|
46
|
+
* DPoP do Poppy, por exemplo. Igual em todas as instâncias, sem expor a chave.
|
|
47
|
+
*/
|
|
48
|
+
deriveSecret(label) {
|
|
49
|
+
return createHmac('sha256', this.#appKey).update(`authkit:${label}`).digest();
|
|
50
|
+
}
|
|
44
51
|
/** Pathname do issuer sem barra final (ex.: `/oidc`). Vazio quando montado na raiz. */
|
|
45
52
|
mountPath;
|
|
46
53
|
recorder;
|
|
@@ -137,8 +144,12 @@ export class OidcService {
|
|
|
137
144
|
claims: async (_use, _scope) => {
|
|
138
145
|
const base = {
|
|
139
146
|
sub: user.id,
|
|
140
|
-
|
|
141
|
-
|
|
147
|
+
...(user.email
|
|
148
|
+
? {
|
|
149
|
+
email: user.email,
|
|
150
|
+
email_verified: (await config.accountStore.isEmailVerified?.(user.id)) ?? false,
|
|
151
|
+
}
|
|
152
|
+
: {}),
|
|
142
153
|
name: user.name,
|
|
143
154
|
picture: user.avatarUrl,
|
|
144
155
|
};
|
|
@@ -321,7 +332,7 @@ export class OidcService {
|
|
|
321
332
|
/** Verifica client_id + client_secret contra os clients da config (p/ endpoints custom como introspecção de PAT). */
|
|
322
333
|
verifyClientCredentials(clientId, clientSecret) {
|
|
323
334
|
const client = this.#clients.find((c) => c.clientId === clientId);
|
|
324
|
-
if (!client
|
|
335
|
+
if (!client?.clientSecret)
|
|
325
336
|
return false;
|
|
326
337
|
const a = Buffer.from(client.clientSecret);
|
|
327
338
|
const b = Buffer.from(clientSecret);
|
|
@@ -295,6 +295,130 @@ const TABLES = [
|
|
|
295
295
|
created_at: (t) => t.dateTime('created_at', { useTz: true, precision: 3 }).nullable(),
|
|
296
296
|
},
|
|
297
297
|
},
|
|
298
|
+
/*
|
|
299
|
+
* Personal Agent Protocol (Poppy). Os Account Tokens são grants de
|
|
300
|
+
* `auth_agent_grants` (prefixo `pgrant_`) com o hash em
|
|
301
|
+
* `auth_agent_refresh_tokens`; aqui ficam Sessions, Session Tokens, pedidos de
|
|
302
|
+
* sign-in e os `jti` vistos. Mesmas regras de data das tabelas acima.
|
|
303
|
+
*/
|
|
304
|
+
{
|
|
305
|
+
name: 'auth_poppy_sessions',
|
|
306
|
+
/**
|
|
307
|
+
* Uma Session: um usuário do agente (`user_id`, opaco) num agente
|
|
308
|
+
* (`client_id`). `account_id` é a conta a que ficou PRESA ao logar (não troca);
|
|
309
|
+
* `grant_id` o Account Token com que está logada agora (`null` = deslogada).
|
|
310
|
+
*/
|
|
311
|
+
create: (t) => {
|
|
312
|
+
t.string('id').primary();
|
|
313
|
+
t.string('client_id', 2048).notNullable();
|
|
314
|
+
t.string('user_id').notNullable();
|
|
315
|
+
t.string('account_id').nullable().index();
|
|
316
|
+
t.string('grant_id').nullable().index();
|
|
317
|
+
t.text('scope').notNullable();
|
|
318
|
+
t.dateTime('expires_at', { useTz: true, precision: 3 }).notNullable();
|
|
319
|
+
t.dateTime('ended_at', { useTz: true, precision: 3 }).nullable();
|
|
320
|
+
t.dateTime('created_at', { useTz: true, precision: 3 }).notNullable();
|
|
321
|
+
t.dateTime('updated_at', { useTz: true, precision: 3 }).notNullable();
|
|
322
|
+
},
|
|
323
|
+
columns: {
|
|
324
|
+
client_id: (t) => t.string('client_id', 2048),
|
|
325
|
+
user_id: (t) => t.string('user_id'),
|
|
326
|
+
account_id: (t) => t.string('account_id').nullable().index(),
|
|
327
|
+
grant_id: (t) => t.string('grant_id').nullable().index(),
|
|
328
|
+
scope: (t) => t.text('scope'),
|
|
329
|
+
expires_at: (t) => t.dateTime('expires_at', { useTz: true, precision: 3 }).nullable(),
|
|
330
|
+
ended_at: (t) => t.dateTime('ended_at', { useTz: true, precision: 3 }).nullable(),
|
|
331
|
+
created_at: (t) => t.dateTime('created_at', { useTz: true, precision: 3 }).nullable(),
|
|
332
|
+
updated_at: (t) => t.dateTime('updated_at', { useTz: true, precision: 3 }).nullable(),
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: 'auth_poppy_tokens',
|
|
337
|
+
/** Session Tokens — só o hash. `jkt` = chave DPoP; `null` = Bearer (MCP). */
|
|
338
|
+
create: (t) => {
|
|
339
|
+
t.string('token_hash', 64).primary();
|
|
340
|
+
t.string('session_id').notNullable().index();
|
|
341
|
+
t.string('client_id', 2048).notNullable();
|
|
342
|
+
t.string('user_id').notNullable();
|
|
343
|
+
t.string('account_id').nullable();
|
|
344
|
+
t.string('grant_id').nullable().index();
|
|
345
|
+
t.text('scope').notNullable();
|
|
346
|
+
t.string('resource', 2048).nullable();
|
|
347
|
+
t.string('jkt', 64).nullable();
|
|
348
|
+
t.dateTime('expires_at', { useTz: true, precision: 3 }).notNullable().index();
|
|
349
|
+
t.dateTime('created_at', { useTz: true, precision: 3 }).notNullable();
|
|
350
|
+
},
|
|
351
|
+
columns: {
|
|
352
|
+
session_id: (t) => t.string('session_id').index(),
|
|
353
|
+
client_id: (t) => t.string('client_id', 2048),
|
|
354
|
+
user_id: (t) => t.string('user_id'),
|
|
355
|
+
account_id: (t) => t.string('account_id').nullable(),
|
|
356
|
+
grant_id: (t) => t.string('grant_id').nullable().index(),
|
|
357
|
+
scope: (t) => t.text('scope'),
|
|
358
|
+
resource: (t) => t.string('resource', 2048).nullable(),
|
|
359
|
+
jkt: (t) => t.string('jkt', 64).nullable(),
|
|
360
|
+
expires_at: (t) => t.dateTime('expires_at', { useTz: true, precision: 3 }).nullable(),
|
|
361
|
+
created_at: (t) => t.dateTime('created_at', { useTz: true, precision: 3 }).nullable(),
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
name: 'auth_poppy_requests',
|
|
366
|
+
/**
|
|
367
|
+
* Pedidos de sign-in: `authorize` (Direct — vira código após o consentimento),
|
|
368
|
+
* `device` (RFC 8628) e `mediated` (§4.7, aguardando o código de uso único).
|
|
369
|
+
* Códigos só como hash. Linhas expiradas são apagadas no próximo pedido.
|
|
370
|
+
*/
|
|
371
|
+
create: (t) => {
|
|
372
|
+
t.string('id').primary();
|
|
373
|
+
t.string('kind', 16).notNullable();
|
|
374
|
+
t.string('code_hash', 64).nullable().unique();
|
|
375
|
+
t.string('user_code', 16).nullable().unique();
|
|
376
|
+
t.string('client_id', 2048).notNullable();
|
|
377
|
+
t.string('session_id').nullable();
|
|
378
|
+
t.string('account_id').nullable();
|
|
379
|
+
t.text('requested_scope').notNullable();
|
|
380
|
+
t.text('granted_scope').nullable();
|
|
381
|
+
t.string('redirect_uri', 2048).nullable();
|
|
382
|
+
t.string('code_challenge', 128).nullable();
|
|
383
|
+
t.string('status', 16).notNullable();
|
|
384
|
+
t.integer('attempts').notNullable().defaultTo(0);
|
|
385
|
+
t.integer('interval_seconds').nullable();
|
|
386
|
+
t.dateTime('last_polled_at', { useTz: true, precision: 3 }).nullable();
|
|
387
|
+
t.text('data').nullable();
|
|
388
|
+
t.dateTime('expires_at', { useTz: true, precision: 3 }).notNullable().index();
|
|
389
|
+
t.dateTime('created_at', { useTz: true, precision: 3 }).notNullable();
|
|
390
|
+
},
|
|
391
|
+
columns: {
|
|
392
|
+
kind: (t) => t.string('kind', 16),
|
|
393
|
+
code_hash: (t) => t.string('code_hash', 64).nullable(),
|
|
394
|
+
user_code: (t) => t.string('user_code', 16).nullable(),
|
|
395
|
+
client_id: (t) => t.string('client_id', 2048),
|
|
396
|
+
session_id: (t) => t.string('session_id').nullable(),
|
|
397
|
+
account_id: (t) => t.string('account_id').nullable(),
|
|
398
|
+
requested_scope: (t) => t.text('requested_scope'),
|
|
399
|
+
granted_scope: (t) => t.text('granted_scope').nullable(),
|
|
400
|
+
redirect_uri: (t) => t.string('redirect_uri', 2048).nullable(),
|
|
401
|
+
code_challenge: (t) => t.string('code_challenge', 128).nullable(),
|
|
402
|
+
status: (t) => t.string('status', 16),
|
|
403
|
+
attempts: (t) => t.integer('attempts').defaultTo(0),
|
|
404
|
+
interval_seconds: (t) => t.integer('interval_seconds').nullable(),
|
|
405
|
+
last_polled_at: (t) => t.dateTime('last_polled_at', { useTz: true, precision: 3 }).nullable(),
|
|
406
|
+
data: (t) => t.text('data').nullable(),
|
|
407
|
+
expires_at: (t) => t.dateTime('expires_at', { useTz: true, precision: 3 }).nullable(),
|
|
408
|
+
created_at: (t) => t.dateTime('created_at', { useTz: true, precision: 3 }).nullable(),
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: 'auth_poppy_jtis',
|
|
413
|
+
/** `jti` já aceitos (asserções, provas DPoP), até expirarem — anti-replay entre instâncias. */
|
|
414
|
+
create: (t) => {
|
|
415
|
+
t.string('key', 100).primary();
|
|
416
|
+
t.dateTime('expires_at', { useTz: true, precision: 3 }).notNullable().index();
|
|
417
|
+
},
|
|
418
|
+
columns: {
|
|
419
|
+
expires_at: (t) => t.dateTime('expires_at', { useTz: true, precision: 3 }).nullable(),
|
|
420
|
+
},
|
|
421
|
+
},
|
|
298
422
|
];
|
|
299
423
|
/**
|
|
300
424
|
* Probe searchPath-aware: `schema.hasTable` no Postgres ignora o
|