veryfront 0.1.217 → 0.1.218

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 (39) hide show
  1. package/esm/deno.d.ts +1 -2
  2. package/esm/deno.js +6 -4
  3. package/esm/src/extensions/interfaces/auth-provider.d.ts +30 -3
  4. package/esm/src/extensions/interfaces/auth-provider.d.ts.map +1 -1
  5. package/esm/src/extensions/interfaces/index.d.ts +2 -1
  6. package/esm/src/extensions/interfaces/index.d.ts.map +1 -1
  7. package/esm/src/extensions/interfaces/token-cache-store.d.ts +56 -0
  8. package/esm/src/extensions/interfaces/token-cache-store.d.ts.map +1 -0
  9. package/esm/src/extensions/interfaces/token-cache-store.js +12 -0
  10. package/esm/src/extensions/recommendations.d.ts.map +1 -1
  11. package/esm/src/extensions/recommendations.js +1 -0
  12. package/esm/src/proxy/cache/index.d.ts +1 -1
  13. package/esm/src/proxy/cache/index.d.ts.map +1 -1
  14. package/esm/src/proxy/cache/index.js +25 -15
  15. package/esm/src/proxy/cache/tracing-cache.d.ts +31 -0
  16. package/esm/src/proxy/cache/tracing-cache.d.ts.map +1 -0
  17. package/esm/src/proxy/cache/tracing-cache.js +44 -0
  18. package/esm/src/proxy/cache/types.d.ts +1 -1
  19. package/esm/src/proxy/cache/types.js +1 -1
  20. package/esm/src/proxy/handler.d.ts +7 -0
  21. package/esm/src/proxy/handler.d.ts.map +1 -1
  22. package/esm/src/proxy/handler.js +50 -29
  23. package/esm/src/utils/version-constant.d.ts +1 -1
  24. package/esm/src/utils/version-constant.js +1 -1
  25. package/package.json +66 -35
  26. package/src/deno.js +6 -4
  27. package/src/src/extensions/interfaces/auth-provider.ts +35 -3
  28. package/src/src/extensions/interfaces/index.ts +10 -1
  29. package/src/src/extensions/interfaces/token-cache-store.ts +58 -0
  30. package/src/src/extensions/recommendations.ts +1 -0
  31. package/src/src/proxy/cache/index.ts +27 -15
  32. package/src/src/proxy/cache/tracing-cache.ts +77 -0
  33. package/src/src/proxy/cache/types.ts +1 -1
  34. package/src/src/proxy/handler.ts +57 -31
  35. package/src/src/utils/version-constant.ts +1 -1
  36. package/esm/src/proxy/cache/redis-cache.d.ts +0 -25
  37. package/esm/src/proxy/cache/redis-cache.d.ts.map +0 -1
  38. package/esm/src/proxy/cache/redis-cache.js +0 -219
  39. package/src/src/proxy/cache/redis-cache.ts +0 -255
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veryfront",
3
- "version": "0.1.217",
3
+ "version": "0.1.218",
4
4
  "description": "The simplest way to build AI-powered apps",
