@atlasauth/backend 0.15.0 → 0.17.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/CHANGELOG.md +27 -0
- package/dist/better-auth.d.ts +157 -0
- package/dist/better-auth.js +245 -0
- package/dist/better-auth.js.map +1 -0
- package/dist/botid.d.ts +70 -0
- package/dist/botid.js +88 -0
- package/dist/botid.js.map +1 -0
- package/dist/client/action-gallery.d.ts +215 -0
- package/dist/client/action-gallery.js +66 -0
- package/dist/client/action-gallery.js.map +1 -0
- package/dist/client/actions.d.ts +21 -0
- package/dist/client/actions.js.map +1 -1
- package/dist/client/attack-protection.d.ts +33 -1
- package/dist/client/attack-protection.js.map +1 -1
- package/dist/client/better-auth-imports.d.ts +205 -0
- package/dist/client/better-auth-imports.js +107 -0
- package/dist/client/better-auth-imports.js.map +1 -0
- package/dist/client/billing-plans.d.ts +93 -0
- package/dist/client/billing-plans.js +31 -0
- package/dist/client/billing-plans.js.map +1 -1
- package/dist/client/custom-oauth-providers.d.ts +243 -0
- package/dist/client/custom-oauth-providers.js +60 -0
- package/dist/client/custom-oauth-providers.js.map +1 -0
- package/dist/client/email-templates.d.ts +1 -1
- package/dist/client/email-templates.js.map +1 -1
- package/dist/client/index.d.ts +154 -0
- package/dist/client/index.js +11 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/instance-security.d.ts +70 -0
- package/dist/client/instance-security.js +20 -0
- package/dist/client/instance-security.js.map +1 -1
- package/dist/client/oauth-clients.d.ts +29 -2
- package/dist/client/oauth-clients.js.map +1 -1
- package/dist/client/resource-servers.d.ts +22 -2
- package/dist/client/resource-servers.js.map +1 -1
- package/dist/client/risk-based-mfa.d.ts +3 -0
- package/dist/client/risk-based-mfa.js.map +1 -1
- package/dist/client/sessions.d.ts +16 -0
- package/dist/client/sessions.js +15 -0
- package/dist/client/sessions.js.map +1 -1
- package/dist/client/sso-connections.d.ts +83 -0
- package/dist/client/sso-connections.js +29 -0
- package/dist/client/sso-connections.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/legacy-issuers.d.ts +65 -0
- package/dist/legacy-issuers.js +140 -0
- package/dist/legacy-issuers.js.map +1 -0
- package/dist/oauth-resource.d.ts +81 -1
- package/dist/oauth-resource.js +204 -29
- package/dist/oauth-resource.js.map +1 -1
- package/dist/verify.d.ts +22 -0
- package/dist/verify.js +32 -0
- package/dist/verify.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @atlasauth/backend changelog
|
|
2
2
|
|
|
3
|
+
## 0.17.0 — 2026-10-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Better Auth session bridge: `exchangeBetterAuthSession()` exchanges a Better Auth session for an Atlas session server to server with your secret key (forwarding the user's IP and user agent), plus `migrations.betterAuthBridge` to read and change the bridge settings.
|
|
8
|
+
- `legacyIssuers` on `AtlasBackend`: accept Better Auth-issued JWTs for a bounded window during a migration, mapping `sub` to the Atlas user through `external_id`.
|
|
9
|
+
- `betterAuthImports`: migrate from Better Auth with a sealed bundle (`getPublicKey`, `rotatePublicKey`, `create`, `uploadPart`, `finalize`, `upload` for all three, `dryRun`, `apply` to import or resume, `get`, `list`, `waitFor`, `delete`, `listJwks`, `deleteJwk`), with `BetterAuthImport`, `BetterAuthImportOptions`, the report types and `splitBundleParts`.
|
|
10
|
+
|
|
11
|
+
## 0.16.0 — 2026-10-24
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `actionGallery` (`list`, `get`, `install`, `installs`, `update`) and `actionTemplates` (`list`, `create`, `get`, `update`, `delete`, `addVersion`, `submitVersion`, `deleteVersion`, `publish`, `unlist`) for the Actions gallery.
|
|
16
|
+
- `ActionTestResult.http_requests` (what `api.http` queued in a dry run) and `organizations` / `country` on the sample event.
|
|
17
|
+
- DPoP server nonces (RFC 9449 §8) in `createAccessTokenVerifier`: `dpopNonces` requires every DPoP proof to carry a nonce the API issued, answering `use_dpop_nonce` with a fresh nonce otherwise; every result carries `dpopNonce` for the `DPoP-Nonce` response header. `memoryDpopNonceSource()` (one process) and `hmacDpopNonceSource(secret)` (stateless, shared by every process — and every Atlas server SDK — holding the same secret).
|
|
18
|
+
- Opaque access tokens: `createAccessTokenVerifier({ introspection: { clientId, clientSecret } })` validates a token that is not a JWT at the instance's introspection endpoint (RFC 7662), caching each answer (60 seconds by default, never past `exp`; inactive answers 10 seconds). `introspectJwts: true` checks JWTs live too, so a revoked grant is refused before the token expires. DPoP-bound opaque tokens keep their sender constraint (`cnf.jkt` from introspection). `createIntrospector` is the cache on its own.
|
|
19
|
+
- OAuth clients: `access_token_format` (`jwt` | `opaque`) and `token_lifetimes` (`OAuthTokenLifetimes`) on create, update and reads. Resource servers: `access_token_format`, `refresh_token_absolute_ttl_seconds`, `refresh_token_idle_ttl_seconds`, `introspection_client_ids`.
|
|
20
|
+
- `RotateSigningKeyBody.key` (`main` | `secondary`) and `SigningKeyRotation.secondary_kid` for KMS setups with one key per algorithm (`config.secondary`).
|
|
21
|
+
- KMS-backed signing keys: `instanceSecurity.getSigningKeyProvider()` / `setSigningKeyProvider({ provider, config, credentials })` / `removeSigningKeyProvider()` for AWS KMS, Google Cloud KMS and Azure Key Vault; `SigningKey.provider`.
|
|
22
|
+
- SSO connections: SAML Single Logout fields (`saml_single_logout`, `saml_idp_slo_url`, `saml_idp_slo_response_url`, `saml_idp_slo_binding`, `saml_idp_slo_soap_url` for back-channel logout) and `saml_require_encrypted_assertions`. Reads add `has_saml_encryption_key`, `has_previous_saml_encryption_key`, `saml_encryption_rotated_at`; `SamlMetadata.slo_url`. New `getSamlEncryptionKey()`, `createSamlEncryptionKey()` (generate or rotate), `retirePreviousSamlEncryptionKey()` and `deleteSamlEncryptionKey()` (`SamlEncryptionKey`).
|
|
23
|
+
- Custom OAuth 2.0 / OpenID Connect login providers: `customOAuthProviders` (`list`, `get`, `create`, `update`, `delete`, `discover`, `previewProfileMapping`, and `signingKeys(id)` with `list` / `rotate` / `retire` for `private_key_jwt`), with `ProfileMapping`, `TokenEndpointAuthMethod`, upstream (RP-initiated and front-channel) logout and third-party initiated login settings. Enterprise OIDC connections take the same `profile_mapping`, `token_endpoint_auth_method`, `client_assertion_audience`, `logout` and `idp_initiated_login`, and report `upstream_urls`.
|
|
24
|
+
- Attack protection: `dormant_accounts` (`DormantAccountsSettings`), readable and writable; `country_restrictions` / `device_accounts` typed. Risk-based MFA weights and signals gain `dormantAccount`. Email template key `account_reactivated`.
|
|
25
|
+
- Sessions: `custom_data` on reads and on create; `sessions.setCustomData(id, data)` (replace, `null` clears) and `sessions.updateCustomData(id, patch)` (merge).
|
|
26
|
+
- Billing: add-on plans (`kind: 'base' | 'addon'`) and several subscriptions per subject (`billing.getSubscription(id)`, and `changeSubscription` / `cancelSubscription` / `restoreSubscription` by subscription id); providers `chargebee`, `autumn` and `commet` (`BillingProviderName`); plan `limits` and Autumn `seat_feature_id`; `syncPlanEntitlements(id)`; `getSettings()` / `updateSettings()` for how limits merge across subscriptions (`max` | `sum`); `checkFeature({ subject_type, subject_id, feature, required })`; metered usage takes `feature` and `subscription_id`.
|
|
27
|
+
- Vercel BotID: `botIdAttestationHandler({ secret, checkBotId })` (a fetch-style route handler for your Vercel deployment) and `createBotIdAttestation()`, which sign a BotID verdict for Atlas's `vercel_botid` captcha provider.
|
|
28
|
+
- `instanceSecurity.setWalletRpcSecret({ chain_id, url })`: the wallet RPC endpoint for an EIP-155 chain (token gating and the Sign in with Ethereum ENS lookup); captcha secrets take `provider`.
|
|
29
|
+
|
|
3
30
|
## 0.15.0 — 2026-10-23
|
|
4
31
|
|
|
5
32
|
### Added
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Moving from Better Auth to Atlas without signing anyone out.
|
|
3
|
+
*
|
|
4
|
+
* During the migration window a returning user still carries their Better Auth
|
|
5
|
+
* session cookie (`better-auth.session_token`, `__Secure-` prefixed over
|
|
6
|
+
* https). It is HttpOnly, so only your server can read it. The bridge reads it,
|
|
7
|
+
* exchanges it ONCE with Atlas (`POST /v1/client/migrations/better_auth/exchange`),
|
|
8
|
+
* sets the Atlas session as your own first-party cookies and clears the Better
|
|
9
|
+
* Auth cookies. Atlas finds the session in your import (or asks your old app,
|
|
10
|
+
* with the live fallback on) and runs the normal sign-in path on it — MFA
|
|
11
|
+
* policy included: a user who still owes a second factor is sent to finish it.
|
|
12
|
+
*
|
|
13
|
+
* Framework-neutral: {@link createBetterAuthBridge} returns a decision with the
|
|
14
|
+
* cookies to set and clear; the Next.js middleware and the Express / Fastify /
|
|
15
|
+
* Hono adapters render it.
|
|
16
|
+
*/
|
|
17
|
+
/** Better Auth's default cookie prefix (`advanced.cookiePrefix`). */
|
|
18
|
+
export declare const BETTER_AUTH_COOKIE_PREFIX = "better-auth";
|
|
19
|
+
/** The FAPI path of the exchange (publishable key; per-IP limited — the browser / fallback path). */
|
|
20
|
+
export declare const BETTER_AUTH_EXCHANGE_PATH = "/v1/client/migrations/better_auth/exchange";
|
|
21
|
+
/** The Backend API path (secret key; limited per instance for a bulk cutover — the server path). */
|
|
22
|
+
export declare const BETTER_AUTH_BAPI_EXCHANGE_PATH = "/v1/migrations/better_auth/exchange";
|
|
23
|
+
/** A cookie to set (or, with `maxAge: 0`, clear) on the response. */
|
|
24
|
+
export interface BridgeCookie {
|
|
25
|
+
name: string;
|
|
26
|
+
value: string;
|
|
27
|
+
options: {
|
|
28
|
+
path?: string;
|
|
29
|
+
domain?: string;
|
|
30
|
+
httpOnly?: boolean;
|
|
31
|
+
sameSite?: 'lax' | 'strict' | 'none';
|
|
32
|
+
secure?: boolean;
|
|
33
|
+
maxAge?: number;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** The Better Auth cookies the bridge reads (session_token) and clears (all of them). */
|
|
37
|
+
export declare function betterAuthCookieNames(prefix?: string): {
|
|
38
|
+
sessionToken: string[];
|
|
39
|
+
all: string[];
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* The raw Better Auth session cookie value from a `Cookie` header (the
|
|
43
|
+
* `__Secure-` name first), or undefined. `cookieName` overrides the name when
|
|
44
|
+
* the old app set `advanced.cookies.session_token.name`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function readBetterAuthSessionCookie(cookieHeader: string | null | undefined, options?: {
|
|
47
|
+
cookiePrefix?: string;
|
|
48
|
+
cookieName?: string;
|
|
49
|
+
}): string | undefined;
|
|
50
|
+
/** Why Atlas refused an exchange (`meta.bridge_outcome`). */
|
|
51
|
+
export type BetterAuthRefusal = 'closed' | 'invalid' | 'signature_invalid' | 'expired' | 'reused' | 'user_unavailable' | 'rate_limited' | 'unavailable';
|
|
52
|
+
export type BetterAuthExchangeResult = {
|
|
53
|
+
kind: 'session';
|
|
54
|
+
jwt: string;
|
|
55
|
+
refreshToken: string;
|
|
56
|
+
sessionId: string;
|
|
57
|
+
userId: string;
|
|
58
|
+
expiresIn: number;
|
|
59
|
+
} | {
|
|
60
|
+
/** The user still owes a second factor (or MFA enrolment) — the Better Auth session is spent. */
|
|
61
|
+
kind: 'needs_mfa';
|
|
62
|
+
attemptId: string;
|
|
63
|
+
status: 'needs_second_factor' | 'needs_mfa_enrollment';
|
|
64
|
+
} | {
|
|
65
|
+
kind: 'refused';
|
|
66
|
+
outcome: BetterAuthRefusal;
|
|
67
|
+
status: number;
|
|
68
|
+
};
|
|
69
|
+
export interface ExchangeBetterAuthSessionOptions {
|
|
70
|
+
/** The instance's Frontend API origin, e.g. `https://clerk.example.com` or your FAPI host. */
|
|
71
|
+
fapiOrigin: string;
|
|
72
|
+
publishableKey: string;
|
|
73
|
+
/**
|
|
74
|
+
* The instance secret key. When set (recommended on a server), the exchange
|
|
75
|
+
* goes to the Backend API, which is limited per instance with a ceiling sized
|
|
76
|
+
* for a bulk cutover instead of per IP — your server speaks for every user —
|
|
77
|
+
* and takes the user's IP and user agent from `clientIp` / `userAgent`.
|
|
78
|
+
*/
|
|
79
|
+
secretKey?: string;
|
|
80
|
+
/** Backend API origin for the secret-key path. Default `https://api.atlasauth.net`. */
|
|
81
|
+
bapiBaseUrl?: string;
|
|
82
|
+
/** The end user's IP (secret-key path), so Atlas's risk and geo checks see the user, not your server. */
|
|
83
|
+
clientIp?: string;
|
|
84
|
+
/** The raw Better Auth session cookie value (or the bare token). */
|
|
85
|
+
token: string;
|
|
86
|
+
/** false → the Atlas refresh cookie should be a browser-session cookie. */
|
|
87
|
+
remember?: boolean;
|
|
88
|
+
fetchImpl?: typeof fetch;
|
|
89
|
+
/** Forwarded so the session's device record shows the user's browser, not your server. */
|
|
90
|
+
userAgent?: string;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Exchange a Better Auth session for an Atlas session, server to server
|
|
94
|
+
* (`delivery: "body"`). Never throws: a transport failure is
|
|
95
|
+
* `{ kind: 'refused', outcome: 'unavailable' }`, so the caller keeps the Better
|
|
96
|
+
* Auth cookie and tries again on a later request.
|
|
97
|
+
*/
|
|
98
|
+
export declare function exchangeBetterAuthSession(options: ExchangeBetterAuthSessionOptions): Promise<BetterAuthExchangeResult>;
|
|
99
|
+
export interface BetterAuthBridgeOptions {
|
|
100
|
+
fapiOrigin: string;
|
|
101
|
+
publishableKey: string;
|
|
102
|
+
/** The instance secret key: exchanges go through the Backend API (recommended; see {@link ExchangeBetterAuthSessionOptions.secretKey}). */
|
|
103
|
+
secretKey?: string;
|
|
104
|
+
bapiBaseUrl?: string;
|
|
105
|
+
/** Better Auth's `advanced.cookiePrefix` (default `better-auth`). */
|
|
106
|
+
cookiePrefix?: string;
|
|
107
|
+
/** The session cookie's full name, when the old app overrode it. */
|
|
108
|
+
cookieName?: string;
|
|
109
|
+
/** The `domain` the Better Auth cookies were set on (its `crossSubDomainCookies.domain`), so they clear. */
|
|
110
|
+
betterAuthCookieDomain?: string;
|
|
111
|
+
/** Mark the Atlas cookies `Secure`. Default true; set false only for plain-http local development. */
|
|
112
|
+
secure?: boolean;
|
|
113
|
+
fetchImpl?: typeof fetch;
|
|
114
|
+
}
|
|
115
|
+
/** What the bridge decided for one request. */
|
|
116
|
+
export type BetterAuthBridgeDecision =
|
|
117
|
+
/** No Better Auth cookie, or an Atlas session already present: carry on. */
|
|
118
|
+
{
|
|
119
|
+
action: 'none';
|
|
120
|
+
}
|
|
121
|
+
/** Exchanged: set these cookies and re-enter (redirect to the same URL) so the new session is read. */
|
|
122
|
+
| {
|
|
123
|
+
action: 'signed_in';
|
|
124
|
+
cookies: BridgeCookie[];
|
|
125
|
+
session: Extract<BetterAuthExchangeResult, {
|
|
126
|
+
kind: 'session';
|
|
127
|
+
}>;
|
|
128
|
+
}
|
|
129
|
+
/** A second factor is owed: send the browser to your sign-in page carrying the attempt (see `continuationUrl`). */
|
|
130
|
+
| {
|
|
131
|
+
action: 'needs_mfa';
|
|
132
|
+
cookies: BridgeCookie[];
|
|
133
|
+
attemptId: string;
|
|
134
|
+
status: string;
|
|
135
|
+
continuationUrl: (url: string) => string;
|
|
136
|
+
}
|
|
137
|
+
/** Refused. `cookies` clears the Better Auth cookies when the refusal is final (invalid, expired, reused). */
|
|
138
|
+
| {
|
|
139
|
+
action: 'refused';
|
|
140
|
+
cookies: BridgeCookie[];
|
|
141
|
+
outcome: BetterAuthRefusal;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* The bridge as a function of a request's headers. Call it from middleware
|
|
145
|
+
* before your normal auth check; it does nothing (no network) for a request
|
|
146
|
+
* without a Better Auth cookie or with an Atlas session (`__session` /
|
|
147
|
+
* `__atlas_rt`) already present.
|
|
148
|
+
*/
|
|
149
|
+
export declare function createBetterAuthBridge(options: BetterAuthBridgeOptions): (request: {
|
|
150
|
+
headers: {
|
|
151
|
+
get(name: string): string | null;
|
|
152
|
+
} | Record<string, string | string[] | undefined>;
|
|
153
|
+
/** The end user's IP as your framework resolved it (forwarded on the secret-key path). */
|
|
154
|
+
ip?: string;
|
|
155
|
+
}) => Promise<BetterAuthBridgeDecision>;
|
|
156
|
+
/** Serialize a {@link BridgeCookie} as a `Set-Cookie` header value. */
|
|
157
|
+
export declare function serializeBridgeCookie(cookie: BridgeCookie): string;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Moving from Better Auth to Atlas without signing anyone out.
|
|
4
|
+
*
|
|
5
|
+
* During the migration window a returning user still carries their Better Auth
|
|
6
|
+
* session cookie (`better-auth.session_token`, `__Secure-` prefixed over
|
|
7
|
+
* https). It is HttpOnly, so only your server can read it. The bridge reads it,
|
|
8
|
+
* exchanges it ONCE with Atlas (`POST /v1/client/migrations/better_auth/exchange`),
|
|
9
|
+
* sets the Atlas session as your own first-party cookies and clears the Better
|
|
10
|
+
* Auth cookies. Atlas finds the session in your import (or asks your old app,
|
|
11
|
+
* with the live fallback on) and runs the normal sign-in path on it — MFA
|
|
12
|
+
* policy included: a user who still owes a second factor is sent to finish it.
|
|
13
|
+
*
|
|
14
|
+
* Framework-neutral: {@link createBetterAuthBridge} returns a decision with the
|
|
15
|
+
* cookies to set and clear; the Next.js middleware and the Express / Fastify /
|
|
16
|
+
* Hono adapters render it.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.BETTER_AUTH_BAPI_EXCHANGE_PATH = exports.BETTER_AUTH_EXCHANGE_PATH = exports.BETTER_AUTH_COOKIE_PREFIX = void 0;
|
|
20
|
+
exports.betterAuthCookieNames = betterAuthCookieNames;
|
|
21
|
+
exports.readBetterAuthSessionCookie = readBetterAuthSessionCookie;
|
|
22
|
+
exports.exchangeBetterAuthSession = exchangeBetterAuthSession;
|
|
23
|
+
exports.createBetterAuthBridge = createBetterAuthBridge;
|
|
24
|
+
exports.serializeBridgeCookie = serializeBridgeCookie;
|
|
25
|
+
/** Better Auth's default cookie prefix (`advanced.cookiePrefix`). */
|
|
26
|
+
exports.BETTER_AUTH_COOKIE_PREFIX = 'better-auth';
|
|
27
|
+
/** The FAPI path of the exchange (publishable key; per-IP limited — the browser / fallback path). */
|
|
28
|
+
exports.BETTER_AUTH_EXCHANGE_PATH = '/v1/client/migrations/better_auth/exchange';
|
|
29
|
+
/** The Backend API path (secret key; limited per instance for a bulk cutover — the server path). */
|
|
30
|
+
exports.BETTER_AUTH_BAPI_EXCHANGE_PATH = '/v1/migrations/better_auth/exchange';
|
|
31
|
+
/** Default Backend API origin. */
|
|
32
|
+
const DEFAULT_BAPI_URL = 'https://api.atlasauth.net';
|
|
33
|
+
/** The Better Auth cookies the bridge reads (session_token) and clears (all of them). */
|
|
34
|
+
function betterAuthCookieNames(prefix = exports.BETTER_AUTH_COOKIE_PREFIX) {
|
|
35
|
+
const base = ['session_token', 'session_data', 'account_data', 'dont_remember'].map((n) => `${prefix}.${n}`);
|
|
36
|
+
const secure = base.map((n) => `__Secure-${n}`);
|
|
37
|
+
return {
|
|
38
|
+
sessionToken: [`__Secure-${prefix}.session_token`, `${prefix}.session_token`],
|
|
39
|
+
all: [...secure, ...base],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function readCookie(header, name) {
|
|
43
|
+
if (!header)
|
|
44
|
+
return undefined;
|
|
45
|
+
for (const part of header.split(';')) {
|
|
46
|
+
const [key, ...rest] = part.trim().split('=');
|
|
47
|
+
if (key === name)
|
|
48
|
+
return rest.join('=');
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The raw Better Auth session cookie value from a `Cookie` header (the
|
|
54
|
+
* `__Secure-` name first), or undefined. `cookieName` overrides the name when
|
|
55
|
+
* the old app set `advanced.cookies.session_token.name`.
|
|
56
|
+
*/
|
|
57
|
+
function readBetterAuthSessionCookie(cookieHeader, options = {}) {
|
|
58
|
+
const names = options.cookieName
|
|
59
|
+
? [`__Secure-${options.cookieName}`, options.cookieName]
|
|
60
|
+
: betterAuthCookieNames(options.cookiePrefix).sessionToken;
|
|
61
|
+
for (const name of names) {
|
|
62
|
+
const value = readCookie(cookieHeader, name);
|
|
63
|
+
if (value)
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Exchange a Better Auth session for an Atlas session, server to server
|
|
70
|
+
* (`delivery: "body"`). Never throws: a transport failure is
|
|
71
|
+
* `{ kind: 'refused', outcome: 'unavailable' }`, so the caller keeps the Better
|
|
72
|
+
* Auth cookie and tries again on a later request.
|
|
73
|
+
*/
|
|
74
|
+
async function exchangeBetterAuthSession(options) {
|
|
75
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
76
|
+
const viaSecretKey = Boolean(options.secretKey);
|
|
77
|
+
const url = viaSecretKey
|
|
78
|
+
? `${(options.bapiBaseUrl ?? DEFAULT_BAPI_URL).replace(/\/+$/, '')}${exports.BETTER_AUTH_BAPI_EXCHANGE_PATH}`
|
|
79
|
+
: new URL(exports.BETTER_AUTH_EXCHANGE_PATH, options.fapiOrigin).toString();
|
|
80
|
+
const headers = { 'content-type': 'application/json' };
|
|
81
|
+
if (viaSecretKey) {
|
|
82
|
+
headers.authorization = `Bearer ${options.secretKey}`;
|
|
83
|
+
if (options.clientIp)
|
|
84
|
+
headers['x-atlas-client-ip'] = options.clientIp;
|
|
85
|
+
if (options.userAgent)
|
|
86
|
+
headers['x-atlas-client-user-agent'] = options.userAgent;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
headers['x-publishable-key'] = options.publishableKey;
|
|
90
|
+
if (options.userAgent)
|
|
91
|
+
headers['user-agent'] = options.userAgent;
|
|
92
|
+
}
|
|
93
|
+
let res;
|
|
94
|
+
try {
|
|
95
|
+
res = await doFetch(url, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers,
|
|
98
|
+
body: JSON.stringify({
|
|
99
|
+
token: options.token,
|
|
100
|
+
...(viaSecretKey ? {} : { delivery: 'body' }),
|
|
101
|
+
...(options.remember === false ? { remember: false } : {}),
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return { kind: 'refused', outcome: 'unavailable', status: 0 };
|
|
107
|
+
}
|
|
108
|
+
const data = (await res.json().catch(() => null));
|
|
109
|
+
if (res.ok && data?.object === 'session_tokens' && data.jwt && data.refresh_token) {
|
|
110
|
+
return {
|
|
111
|
+
kind: 'session',
|
|
112
|
+
jwt: data.jwt,
|
|
113
|
+
refreshToken: data.refresh_token,
|
|
114
|
+
sessionId: data.session_id ?? '',
|
|
115
|
+
userId: data.user_id ?? '',
|
|
116
|
+
expiresIn: data.expires_in ?? 0,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (res.ok &&
|
|
120
|
+
data?.object === 'sign_in_attempt' &&
|
|
121
|
+
data.id &&
|
|
122
|
+
(data.status === 'needs_second_factor' || data.status === 'needs_mfa_enrollment')) {
|
|
123
|
+
return { kind: 'needs_mfa', attemptId: data.id, status: data.status };
|
|
124
|
+
}
|
|
125
|
+
if (res.status === 429)
|
|
126
|
+
return { kind: 'refused', outcome: 'rate_limited', status: 429 };
|
|
127
|
+
const outcome = data?.errors?.[0]?.meta?.bridge_outcome;
|
|
128
|
+
const known = ['closed', 'invalid', 'signature_invalid', 'expired', 'reused', 'user_unavailable', 'rate_limited'];
|
|
129
|
+
if (outcome && known.includes(outcome)) {
|
|
130
|
+
return { kind: 'refused', outcome: outcome, status: res.status };
|
|
131
|
+
}
|
|
132
|
+
// Anything else (a 5xx, a guard refusal, an unparsable body) is not a verdict
|
|
133
|
+
// on the Better Auth session: keep it and retry later.
|
|
134
|
+
return { kind: 'refused', outcome: 'unavailable', status: res.status };
|
|
135
|
+
}
|
|
136
|
+
/** Refusals that say the Better Auth session will never exchange: its cookies are cleared. */
|
|
137
|
+
const FINAL = ['invalid', 'signature_invalid', 'expired', 'reused', 'user_unavailable'];
|
|
138
|
+
/**
|
|
139
|
+
* The bridge as a function of a request's headers. Call it from middleware
|
|
140
|
+
* before your normal auth check; it does nothing (no network) for a request
|
|
141
|
+
* without a Better Auth cookie or with an Atlas session (`__session` /
|
|
142
|
+
* `__atlas_rt`) already present.
|
|
143
|
+
*/
|
|
144
|
+
function createBetterAuthBridge(options) {
|
|
145
|
+
const secure = options.secure !== false;
|
|
146
|
+
const clearBetterAuth = () => betterAuthCookieNames(options.cookiePrefix).all.concat(options.cookieName ? [options.cookieName, `__Secure-${options.cookieName}`] : []).map((name) => ({
|
|
147
|
+
name,
|
|
148
|
+
value: '',
|
|
149
|
+
options: {
|
|
150
|
+
path: '/',
|
|
151
|
+
maxAge: 0,
|
|
152
|
+
...(options.betterAuthCookieDomain ? { domain: options.betterAuthCookieDomain } : {}),
|
|
153
|
+
// A `__Secure-` cookie can only be overwritten by a Secure Set-Cookie.
|
|
154
|
+
...(name.startsWith('__Secure-') ? { secure: true } : {}),
|
|
155
|
+
},
|
|
156
|
+
}));
|
|
157
|
+
return async function bridge(request) {
|
|
158
|
+
const cookieHeader = header(request.headers, 'cookie');
|
|
159
|
+
const token = readBetterAuthSessionCookie(cookieHeader, options);
|
|
160
|
+
if (!token)
|
|
161
|
+
return { action: 'none' };
|
|
162
|
+
// Already on Atlas: leave the old cookie for the old app's own expiry.
|
|
163
|
+
if (readCookie(cookieHeader, '__session') || readCookie(cookieHeader, '__atlas_rt'))
|
|
164
|
+
return { action: 'none' };
|
|
165
|
+
const result = await exchangeBetterAuthSession({
|
|
166
|
+
fapiOrigin: options.fapiOrigin,
|
|
167
|
+
publishableKey: options.publishableKey,
|
|
168
|
+
token,
|
|
169
|
+
fetchImpl: options.fetchImpl,
|
|
170
|
+
userAgent: header(request.headers, 'user-agent'),
|
|
171
|
+
secretKey: options.secretKey,
|
|
172
|
+
bapiBaseUrl: options.bapiBaseUrl,
|
|
173
|
+
clientIp: request.ip ?? clientIpFrom(request.headers),
|
|
174
|
+
});
|
|
175
|
+
if (result.kind === 'session') {
|
|
176
|
+
return {
|
|
177
|
+
action: 'signed_in',
|
|
178
|
+
session: result,
|
|
179
|
+
cookies: [
|
|
180
|
+
{ name: '__session', value: result.jwt, options: { path: '/', sameSite: 'lax', secure } },
|
|
181
|
+
{
|
|
182
|
+
name: '__atlas_rt',
|
|
183
|
+
value: result.refreshToken,
|
|
184
|
+
options: { path: '/', httpOnly: true, sameSite: 'lax', secure },
|
|
185
|
+
},
|
|
186
|
+
...clearBetterAuth(),
|
|
187
|
+
],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (result.kind === 'needs_mfa') {
|
|
191
|
+
return {
|
|
192
|
+
action: 'needs_mfa',
|
|
193
|
+
attemptId: result.attemptId,
|
|
194
|
+
status: result.status,
|
|
195
|
+
cookies: clearBetterAuth(),
|
|
196
|
+
continuationUrl: (url) => {
|
|
197
|
+
const next = new URL(url);
|
|
198
|
+
next.searchParams.set('__atlas_attempt', result.attemptId);
|
|
199
|
+
next.searchParams.set('__atlas_status', result.status);
|
|
200
|
+
return next.toString();
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
action: 'refused',
|
|
206
|
+
outcome: result.outcome,
|
|
207
|
+
cookies: FINAL.includes(result.outcome) ? clearBetterAuth() : [],
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function header(headers, name) {
|
|
212
|
+
if (typeof headers.get === 'function') {
|
|
213
|
+
return headers.get(name) ?? undefined;
|
|
214
|
+
}
|
|
215
|
+
const record = headers;
|
|
216
|
+
const value = record[name] ?? record[name.toLowerCase()];
|
|
217
|
+
return Array.isArray(value) ? value.join('; ') : value;
|
|
218
|
+
}
|
|
219
|
+
/** Serialize a {@link BridgeCookie} as a `Set-Cookie` header value. */
|
|
220
|
+
function serializeBridgeCookie(cookie) {
|
|
221
|
+
const o = cookie.options;
|
|
222
|
+
const parts = [`${cookie.name}=${cookie.value}`];
|
|
223
|
+
parts.push(`Path=${o.path ?? '/'}`);
|
|
224
|
+
if (o.domain)
|
|
225
|
+
parts.push(`Domain=${o.domain}`);
|
|
226
|
+
if (o.maxAge !== undefined) {
|
|
227
|
+
parts.push(`Max-Age=${o.maxAge}`);
|
|
228
|
+
if (o.maxAge === 0)
|
|
229
|
+
parts.push('Expires=Thu, 01 Jan 1970 00:00:00 GMT');
|
|
230
|
+
}
|
|
231
|
+
if (o.httpOnly)
|
|
232
|
+
parts.push('HttpOnly');
|
|
233
|
+
if (o.secure)
|
|
234
|
+
parts.push('Secure');
|
|
235
|
+
if (o.sameSite)
|
|
236
|
+
parts.push(`SameSite=${o.sameSite[0].toUpperCase()}${o.sameSite.slice(1)}`);
|
|
237
|
+
return parts.join('; ');
|
|
238
|
+
}
|
|
239
|
+
/** The client IP a proxy in front of your server recorded (first X-Forwarded-For hop, or X-Real-IP). */
|
|
240
|
+
function clientIpFrom(headers) {
|
|
241
|
+
const xff = header(headers, 'x-forwarded-for');
|
|
242
|
+
const first = xff?.split(',')[0]?.trim();
|
|
243
|
+
return first || header(headers, 'x-real-ip')?.trim() || undefined;
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=better-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"better-auth.js","sourceRoot":"","sources":["../src/better-auth.ts"],"names":[],"mappings":"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"}
|
package/dist/botid.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vercel BotID for Atlas — the server half that runs on YOUR Vercel deployment.
|
|
3
|
+
*
|
|
4
|
+
* BotID's verdict only exists inside a Vercel-hosted request (`checkBotId()`
|
|
5
|
+
* from `botid/server` reads signals Vercel's edge attaches). Atlas is not that
|
|
6
|
+
* request, so your route runs `checkBotId()` and turns the verdict into a short
|
|
7
|
+
* signed attestation; the browser sends it to Atlas as `captcha_token`, and
|
|
8
|
+
* Atlas checks the signature, freshness and `isBot === false`, and spends it
|
|
9
|
+
* (each attestation passes once).
|
|
10
|
+
*
|
|
11
|
+
* // app/api/atlas/botid/route.ts (Next.js on Vercel)
|
|
12
|
+
* import { checkBotId } from 'botid/server';
|
|
13
|
+
* import { botIdAttestationHandler } from '@atlasauth/backend';
|
|
14
|
+
* export const POST = botIdAttestationHandler({
|
|
15
|
+
* secret: process.env.ATLAS_BOTID_SECRET!,
|
|
16
|
+
* checkBotId,
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* The same secret is stored in Atlas for the `vercel_botid` captcha provider.
|
|
20
|
+
* Protect the route with BotID on the client (`initBotId({ protect: [{ path:
|
|
21
|
+
* '/api/atlas/botid', method: 'POST' }] })`) and set the captcha's
|
|
22
|
+
* `attestationUrl` to it, so the prebuilt components fetch a verdict on submit.
|
|
23
|
+
*
|
|
24
|
+
* Format (version 1): `atlas_botid.v1.<payload>.<sig>` — `payload` is base64url
|
|
25
|
+
* JSON `{ v: 1, is_bot, verified_bot, iat, exp, jti }`, `sig` is base64url
|
|
26
|
+
* HMAC-SHA256 over `atlas_botid.v1.<payload>` keyed by the shared secret.
|
|
27
|
+
*/
|
|
28
|
+
/** The prefix every attestation carries. */
|
|
29
|
+
export declare const BOTID_ATTESTATION_PREFIX = "atlas_botid.v1.";
|
|
30
|
+
/** Atlas refuses attestations that live longer than this, in seconds. */
|
|
31
|
+
export declare const BOTID_ATTESTATION_MAX_TTL_SECONDS = 300;
|
|
32
|
+
/** Atlas refuses secrets shorter than this. */
|
|
33
|
+
export declare const BOTID_SECRET_MIN_LENGTH = 32;
|
|
34
|
+
/** The fields of BotID's `checkBotId()` result this helper reads. */
|
|
35
|
+
export interface BotIdVerdict {
|
|
36
|
+
isBot: boolean;
|
|
37
|
+
/** True for a known good bot (a search engine crawler). Recorded, still refused. */
|
|
38
|
+
isVerifiedBot?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface CreateBotIdAttestationOptions {
|
|
41
|
+
/** The attestation secret shared with Atlas (at least 32 characters). */
|
|
42
|
+
secret: string;
|
|
43
|
+
/** The verdict `checkBotId()` returned for this request. */
|
|
44
|
+
verdict: BotIdVerdict;
|
|
45
|
+
/** Lifetime in seconds. Default 60, at most 300. */
|
|
46
|
+
ttlSeconds?: number;
|
|
47
|
+
/** Clock override, epoch ms (tests). */
|
|
48
|
+
now?: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Sign a BotID verdict for Atlas. Throws when the secret is too short (Atlas
|
|
52
|
+
* would refuse every attestation it signs). A bot verdict is still signed — it
|
|
53
|
+
* is Atlas that refuses it — so your route needs no branching.
|
|
54
|
+
*/
|
|
55
|
+
export declare function createBotIdAttestation(options: CreateBotIdAttestationOptions): string;
|
|
56
|
+
export interface BotIdAttestationHandlerOptions {
|
|
57
|
+
/** The attestation secret shared with Atlas. */
|
|
58
|
+
secret: string;
|
|
59
|
+
/** `checkBotId` from `botid/server`. */
|
|
60
|
+
checkBotId: () => Promise<BotIdVerdict>;
|
|
61
|
+
/** Lifetime of each attestation in seconds. Default 60. */
|
|
62
|
+
ttlSeconds?: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A Web-standard `(request) => Response` route handler (Next.js App Router,
|
|
66
|
+
* Hono, any fetch-style runtime): runs `checkBotId()` and answers
|
|
67
|
+
* `{ token }` with the signed attestation, `no-store`. A BotID failure answers
|
|
68
|
+
* 503 so the client never sends a token that was never checked.
|
|
69
|
+
*/
|
|
70
|
+
export declare function botIdAttestationHandler(options: BotIdAttestationHandlerOptions): (request?: Request) => Promise<Response>;
|
package/dist/botid.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BOTID_SECRET_MIN_LENGTH = exports.BOTID_ATTESTATION_MAX_TTL_SECONDS = exports.BOTID_ATTESTATION_PREFIX = void 0;
|
|
4
|
+
exports.createBotIdAttestation = createBotIdAttestation;
|
|
5
|
+
exports.botIdAttestationHandler = botIdAttestationHandler;
|
|
6
|
+
const node_crypto_1 = require("node:crypto");
|
|
7
|
+
/**
|
|
8
|
+
* Vercel BotID for Atlas — the server half that runs on YOUR Vercel deployment.
|
|
9
|
+
*
|
|
10
|
+
* BotID's verdict only exists inside a Vercel-hosted request (`checkBotId()`
|
|
11
|
+
* from `botid/server` reads signals Vercel's edge attaches). Atlas is not that
|
|
12
|
+
* request, so your route runs `checkBotId()` and turns the verdict into a short
|
|
13
|
+
* signed attestation; the browser sends it to Atlas as `captcha_token`, and
|
|
14
|
+
* Atlas checks the signature, freshness and `isBot === false`, and spends it
|
|
15
|
+
* (each attestation passes once).
|
|
16
|
+
*
|
|
17
|
+
* // app/api/atlas/botid/route.ts (Next.js on Vercel)
|
|
18
|
+
* import { checkBotId } from 'botid/server';
|
|
19
|
+
* import { botIdAttestationHandler } from '@atlasauth/backend';
|
|
20
|
+
* export const POST = botIdAttestationHandler({
|
|
21
|
+
* secret: process.env.ATLAS_BOTID_SECRET!,
|
|
22
|
+
* checkBotId,
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* The same secret is stored in Atlas for the `vercel_botid` captcha provider.
|
|
26
|
+
* Protect the route with BotID on the client (`initBotId({ protect: [{ path:
|
|
27
|
+
* '/api/atlas/botid', method: 'POST' }] })`) and set the captcha's
|
|
28
|
+
* `attestationUrl` to it, so the prebuilt components fetch a verdict on submit.
|
|
29
|
+
*
|
|
30
|
+
* Format (version 1): `atlas_botid.v1.<payload>.<sig>` — `payload` is base64url
|
|
31
|
+
* JSON `{ v: 1, is_bot, verified_bot, iat, exp, jti }`, `sig` is base64url
|
|
32
|
+
* HMAC-SHA256 over `atlas_botid.v1.<payload>` keyed by the shared secret.
|
|
33
|
+
*/
|
|
34
|
+
/** The prefix every attestation carries. */
|
|
35
|
+
exports.BOTID_ATTESTATION_PREFIX = 'atlas_botid.v1.';
|
|
36
|
+
/** Atlas refuses attestations that live longer than this, in seconds. */
|
|
37
|
+
exports.BOTID_ATTESTATION_MAX_TTL_SECONDS = 300;
|
|
38
|
+
/** Atlas refuses secrets shorter than this. */
|
|
39
|
+
exports.BOTID_SECRET_MIN_LENGTH = 32;
|
|
40
|
+
/**
|
|
41
|
+
* Sign a BotID verdict for Atlas. Throws when the secret is too short (Atlas
|
|
42
|
+
* would refuse every attestation it signs). A bot verdict is still signed — it
|
|
43
|
+
* is Atlas that refuses it — so your route needs no branching.
|
|
44
|
+
*/
|
|
45
|
+
function createBotIdAttestation(options) {
|
|
46
|
+
const { secret, verdict } = options;
|
|
47
|
+
if (typeof secret !== 'string' || secret.length < exports.BOTID_SECRET_MIN_LENGTH) {
|
|
48
|
+
throw new Error(`The BotID attestation secret must be at least ${exports.BOTID_SECRET_MIN_LENGTH} characters.`);
|
|
49
|
+
}
|
|
50
|
+
const ttl = Math.min(Math.max(Math.floor(options.ttlSeconds ?? 60), 1), exports.BOTID_ATTESTATION_MAX_TTL_SECONDS);
|
|
51
|
+
const iat = Math.floor((options.now ?? Date.now()) / 1000);
|
|
52
|
+
const payload = Buffer.from(JSON.stringify({
|
|
53
|
+
v: 1,
|
|
54
|
+
is_bot: verdict.isBot !== false,
|
|
55
|
+
verified_bot: verdict.isVerifiedBot === true,
|
|
56
|
+
iat,
|
|
57
|
+
exp: iat + ttl,
|
|
58
|
+
jti: (0, node_crypto_1.randomBytes)(18).toString('base64url'),
|
|
59
|
+
}), 'utf8').toString('base64url');
|
|
60
|
+
const sig = (0, node_crypto_1.createHmac)('sha256', secret).update(exports.BOTID_ATTESTATION_PREFIX + payload).digest('base64url');
|
|
61
|
+
return `${exports.BOTID_ATTESTATION_PREFIX}${payload}.${sig}`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A Web-standard `(request) => Response` route handler (Next.js App Router,
|
|
65
|
+
* Hono, any fetch-style runtime): runs `checkBotId()` and answers
|
|
66
|
+
* `{ token }` with the signed attestation, `no-store`. A BotID failure answers
|
|
67
|
+
* 503 so the client never sends a token that was never checked.
|
|
68
|
+
*/
|
|
69
|
+
function botIdAttestationHandler(options) {
|
|
70
|
+
return async () => {
|
|
71
|
+
let verdict;
|
|
72
|
+
try {
|
|
73
|
+
verdict = await options.checkBotId();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return new Response(JSON.stringify({ error: 'botid_unavailable' }), {
|
|
77
|
+
status: 503,
|
|
78
|
+
headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const token = createBotIdAttestation({ secret: options.secret, verdict, ttlSeconds: options.ttlSeconds });
|
|
82
|
+
return new Response(JSON.stringify({ token }), {
|
|
83
|
+
status: 200,
|
|
84
|
+
headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=botid.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"botid.js","sourceRoot":"","sources":["../src/botid.ts"],"names":[],"mappings":";;;AA8DA,wDAoBC;AAiBD,0DAiBC;AApHD,6CAAsD;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,4CAA4C;AAC/B,QAAA,wBAAwB,GAAG,iBAAiB,CAAC;AAE1D,yEAAyE;AAC5D,QAAA,iCAAiC,GAAG,GAAG,CAAC;AAErD,+CAA+C;AAClC,QAAA,uBAAuB,GAAG,EAAE,CAAC;AAoB1C;;;;GAIG;AACH,SAAgB,sBAAsB,CAAC,OAAsC;IAC3E,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IACpC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,GAAG,+BAAuB,EAAE,CAAC;QAC1E,MAAM,IAAI,KAAK,CAAC,iDAAiD,+BAAuB,cAAc,CAAC,CAAC;IAC1G,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,yCAAiC,CAAC,CAAC;IAC3G,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;IAC3D,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CACzB,IAAI,CAAC,SAAS,CAAC;QACb,CAAC,EAAE,CAAC;QACJ,MAAM,EAAE,OAAO,CAAC,KAAK,KAAK,KAAK;QAC/B,YAAY,EAAE,OAAO,CAAC,aAAa,KAAK,IAAI;QAC5C,GAAG;QACH,GAAG,EAAE,GAAG,GAAG,GAAG;QACd,GAAG,EAAE,IAAA,yBAAW,EAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC;KAC3C,CAAC,EACF,MAAM,CACP,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IACxB,MAAM,GAAG,GAAG,IAAA,wBAAU,EAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,gCAAwB,GAAG,OAAO,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IACxG,OAAO,GAAG,gCAAwB,GAAG,OAAO,IAAI,GAAG,EAAE,CAAC;AACxD,CAAC;AAWD;;;;;GAKG;AACH,SAAgB,uBAAuB,CAAC,OAAuC;IAC7E,OAAO,KAAK,IAAI,EAAE;QAChB,IAAI,OAAqB,CAAC;QAC1B,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,EAAE,CAAC;QACvC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC,EAAE;gBAClE,MAAM,EAAE,GAAG;gBACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,eAAe,EAAE,UAAU,EAAE;aAC7E,CAAC,CAAC;QACL,CAAC;QACD,MAAM,KAAK,GAAG,sBAAsB,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;QAC1G,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE;YAC7C,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,eAAe,EAAE,UAAU,EAAE;SAC7E,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC"}
|