@supabase/server 0.1.1 → 0.1.2-rc.32

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.
@@ -0,0 +1,240 @@
1
+ # Core Primitives
2
+
3
+ ## When to use primitives
4
+
5
+ Use `withSupabase` or `createSupabaseContext` for standard use cases. Drop down to core primitives when you need:
6
+
7
+ - Multiple routes with different auth in a single handler
8
+ - Custom response headers or error formats
9
+ - Integration with frameworks other than the ones provided
10
+ - Pre-extracted credentials (e.g., from cookies, custom headers)
11
+ - Just auth verification without client creation
12
+
13
+ All primitives are available from `@supabase/server/core`.
14
+
15
+ ## The composition pipeline
16
+
17
+ The primitives compose into a pipeline. Each step is independent — use only what you need:
18
+
19
+ ```
20
+ resolveEnv() → SupabaseEnv
21
+ extractCredentials(request) → Credentials { token, apikey }
22
+ verifyCredentials(credentials, opts) → AuthResult { authType, token, userClaims, claims, keyName }
23
+ createContextClient(options) → SupabaseClient (RLS-scoped)
24
+ createAdminClient(options) → SupabaseClient (bypasses RLS)
25
+ ```
26
+
27
+ Or use the convenience function that combines extraction and verification:
28
+
29
+ ```
30
+ verifyAuth(request, opts) → AuthResult (extractCredentials + verifyCredentials in one call)
31
+ ```
32
+
33
+ ## resolveEnv
34
+
35
+ Resolves Supabase environment configuration from runtime variables. The only hard requirement is `SUPABASE_URL`.
36
+
37
+ ```ts
38
+ import { resolveEnv } from '@supabase/server/core'
39
+
40
+ const { data: env, error } = resolveEnv()
41
+ if (error) {
42
+ // error is an EnvError — e.g., SUPABASE_URL not set
43
+ console.error(error.message)
44
+ }
45
+ ```
46
+
47
+ With partial overrides:
48
+
49
+ ```ts
50
+ const { data: envOverridden } = resolveEnv({
51
+ url: 'http://localhost:54321',
52
+ })
53
+ ```
54
+
55
+ Returns `{ data: SupabaseEnv, error: null }` on success, `{ data: null, error: EnvError }` on failure.
56
+
57
+ ## extractCredentials
58
+
59
+ Pure extraction — reads headers, performs no validation.
60
+
61
+ ```ts
62
+ import { extractCredentials } from '@supabase/server/core'
63
+
64
+ const creds = extractCredentials(request)
65
+ // creds.token → string | null (from Authorization: Bearer <token>)
66
+ // creds.apikey → string | null (from apikey header)
67
+ ```
68
+
69
+ This is synchronous and never fails. Fields are `null` when the corresponding header is absent.
70
+
71
+ ## verifyCredentials
72
+
73
+ Verifies pre-extracted credentials against allowed auth modes. Use this when credentials come from a non-standard source (cookies, custom headers, etc.).
74
+
75
+ ```ts
76
+ import { verifyCredentials } from '@supabase/server/core'
77
+
78
+ const credentials = { token: cookieToken, apikey: null }
79
+ const { data: auth, error } = await verifyCredentials(credentials, {
80
+ allow: 'user',
81
+ })
82
+
83
+ if (error) {
84
+ return Response.json({ message: error.message }, { status: error.status })
85
+ }
86
+
87
+ console.log(auth!.authType) // 'user'
88
+ console.log(auth!.userClaims) // { id: '...', email: '...', role: 'authenticated' }
89
+ ```
90
+
91
+ Supports all auth mode syntax — single mode, arrays, and named keys:
92
+
93
+ ```ts
94
+ // Multiple modes
95
+ const { data: auth } = await verifyCredentials(creds, {
96
+ allow: ['user', 'public'],
97
+ })
98
+
99
+ // Named key
100
+ const { data: auth } = await verifyCredentials(creds, {
101
+ allow: 'public:web',
102
+ })
103
+
104
+ // Wildcard
105
+ const { data: auth } = await verifyCredentials(creds, {
106
+ allow: 'secret:*',
107
+ })
108
+ ```
109
+
110
+ ## verifyAuth
111
+
112
+ Convenience function that combines `extractCredentials` and `verifyCredentials` in a single call. Use this when working with a standard `Request`:
113
+
114
+ ```ts
115
+ import { verifyAuth } from '@supabase/server/core'
116
+
117
+ const { data: auth, error } = await verifyAuth(request, {
118
+ allow: 'user',
119
+ })
120
+
121
+ if (error) {
122
+ return Response.json({ message: error.message }, { status: error.status })
123
+ }
124
+
125
+ console.log(auth.userClaims!.id) // "d0f1a2b3-..."
126
+ console.log(auth.token) // the verified JWT string
127
+ ```
128
+
129
+ ## createContextClient
130
+
131
+ Creates a Supabase client scoped to the caller's identity. RLS policies apply.
132
+
133
+ ```ts
134
+ import { verifyAuth, createContextClient } from '@supabase/server/core'
135
+
136
+ // With a user's token (from verifyAuth)
137
+ const { data: auth } = await verifyAuth(request, { allow: 'user' })
138
+ const supabase = createContextClient({
139
+ auth: { token: auth!.token, keyName: auth!.keyName },
140
+ })
141
+ ```
142
+
143
+ ```ts
144
+ // Anonymous (no token) — RLS as anon role
145
+ const anonClient = createContextClient()
146
+ ```
147
+
148
+ The client is configured with:
149
+
150
+ - The publishable key as the `apikey` header
151
+ - The user's JWT as the `Authorization: Bearer` header (if token is provided)
152
+ - Server-safe auth settings: `persistSession: false`, `autoRefreshToken: false`, `detectSessionInUrl: false`
153
+
154
+ This function throws `EnvError` if `SUPABASE_URL` or the required publishable key is missing. Wrap in try/catch when using directly.
155
+
156
+ ## createAdminClient
157
+
158
+ Creates a Supabase client that bypasses Row-Level Security using a secret key.
159
+
160
+ ```ts
161
+ import { createAdminClient } from '@supabase/server/core'
162
+
163
+ const supabaseAdmin = createAdminClient()
164
+ ```
165
+
166
+ ```ts
167
+ // With a specific named key
168
+ const supabaseAdminInternal = createAdminClient({
169
+ auth: { keyName: 'internal' },
170
+ })
171
+ ```
172
+
173
+ Same server-safe settings as `createContextClient`. Throws `EnvError` if the secret key is missing.
174
+
175
+ ## Full example: custom multi-route handler
176
+
177
+ Using primitives to build a handler with different auth per route, without a framework:
178
+
179
+ ```ts
180
+ import {
181
+ verifyAuth,
182
+ createContextClient,
183
+ createAdminClient,
184
+ } from '@supabase/server/core'
185
+
186
+ export default {
187
+ fetch: async (req: Request) => {
188
+ const url = new URL(req.url)
189
+
190
+ // Public route — no auth needed
191
+ if (url.pathname === '/health') {
192
+ return Response.json({ status: 'ok' })
193
+ }
194
+
195
+ // User-authenticated route
196
+ if (url.pathname === '/todos') {
197
+ const { data: auth, error } = await verifyAuth(req, { allow: 'user' })
198
+ if (error) {
199
+ return Response.json(
200
+ { message: error.message },
201
+ { status: error.status },
202
+ )
203
+ }
204
+
205
+ const supabase = createContextClient({
206
+ auth: { token: auth!.token, keyName: auth!.keyName },
207
+ })
208
+ const { data } = await supabase.from('todos').select()
209
+ return Response.json(data)
210
+ }
211
+
212
+ // Admin route — secret key only
213
+ if (url.pathname === '/admin/users') {
214
+ const { data: auth, error } = await verifyAuth(req, {
215
+ allow: 'secret',
216
+ })
217
+ if (error) {
218
+ return Response.json(
219
+ { message: error.message },
220
+ { status: error.status },
221
+ )
222
+ }
223
+
224
+ const supabaseAdmin = createAdminClient({
225
+ auth: { keyName: auth!.keyName },
226
+ })
227
+ const { data } = await supabaseAdmin.from('profiles').select()
228
+ return Response.json(data)
229
+ }
230
+
231
+ return new Response('Not found', { status: 404 })
232
+ },
233
+ }
234
+ ```
235
+
236
+ ## SSR frameworks (Next.js, Nuxt, SvelteKit, Remix)
237
+
238
+ In SSR frameworks, the JWT lives in session cookies rather than the `Authorization` header. Use `verifyCredentials` with a token extracted from cookies, then create clients as usual. This is the key primitive that enables SSR integration — it accepts pre-extracted credentials from any source.
239
+
240
+ For a complete guide with cookie parsing, JWKS caching, env bridging, and full framework adapters, see [ssr-frameworks.md](ssr-frameworks.md).
@@ -0,0 +1,189 @@
1
+ ## Supabase environments (zero config)
2
+
3
+ On Supabase Platform and Local Development (CLI), all variables are auto-provisioned — no configuration needed
4
+
5
+ | Variable | Format | Description | Available in |
6
+ | --------------------------- | ---------------------------------- | ------------------------------------- | --------------------------------- |
7
+ | `SUPABASE_URL` | `https://<ref>.supabase.co` | Your Supabase project URL | All |
8
+ | `SUPABASE_PUBLISHABLE_KEYS` | `{"default":"sb_publishable_..."}` | Named publishable keys as JSON object | Platform, Local Development (CLI) |
9
+ | `SUPABASE_SECRET_KEYS` | `{"default":"sb_secret_..."}` | Named secret keys as JSON object | Platform, Local Development (CLI) |
10
+ | `SUPABASE_JWKS` | `{"keys":[...]}` or `[...]` | JSON Web Key Set for JWT verification | Platform, Local Development (CLI) |
11
+ | `SUPABASE_PUBLISHABLE_KEY` | `sb_publishable_...` | Single publishable key (fallback) | Self-hosted |
12
+ | `SUPABASE_SECRET_KEY` | `sb_secret_...` | Single secret key (fallback) | Self-hosted |
13
+
14
+ ## Non-Supabase environments (Node.js, Bun, Cloudflare, self-hosted)
15
+
16
+ Set these based on which auth modes your app uses:
17
+
18
+ | Variable | Required when |
19
+ | -------------------------- | ------------------------------------------ |
20
+ | `SUPABASE_URL` | Always |
21
+ | `SUPABASE_SECRET_KEY` | `allow: 'secret'` or using `supabaseAdmin` |
22
+ | `SUPABASE_PUBLISHABLE_KEY` | `allow: 'public'` |
23
+ | `SUPABASE_JWKS` | `allow: 'user'` (JWT verification) |
24
+
25
+ ### Minimal `.env` example
26
+
27
+ ```env
28
+ SUPABASE_URL=https://<ref>.supabase.co
29
+ SUPABASE_SECRET_KEY=sb_secret_...
30
+ SUPABASE_PUBLISHABLE_KEY=sb_publishable_...
31
+ SUPABASE_JWKS={"keys":[...]}
32
+ ```
33
+
34
+ ## Plural vs singular keys
35
+
36
+ The SDK checks the plural form first (`SUPABASE_PUBLISHABLE_KEYS`), then falls back to the singular form (`SUPABASE_PUBLISHABLE_KEY`). The same applies to secret keys.
37
+
38
+ ### Plural form — named keys as a JSON object
39
+
40
+ Use this when you have multiple keys for different clients (web, mobile, internal):
41
+
42
+ ```
43
+ SUPABASE_PUBLISHABLE_KEYS={"default":"sb_publishable_default_abc","web":"sb_publishable_web_xyz","mobile":"sb_publishable_mobile_123"}
44
+ SUPABASE_SECRET_KEYS={"default":"sb_secret_default_abc","internal":"sb_secret_internal_xyz"}
45
+ ```
46
+
47
+ You can then validate against specific keys with named key syntax:
48
+
49
+ ```ts
50
+ // Only accept the "web" publishable key
51
+ withSupabase({ allow: 'public:web' }, handler)
52
+
53
+ // Accept any secret key
54
+ withSupabase({ allow: 'secret:*' }, handler)
55
+ ```
56
+
57
+ ### Singular form — equivalent to a single "default" key
58
+
59
+ ```
60
+ SUPABASE_PUBLISHABLE_KEY=sb_publishable_default_abc
61
+ SUPABASE_SECRET_KEY=sb_secret_default_abc
62
+ ```
63
+
64
+ This is equivalent to setting the plural form with a single `"default"` entry:
65
+
66
+ ```
67
+ # These two are the same:
68
+ SUPABASE_PUBLISHABLE_KEY=sb_publishable_default_abc
69
+ SUPABASE_PUBLISHABLE_KEYS={"default":"sb_publishable_default_abc"}
70
+ ```
71
+
72
+ The singular form is a convenience for the common case where you only have one key. The SDK stores it internally as `{ default: "<value>" }`, so `allow: 'public'` (which looks for the `"default"` key) works with both forms.
73
+
74
+ ### Priority
75
+
76
+ When both singular and plural forms are set, the plural form takes priority.
77
+
78
+ ## JWKS format
79
+
80
+ `SUPABASE_JWKS` accepts two formats:
81
+
82
+ ```
83
+ # Standard JWKS format
84
+ SUPABASE_JWKS={"keys":[{"kty":"RSA","n":"...","e":"AQAB"}]}
85
+
86
+ # Bare array (convenience)
87
+ SUPABASE_JWKS=[{"kty":"RSA","n":"...","e":"AQAB"}]
88
+ ```
89
+
90
+ When `SUPABASE_JWKS` is not set, JWT verification (`allow: 'user'`) is unavailable.
91
+
92
+ ## Runtime-specific behavior
93
+
94
+ The SDK reads environment variables using this priority:
95
+
96
+ 1. `Deno.env.get(name)` — Deno (including Supabase Edge Functions)
97
+ 2. `process.env[name]` — Node.js, Bun, Cloudflare Workers (with node-compat)
98
+
99
+ ### Supabase Edge Functions
100
+
101
+ Environment variables are auto-provisioned by the platform. Nothing to configure.
102
+
103
+ ### Deno / Node.js / Bun
104
+
105
+ Set variables via `.env` files (with a loader like `dotenv` for Node.js) or your deployment platform's environment configuration.
106
+
107
+ ### Cloudflare Workers
108
+
109
+ Cloudflare Workers don't expose `Deno.env` or `process.env` by default. Two options:
110
+
111
+ 1. **Enable node-compat** in `wrangler.toml`:
112
+
113
+ ```toml
114
+ compatibility_flags = ["nodejs_compat"]
115
+ ```
116
+
117
+ 2. **Pass overrides** via the `env` config option:
118
+
119
+ ```ts
120
+ withSupabase(
121
+ {
122
+ allow: 'user',
123
+ env: {
124
+ url: env.SUPABASE_URL,
125
+ publishableKeys: { default: env.SUPABASE_PUBLISHABLE_KEY },
126
+ secretKeys: { default: env.SUPABASE_SECRET_KEY },
127
+ },
128
+ },
129
+ handler,
130
+ )
131
+ ```
132
+
133
+ ## Using env overrides
134
+
135
+ The `env` option on `withSupabase`, `createSupabaseContext`, and core primitives lets you override auto-detected values. Partial overrides are merged with what's resolved from environment variables:
136
+
137
+ ```ts
138
+ import { withSupabase } from '@supabase/server'
139
+
140
+ export default {
141
+ fetch: withSupabase(
142
+ {
143
+ allow: 'user',
144
+ env: {
145
+ url: 'http://localhost:54321', // override just the URL
146
+ },
147
+ },
148
+ handler,
149
+ ),
150
+ }
151
+ ```
152
+
153
+ ## Using resolveEnv directly
154
+
155
+ For manual environment resolution — useful in tests, custom setups, or debugging:
156
+
157
+ ```ts
158
+ import { resolveEnv } from '@supabase/server/core'
159
+
160
+ const { data: env, error } = resolveEnv()
161
+ if (error) {
162
+ console.error(`Missing config: ${error.message}`)
163
+ }
164
+
165
+ // With overrides
166
+ const { data: envOverridden } = resolveEnv({
167
+ url: 'http://localhost:54321',
168
+ publishableKeys: { default: 'test-key' },
169
+ })
170
+ ```
171
+
172
+ `resolveEnv` returns a `SupabaseEnv` object:
173
+
174
+ ```ts
175
+ interface SupabaseEnv {
176
+ url: string
177
+ publishableKeys: Record<string, string>
178
+ secretKeys: Record<string, string>
179
+ jwks: JsonWebKeySet | null
180
+ }
181
+ ```
182
+
183
+ ## Graceful parsing
184
+
185
+ Malformed JSON in environment variables doesn't throw — the SDK falls back to empty values:
186
+
187
+ - Malformed `SUPABASE_PUBLISHABLE_KEYS` or `SUPABASE_SECRET_KEYS` → empty `{}`
188
+ - Malformed `SUPABASE_JWKS` → `null` (JWT verification unavailable)
189
+ - Missing `SUPABASE_URL` → `EnvError` (this is the only hard requirement)
@@ -0,0 +1,191 @@
1
+ # Error Handling
2
+
3
+ ## Error classes
4
+
5
+ The SDK has two error classes, both with `status` (HTTP code) and `code` (machine-readable string) properties.
6
+
7
+ ### EnvError
8
+
9
+ Thrown when a required environment variable is missing or malformed. Always `status: 500` — these are server configuration issues, not client errors.
10
+
11
+ | Code | Meaning |
12
+ | --------------------------------- | -------------------------------------------------------------- |
13
+ | `MISSING_SUPABASE_URL` | `SUPABASE_URL` is not set |
14
+ | `MISSING_PUBLISHABLE_KEY` | Named publishable key not found in `SUPABASE_PUBLISHABLE_KEYS` |
15
+ | `MISSING_DEFAULT_PUBLISHABLE_KEY` | No default publishable key found |
16
+ | `MISSING_SECRET_KEY` | Named secret key not found in `SUPABASE_SECRET_KEYS` |
17
+ | `MISSING_DEFAULT_SECRET_KEY` | No default secret key found |
18
+ | `ENV_ERROR` | Generic environment error |
19
+
20
+ ### AuthError
21
+
22
+ Thrown when authentication or authorization fails. Status is `401` for invalid credentials, `500` for server-side auth failures.
23
+
24
+ | Code | Status | Meaning |
25
+ | ------------------------------ | ------ | ------------------------------------------- |
26
+ | `INVALID_CREDENTIALS` | 401 | No credential matched any allowed auth mode |
27
+ | `CREATE_SUPABASE_CLIENT_ERROR` | 500 | Auth succeeded but client creation failed |
28
+ | `AUTH_ERROR` | 401 | Generic authentication error |
29
+
30
+ ## How errors surface in each layer
31
+
32
+ Different layers of the SDK handle errors differently. Understanding which pattern each function uses prevents surprises.
33
+
34
+ | Function | Pattern | What happens on error |
35
+ | ------------------------- | ------------- | ------------------------------------------------------------------------ |
36
+ | `withSupabase()` | Auto-response | Returns `Response.json({ message, code }, { status })` with CORS headers |
37
+ | `createSupabaseContext()` | Result tuple | Returns `{ data: null, error: AuthError }` |
38
+ | `verifyAuth()` | Result tuple | Returns `{ data: null, error: AuthError }` |
39
+ | `verifyCredentials()` | Result tuple | Returns `{ data: null, error: AuthError }` |
40
+ | `resolveEnv()` | Result tuple | Returns `{ data: null, error: EnvError }` |
41
+ | `createContextClient()` | **Throws** | Throws `EnvError` |
42
+ | `createAdminClient()` | **Throws** | Throws `EnvError` |
43
+ | Hono `withSupabase()` | HTTPException | Throws `HTTPException` with `cause: AuthError` |
44
+
45
+ The two client factory functions (`createContextClient`, `createAdminClient`) are the only ones that throw. Everything else returns a result tuple `{ data, error }`.
46
+
47
+ ## Handling errors in withSupabase
48
+
49
+ `withSupabase` handles errors automatically. If auth fails, the caller receives a JSON response:
50
+
51
+ ```json
52
+ { "message": "Invalid credentials", "code": "INVALID_CREDENTIALS" }
53
+ ```
54
+
55
+ with the appropriate HTTP status code and CORS headers. Your handler never runs.
56
+
57
+ If you need custom error formatting, use `createSupabaseContext` instead:
58
+
59
+ ```ts
60
+ import { createSupabaseContext } from '@supabase/server'
61
+
62
+ export default {
63
+ fetch: async (req: Request) => {
64
+ const { data: ctx, error } = await createSupabaseContext(req, {
65
+ allow: 'user',
66
+ })
67
+
68
+ if (error) {
69
+ // Custom error format
70
+ return Response.json(
71
+ {
72
+ success: false,
73
+ error: { message: error.message, code: error.code },
74
+ },
75
+ { status: error.status },
76
+ )
77
+ }
78
+
79
+ const { data } = await ctx!.supabase.from('todos').select()
80
+ return Response.json({ success: true, data })
81
+ },
82
+ }
83
+ ```
84
+
85
+ ## Handling errors in Hono
86
+
87
+ The Hono adapter throws an `HTTPException` when auth fails. Access the original `AuthError` via `.cause`:
88
+
89
+ ```ts
90
+ import { Hono } from 'hono'
91
+ import { HTTPException } from 'hono/http-exception'
92
+ import { withSupabase } from '@supabase/server/adapters/hono'
93
+
94
+ const app = new Hono()
95
+
96
+ app.use('*', withSupabase({ allow: 'user' }))
97
+
98
+ app.onError((err, c) => {
99
+ if (err instanceof HTTPException && err.cause) {
100
+ const authError = err.cause
101
+ return c.json(
102
+ { message: authError.message, code: authError.code },
103
+ err.status,
104
+ )
105
+ }
106
+ return c.json({ message: 'Internal error' }, 500)
107
+ })
108
+ ```
109
+
110
+ ## Handling errors in core primitives
111
+
112
+ Result-tuple functions:
113
+
114
+ ```ts
115
+ import { verifyAuth, resolveEnv } from '@supabase/server/core'
116
+
117
+ // verifyAuth returns { data, error }
118
+ const { data: auth, error } = await verifyAuth(request, { allow: 'user' })
119
+ if (error) {
120
+ return Response.json({ message: error.message }, { status: error.status })
121
+ }
122
+
123
+ // resolveEnv returns { data, error }
124
+ const { data: env, error: envError } = resolveEnv()
125
+ if (envError) {
126
+ console.error(`Config issue [${envError.code}]: ${envError.message}`)
127
+ }
128
+ ```
129
+
130
+ Client factories throw — wrap them in try/catch:
131
+
132
+ ```ts
133
+ import {
134
+ verifyAuth,
135
+ createContextClient,
136
+ createAdminClient,
137
+ } from '@supabase/server/core'
138
+ import { EnvError } from '@supabase/server'
139
+
140
+ const { data: auth, error } = await verifyAuth(request, { allow: 'user' })
141
+ // ... handle error ...
142
+
143
+ try {
144
+ const supabase = createContextClient({ auth: { token: auth!.token } })
145
+ const supabaseAdmin = createAdminClient()
146
+ } catch (e) {
147
+ if (e instanceof EnvError) {
148
+ console.error(`Config issue [${e.code}]: ${e.message}`)
149
+ return Response.json({ message: e.message }, { status: 500 })
150
+ }
151
+ throw e
152
+ }
153
+ ```
154
+
155
+ ## Using the Errors factory map
156
+
157
+ The `Errors` object provides factory functions for creating error instances by code. Useful when building custom error handling or testing:
158
+
159
+ ```ts
160
+ import {
161
+ Errors,
162
+ MissingSupabaseURLError,
163
+ InvalidCredentialsError,
164
+ } from '@supabase/server'
165
+
166
+ // Create specific errors
167
+ const envError = Errors[MissingSupabaseURLError]()
168
+ // → EnvError { message: "SUPABASE_URL is required but not set", code: "MISSING_SUPABASE_URL", status: 500 }
169
+
170
+ const authError = Errors[InvalidCredentialsError]()
171
+ // → AuthError { message: "Invalid credentials", code: "INVALID_CREDENTIALS", status: 401 }
172
+ ```
173
+
174
+ ## Checking error types
175
+
176
+ ```ts
177
+ import { AuthError, EnvError } from '@supabase/server'
178
+
179
+ try {
180
+ // ... some operation
181
+ } catch (e) {
182
+ if (e instanceof AuthError) {
183
+ // e.status is 401 or 500
184
+ // e.code is 'INVALID_CREDENTIALS', 'CREATE_SUPABASE_CLIENT_ERROR', or 'AUTH_ERROR'
185
+ }
186
+ if (e instanceof EnvError) {
187
+ // e.status is always 500
188
+ // e.code is one of the MISSING_* constants or 'ENV_ERROR'
189
+ }
190
+ }
191
+ ```