5
5
  "keywords": [
6
6
  "react",
@@ -26,100 +26,132 @@
26
26
  "module": "./esm/src/index.js",
27
27
  "exports": {
28
28
  ".": {
29
- "import": "./esm/src/index.js"
29
+ "import": "./esm/src/index.js",
30
+ "types": "./esm/src/index.d.ts"
30
31
  },
31
32
  "./head": {
32
- "import": "./esm/src/react/components/Head.js"
33
+ "import": "./esm/src/react/components/Head.js",
34
+ "types": "./esm/src/react/components/Head.d.ts"
33
35
  },
34
36
  "./router": {
35
- "import": "./esm/src/react/router/index.js"
37
+ "import": "./esm/src/react/router/index.js",
38
+ "types": "./esm/src/react/router/index.d.ts"
36
39
  },
37
40
  "./context": {
38
- "import": "./esm/src/react/context/index.js"
41
+ "import": "./esm/src/react/context/index.js",
42
+ "types": "./esm/src/react/context/index.d.ts"
39
43
  },
40
44
  "./fonts": {
41
- "import": "./esm/src/react/fonts/index.js"
45
+ "import": "./esm/src/react/fonts/index.js",
46
+ "types": "./esm/src/react/fonts/index.d.ts"
42
47
  },
43
48
  "./chat": {
44
- "import": "./esm/src/chat/index.js"
49
+ "import": "./esm/src/chat/index.js",
50
+ "types": "./esm/src/chat/index.d.ts"
45
51
  },
46
52
  "./chat/ag-ui": {
47
- "import": "./esm/src/chat/ag-ui.js"
53
+ "import": "./esm/src/chat/ag-ui.js",
54
+ "types": "./esm/src/chat/ag-ui.d.ts"
48
55
  },
49
56
  "./chat/protocol": {
50
- "import": "./esm/src/chat/protocol.js"
57
+ "import": "./esm/src/chat/protocol.js",
58
+ "types": "./esm/src/chat/protocol.d.ts"
51
59
  },
52
60
  "./markdown": {
53
- "import": "./esm/src/markdown/index.js"
61
+ "import": "./esm/src/markdown/index.js",
62
+ "types": "./esm/src/markdown/index.d.ts"
54
63
  },
55
64
  "./mdx": {
56
- "import": "./esm/src/mdx/index.js"
65
+ "import": "./esm/src/mdx/index.js",
66
+ "types": "./esm/src/mdx/index.d.ts"
57
67
  },
58
68
  "./agent": {
59
- "import": "./esm/src/agent/index.js"
69
+ "import": "./esm/src/agent/index.js",
70
+ "types": "./esm/src/agent/index.d.ts"
60
71
  },
61
72
  "./tool": {
62
- "import": "./esm/src/tool/index.js"
73
+ "import": "./esm/src/tool/index.js",
74
+ "types": "./esm/src/tool/index.d.ts"
63
75
  },
64
76
  "./workflow": {
65
- "import": "./esm/src/workflow/index.js"
77
+ "import": "./esm/src/workflow/index.js",
78
+ "types": "./esm/src/workflow/index.d.ts"
66
79
  },
67
80
  "./workflow/worker": {
68
- "import": "./esm/src/workflow/worker/index.js"
81
+ "import": "./esm/src/workflow/worker/index.js",
82
+ "types": "./esm/src/workflow/worker/index.d.ts"
69
83
  },
70
84
  "./workflow/claude-code": {
71
- "import": "./esm/src/workflow/claude-code/index.js"
85
+ "import": "./esm/src/workflow/claude-code/index.js",
86
+ "types": "./esm/src/workflow/claude-code/index.d.ts"
72
87
  },
73
88
  "./workflow/claude-code/react": {
74
- "import": "./esm/src/workflow/claude-code/react/index.js"
89
+ "import": "./esm/src/workflow/claude-code/react/index.js",
90
+ "types": "./esm/src/workflow/claude-code/react/index.d.ts"
75
91
  },
76
92
  "./workflow/discovery": {
77
- "import": "./esm/src/workflow/discovery/index.js"
93
+ "import": "./esm/src/workflow/discovery/index.js",
94
+ "types": "./esm/src/workflow/discovery/index.d.ts"
78
95
  },
79
96
  "./prompt": {
80
- "import": "./esm/src/prompt/index.js"
97
+ "import": "./esm/src/prompt/index.js",
98
+ "types": "./esm/src/prompt/index.d.ts"
81
99
  },
82
100
  "./resource": {
83
- "import": "./esm/src/resource/index.js"
101
+ "import": "./esm/src/resource/index.js",
102
+ "types": "./esm/src/resource/index.d.ts"
84
103
  },
85
104
  "./jobs": {
86
- "import": "./esm/src/jobs/index.js"
105
+ "import": "./esm/src/jobs/index.js",
106
+ "types": "./esm/src/jobs/index.d.ts"
87
107
  },
88
108
  "./mcp": {
89
- "import": "./esm/src/mcp/index.js"
109
+ "import": "./esm/src/mcp/index.js",
110
+ "types": "./esm/src/mcp/index.d.ts"
90
111
  },
91
112
  "./middleware": {
92
- "import": "./esm/src/middleware/index.js"
113
+ "import": "./esm/src/middleware/index.js",
114
+ "types": "./esm/src/middleware/index.d.ts"
93
115
  },
94
116
  "./utils": {
95
- "import": "./esm/src/utils/index.js"
117
+ "import": "./esm/src/utils/index.js",
118
+ "types": "./esm/src/utils/index.d.ts"
96
119
  },
97
120
  "./oauth": {
98
- "import": "./esm/src/oauth/index.js"
121
+ "import": "./esm/src/oauth/index.js",
122
+ "types": "./esm/src/oauth/index.d.ts"
99
123
  },
100
124
  "./provider": {
101
- "import": "./esm/src/provider/index.js"
125
+ "import": "./esm/src/provider/index.js",
126
+ "types": "./esm/src/provider/index.d.ts"
102
127
  },
103
128
  "./fs": {
104
- "import": "./esm/src/fs/index.js"
129
+ "import": "./esm/src/fs/index.js",
130
+ "types": "./esm/src/fs/index.d.ts"
105
131
  },
106
132
  "./integrations": {
107
- "import": "./esm/src/integrations/index.js"
133
+ "import": "./esm/src/integrations/index.js",
134
+ "types": "./esm/src/integrations/index.d.ts"
108
135
  },
109
136
  "./sandbox": {
110
- "import": "./esm/src/sandbox/index.js"
137
+ "import": "./esm/src/sandbox/index.js",
138
+ "types": "./esm/src/sandbox/index.d.ts"
111
139
  },
112
140
  "./embedding": {
113
- "import": "./esm/src/embedding/index.js"
141
+ "import": "./esm/src/embedding/index.js",
142
+ "types": "./esm/src/embedding/index.d.ts"
114
143
  },
115
144
  "./extensions": {
116
- "import": "./esm/src/extensions/index.js"
145
+ "import": "./esm/src/extensions/index.js",
146
+ "types": "./esm/src/extensions/index.d.ts"
117
147
  },
118
148
  "./extensions/interfaces": {
119
- "import": "./esm/src/extensions/interfaces/index.js"
149
+ "import": "./esm/src/extensions/interfaces/index.js",
150
+ "types": "./esm/src/extensions/interfaces/index.d.ts"
120
151
  },
121
152
  "./cli": {
122
- "import": "./esm/cli/main.js"
153
+ "import": "./esm/cli/main.js",
154
+ "types": "./esm/cli/main.d.ts"
123
155
  },
124
156
  "./tsconfig.json": "./tsconfig.json"
125
157
  },
@@ -158,12 +190,10 @@
158
190
  "esbuild": "0.27.4",
159
191
  "github-slugger": "2.0.0",
160
192
  "gray-matter": "4.0.3",
161
- "jose": "5.9.6",
162
193
  "mdast-util-to-string": "4.0.0",
163
194
  "mime-types": "3.0.2",
164
195
  "react": "19.2.4",
165
196
  "react-dom": "19.2.4",
166
- "redis": "5.11.0",
167
197
  "rehype-highlight": "7.0.2",
168
198
  "rehype-raw": "7.0.0",
169
199
  "rehype-sanitize": "6.0.0",
@@ -199,6 +229,7 @@
199
229
  },
