@growth-labs/auth 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/index.js +4 -1
  3. package/dist/index.js.map +1 -1
  4. package/package.json +7 -2
  5. package/src/client/login-page.ts +12 -0
  6. package/src/cloudflare-workers.d.ts +3 -0
  7. package/src/config-store.ts +70 -0
  8. package/src/index.ts +177 -0
  9. package/src/middleware/auth.ts +88 -0
  10. package/src/options.ts +75 -0
  11. package/src/renderers.ts +182 -0
  12. package/src/routes/api/code-request.ts +33 -0
  13. package/src/routes/api/code-verify.ts +35 -0
  14. package/src/routes/api/password-reset-confirm.ts +36 -0
  15. package/src/routes/api/password-reset-request.ts +32 -0
  16. package/src/routes/api/password-verify.ts +35 -0
  17. package/src/routes/api/shared.ts +130 -0
  18. package/src/routes/callback.ts +105 -0
  19. package/src/routes/flow.ts +53 -0
  20. package/src/routes/forgot-password.ts +31 -0
  21. package/src/routes/login.ts +130 -0
  22. package/src/routes/logout.ts +59 -0
  23. package/src/routes/password.ts +27 -0
  24. package/src/routes/reset-password.ts +33 -0
  25. package/src/routes/verify-code.ts +28 -0
  26. package/src/runtime-bridge.ts +15 -0
  27. package/src/runtime.ts +5 -0
  28. package/src/server.ts +185 -0
  29. package/src/types.ts +158 -0
  30. package/src/utils/analytics.ts +56 -0
  31. package/src/utils/client-secret.ts +21 -0
  32. package/src/utils/cookies.ts +134 -0
  33. package/src/utils/encoding.ts +5 -0
  34. package/src/utils/env-marker.ts +39 -0
  35. package/src/utils/identity-cookie.ts +138 -0
  36. package/src/utils/identity.ts +240 -0
  37. package/src/utils/index.ts +19 -0
  38. package/src/utils/issuer.ts +7 -0
  39. package/src/utils/openauth.ts +163 -0
  40. package/src/utils/pkce.ts +16 -0
  41. package/src/utils/state.ts +20 -0
  42. package/src/utils/tokens.ts +128 -0
  43. package/src/virtual.d.ts +5 -0
