@growth-labs/auth 0.10.0 → 0.10.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 (82) hide show
  1. package/README.md +17 -5
  2. package/dist/config-store.d.ts.map +1 -1
  3. package/dist/config-store.js +12 -1
  4. package/dist/config-store.js.map +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +8 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/middleware/auth.d.ts.map +1 -1
  9. package/dist/middleware/auth.js +23 -3
  10. package/dist/middleware/auth.js.map +1 -1
  11. package/dist/options.d.ts +4 -3
  12. package/dist/options.d.ts.map +1 -1
  13. package/dist/options.js +15 -1
  14. package/dist/options.js.map +1 -1
  15. package/dist/routes/api/code-request.d.ts.map +1 -1
  16. package/dist/routes/api/code-request.js +5 -1
  17. package/dist/routes/api/code-request.js.map +1 -1
  18. package/dist/routes/api/code-verify.d.ts.map +1 -1
  19. package/dist/routes/api/code-verify.js +5 -1
  20. package/dist/routes/api/code-verify.js.map +1 -1
  21. package/dist/routes/api/password-reset-confirm.d.ts.map +1 -1
  22. package/dist/routes/api/password-reset-confirm.js +5 -1
  23. package/dist/routes/api/password-reset-confirm.js.map +1 -1
  24. package/dist/routes/api/password-reset-request.d.ts.map +1 -1
  25. package/dist/routes/api/password-reset-request.js +5 -1
  26. package/dist/routes/api/password-reset-request.js.map +1 -1
  27. package/dist/routes/api/password-verify.d.ts.map +1 -1
  28. package/dist/routes/api/password-verify.js +5 -1
  29. package/dist/routes/api/password-verify.js.map +1 -1
  30. package/dist/routes/api/shared.d.ts +1 -0
  31. package/dist/routes/api/shared.d.ts.map +1 -1
  32. package/dist/routes/api/shared.js +26 -2
  33. package/dist/routes/api/shared.js.map +1 -1
  34. package/dist/routes/callback.d.ts.map +1 -1
  35. package/dist/routes/callback.js +12 -3
  36. package/dist/routes/callback.js.map +1 -1
  37. package/dist/routes/logout.d.ts.map +1 -1
  38. package/dist/routes/logout.js +35 -6
  39. package/dist/routes/logout.js.map +1 -1
  40. package/dist/server.d.ts +2 -0
  41. package/dist/server.d.ts.map +1 -1
  42. package/dist/server.js +15 -4
  43. package/dist/server.js.map +1 -1
  44. package/dist/utils/client-secret-literal.d.ts +13 -0
  45. package/dist/utils/client-secret-literal.d.ts.map +1 -0
  46. package/dist/utils/client-secret-literal.js +50 -0
  47. package/dist/utils/client-secret-literal.js.map +1 -0
  48. package/dist/utils/client-secret.d.ts +12 -1
  49. package/dist/utils/client-secret.d.ts.map +1 -1
  50. package/dist/utils/client-secret.js +108 -14
  51. package/dist/utils/client-secret.js.map +1 -1
  52. package/dist/utils/fail-closed.d.ts +14 -0
  53. package/dist/utils/fail-closed.d.ts.map +1 -0
  54. package/dist/utils/fail-closed.js +53 -0
  55. package/dist/utils/fail-closed.js.map +1 -0
  56. package/dist/utils/index.d.ts +4 -1
  57. package/dist/utils/index.d.ts.map +1 -1
  58. package/dist/utils/index.js +3 -1
  59. package/dist/utils/index.js.map +1 -1
  60. package/dist/utils/openauth.d.ts +7 -1
  61. package/dist/utils/openauth.d.ts.map +1 -1
  62. package/dist/utils/openauth.js +14 -5
  63. package/dist/utils/openauth.js.map +1 -1
  64. package/package.json +2 -2
  65. package/src/config-store.ts +13 -1
  66. package/src/index.ts +11 -1
  67. package/src/middleware/auth.ts +30 -3
  68. package/src/options.ts +16 -1
  69. package/src/routes/api/code-request.ts +5 -0
  70. package/src/routes/api/code-verify.ts +5 -0
  71. package/src/routes/api/password-reset-confirm.ts +5 -0
  72. package/src/routes/api/password-reset-request.ts +5 -0
  73. package/src/routes/api/password-verify.ts +5 -0
  74. package/src/routes/api/shared.ts +33 -2
  75. package/src/routes/callback.ts +12 -2
  76. package/src/routes/logout.ts +35 -6
  77. package/src/server.ts +21 -4
  78. package/src/utils/client-secret-literal.ts +71 -0
  79. package/src/utils/client-secret.ts +132 -18
  80. package/src/utils/fail-closed.ts +74 -0
  81. package/src/utils/index.ts +14 -1
  82. package/src/utils/openauth.ts +29 -5
