@growth-labs/auth 0.1.3 → 0.2.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 +132 -34
- package/dist/client/login-page.d.ts +16 -1
- package/dist/client/login-page.d.ts.map +1 -1
- package/dist/client/login-page.js +20 -1
- package/dist/client/login-page.js.map +1 -1
- package/dist/middleware/auth.js +2 -2
- package/dist/middleware/auth.js.map +1 -1
- package/dist/routes/callback.d.ts.map +1 -1
- package/dist/routes/callback.js +27 -4
- package/dist/routes/callback.js.map +1 -1
- package/dist/routes/forgot-password.d.ts +12 -1
- package/dist/routes/forgot-password.d.ts.map +1 -1
- package/dist/routes/forgot-password.js +14 -54
- package/dist/routes/forgot-password.js.map +1 -1
- package/dist/routes/login.d.ts.map +1 -1
- package/dist/routes/login.js +21 -27
- package/dist/routes/login.js.map +1 -1
- package/dist/routes/reset-password.d.ts +10 -0
- package/dist/routes/reset-password.d.ts.map +1 -1
- package/dist/routes/reset-password.js +12 -44
- package/dist/routes/reset-password.js.map +1 -1
- package/dist/types.d.ts +18 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/cookies.d.ts +18 -0
- package/dist/utils/cookies.d.ts.map +1 -1
- package/dist/utils/cookies.js +27 -0
- package/dist/utils/cookies.js.map +1 -1
- package/dist/utils/index.d.ts +4 -2
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +3 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/openauth.d.ts +50 -5
- package/dist/utils/openauth.d.ts.map +1 -1
- package/dist/utils/openauth.js +70 -18
- package/dist/utils/openauth.js.map +1 -1
- package/dist/utils/tokens.d.ts +5 -8
- package/dist/utils/tokens.d.ts.map +1 -1
- package/dist/utils/tokens.js +54 -24
- package/dist/utils/tokens.js.map +1 -1
- package/package.json +74 -58
package/README.md
CHANGED
|
@@ -2,47 +2,94 @@
|
|
|
2
2
|
|
|
3
3
|
OpenAuth **client** package for Astro + Cloudflare. Verifies JWTs, manages session cookies, gates protected paths, and injects login/logout/callback routes.
|
|
4
4
|
|
|
5
|
-
**This is a client only.**
|
|
5
|
+
**This is a client only.** The OpenAuth issuer is a separate Worker — point this package at any standards-compliant OpenAuth issuer.
|
|
6
6
|
|
|
7
7
|
**Identity, not authorization.** Answers "who is this user?" — not "what can they access?" Roles, entitlements, and premium checks are consumer concerns (e.g. `@fulcrum/auth`).
|
|
8
8
|
|
|
9
|
+
## Live issuer
|
|
10
|
+
|
|
11
|
+
The canonical issuer for Fulcrum and Growth Labs sites is the standalone identity-platform at `https://auth.fulcrum-labs.com`. Discovery is at `/.well-known/oauth-authorization-server`:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
issuer: https://auth.fulcrum-labs.com
|
|
15
|
+
authorization_endpoint: https://auth.fulcrum-labs.com/authorize
|
|
16
|
+
token_endpoint: https://auth.fulcrum-labs.com/token
|
|
17
|
+
revocation_endpoint: https://auth.fulcrum-labs.com/revoke
|
|
18
|
+
jwks_uri: https://auth.fulcrum-labs.com/.well-known/jwks.json
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The repo's `apps/auth-server/` is a **reference sample** for bootstrapping new issuer Workers — it is not the canonical issuer.
|
|
22
|
+
|
|
9
23
|
## Config
|
|
10
24
|
|
|
11
25
|
```typescript
|
|
12
26
|
import auth from '@growth-labs/auth'
|
|
13
27
|
|
|
14
28
|
auth({
|
|
15
|
-
issuer: 'https://auth.
|
|
16
|
-
cookiePrefix: '
|
|
17
|
-
sessionCookieDomain: '.
|
|
18
|
-
gatedPaths: ['/premium/*', '/account/*'],
|
|
19
|
-
loginPath: '/login',
|
|
29
|
+
issuer: 'https://auth.fulcrum-labs.com', // OpenAuth issuer URL (separate Worker)
|
|
30
|
+
cookiePrefix: 'fronts', // → fronts_at, fronts_rt cookies
|
|
31
|
+
sessionCookieDomain: '.fronts.co', // Cross-subdomain sharing (optional)
|
|
32
|
+
gatedPaths: ['/premium/*', '/account/*'], // Glob patterns for protected routes
|
|
33
|
+
loginPath: '/login', // Redirect target for unauthenticated users
|
|
20
34
|
logoutRedirect: '/',
|
|
21
35
|
callbackPath: '/api/auth/callback',
|
|
36
|
+
forgotPasswordPath: '/forgot-password', // Auto-injected when password provider is enabled
|
|
37
|
+
resetPasswordPath: '/reset-password',
|
|
22
38
|
providers: ['google', 'password', 'email-code'],
|
|
23
39
|
loginPage: {
|
|
24
40
|
title: 'Sign in',
|
|
25
|
-
brandName: '
|
|
26
|
-
logoUrl: 'https://media.
|
|
41
|
+
brandName: 'fronts.co',
|
|
42
|
+
logoUrl: 'https://media.fronts.co/logo.png',
|
|
27
43
|
},
|
|
28
|
-
accessTokenMaxAge: 900,
|
|
29
|
-
refreshTokenMaxAge: 2592000,
|
|
44
|
+
accessTokenMaxAge: 900, // 15 min (seconds)
|
|
45
|
+
refreshTokenMaxAge: 2592000, // 30 days
|
|
30
46
|
})
|
|
31
47
|
```
|
|
32
48
|
|
|
33
49
|
## What It Injects
|
|
34
50
|
|
|
35
51
|
**Middleware** (runs on every request):
|
|
52
|
+
|
|
36
53
|
1. Reads `{cookiePrefix}_at` and `{cookiePrefix}_rt` cookies
|
|
37
|
-
2. Verifies access token JWT against issuer's JWKS (`{issuer}/.well-known/jwks.json`, cached
|
|
54
|
+
2. Verifies access token JWT against issuer's JWKS (`{issuer}/.well-known/jwks.json`, cached/refreshed by `jose.createRemoteJWKSet`)
|
|
38
55
|
3. If expired but refresh token valid → silent refresh, set new cookies
|
|
39
56
|
4. Populates `context.locals.user` as `AuthenticatedUser | UnauthenticatedUser`
|
|
40
57
|
5. If path matches `gatedPaths` and user unauthenticated → redirect to `loginPath`
|
|
41
58
|
|
|
42
59
|
**Routes:**
|
|
43
|
-
|
|
44
|
-
- `GET /
|
|
45
|
-
- `GET /
|
|
60
|
+
|
|
61
|
+
- `GET /login` — default login page with provider buttons. Each button is a link to `{issuer}/authorize?provider=...` (Google, password, and email-code via OpenAuth's `code` provider). Override by creating `src/pages/login.astro`.
|
|
62
|
+
- `GET /logout` — clears cookies, revokes refresh token at `{issuer}/revoke`, redirects.
|
|
63
|
+
- `GET /api/auth/callback` — OAuth callback handler (PKCE code exchange, cookie setting, analytics emission).
|
|
64
|
+
- `GET /forgot-password` — thin 302 to `{issuer}/forgot-password` (issuer owns the UI). Only injected when the `password` provider is enabled.
|
|
65
|
+
- `GET /reset-password?token=...` — thin 302 to `{issuer}/reset-password?token=...`. Only injected when the `password` provider is enabled.
|
|
66
|
+
|
|
67
|
+
## Subject claims contract
|
|
68
|
+
|
|
69
|
+
The package expects an OpenAuth-shaped access token. The subject payload lives under `properties` in the JWT and contains:
|
|
70
|
+
|
|
71
|
+
| Claim | Type | Notes |
|
|
72
|
+
|-------------|---------|---------------------------------------------|
|
|
73
|
+
| `userId` | string | Stable user identifier |
|
|
74
|
+
| `email` | string | |
|
|
75
|
+
| `name` | string | Optional |
|
|
76
|
+
| `image` | string | Optional |
|
|
77
|
+
| `isNewUser` | boolean | Whether this user was just created |
|
|
78
|
+
|
|
79
|
+
The JWT's top-level `sub` is an OpenAuth-internal subject hash (`{type}:{sha1}`) and is **never** the user id.
|
|
80
|
+
|
|
81
|
+
## /token response contract
|
|
82
|
+
|
|
83
|
+
```jsonc
|
|
84
|
+
// POST {issuer}/token
|
|
85
|
+
{
|
|
86
|
+
"access_token": "<jwt>",
|
|
87
|
+
"refresh_token": "<opaque>",
|
|
88
|
+
"expires_in": 900
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The package does **not** read `is_new_user` or `provider` from the /token response — those are not part of the standalone identity-platform contract. `isNewUser` comes from the JWT subject claims; provider attribution comes from a `{cookiePrefix}_provider` cookie set when the user clicked a provider button.
|
|
46
93
|
|
|
47
94
|
## Using `context.locals.user`
|
|
48
95
|
|
|
@@ -61,43 +108,94 @@ if (user.authenticated) {
|
|
|
61
108
|
|
|
62
109
|
```typescript
|
|
63
110
|
interface AuthenticatedUser {
|
|
64
|
-
id: string
|
|
111
|
+
id: string
|
|
112
|
+
email: string
|
|
113
|
+
name?: string
|
|
114
|
+
image?: string
|
|
115
|
+
authenticated: true
|
|
65
116
|
}
|
|
66
117
|
interface UnauthenticatedUser { authenticated: false }
|
|
67
118
|
type User = AuthenticatedUser | UnauthenticatedUser
|
|
68
119
|
// Available on context.locals.user (Astro.locals.user in .astro files)
|
|
69
120
|
```
|
|
70
121
|
|
|
71
|
-
## Wrangler
|
|
122
|
+
## Wrangler bindings
|
|
72
123
|
|
|
73
|
-
None required by this package. The issuer needs KV + D1 — see `apps/auth-server/README.md
|
|
124
|
+
None required by this package. The issuer needs KV + D1 — see `apps/auth-server/README.md` (reference sample) or your standalone identity-platform's deployment docs.
|
|
74
125
|
|
|
75
|
-
## Integration
|
|
126
|
+
## Integration order
|
|
76
127
|
|
|
77
128
|
Auth middleware MUST run before consent and analytics middleware. List `auth()` first in `astro.config.mjs` integrations array.
|
|
78
129
|
|
|
79
|
-
## Issuer
|
|
130
|
+
## Issuer unreachable (cold start)
|
|
80
131
|
|
|
81
|
-
JWKS cached
|
|
82
|
-
- **Non-gated paths:** fail open — `user = { authenticated: false }`, page serves normally
|
|
83
|
-
- **Gated paths:** fail closed — redirect to loginPath
|
|
132
|
+
JWKS is fetched lazily by `jose.createRemoteJWKSet` (cached + refreshed automatically when the key set rotates). If the issuer is unreachable on a first verification:
|
|
84
133
|
|
|
85
|
-
|
|
134
|
+
- **Non-gated paths:** fail open — `user = { authenticated: false }`, page serves normally.
|
|
135
|
+
- **Gated paths:** fail closed — middleware redirects to `loginPath`.
|
|
86
136
|
|
|
87
|
-
|
|
88
|
-
2. Issuer authenticates → redirects to `/api/auth/callback?code=...&state=...`
|
|
89
|
-
3. Callback exchanges code for tokens via PKCE, sets session cookies, redirects to original page
|
|
137
|
+
## OAuth flow summary
|
|
90
138
|
|
|
91
|
-
|
|
139
|
+
1. User clicks a provider on the login page → browser navigates to `{issuer}/authorize?response_type=code&...` with PKCE challenge. The login page's inline JS records the chosen provider in `{cookiePrefix}_provider`.
|
|
140
|
+
2. Issuer authenticates → redirects to `/api/auth/callback?code=...&state=...`.
|
|
141
|
+
3. Callback exchanges code for tokens via PKCE, decodes the JWT to read subject claims, emits `signup_completed` (if `isNewUser`) or `login_completed`, sets session cookies, redirects to original page.
|
|
92
142
|
|
|
93
|
-
|
|
143
|
+
## Password / forgot / reset
|
|
94
144
|
|
|
95
|
-
|
|
145
|
+
Password, forgot-password, and reset-password are **issuer-owned flows**. The package does NOT implement password verification locally:
|
|
96
146
|
|
|
97
|
-
-
|
|
98
|
-
-
|
|
147
|
+
- **Login with password** → the package's login page renders a button that 302s the user into `{issuer}/authorize?provider=password&...`. The issuer hosts the password UI.
|
|
148
|
+
- **Forgot password** → `/forgot-password` (consumer URL) → 302 → `{issuer}/forgot-password`.
|
|
149
|
+
- **Reset password** → reset email link → `/reset-password?token=...` (consumer URL) → 302 → `{issuer}/reset-password?token=...`.
|
|
150
|
+
|
|
151
|
+
Consumer URLs exist so emails and bookmarks remain stable across issuer changes.
|
|
152
|
+
|
|
153
|
+
## Email code (magic link)
|
|
154
|
+
|
|
155
|
+
The `email-code` package alias maps to OpenAuth's `code` provider. The login page links into `{issuer}/authorize?provider=code&...`; the issuer hosts the email/code UI and (per its own configuration) sends the one-time code via whatever transport it's wired to.
|
|
156
|
+
|
|
157
|
+
## Analytics integration
|
|
158
|
+
|
|
159
|
+
Optional peer dependency on `@growth-labs/analytics`. If installed, the callback emits:
|
|
160
|
+
|
|
161
|
+
- `signup_completed` when the JWT's `properties.isNewUser` is `true`
|
|
162
|
+
- `login_completed` otherwise
|
|
163
|
+
- Both include `{ provider, userId }` properties
|
|
164
|
+
|
|
165
|
+
If `@growth-labs/analytics` is not installed, analytics calls are silently skipped.
|
|
166
|
+
|
|
167
|
+
## Custom login pages
|
|
168
|
+
|
|
169
|
+
Use the exported helpers to build branded login pages while keeping the package's PKCE/state plumbing:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
import {
|
|
173
|
+
createAuthFlow,
|
|
174
|
+
getAuthorizationUrl,
|
|
175
|
+
getPasswordLoginUrl,
|
|
176
|
+
getMagicLinkUrl,
|
|
177
|
+
setAuthFlowCookies,
|
|
178
|
+
setProviderCookie,
|
|
179
|
+
} from '@growth-labs/auth/utils'
|
|
180
|
+
|
|
181
|
+
// In a custom /login page or API route:
|
|
182
|
+
const flow = await createAuthFlow(Astro)
|
|
183
|
+
const google = await getAuthorizationUrl(Astro, 'google', redirectPath, flow)
|
|
184
|
+
const password = await getPasswordLoginUrl(Astro, redirectPath, flow)
|
|
185
|
+
const emailCode = await getMagicLinkUrl(Astro, redirectPath, flow)
|
|
186
|
+
|
|
187
|
+
// On the response, set the flow cookies the callback handler needs:
|
|
188
|
+
setAuthFlowCookies(response, options, flow.verifier, flow.state, redirectPath)
|
|
189
|
+
// Optionally record provider attribution server-side instead of via JS:
|
|
190
|
+
setProviderCookie(response, options, 'google')
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Key patterns
|
|
194
|
+
|
|
195
|
+
- Virtual module: `globalThis.__GL_AUTH_CONFIG__` is set at build time via `vite.define` for runtime access; module-level `_setConfig` is the dev-time fallback.
|
|
196
|
+
- Cookie names: `{cookiePrefix}_at`, `{cookiePrefix}_rt`, `{cookiePrefix}_pkce`, `{cookiePrefix}_state`, `{cookiePrefix}_redirect`, `{cookiePrefix}_provider`
|
|
99
197
|
- PKCE flow with `code_verifier` / `code_challenge` (S256)
|
|
100
|
-
-
|
|
101
|
-
- CSRF state parameter verified on callback
|
|
198
|
+
- CSRF state parameter verified on callback (constant-time compare)
|
|
102
199
|
- No `aud` claim validation in this package — single client per issuer assumed
|
|
103
|
-
-
|
|
200
|
+
- `email-code` is a client-side alias for the issuer's OpenAuth `code` provider
|
|
201
|
+
- All token endpoint URLs are constructed via `new URL('/path', issuer)` so issuer values with or without trailing slashes both work
|
|
@@ -1,2 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Inline JS for the package's default login page.
|
|
3
|
+
*
|
|
4
|
+
* On a provider click:
|
|
5
|
+
* 1. Records the provider in `{cookiePrefix}_provider` so the callback
|
|
6
|
+
* handler can attribute analytics correctly. The standalone
|
|
7
|
+
* identity-platform issuer does not echo the provider back in the
|
|
8
|
+
* /token response, so we must remember it client-side.
|
|
9
|
+
* 2. Emits a `login_clicked` analytics event if `window.glAnalytics`
|
|
10
|
+
* is wired up (via `@growth-labs/analytics`).
|
|
11
|
+
*
|
|
12
|
+
* The provider cookie is intentionally NOT httpOnly — it must be writable
|
|
13
|
+
* from JS. Provider attribution is non-sensitive (just an analytics
|
|
14
|
+
* label) so this is an acceptable tradeoff.
|
|
15
|
+
*/
|
|
16
|
+
export declare function loginPageScript(cookiePrefix: string): string;
|
|
2
17
|
//# sourceMappingURL=login-page.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login-page.d.ts","sourceRoot":"","sources":["../../src/client/login-page.ts"],"names":[],"mappings":"AAAA,wBAAgB,eAAe,
|
|
1
|
+
{"version":3,"file":"login-page.d.ts","sourceRoot":"","sources":["../../src/client/login-page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAe5D"}
|
|
@@ -1,8 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Inline JS for the package's default login page.
|
|
3
|
+
*
|
|
4
|
+
* On a provider click:
|
|
5
|
+
* 1. Records the provider in `{cookiePrefix}_provider` so the callback
|
|
6
|
+
* handler can attribute analytics correctly. The standalone
|
|
7
|
+
* identity-platform issuer does not echo the provider back in the
|
|
8
|
+
* /token response, so we must remember it client-side.
|
|
9
|
+
* 2. Emits a `login_clicked` analytics event if `window.glAnalytics`
|
|
10
|
+
* is wired up (via `@growth-labs/analytics`).
|
|
11
|
+
*
|
|
12
|
+
* The provider cookie is intentionally NOT httpOnly — it must be writable
|
|
13
|
+
* from JS. Provider attribution is non-sensitive (just an analytics
|
|
14
|
+
* label) so this is an acceptable tradeoff.
|
|
15
|
+
*/
|
|
16
|
+
export function loginPageScript(cookiePrefix) {
|
|
17
|
+
// JSON.stringify safely escapes the prefix into a JS string literal.
|
|
18
|
+
const prefixLiteral = JSON.stringify(cookiePrefix);
|
|
2
19
|
return `
|
|
3
20
|
document.querySelectorAll('[data-provider]').forEach(function(el) {
|
|
4
21
|
el.addEventListener('click', function() {
|
|
5
22
|
var provider = el.getAttribute('data-provider');
|
|
23
|
+
var prefix = ${prefixLiteral};
|
|
24
|
+
document.cookie = prefix + '_provider=' + encodeURIComponent(provider) + '; max-age=600; path=/; samesite=lax; secure';
|
|
6
25
|
if (window.glAnalytics) {
|
|
7
26
|
window.glAnalytics.track('login_clicked', { provider: provider });
|
|
8
27
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login-page.js","sourceRoot":"","sources":["../../src/client/login-page.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,eAAe;
|
|
1
|
+
{"version":3,"file":"login-page.js","sourceRoot":"","sources":["../../src/client/login-page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,eAAe,CAAC,YAAoB;IACnD,qEAAqE;IACrE,MAAM,aAAa,GAAG,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,CAAA;IAClD,OAAO;;;;uBAIe,aAAa;;;;;;;GAOjC,CAAA;AACH,CAAC"}
|
package/dist/middleware/auth.js
CHANGED
|
@@ -14,7 +14,7 @@ export async function onRequest(context, next) {
|
|
|
14
14
|
const verified = await verifyAccessToken(options.issuer, accessToken);
|
|
15
15
|
if (verified) {
|
|
16
16
|
user = {
|
|
17
|
-
id: verified.
|
|
17
|
+
id: verified.userId,
|
|
18
18
|
email: verified.email,
|
|
19
19
|
name: verified.name,
|
|
20
20
|
image: verified.image,
|
|
@@ -27,7 +27,7 @@ export async function onRequest(context, next) {
|
|
|
27
27
|
refreshed = await refreshTokens(options.issuer, refreshToken, clientId);
|
|
28
28
|
if (refreshed) {
|
|
29
29
|
user = {
|
|
30
|
-
id: refreshed.claims.
|
|
30
|
+
id: refreshed.claims.userId,
|
|
31
31
|
email: refreshed.claims.email,
|
|
32
32
|
name: refreshed.claims.name,
|
|
33
33
|
image: refreshed.claims.image,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAE9C,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAChE,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAErE,MAAM,CAAC,KAAK,UAAU,SAAS,CAC9B,OAAwE,EACxE,IAA6B;IAE7B,MAAM,OAAO,GAAG,SAAS,EAAE,CAAA;IAC3B,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;IAE3B,8BAA8B;IAC9B,MAAM,WAAW,GAAG,SAAS,CAAC,OAAO,EAAE,GAAG,OAAO,CAAC,YAAY,KAAK,CAAC,CAAA;IACpE,MAAM,YAAY,GAAG,SAAS,CAAC,OAAO,EAAE,GAAG,OAAO,CAAC,YAAY,KAAK,CAAC,CAAA;IAErE,6BAA6B;IAC7B,IAAI,IAAI,GAAS,EAAE,aAAa,EAAE,KAAK,EAAE,CAAA;IACzC,IAAI,SAAS,GAA8C,IAAI,CAAA;IAE/D,IAAI,WAAW,EAAE,CAAC;QACjB,MAAM,QAAQ,GAAG,MAAM,iBAAiB,CAAC,OAAO,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;QACrE,IAAI,QAAQ,EAAE,CAAC;YACd,IAAI,GAAG;gBACN,EAAE,EAAE,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"callback.d.ts","sourceRoot":"","sources":["../../src/routes/callback.ts"],"names":[],"mappings":"AAMA,UAAU,eAAe;IACxB,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,EAAE,GAAG,CAAA;IACR,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAA;CAC/C;
|
|
1
|
+
{"version":3,"file":"callback.d.ts","sourceRoot":"","sources":["../../src/routes/callback.ts"],"names":[],"mappings":"AAMA,UAAU,eAAe;IACxB,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,EAAE,GAAG,CAAA;IACR,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAA;CAC/C;AAYD,wBAAsB,GAAG,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,QAAQ,CAAC,CAqErE"}
|
package/dist/routes/callback.js
CHANGED
|
@@ -3,6 +3,16 @@ import { analyticsAvailable, emitAuthEvent } from '../utils/analytics.js';
|
|
|
3
3
|
import { clearAuthFlowCookies, getCookie, setTokenCookies } from '../utils/cookies.js';
|
|
4
4
|
import { exchangeCode } from '../utils/openauth.js';
|
|
5
5
|
import { verifyState } from '../utils/state.js';
|
|
6
|
+
const ALLOWED_PROVIDERS = new Set(['google', 'password', 'email-code', 'code']);
|
|
7
|
+
function normalizeProvider(value) {
|
|
8
|
+
if (!value)
|
|
9
|
+
return 'unknown';
|
|
10
|
+
if (!ALLOWED_PROVIDERS.has(value))
|
|
11
|
+
return 'unknown';
|
|
12
|
+
// `code` is the OpenAuth provider name; the package surfaces it as
|
|
13
|
+
// `email-code` for consumer-facing analytics consistency.
|
|
14
|
+
return value === 'code' ? 'email-code' : value;
|
|
15
|
+
}
|
|
6
16
|
export async function GET(context) {
|
|
7
17
|
const options = getConfig();
|
|
8
18
|
const { url, request } = context;
|
|
@@ -18,11 +28,24 @@ export async function GET(context) {
|
|
|
18
28
|
if (!codeVerifier) {
|
|
19
29
|
return new Response('Missing PKCE verifier', { status: 400 });
|
|
20
30
|
}
|
|
21
|
-
|
|
31
|
+
if (!code) {
|
|
32
|
+
return new Response('Missing authorization code', { status: 400 });
|
|
33
|
+
}
|
|
34
|
+
// Exchange code for tokens. The /token response from the standalone
|
|
35
|
+
// identity-platform issuer is { access_token, refresh_token } — it does
|
|
36
|
+
// NOT include is_new_user or provider. Both come from elsewhere.
|
|
22
37
|
const tokenResponse = await exchangeCode(url, code, codeVerifier);
|
|
23
38
|
if (!tokenResponse) {
|
|
24
39
|
return Response.redirect(new URL(`${options.loginPath}?error=auth_failed`, url.origin).toString(), 302);
|
|
25
40
|
}
|
|
41
|
+
// Provider attribution: read from the client-side flow cookie set when
|
|
42
|
+
// the user clicked the provider button. Falls back to 'unknown' if the
|
|
43
|
+
// caller (e.g. a custom login page) didn't set it.
|
|
44
|
+
const provider = normalizeProvider(getCookie(request, `${options.cookiePrefix}_provider`));
|
|
45
|
+
// isNewUser: derive from the access token's subject claims, NOT from a
|
|
46
|
+
// top-level token response field. The standalone issuer encodes
|
|
47
|
+
// isNewUser inside the subject payload (`properties.isNewUser`).
|
|
48
|
+
const isNewUser = tokenResponse.claims.isNewUser === true;
|
|
26
49
|
// Build redirect response
|
|
27
50
|
const redirectPath = getCookie(request, `${options.cookiePrefix}_redirect`) || '/';
|
|
28
51
|
const response = Response.redirect(new URL(redirectPath, url.origin).toString(), 302);
|
|
@@ -36,10 +59,10 @@ export async function GET(context) {
|
|
|
36
59
|
clearAuthFlowCookies(mutableResponse, options);
|
|
37
60
|
// Emit analytics event (non-blocking)
|
|
38
61
|
if (analyticsAvailable(context)) {
|
|
39
|
-
const eventName =
|
|
62
|
+
const eventName = isNewUser ? 'signup_completed' : 'login_completed';
|
|
40
63
|
const promise = emitAuthEvent(context, eventName, {
|
|
41
|
-
provider
|
|
42
|
-
userId: tokenResponse.claims.
|
|
64
|
+
provider,
|
|
65
|
+
userId: tokenResponse.claims.userId,
|
|
43
66
|
});
|
|
44
67
|
context.waitUntil?.(promise);
|
|
45
68
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"callback.js","sourceRoot":"","sources":["../../src/routes/callback.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACzE,OAAO,EAAE,oBAAoB,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AACtF,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAS/C,MAAM,CAAC,KAAK,UAAU,GAAG,CAAC,OAAwB;IACjD,MAAM,OAAO,GAAG,SAAS,EAAE,CAAA;IAC3B,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;IAEhC,MAAM,IAAI,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IACzC,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IAE3C,oBAAoB;IACpB,MAAM,WAAW,GAAG,SAAS,CAAC,OAAO,EAAE,GAAG,OAAO,CAAC,YAAY,QAAQ,CAAC,CAAA;IACvE,IAAI,CAAC,KAAK,IAAI,CAAC,WAAW,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,WAAW,CAAC,EAAE,CAAC;QAChE,OAAO,IAAI,QAAQ,CAAC,yBAAyB,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAChE,CAAC;IAED,qBAAqB;IACrB,MAAM,YAAY,GAAG,SAAS,CAAC,OAAO,EAAE,GAAG,OAAO,CAAC,YAAY,OAAO,CAAC,CAAA;IACvE,IAAI,CAAC,YAAY,EAAE,CAAC;QACnB,OAAO,IAAI,QAAQ,CAAC,uBAAuB,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAC9D,CAAC;IAED,
|
|
1
|
+
{"version":3,"file":"callback.js","sourceRoot":"","sources":["../../src/routes/callback.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,6 +2,17 @@ interface ForgotPasswordContext {
|
|
|
2
2
|
request: Request;
|
|
3
3
|
url: URL;
|
|
4
4
|
}
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Thin proxy into the issuer-hosted forgot-password flow.
|
|
7
|
+
*
|
|
8
|
+
* Password (and forgot/reset) are issuer-owned flows: the standalone
|
|
9
|
+
* identity-platform issuer hosts the UI. This route only exists so that
|
|
10
|
+
* consumer URLs are stable (e.g. emails can deep-link to
|
|
11
|
+
* `https://site.example/forgot-password`). The route 302s to the
|
|
12
|
+
* issuer's hosted page.
|
|
13
|
+
*
|
|
14
|
+
* Returns 404 when the `password` provider isn't configured.
|
|
15
|
+
*/
|
|
16
|
+
export declare function GET(_context: ForgotPasswordContext): Promise<Response>;
|
|
6
17
|
export {};
|
|
7
18
|
//# sourceMappingURL=forgot-password.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forgot-password.d.ts","sourceRoot":"","sources":["../../src/routes/forgot-password.ts"],"names":[],"mappings":"AAGA,UAAU,qBAAqB;IAC9B,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,EAAE,GAAG,CAAA;CACR;AAED,wBAAsB,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"forgot-password.d.ts","sourceRoot":"","sources":["../../src/routes/forgot-password.ts"],"names":[],"mappings":"AAGA,UAAU,qBAAqB;IAC9B,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,EAAE,GAAG,CAAA;CACR;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,GAAG,CAAC,QAAQ,EAAE,qBAAqB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAS5E"}
|
|
@@ -1,62 +1,22 @@
|
|
|
1
1
|
import { getConfig } from '../config-store.js';
|
|
2
2
|
import { getForgotPasswordUrl } from '../utils/openauth.js';
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Thin proxy into the issuer-hosted forgot-password flow.
|
|
5
|
+
*
|
|
6
|
+
* Password (and forgot/reset) are issuer-owned flows: the standalone
|
|
7
|
+
* identity-platform issuer hosts the UI. This route only exists so that
|
|
8
|
+
* consumer URLs are stable (e.g. emails can deep-link to
|
|
9
|
+
* `https://site.example/forgot-password`). The route 302s to the
|
|
10
|
+
* issuer's hosted page.
|
|
11
|
+
*
|
|
12
|
+
* Returns 404 when the `password` provider isn't configured.
|
|
13
|
+
*/
|
|
14
|
+
export async function GET(_context) {
|
|
4
15
|
const options = getConfig();
|
|
5
16
|
if (!options.providers.includes('password')) {
|
|
6
17
|
return new Response('Not found', { status: 404 });
|
|
7
18
|
}
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
const html = renderForgotPasswordPage(options, context.url, { sent, error });
|
|
11
|
-
return new Response(html, {
|
|
12
|
-
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
function renderForgotPasswordPage(options, url, data) {
|
|
16
|
-
const { loginPage } = options;
|
|
17
|
-
const actionUrl = getForgotPasswordUrl();
|
|
18
|
-
const redirectUri = new URL(options.forgotPasswordPath, url.origin).toString();
|
|
19
|
-
const resetUri = new URL(options.resetPasswordPath, url.origin).toString();
|
|
20
|
-
return `<!DOCTYPE html>
|
|
21
|
-
<html lang="en">
|
|
22
|
-
<head>
|
|
23
|
-
<meta charset="utf-8">
|
|
24
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
25
|
-
<title>Forgot password</title>
|
|
26
|
-
<style>
|
|
27
|
-
body { font-family: system-ui, sans-serif; max-width: 400px; margin: 80px auto; padding: 0 20px; }
|
|
28
|
-
h1 { text-align: center; }
|
|
29
|
-
.logo { display: block; margin: 0 auto 16px; max-height: 48px; }
|
|
30
|
-
.btn { display: block; width: 100%; padding: 12px; margin: 8px 0; border: 1px solid #ccc; border-radius: 6px; font-size: 16px; cursor: pointer; text-align: center; text-decoration: none; box-sizing: border-box; }
|
|
31
|
-
input[type="email"] { display: block; width: 100%; padding: 10px; margin: 8px 0; border: 1px solid #ccc; border-radius: 6px; font-size: 16px; box-sizing: border-box; }
|
|
32
|
-
.error { color: #c00; text-align: center; margin: 12px 0; }
|
|
33
|
-
.success { color: #080; text-align: center; margin: 12px 0; }
|
|
34
|
-
.brand { text-align: center; color: #666; margin-bottom: 4px; }
|
|
35
|
-
.back-link { display: block; text-align: center; margin-top: 16px; font-size: 14px; color: #555; }
|
|
36
|
-
p.hint { text-align: center; color: #666; font-size: 14px; }
|
|
37
|
-
</style>
|
|
38
|
-
</head>
|
|
39
|
-
<body>
|
|
40
|
-
${loginPage.logoUrl ? `<img src="${loginPage.logoUrl}" alt="${loginPage.brandName || ''}" class="logo">` : ''}
|
|
41
|
-
${loginPage.brandName ? `<p class="brand">${loginPage.brandName}</p>` : ''}
|
|
42
|
-
<h1>Forgot password</h1>
|
|
43
|
-
${data.error ? '<p class="error">Something went wrong. Please try again.</p>' : ''}
|
|
44
|
-
${data.sent
|
|
45
|
-
? `
|
|
46
|
-
<p class="success">Check your email for a reset link.</p>
|
|
47
|
-
<p class="hint">If you don't see it, check your spam folder.</p>
|
|
48
|
-
`
|
|
49
|
-
: `
|
|
50
|
-
<p class="hint">Enter your email and we'll send you a link to reset your password.</p>
|
|
51
|
-
<form method="POST" action="${actionUrl}">
|
|
52
|
-
<input type="email" name="email" placeholder="Email address" required>
|
|
53
|
-
<input type="hidden" name="redirect_uri" value="${redirectUri}">
|
|
54
|
-
<input type="hidden" name="reset_uri" value="${resetUri}">
|
|
55
|
-
<button type="submit" class="btn">Send reset link</button>
|
|
56
|
-
</form>
|
|
57
|
-
`}
|
|
58
|
-
<a href="${options.loginPath}" class="back-link">Back to sign in</a>
|
|
59
|
-
</body>
|
|
60
|
-
</html>`;
|
|
19
|
+
const target = getForgotPasswordUrl();
|
|
20
|
+
return Response.redirect(target, 302);
|
|
61
21
|
}
|
|
62
22
|
//# sourceMappingURL=forgot-password.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forgot-password.js","sourceRoot":"","sources":["../../src/routes/forgot-password.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAO3D,MAAM,CAAC,KAAK,UAAU,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"forgot-password.js","sourceRoot":"","sources":["../../src/routes/forgot-password.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAO3D;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,GAAG,CAAC,QAA+B;IACxD,MAAM,OAAO,GAAG,SAAS,EAAE,CAAA;IAE3B,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QAC7C,OAAO,IAAI,QAAQ,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAClD,CAAC;IAED,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAA;IACrC,OAAO,QAAQ,CAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;AACtC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/routes/login.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/routes/login.ts"],"names":[],"mappings":"AAKA,UAAU,YAAY;IACrB,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,EAAE,GAAG,CAAA;CACR;AAED,wBAAsB,GAAG,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,QAAQ,CAAC,CAiClE"}
|
package/dist/routes/login.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { loginPageScript } from '../client/login-page.js';
|
|
2
2
|
import { getConfig } from '../config-store.js';
|
|
3
3
|
import { setAuthFlowCookies } from '../utils/cookies.js';
|
|
4
|
-
import { createAuthFlow, getAuthorizationUrl
|
|
5
|
-
function _shouldShowForgotLink(options) {
|
|
6
|
-
return options.providers.includes('password');
|
|
7
|
-
}
|
|
4
|
+
import { createAuthFlow, getAuthorizationUrl } from '../utils/openauth.js';
|
|
8
5
|
export async function GET(context) {
|
|
9
6
|
const options = getConfig();
|
|
10
7
|
const redirectPath = context.url.searchParams.get('redirect') || '/';
|
|
@@ -14,10 +11,21 @@ export async function GET(context) {
|
|
|
14
11
|
const google = options.providers.includes('google')
|
|
15
12
|
? await getAuthorizationUrl(context, 'google', redirectPath, authFlow)
|
|
16
13
|
: null;
|
|
14
|
+
const password = options.providers.includes('password')
|
|
15
|
+
? await getAuthorizationUrl(context, 'password', redirectPath, authFlow)
|
|
16
|
+
: null;
|
|
17
17
|
const emailCode = options.providers.includes('email-code')
|
|
18
18
|
? await getAuthorizationUrl(context, 'code', redirectPath, authFlow)
|
|
19
19
|
: null;
|
|
20
|
-
const html = renderLoginPage(options, {
|
|
20
|
+
const html = renderLoginPage(options, {
|
|
21
|
+
google,
|
|
22
|
+
password,
|
|
23
|
+
emailCode,
|
|
24
|
+
redirectPath,
|
|
25
|
+
error,
|
|
26
|
+
reset,
|
|
27
|
+
authFlow,
|
|
28
|
+
});
|
|
21
29
|
const response = new Response(html, {
|
|
22
30
|
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
23
31
|
});
|
|
@@ -26,8 +34,7 @@ export async function GET(context) {
|
|
|
26
34
|
}
|
|
27
35
|
function renderLoginPage(options, data) {
|
|
28
36
|
const { loginPage, providers } = options;
|
|
29
|
-
const
|
|
30
|
-
const passwordUrl = getPasswordLoginUrl({ url: new URL(options.issuer) });
|
|
37
|
+
const cookiePrefix = options.cookiePrefix;
|
|
31
38
|
return `<!DOCTYPE html>
|
|
32
39
|
<html lang="en">
|
|
33
40
|
<head>
|
|
@@ -41,11 +48,10 @@ function renderLoginPage(options, data) {
|
|
|
41
48
|
.btn { display: block; width: 100%; padding: 12px; margin: 8px 0; border: 1px solid #ccc; border-radius: 6px; font-size: 16px; cursor: pointer; text-align: center; text-decoration: none; box-sizing: border-box; }
|
|
42
49
|
.btn-google { background: #4285f4; color: white; border: none; }
|
|
43
50
|
.divider { text-align: center; margin: 20px 0; color: #888; }
|
|
44
|
-
input[type="email"], input[type="password"] { display: block; width: 100%; padding: 10px; margin: 8px 0; border: 1px solid #ccc; border-radius: 6px; font-size: 16px; box-sizing: border-box; }
|
|
45
51
|
.error { color: #c00; text-align: center; margin: 12px 0; }
|
|
46
52
|
.success { color: #080; text-align: center; margin: 12px 0; }
|
|
47
53
|
.brand { text-align: center; color: #666; margin-bottom: 4px; }
|
|
48
|
-
.forgot-link { display: block; text-align:
|
|
54
|
+
.forgot-link { display: block; text-align: center; font-size: 14px; margin: 12px 0; color: #555; }
|
|
49
55
|
</style>
|
|
50
56
|
</head>
|
|
51
57
|
<body>
|
|
@@ -55,25 +61,13 @@ function renderLoginPage(options, data) {
|
|
|
55
61
|
${data.reset ? '<p class="success">Password reset successfully. Sign in with your new password.</p>' : ''}
|
|
56
62
|
${data.error ? '<p class="error">Authentication failed. Please try again.</p>' : ''}
|
|
57
63
|
${data.google ? `<a href="${data.google.url}" class="btn btn-google" data-provider="google">Sign in with Google</a>` : ''}
|
|
58
|
-
${data.google && (
|
|
59
|
-
${data.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
? `
|
|
64
|
-
<form method="POST" action="${passwordUrl}">
|
|
65
|
-
<input type="email" name="email" placeholder="Email address" required>
|
|
66
|
-
<input type="password" name="password" placeholder="Password" required>
|
|
67
|
-
<a href="${options.forgotPasswordPath}" class="forgot-link">Forgot password?</a>
|
|
68
|
-
<input type="hidden" name="redirect_uri" value="${data.authFlow.redirectUri}">
|
|
69
|
-
<input type="hidden" name="client_id" value="${data.authFlow.clientId}">
|
|
70
|
-
<input type="hidden" name="state" value="${data.authFlow.state}">
|
|
71
|
-
<input type="hidden" name="code_challenge" value="${data.authFlow.challenge}">
|
|
72
|
-
<input type="hidden" name="code_challenge_method" value="S256">
|
|
73
|
-
<button type="submit" class="btn" data-provider="password">Sign in with password</button>
|
|
74
|
-
</form>`
|
|
64
|
+
${data.google && (data.password || data.emailCode) ? '<div class="divider">or</div>' : ''}
|
|
65
|
+
${data.password ? `<a href="${data.password.url}" class="btn" data-provider="password">Sign in with password</a>` : ''}
|
|
66
|
+
${data.emailCode ? `<a href="${data.emailCode.url}" class="btn" data-provider="email-code">Sign in with email</a>` : ''}
|
|
67
|
+
${providers.includes('password')
|
|
68
|
+
? `<a href="${options.forgotPasswordPath}" class="forgot-link">Forgot password?</a>`
|
|
75
69
|
: ''}
|
|
76
|
-
<script>${loginPageScript()}</script>
|
|
70
|
+
<script>${loginPageScript(cookiePrefix)}</script>
|
|
77
71
|
</body>
|
|
78
72
|
</html>`;
|
|
79
73
|
}
|
package/dist/routes/login.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/routes/login.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AACxD,OAAO,
|
|
1
|
+
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/routes/login.ts"],"names":[],"mappings":"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"}
|