200
230
  "_generatedBy": "dnt@dev",
201
231
  "type": "module",
232
+ "types": "./esm/src/index.d.ts",
202
233
  "bin": {
203
234
  "veryfront": "bin/veryfront.js"
204
235
  },
package/src/deno.js CHANGED
@@ -1,8 +1,11 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.1.217",
3
+ "version": "0.1.218",
4
4
  "license": "Apache-2.0",
5
5
  "nodeModulesDir": "auto",
6
+ "workspace": [
7
+ "./extensions/ext-redis"
8
+ ],
6
9
  "exclude": [
7
10
  "npm/",
8
11
  "dist/",
@@ -12,7 +15,8 @@ export default {
12
15
  "data/",
13
16
  ".deno_cache/",
14
17
  "cli/templates/files/",
15
- "cli/templates/integrations/"
18
+ "cli/templates/integrations/",
19
+ "extensions/"
16
20
  ],
17
21
  "exports": {
18
22
  ".": "./src/index.ts",
@@ -260,9 +264,7 @@ export default {
260
264
  "tailwindcss/plugin": "https://esm.sh/tailwindcss@4.2.2/plugin",
261
265
  "tailwindcss/defaultTheme": "https://esm.sh/tailwindcss@4.2.2/defaultTheme",
262
266
  "tailwindcss/colors": "https://esm.sh/tailwindcss@4.2.2/colors",
263
- "redis": "npm:redis@5.11.0",
264
267
  "pg": "npm:pg@8.13.1",
265
- "jose": "npm:jose@5.9.6",
266
268
  "@opentelemetry/api": "npm:@opentelemetry/api@1.9.0",
267
269
  "@opentelemetry/core": "npm:@opentelemetry/core@2.6.0",
268
270
  "@opentelemetry/context-async-hooks": "npm:@opentelemetry/context-async-hooks@2.6.0",
@@ -36,17 +36,49 @@ export interface VerifyOptions {
36
36
  [key: string]: unknown;
37
37
  }
38
38
 
39
+ /**
40
+ * The parsed, unverified header of a JWT.
41
+ *
42
+ * Returned by {@link AuthProvider.decode}. `alg` is the signing algorithm
43
+ * advertised by the token (e.g. `"HS256"`, `"RS256"`); additional fields
44
+ * such as `kid` or `typ` may be present.
45
+ */
46
+ export interface TokenHeader {
47
+ /** Signing algorithm advertised by the token header. */
48
+ alg?: string;
49
+ /** Additional header fields. */
50
+ [key: string]: unknown;
51
+ }
52
+
39
53
  /**
40
54
  * AuthProvider contract interface.
41
55
  *
42
56
  * Implementations sign, verify, and decode authentication tokens
43
- * (e.g. JWTs) for request authentication.
57
+ * (e.g. JWTs) for request authentication, and verify third-party tokens
58
+ * against a remote JWKS.
44
59
  */
45
60
  export interface AuthProvider {
46
61
  /** Sign a payload into a token string. */
47
62
  sign(payload: TokenPayload, options?: SignOptions): Promise<string>;
48
63
  /** Verify a token and return its decoded payload. Throws on invalid tokens. */
49
64
  verify(token: string, options?: VerifyOptions): Promise<TokenPayload>;
50
- /** Decode a token without verifying its signature. */
51
- decode(token: string): TokenPayload | undefined;
65
+ /**
66
+ * Verify a token against a remote JSON Web Key Set.
67
+ *
68
+ * Fetches (and caches) the JWKS at `jwksUrl`, then verifies the token's
69
+ * signature and claims. Throws on invalid tokens, unreachable JWKS, or
70
+ * `kid`/algorithm mismatch.
71
+ */
72
+ verifyWithJwks(
73
+ token: string,
74
+ jwksUrl: string,
75
+ options?: VerifyOptions,
76
+ ): Promise<TokenPayload>;
77
+ /**
78
+ * Decode a token's protected header without verifying its signature.
79
+ *
80
+ * Returns `undefined` on malformed input. Useful for inspecting `alg`
81
+ * before choosing a verification strategy.
82
+ */
83
+ decode(token: string): TokenHeader | undefined;
52
84
  }
@@ -23,6 +23,9 @@ export type {
23
23
  // Cache store
24
24
  export type { CacheStore } from "./cache-store.js";
25
25
 
26
+ // Token cache store (proxy-grade cache with scan + stats)
27
+ export type { TokenCacheEntry, TokenCacheStats, TokenCacheStore } from "./token-cache-store.js";
28
+
26
29
  // CSS processor
27
30
  export type { CSSProcessOptions, CSSProcessor, CSSProcessResult } from "./css-processor.js";
28
31
 
@@ -37,7 +40,13 @@ export type {
37
40
  export type { DatabaseClient, QueryResult } from "./database-client.js";
38
41
 
39
42
  // Auth provider
40
- export type { AuthProvider, SignOptions, TokenPayload, VerifyOptions } from "./auth-provider.js";
43
+ export type {
44
+ AuthProvider,
45
+ SignOptions,
46
+ TokenHeader,
47
+ TokenPayload,
48
+ VerifyOptions,
49
+ } from "./auth-provider.js";
41
50
 
42
51
  // Tracing exporter
43
52
  export type { SpanData, TracingExporter } from "./tracing-exporter.js";
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Contract interface for OAuth-token-style cache stores used by the proxy.
3
+ *
4
+ * This contract is richer than the generic `CacheStore` — it adds scan-by-prefix,
5
+ * bulk read, and usage statistics primitives that the proxy's token cache needs.
6
+ * Simpler key-value consumers should use `CacheStore` instead.
7
+ *
8
+ * Default implementation: `@veryfront/ext-redis`.
9
+ *
10
+ * @module extensions/interfaces/token-cache-store
11
+ */
12
+
13
+ /**
14
+ * A cache entry stored by `TokenCacheStore`.
15
+ *
16
+ * The proxy persists OAuth tokens keyed by request metadata; this entry shape
17
+ * mirrors what the proxy has historically stored.
18
+ */
19
+ export interface TokenCacheEntry {
20
+ token: string;
21
+ /** Unix timestamp in milliseconds. */
22
+ expiresAt: number;
23
+ scope: "preview" | "production";
24
+ projectSlug?: string;
25
+ }
26
+
27
+ /**
28
+ * Aggregate usage statistics for a `TokenCacheStore`.
29
+ */
30
+ export interface TokenCacheStats {
31
+ hits: number;
32
+ misses: number;
33
+ size: number;
34
+ type: "memory" | "redis";
35
+ }
36
+
37
+ /**
38
+ * TokenCacheStore contract interface.
39
+ *
40
+ * Implementations provide TTL-aware token caching, plus scan and stats
41
+ * primitives that generic key-value caches do not require.
42
+ */
43
+ export interface TokenCacheStore {
44
+ /** Retrieve a cached entry by key. Returns `null` on miss or expiry. */
45
+ get(key: string): Promise<TokenCacheEntry | null>;
46
+ /** Store an entry. TTL is derived from `entry.expiresAt`. */
47
+ set(key: string, entry: TokenCacheEntry): Promise<void>;
48
+ /** Delete a cached entry. No-op if the key does not exist. */
49
+ delete(key: string): Promise<void>;
50
+ /** Remove every entry owned by this store. */
51
+ clear(): Promise<void>;
52
+ /** Check whether a non-expired entry exists for the given key. */
53
+ has(key: string): Promise<boolean>;
54
+ /** Return current hit/miss/size statistics. */
55
+ stats(): Promise<TokenCacheStats>;
56
+ /** Close connections and release resources. */
57
+ close(): Promise<void>;
58
+ }
@@ -7,6 +7,7 @@
7
7
  const recommendations = new Map<string, string>([
8
8
  ["Bundler", "@veryfront/ext-esbuild"],
9
9
  ["CacheStore", "@veryfront/ext-redis"],
10
+ ["TokenCacheStore", "@veryfront/ext-redis"],
10
11
  ["CSSProcessor", "@veryfront/ext-tailwind"],
11
12
  ["ContentTransformer", "@veryfront/ext-mdx"],
12
13
  ["DatabaseClient", "@veryfront/ext-postgres"],
@@ -13,24 +13,34 @@ export type {
13
13
  TokenCacheEntry,
14
14
  } from "./types.js";
15
15
  export { MemoryCache } from "./memory-cache.js";
16
- export { RedisCache } from "./redis-cache.js";
17
16
  export { ResilientCache } from "./resilient-cache.js";
17
+ export { TracingTokenCache } from "./tracing-cache.js";
18
18
 
19
19
  import type { CacheOptions, TokenCache } from "./types.js";
20
+ import type { TokenCacheStore } from "../../extensions/interfaces/token-cache-store.js";
20
21
  import { MemoryCache } from "./memory-cache.js";
21
- import { RedisCache } from "./redis-cache.js";
22
22
  import { ResilientCache } from "./resilient-cache.js";
23
+ import { TracingTokenCache } from "./tracing-cache.js";
24
+ import { tryResolve } from "../../extensions/contracts.js";
23
25
  import { getEnv } from "../../platform/compat/process.js";
24
26
  import { proxyLogger } from "../logger.js";
25
27
  import { withSpan } from "../tracing.js";
26
28
 
27
29
  const logger = proxyLogger.child({ module: "cache" });
28
30
 
31
+ const MISSING_EXTENSION_INFO =
32
+ "TokenCacheStore contract not provided — install @veryfront/ext-redis or scaffold extensions/ext-redis/";
33
+
29
34
  export async function createCache(options: CacheOptions): Promise<TokenCache> {
30
35
  return withSpan(
31
36
  "cache.create",
32
37
  async () => {
33
- if (options.type === "redis") return new RedisCache(options.options);
38
+ if (options.type === "redis") {
39
+ const tokenCache = tryResolve<TokenCacheStore>("TokenCacheStore");
40
+ if (tokenCache) return new TracingTokenCache(tokenCache as unknown as TokenCache);
41
+ logger.info(MISSING_EXTENSION_INFO);
42
+ return new MemoryCache(undefined);
43
+ }
34
44
  return new MemoryCache(options.options);
35
45
  },
36
46
  { "cache.type": options.type },
@@ -45,21 +55,23 @@ export async function createCacheFromEnv(): Promise<TokenCache> {
45
55
  async () => {
46
56
  if (cacheType !== "redis") return new MemoryCache();
47
57
 
48
- const url = getEnv("REDIS_URL");
49
- if (!url) {
50
- logger.warn("[Cache] CACHE_TYPE=redis but REDIS_URL not set, falling back to memory");
58
+ const tokenCache = tryResolve<TokenCacheStore>("TokenCacheStore");
59
+ if (!tokenCache) {
60
+ // Redis was requested via config/env but no extension registered the
61
+ // TokenCacheStore contract. Log an info (misconfiguration, not error),
62
+ // then fall back to an in-memory cache so the proxy still boots.
63
+ logger.info(MISSING_EXTENSION_INFO);
51
64
  return new MemoryCache();
52
65
  }
53
66
 
54
- const redisCache = new RedisCache({
55
- url,
56
- prefix: getEnv("REDIS_PREFIX") || "vf:token:",
57
- });
58
-
59
- // Wrap Redis with resilient fallback to memory cache
60
- // This ensures the proxy continues to function when Redis is unavailable
61
- logger.debug("[Cache] Using Redis with memory fallback (ResilientCache)");
62
- return new ResilientCache(redisCache, new MemoryCache());
67
+ // Wrap the extension-provided cache with a memory fallback so a Redis
68
+ // outage does not take the proxy down. TracingTokenCache sits between
69
+ // ResilientCache and the extension impl so spans wrap the actual
70
+ // primary-cache attempt (mirrors the pre-extraction RedisCache which
71
+ // had inner withSpan calls).
72
+ logger.debug("[Cache] Using TokenCacheStore extension with memory fallback (ResilientCache)");
73
+ const traced = new TracingTokenCache(tokenCache as unknown as TokenCache);
74
+ return new ResilientCache(traced, new MemoryCache());
63
75
  },
64
76
  { "cache.type": cacheType },
65
77
  );
@@ -0,0 +1,77 @@
1
+ /**
2
+ * TracingTokenCache
3
+ *
4
+ * Wraps a {@link TokenCache} implementation and emits an OpenTelemetry span
5
+ * around each public method. This keeps extension-provided caches (such as
6
+ * `@veryfront/ext-redis`) tracer-agnostic: the extension implements the
7
+ * contract only, and the proxy applies observability at the factory boundary.
8
+ *
9
+ * Span names default to `cache.redis.<op>` to preserve the pre-extraction
10
+ * behavior of the in-tree RedisCache. Callers may override via
11
+ * {@link TracingTokenCacheOptions.spanPrefix} when wrapping a different
12
+ * backend.
13
+ */
14
+
15
+ import type { CacheStats, TokenCache, TokenCacheEntry } from "./types.js";
16
+ import { withSpan } from "../tracing.js";
17
+
18
+ export interface TracingTokenCacheOptions {
19
+ /** Span name prefix, e.g. "cache.redis" produces "cache.redis.get". */
20
+ spanPrefix?: string;
21
+ }
22
+
23
+ const DEFAULT_SPAN_PREFIX = "cache.redis";
24
+
25
+ export class TracingTokenCache implements TokenCache {
26
+ private readonly inner: TokenCache;
27
+ private readonly prefix: string;
28
+
29
+ constructor(inner: TokenCache, options: TracingTokenCacheOptions = {}) {
30
+ this.inner = inner;
31
+ this.prefix = options.spanPrefix ?? DEFAULT_SPAN_PREFIX;
32
+ }
33
+
34
+ get(key: string): Promise<TokenCacheEntry | null> {
35
+ return withSpan(
36
+ `${this.prefix}.get`,
37
+ () => this.inner.get(key),
38
+ { "cache.key": key },
39
+ );
40
+ }
41
+
42
+ set(key: string, entry: TokenCacheEntry): Promise<void> {
43
+ return withSpan(
44
+ `${this.prefix}.set`,
45
+ () => this.inner.set(key, entry),
46
+ { "cache.key": key },
47
+ );
48
+ }
49
+
50
+ delete(key: string): Promise<void> {
51
+ return withSpan(
52
+ `${this.prefix}.delete`,
53
+ () => this.inner.delete(key),
54
+ { "cache.key": key },
55
+ );
56
+ }
57
+
58
+ clear(): Promise<void> {
59
+ return withSpan(`${this.prefix}.clear`, () => this.inner.clear());
60
+ }
61
+
62
+ has(key: string): Promise<boolean> {
63
+ return withSpan(
64
+ `${this.prefix}.has`,
65
+ () => this.inner.has(key),
66
+ { "cache.key": key },
67
+ );
68
+ }
69
+
70
+ stats(): Promise<CacheStats> {
71
+ return withSpan(`${this.prefix}.stats`, () => this.inner.stats());
72
+ }
73
+
74
+ close(): Promise<void> {
75
+ return withSpan(`${this.prefix}.close`, () => this.inner.close());
76
+ }
77
+ }
@@ -2,7 +2,7 @@
2
2
  * Token Cache Interface
3
3
  *
4
4
  * Abstraction for storing OAuth tokens with TTL support.
5
- * Implementations: MemoryCache, RedisCache
5
+ * Implementations: MemoryCache (built-in), RedisCache (via @veryfront/ext-redis)
6
6
  */
7
7
 
8
8
  export interface TokenCacheEntry {
@@ -6,7 +6,48 @@ import { cwd, getEnv } from "../platform/compat/process.js";
6
6
  import { join } from "../platform/compat/path/index.js";
7
7
  import { injectContext, ProxySpanNames, withSpan } from "./tracing.js";
8
8
  import { computeContentSourceId } from "../cache/keys.js";
9
- import { createRemoteJWKSet, decodeProtectedHeader, jwtVerify } from "jose";
9
+ import { resolve as resolveContract } from "../extensions/contracts.js";
10
+ import type { AuthProvider } from "../extensions/interfaces/auth-provider.js";
11
+
12
+ /**
13
+ * Cache the resolved AuthProvider at module scope so the proxy does not pay
14
+ * the registry lookup on every request. The cache is cleared implicitly when
15
+ * `ExtensionLoader.teardownAll()` clears the registry — the next call
16
+ * re-resolves (or surfaces the "install ext-jwt" hint if the extension was
17
+ * removed).
18
+ */
19
+ let cachedAuthProvider: AuthProvider | undefined;
20
+
21
+ function getAuthProvider(): AuthProvider {
22
+ if (cachedAuthProvider) return cachedAuthProvider;
23
+
24
+ try {
25
+ cachedAuthProvider = resolveContract<AuthProvider>("AuthProvider");
26
+ return cachedAuthProvider;
27
+ } catch (err) {
28
+ // resolve() already throws with a helpful "Recommended: @veryfront/ext-jwt"
29
+ // message, but the proxy is a load-bearing code path — append a concrete
30
+ // remediation hint that names the project-root extension directory so
31
+ // the user knows exactly what's missing.
32
+ const base = err instanceof Error ? err.message : String(err);
33
+ throw new Error(
34
+ `${base}\nTo enable JWT verification in the proxy, install ext-jwt ` +
35
+ `(scaffold with \`deno task cli extension init ext-jwt\` or add the ` +
36
+ `npm package @veryfront/ext-jwt).`,
37
+ { cause: err },
38
+ );
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Reset the cached AuthProvider. Intended for tests that `register()` a mock
44
+ * after the handler module has been imported.
45
+ *
46
+ * @internal
47
+ */
48
+ export function __resetCachedAuthProviderForTests(): void {
49
+ cachedAuthProvider = undefined;
50
+ }
10
51
 
11
52
  export const INTERNAL_PROXY_HEADERS = [
12
53
  "x-token",
@@ -64,24 +105,10 @@ interface DomainLookupResult {
64
105
  }>;
65
106
  }
66
107
 
67
- const remoteJwksByUrl = new Map<string, ReturnType<typeof createRemoteJWKSet>>();
68
-
69
- function getApiJwks(apiBaseUrl: string, logger?: ProxyLogger) {
108
+ function resolveApiJwksUrl(apiBaseUrl: string, logger?: ProxyLogger): string | undefined {
70
109
  try {
71
110
  const normalizedBaseUrl = apiBaseUrl.endsWith("/") ? apiBaseUrl : `${apiBaseUrl}/`;
72
- const jwksUrl = new URL(".well-known/jwks.json", normalizedBaseUrl);
73
- const cacheKey = jwksUrl.toString();
74
-
75
- // Lazily initialize and cache JWKS in a single, idempotent step to avoid
76
- // unsynchronized read/then-write on the shared Map across concurrent calls.
77
- let jwks = remoteJwksByUrl.get(cacheKey);
78
- if (!jwks) {
79
- const created = createRemoteJWKSet(jwksUrl);
80
- remoteJwksByUrl.set(cacheKey, created);
81
- jwks = created;
82
- }
83
-
84
- return jwks;
111
+ return new URL(".well-known/jwks.json", normalizedBaseUrl).toString();
85
112
  } catch (error) {
86
113
  logger?.error("Invalid API base URL for JWKS lookup", error as Error, {
87
114
  apiBaseUrl,
@@ -229,23 +256,22 @@ async function extractUserIdFromToken(
229
256
  apiBaseUrl: string,
230
257
  log?: ProxyLogger,
231
258
  ): Promise<string | undefined> {
232
- let algorithm: string | undefined;
259
+ const auth = getAuthProvider();
233
260
 
234
- try {
235
- algorithm = decodeProtectedHeader(token).alg;
236
- } catch (error) {
237
- log?.debug("Failed to decode JWT header", {
238
- error: error instanceof Error ? error.message : String(error),
239
- });
261
+ const header = auth.decode(token);
262
+ if (!header) {
263
+ log?.debug("Failed to decode JWT header");
240
264
  return undefined;
241
265
  }
242
266
 
267
+ const algorithm = header.alg;
268
+
243
269
  if (algorithm === "RS256") {
244
- const jwks = getApiJwks(apiBaseUrl, log);
245
- if (!jwks) return undefined;
270
+ const jwksUrl = resolveApiJwksUrl(apiBaseUrl, log);
271
+ if (!jwksUrl) return undefined;
246
272
 
247
273
  try {
248
- const { payload } = await jwtVerify(token, jwks, {
274
+ const payload = await auth.verifyWithJwks(token, jwksUrl, {
249
275
  algorithms: ["RS256"],
250
276
  });
251
277
  return (payload as { userId?: string }).userId;
@@ -270,10 +296,10 @@ async function extractUserIdFromToken(
270
296
  }
271
297
 
272
298
  try {
273
- const secret = new TextEncoder().encode(jwtSecret);
274
- const { payload } = await jwtVerify(token, secret, {
275
- algorithms: ["HS256"],
276
- });
299
+ // ext-jwt reads JWT_SECRET from the environment when no `secret` was
300
+ // passed to the extension factory; the explicit env check above is kept
301
+ // so callers can warn once before we attempt verification.
302
+ const payload = await auth.verify(token, { algorithms: ["HS256"] });
277
303
  return (payload as { userId?: string }).userId;
278
304
  } catch (error) {
279
305
  log?.debug("JWT verification failed", {
@@ -1,3 +1,3 @@
1
1
  // Keep in sync with deno.json version.
2
2
  // scripts/release.ts updates this constant during releases.
3
- export const VERSION = "0.1.217";
3
+ export const VERSION = "0.1.218";