@@ -1 +1 @@
1
- {"version":3,"file":"openauth.d.ts","sourceRoot":"","sources":["../../src/utils/openauth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAOtD,MAAM,WAAW,QAAQ;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;CACnB;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAAE,GAAG,EAAE,GAAG,CAAA;CAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAY7E;AAED,wBAAsB,mBAAmB,CACxC,OAAO,EAAE;IAAE,GAAG,EAAE,GAAG,CAAA;CAAE,EACrB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAqB/D;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE;IAAE,GAAG,EAAE,GAAG,CAAA;CAAE,GAAG,MAAM,CAGlE;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE;IAAE,GAAG,EAAE,GAAG,CAAA;CAAE,GAAG,MAAM,CAG9D;AAED,wBAAgB,oBAAoB,IAAI,MAAM,CAG7C;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAG5C;AAYD,wBAAsB,YAAY,CACjC,WAAW,EAAE,GAAG,EAChB,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,kBAAkB,CAAC,EAAE,mBAAmB,CAAC,UAAU,CAAC,GAClD,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAmDrC;AAED,wBAAsB,WAAW,CAChC,YAAY,EAAE,MAAM,EACpB,QAAQ,CAAC,EAAE,MAAM,EACjB,YAAY,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAqBf"}
1
+ {"version":3,"file":"openauth.d.ts","sourceRoot":"","sources":["../../src/utils/openauth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AACtD,OAAO,EACN,KAAK,+BAA+B,EAGpC,MAAM,oBAAoB,CAAA;AAM3B,MAAM,WAAW,QAAQ;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;CACnB;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAAE,GAAG,EAAE,GAAG,CAAA;CAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAY7E;AAED,wBAAsB,mBAAmB,CACxC,OAAO,EAAE;IAAE,GAAG,EAAE,GAAG,CAAA;CAAE,EACrB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAqB/D;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE;IAAE,GAAG,EAAE,GAAG,CAAA;CAAE,GAAG,MAAM,CAGlE;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE;IAAE,GAAG,EAAE,GAAG,CAAA;CAAE,GAAG,MAAM,CAG9D;AAED,wBAAgB,oBAAoB,IAAI,MAAM,CAG7C;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAG5C;AAgBD,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,YAAY,EAAE,IAAI,CAAA;IAC3B,QAAQ,CAAC,IAAI,EAAE,+BAA+B,CAAA;CAC9C;AAID,eAAO,MAAM,uBAAuB,EAAE,qBAGpC,CAAA;AAEF,wBAAsB,YAAY,CACjC,WAAW,EAAE,GAAG,EAChB,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,EACpB,kBAAkB,CAAC,EAAE,mBAAmB,CAAC,UAAU,CAAC,EACpD,OAAO,CAAC,EAAE,OAAO,GACf,OAAO,CAAC,mBAAmB,GAAG,qBAAqB,GAAG,IAAI,CAAC,CAsD7D;AAED,wBAAsB,WAAW,CAChC,YAAY,EAAE,MAAM,EACpB,QAAQ,CAAC,EAAE,MAAM,EACjB,YAAY,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAqBf"}
@@ -1,5 +1,5 @@
1
1
  import { getConfig } from '../config-store.js';
2
- import { createClientSecretBasicAuthHeader, resolveClientSecret } from './client-secret.js';
2
+ import { createClientSecretBasicAuthHeader, resolveClientSecret, } from './client-secret.js';
3
3
  import { resolveInternalIssuer } from './issuer.js';
4
4
  import { generatePKCE } from './pkce.js';
5
5
  import { generateState } from './state.js';
@@ -58,17 +58,26 @@ function tokenResponseHasTokens(data) {
58
58
  return (typeof data.access_token === 'string' &&
59
59
  typeof data.refresh_token === 'string');
60
60
  }
61
- export async function exchangeCode(callbackUrl, code, codeVerifier, providerFromCookie) {
61
+ // Backward-compatible exported sentinel for callers that only need a bounded
62
+ // representative failure. Runtime exchangeCode results carry the actual code.
63
+ export const EXCHANGE_SECRET_FAILURE = Object.freeze({
64
+ secretFailed: true,
65
+ code: 'BINDING_REJECTED',
66
+ });
67
+ export async function exchangeCode(callbackUrl, code, codeVerifier, providerFromCookie, request) {
62
68
  const options = getConfig();
63
69
  const issuer = resolveInternalIssuer(options);
64
70
  const clientId = options.clientId || callbackUrl.hostname;
65
71
  const redirectUri = new URL(options.callbackPath, callbackUrl.origin).toString();
66
- const clientSecret = await resolveClientSecret(options.clientSecret);
72
+ const secretResult = await resolveClientSecret(options.clientSecret, request);
73
+ if (!secretResult.ok) {
74
+ return Object.freeze({ secretFailed: true, code: secretResult.code });
75
+ }
67
76
  const headers = {
68
77
  'Content-Type': 'application/x-www-form-urlencoded',
69
78
  };
70
- if (clientSecret) {
71
- headers.Authorization = createClientSecretBasicAuthHeader(clientId, clientSecret);
79
+ if (secretResult.secret) {
80
+ headers.Authorization = createClientSecretBasicAuthHeader(clientId, secretResult.secret);
72
81
  }
73
82
  const body = new URLSearchParams({
74
83
  grant_type: 'authorization_code',
@@ -1 +1 @@
1
- {"version":3,"file":"openauth.js","sourceRoot":"","sources":["../../src/utils/openauth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"openauth.js","sourceRoot":"","sources":["../../src/utils/openauth.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growth-labs/auth",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "exports": {
@@ -57,7 +57,7 @@
57
57
  "typescript": "^5.8.0",
58
58
  "vite": "^7.3.5",
59
59
  "vitest": "^4.1.8",
60
- "@growth-labs/analytics": "0.14.6"
60
+ "@growth-labs/analytics": "0.14.7"
61
61
  },
62
62
  "scripts": {
63
63
  "build": "tsc",
@@ -1,4 +1,5 @@
1
1
  import type { ResolvedAuthOptions } from './options.js'
2
+ import { rehydrateClientSecretLiteral } from './utils/client-secret-literal.js'
2
3
  import { isEnvMarker, resolveEnvMarker } from './utils/env-marker.js'
3
4
 
4
5
  let _config: ResolvedAuthOptions | null = null
@@ -13,7 +14,7 @@ export function getConfig(): ResolvedAuthOptions {
13
14
  __GL_AUTH_CONFIG__?: ResolvedAuthOptions
14
15
  }
15
16
  ).__GL_AUTH_CONFIG__
16
- const base = runtimeConfig ?? _config
17
+ const base = runtimeConfig ? rehydrateRuntimeConfig(runtimeConfig) : _config
17
18
  if (!base) {
18
19
  throw new Error(
19
20
  '@growth-labs/auth: integration not configured. Add auth() to your astro.config.mjs integrations array.',
@@ -22,6 +23,17 @@ export function getConfig(): ResolvedAuthOptions {
22
23
  return resolveEnvMarkers(base)
23
24
  }
24
25
 
26
+ function rehydrateRuntimeConfig(base: ResolvedAuthOptions): ResolvedAuthOptions {
27
+ // The Vite global is emitted only after authOptionsSchema accepts a
28
+ // WeakSet-issued literal at build time. Treat the structural wire shape as
29
+ // transport encoding here, then re-issue the runtime object; the resolver
30
+ // itself never trusts the structural marker.
31
+ const clientSecret = rehydrateClientSecretLiteral(base.clientSecret)
32
+ return clientSecret === base.clientSecret
33
+ ? base
34
+ : ({ ...base, clientSecret } as ResolvedAuthOptions)
35
+ }
36
+
25
37
  /**
26
38
  * Resolves any `"env:VAR_NAME"` markers on auth-tuple fields against the
27
39
  * runtime env stashed by `src/runtime-bridge.ts`. The bridge is loaded
package/src/index.ts CHANGED
@@ -2,6 +2,10 @@ import { _setConfig } from './config-store.js'
2
2
  import type { AuthOptions } from './options.js'
3
3
  import { authOptionsSchema } from './options.js'
4
4
  import type { AuthRenderers } from './types.js'
5
+ import {
6
+ isClientSecretLiteral,
7
+ serializeClientSecretLiteral,
8
+ } from './utils/client-secret-literal.js'
5
9
 
6
10
  interface AstroIntegration {
7
11
  name: string
@@ -72,7 +76,13 @@ function isValidRendererSpec(value: unknown): value is { module: string; export:
72
76
  export default function auth(userOptions: AuthOptions): AstroIntegration {
73
77
  const options = authOptionsSchema.parse(userOptions)
74
78
  _setConfig(options)
75
- const serializableOptions = { ...options, renderers: {} }
79
+ const serializableOptions = {
80
+ ...options,
81
+ clientSecret: isClientSecretLiteral(options.clientSecret)
82
+ ? serializeClientSecretLiteral(options.clientSecret)
83
+ : options.clientSecret,
84
+ renderers: {},
85
+ }
76
86
 
77
87
  return {
78
88
  name: '@growth-labs/auth',
@@ -3,6 +3,7 @@ import '../runtime-bridge.js'
3
3
  import type { User } from '../types.js'
4
4
  import { resolveClientSecret } from '../utils/client-secret.js'
5
5
  import { getCookie, setTokenCookies } from '../utils/cookies.js'
6
+ import { secretResolutionFailureResponse } from '../utils/fail-closed.js'
6
7
  import { resolveInternalIssuer } from '../utils/issuer.js'
7
8
  import { refreshTokens, verifyAccessToken } from '../utils/tokens.js'
8
9
 
@@ -21,6 +22,27 @@ export async function onRequest(
21
22
  // 2. Attempt to authenticate
22
23
  let user: User | null = null
23
24
  let refreshed: Awaited<ReturnType<typeof refreshTokens>> = null
25
+ let resolvedClientSecret: string | undefined
26
+
27
+ // A request carrying a refresh token can enter the confidential-client
28
+ // refresh path if access-token verification fails. Resolve that request's
29
+ // configured secret before verification so a cold JWKS lookup can never
30
+ // precede discovery of a broken binding. The module-private Request-keyed
31
+ // resolver cache makes this the single read, and the exact bytes are reused
32
+ // below. A refresh token explicitly opts the request into this fail-closed
33
+ // confidential-session invariant even when its access token would verify;
34
+ // an access-token-only request never needs or reads the client secret.
35
+ if (refreshToken && options.clientSecret !== undefined) {
36
+ const secretResult = await resolveClientSecret(options.clientSecret, request)
37
+ if (!secretResult.ok) {
38
+ context.locals.user = null
39
+ return secretResolutionFailureResponse(request, {
40
+ surface: 'middleware_refresh',
41
+ code: secretResult.code,
42
+ })
43
+ }
44
+ resolvedClientSecret = secretResult.secret ?? undefined
45
+ }
24
46
 
25
47
  if (accessToken) {
26
48
  const verified = await verifyAccessToken(issuer, accessToken, { audience: options.resource })
@@ -39,8 +61,13 @@ export async function onRequest(
39
61
 
40
62
  if (!user && refreshToken) {
41
63
  const clientId = options.clientId || context.url.hostname
42
- const clientSecret = await resolveClientSecret(options.clientSecret)
43
- refreshed = await refreshTokens(issuer, refreshToken, clientId, clientSecret, options.resource)
64
+ refreshed = await refreshTokens(
65
+ issuer,
66
+ refreshToken,
67
+ clientId,
68
+ resolvedClientSecret,
69
+ options.resource,
70
+ )
44
71
  if (refreshed) {
45
72
  user = {
46
73
  id: refreshed.claims.sub,
@@ -66,7 +93,7 @@ export async function onRequest(
66
93
  // 5. Continue to the page
67
94
  const response = await next()
68
95
 
69
- // 6. If tokens were refreshed or response must be modified, clone and apply changes
96
+ // 6. If tokens were refreshed or the response must carry no-store, clone and apply.
70
97
  if (refreshed || context.locals.synthetic === true) {
71
98
  const modifiedResponse = new Response(response.body, {
72
99
  status: response.status,
package/src/options.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { z } from 'zod'
2
2
  import type { AuthRenderers } from './types.js'
3
+ import type { ClientSecretLiteral } from './utils/client-secret-literal.js'
4
+ import { isClientSecretLiteral } from './utils/client-secret-literal.js'
3
5
 
4
6
  // String accepted as either a literal URL or an "env:VAR_NAME" marker that
5
7
  // gets resolved against the Worker runtime env at request time. See
@@ -27,7 +29,20 @@ export const authOptionsSchema = z.object({
27
29
 
28
30
  // ─── Client identity ───
29
31
  clientId: stringOrEnvMarker.optional(),
30
- clientSecret: z.string().optional(),
32
+ // clientSecret accepts either a binding name (string resolved at request time
33
+ // against the Worker runtime env) or an explicit literalClientSecret() value
34
+ // (local-dev only; embedded in the serialized config). A bare string is
35
+ // always treated as a binding name — fail closed when the binding is absent.
36
+ // An empty or whitespace-only binding name is schema-rejected; it can never
37
+ // resolve to a usable secret and must not silently become a public client.
38
+ clientSecret: z
39
+ .union([
40
+ z.string().refine((v) => v.trim().length > 0, {
41
+ message: 'clientSecret binding name must not be empty or whitespace-only',
42
+ }),
43
+ z.custom<ClientSecretLiteral>(isClientSecretLiteral),
44
+ ])
45
+ .optional(),
31
46
  resource: urlOrEnvMarker,
32
47
 
33
48
  // ─── Cookie configuration ───
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  authClient,
3
+ failClosedOnSecretResolutionFailure,
3
4
  getConfig,
4
5
  type RouteContext,
5
6
  readForm,
@@ -9,6 +10,7 @@ import {
9
10
  } from './shared.js'
10
11
 
11
12
  export async function POST(context: RouteContext): Promise<Response> {
13
+ // Trust boundary: Origin rejection must precede config, secret, form, and issuer I/O.
12
14
  const crossSitePost = rejectCrossSitePost(context)
13
15
  if (crossSitePost) return crossSitePost
14
16
 
@@ -17,6 +19,9 @@ export async function POST(context: RouteContext): Promise<Response> {
17
19
  return new Response('Not found', { status: 404 })
18
20
  }
19
21
 
22
+ const secretError = await failClosedOnSecretResolutionFailure(context)
23
+ if (secretError) return secretError
24
+
20
25
  const form = await readForm(context)
21
26
  const email = await readFormString(form, 'email')
22
27
  const redirect = form.get('redirect')?.toString()
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  authClient,
3
3
  callbackRedirect,
4
+ failClosedOnSecretResolutionFailure,
4
5
  getConfig,
5
6
  type RouteContext,
6
7
  readForm,
@@ -9,6 +10,7 @@ import {
9
10
  } from './shared.js'
10
11
 
11
12
  export async function POST(context: RouteContext): Promise<Response> {
13
+ // Trust boundary: Origin rejection must precede config, secret, form, and issuer I/O.
12
14
  const crossSitePost = rejectCrossSitePost(context)
13
15
  if (crossSitePost) return crossSitePost
14
16
 
@@ -17,6 +19,9 @@ export async function POST(context: RouteContext): Promise<Response> {
17
19
  return new Response('Not found', { status: 404 })
18
20
  }
19
21
 
22
+ const secretError = await failClosedOnSecretResolutionFailure(context)
23
+ if (secretError) return secretError
24
+
20
25
  const form = await readForm(context)
21
26
  const email = await readFormString(form, 'email')
22
27
  const code = await readFormString(form, 'code')
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  authClient,
3
+ failClosedOnSecretResolutionFailure,
3
4
  getConfig,
4
5
  type RouteContext,
5
6
  readForm,
@@ -9,6 +10,7 @@ import {
9
10
  } from './shared.js'
10
11
 
11
12
  export async function POST(context: RouteContext): Promise<Response> {
13
+ // Trust boundary: Origin rejection must precede config, secret, form, and issuer I/O.
12
14
  const crossSitePost = rejectCrossSitePost(context)
13
15
  if (crossSitePost) return crossSitePost
14
16
 
@@ -17,6 +19,9 @@ export async function POST(context: RouteContext): Promise<Response> {
17
19
  return new Response('Not found', { status: 404 })
18
20
  }
19
21
 
22
+ const secretError = await failClosedOnSecretResolutionFailure(context)
23
+ if (secretError) return secretError
24
+
20
25
  const form = await readForm(context)
21
26
  const token = await readFormString(form, 'token')
22
27
  const password = await readFormString(form, 'password', { trim: false })
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  authClient,
3
+ failClosedOnSecretResolutionFailure,
3
4
  getConfig,
4
5
  type RouteContext,
5
6
  readForm,
@@ -9,6 +10,7 @@ import {
9
10
  } from './shared.js'
10
11
 
11
12
  export async function POST(context: RouteContext): Promise<Response> {
13
+ // Trust boundary: Origin rejection must precede config, secret, form, and issuer I/O.
12
14
  const crossSitePost = rejectCrossSitePost(context)
13
15
  if (crossSitePost) return crossSitePost
14
16
 
@@ -17,6 +19,9 @@ export async function POST(context: RouteContext): Promise<Response> {
17
19
  return new Response('Not found', { status: 404 })
18
20
  }
19
21
 
22
+ const secretError = await failClosedOnSecretResolutionFailure(context)
23
+ if (secretError) return secretError
24
+
20
25
  const form = await readForm(context)
21
26
  const email = await readFormString(form, 'email')
22
27
  const result = await authClient(context).password.reset.request({ email })
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  authClient,
3
3
  callbackRedirect,
4
+ failClosedOnSecretResolutionFailure,
4
5
  getConfig,
5
6
  type RouteContext,
6
7
  readForm,
@@ -9,6 +10,7 @@ import {
9
10
  } from './shared.js'
10
11
 
11
12
  export async function POST(context: RouteContext): Promise<Response> {
13
+ // Trust boundary: Origin rejection must precede config, secret, form, and issuer I/O.
12
14
  const crossSitePost = rejectCrossSitePost(context)
13
15
  if (crossSitePost) return crossSitePost
14
16
 
@@ -17,6 +19,9 @@ export async function POST(context: RouteContext): Promise<Response> {
17
19
  return new Response('Not found', { status: 404 })
18
20
  }
19
21
 
22
+ const secretError = await failClosedOnSecretResolutionFailure(context)
23
+ if (secretError) return secretError
24
+
20
25
  const form = await readForm(context)
21
26
  const email = await readFormString(form, 'email')
22
27
  const password = await readFormString(form, 'password', { trim: false })
@@ -7,6 +7,7 @@ import {
7
7
  emitAuthFailureEvent,
8
8
  emitAuthSuccessEvent,
9
9
  } from '../../utils/analytics.js'
10
+ import { resolveClientSecret } from '../../utils/client-secret.js'
10
11
  import {
11
12
  clearAuthFlowCookies,
12
13
  getCookie,
@@ -14,6 +15,7 @@ import {
14
15
  setIdentityCookie,
15
16
  setTokenCookies,
16
17
  } from '../../utils/cookies.js'
18
+ import { secretResolutionFailureResponse } from '../../utils/fail-closed.js'
17
19
  import { canonicalIdentitySubject } from '../../utils/identity.js'
18
20
  import { createIdentityCookiePayload, signIdentityCookie } from '../../utils/identity-cookie.js'
19
21
  import { exchangeCode } from '../../utils/openauth.js'
@@ -133,6 +135,22 @@ function routeResult(
133
135
  })
134
136
  }
135
137
 
138
+ // Returns a 503 fail-closed response when the configured clientSecret binding
139
+ // cannot be resolved; null when resolution succeeds or no secret is configured.
140
+ // Call this before any authClient() call to guarantee zero issuer fetch on failure.
141
+ export async function failClosedOnSecretResolutionFailure(
142
+ context: RouteContext,
143
+ ): Promise<Response | null> {
144
+ const options = getConfig()
145
+ if (options.clientSecret === undefined) return null
146
+ const secretResult = await resolveClientSecret(options.clientSecret, context.request)
147
+ if (secretResult.ok) return null
148
+ return secretResolutionFailureResponse(context.request, {
149
+ surface: 'route_preflight',
150
+ code: secretResult.code,
151
+ })
152
+ }
153
+
136
154
  export type { RouteContext }
137
155
  export { authClient, getConfig, readForm, redirectTo }
138
156
 
@@ -163,11 +181,24 @@ async function completeJsonAuth(
163
181
  )
164
182
  }
165
183
 
166
- const tokenResponse = await exchangeCode(context.url, code, codeVerifier, provider)
167
- if (!tokenResponse) {
184
+ const exchangeResult = await exchangeCode(
185
+ context.url,
186
+ code,
187
+ codeVerifier,
188
+ provider,
189
+ context.request,
190
+ )
191
+ if (exchangeResult !== null && 'secretFailed' in exchangeResult) {
192
+ return secretResolutionFailureResponse(context.request, {
193
+ surface: 'route_exchange',
194
+ code: exchangeResult.code,
195
+ })
196
+ }
197
+ if (!exchangeResult) {
168
198
  queueAuthFailure(context, provider, 'token_exchange_failed')
169
199
  return routeResult(context, errorPath, { ...errorParams, error: 'auth_failed' }, 'auth_failed')
170
200
  }
201
+ const tokenResponse = exchangeResult
171
202
 
172
203
  const redirectPath = getCookie(context.request, `${options.cookiePrefix}_redirect`) || '/'
173
204
  const redirect = new URL(redirectPath, context.url.origin).toString()
@@ -11,6 +11,7 @@ import {
11
11
  setIdentityCookie,
12
12
  setTokenCookies,
13
13
  } from '../utils/cookies.js'
14
+ import { secretResolutionFailureResponse } from '../utils/fail-closed.js'
14
15
  import { canonicalIdentitySubject } from '../utils/identity.js'
15
16
  import { createIdentityCookiePayload, signIdentityCookie } from '../utils/identity-cookie.js'
16
17
  import { exchangeCode } from '../utils/openauth.js'
@@ -106,18 +107,27 @@ export async function GET(context: CallbackContext): Promise<Response> {
106
107
  }
107
108
 
108
109
  // Exchange code for tokens
109
- const tokenResponse = await exchangeCode(
110
+ const exchangeResult = await exchangeCode(
110
111
  url,
111
112
  code,
112
113
  codeVerifier,
113
114
  provider === 'google' || provider === 'password' || provider === 'email-code'
114
115
  ? provider
115
116
  : undefined,
117
+ request,
116
118
  )
117
- if (!tokenResponse) {
119
+ // Typed distinction: secret binding failure → fail closed; other failure → auth_failed.
120
+ if (exchangeResult !== null && 'secretFailed' in exchangeResult) {
121
+ return secretResolutionFailureResponse(request, {
122
+ surface: 'callback_exchange',
123
+ code: exchangeResult.code,
124
+ })
125
+ }
126
+ if (!exchangeResult) {
118
127
  queueAuthFailure(context, provider, 'token_exchange_failed')
119
128
  return loginErrorResponse(request, url.origin, options.loginPath)
120
129
  }
130
+ const tokenResponse = exchangeResult
121
131
 
122
132
  // Build redirect response
123
133
  const redirectPath = getCookie(request, `${options.cookiePrefix}_redirect`) || '/'
@@ -1,5 +1,6 @@
1
1
  import { getConfig } from '../config-store.js'
2
2
  import { analyticsAvailable, emitAuthEvent } from '../utils/analytics.js'
3
+ import type { ClientSecretResolutionErrorCode } from '../utils/client-secret.js'
3
4
  import { resolveClientSecret } from '../utils/client-secret.js'
4
5
  import {
5
6
  clearAuthFlowCookies,
@@ -7,6 +8,7 @@ import {
7
8
  clearTokenCookies,
8
9
  getCookie,
9
10
  } from '../utils/cookies.js'
11
+ import { secretResolutionFailureResponse } from '../utils/fail-closed.js'
10
12
  import { revokeToken } from '../utils/openauth.js'
11
13
  import { resolveWaitUntil } from '../utils/wait-until.js'
12
14
 
@@ -26,13 +28,27 @@ export async function GET(context: LogoutContext): Promise<Response> {
26
28
  const clientId = options.clientId || context.url.hostname
27
29
  const waitUntil = resolveWaitUntil(context)
28
30
 
29
- // Revoke at issuer (non-blocking)
31
+ // Revoke at issuer (non-blocking).
32
+ // Public clients (no clientSecret configured) always attempt revocation.
33
+ // Confidential clients only attempt revocation when the secret resolves successfully;
34
+ // a binding failure skips revocation rather than sending unauthenticated requests
35
+ // to a confidential-client issuer endpoint.
36
+ let secretFailureCode: ClientSecretResolutionErrorCode | null = null
30
37
  if (refreshToken) {
31
- const clientSecret = await resolveClientSecret(options.clientSecret)
32
- const promise = clientSecret
33
- ? revokeToken(refreshToken, clientId, clientSecret)
34
- : revokeToken(refreshToken, clientId)
35
- waitUntil(promise)
38
+ if (options.clientSecret === undefined) {
39
+ waitUntil(revokeToken(refreshToken, clientId))
40
+ } else {
41
+ const secretResult = await resolveClientSecret(options.clientSecret, context.request)
42
+ if (secretResult.ok) {
43
+ const secret = secretResult.secret ?? undefined
44
+ const promise = secret
45
+ ? revokeToken(refreshToken, clientId, secret)
46
+ : revokeToken(refreshToken, clientId)
47
+ waitUntil(promise)
48
+ } else {
49
+ secretFailureCode = secretResult.code
50
+ }
51
+ }
36
52
  }
37
53
 
38
54
  // Emit auth_logout (non-blocking) when the consumer has @growth-labs/analytics
@@ -45,6 +61,19 @@ export async function GET(context: LogoutContext): Promise<Response> {
45
61
  waitUntil(promise)
46
62
  }
47
63
 
64
+ if (secretFailureCode) {
65
+ // Fail closed: clear all local cookies but return a fixed 503 instead of
66
+ // an ordinary redirect. Revocation was already skipped above.
67
+ const failResponse = secretResolutionFailureResponse(context.request, {
68
+ surface: 'logout_revocation',
69
+ code: secretFailureCode,
70
+ })
71
+ clearTokenCookies(failResponse, options)
72
+ clearIdentityCookie(failResponse)
73
+ clearAuthFlowCookies(failResponse, options)
74
+ return failResponse
75
+ }
76
+
48
77
  // Build redirect response
49
78
  const redirectUrl = new URL(options.logoutRedirect, context.url.origin).toString()
50
79
  const response = Response.redirect(redirectUrl, 302)
package/src/server.ts CHANGED
@@ -2,9 +2,15 @@ import { getConfig } from './config-store.js'
2
2
  import type { ResolvedAuthOptions } from './options.js'
3
3
  import { createClientSecretBasicAuthHeader, resolveClientSecret } from './utils/client-secret.js'
4
4
  import { getCookie } from './utils/cookies.js'
5
+ import {
6
+ ClientSecretResolutionError,
7
+ reportClientSecretResolutionFailure,
8
+ } from './utils/fail-closed.js'
5
9
  import { resolveInternalIssuer } from './utils/issuer.js'
6
10
  import { createPKCEChallenge } from './utils/pkce.js'
7
11
 
12
+ export { ClientSecretResolutionError }
13
+
8
14
  export type AuthClientCodeResult = { code: string } | { error: string }
9
15
  export type AuthClientOkResult = { ok: true } | { error: string }
10
16
 
@@ -69,9 +75,13 @@ async function headersFor(
69
75
  context: AuthClientContext,
70
76
  ): Promise<Record<string, string>> {
71
77
  const headers: Record<string, string> = { 'Content-Type': 'application/json' }
72
- const secret = await resolveClientSecret(options.clientSecret)
73
- if (secret) {
74
- headers.Authorization = createClientSecretBasicAuthHeader(clientId(options, context), secret)
78
+ const secretResult = await resolveClientSecret(options.clientSecret, context.request)
79
+ if (!secretResult.ok) throw new ClientSecretResolutionError(secretResult.code)
80
+ if (secretResult.secret) {
81
+ headers.Authorization = createClientSecretBasicAuthHeader(
82
+ clientId(options, context),
83
+ secretResult.secret,
84
+ )
75
85
  }
76
86
  return headers
77
87
  }
@@ -116,7 +126,14 @@ async function postJson<T extends AuthClientCodeResult | AuthClientOkResult>(
116
126
  return { ok: true } as T
117
127
  }
118
128
  return { error: 'invalid_response' } as T
119
- } catch {
129
+ } catch (err) {
130
+ if (err instanceof ClientSecretResolutionError) {
131
+ reportClientSecretResolutionFailure({
132
+ surface: 'server_auth_client',
133
+ code: err.code,
134
+ })
135
+ return { error: 'auth_service_unavailable' } as T
136
+ }
120
137
  return { error: 'request_failed' } as T
121
138
  }
122
139
  }
@@ -0,0 +1,71 @@
1
+ // Explicit local-dev literal type and predicate. Intentionally no runtime
2
+ // imports — this file is reachable from options.ts (the package main entry
3
+ // static-import chain) so it must not pull in cloudflare:workers.
4
+ // Resolution logic that requires the runtime lives in client-secret.ts.
5
+ export interface ClientSecretLiteral {
6
+ readonly __clientSecretLiteral: true
7
+ readonly value: string
8
+ }
9
+
10
+ export interface SerializedClientSecretLiteral {
11
+ readonly __growthLabsAuthClientSecretLiteral: 1
12
+ readonly value: string
13
+ }
14
+
15
+ const issuedClientSecretLiterals = new WeakSet<ClientSecretLiteral>()
16
+
17
+ export function literalClientSecret(value: string): ClientSecretLiteral {
18
+ const literal = Object.freeze({ __clientSecretLiteral: true as const, value })
19
+ issuedClientSecretLiterals.add(literal)
20
+ return literal
21
+ }
22
+
23
+ export function isClientSecretLiteral(v: unknown): v is ClientSecretLiteral {
24
+ return (
25
+ typeof v === 'object' &&
26
+ v !== null &&
27
+ issuedClientSecretLiterals.has(v as ClientSecretLiteral) &&
28
+ (v as Record<string, unknown>).__clientSecretLiteral === true &&
29
+ typeof (v as Record<string, unknown>).value === 'string'
30
+ )
31
+ }
32
+
33
+ // Only the integration calls this after authOptionsSchema has accepted a
34
+ // WeakSet-issued literal. The distinct wire marker is never accepted directly
35
+ // by resolveClientSecret; config-store must rehydrate it into a newly issued
36
+ // literal after Vite's JSON serialization boundary.
37
+ export function serializeClientSecretLiteral(
38
+ literal: ClientSecretLiteral,
39
+ ): SerializedClientSecretLiteral {
40
+ if (!isClientSecretLiteral(literal)) {
41
+ throw new TypeError('client secret literal was not issued by literalClientSecret()')
42
+ }
43
+ return Object.freeze({
44
+ __growthLabsAuthClientSecretLiteral: 1 as const,
45
+ value: literal.value,
46
+ })
47
+ }
48
+
49
+ export function rehydrateClientSecretLiteral(value: unknown): unknown {
50
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return value
51
+ const prototype = Object.getPrototypeOf(value)
52
+ if (prototype !== Object.prototype && prototype !== null) return value
53
+ const descriptors = Object.getOwnPropertyDescriptors(value)
54
+ const keys = Object.keys(descriptors)
55
+ const marker = descriptors.__growthLabsAuthClientSecretLiteral
56
+ const literalValue = descriptors.value
57
+ if (
58
+ keys.length !== 2 ||
59
+ !keys.includes('__growthLabsAuthClientSecretLiteral') ||
60
+ !keys.includes('value') ||
61
+ !marker ||
62
+ !('value' in marker) ||
63
+ marker.value !== 1 ||
64
+ !literalValue ||
65
+ !('value' in literalValue) ||
66
+ typeof literalValue.value !== 'string'
67
+ ) {
68
+ return value
69
+ }
70
+ return literalClientSecret(literalValue.value)
71
+ }