@adonis-agora/authkit-server 1.0.0-rc.0 → 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.
- package/CHANGELOG.md +111 -0
- package/README.md +4 -5
- package/build/commands/agora_doctor.d.ts +22 -0
- package/build/commands/agora_doctor.js +109 -0
- package/build/commands/clients_create.d.ts +4 -0
- package/build/commands/clients_create.js +97 -0
- package/build/commands/commands.json +78 -0
- package/build/commands/doctor.js +8 -74
- package/build/host/views/account/apps.edge +1 -5
- package/build/host/views/account/extension.edge +77 -0
- package/build/host/views/account/mfa.edge +2 -0
- package/build/host/views/account/partials/extension_actions.edge +14 -0
- package/build/host/views/account/partials/extension_block.edge +197 -0
- package/build/host/views/account/partials/extension_value.edge +8 -0
- package/build/host/views/account/partials/nav.edge +14 -0
- package/build/host/views/account/security.edge +1 -5
- package/build/host/views/account/tokens.edge +2 -2
- package/build/host/views/agents/consent.edge +11 -2
- package/build/host/views/agents/done.edge +1 -1
- package/build/host/views/partials/styles.edge +1 -1
- package/build/index.d.ts +14 -12
- package/build/index.js +8 -9
- package/build/providers/authkit_server_provider.js +36 -8
- package/build/src/account/extension.d.ts +10 -0
- package/build/src/account/extension.js +9 -0
- package/build/src/account/http.d.ts +77 -0
- package/build/src/account/http.js +381 -0
- package/build/src/account/mount.d.ts +40 -0
- package/build/src/account/mount.js +171 -0
- package/build/src/account/registry.d.ts +41 -0
- package/build/src/account/registry.js +81 -0
- package/build/src/account/remote.d.ts +72 -0
- package/build/src/account/remote.js +406 -0
- package/build/src/account/runtime.d.ts +289 -0
- package/build/src/account/runtime.js +641 -0
- package/build/src/accounts/account_store.d.ts +6 -0
- package/build/src/accounts/lucid_store/organizations.js +6 -0
- package/build/src/agents/config.d.ts +9 -13
- package/build/src/agents/config.js +17 -11
- package/build/src/agents/delegation_service.js +3 -3
- package/build/src/agents/delegation_store.d.ts +3 -2
- package/build/src/agents/delegation_store.js +6 -5
- package/build/src/agents/extension.d.ts +261 -0
- package/build/src/agents/extension.js +40 -0
- package/build/src/agents/middleware.js +1 -1
- package/build/src/agents/protocol.d.ts +4 -4
- package/build/src/agents/registry_store.d.ts +3 -2
- package/build/src/agents/registry_store.js +1 -1
- package/build/src/agents/runtime.d.ts +26 -7
- package/build/src/agents/runtime.js +110 -75
- package/build/src/audit/audit_sink.d.ts +1 -1
- package/build/src/audit/audit_sink.js +10 -1
- package/build/src/console_authorize.d.ts +56 -0
- package/build/src/console_authorize.js +85 -0
- package/build/src/console_switcher.d.ts +132 -0
- package/build/src/console_switcher.js +206 -0
- package/build/src/define_config.d.ts +54 -14
- package/build/src/define_config.js +17 -8
- package/build/src/dev_tools/contract.d.ts +72 -0
- package/build/src/dev_tools/contract.js +10 -0
- package/build/src/dev_tools/gate.d.ts +17 -0
- package/build/src/dev_tools/gate.js +69 -0
- package/build/src/dev_tools/register.d.ts +11 -0
- package/build/src/dev_tools/register.js +25 -0
- package/build/src/dev_tools/tools.d.ts +24 -0
- package/build/src/dev_tools/tools.js +384 -0
- package/build/src/doctor/agora_checks.d.ts +15 -0
- package/build/src/doctor/agora_checks.js +150 -0
- package/build/src/doctor/agora_doctor.d.ts +115 -0
- package/build/src/doctor/agora_doctor.js +692 -0
- package/build/src/doctor/checks.d.ts +7 -1
- package/build/src/doctor/checks.js +47 -1
- package/build/src/doctor/doctor_input.d.ts +27 -0
- package/build/src/doctor/doctor_input.js +86 -0
- package/build/src/doctor/index.d.ts +5 -0
- package/build/src/doctor/index.js +4 -0
- package/build/src/host/access_token_verifier.js +7 -0
- package/build/src/host/account_api/account_api_controller.js +8 -6
- package/build/src/host/account_session_sign_in.d.ts +12 -0
- package/build/src/host/account_session_sign_in.js +19 -0
- package/build/src/host/admin_api/admin_users_service.d.ts +5 -2
- package/build/src/host/admin_api/api_clients_controller.d.ts +37 -0
- package/build/src/host/admin_api/api_clients_controller.js +31 -1
- package/build/src/host/admin_api/dto.d.ts +3 -1
- package/build/src/host/admin_api/dto.js +2 -0
- package/build/src/host/admin_authorize.d.ts +8 -0
- package/build/src/host/admin_authorize.js +25 -0
- package/build/src/host/admin_clients_service.d.ts +20 -0
- package/build/src/host/admin_clients_service.js +65 -1
- package/build/src/host/admin_console/admin_console_entry.d.ts +14 -0
- package/build/src/host/admin_console/admin_console_entry.js +80 -0
- package/build/src/host/admin_console/admin_shell_controller.js +8 -0
- package/build/src/host/admin_console/console_clients_controller.d.ts +41 -0
- package/build/src/host/admin_console/console_clients_controller.js +34 -1
- package/build/src/host/admin_console/console_personal_agents_controller.d.ts +1 -6
- package/build/src/host/admin_console/console_personal_agents_controller.js +50 -30
- package/build/src/host/admin_console/console_switcher_controller.d.ts +11 -0
- package/build/src/host/admin_console/console_switcher_controller.js +23 -0
- package/build/src/host/admin_console_warning.d.ts +8 -0
- package/build/src/host/admin_console_warning.js +18 -0
- package/build/src/host/admin_sessions_service.js +8 -5
- package/build/src/host/admin_validators.d.ts +29 -0
- package/build/src/host/admin_validators.js +30 -0
- package/build/src/host/auth_host_config.d.ts +14 -2
- package/build/src/host/console_denied_page.d.ts +6 -0
- package/build/src/host/console_denied_page.js +18 -0
- package/build/src/host/console_sso.d.ts +24 -0
- package/build/src/host/console_sso.js +70 -0
- package/build/src/host/controllers/account_apps_controller.js +15 -11
- package/build/src/host/controllers/account_session_controller.js +10 -1
- package/build/src/host/controllers/console_sso_controller.d.ts +12 -0
- package/build/src/host/controllers/console_sso_controller.js +61 -0
- package/build/src/host/controllers/interaction_controller.js +3 -1
- package/build/src/host/csrf.d.ts +3 -3
- package/build/src/host/csrf.js +3 -13
- package/build/src/host/default_mailer.d.ts +11 -6
- package/build/src/host/default_mailer.js +16 -12
- package/build/src/host/i18n.d.ts +41 -14
- package/build/src/host/i18n.js +55 -13
- package/build/src/host/rate_limit.d.ts +8 -0
- package/build/src/host/rate_limit.js +9 -0
- package/build/src/host/register_auth_host.d.ts +16 -0
- package/build/src/host/register_auth_host.js +230 -88
- package/build/src/host/renderers/edge_renderer.js +5 -0
- package/build/src/host/renderers/inertia_renderer.js +7 -1
- package/build/src/host/screen_props.d.ts +39 -8
- package/build/src/host/ui-dist/assets/index-Dq3dJVBv.js +67 -0
- package/build/src/host/ui-dist/assets/{index-DTSmD4RU.css → index-WNIyCEPA.css} +1 -1
- package/build/src/host/ui-dist/index.html +2 -2
- package/build/src/provider/build_provider.js +43 -6
- package/build/src/provider/oidc_service.d.ts +1 -1
- package/build/src/provider/oidc_service.js +1 -1
- package/build/src/schema/ensure.d.ts +14 -0
- package/build/src/schema/ensure.js +43 -150
- package/build/src/service_tenants/client_metadata.d.ts +39 -0
- package/build/src/service_tenants/client_metadata.js +80 -0
- package/build/src/service_tenants/config.d.ts +44 -0
- package/build/src/service_tenants/config.js +44 -0
- package/build/src/service_tenants/provider_hooks.d.ts +52 -0
- package/build/src/service_tenants/provider_hooks.js +124 -0
- package/build/src/service_tenants/tenant_verifier.d.ts +73 -0
- package/build/src/service_tenants/tenant_verifier.js +104 -0
- package/build/src/tenancy/tenant_labels.d.ts +30 -0
- package/build/src/tenancy/tenant_labels.js +64 -0
- package/build/types.d.ts +3 -1
- package/package.json +12 -9
- package/build/src/agents/poppy/client_registry.d.ts +0 -50
- package/build/src/agents/poppy/client_registry.js +0 -260
- package/build/src/agents/poppy/config.d.ts +0 -396
- package/build/src/agents/poppy/config.js +0 -228
- package/build/src/agents/poppy/dpop.d.ts +0 -61
- package/build/src/agents/poppy/dpop.js +0 -158
- package/build/src/agents/poppy/email_code_sign_in.d.ts +0 -27
- package/build/src/agents/poppy/email_code_sign_in.js +0 -140
- package/build/src/agents/poppy/errors.d.ts +0 -25
- package/build/src/agents/poppy/errors.js +0 -25
- package/build/src/agents/poppy/replay.d.ts +0 -25
- package/build/src/agents/poppy/replay.js +0 -106
- package/build/src/agents/poppy/resource.d.ts +0 -100
- package/build/src/agents/poppy/resource.js +0 -129
- package/build/src/agents/poppy/safe_fetch.d.ts +0 -48
- package/build/src/agents/poppy/safe_fetch.js +0 -262
- package/build/src/agents/poppy/service.d.ts +0 -293
- package/build/src/agents/poppy/service.js +0 -1458
- package/build/src/agents/poppy/session_jwt.d.ts +0 -30
- package/build/src/agents/poppy/session_jwt.js +0 -133
- package/build/src/agents/poppy/store.d.ts +0 -106
- package/build/src/agents/poppy/store.js +0 -261
- package/build/src/agents/poppy/web_session.d.ts +0 -39
- package/build/src/agents/poppy/web_session.js +0 -122
- package/build/src/host/controllers/poppy_consent_controller.d.ts +0 -19
- package/build/src/host/controllers/poppy_consent_controller.js +0 -199
- package/build/src/host/controllers/poppy_controller.d.ts +0 -33
- package/build/src/host/controllers/poppy_controller.js +0 -219
- package/build/src/host/poppy_session_key.d.ts +0 -9
- package/build/src/host/poppy_session_key.js +0 -9
- package/build/src/host/ui-dist/assets/index-D0EIeyKK.js +0 -67
|
@@ -17,6 +17,19 @@ export declare const clientInputValidator: import("@vinejs/vine").VineValidator<
|
|
|
17
17
|
backchannelLogoutSessionRequired: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine").VineBoolean>;
|
|
18
18
|
clientName: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine/schema/base/literal").NullableModifier<import("@vinejs/vine").VineString>>;
|
|
19
19
|
logoUri: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine/schema/base/literal").NullableModifier<import("@vinejs/vine").VineString>>;
|
|
20
|
+
serviceTenant: import("@vinejs/vine").OptionalModifier<import("@vinejs/vine").NullableModifier<import("@vinejs/vine").VineObject<{
|
|
21
|
+
partition: import("@vinejs/vine").VineString;
|
|
22
|
+
capabilities: import("@vinejs/vine").OptionalModifier<import("@vinejs/vine").VineArray<import("@vinejs/vine").VineString>>;
|
|
23
|
+
}, {
|
|
24
|
+
capabilities?: string[] | null | undefined;
|
|
25
|
+
partition: string;
|
|
26
|
+
}, {
|
|
27
|
+
capabilities?: string[] | undefined;
|
|
28
|
+
partition: string;
|
|
29
|
+
}, {
|
|
30
|
+
capabilities?: string[] | undefined;
|
|
31
|
+
partition: string;
|
|
32
|
+
}>>>;
|
|
20
33
|
}, {
|
|
21
34
|
applicationType?: "native" | "web" | null | undefined;
|
|
22
35
|
backchannelLogoutSessionRequired?: string | number | boolean | null | undefined;
|
|
@@ -27,6 +40,10 @@ export declare const clientInputValidator: import("@vinejs/vine").VineValidator<
|
|
|
27
40
|
logoUri?: string | null | undefined;
|
|
28
41
|
postLogoutRedirectUris?: string[] | null | undefined;
|
|
29
42
|
redirectUris?: string[] | null | undefined;
|
|
43
|
+
serviceTenant?: {
|
|
44
|
+
capabilities?: string[] | null | undefined;
|
|
45
|
+
partition: string;
|
|
46
|
+
} | null | undefined;
|
|
30
47
|
tokenEndpointAuthMethod?: "client_secret_basic" | "client_secret_post" | "none" | null | undefined;
|
|
31
48
|
}, {
|
|
32
49
|
applicationType?: "native" | "web" | undefined;
|
|
@@ -38,6 +55,10 @@ export declare const clientInputValidator: import("@vinejs/vine").VineValidator<
|
|
|
38
55
|
logoUri?: string | null | undefined;
|
|
39
56
|
postLogoutRedirectUris?: string[] | undefined;
|
|
40
57
|
redirectUris?: string[] | undefined;
|
|
58
|
+
serviceTenant?: {
|
|
59
|
+
capabilities?: string[] | undefined;
|
|
60
|
+
partition: string;
|
|
61
|
+
} | null | undefined;
|
|
41
62
|
tokenEndpointAuthMethod?: "client_secret_basic" | "client_secret_post" | "none" | undefined;
|
|
42
63
|
}, {
|
|
43
64
|
applicationType?: "native" | "web" | undefined;
|
|
@@ -49,6 +70,10 @@ export declare const clientInputValidator: import("@vinejs/vine").VineValidator<
|
|
|
49
70
|
logoUri?: string | null | undefined;
|
|
50
71
|
postLogoutRedirectUris?: string[] | undefined;
|
|
51
72
|
redirectUris?: string[] | undefined;
|
|
73
|
+
serviceTenant?: {
|
|
74
|
+
capabilities?: string[] | undefined;
|
|
75
|
+
partition: string;
|
|
76
|
+
} | null | undefined;
|
|
52
77
|
tokenEndpointAuthMethod?: "client_secret_basic" | "client_secret_post" | "none" | undefined;
|
|
53
78
|
}>, Record<string, any> | undefined>;
|
|
54
79
|
/** Forma validada de um client (saída do {@link clientInputValidator}). */
|
|
@@ -63,6 +88,10 @@ export type ClientInputFields = {
|
|
|
63
88
|
backchannelLogoutSessionRequired?: boolean;
|
|
64
89
|
clientName?: string | null;
|
|
65
90
|
logoUri?: string | null;
|
|
91
|
+
serviceTenant?: {
|
|
92
|
+
partition: string;
|
|
93
|
+
capabilities?: string[];
|
|
94
|
+
} | null;
|
|
66
95
|
};
|
|
67
96
|
/**
|
|
68
97
|
* Mapeia o input validado para um {@link ClientInput} COMPLETO (create), com defaults.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import vine from '@vinejs/vine';
|
|
2
|
+
import { CAPABILITY_PATTERN, MAX_CAPABILITIES, PARTITION_PATTERN, } from '../service_tenants/client_metadata.js';
|
|
2
3
|
import { APPLICATION_TYPES, redirectUriProblem } from './client_metadata.js';
|
|
3
4
|
import { normalizeEmailIdentifier } from './email_identifier.js';
|
|
4
5
|
/**
|
|
@@ -103,6 +104,18 @@ export const clientInputValidator = vine.compile(vine.object({
|
|
|
103
104
|
// `null` (update) removes them.
|
|
104
105
|
clientName: vine.string().trim().maxLength(200).nullable().optional(),
|
|
105
106
|
logoUri: vine.string().trim().url(URL_OPTS).nullable().optional(),
|
|
107
|
+
// Service tenant (`client_credentials` + partition claim). `null` (update) turns it
|
|
108
|
+
// back into a regular client.
|
|
109
|
+
serviceTenant: vine
|
|
110
|
+
.object({
|
|
111
|
+
partition: vine.string().trim().regex(PARTITION_PATTERN),
|
|
112
|
+
capabilities: vine
|
|
113
|
+
.array(vine.string().trim().regex(CAPABILITY_PATTERN))
|
|
114
|
+
.maxLength(MAX_CAPABILITIES)
|
|
115
|
+
.optional(),
|
|
116
|
+
})
|
|
117
|
+
.nullable()
|
|
118
|
+
.optional(),
|
|
106
119
|
}));
|
|
107
120
|
/**
|
|
108
121
|
* Mapeia o input validado para um {@link ClientInput} COMPLETO (create), com defaults.
|
|
@@ -111,6 +124,21 @@ export const clientInputValidator = vine.compile(vine.object({
|
|
|
111
124
|
*/
|
|
112
125
|
export function clientCreateInput(v) {
|
|
113
126
|
const applicationType = v.applicationType ?? 'web';
|
|
127
|
+
// A service tenant only uses client_credentials: that is its default grant.
|
|
128
|
+
if (v.serviceTenant) {
|
|
129
|
+
return {
|
|
130
|
+
clientId: v.clientId?.trim() || undefined,
|
|
131
|
+
applicationType,
|
|
132
|
+
redirectUris: v.redirectUris ?? [],
|
|
133
|
+
postLogoutRedirectUris: v.postLogoutRedirectUris ?? [],
|
|
134
|
+
grantTypes: v.grantTypes ?? ['client_credentials'],
|
|
135
|
+
tokenEndpointAuthMethod: v.tokenEndpointAuthMethod ?? 'client_secret_basic',
|
|
136
|
+
backchannelLogoutUri: v.backchannelLogoutUri || undefined,
|
|
137
|
+
clientName: v.clientName || undefined,
|
|
138
|
+
logoUri: v.logoUri || undefined,
|
|
139
|
+
serviceTenant: v.serviceTenant,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
114
142
|
return {
|
|
115
143
|
clientId: v.clientId?.trim() || undefined,
|
|
116
144
|
applicationType,
|
|
@@ -148,6 +176,8 @@ export function clientPartialInput(v) {
|
|
|
148
176
|
out.clientName = v.clientName?.trim() || null;
|
|
149
177
|
if (v.logoUri !== undefined)
|
|
150
178
|
out.logoUri = v.logoUri?.trim() || null;
|
|
179
|
+
if (v.serviceTenant !== undefined)
|
|
180
|
+
out.serviceTenant = v.serviceTenant;
|
|
151
181
|
return out;
|
|
152
182
|
}
|
|
153
183
|
// ─── Usuários ───────────────────────────────────────────────────────────────
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
import type { AccountConsoleConfig } from '../account/runtime.js';
|
|
3
|
+
import type { ResolvedPersonalAgentExtension } from '../agents/extension.js';
|
|
2
4
|
import type { AuthSocialConfig, ResolvedRateLimitConfig } from '../define_config.js';
|
|
3
5
|
import type { PolicyRouteOption } from './config_locks.js';
|
|
4
6
|
import type { AuthHostOptions } from './register_auth_host.js';
|
|
@@ -35,11 +37,21 @@ export interface AuthHostRuntimeConfig {
|
|
|
35
37
|
* Ver `deriveLockedRouteOptions`.
|
|
36
38
|
*/
|
|
37
39
|
lockedRouteOptions?: PolicyRouteOption[];
|
|
38
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Personal agents ligados → prefixo das rotas do PACT (`/agents`) e as
|
|
42
|
+
* `personalAgents.extensions` resolvidas (rotas, isenções de CSRF, navegador do agente).
|
|
43
|
+
*/
|
|
39
44
|
personalAgents?: {
|
|
40
45
|
prefix: string;
|
|
41
|
-
|
|
46
|
+
extensions?: readonly ResolvedPersonalAgentExtension[];
|
|
42
47
|
};
|
|
48
|
+
/**
|
|
49
|
+
* `accountSession.acceptIdpSession: false` com `authMethods: { password: false }`:
|
|
50
|
+
* o console só entra por um `accountLoginUrl` do host (`registerAuthHost` recusa sem ele).
|
|
51
|
+
*/
|
|
52
|
+
consoleNeedsLoginUrl?: boolean;
|
|
53
|
+
/** `accountConsole` do config: os overrides das páginas de extension. */
|
|
54
|
+
accountConsole?: AccountConsoleConfig;
|
|
43
55
|
/**
|
|
44
56
|
* API headless (Clerk-style). Presente quando o host a declarou — o provider o
|
|
45
57
|
* stash no boot para `registerAuthHost` montar as rotas sem reler o config inteiro.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page a browser gets when `admin.authorize` denies the admin console
|
|
3
|
+
* (403), or asks for a sign-in it cannot do itself (401). Self-contained: no
|
|
4
|
+
* script and no external asset, so any CSP renders it.
|
|
5
|
+
*/
|
|
6
|
+
export declare function consoleDeniedPage(status: 401 | 403): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page a browser gets when `admin.authorize` denies the admin console
|
|
3
|
+
* (403), or asks for a sign-in it cannot do itself (401). Self-contained: no
|
|
4
|
+
* script and no external asset, so any CSP renders it.
|
|
5
|
+
*/
|
|
6
|
+
export function consoleDeniedPage(status) {
|
|
7
|
+
const title = status === 401 ? 'Sign in required' : 'Access denied';
|
|
8
|
+
const body = status === 401
|
|
9
|
+
? 'This console needs credentials this request did not carry.'
|
|
10
|
+
: 'You do not have access to this console.';
|
|
11
|
+
return `<!doctype html>
|
|
12
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
13
|
+
<title>${title}</title>
|
|
14
|
+
<style>body{font-family:system-ui,sans-serif;background:#f9fafb;color:#111827;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;padding:16px}
|
|
15
|
+
.box{max-width:420px;text-align:center}h1{font-size:20px;margin:0 0 8px}p{color:#4b5563;font-size:14px;line-height:1.6;margin:0}
|
|
16
|
+
.code{font-size:12px;color:#9ca3af;margin-bottom:8px}</style>
|
|
17
|
+
</head><body><main class="box"><p class="code">${status}</p><h1>${title}</h1><p>${body}</p></main></body></html>`;
|
|
18
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** `client_id` do client interno do console. Reservado: não pode ser cadastrado. */
|
|
2
|
+
export declare const CONSOLE_SSO_CLIENT_ID = "authkit-console";
|
|
3
|
+
/** Rota que inicia o login do console pelo IdP. */
|
|
4
|
+
export declare const CONSOLE_SSO_START_PATH = "/auth/console/sso";
|
|
5
|
+
/** `redirect_uri` do client interno: valida o `state` e abre o console. */
|
|
6
|
+
export declare const CONSOLE_SSO_CALLBACK_PATH = "/auth/console/callback";
|
|
7
|
+
/** Chave da sessão Adonis com o `state` e o destino de um login em andamento. */
|
|
8
|
+
export declare const CONSOLE_SSO_STATE_KEY = "authkit_console_sso";
|
|
9
|
+
/** O que fica na sessão entre o início e o callback. */
|
|
10
|
+
export interface ConsoleSsoState {
|
|
11
|
+
state: string;
|
|
12
|
+
returnTo: string | null;
|
|
13
|
+
}
|
|
14
|
+
/** Metadata do client interno para o `clients` estático do oidc-provider. */
|
|
15
|
+
export declare function consoleSsoClientMetadata(issuer: string): Record<string, unknown>;
|
|
16
|
+
/** Novo `state` opaco para um login do console. */
|
|
17
|
+
export declare function newConsoleSsoState(): string;
|
|
18
|
+
/**
|
|
19
|
+
* Path do authorize do provider. O issuer termina no `mountPath` (garantido por
|
|
20
|
+
* `registerAuthHost`), então o mount é o path do issuer.
|
|
21
|
+
*/
|
|
22
|
+
export declare function authorizationPath(issuer: string, provider?: any): string;
|
|
23
|
+
/** URL do login do console pelo IdP, preservando o destino como `return_to`. */
|
|
24
|
+
export declare function consoleSsoUrl(returnTo?: string | null): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login do console pelo IdP (SSO), o jeito padrão de entrar em `/account/*` e no
|
|
3
|
+
* console admin.
|
|
4
|
+
*
|
|
5
|
+
* O console é tratado como mais um app do IdP, como o Keycloak faz com o
|
|
6
|
+
* `account-console` e o `security-admin-console`: sem sessão, o visitante vai
|
|
7
|
+
* para a tela de login do IdP (a mesma interaction de qualquer client: senha,
|
|
8
|
+
* magic link, OTP, passkey, social), volta, e a ponte de
|
|
9
|
+
* `idp_session_bridge.ts` abre o console para a conta da sessão do IdP.
|
|
10
|
+
*
|
|
11
|
+
* Para isso existe um client interno, `authkit-console`, injetado como client
|
|
12
|
+
* ESTÁTICO do provider (não fica no adapter, então não aparece na lista de
|
|
13
|
+
* clients do console nem pode ser editado):
|
|
14
|
+
*
|
|
15
|
+
* - `response_types: ['none']`: o authorize não emite code nem token. O client
|
|
16
|
+
* só usa o IdP para autenticar; o que abre o console é a sessão do IdP que o
|
|
17
|
+
* login criou, lida pela ponte. Não há segredo, PKCE ou troca de code.
|
|
18
|
+
* - o consent é concedido automaticamente (é a própria lib).
|
|
19
|
+
* - a única `redirect_uri` é `/auth/console/callback`, no MESMO host do issuer.
|
|
20
|
+
*
|
|
21
|
+
* Só existe com `accountSession.acceptIdpSession` ligado (o default).
|
|
22
|
+
*/
|
|
23
|
+
import { randomBytes } from 'node:crypto';
|
|
24
|
+
/** `client_id` do client interno do console. Reservado: não pode ser cadastrado. */
|
|
25
|
+
export const CONSOLE_SSO_CLIENT_ID = 'authkit-console';
|
|
26
|
+
/** Rota que inicia o login do console pelo IdP. */
|
|
27
|
+
export const CONSOLE_SSO_START_PATH = '/auth/console/sso';
|
|
28
|
+
/** `redirect_uri` do client interno: valida o `state` e abre o console. */
|
|
29
|
+
export const CONSOLE_SSO_CALLBACK_PATH = '/auth/console/callback';
|
|
30
|
+
/** Chave da sessão Adonis com o `state` e o destino de um login em andamento. */
|
|
31
|
+
export const CONSOLE_SSO_STATE_KEY = 'authkit_console_sso';
|
|
32
|
+
/** Metadata do client interno para o `clients` estático do oidc-provider. */
|
|
33
|
+
export function consoleSsoClientMetadata(issuer) {
|
|
34
|
+
const origin = new URL(issuer).origin;
|
|
35
|
+
return {
|
|
36
|
+
client_id: CONSOLE_SSO_CLIENT_ID,
|
|
37
|
+
client_name: 'Account console',
|
|
38
|
+
application_type: 'web',
|
|
39
|
+
token_endpoint_auth_method: 'none',
|
|
40
|
+
response_types: ['none'],
|
|
41
|
+
grant_types: [],
|
|
42
|
+
redirect_uris: [`${origin}${CONSOLE_SSO_CALLBACK_PATH}`],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Novo `state` opaco para um login do console. */
|
|
46
|
+
export function newConsoleSsoState() {
|
|
47
|
+
return randomBytes(24).toString('base64url');
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Path do authorize do provider. O issuer termina no `mountPath` (garantido por
|
|
51
|
+
* `registerAuthHost`), então o mount é o path do issuer.
|
|
52
|
+
*/
|
|
53
|
+
export function authorizationPath(issuer, provider) {
|
|
54
|
+
const mount = new URL(issuer).pathname.replace(/\/+$/, '');
|
|
55
|
+
if (provider && typeof provider.pathFor === 'function') {
|
|
56
|
+
try {
|
|
57
|
+
return provider.pathFor('authorization', { mountPath: mount });
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// cai no default do oidc-provider abaixo
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return `${mount}/auth`;
|
|
64
|
+
}
|
|
65
|
+
/** URL do login do console pelo IdP, preservando o destino como `return_to`. */
|
|
66
|
+
export function consoleSsoUrl(returnTo) {
|
|
67
|
+
if (!returnTo)
|
|
68
|
+
return CONSOLE_SSO_START_PATH;
|
|
69
|
+
return `${CONSOLE_SSO_START_PATH}?return_to=${encodeURIComponent(returnTo)}`;
|
|
70
|
+
}
|
|
@@ -38,16 +38,19 @@ export default class AccountAppsController {
|
|
|
38
38
|
scopes: g.scopes,
|
|
39
39
|
})))
|
|
40
40
|
: null;
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
// Conexões das `personalAgents.extensions`: desconectadas pela mesma lista.
|
|
42
|
+
let extensionAgents = null;
|
|
43
|
+
for (const { runtime } of agentsRuntime?.extensions.values() ?? []) {
|
|
44
|
+
extensionAgents ??= [];
|
|
45
|
+
for (const g of await runtime.listAccountGrants(accountId)) {
|
|
46
|
+
extensionAgents.push({
|
|
47
|
+
id: g.id,
|
|
48
|
+
name: await runtime.displayName(g.clientId),
|
|
49
|
+
scopes: g.scopes,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const agents = pactAgents || extensionAgents ? [...(pactAgents ?? []), ...(extensionAgents ?? [])] : null;
|
|
51
54
|
return render(ctx, 'account/apps', {
|
|
52
55
|
csrfToken: ctx.request.csrfToken,
|
|
53
56
|
supported,
|
|
@@ -89,8 +92,9 @@ export default class AccountAppsController {
|
|
|
89
92
|
/** POST /account/apps/agents/:grantId/revoke — revoga a delegação de um personal agent. */
|
|
90
93
|
async revokeAgent(ctx) {
|
|
91
94
|
const runtime = await personalAgentsFor(ctx);
|
|
92
|
-
if (!runtime
|
|
95
|
+
if (!runtime || (!runtime.delegation && runtime.extensions.size === 0)) {
|
|
93
96
|
return ctx.response.notFound();
|
|
97
|
+
}
|
|
94
98
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
95
99
|
const cfg = service.config;
|
|
96
100
|
const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
@@ -3,6 +3,7 @@ import { accountHome } from '../account_home.js';
|
|
|
3
3
|
import { getAccountLoginUrl } from '../account_login_url.js';
|
|
4
4
|
import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
|
|
5
5
|
import { syncAdonisAuthLogin, syncAdonisAuthLogout } from '../adonis_auth_sync.js';
|
|
6
|
+
import { consoleSsoUrl } from '../console_sso.js';
|
|
6
7
|
import { normalizeEmailIdentifier } from '../email_identifier.js';
|
|
7
8
|
import { translate } from '../i18n.js';
|
|
8
9
|
import { endBridgedIdpSession } from '../idp_session_bridge.js';
|
|
@@ -49,8 +50,16 @@ export default class AccountSessionController {
|
|
|
49
50
|
return ctx.response.redirect(accountHome(cfg));
|
|
50
51
|
}
|
|
51
52
|
// Lê e valida o return_to da query-string — descarta valores inválidos (open-redirect).
|
|
52
|
-
const
|
|
53
|
+
const qs = (ctx.request.qs?.() ?? {});
|
|
54
|
+
const rawReturnTo = qs.return_to ?? ctx.request.input?.('return_to');
|
|
53
55
|
const returnTo = validateReturnTo(rawReturnTo);
|
|
56
|
+
// SSO (default): o console entra pelo login do IdP, a mesma tela de qualquer
|
|
57
|
+
// app. O formulário de senha daqui fica como porta alternativa
|
|
58
|
+
// (`?method=password`), e só quando a senha não foi desligada no config.
|
|
59
|
+
const passwordForm = qs.method === 'password' && cfg.authMethods?.password !== false;
|
|
60
|
+
if (cfg.accountSession?.acceptIdpSession === true && !passwordForm) {
|
|
61
|
+
return redirectExact(ctx.response, consoleSsoUrl(returnTo));
|
|
62
|
+
}
|
|
54
63
|
const props = {
|
|
55
64
|
csrfToken: ctx.request.csrfToken,
|
|
56
65
|
returnTo,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import '../augmentations.js';
|
|
2
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
3
|
+
/**
|
|
4
|
+
* Login do console pelo IdP (ver `console_sso.ts`).
|
|
5
|
+
*
|
|
6
|
+
* GET /auth/console/sso?return_to=… → authorize do client interno
|
|
7
|
+
* GET /auth/console/callback?state=… → valida o state, abre o console pela ponte
|
|
8
|
+
*/
|
|
9
|
+
export default class ConsoleSsoController {
|
|
10
|
+
start(ctx: HttpContext): Promise<unknown>;
|
|
11
|
+
callback(ctx: HttpContext): Promise<unknown>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import '../augmentations.js';
|
|
2
|
+
import { accountHome } from '../account_home.js';
|
|
3
|
+
import { getAccountLoginUrl } from '../account_login_url.js';
|
|
4
|
+
import { authorizationPath, CONSOLE_SSO_CALLBACK_PATH, CONSOLE_SSO_CLIENT_ID, CONSOLE_SSO_STATE_KEY, newConsoleSsoState, } from '../console_sso.js';
|
|
5
|
+
import { ensureConsoleSession } from '../idp_session_bridge.js';
|
|
6
|
+
import { redirectExact } from '../redirect_exact.js';
|
|
7
|
+
import { validateReturnTo } from './account_session_controller.js';
|
|
8
|
+
/**
|
|
9
|
+
* Login do console pelo IdP (ver `console_sso.ts`).
|
|
10
|
+
*
|
|
11
|
+
* GET /auth/console/sso?return_to=… → authorize do client interno
|
|
12
|
+
* GET /auth/console/callback?state=… → valida o state, abre o console pela ponte
|
|
13
|
+
*/
|
|
14
|
+
export default class ConsoleSsoController {
|
|
15
|
+
async start(ctx) {
|
|
16
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
17
|
+
const cfg = service.config;
|
|
18
|
+
const returnTo = validateReturnTo(ctx.request.qs?.()?.return_to ?? ctx.request.input?.('return_to'));
|
|
19
|
+
// SSO desligado (`acceptIdpSession: false`): o console só tem o login próprio.
|
|
20
|
+
if (cfg.accountSession?.acceptIdpSession !== true) {
|
|
21
|
+
const login = getAccountLoginUrl();
|
|
22
|
+
const sep = login.includes('?') ? '&' : '?';
|
|
23
|
+
return redirectExact(ctx.response, returnTo ? `${login}${sep}return_to=${encodeURIComponent(returnTo)}` : login);
|
|
24
|
+
}
|
|
25
|
+
// Já tem sessão (de console ou do IdP): não precisa passar pelo IdP.
|
|
26
|
+
if (await ensureConsoleSession(ctx)) {
|
|
27
|
+
return redirectExact(ctx.response, returnTo ?? accountHome(cfg));
|
|
28
|
+
}
|
|
29
|
+
const state = newConsoleSsoState();
|
|
30
|
+
ctx.session.put(CONSOLE_SSO_STATE_KEY, { state, returnTo });
|
|
31
|
+
const origin = new URL(cfg.issuer).origin;
|
|
32
|
+
const query = new URLSearchParams({
|
|
33
|
+
client_id: CONSOLE_SSO_CLIENT_ID,
|
|
34
|
+
response_type: 'none',
|
|
35
|
+
scope: 'openid',
|
|
36
|
+
redirect_uri: `${origin}${CONSOLE_SSO_CALLBACK_PATH}`,
|
|
37
|
+
state,
|
|
38
|
+
});
|
|
39
|
+
return redirectExact(ctx.response, `${authorizationPath(cfg.issuer, service.provider)}?${query.toString()}`);
|
|
40
|
+
}
|
|
41
|
+
async callback(ctx) {
|
|
42
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
43
|
+
const cfg = service.config;
|
|
44
|
+
const pending = ctx.session.get(CONSOLE_SSO_STATE_KEY);
|
|
45
|
+
ctx.session.forget(CONSOLE_SSO_STATE_KEY);
|
|
46
|
+
const qs = (ctx.request.qs?.() ?? {});
|
|
47
|
+
// `state` amarra o callback ao navegador que iniciou o login (anti-CSRF de login).
|
|
48
|
+
if (!pending || typeof qs.state !== 'string' || qs.state !== pending.state) {
|
|
49
|
+
return ctx.response.status(400).send('Invalid or expired sign-in request.');
|
|
50
|
+
}
|
|
51
|
+
// O IdP recusou (o usuário cancelou, conta desabilitada, …): sem console.
|
|
52
|
+
// Vai para a raiz, e não de volta ao console, para não entrar em loop.
|
|
53
|
+
if (typeof qs.error === 'string') {
|
|
54
|
+
return redirectExact(ctx.response, '/');
|
|
55
|
+
}
|
|
56
|
+
if (!(await ensureConsoleSession(ctx))) {
|
|
57
|
+
return redirectExact(ctx.response, '/');
|
|
58
|
+
}
|
|
59
|
+
return redirectExact(ctx.response, pending.returnTo ?? accountHome(cfg));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -5,6 +5,7 @@ import { AdminSessionsService } from '../admin_sessions_service.js';
|
|
|
5
5
|
import { guardBotProtection, resolveEffectiveBotProtection } from '../bot_protection.js';
|
|
6
6
|
import { brandFor, isFirstParty } from '../branding.js';
|
|
7
7
|
import { consentRequestParams } from '../consent_params.js';
|
|
8
|
+
import { CONSOLE_SSO_CLIENT_ID } from '../console_sso.js';
|
|
8
9
|
import { isCustomLoginMethodId } from '../custom_login.js';
|
|
9
10
|
import { completeCustomLoginSession } from '../custom_login_completion.js';
|
|
10
11
|
import { beginCustomMfa, clearMfaFlow, customMfaViewProps, getMfaFlow, initializeMfaFlow, recordVerifiedMfaFactor, resolveCustomMfaMethods, verifyCustomMfa, } from '../custom_mfa.js';
|
|
@@ -254,7 +255,8 @@ export default class AuthInteractionController {
|
|
|
254
255
|
}
|
|
255
256
|
}
|
|
256
257
|
if (details.prompt.name === 'consent' &&
|
|
257
|
-
|
|
258
|
+
(details.params.client_id === CONSOLE_SSO_CLIENT_ID ||
|
|
259
|
+
isFirstParty(cfg.branding, details.params.client_id))) {
|
|
258
260
|
// Clients first-party: auto-concede o consent (pula a tela de consent).
|
|
259
261
|
// interactions.consent monta o Grant + interactionFinished e escreve o
|
|
260
262
|
// redirect de volta para o client — opera via provider.interactionDetails,
|
package/build/src/host/csrf.d.ts
CHANGED
|
@@ -11,10 +11,10 @@ export interface AuthkitCsrfOptions {
|
|
|
11
11
|
*/
|
|
12
12
|
personalAgentsPrefix?: string | false;
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Isenta os `csrfExemptPaths` das `personalAgents.extensions` do config. Default: true.
|
|
15
|
+
* `false` desliga.
|
|
16
16
|
*/
|
|
17
|
-
|
|
17
|
+
personalAgentExtensions?: boolean;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
20
|
* Retorna `true` quando `url` é uma rota AuthKit que deve ser ISENTA de CSRF
|
package/build/src/host/csrf.js
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
18
|
import { normalizePersonalAgentsPrefix } from '../agents/config.js';
|
|
19
|
-
import { normalizePoppyPrefix } from '../agents/poppy/config.js';
|
|
20
19
|
import { getAuthHostConfig } from './auth_host_config.js';
|
|
21
20
|
/**
|
|
22
21
|
* Retorna `true` quando `url` é uma rota AuthKit que deve ser ISENTA de CSRF
|
|
@@ -34,21 +33,12 @@ export function authkitCsrfExceptions(url, options = {}) {
|
|
|
34
33
|
? normalizePersonalAgentsPrefix(options.personalAgentsPrefix)
|
|
35
34
|
: getAuthHostConfig()?.personalAgents?.prefix;
|
|
36
35
|
const agentsOAuth = agentsPrefix ? `${agentsPrefix}/oauth/` : null;
|
|
37
|
-
const poppyPrefix = options.poppyPrefix === false
|
|
38
|
-
? undefined
|
|
39
|
-
: options.poppyPrefix !== undefined
|
|
40
|
-
? normalizePoppyPrefix(options.poppyPrefix)
|
|
41
|
-
: getAuthHostConfig()?.personalAgents?.poppyPrefix;
|
|
42
36
|
const path = url.split('?')[0];
|
|
43
|
-
const
|
|
44
|
-
(
|
|
45
|
-
path === `${poppyPrefix}/oauth/revoke` ||
|
|
46
|
-
path === `${poppyPrefix}/oauth/device` ||
|
|
47
|
-
path === `${poppyPrefix}/browser-session` ||
|
|
48
|
-
path === `${poppyPrefix}/browser-session/authorize`);
|
|
37
|
+
const extensionExempt = options.personalAgentExtensions !== false &&
|
|
38
|
+
(getAuthHostConfig()?.personalAgents?.extensions ?? []).some((e) => e.csrfExemptPaths?.includes(path));
|
|
49
39
|
return (url.includes(mountPath) ||
|
|
50
40
|
url.includes('/authkit/pat') ||
|
|
51
41
|
(backchannel !== null && url === backchannel) ||
|
|
52
42
|
(agentsOAuth !== null && url.startsWith(agentsOAuth)) ||
|
|
53
|
-
|
|
43
|
+
extensionExempt);
|
|
54
44
|
}
|
|
@@ -67,15 +67,20 @@ export declare function sendMagicLinkEmail(ctx: HttpContext, data: {
|
|
|
67
67
|
channel?: 'code' | 'link';
|
|
68
68
|
}): Promise<void>;
|
|
69
69
|
/**
|
|
70
|
-
* Envia
|
|
71
|
-
*
|
|
72
|
-
*
|
|
70
|
+
* Envia um e-mail só com um código (sem link: quem digita é a pessoa, em outro lugar) pelo mailer
|
|
71
|
+
* default do host, com o branding e o i18n do authkit. Os textos saem do catálogo pelas chaves
|
|
72
|
+
* `{messages}.subject`, `.heading`, `.intro`, `.code_label` e `.fallback`, com `params`
|
|
73
|
+
* interpolados — uma extension registra as dela em `messages`. Best-effort: sem `@adonisjs/mail`
|
|
74
|
+
* loga o código (dev); nunca lança.
|
|
75
|
+
*
|
|
76
|
+
* Estável no 1.x (é o mailer das `personalAgents.extensions`).
|
|
73
77
|
*/
|
|
74
|
-
export declare function
|
|
78
|
+
export declare function sendAuthkitCodeEmail(ctx: HttpContext, data: {
|
|
75
79
|
email: string;
|
|
76
80
|
code: string;
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
/** Prefixo das chaves no catálogo (`'mail.poppy_sign_in'`). */
|
|
82
|
+
messages: string;
|
|
83
|
+
params?: Record<string, string | number>;
|
|
79
84
|
}): Promise<void>;
|
|
80
85
|
/**
|
|
81
86
|
* Envia o link de CONFIRMAÇÃO DE IDENTIDADE (sudo) pelo mailer default do host.
|
|
@@ -295,32 +295,36 @@ export async function sendMagicLinkEmail(ctx, data) {
|
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
297
|
/**
|
|
298
|
-
* Envia
|
|
299
|
-
*
|
|
300
|
-
*
|
|
298
|
+
* Envia um e-mail só com um código (sem link: quem digita é a pessoa, em outro lugar) pelo mailer
|
|
299
|
+
* default do host, com o branding e o i18n do authkit. Os textos saem do catálogo pelas chaves
|
|
300
|
+
* `{messages}.subject`, `.heading`, `.intro`, `.code_label` e `.fallback`, com `params`
|
|
301
|
+
* interpolados — uma extension registra as dela em `messages`. Best-effort: sem `@adonisjs/mail`
|
|
302
|
+
* loga o código (dev); nunca lança.
|
|
303
|
+
*
|
|
304
|
+
* Estável no 1.x (é o mailer das `personalAgents.extensions`).
|
|
301
305
|
*/
|
|
302
|
-
export async function
|
|
306
|
+
export async function sendAuthkitCodeEmail(ctx, data) {
|
|
303
307
|
try {
|
|
304
308
|
const brand = resolveBrand(ctx);
|
|
305
309
|
const { messages: t, locale } = resolveMailMessages(ctx);
|
|
306
|
-
const
|
|
310
|
+
const key = (name) => `${data.messages}.${name}`;
|
|
307
311
|
const content = renderTransactionalEmail({
|
|
308
312
|
brand,
|
|
309
313
|
locale,
|
|
310
|
-
subject: translate(t, '
|
|
311
|
-
heading: translate(t, '
|
|
312
|
-
intro: translate(t, '
|
|
313
|
-
footnote: translate(t, '
|
|
314
|
+
subject: translate(t, key('subject'), data.params),
|
|
315
|
+
heading: translate(t, key('heading'), data.params),
|
|
316
|
+
intro: translate(t, key('intro'), data.params),
|
|
317
|
+
footnote: translate(t, key('fallback'), data.params),
|
|
314
318
|
code: data.code,
|
|
315
|
-
codeLabel: translate(t, '
|
|
319
|
+
codeLabel: translate(t, key('code_label'), data.params),
|
|
316
320
|
});
|
|
317
321
|
const sent = await sendEmail(ctx, data.email, content);
|
|
318
322
|
if (!sent) {
|
|
319
|
-
ctx.logger.info({ email: data.email,
|
|
323
|
+
ctx.logger.info({ email: data.email, code: data.code, messages: data.messages, ...(data.params ?? {}) }, 'authkit: código por e-mail (dev — @adonisjs/mail ausente)');
|
|
320
324
|
}
|
|
321
325
|
}
|
|
322
326
|
catch (error) {
|
|
323
|
-
ctx.logger.error({ err: error, email: data.email }, 'authkit: falha ao enviar o
|
|
327
|
+
ctx.logger.error({ err: error, email: data.email, messages: data.messages }, 'authkit: falha ao enviar o e-mail com código');
|
|
324
328
|
}
|
|
325
329
|
}
|
|
326
330
|
/**
|