effect-vercel 0.3.0 → 0.4.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 CHANGED
@@ -1,19 +1,25 @@
1
1
  # effect-vercel
2
2
 
3
- [Effect](https://effect.website) (v4) integrations for Vercel. Each one is its own entry point:
3
+ [Effect](https://effect.website) (v4) integrations for Vercel. Each module is its own entry
4
+ point, imported as a namespace like Effect's own modules:
4
5
 
5
- | Import | What |
6
- | ----------------------------- | ------------------------------------------------------------------------------- |
7
- | `effect-vercel/ai-gateway` | [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) for Effect AI |
8
- | `effect-vercel/oidc` | The OIDC token Vercel issues to a deployment |
9
- | `effect-vercel/runtime-cache` | [Runtime Cache](https://vercel.com/docs/runtime-cache) for Effect `Persistence` |
6
+ ```ts
7
+ import * as AiGateway from "effect-vercel/AiGateway"
8
+ ```
9
+
10
+ | Module | What |
11
+ | ------------------------------------ | ------------------------------------------------------------------------------- |
12
+ | `effect-vercel/AiGateway` | [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) for Effect AI |
13
+ | `effect-vercel/AiGatewayCredentials` | How AI Gateway requests authenticate |
14
+ | `effect-vercel/Oidc` | The OIDC token Vercel issues to a deployment |
15
+ | `effect-vercel/RuntimeCache` | [Runtime Cache](https://vercel.com/docs/runtime-cache) for Effect `Persistence` |
10
16
 
11
17
  ## Install
12
18
 
13
19
  ```sh
14
20
  pnpm add effect-vercel effect
15
- pnpm add @effect/ai-anthropic # only for effect-vercel/ai-gateway
16
- pnpm add @vercel/functions # only for effect-vercel/runtime-cache
21
+ pnpm add @effect/ai-anthropic # only for effect-vercel/AiGateway
22
+ pnpm add @vercel/functions # only for effect-vercel/RuntimeCache
17
23
  ```
18
24
 
19
25
  ## AI Gateway
@@ -26,7 +32,7 @@ dialect and what the provider's schemas expect.
26
32
  ### Usage
27
33
 
28
34
  ```ts
29
- import { AiGateway } from "effect-vercel/ai-gateway"
35
+ import * as AiGateway from "effect-vercel/AiGateway"
30
36
  import { AnthropicLanguageModel } from "@effect/ai-anthropic"
31
37
  import { Effect, Layer } from "effect"
32
38
  import { LanguageModel } from "effect/ai"
@@ -56,6 +62,8 @@ To pick the source yourself, use `AiGateway.layerWithoutCredentials` and provide
56
62
  `AiGatewayCredentials` layer:
57
63
 
58
64
  ```ts
65
+ import * as AiGatewayCredentials from "effect-vercel/AiGatewayCredentials"
66
+
59
67
  AnthropicLanguageModel.layer({ model: "google/gemini-2.5-flash" }).pipe(
60
68
  Layer.provide(AiGateway.layerWithoutCredentials),
61
69
  Layer.provide([AiGatewayCredentials.layerFromApiKey(key), FetchHttpClient.layer]),
@@ -64,9 +72,9 @@ AnthropicLanguageModel.layer({ model: "google/gemini-2.5-flash" }).pipe(
64
72
 
65
73
  | Layer | Source |
66
74
  | ------------------------------------------- | --------------------------------------------------------- |
67
- | `AiGatewayCredentials.layer` | The default: `layerFromEnv`, then `layerFromVercelOidc`. |
75
+ | `AiGatewayCredentials.layer` | The default: `layerFromEnv`, then `layerFromOidc`. |
68
76
  | `AiGatewayCredentials.layerFromEnv` | `AI_GATEWAY_API_KEY`. Local dev, CI, anywhere off Vercel. |
69
- | `AiGatewayCredentials.layerFromVercelOidc` | The Vercel OIDC token (see [OIDC](#oidc)). |
77
+ | `AiGatewayCredentials.layerFromOidc` | The Vercel OIDC token (see [OIDC](#oidc)). |
70
78
  | `AiGatewayCredentials.layerFromApiKey(key)` | A fixed key. Tests, custom wiring. |
71
79
 
72
80
  The credential is resolved on every request, so a rotating token is always current.
@@ -96,31 +104,29 @@ Vercel issues each deployment an OIDC token. Use it for the AI Gateway, or excha
96
104
  cloud credentials (AWS `AssumeRoleWithWebIdentity`, GCP workload identity federation).
97
105
 
98
106
  ```ts
99
- import { VercelOidc } from "effect-vercel/oidc"
107
+ import * as Oidc from "effect-vercel/Oidc"
100
108
  import { Effect, Redacted } from "effect"
101
109
 
102
110
  const program = Effect.gen(function* () {
103
- const token = yield* VercelOidc.token
111
+ const token = yield* Oidc.ambientToken
104
112
  // Redacted.value(token) is the web identity token
105
113
  })
106
114
  ```
107
115
 
108
- - `VercelOidc.token` reads the `x-vercel-oidc-token` header of the current request
109
- (Functions), else `VERCEL_OIDC_TOKEN` (builds, `vercel env pull`). It fails with a
110
- `VercelOidcError` when neither is set.
111
- - `VercelOidc.find` is the same but returns `Option.none()` instead of failing, for fallback
112
- chains.
116
+ `Oidc.ambientToken` reads the `x-vercel-oidc-token` header of the current request
117
+ (Functions), else `VERCEL_OIDC_TOKEN` (builds, `vercel env pull`). Empty values count as
118
+ unset. It fails with an `OidcError` when neither is set.
113
119
 
114
- Both read the token on every run, since it rotates per request. They do not refresh an
120
+ It reads the token on every run, since it rotates per request. It does not refresh an
115
121
  expired local token; re-run `vercel env pull`.
116
122
 
117
123
  ## Runtime Cache
118
124
 
119
- `VercelRuntimeCache.layer` provides Effect's `Persistence`, stored in the Vercel Runtime Cache.
125
+ `RuntimeCache.layer` provides Effect's `Persistence`, stored in the Vercel Runtime Cache.
120
126
  Use it with `PersistedCache` to share results across Function instances in a region.
121
127
 
122
128
  ```ts
123
- import { VercelRuntimeCache } from "effect-vercel/runtime-cache"
129
+ import * as RuntimeCache from "effect-vercel/RuntimeCache"
124
130
  import { Effect, Schema } from "effect"
125
131
  import { Persistable, PersistedCache } from "effect/persistence"
126
132
 
@@ -136,14 +142,14 @@ const program = Effect.gen(function* () {
136
142
  timeToLive: () => "1 hour",
137
143
  })
138
144
  return yield* users.get(new GetUser({ id: 1 }))
139
- }).pipe(Effect.scoped, Effect.provide(VercelRuntimeCache.layer))
145
+ }).pipe(Effect.scoped, Effect.provide(RuntimeCache.layer))
140
146
  ```
141
147
 
142
148
  - Off Vercel, `@vercel/functions` falls back to an in-memory cache, so the layer works in
143
149
  local dev and tests.
144
150
  - Entries are keyed by `[storeId, primaryKey]`, hashed with SHA-256. Only the hash reaches
145
151
  Vercel. `getCache`'s default hash is 32-bit and can collide, so
146
- `VercelRuntimeCache.makeBacking(cache)` expects a cache with a stronger `keyHashFunction` if
152
+ `RuntimeCache.makeBacking(cache)` expects a cache with a stronger `keyHashFunction` if
147
153
  you build your own.
148
154
  - Each entry is tagged `effect-persistence:<sha256 of storeId>`. `clear` expires that tag.
149
155
  - TTLs round up to whole seconds.
@@ -0,0 +1,44 @@
1
+ import { AiGatewayCredentials, t as AiGatewayCredentials_d_exports } from "./AiGatewayCredentials.js";
2
+ import { AnthropicClient } from "@effect/ai-anthropic";
3
+ import { Effect, Layer } from "effect";
4
+ import { HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/http";
5
+ //#region src/AiGateway.d.ts
6
+ /** Base URL of the Vercel AI Gateway. */
7
+ export declare const AI_GATEWAY_URL = "https://ai-gateway.vercel.sh";
8
+ /**
9
+ * Builds the Anthropic client against the gateway, authenticating each
10
+ * request with `credentials`. A credential failure fails that request as an
11
+ * `HttpClientError`, which Effect AI surfaces as a network `AiError`.
12
+ */
13
+ export declare const make: (credentials: typeof AiGatewayCredentials.Service) => Effect.Effect<AnthropicClient.Service, never, HttpClient.HttpClient>;
14
+ /**
15
+ * `AnthropicClient` backed by the gateway, authenticating with the
16
+ * `AiGatewayCredentials` you provide (see that module's `layerFrom*` layers).
17
+ * Needs an `HttpClient` too.
18
+ */
19
+ export declare const layerWithoutCredentials: Layer.Layer<AnthropicClient.AnthropicClient, never, AiGatewayCredentials | HttpClient.HttpClient>;
20
+ /**
21
+ * `layerWithoutCredentials` with the default `AiGatewayCredentials.layer`:
22
+ * `AI_GATEWAY_API_KEY`, else the Vercel OIDC token. Needs an `HttpClient`.
23
+ * Building never fails; without a usable credential, the requests fail.
24
+ */
25
+ export declare const layer: Layer.Layer<AnthropicClient.AnthropicClient, never, HttpClient.HttpClient>;
26
+ /**
27
+ * The Effect provider writes `"cache_control": null` on every content block
28
+ * it has no cache setting for. Anthropic accepts that; the gateway rejects
29
+ * the request with `messages.0.content: Invalid input`. Remove the null keys.
30
+ */
31
+ export declare function dropNullCacheControl(request: HttpClientRequest.HttpClientRequest): HttpClientRequest.HttpClientRequest;
32
+ /**
33
+ * The gateway's Anthropic-shaped responses leave out keys that Anthropic
34
+ * always sends and the Effect provider's schema requires: the cache and
35
+ * service-tier usage fields, the `signature` of a thinking block from a
36
+ * non-Anthropic model, and `type`/`request_id` on error envelopes, whose
37
+ * `error.type` may also be a gateway-specific value. Fill them with the values
38
+ * Anthropic uses when there is nothing to report. Streaming responses are not
39
+ * JSON and pass through untouched.
40
+ */
41
+ export declare function fillMissingResponseKeys(response: HttpClientResponse.HttpClientResponse): Effect.Effect<HttpClientResponse.HttpClientResponse, HttpClientError.HttpClientError>;
42
+ export declare function patchResponseBody(body: Record<string, unknown>): Record<string, unknown>;
43
+ //#endregion
44
+ //# sourceMappingURL=AiGateway.d.ts.map
@@ -1,99 +1,7 @@
1
- import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { n as find, r as hints$1 } from "./VercelOidc-DCkdXW3E.js";
1
+ import { AiGatewayCredentials, formatHeaders, layer as layer$1 } from "./AiGatewayCredentials.js";
3
2
  import { AnthropicClient } from "@effect/ai-anthropic";
4
- import { Config, Context, Data, Effect, Layer, Option, Redacted } from "effect";
3
+ import { Effect, Layer } from "effect";
5
4
  import { HttpBody, HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/http";
6
- //#region src/AiGatewayCredentials.ts
7
- /**
8
- * How gateway requests authenticate.
9
- *
10
- * `AiGatewayCredentials` holds an effect that produces the credential for one
11
- * request. It runs per request rather than once at startup: on Vercel the OIDC
12
- * token arrives on the request context and rotates, so it must not be cached.
13
- * `layer` is the default; pick a `layerFrom*` layer to use one source only.
14
- *
15
- * @example
16
- * ```ts
17
- * import { AiGatewayCredentials } from "effect-vercel/ai-gateway"
18
- *
19
- * AiGatewayCredentials.layer // AI_GATEWAY_API_KEY, else the Vercel OIDC token
20
- * AiGatewayCredentials.layerFromEnv // AI_GATEWAY_API_KEY only
21
- * AiGatewayCredentials.layerFromVercelOidc // the Vercel OIDC token only
22
- * AiGatewayCredentials.layerFromApiKey("…") // a fixed value
23
- * ```
24
- */
25
- var AiGatewayCredentials_exports = /* @__PURE__ */ __exportAll({
26
- API_KEY_ENV: () => API_KEY_ENV,
27
- AiGatewayCredentials: () => AiGatewayCredentials,
28
- AiGatewayCredentialsError: () => AiGatewayCredentialsError,
29
- formatHeaders: () => formatHeaders,
30
- layer: () => layer$1,
31
- layerFromApiKey: () => layerFromApiKey,
32
- layerFromEnv: () => layerFromEnv,
33
- layerFromVercelOidc: () => layerFromVercelOidc
34
- });
35
- /** The environment variable `layerFromEnv` reads. */
36
- const API_KEY_ENV = "AI_GATEWAY_API_KEY";
37
- /** No credential could be produced for a gateway request. */
38
- var AiGatewayCredentialsError = class extends Data.TaggedError("AiGatewayCredentialsError") {};
39
- var AiGatewayCredentials = class extends Context.Service()("effect-vercel/ai-gateway/AiGatewayCredentials") {};
40
- /** The request headers that carry `credentials`. */
41
- const formatHeaders = (credentials) => ({
42
- "x-api-key": Redacted.value(credentials.token),
43
- "ai-gateway-auth-method": credentials.method
44
- });
45
- const hints = {
46
- "api-key": [],
47
- env: [`Set ${API_KEY_ENV} to an AI Gateway API key.`],
48
- "vercel-oidc": hints$1,
49
- default: [`Set ${API_KEY_ENV} to an AI Gateway API key.`, "Or run on Vercel / `vercel env pull` so an OIDC token is available."]
50
- };
51
- const apiKeyLookup = Config.option(Config.Redacted(API_KEY_ENV)).pipe(Effect.map(Option.map((token) => ({
52
- method: "api-key",
53
- token
54
- }))), Effect.catchTag("ConfigError", (cause) => new AiGatewayCredentialsError({
55
- message: `Failed to read ${API_KEY_ENV}.`,
56
- source: "env",
57
- hints: hints.env,
58
- cause
59
- })));
60
- const vercelOidcLookup = find.pipe(Effect.map(Option.map((token) => ({
61
- method: "oidc",
62
- token
63
- }))), Effect.mapError((cause) => new AiGatewayCredentialsError({
64
- message: cause.message,
65
- source: "vercel-oidc",
66
- hints: cause.hints,
67
- cause
68
- })));
69
- const firstOf = (lookups) => Effect.gen(function* () {
70
- for (const lookup of lookups) {
71
- const found = yield* lookup;
72
- if (Option.isSome(found)) return found;
73
- }
74
- return Option.none();
75
- });
76
- const toLayer = (lookup, source, message) => Layer.succeed(AiGatewayCredentials)(Effect.flatMap(lookup, (found) => Option.isSome(found) ? Effect.succeed(found.value) : Effect.fail(new AiGatewayCredentialsError({
77
- message,
78
- source,
79
- hints: hints[source]
80
- }))));
81
- /** A fixed API key. */
82
- const layerFromApiKey = (apiKey) => Layer.succeed(AiGatewayCredentials)(Effect.succeed({
83
- method: "api-key",
84
- token: Redacted.isRedacted(apiKey) ? apiKey : Redacted.make(apiKey)
85
- }));
86
- /** `AI_GATEWAY_API_KEY`: local dev, CI, or anywhere off Vercel. */
87
- const layerFromEnv = toLayer(apiKeyLookup, "env", `${API_KEY_ENV} is not set.`);
88
- /** The Vercel OIDC token (see `VercelOidc.find`). Not refreshed when it expires locally. */
89
- const layerFromVercelOidc = toLayer(vercelOidcLookup, "vercel-oidc", "No Vercel OIDC token is available.");
90
- /**
91
- * The default: `layerFromEnv`, then `layerFromVercelOidc`. Same order as
92
- * Vercel's own SDK, so it works locally with an API key and on Vercel without
93
- * one.
94
- */
95
- const layer$1 = toLayer(firstOf([apiKeyLookup, vercelOidcLookup]), "default", "No AI Gateway credential found.");
96
- //#endregion
97
5
  //#region src/AiGateway.ts
98
6
  /**
99
7
  * Vercel AI Gateway as an Anthropic Messages API client for Effect AI.
@@ -112,7 +20,7 @@ const layer$1 = toLayer(firstOf([apiKeyLookup, vercelOidcLookup]), "default", "N
112
20
  *
113
21
  * @example
114
22
  * ```ts
115
- * import { AiGateway } from "effect-vercel/ai-gateway"
23
+ * import * as AiGateway from "effect-vercel/AiGateway"
116
24
  * import { AnthropicLanguageModel } from "@effect/ai-anthropic"
117
25
  * import { Layer } from "effect"
118
26
  * import { FetchHttpClient } from "effect/http"
@@ -123,15 +31,6 @@ const layer$1 = toLayer(firstOf([apiKeyLookup, vercelOidcLookup]), "default", "N
123
31
  * )
124
32
  * ```
125
33
  */
126
- var AiGateway_exports = /* @__PURE__ */ __exportAll({
127
- AI_GATEWAY_URL: () => AI_GATEWAY_URL,
128
- dropNullCacheControl: () => dropNullCacheControl,
129
- fillMissingResponseKeys: () => fillMissingResponseKeys,
130
- layer: () => layer,
131
- layerWithoutCredentials: () => layerWithoutCredentials,
132
- make: () => make,
133
- patchResponseBody: () => patchResponseBody
134
- });
135
34
  /** Base URL of the Vercel AI Gateway. */
136
35
  const AI_GATEWAY_URL = "https://ai-gateway.vercel.sh";
137
36
  /**
@@ -250,6 +149,6 @@ function isJson(contentType) {
250
149
  return contentType.startsWith("application/json");
251
150
  }
252
151
  //#endregion
253
- export { AiGateway_exports as AiGateway, AiGatewayCredentials_exports as AiGatewayCredentials };
152
+ export { AI_GATEWAY_URL, dropNullCacheControl, fillMissingResponseKeys, layer, layerWithoutCredentials, make, patchResponseBody };
254
153
 
255
- //# sourceMappingURL=ai-gateway.js.map
154
+ //# sourceMappingURL=AiGateway.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AiGateway.js","names":["AiGatewayCredentials.AiGatewayCredentials","AiGatewayCredentials.layer","AiGatewayCredentials.formatHeaders"],"sources":["../src/AiGateway.ts"],"sourcesContent":["/**\n * Vercel AI Gateway as an Anthropic Messages API client for Effect AI.\n *\n * The gateway serves the Anthropic Messages API at its root URL and accepts\n * any model it lists, named `provider/model` (for example\n * `anthropic/claude-sonnet-4.5` or `google/gemini-2.5-flash`). Effect AI's\n * Anthropic provider only needs the base URL and the credential swapped, plus\n * two small dialect fixes (see `dropNullCacheControl` and\n * `fillMissingResponseKeys`), so this module is the one place the gateway is\n * configured.\n *\n * Structured output (`output_config.format`) works for every provider behind\n * the gateway, so the Anthropic provider's default of native structured\n * output holds for models it does not recognise.\n *\n * @example\n * ```ts\n * import * as AiGateway from \"effect-vercel/AiGateway\"\n * import { AnthropicLanguageModel } from \"@effect/ai-anthropic\"\n * import { Layer } from \"effect\"\n * import { FetchHttpClient } from \"effect/http\"\n *\n * const Model = AnthropicLanguageModel.layer({ model: \"anthropic/claude-sonnet-4.5\" }).pipe(\n * Layer.provide(AiGateway.layer),\n * Layer.provide(FetchHttpClient.layer),\n * )\n * ```\n */\n\nimport { AnthropicClient } from \"@effect/ai-anthropic\"\nimport { Effect, Layer } from \"effect\"\nimport {\n HttpBody,\n HttpClient,\n HttpClientError,\n HttpClientRequest,\n HttpClientResponse,\n} from \"effect/http\"\nimport * as AiGatewayCredentials from \"./AiGatewayCredentials.js\"\n\n/** Base URL of the Vercel AI Gateway. */\nexport const AI_GATEWAY_URL = \"https://ai-gateway.vercel.sh\"\n\n/**\n * Builds the Anthropic client against the gateway, authenticating each\n * request with `credentials`. A credential failure fails that request as an\n * `HttpClientError`, which Effect AI surfaces as a network `AiError`.\n */\nexport const make = (credentials: typeof AiGatewayCredentials.AiGatewayCredentials.Service) =>\n AnthropicClient.make({\n apiUrl: AI_GATEWAY_URL,\n transformClient: (client) =>\n client.pipe(\n HttpClient.mapRequestEffect(authenticate(credentials)),\n HttpClient.mapRequest(dropNullCacheControl),\n HttpClient.transformResponse(Effect.flatMap(fillMissingResponseKeys)),\n ),\n })\n\n/**\n * `AnthropicClient` backed by the gateway, authenticating with the\n * `AiGatewayCredentials` you provide (see that module's `layerFrom*` layers).\n * Needs an `HttpClient` too.\n */\nexport const layerWithoutCredentials: Layer.Layer<\n AnthropicClient.AnthropicClient,\n never,\n AiGatewayCredentials.AiGatewayCredentials | HttpClient.HttpClient\n> = Layer.effect(AnthropicClient.AnthropicClient)(\n Effect.flatMap(Effect.service(AiGatewayCredentials.AiGatewayCredentials), make),\n)\n\n/**\n * `layerWithoutCredentials` with the default `AiGatewayCredentials.layer`:\n * `AI_GATEWAY_API_KEY`, else the Vercel OIDC token. Needs an `HttpClient`.\n * Building never fails; without a usable credential, the requests fail.\n */\nexport const layer: Layer.Layer<AnthropicClient.AnthropicClient, never, HttpClient.HttpClient> =\n layerWithoutCredentials.pipe(Layer.provide(AiGatewayCredentials.layer))\n\nfunction authenticate(credentials: typeof AiGatewayCredentials.AiGatewayCredentials.Service) {\n return (request: HttpClientRequest.HttpClientRequest) =>\n credentials.pipe(\n Effect.map((resolved) =>\n HttpClientRequest.setHeaders(request, AiGatewayCredentials.formatHeaders(resolved)),\n ),\n Effect.mapError(\n (error) =>\n new HttpClientError.HttpClientError({\n reason: new HttpClientError.TransportError({\n request,\n cause: error,\n description: \"AI Gateway credential unavailable\",\n }),\n }),\n ),\n )\n}\n\n/**\n * The Effect provider writes `\"cache_control\": null` on every content block\n * it has no cache setting for. Anthropic accepts that; the gateway rejects\n * the request with `messages.0.content: Invalid input`. Remove the null keys.\n */\nexport function dropNullCacheControl(\n request: HttpClientRequest.HttpClientRequest,\n): HttpClientRequest.HttpClientRequest {\n const body = request.body\n if (body._tag !== \"Uint8Array\" || !isJson(body.contentType)) {\n return request\n }\n const json: unknown = JSON.parse(new TextDecoder().decode(body.body))\n return HttpClientRequest.setBody(\n request,\n HttpBody.jsonUnsafe(withoutNullCacheControl(json), body.contentType),\n )\n}\n\nfunction withoutNullCacheControl(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(withoutNullCacheControl)\n if (value === null || typeof value !== \"object\") return value\n const result: Record<string, unknown> = {}\n for (const [key, entry] of Object.entries(value)) {\n if (key === \"cache_control\" && entry === null) continue\n result[key] = withoutNullCacheControl(entry)\n }\n return result\n}\n\n/**\n * The gateway's Anthropic-shaped responses leave out keys that Anthropic\n * always sends and the Effect provider's schema requires: the cache and\n * service-tier usage fields, the `signature` of a thinking block from a\n * non-Anthropic model, and `type`/`request_id` on error envelopes, whose\n * `error.type` may also be a gateway-specific value. Fill them with the values\n * Anthropic uses when there is nothing to report. Streaming responses are not\n * JSON and pass through untouched.\n */\nexport function fillMissingResponseKeys(\n response: HttpClientResponse.HttpClientResponse,\n): Effect.Effect<HttpClientResponse.HttpClientResponse, HttpClientError.HttpClientError> {\n if (!isJson(response.headers[\"content-type\"] ?? \"\")) {\n return Effect.succeed(response)\n }\n return Effect.map(response.text, (text) => {\n const body = JSON.parse(text) as Record<string, unknown>\n return HttpClientResponse.fromWeb(\n response.request,\n new Response(JSON.stringify(patchResponseBody(body)), {\n status: response.status,\n headers: response.headers,\n }),\n )\n })\n}\n\nexport function patchResponseBody(body: Record<string, unknown>): Record<string, unknown> {\n if (body.type === \"message\") {\n const usage = (body.usage ?? {}) as Record<string, unknown>\n body.usage = {\n cache_creation: null,\n cache_creation_input_tokens: null,\n cache_read_input_tokens: null,\n inference_geo: null,\n service_tier: null,\n ...usage,\n }\n if (Array.isArray(body.content)) {\n body.content = body.content.map((block: unknown) =>\n isRecord(block) && block.type === \"thinking\" ? { signature: \"\", ...block } : block,\n )\n }\n return body\n }\n if (isRecord(body.error)) {\n const error = ANTHROPIC_ERROR_TYPES.has(String(body.error.type))\n ? body.error\n : { ...body.error, type: \"api_error\" }\n return { type: \"error\", request_id: null, ...body, error }\n }\n return body\n}\n\nconst ANTHROPIC_ERROR_TYPES = new Set([\n \"invalid_request_error\",\n \"authentication_error\",\n \"billing_error\",\n \"permission_error\",\n \"not_found_error\",\n \"rate_limit_error\",\n \"timeout_error\",\n \"api_error\",\n \"overloaded_error\",\n])\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\n\nfunction isJson(contentType: string): boolean {\n return contentType.startsWith(\"application/json\")\n}\n"],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ import { Context, Effect, Layer, Redacted } from "effect";
2
+ declare namespace AiGatewayCredentials_d_exports {
3
+ export { API_KEY_ENV, AiGatewayCredentials, AiGatewayCredentialsError, Resolved, Source, formatHeaders, layer, layerFromApiKey, layerFromEnv, layerFromOidc };
4
+ }
5
+ /** The environment variable `layerFromEnv` reads. */
6
+ export declare const API_KEY_ENV = "AI_GATEWAY_API_KEY";
7
+ /** Which layer produced or failed to produce a credential. */
8
+ export type Source = "api-key" | "env" | "oidc" | "default";
9
+ /**
10
+ * A credential ready to put on a request. `method` is what the gateway
11
+ * records as the auth method; it matches the values Vercel's own SDK sends.
12
+ */
13
+ export interface Resolved {
14
+ readonly method: "api-key" | "oidc";
15
+ readonly token: Redacted.Redacted<string>;
16
+ }
17
+ declare const AiGatewayCredentialsError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
18
+ readonly _tag: "AiGatewayCredentialsError";
19
+ } & Readonly<A>;
20
+ /** No credential could be produced for a gateway request. */
21
+ export declare class AiGatewayCredentialsError extends AiGatewayCredentialsError_base<{
22
+ readonly message: string;
23
+ readonly source: Source;
24
+ readonly hints: ReadonlyArray<string>;
25
+ readonly cause?: unknown;
26
+ }> {}
27
+ declare const AiGatewayCredentials_base: Context.ServiceClass<AiGatewayCredentials, "effect-vercel/AiGatewayCredentials", Effect.Effect<Resolved, AiGatewayCredentialsError, never>>;
28
+ export declare class AiGatewayCredentials extends AiGatewayCredentials_base {}
29
+ /** The request headers that carry `credentials`. */
30
+ export declare const formatHeaders: (credentials: Resolved) => Record<string, string>;
31
+ /** A fixed API key. */
32
+ export declare const layerFromApiKey: (apiKey: string | Redacted.Redacted<string>) => Layer.Layer<AiGatewayCredentials>;
33
+ /** `AI_GATEWAY_API_KEY`: local dev, CI, or anywhere off Vercel. */
34
+ export declare const layerFromEnv: Layer.Layer<AiGatewayCredentials>;
35
+ /** The Vercel OIDC token (see `Oidc.ambientToken`). */
36
+ export declare const layerFromOidc: Layer.Layer<AiGatewayCredentials>;
37
+ /**
38
+ * The default: `layerFromEnv`, then `layerFromOidc`. Same order as
39
+ * Vercel's own SDK, so it works locally with an API key and on Vercel without
40
+ * one. Falls back only when the key is unset, not when it can't be read.
41
+ */
42
+ export declare const layer: Layer.Layer<AiGatewayCredentials>;
43
+ //#endregion
44
+ export { AiGatewayCredentials_d_exports as t };
45
+ //# sourceMappingURL=AiGatewayCredentials.d.ts.map
@@ -0,0 +1,94 @@
1
+ import { ambientToken, hints as hints$1 } from "./Oidc.js";
2
+ import { Config, Context, Data, Effect, Layer, Option, Redacted } from "effect";
3
+ //#region src/AiGatewayCredentials.ts
4
+ /**
5
+ * How gateway requests authenticate.
6
+ *
7
+ * `AiGatewayCredentials` holds an effect that produces the credential for one
8
+ * request. It runs per request rather than once at startup: on Vercel the OIDC
9
+ * token arrives on the request context and rotates, so it must not be cached.
10
+ * `layer` is the default; pick a `layerFrom*` layer to use one source only.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import * as AiGatewayCredentials from "effect-vercel/AiGatewayCredentials"
15
+ *
16
+ * AiGatewayCredentials.layer // AI_GATEWAY_API_KEY, else the Vercel OIDC token
17
+ * AiGatewayCredentials.layerFromEnv // AI_GATEWAY_API_KEY only
18
+ * AiGatewayCredentials.layerFromOidc // the Vercel OIDC token only
19
+ * AiGatewayCredentials.layerFromApiKey("…") // a fixed value
20
+ * ```
21
+ */
22
+ /** The environment variable `layerFromEnv` reads. */
23
+ const API_KEY_ENV = "AI_GATEWAY_API_KEY";
24
+ /** No credential could be produced for a gateway request. */
25
+ var AiGatewayCredentialsError = class extends Data.TaggedError("AiGatewayCredentialsError") {};
26
+ var AiGatewayCredentials = class extends Context.Service()("effect-vercel/AiGatewayCredentials") {};
27
+ /** The request headers that carry `credentials`. */
28
+ const formatHeaders = (credentials) => ({
29
+ "x-api-key": Redacted.value(credentials.token),
30
+ "ai-gateway-auth-method": credentials.method
31
+ });
32
+ const hints = {
33
+ "api-key": [],
34
+ env: [`Set ${API_KEY_ENV} to an AI Gateway API key.`],
35
+ oidc: hints$1,
36
+ default: [`Set ${API_KEY_ENV} to an AI Gateway API key.`, "Or run on Vercel / `vercel env pull` so an OIDC token is available."]
37
+ };
38
+ /** `AI_GATEWAY_API_KEY`, or `None` when unset. Fails only when it can't be read. */
39
+ const envApiKey = Config.option(Config.Redacted(API_KEY_ENV)).pipe(Effect.mapError((cause) => new AiGatewayCredentialsError({
40
+ message: `Failed to read ${API_KEY_ENV}.`,
41
+ source: "env",
42
+ hints: hints.env,
43
+ cause
44
+ })));
45
+ const fromEnv = Effect.flatMap(envApiKey, Option.match({
46
+ onNone: () => Effect.fail(new AiGatewayCredentialsError({
47
+ message: `${API_KEY_ENV} is not set.`,
48
+ source: "env",
49
+ hints: hints.env
50
+ })),
51
+ onSome: (token) => Effect.succeed({
52
+ method: "api-key",
53
+ token
54
+ })
55
+ }));
56
+ const fromOidc = ambientToken.pipe(Effect.map((token) => ({
57
+ method: "oidc",
58
+ token
59
+ })), Effect.mapError((cause) => new AiGatewayCredentialsError({
60
+ message: cause.message,
61
+ source: "oidc",
62
+ hints: cause.hints,
63
+ cause
64
+ })));
65
+ /** A fixed API key. */
66
+ const layerFromApiKey = (apiKey) => Layer.succeed(AiGatewayCredentials)(Effect.succeed({
67
+ method: "api-key",
68
+ token: Redacted.isRedacted(apiKey) ? apiKey : Redacted.make(apiKey)
69
+ }));
70
+ /** `AI_GATEWAY_API_KEY`: local dev, CI, or anywhere off Vercel. */
71
+ const layerFromEnv = Layer.succeed(AiGatewayCredentials)(fromEnv);
72
+ /** The Vercel OIDC token (see `Oidc.ambientToken`). */
73
+ const layerFromOidc = Layer.succeed(AiGatewayCredentials)(fromOidc);
74
+ /**
75
+ * The default: `layerFromEnv`, then `layerFromOidc`. Same order as
76
+ * Vercel's own SDK, so it works locally with an API key and on Vercel without
77
+ * one. Falls back only when the key is unset, not when it can't be read.
78
+ */
79
+ const layer = Layer.succeed(AiGatewayCredentials)(Effect.flatMap(envApiKey, Option.match({
80
+ onNone: () => Effect.mapError(fromOidc, (cause) => new AiGatewayCredentialsError({
81
+ message: "No AI Gateway credential found.",
82
+ source: "default",
83
+ hints: hints.default,
84
+ cause
85
+ })),
86
+ onSome: (token) => Effect.succeed({
87
+ method: "api-key",
88
+ token
89
+ })
90
+ })));
91
+ //#endregion
92
+ export { API_KEY_ENV, AiGatewayCredentials, AiGatewayCredentialsError, formatHeaders, layer, layerFromApiKey, layerFromEnv, layerFromOidc };
93
+
94
+ //# sourceMappingURL=AiGatewayCredentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AiGatewayCredentials.js","names":["Oidc.hints"],"sources":["../src/AiGatewayCredentials.ts"],"sourcesContent":["/**\n * How gateway requests authenticate.\n *\n * `AiGatewayCredentials` holds an effect that produces the credential for one\n * request. It runs per request rather than once at startup: on Vercel the OIDC\n * token arrives on the request context and rotates, so it must not be cached.\n * `layer` is the default; pick a `layerFrom*` layer to use one source only.\n *\n * @example\n * ```ts\n * import * as AiGatewayCredentials from \"effect-vercel/AiGatewayCredentials\"\n *\n * AiGatewayCredentials.layer // AI_GATEWAY_API_KEY, else the Vercel OIDC token\n * AiGatewayCredentials.layerFromEnv // AI_GATEWAY_API_KEY only\n * AiGatewayCredentials.layerFromOidc // the Vercel OIDC token only\n * AiGatewayCredentials.layerFromApiKey(\"…\") // a fixed value\n * ```\n */\n\nimport { Config, Context, Data, Effect, Layer, Option, Redacted } from \"effect\"\nimport * as Oidc from \"./Oidc.js\"\n\n/** The environment variable `layerFromEnv` reads. */\nexport const API_KEY_ENV = \"AI_GATEWAY_API_KEY\"\n\n/** Which layer produced or failed to produce a credential. */\nexport type Source = \"api-key\" | \"env\" | \"oidc\" | \"default\"\n\n/**\n * A credential ready to put on a request. `method` is what the gateway\n * records as the auth method; it matches the values Vercel's own SDK sends.\n */\nexport interface Resolved {\n readonly method: \"api-key\" | \"oidc\"\n readonly token: Redacted.Redacted<string>\n}\n\n/** No credential could be produced for a gateway request. */\nexport class AiGatewayCredentialsError extends Data.TaggedError(\"AiGatewayCredentialsError\")<{\n readonly message: string\n readonly source: Source\n readonly hints: ReadonlyArray<string>\n readonly cause?: unknown\n}> {}\n\nexport class AiGatewayCredentials extends Context.Service<\n AiGatewayCredentials,\n Effect.Effect<Resolved, AiGatewayCredentialsError>\n>()(\"effect-vercel/AiGatewayCredentials\") {}\n\n/** The request headers that carry `credentials`. */\nexport const formatHeaders = (credentials: Resolved): Record<string, string> => ({\n \"x-api-key\": Redacted.value(credentials.token),\n \"ai-gateway-auth-method\": credentials.method,\n})\n\nconst hints: Record<Source, ReadonlyArray<string>> = {\n \"api-key\": [],\n env: [`Set ${API_KEY_ENV} to an AI Gateway API key.`],\n oidc: Oidc.hints,\n default: [\n `Set ${API_KEY_ENV} to an AI Gateway API key.`,\n \"Or run on Vercel / `vercel env pull` so an OIDC token is available.\",\n ],\n}\n\n/** `AI_GATEWAY_API_KEY`, or `None` when unset. Fails only when it can't be read. */\nconst envApiKey = Config.option(Config.Redacted(API_KEY_ENV)).pipe(\n Effect.mapError(\n (cause) =>\n new AiGatewayCredentialsError({\n message: `Failed to read ${API_KEY_ENV}.`,\n source: \"env\",\n hints: hints.env,\n cause,\n }),\n ),\n)\n\nconst fromEnv: Effect.Effect<Resolved, AiGatewayCredentialsError> = Effect.flatMap(\n envApiKey,\n Option.match({\n onNone: () =>\n Effect.fail(\n new AiGatewayCredentialsError({\n message: `${API_KEY_ENV} is not set.`,\n source: \"env\",\n hints: hints.env,\n }),\n ),\n onSome: (token) => Effect.succeed({ method: \"api-key\" as const, token }),\n }),\n)\n\nconst fromOidc: Effect.Effect<Resolved, AiGatewayCredentialsError> = Oidc.ambientToken.pipe(\n Effect.map((token) => ({ method: \"oidc\" as const, token })),\n Effect.mapError(\n (cause) =>\n new AiGatewayCredentialsError({\n message: cause.message,\n source: \"oidc\",\n hints: cause.hints,\n cause,\n }),\n ),\n)\n\n/** A fixed API key. */\nexport const layerFromApiKey = (\n apiKey: string | Redacted.Redacted<string>,\n): Layer.Layer<AiGatewayCredentials> =>\n Layer.succeed(AiGatewayCredentials)(\n Effect.succeed({\n method: \"api-key\",\n token: Redacted.isRedacted(apiKey) ? apiKey : Redacted.make(apiKey),\n }),\n )\n\n/** `AI_GATEWAY_API_KEY`: local dev, CI, or anywhere off Vercel. */\nexport const layerFromEnv: Layer.Layer<AiGatewayCredentials> =\n Layer.succeed(AiGatewayCredentials)(fromEnv)\n\n/** The Vercel OIDC token (see `Oidc.ambientToken`). */\nexport const layerFromOidc: Layer.Layer<AiGatewayCredentials> =\n Layer.succeed(AiGatewayCredentials)(fromOidc)\n\n/**\n * The default: `layerFromEnv`, then `layerFromOidc`. Same order as\n * Vercel's own SDK, so it works locally with an API key and on Vercel without\n * one. Falls back only when the key is unset, not when it can't be read.\n */\nexport const layer: Layer.Layer<AiGatewayCredentials> = Layer.succeed(AiGatewayCredentials)(\n Effect.flatMap(\n envApiKey,\n Option.match({\n onNone: () =>\n Effect.mapError(\n fromOidc,\n (cause) =>\n new AiGatewayCredentialsError({\n message: \"No AI Gateway credential found.\",\n source: \"default\",\n hints: hints.default,\n cause,\n }),\n ),\n onSome: (token) => Effect.succeed({ method: \"api-key\" as const, token }),\n }),\n ),\n)\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAa,cAAc;;AAe3B,IAAa,4BAAb,cAA+C,KAAK,YAAY,2BAA2B,CAAC,CAKzF,CAAC;AAEJ,IAAa,uBAAb,cAA0C,QAAQ,QAGhD,CAAC,CAAC,oCAAoC,CAAC,CAAC,CAAC;;AAG3C,MAAa,iBAAiB,iBAAmD;CAC/E,aAAa,SAAS,MAAM,YAAY,KAAK;CAC7C,0BAA0B,YAAY;AACxC;AAEA,MAAM,QAA+C;CACnD,WAAW,CAAC;CACZ,KAAK,CAAC,OAAO,YAAY,2BAA2B;CACpD,MAAMA;CACN,SAAS,CACP,OAAO,YAAY,6BACnB,qEACF;AACF;;AAGA,MAAM,YAAY,OAAO,OAAO,OAAO,SAAS,WAAW,CAAC,CAAC,CAAC,KAC5D,OAAO,UACJ,UACC,IAAI,0BAA0B;CAC5B,SAAS,kBAAkB,YAAY;CACvC,QAAQ;CACR,OAAO,MAAM;CACb;AACF,CAAC,CACL,CACF;AAEA,MAAM,UAA8D,OAAO,QACzE,WACA,OAAO,MAAM;CACX,cACE,OAAO,KACL,IAAI,0BAA0B;EAC5B,SAAS,GAAG,YAAY;EACxB,QAAQ;EACR,OAAO,MAAM;CACf,CAAC,CACH;CACF,SAAS,UAAU,OAAO,QAAQ;EAAE,QAAQ;EAAoB;CAAM,CAAC;AACzE,CAAC,CACH;AAEA,MAAM,WAAA,aAAiF,KACrF,OAAO,KAAK,WAAW;CAAE,QAAQ;CAAiB;AAAM,EAAE,GAC1D,OAAO,UACJ,UACC,IAAI,0BAA0B;CAC5B,SAAS,MAAM;CACf,QAAQ;CACR,OAAO,MAAM;CACb;AACF,CAAC,CACL,CACF;;AAGA,MAAa,mBACX,WAEA,MAAM,QAAQ,oBAAoB,CAAC,CACjC,OAAO,QAAQ;CACb,QAAQ;CACR,OAAO,SAAS,WAAW,MAAM,IAAI,SAAS,SAAS,KAAK,MAAM;AACpE,CAAC,CACH;;AAGF,MAAa,eACX,MAAM,QAAQ,oBAAoB,CAAC,CAAC,OAAO;;AAG7C,MAAa,gBACX,MAAM,QAAQ,oBAAoB,CAAC,CAAC,QAAQ;;;;;;AAO9C,MAAa,QAA2C,MAAM,QAAQ,oBAAoB,CAAC,CACzF,OAAO,QACL,WACA,OAAO,MAAM;CACX,cACE,OAAO,SACL,WACC,UACC,IAAI,0BAA0B;EAC5B,SAAS;EACT,QAAQ;EACR,OAAO,MAAM;EACb;CACF,CAAC,CACL;CACF,SAAS,UAAU,OAAO,QAAQ;EAAE,QAAQ;EAAoB;CAAM,CAAC;AACzE,CAAC,CACH,CACF"}
package/dist/Oidc.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { Effect, Redacted } from "effect";
2
+ //#region src/Oidc.d.ts
3
+ /** The environment variable read when no request carries a token. */
4
+ export declare const TOKEN_ENV = "VERCEL_OIDC_TOKEN";
5
+ declare const OidcError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
6
+ readonly _tag: "VercelOidcError";
7
+ } & Readonly<A>;
8
+ /** No usable OIDC token. */
9
+ export declare class OidcError extends OidcError_base<{
10
+ readonly message: string;
11
+ readonly hints: ReadonlyArray<string>;
12
+ readonly cause?: unknown;
13
+ }> {}
14
+ /** How to make a token available. */
15
+ export declare const hints: ReadonlyArray<string>;
16
+ /**
17
+ * The ambient token: the request header, else `VERCEL_OIDC_TOKEN`. Empty
18
+ * values count as unset. Fails when neither is set. Does not refresh an
19
+ * expired local token; re-run `vercel env pull`.
20
+ */
21
+ export declare const ambientToken: Effect.Effect<Redacted.Redacted<string>, OidcError>;
22
+ //#endregion
23
+ //# sourceMappingURL=Oidc.d.ts.map
package/dist/Oidc.js ADDED
@@ -0,0 +1,59 @@
1
+ import { Config, Data, Effect, Redacted } from "effect";
2
+ //#region src/Oidc.ts
3
+ /**
4
+ * The OIDC token Vercel issues to a deployment.
5
+ *
6
+ * `ambientToken` reads the token the platform provides. In Functions it
7
+ * arrives on each request as the `x-vercel-oidc-token` header and rotates, so
8
+ * it is read on every run, never cached. In builds and after `vercel env pull`
9
+ * it is `VERCEL_OIDC_TOKEN`. Use it to authenticate to the AI Gateway, or
10
+ * exchange it for cloud credentials (AWS `AssumeRoleWithWebIdentity`, GCP
11
+ * workload identity federation).
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import * as Oidc from "effect-vercel/Oidc"
16
+ * import { Effect, Redacted } from "effect"
17
+ *
18
+ * const program = Effect.gen(function* () {
19
+ * const token = yield* Oidc.ambientToken
20
+ * // pass Redacted.value(token) as the web identity token
21
+ * })
22
+ * ```
23
+ */
24
+ /** The environment variable read when no request carries a token. */
25
+ const TOKEN_ENV = "VERCEL_OIDC_TOKEN";
26
+ /** No usable OIDC token. */
27
+ var OidcError = class extends Data.TaggedError("VercelOidcError") {};
28
+ /** How to make a token available. */
29
+ const hints = ["Run on Vercel, where each request carries an OIDC token.", `Or run \`vercel env pull\` locally to populate ${TOKEN_ENV}.`];
30
+ /**
31
+ * Vercel exposes the current request's context on a well-known global symbol.
32
+ * In Functions, the request's headers live there. `@vercel/oidc` reads the
33
+ * same place.
34
+ */
35
+ const REQUEST_CONTEXT = Symbol.for("@vercel/request-context");
36
+ /** The `x-vercel-oidc-token` header of the current request, if set. */
37
+ const requestToken = () => {
38
+ const token = globalThis[REQUEST_CONTEXT]?.get?.()?.headers?.["x-vercel-oidc-token"];
39
+ return token === "" ? void 0 : token;
40
+ };
41
+ const envToken = Config.NonEmptyString(TOKEN_ENV).pipe(Config.map(Redacted.make));
42
+ /**
43
+ * The ambient token: the request header, else `VERCEL_OIDC_TOKEN`. Empty
44
+ * values count as unset. Fails when neither is set. Does not refresh an
45
+ * expired local token; re-run `vercel env pull`.
46
+ */
47
+ const ambientToken = Effect.suspend(() => {
48
+ const token = requestToken();
49
+ if (token !== void 0) return Effect.succeed(Redacted.make(token));
50
+ return Effect.mapError(envToken, (cause) => new OidcError({
51
+ message: "No Vercel OIDC token is available.",
52
+ hints,
53
+ cause
54
+ }));
55
+ });
56
+ //#endregion
57
+ export { OidcError, TOKEN_ENV, ambientToken, hints };
58
+
59
+ //# sourceMappingURL=Oidc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Oidc.js","names":["holder"],"sources":["../src/Oidc.ts"],"sourcesContent":["/**\n * The OIDC token Vercel issues to a deployment.\n *\n * `ambientToken` reads the token the platform provides. In Functions it\n * arrives on each request as the `x-vercel-oidc-token` header and rotates, so\n * it is read on every run, never cached. In builds and after `vercel env pull`\n * it is `VERCEL_OIDC_TOKEN`. Use it to authenticate to the AI Gateway, or\n * exchange it for cloud credentials (AWS `AssumeRoleWithWebIdentity`, GCP\n * workload identity federation).\n *\n * @example\n * ```ts\n * import * as Oidc from \"effect-vercel/Oidc\"\n * import { Effect, Redacted } from \"effect\"\n *\n * const program = Effect.gen(function* () {\n * const token = yield* Oidc.ambientToken\n * // pass Redacted.value(token) as the web identity token\n * })\n * ```\n */\n\nimport { Config, Data, Effect, Redacted } from \"effect\"\n\n/** The environment variable read when no request carries a token. */\nexport const TOKEN_ENV = \"VERCEL_OIDC_TOKEN\"\n\n/** No usable OIDC token. */\nexport class OidcError extends Data.TaggedError(\"VercelOidcError\")<{\n readonly message: string\n readonly hints: ReadonlyArray<string>\n readonly cause?: unknown\n}> {}\n\n/** How to make a token available. */\nexport const hints: ReadonlyArray<string> = [\n \"Run on Vercel, where each request carries an OIDC token.\",\n `Or run \\`vercel env pull\\` locally to populate ${TOKEN_ENV}.`,\n]\n\n/**\n * Vercel exposes the current request's context on a well-known global symbol.\n * In Functions, the request's headers live there. `@vercel/oidc` reads the\n * same place.\n */\nconst REQUEST_CONTEXT = Symbol.for(\"@vercel/request-context\")\n\ntype RequestContext = { readonly headers?: Record<string, string | undefined> }\n\n/** The `x-vercel-oidc-token` header of the current request, if set. */\nconst requestToken = (): string | undefined => {\n const holder = globalThis as typeof globalThis & {\n [REQUEST_CONTEXT]?: { get?: () => RequestContext | undefined }\n }\n const token = holder[REQUEST_CONTEXT]?.get?.()?.headers?.[\"x-vercel-oidc-token\"]\n return token === \"\" ? undefined : token\n}\n\nconst envToken = Config.NonEmptyString(TOKEN_ENV).pipe(Config.map(Redacted.make))\n\n/**\n * The ambient token: the request header, else `VERCEL_OIDC_TOKEN`. Empty\n * values count as unset. Fails when neither is set. Does not refresh an\n * expired local token; re-run `vercel env pull`.\n */\nexport const ambientToken: Effect.Effect<Redacted.Redacted<string>, OidcError> = Effect.suspend(\n () => {\n const token = requestToken()\n if (token !== undefined) return Effect.succeed(Redacted.make(token))\n return Effect.mapError(\n envToken,\n (cause) => new OidcError({ message: \"No Vercel OIDC token is available.\", hints, cause }),\n )\n },\n)\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAyBA,MAAa,YAAY;;AAGzB,IAAa,YAAb,cAA+B,KAAK,YAAY,iBAAiB,CAAC,CAI/D,CAAC;;AAGJ,MAAa,QAA+B,CAC1C,4DACA,kDAAkD,UAAU,EAC9D;;;;;;AAOA,MAAM,kBAAkB,OAAO,IAAI,yBAAyB;;AAK5D,MAAM,qBAAyC;CAI7C,MAAM,QAAQA,WAAO,gBAAgB,EAAE,MAAM,CAAC,EAAE,UAAU;CAC1D,OAAO,UAAU,KAAK,KAAA,IAAY;AACpC;AAEA,MAAM,WAAW,OAAO,eAAe,SAAS,CAAC,CAAC,KAAK,OAAO,IAAI,SAAS,IAAI,CAAC;;;;;;AAOhF,MAAa,eAAoE,OAAO,cAChF;CACJ,MAAM,QAAQ,aAAa;CAC3B,IAAI,UAAU,KAAA,GAAW,OAAO,OAAO,QAAQ,SAAS,KAAK,KAAK,CAAC;CACnE,OAAO,OAAO,SACZ,WACC,UAAU,IAAI,UAAU;EAAE,SAAS;EAAsC;EAAO;CAAM,CAAC,CAC1F;AACF,CACF"}
@@ -1,9 +1,7 @@
1
1
  import { Layer } from "effect";
2
2
  import { RuntimeCache } from "@vercel/functions";
3
3
  import { Persistence } from "effect/persistence";
4
- declare namespace VercelRuntimeCache_d_exports {
5
- export { layer, layerBacking, makeBacking };
6
- }
4
+ //#region src/RuntimeCache.d.ts
7
5
  /**
8
6
  * A `BackingPersistence` over `cache`.
9
7
  *
@@ -17,11 +15,10 @@ declare namespace VercelRuntimeCache_d_exports {
17
15
  * `getCache()` hashes keys with a 32-bit hash by default, which can collide.
18
16
  * Pass a cache made with a stronger `keyHashFunction`, as `layerBacking` does.
19
17
  */
20
- declare const makeBacking: (cache: RuntimeCache) => Persistence.BackingPersistence["Service"];
18
+ export declare const makeBacking: (cache: RuntimeCache) => Persistence.BackingPersistence["Service"];
21
19
  /** `BackingPersistence` over `getCache()`, with SHA-256 key hashing. */
22
- declare const layerBacking: Layer.Layer<Persistence.BackingPersistence>;
20
+ export declare const layerBacking: Layer.Layer<Persistence.BackingPersistence>;
23
21
  /** `Persistence` backed by the Vercel Runtime Cache. */
24
- declare const layer: Layer.Layer<Persistence.Persistence>;
22
+ export declare const layer: Layer.Layer<Persistence.Persistence>;
25
23
  //#endregion
26
- export { VercelRuntimeCache_d_exports as VercelRuntimeCache };
27
- //# sourceMappingURL=runtime-cache.d.ts.map
24
+ //# sourceMappingURL=RuntimeCache.d.ts.map