@devicecloud.dev/dcd 5.9.0-beta.1 → 5.9.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.
@@ -35,7 +35,8 @@ import * as p from '@clack/prompts';
35
35
  import { defineCommand } from 'citty';
36
36
  import { spawn } from 'node:child_process';
37
37
  import { createHash, randomBytes } from 'node:crypto';
38
- import { ENVIRONMENTS, inferEnvFromApiUrl, resolveFrontendUrl } from '../config/environments.js';
38
+ import { ENVIRONMENTS, inferEnvFromApiUrl, resolveFrontendUrl, } from '../config/environments.js';
39
+ import { getSupabaseKey } from '../config/supabase-key.js';
39
40
  import { jsonFlag } from '../config/flags/output.flags.js';
40
41
  import { canPrompt } from '../utils/ci.js';
41
42
  import { CliError, logger } from '../utils/cli.js';
@@ -179,7 +180,7 @@ export const loginCommand = defineCommand({
179
180
  // rejects it, the token itself is bad; if Supabase accepts, the
180
181
  // api's JwtService is misconfigured.
181
182
  const iss = decodeJwtIssuer(payload.access_token);
182
- const supabaseUser = await probeSupabaseUser(iss, ENVIRONMENTS[env].supabase.anonKey, payload.access_token);
183
+ const supabaseUser = await probeSupabaseUser(iss, env, payload.access_token);
183
184
  const parts = [
184
185
  iss ? `token iss: ${iss}` : null,
185
186
  `supabase /auth/v1/user: ${supabaseUser}`,
@@ -287,14 +288,14 @@ async function pollForClaim(apiUrl, state, codeVerifier) {
287
288
  function base64url(buf) {
288
289
  return buf.toString('base64').replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
289
290
  }
290
- async function probeSupabaseUser(iss, anonKey, token) {
291
+ async function probeSupabaseUser(iss, env, token) {
291
292
  if (!iss)
292
293
  return 'skipped (no iss)';
293
294
  try {
294
295
  const res = await fetch(`${iss}/user`, {
295
296
  headers: {
296
297
  authorization: `Bearer ${token}`,
297
- apikey: anonKey,
298
+ apikey: await getSupabaseKey(env),
298
299
  },
299
300
  });
300
301
  const bodyText = await res.text().catch(() => '');
@@ -3,7 +3,6 @@
3
3
  * it on Supabase. Leaves DEVICE_CLOUD_API_KEY (env/flag) untouched.
4
4
  */
5
5
  import { defineCommand } from 'citty';
6
- import { ENVIRONMENTS } from '../config/environments.js';
7
6
  import { jsonFlag } from '../config/flags/output.flags.js';
8
7
  import { CliAuthGateway } from '../gateways/cli-auth-gateway.js';
9
8
  import { logger } from '../utils/cli.js';
@@ -31,8 +30,7 @@ export const logoutCommand = defineCommand({
31
30
  }
32
31
  return;
33
32
  }
34
- const { anonKey } = ENVIRONMENTS[config.env].supabase;
35
- await CliAuthGateway.signOut(config.supabase_url, anonKey, config.session);
33
+ await CliAuthGateway.signOut(config.supabase_url, config.env, config.session);
36
34
  clearConfig();
37
35
  if (json) {
38
36
  logger.json({ status: 'SUCCESS', loggedOut: true, user: config.session.user_email });
@@ -1,12 +1,10 @@
1
1
  /**
2
2
  * Per-env devicecloud.dev coordinates — single source of truth for the URLs
3
- * and Supabase credentials the CLI uses.
3
+ * the CLI uses.
4
4
  *
5
- * **These `anonKey` values are public.** Supabase anon keys are JWTs designed
6
- * to be embedded in client code (web apps, mobile apps, CLIs). They only
7
- * grant whatever the project's RLS policies expose. Don't worry about seeing
8
- * them here — every Supabase-based product ships its anon key the same way.
9
- * https://supabase.com/docs/guides/api/api-keys
5
+ * There is deliberately no Supabase key here. A compiled-in key can never be
6
+ * rotated without breaking every pinned CLI version, so it is fetched from the
7
+ * API at runtime instead: see `getSupabaseKey` in ./supabase-key.ts (dcd#1112).
10
8
  */
11
9
  export type DcdEnvName = 'dev' | 'prod';
12
10
  export interface DcdEnvironment {
@@ -15,13 +13,12 @@ export interface DcdEnvironment {
15
13
  supabase: {
16
14
  url: string;
17
15
  projectRef: string;
18
- anonKey: string;
19
16
  };
20
17
  /**
21
18
  * Pinned KEK public key for client-side binary envelope encryption (dcd#1138).
22
19
  * `key` is base64 of the raw 32-byte X25519 public key; `version` selects which
23
- * KEK the platform API unwraps with. **Public — safe to embed** (like the anon
24
- * key above): it can only *encrypt*; the private half lives solely on the API.
20
+ * KEK the platform API unwraps with. **Public — safe to embed**: it can only
21
+ * *encrypt*; the private half lives solely on the API.
25
22
  *
26
23
  * Left `null` until the KEK is generated and provisioned. Generate a keypair
27
24
  * with, e.g.:
@@ -1,12 +1,10 @@
1
1
  /**
2
2
  * Per-env devicecloud.dev coordinates — single source of truth for the URLs
3
- * and Supabase credentials the CLI uses.
3
+ * the CLI uses.
4
4
  *
5
- * **These `anonKey` values are public.** Supabase anon keys are JWTs designed
6
- * to be embedded in client code (web apps, mobile apps, CLIs). They only
7
- * grant whatever the project's RLS policies expose. Don't worry about seeing
8
- * them here — every Supabase-based product ships its anon key the same way.
9
- * https://supabase.com/docs/guides/api/api-keys
5
+ * There is deliberately no Supabase key here. A compiled-in key can never be
6
+ * rotated without breaking every pinned CLI version, so it is fetched from the
7
+ * API at runtime instead: see `getSupabaseKey` in ./supabase-key.ts (dcd#1112).
10
8
  */
11
9
  export const ENVIRONMENTS = {
12
10
  prod: {
@@ -15,7 +13,6 @@ export const ENVIRONMENTS = {
15
13
  supabase: {
16
14
  url: 'https://cloud.devicecloud.dev',
17
15
  projectRef: 'pgydnphbimetinsgfkbo',
18
- anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBneWRucGhiaW1ldGluc2dma2JvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDc1OTQzNDYsImV4cCI6MjAyMzE3MDM0Nn0.hAYOMFxxwX1exkQkY9xyQJGC_GhGnyogkj2N-kBkMI8',
19
16
  },
20
17
  kekPublicKey: { version: 1, key: 'wtfyWEwK7nJzwI4PD+9RAW8jxIR1u8kMQq2IhsrVnH4=' },
21
18
  },
@@ -25,7 +22,6 @@ export const ENVIRONMENTS = {
25
22
  supabase: {
26
23
  url: 'https://lbmsowehtjwnqlurpemb.supabase.co',
27
24
  projectRef: 'lbmsowehtjwnqlurpemb',
28
- anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxibXNvd2VodGp3bnFsdXJwZW1iIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDkyMTg0ODcsImV4cCI6MjAyNDc5NDQ4N30.zeLTMAuZ_WwYvGdeP0kdvL_Zrs-RQee5APPyxmWq7qQ',
29
25
  },
30
26
  kekPublicKey: { version: 1, key: 'RgcToF/OJpcQI9koYvSvtj/WLaebfcN4v5GJoqtr/00=' },
31
27
  },
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The Supabase publishable key, fetched from the dcd API at runtime (dcd#1112).
3
+ *
4
+ * Releases up to this one compiled the anon key in, so the key could never be
5
+ * rotated without breaking every CLI version pinned in customer CI. The API now
6
+ * serves it from the public `GET /cli-config`, and rotating it is an env change
7
+ * on the API.
8
+ *
9
+ * Fetched once per process and kept in memory only. It is deliberately not
10
+ * cached on disk: a stale key makes GoTrue answer a refresh with a 4xx, which
11
+ * `resolveAuth` treats as a dead session and deletes the user's login.
12
+ *
13
+ * Only the key comes from the API. The Supabase URLs stay pinned in
14
+ * `ENVIRONMENTS`: prod goes through the `cloud.devicecloud.dev` custom domain,
15
+ * which customer firewalls may allow-list, not the API's raw `supabase.co` URL.
16
+ */
17
+ import { type DcdEnvName } from './environments.js';
18
+ /** The publishable key for `env`. A failed fetch is not cached, so the next call retries. */
19
+ export declare function getSupabaseKey(env: DcdEnvName): Promise<string>;
20
+ /**
21
+ * Headers that authenticate a raw Storage/Auth request with `key` alone. A
22
+ * legacy anon key is a JWT and also goes in `Authorization`; an
23
+ * `sb_publishable_` key is not a JWT and must only be sent as `apikey`.
24
+ */
25
+ export declare function supabaseKeyHeaders(key: string): Record<string, string>;
26
+ /** Test seam: forget fetched keys. */
27
+ export declare function resetSupabaseKeyCache(): void;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The Supabase publishable key, fetched from the dcd API at runtime (dcd#1112).
3
+ *
4
+ * Releases up to this one compiled the anon key in, so the key could never be
5
+ * rotated without breaking every CLI version pinned in customer CI. The API now
6
+ * serves it from the public `GET /cli-config`, and rotating it is an env change
7
+ * on the API.
8
+ *
9
+ * Fetched once per process and kept in memory only. It is deliberately not
10
+ * cached on disk: a stale key makes GoTrue answer a refresh with a 4xx, which
11
+ * `resolveAuth` treats as a dead session and deletes the user's login.
12
+ *
13
+ * Only the key comes from the API. The Supabase URLs stay pinned in
14
+ * `ENVIRONMENTS`: prod goes through the `cloud.devicecloud.dev` custom domain,
15
+ * which customer firewalls may allow-list, not the API's raw `supabase.co` URL.
16
+ */
17
+ import { ENVIRONMENTS } from './environments.js';
18
+ const FETCH_TIMEOUT_MS = 10_000;
19
+ const cache = new Map();
20
+ async function fetchSupabaseKey(env) {
21
+ const url = `${ENVIRONMENTS[env].apiUrl}/cli-config`;
22
+ const res = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
23
+ if (!res.ok) {
24
+ throw new Error(`Failed to fetch CLI config from ${url}: HTTP ${res.status}`);
25
+ }
26
+ const body = (await res.json());
27
+ const key = body.supabase_publishable_key;
28
+ if (typeof key !== 'string' || key.length === 0) {
29
+ throw new Error(`CLI config from ${url} has no supabase_publishable_key`);
30
+ }
31
+ return key;
32
+ }
33
+ /** The publishable key for `env`. A failed fetch is not cached, so the next call retries. */
34
+ export function getSupabaseKey(env) {
35
+ let pending = cache.get(env);
36
+ if (!pending) {
37
+ pending = fetchSupabaseKey(env);
38
+ cache.set(env, pending);
39
+ pending.catch(() => cache.delete(env));
40
+ }
41
+ return pending;
42
+ }
43
+ /**
44
+ * Headers that authenticate a raw Storage/Auth request with `key` alone. A
45
+ * legacy anon key is a JWT and also goes in `Authorization`; an
46
+ * `sb_publishable_` key is not a JWT and must only be sent as `apikey`.
47
+ */
48
+ export function supabaseKeyHeaders(key) {
49
+ return key.startsWith('sb_') ? { apikey: key } : { apikey: key, authorization: `Bearer ${key}` };
50
+ }
51
+ /** Test seam: forget fetched keys. */
52
+ export function resetSupabaseKeyCache() {
53
+ cache.clear();
54
+ }
@@ -1,3 +1,4 @@
1
+ import type { DcdEnvName } from '../config/environments.js';
1
2
  import type { StoredSession } from '../utils/config-store.js';
2
3
  /**
3
4
  * Thrown when a session refresh fails. `definitive` distinguishes "Supabase
@@ -18,6 +19,6 @@ export interface RefreshedSession {
18
19
  user_id: string;
19
20
  }
20
21
  export declare const CliAuthGateway: {
21
- refresh(supabaseUrl: string, supabaseAnonKey: string, session: StoredSession): Promise<RefreshedSession>;
22
- signOut(supabaseUrl: string, supabaseAnonKey: string, session: StoredSession): Promise<void>;
22
+ refresh(supabaseUrl: string, env: DcdEnvName, session: StoredSession): Promise<RefreshedSession>;
23
+ signOut(supabaseUrl: string, env: DcdEnvName, session: StoredSession): Promise<void>;
23
24
  };
@@ -6,6 +6,7 @@
6
6
  * login command's PKCE rendezvous flow (see src/commands/login.ts).
7
7
  */
8
8
  import { createClient, isAuthApiError } from '@supabase/supabase-js';
9
+ import { getSupabaseKey } from '../config/supabase-key.js';
9
10
  /**
10
11
  * Thrown when a session refresh fails. `definitive` distinguishes "Supabase
11
12
  * rejected this refresh token" (revoked, already used, malformed — re-login
@@ -20,14 +21,23 @@ export class SessionRefreshError extends Error {
20
21
  this.name = 'SessionRefreshError';
21
22
  }
22
23
  }
23
- function client(supabaseUrl, supabaseAnonKey) {
24
- return createClient(supabaseUrl, supabaseAnonKey, {
24
+ function client(supabaseUrl, supabaseKey) {
25
+ return createClient(supabaseUrl, supabaseKey, {
25
26
  auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false },
26
27
  });
27
28
  }
28
29
  export const CliAuthGateway = {
29
- async refresh(supabaseUrl, supabaseAnonKey, session) {
30
- const sb = client(supabaseUrl, supabaseAnonKey);
30
+ async refresh(supabaseUrl, env, session) {
31
+ let supabaseKey;
32
+ try {
33
+ supabaseKey = await getSupabaseKey(env);
34
+ }
35
+ catch (error) {
36
+ // Never definitive: the refresh token was not even tried, so the stored
37
+ // session must survive for the next attempt.
38
+ throw new SessionRefreshError(`Failed to refresh session: ${error.message}`, false);
39
+ }
40
+ const sb = client(supabaseUrl, supabaseKey);
31
41
  const { data, error } = await sb.auth.refreshSession({
32
42
  refresh_token: session.refresh_token,
33
43
  });
@@ -53,14 +63,14 @@ export const CliAuthGateway = {
53
63
  user_id: u.id,
54
64
  };
55
65
  },
56
- async signOut(supabaseUrl, supabaseAnonKey, session) {
66
+ async signOut(supabaseUrl, env, session) {
57
67
  // `scope: 'local'` clears this client's SDK state only — no server call,
58
68
  // no revocation. supabase-js defaults to `scope: 'global'`, which revokes
59
69
  // EVERY session for the user (browser, mobile, other CLIs), so
60
70
  // `dcd logout` would kick the user out of the web app. We don't want
61
71
  // that — logging out locally should be local.
62
- const sb = client(supabaseUrl, supabaseAnonKey);
63
72
  try {
73
+ const sb = client(supabaseUrl, await getSupabaseKey(env));
64
74
  await sb.auth.setSession({
65
75
  access_token: session.access_token,
66
76
  refresh_token: session.refresh_token,
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import { createClient, REALTIME_SUBSCRIBE_STATES, } from '@supabase/supabase-js';
17
17
  import { ENVIRONMENTS } from '../config/environments.js';
18
+ import { getSupabaseKey } from '../config/supabase-key.js';
18
19
  export class RealtimeResultsGateway {
19
20
  /**
20
21
  * Open a realtime subscription to `results` changes for the given upload.
@@ -34,17 +35,28 @@ export class RealtimeResultsGateway {
34
35
  onConnectionChange?.(next);
35
36
  };
36
37
  let client;
37
- try {
38
- const { url, anonKey } = ENVIRONMENTS[env].supabase;
39
- client = createClient(url, anonKey, {
38
+ let channel;
39
+ let closed = false;
40
+ // The latest JWT, so a refresh that lands while the key is still being
41
+ // fetched is the one the socket joins with.
42
+ let token = accessToken;
43
+ // The publishable key comes from the API (dcd#1112), so the socket opens
44
+ // asynchronously. `subscribe` stays synchronous for the caller: until it
45
+ // connects, `isConnected` is false and the backstop poll covers the gap.
46
+ const ready = getSupabaseKey(env)
47
+ .then((supabaseKey) => {
48
+ if (closed)
49
+ return;
50
+ const sb = createClient(ENVIRONMENTS[env].supabase.url, supabaseKey, {
40
51
  // Match SupabaseClientGateway / the frontend; no session persistence —
41
52
  // we set the token explicitly below.
42
53
  auth: { autoRefreshToken: false, persistSession: false },
43
54
  realtime: { params: { eventsPerSecond: 10 } },
44
55
  });
56
+ client = sb;
45
57
  // Attach the user's JWT to the socket so RLS is enforced on the channel.
46
- client.realtime.setAuth(accessToken);
47
- const channel = client
58
+ sb.realtime.setAuth(token);
59
+ channel = sb
48
60
  .channel(`results-cli-${uploadId}`)
49
61
  .on(
50
62
  // The typings don't cover the postgres_changes overload cleanly.
@@ -73,29 +85,8 @@ export class RealtimeResultsGateway {
73
85
  setConnected(false);
74
86
  }
75
87
  });
76
- const activeClient = client;
77
- return {
78
- isConnected: () => connected,
79
- async unsubscribe() {
80
- setConnected(false);
81
- try {
82
- await activeClient.removeChannel(channel);
83
- await activeClient.realtime.disconnect();
84
- }
85
- catch {
86
- /* best effort — process is exiting anyway */
87
- }
88
- },
89
- updateAccessToken(nextToken) {
90
- // setAuth updates the join payload and pushes the token to joined
91
- // channels. A failure only costs realtime; the backstop poll stays.
92
- activeClient.realtime.setAuth(nextToken).catch((error) => {
93
- dbg(`failed to update the socket's token: ${error instanceof Error ? error.message : String(error)}`);
94
- });
95
- },
96
- };
97
- }
98
- catch (error) {
88
+ })
89
+ .catch((error) => {
99
90
  dbg(`failed to subscribe: ${error instanceof Error ? error.message : String(error)}`);
100
91
  // Best-effort cleanup of a half-built client, then degrade to polling.
101
92
  try {
@@ -104,11 +95,34 @@ export class RealtimeResultsGateway {
104
95
  catch {
105
96
  /* ignore */
106
97
  }
107
- return {
108
- isConnected: () => false,
109
- async unsubscribe() { },
110
- updateAccessToken() { },
111
- };
112
- }
98
+ client = undefined;
99
+ channel = undefined;
100
+ });
101
+ return {
102
+ isConnected: () => connected,
103
+ async unsubscribe() {
104
+ closed = true;
105
+ setConnected(false);
106
+ await ready;
107
+ if (!client)
108
+ return;
109
+ try {
110
+ if (channel)
111
+ await client.removeChannel(channel);
112
+ await client.realtime.disconnect();
113
+ }
114
+ catch {
115
+ /* best effort — process is exiting anyway */
116
+ }
117
+ },
118
+ updateAccessToken(nextToken) {
119
+ token = nextToken;
120
+ // setAuth updates the join payload and pushes the token to joined
121
+ // channels. A failure only costs realtime; the backstop poll stays.
122
+ client?.realtime.setAuth(nextToken).catch((error) => {
123
+ dbg(`failed to update the socket's token: ${error instanceof Error ? error.message : String(error)}`);
124
+ });
125
+ },
126
+ };
113
127
  }
114
128
  }
@@ -9,7 +9,7 @@ export interface ResumableUploadSource {
9
9
  export declare class SupabaseGateway {
10
10
  /**
11
11
  * Upload to Supabase using resumable uploads (TUS protocol)
12
- * Uploads to staging location (uploads/{id}/) using anon key
12
+ * Uploads to staging location (uploads/{id}/) using the publishable key
13
13
  * File is later moved to final location by API after finalization
14
14
  * @param env - Environment (dev or prod)
15
15
  * @param path - Staging storage path (uploads/{id}/file.ext)
@@ -2,10 +2,11 @@ import { createReadStream } from 'node:fs';
2
2
  import { createClient } from '@supabase/supabase-js';
3
3
  import * as tus from 'tus-js-client';
4
4
  import { ENVIRONMENTS } from '../config/environments.js';
5
+ import { getSupabaseKey, supabaseKeyHeaders } from '../config/supabase-key.js';
5
6
  export class SupabaseGateway {
6
7
  /**
7
8
  * Upload to Supabase using resumable uploads (TUS protocol)
8
- * Uploads to staging location (uploads/{id}/) using anon key
9
+ * Uploads to staging location (uploads/{id}/) using the publishable key
9
10
  * File is later moved to final location by API after finalization
10
11
  * @param env - Environment (dev or prod)
11
12
  * @param path - Staging storage path (uploads/{id}/file.ext)
@@ -15,7 +16,8 @@ export class SupabaseGateway {
15
16
  * @returns Promise that resolves when upload completes
16
17
  */
17
18
  static async uploadResumable(env, path, source, debug = false, onProgress) {
18
- const { anonKey: SUPABASE_PUBLIC_KEY, projectRef } = ENVIRONMENTS[env].supabase;
19
+ const { projectRef } = ENVIRONMENTS[env].supabase;
20
+ const supabaseKey = await getSupabaseKey(env);
19
21
  const storageUrl = `https://${projectRef}.storage.supabase.co`;
20
22
  if (debug) {
21
23
  console.log(`[DEBUG] Resumable upload starting...`);
@@ -35,9 +37,9 @@ export class SupabaseGateway {
35
37
  // Retry configuration - will retry 5 times with increasing delays
36
38
  retryDelays: [0, 3000, 5000, 10_000, 20_000],
37
39
  // Authentication and headers
38
- // Use anon key for staging uploads to uploads/* folder
40
+ // Publishable key for staging uploads to uploads/* folder
39
41
  headers: {
40
- authorization: `Bearer ${SUPABASE_PUBLIC_KEY}`,
42
+ ...supabaseKeyHeaders(supabaseKey),
41
43
  'x-upsert': 'true', // Allow overwriting existing files
42
44
  },
43
45
  // Upload metadata
@@ -93,7 +95,7 @@ export class SupabaseGateway {
93
95
  });
94
96
  }
95
97
  static async uploadToSignedUrl(env, path, token, file, debug = false) {
96
- const { url: SUPABASE_URL, anonKey: SUPABASE_PUBLIC_KEY } = ENVIRONMENTS[env].supabase;
98
+ const { url: SUPABASE_URL } = ENVIRONMENTS[env].supabase;
97
99
  if (debug) {
98
100
  console.log(`[DEBUG] Supabase upload starting...`);
99
101
  console.log(`[DEBUG] Supabase URL: ${SUPABASE_URL}`);
@@ -102,7 +104,7 @@ export class SupabaseGateway {
102
104
  console.log(`[DEBUG] File type: ${file.type}`);
103
105
  }
104
106
  try {
105
- const supabase = createClient(SUPABASE_URL, SUPABASE_PUBLIC_KEY);
107
+ const supabase = createClient(SUPABASE_URL, await getSupabaseKey(env));
106
108
  const uploadToUrl = await supabase.storage
107
109
  .from('organizations')
108
110
  .uploadToSignedUrl(path, token, file);
@@ -9,7 +9,6 @@
9
9
  * poll), which re-checks with isAuthExpiring / refreshAuth below.
10
10
  */
11
11
  import { closeSync, openSync, rmSync, statSync } from 'node:fs';
12
- import { ENVIRONMENTS } from '../config/environments.js';
13
12
  import { CliAuthGateway, SessionRefreshError, } from '../gateways/cli-auth-gateway.js';
14
13
  import { telemetry } from '../services/telemetry.service.js';
15
14
  import { CliError } from './cli.js';
@@ -144,10 +143,9 @@ async function refreshSessionWithLock(initial) {
144
143
  if (session.expires_at > now + REFRESH_SKEW_SECONDS) {
145
144
  return { config: current, session };
146
145
  }
147
- const { anonKey } = ENVIRONMENTS[current.env].supabase;
148
146
  let refreshed;
149
147
  try {
150
- refreshed = await CliAuthGateway.refresh(current.supabase_url, anonKey, session);
148
+ refreshed = await CliAuthGateway.refresh(current.supabase_url, current.env, session);
151
149
  }
152
150
  catch (error) {
153
151
  if (error instanceof SessionRefreshError && error.definitive) {
package/package.json CHANGED
@@ -68,7 +68,7 @@
68
68
  "test:unit": "node scripts/test-runner.mjs --unit",
69
69
  "typecheck": "tsc --noEmit -p tsconfig.test.json"
70
70
  },
71
- "version": "5.9.0-beta.1",
71
+ "version": "5.9.0",
72
72
  "bugs": {
73
73
  "url": "https://discord.gg/gm3mJwcNw8"
74
74
  },