@@ -0,0 +1,27 @@
1
+ import { getConfig } from '../config-store.js'
2
+ import { getRenderer, passwordRendererPayload, renderHtmlResponse } from '../renderers.js'
3
+ import { type RouteContext, withAuthFlowCookies } from './flow.js'
4
+
5
+ export async function GET(context: RouteContext): Promise<Response> {
6
+ const options = getConfig()
7
+ if (!options.providers.includes('password')) {
8
+ return new Response('Not found', { status: 404 })
9
+ }
10
+
11
+ const renderer = getRenderer('password')
12
+ if (!renderer) {
13
+ return new Response('Not found', { status: 404 })
14
+ }
15
+
16
+ const redirectPath = context.url.searchParams.get('redirect') || '/'
17
+ const response = await renderHtmlResponse(
18
+ renderer(
19
+ passwordRendererPayload(options, {
20
+ email: context.url.searchParams.get('email'),
21
+ error: context.url.searchParams.get('error'),
22
+ redirectPath,
23
+ }),
24
+ ),
25
+ )
26
+ return withAuthFlowCookies(context, options, response, redirectPath, 'password')
27
+ }
@@ -0,0 +1,33 @@
1
+ import { getConfig } from '../config-store.js'
2
+ import { getRenderer, renderHtmlResponse, resetPasswordRendererPayload } from '../renderers.js'
3
+ import { getResetPasswordUrl } from '../utils/openauth.js'
4
+
5
+ interface ResetPasswordContext {
6
+ request: Request
7
+ url: URL
8
+ }
9
+
10
+ export async function GET(context: ResetPasswordContext): Promise<Response> {
11
+ const options = getConfig()
12
+
13
+ if (!options.providers.includes('password')) {
14
+ return new Response('Not found', { status: 404 })
15
+ }
16
+
17
+ const target = new URL(getResetPasswordUrl())
18
+ target.search = context.url.search
19
+ const renderer = getRenderer('resetPassword')
20
+ if (renderer) {
21
+ return renderHtmlResponse(
22
+ renderer(
23
+ resetPasswordRendererPayload(options, {
24
+ resetPasswordUrl: target.toString(),
25
+ error: context.url.searchParams.get('error'),
26
+ token: context.url.searchParams.get('token'),
27
+ }),
28
+ ),
29
+ )
30
+ }
31
+
32
+ return Response.redirect(target.toString(), 302)
33
+ }
@@ -0,0 +1,28 @@
1
+ import { getConfig } from '../config-store.js'
2
+ import { getRenderer, renderHtmlResponse, verifyCodeRendererPayload } from '../renderers.js'
3
+ import { type RouteContext, withAuthFlowCookies } from './flow.js'
4
+
5
+ export async function GET(context: RouteContext): Promise<Response> {
6
+ const options = getConfig()
7
+ if (!options.providers.includes('email-code')) {
8
+ return new Response('Not found', { status: 404 })
9
+ }
10
+
11
+ const renderer = getRenderer('verifyCode')
12
+ if (!renderer) {
13
+ return new Response('Not found', { status: 404 })
14
+ }
15
+
16
+ const redirectPath = context.url.searchParams.get('redirect') || '/'
17
+ const response = await renderHtmlResponse(
18
+ renderer(
19
+ verifyCodeRendererPayload(options, {
20
+ email: context.url.searchParams.get('email'),
21
+ error: context.url.searchParams.get('error'),
22
+ sent: context.url.searchParams.get('sent'),
23
+ redirectPath,
24
+ }),
25
+ ),
26
+ )
27
+ return withAuthFlowCookies(context, options, response, redirectPath, 'email-code')
28
+ }
@@ -0,0 +1,15 @@
1
+ // Side-effect module: stashes the Cloudflare Worker runtime env onto
2
+ // `globalThis.__GL_AUTH_RUNTIME_ENV__` so that `src/config-store.ts` can
3
+ // read env values without statically importing `cloudflare:workers`.
4
+ //
5
+ // Loaded by `src/middleware/auth.ts` only. The package main entry
6
+ // (`dist/index.js`) does NOT statically import this file, which keeps the
7
+ // pure-Node import gate green — `cloudflare:workers` only enters the
8
+ // graph through middleware/routes that already needed it.
9
+ import { getRuntimeEnv } from './runtime.js'
10
+
11
+ ;(
12
+ globalThis as typeof globalThis & {
13
+ __GL_AUTH_RUNTIME_ENV__?: Record<string, unknown>
14
+ }
15
+ ).__GL_AUTH_RUNTIME_ENV__ = getRuntimeEnv()
package/src/runtime.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { env as cloudflareEnv } from 'cloudflare:workers'
2
+
3
+ export function getRuntimeEnv(): Record<string, unknown> {
4
+ return cloudflareEnv as Record<string, unknown>
5
+ }
package/src/server.ts ADDED
@@ -0,0 +1,185 @@
1
+ import { getConfig } from './config-store.js'
2
+ import type { ResolvedAuthOptions } from './options.js'
3
+ import { createClientSecretBasicAuthHeader, resolveClientSecret } from './utils/client-secret.js'
4
+ import { getCookie } from './utils/cookies.js'
5
+ import { resolveInternalIssuer } from './utils/issuer.js'
6
+ import { createPKCEChallenge } from './utils/pkce.js'
7
+
8
+ export type AuthClientCodeResult = { code: string } | { error: string }
9
+ export type AuthClientOkResult = { ok: true } | { error: string }
10
+
11
+ export interface AuthClientContext {
12
+ request: Request
13
+ url: URL
14
+ }
15
+
16
+ export interface AuthClient {
17
+ password: {
18
+ verify(input: { email: string; password: string }): Promise<AuthClientCodeResult>
19
+ reset: {
20
+ request(input: { email: string }): Promise<AuthClientOkResult>
21
+ confirm(input: { token: string; newPassword: string }): Promise<AuthClientOkResult>
22
+ }
23
+ }
24
+ code: {
25
+ request(input: { email: string }): Promise<AuthClientOkResult>
26
+ verify(input: { email: string; code: string }): Promise<AuthClientCodeResult>
27
+ }
28
+ }
29
+
30
+ interface AuthFlowPayload {
31
+ client_id: string
32
+ redirect_uri: string
33
+ code_challenge: string
34
+ code_challenge_method: 'S256'
35
+ state: string
36
+ resource?: string
37
+ }
38
+
39
+ function clientId(options: ResolvedAuthOptions, context: AuthClientContext): string {
40
+ return options.clientId || context.url.hostname
41
+ }
42
+
43
+ function redirectUri(options: ResolvedAuthOptions, context: AuthClientContext): string {
44
+ return new URL(options.callbackPath, context.url.origin).toString()
45
+ }
46
+
47
+ async function authFlowPayload(
48
+ options: ResolvedAuthOptions,
49
+ context: AuthClientContext,
50
+ ): Promise<AuthFlowPayload | { error: string }> {
51
+ const verifier = getCookie(context.request, `${options.cookiePrefix}_pkce`)
52
+ const state = getCookie(context.request, `${options.cookiePrefix}_state`)
53
+ if (!verifier || !state) {
54
+ return { error: 'missing_auth_flow' }
55
+ }
56
+
57
+ return {
58
+ client_id: clientId(options, context),
59
+ redirect_uri: redirectUri(options, context),
60
+ code_challenge: await createPKCEChallenge(verifier),
61
+ code_challenge_method: 'S256',
62
+ state,
63
+ ...(options.resource && { resource: options.resource }),
64
+ }
65
+ }
66
+
67
+ function headersFor(
68
+ options: ResolvedAuthOptions,
69
+ context: AuthClientContext,
70
+ ): Record<string, string> {
71
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' }
72
+ const secret = resolveClientSecret(options.clientSecret)
73
+ if (secret) {
74
+ headers.Authorization = createClientSecretBasicAuthHeader(clientId(options, context), secret)
75
+ }
76
+ return headers
77
+ }
78
+
79
+ function isRecord(value: unknown): value is Record<string, unknown> {
80
+ return typeof value === 'object' && value !== null
81
+ }
82
+
83
+ async function readJson(response: Response): Promise<Record<string, unknown>> {
84
+ try {
85
+ const body = await response.json()
86
+ return isRecord(body) ? body : {}
87
+ } catch {
88
+ return {}
89
+ }
90
+ }
91
+
92
+ async function postJson<T extends AuthClientCodeResult | AuthClientOkResult>(
93
+ options: ResolvedAuthOptions,
94
+ context: AuthClientContext,
95
+ pathname: string,
96
+ body: Record<string, unknown>,
97
+ ): Promise<T> {
98
+ try {
99
+ const response = await fetch(`${resolveInternalIssuer(options)}${pathname}`, {
100
+ method: 'POST',
101
+ headers: headersFor(options, context),
102
+ body: JSON.stringify(body),
103
+ })
104
+ const json = await readJson(response)
105
+
106
+ if (typeof json.error === 'string') {
107
+ return { error: json.error } as T
108
+ }
109
+ if (!response.ok) {
110
+ return { error: 'request_failed' } as T
111
+ }
112
+ if (typeof json.code === 'string') {
113
+ return { code: json.code } as T
114
+ }
115
+ if (json.ok === true) {
116
+ return { ok: true } as T
117
+ }
118
+ return { error: 'invalid_response' } as T
119
+ } catch {
120
+ return { error: 'request_failed' } as T
121
+ }
122
+ }
123
+
124
+ export function authClient(context: AuthClientContext): AuthClient {
125
+ const options = getConfig()
126
+
127
+ return {
128
+ password: {
129
+ async verify(input) {
130
+ const flow = await authFlowPayload(options, context)
131
+ if ('error' in flow) return flow
132
+ return postJson<AuthClientCodeResult>(options, context, '/api/v1/auth/password/verify', {
133
+ ...flow,
134
+ email: input.email,
135
+ password: input.password,
136
+ provider: 'password',
137
+ })
138
+ },
139
+ reset: {
140
+ request(input) {
141
+ return postJson<AuthClientOkResult>(
142
+ options,
143
+ context,
144
+ '/api/v1/auth/password/reset/request',
145
+ {
146
+ email: input.email,
147
+ client_id: clientId(options, context),
148
+ reset_url_base: context.url.origin,
149
+ },
150
+ )
151
+ },
152
+ confirm(input) {
153
+ return postJson<AuthClientOkResult>(
154
+ options,
155
+ context,
156
+ '/api/v1/auth/password/reset/confirm',
157
+ {
158
+ client_id: clientId(options, context),
159
+ token: input.token,
160
+ new_password: input.newPassword,
161
+ },
162
+ )
163
+ },
164
+ },
165
+ },
166
+ code: {
167
+ request(input) {
168
+ return postJson<AuthClientOkResult>(options, context, '/api/v1/auth/code/request', {
169
+ email: input.email,
170
+ client_id: clientId(options, context),
171
+ })
172
+ },
173
+ async verify(input) {
174
+ const flow = await authFlowPayload(options, context)
175
+ if ('error' in flow) return flow
176
+ return postJson<AuthClientCodeResult>(options, context, '/api/v1/auth/code/verify', {
177
+ ...flow,
178
+ email: input.email,
179
+ code: input.code,
180
+ provider: 'code',
181
+ })
182
+ },
183
+ },
184
+ }
185
+ }
package/src/types.ts ADDED
@@ -0,0 +1,158 @@
1
+ export interface User {
2
+ id: string
3
+ email: string
4
+ name?: string | null
5
+ avatarUrl?: string | null
6
+ }
7
+
8
+ export type AuthenticatedUser = User
9
+ export type UnauthenticatedUser = null
10
+
11
+ export interface AuthRendererBrand {
12
+ title: string
13
+ brandName: string
14
+ logoUrl: string
15
+ }
16
+
17
+ export interface LoginRendererPayload {
18
+ providerUrls: {
19
+ google?: string
20
+ password?: string
21
+ emailCode?: string
22
+ }
23
+ errorCode?: 'auth_failed' | 'session_expired' | string
24
+ reset?: boolean
25
+ redirect: string
26
+ brand: AuthRendererBrand
27
+ }
28
+
29
+ export interface ForgotPasswordRendererPayload {
30
+ forgotPasswordUrl: string
31
+ passwordResetRequestUrl: string
32
+ errorCode?: string
33
+ sent?: boolean
34
+ loginUrl: string
35
+ brand: AuthRendererBrand
36
+ }
37
+
38
+ export interface ResetPasswordRendererPayload {
39
+ resetPasswordUrl: string
40
+ passwordResetConfirmUrl: string
41
+ errorCode?: string
42
+ token?: string
43
+ loginUrl: string
44
+ brand: AuthRendererBrand
45
+ }
46
+
47
+ export interface PasswordRendererPayload {
48
+ passwordVerifyUrl: string
49
+ forgotPasswordUrl: string
50
+ email?: string
51
+ errorCode?: string
52
+ redirect: string
53
+ loginUrl: string
54
+ brand: AuthRendererBrand
55
+ }
56
+
57
+ export interface VerifyCodeRendererPayload {
58
+ codeRequestUrl: string
59
+ codeVerifyUrl: string
60
+ email?: string
61
+ errorCode?: string
62
+ sent?: boolean
63
+ redirect: string
64
+ loginUrl: string
65
+ brand: AuthRendererBrand
66
+ }
67
+
68
+ export interface EmailCodeConfirmRendererPayload {
69
+ loginUrl: string
70
+ brand: AuthRendererBrand
71
+ }
72
+
73
+ export type AuthRendererResult = string | Response | Promise<string | Response>
74
+
75
+ export type LoginRenderer = (payload: LoginRendererPayload) => AuthRendererResult
76
+ export type ForgotPasswordRenderer = (payload: ForgotPasswordRendererPayload) => AuthRendererResult
77
+ export type ResetPasswordRenderer = (payload: ResetPasswordRendererPayload) => AuthRendererResult
78
+ export type PasswordRenderer = (payload: PasswordRendererPayload) => AuthRendererResult
79
+ export type VerifyCodeRenderer = (payload: VerifyCodeRendererPayload) => AuthRendererResult
80
+ export type EmailCodeConfirmRenderer = (
81
+ payload: EmailCodeConfirmRendererPayload,
82
+ ) => AuthRendererResult
83
+
84
+ /**
85
+ * A renderer registration for the `renderers` integration option.
86
+ *
87
+ * `module` is an ES module specifier resolvable by the consumer's bundler
88
+ * (path alias, package name, or relative path). `export` is the named
89
+ * export within that module that holds the renderer function. The Vite
90
+ * plugin emits a real static `import { <export> as <local> } from <module>`
91
+ * at the top of the virtual `virtual:growth-labs/auth/renderers` module,
92
+ * which makes the renderer fully bundleable and tree-shakeable in
93
+ * production. **Do not pass an inline async function** — that approach
94
+ * (via `Function.prototype.toString()`) crashed in production bundles in
95
+ * 0.3.6 because Vite's SSR transform rewrote dynamic imports inside the
96
+ * serialized body to `__vite_ssr_dynamic_import__()`, a helper that does
97
+ * not exist in production bundles. See `MIGRATION.md`.
98
+ */
99
+ export interface AuthRendererSpec {
100
+ module: string
101
+ export: string
102
+ }
103
+
104
+ export interface AuthRenderers {
105
+ login?: AuthRendererSpec
106
+ forgotPassword?: AuthRendererSpec
107
+ resetPassword?: AuthRendererSpec
108
+ password?: AuthRendererSpec
109
+ verifyCode?: AuthRendererSpec
110
+ emailCodeConfirm?: AuthRendererSpec
111
+ }
112
+
113
+ /**
114
+ * The shape of `virtual:growth-labs/auth/renderers` after the plugin
115
+ * resolves all registered specs into static ES imports. Internal — only
116
+ * the package's own routes import from this virtual module.
117
+ */
118
+ export interface ResolvedAuthRenderers {
119
+ login?: LoginRenderer
120
+ forgotPassword?: ForgotPasswordRenderer
121
+ resetPassword?: ResetPasswordRenderer
122
+ password?: PasswordRenderer
123
+ verifyCode?: VerifyCodeRenderer
124
+ emailCodeConfirm?: EmailCodeConfirmRenderer
125
+ }
126
+
127
+ export interface TokenRefreshResult {
128
+ accessToken: string
129
+ refreshToken: string
130
+ claims: {
131
+ sub: string
132
+ email: string
133
+ name?: string | null
134
+ image?: string
135
+ }
136
+ }
137
+
138
+ export interface TokenExchangeResult {
139
+ accessToken: string
140
+ refreshToken: string
141
+ isNewUser: boolean
142
+ provider: 'google' | 'password' | 'email-code' | 'unknown'
143
+ claims: {
144
+ sub: string
145
+ email: string
146
+ name?: string | null
147
+ image?: string
148
+ }
149
+ }
150
+
151
+ // Augment Astro's locals
152
+ declare global {
153
+ namespace App {
154
+ interface Locals {
155
+ user: User | null
156
+ }
157
+ }
158
+ }
@@ -0,0 +1,56 @@
1
+ type TrackFn = (context: unknown, event: string, props?: Record<string, unknown>) => Promise<void>
2
+
3
+ let trackFn: TrackFn | null = null
4
+ let resolved = false
5
+ const ANALYTICS_UTILS_MODULE = '@growth-labs/analytics/utils'
6
+
7
+ async function importOptionalModule(modulePath: string): Promise<Record<string, unknown> | null> {
8
+ try {
9
+ return (await import(/* @vite-ignore */ modulePath)) as Record<string, unknown>
10
+ } catch {
11
+ return null
12
+ }
13
+ }
14
+
15
+ async function resolveAnalytics(): Promise<void> {
16
+ if (resolved) return
17
+ resolved = true
18
+ const mod = await importOptionalModule(ANALYTICS_UTILS_MODULE)
19
+ let candidate: unknown
20
+ try {
21
+ candidate = mod?.trackServerEvent
22
+ } catch {
23
+ candidate = undefined
24
+ }
25
+ if (typeof candidate === 'function') {
26
+ trackFn = candidate as TrackFn
27
+ }
28
+ }
29
+
30
+ export async function emitAuthEvent(
31
+ context: unknown,
32
+ eventName: string,
33
+ data: Record<string, unknown>,
34
+ ): Promise<void> {
35
+ await resolveAnalytics()
36
+ if (!trackFn) return
37
+ return trackFn(context, eventName, data)
38
+ }
39
+
40
+ export function analyticsAvailable(context: { locals?: Record<string, unknown> }): boolean {
41
+ void context
42
+ return true
43
+ }
44
+
45
+ export async function trackAuthEvent(
46
+ context: unknown,
47
+ eventName: string,
48
+ data: Record<string, unknown> = {},
49
+ ): Promise<void> {
50
+ return emitAuthEvent(context, eventName, data)
51
+ }
52
+
53
+ export function _resetAnalytics(): void {
54
+ trackFn = null
55
+ resolved = false
56
+ }
@@ -0,0 +1,21 @@
1
+ import { getRuntimeEnv } from '../runtime.js'
2
+
3
+ export function resolveClientSecret(clientSecret?: string): string | undefined {
4
+ if (!clientSecret) return undefined
5
+
6
+ const runtimeValue = getRuntimeEnv()[clientSecret]
7
+ if (typeof runtimeValue === 'string' && runtimeValue.length > 0) {
8
+ return runtimeValue
9
+ }
10
+
11
+ return clientSecret
12
+ }
13
+
14
+ export function createClientSecretBasicAuthHeader(clientId: string, clientSecret: string): string {
15
+ return `Basic ${btoa(`${formEncode(clientId)}:${formEncode(clientSecret)}`)}`
16
+ }
17
+
18
+ function formEncode(value: string): string {
19
+ const params = new URLSearchParams({ value })
20
+ return params.toString().slice('value='.length)
21
+ }
@@ -0,0 +1,134 @@
1
+ import type { ResolvedAuthOptions } from '../options.js'
2
+
3
+ export interface CookieAttributes {
4
+ path?: string
5
+ maxAge?: number
6
+ httpOnly?: boolean
7
+ secure?: boolean
8
+ sameSite?: 'strict' | 'lax' | 'none'
9
+ domain?: string
10
+ }
11
+
12
+ const IDENTITY_COOKIE_NAME = 'gl_id'
13
+ const IDENTITY_COOKIE_MAX_AGE = 2592000
14
+
15
+ export function getCookie(request: Request, name: string): string | null {
16
+ const header = request.headers.get('cookie')
17
+ if (!header) return null
18
+ const match = header.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`))
19
+ return match ? decodeURIComponent(match[1]) : null
20
+ }
21
+
22
+ export function setCookie(
23
+ response: Response,
24
+ name: string,
25
+ value: string,
26
+ attributes: CookieAttributes = {},
27
+ ): void {
28
+ const parts = [`${name}=${encodeURIComponent(value)}`]
29
+ if (attributes.path) parts.push(`Path=${attributes.path}`)
30
+ if (attributes.maxAge !== undefined) parts.push(`Max-Age=${attributes.maxAge}`)
31
+ if (attributes.httpOnly) parts.push('HttpOnly')
32
+ if (attributes.secure) parts.push('Secure')
33
+ if (attributes.sameSite) parts.push(`SameSite=${attributes.sameSite}`)
34
+ if (attributes.domain) parts.push(`Domain=${attributes.domain}`)
35
+ response.headers.append('Set-Cookie', parts.join('; '))
36
+ }
37
+
38
+ export function clearCookie(response: Response, name: string, domain?: string, path = '/'): void {
39
+ setCookie(response, name, '', {
40
+ path,
41
+ maxAge: 0,
42
+ ...(domain && { domain }),
43
+ })
44
+ }
45
+
46
+ export function setIdentityCookie(response: Response, value: string): void {
47
+ setCookie(response, IDENTITY_COOKIE_NAME, value, {
48
+ path: '/',
49
+ maxAge: IDENTITY_COOKIE_MAX_AGE,
50
+ httpOnly: true,
51
+ secure: true,
52
+ sameSite: 'lax',
53
+ })
54
+ }
55
+
56
+ export function clearIdentityCookie(response: Response): void {
57
+ setCookie(response, IDENTITY_COOKIE_NAME, '', {
58
+ path: '/',
59
+ maxAge: 0,
60
+ httpOnly: true,
61
+ secure: true,
62
+ sameSite: 'lax',
63
+ })
64
+ }
65
+
66
+ export function setTokenCookies(
67
+ response: Response,
68
+ options: ResolvedAuthOptions,
69
+ accessToken: string,
70
+ refreshToken: string,
71
+ ): void {
72
+ const base: CookieAttributes = {
73
+ path: '/',
74
+ httpOnly: true,
75
+ secure: true,
76
+ sameSite: 'lax',
77
+ ...(options.sessionCookieDomain && { domain: options.sessionCookieDomain }),
78
+ }
79
+ setCookie(response, `${options.cookiePrefix}_at`, accessToken, {
80
+ ...base,
81
+ maxAge: options.accessTokenMaxAge,
82
+ })
83
+ setCookie(response, `${options.cookiePrefix}_rt`, refreshToken, {
84
+ ...base,
85
+ maxAge: options.refreshTokenMaxAge,
86
+ })
87
+ }
88
+
89
+ export function clearTokenCookies(response: Response, options: ResolvedAuthOptions): void {
90
+ clearCookie(response, `${options.cookiePrefix}_at`, options.sessionCookieDomain)
91
+ clearCookie(response, `${options.cookiePrefix}_rt`, options.sessionCookieDomain)
92
+ }
93
+
94
+ export function clearAuthFlowCookies(response: Response, options: ResolvedAuthOptions): void {
95
+ clearCookie(response, `${options.cookiePrefix}_pkce`, undefined, options.callbackPath)
96
+ clearCookie(response, `${options.cookiePrefix}_pkce`)
97
+ clearCookie(response, `${options.cookiePrefix}_state`)
98
+ clearCookie(response, `${options.cookiePrefix}_redirect`)
99
+ clearCookie(response, `${options.cookiePrefix}_provider`)
100
+ clearCookie(response, `${options.cookiePrefix}_auth_path`)
101
+ }
102
+
103
+ export function setAuthFlowCookies(
104
+ response: Response,
105
+ options: ResolvedAuthOptions,
106
+ verifier: string,
107
+ state: string,
108
+ redirectPath: string,
109
+ provider?: string,
110
+ flowOptions: { pkcePath?: string; additionalPkcePaths?: string[] } = {},
111
+ ): void {
112
+ const baseAttrs: CookieAttributes = {
113
+ path: '/',
114
+ httpOnly: true,
115
+ secure: true,
116
+ sameSite: 'lax',
117
+ maxAge: 600,
118
+ }
119
+ const pkcePaths = new Set([
120
+ flowOptions.pkcePath ?? options.callbackPath,
121
+ ...(flowOptions.additionalPkcePaths ?? []),
122
+ ])
123
+ for (const path of pkcePaths) {
124
+ setCookie(response, `${options.cookiePrefix}_pkce`, verifier, {
125
+ ...baseAttrs,
126
+ path,
127
+ })
128
+ }
129
+ setCookie(response, `${options.cookiePrefix}_state`, state, baseAttrs)
130
+ setCookie(response, `${options.cookiePrefix}_redirect`, redirectPath, baseAttrs)
131
+ if (provider) {
132
+ setCookie(response, `${options.cookiePrefix}_provider`, provider, baseAttrs)
133
+ }
134
+ }
@@ -0,0 +1,5 @@
1
+ export function base64url(bytes: Uint8Array): string {
2
+ let binary = ''
3
+ for (const byte of bytes) binary += String.fromCharCode(byte)
4
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '')
5
+ }