@tiangong-lca/cli 0.1.2 → 0.1.5

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/README.md +51 -12
  2. package/dist/src/auth-identity-receipt.d.ts +2 -0
  3. package/dist/src/auth-identity-receipt.js +2 -0
  4. package/dist/src/auth-identity-receipt.js.map +1 -0
  5. package/dist/src/cli.d.ts +4 -0
  6. package/dist/src/cli.js +264 -7
  7. package/dist/src/cli.js.map +1 -1
  8. package/dist/src/lib/auth-identity-receipt.d.ts +2 -2
  9. package/dist/src/lib/auth-identity-receipt.js +20 -12
  10. package/dist/src/lib/auth-identity-receipt.js.map +1 -1
  11. package/dist/src/lib/env.d.ts +8 -0
  12. package/dist/src/lib/env.js +36 -3
  13. package/dist/src/lib/env.js.map +1 -1
  14. package/dist/src/lib/lca-release.d.ts +1 -1
  15. package/dist/src/lib/lca-release.js +3 -4
  16. package/dist/src/lib/lca-release.js.map +1 -1
  17. package/dist/src/lib/lifecyclemodel-resulting-process.js +6 -2
  18. package/dist/src/lib/lifecyclemodel-resulting-process.js.map +1 -1
  19. package/dist/src/lib/oauth-loopback.d.ts +52 -0
  20. package/dist/src/lib/oauth-loopback.js +229 -0
  21. package/dist/src/lib/oauth-loopback.js.map +1 -0
  22. package/dist/src/lib/oauth-pkce.d.ts +63 -0
  23. package/dist/src/lib/oauth-pkce.js +268 -0
  24. package/dist/src/lib/oauth-pkce.js.map +1 -0
  25. package/dist/src/lib/process-refresh-references.js +2 -2
  26. package/dist/src/lib/process-refresh-references.js.map +1 -1
  27. package/dist/src/lib/process-scope-statistics.js +8 -3
  28. package/dist/src/lib/process-scope-statistics.js.map +1 -1
  29. package/dist/src/lib/state-lock.js +10 -4
  30. package/dist/src/lib/state-lock.js.map +1 -1
  31. package/dist/src/lib/supabase-client.d.ts +13 -1
  32. package/dist/src/lib/supabase-client.js +64 -3
  33. package/dist/src/lib/supabase-client.js.map +1 -1
  34. package/dist/src/lib/supabase-json-ordered-write.js +7 -3
  35. package/dist/src/lib/supabase-json-ordered-write.js.map +1 -1
  36. package/dist/src/lib/supabase-session.d.ts +63 -8
  37. package/dist/src/lib/supabase-session.js +315 -21
  38. package/dist/src/lib/supabase-session.js.map +1 -1
  39. package/package.json +5 -1
@@ -1,14 +1,17 @@
1
- import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
1
+ import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { createClient } from '@supabase/supabase-js';
5
5
  import { CliError } from './errors.js';
6
+ import { openSystemBrowser, receiveOAuthLoopbackCallback, SYSTEM_BROWSER_OPTIONS, } from './oauth-loopback.js';
7
+ import { buildOAuthAuthorizationUrl, createOAuthPkceValues, DEFAULT_OAUTH_SCOPES, exchangeOAuthAuthorizationCode, fetchOAuthUserInfo, refreshOAuthTokens, } from './oauth-pkce.js';
6
8
  import { withStateFileLock } from './state-lock.js';
7
9
  import { createSupabaseFetch, deriveSupabaseProjectBaseUrl, } from './supabase-client.js';
8
10
  import { fingerprintSecret, fingerprintUserApiKey, requireUserApiKeyCredentials, } from './user-api-key.js';
9
11
  const DEFAULT_TIMEOUT_MS = 10_000;
10
12
  const SESSION_REFRESH_WINDOW_SECONDS = 300;
11
13
  const SESSION_MEMORY_CACHE = new Map();
14
+ const ACCESS_TOKEN_MEMORY_CACHE = new Map();
12
15
  const SESSION_OPERATION_CHAINS = new Map();
13
16
  function trimToken(value) {
14
17
  return typeof value === 'string' ? value.trim() : '';
@@ -32,12 +35,19 @@ function parseCachedSessionRecord(value) {
32
35
  if (!isRecord(value)) {
33
36
  return null;
34
37
  }
35
- if (value.schema_version !== 1) {
38
+ if (value.schema_version !== 1 && value.schema_version !== 2) {
36
39
  return null;
37
40
  }
38
41
  const supabaseUrl = trimToken(value.supabase_url);
39
42
  const publishableKeyFingerprint = trimToken(value.publishable_key_fingerprint);
40
- const userApiKeyFingerprint = trimToken(value.user_api_key_fingerprint);
43
+ const authMethod = value.schema_version === 1
44
+ ? 'legacy_user_api_key'
45
+ : value.auth_method === 'oauth' || value.auth_method === 'legacy_user_api_key'
46
+ ? value.auth_method
47
+ : null;
48
+ const authBindingFingerprint = value.schema_version === 1
49
+ ? trimToken(value.user_api_key_fingerprint)
50
+ : trimToken(value.auth_binding_fingerprint);
41
51
  const userEmail = trimToken(value.user_email);
42
52
  const accessToken = trimToken(value.access_token);
43
53
  const refreshToken = trimToken(value.refresh_token);
@@ -47,25 +57,35 @@ function parseCachedSessionRecord(value) {
47
57
  : value.expires_at === null
48
58
  ? null
49
59
  : NaN;
60
+ const grantedScopes = value.schema_version === 1
61
+ ? []
62
+ : Array.isArray(value.granted_scopes) &&
63
+ value.granted_scopes.every((scope) => typeof scope === 'string' && scope.trim())
64
+ ? [...new Set(value.granted_scopes.map((scope) => scope.trim()))].sort((left, right) => left.localeCompare(right))
65
+ : null;
50
66
  if (!supabaseUrl ||
51
67
  !publishableKeyFingerprint ||
52
- !userApiKeyFingerprint ||
68
+ !authMethod ||
69
+ !authBindingFingerprint ||
53
70
  !userEmail ||
54
71
  !accessToken ||
55
72
  !refreshToken ||
56
73
  !updatedAtUtc ||
57
- Number.isNaN(expiresAt)) {
74
+ Number.isNaN(expiresAt) ||
75
+ grantedScopes === null) {
58
76
  return null;
59
77
  }
60
78
  return {
61
- schema_version: 1,
79
+ schema_version: 2,
80
+ auth_method: authMethod,
62
81
  supabase_url: supabaseUrl,
63
82
  publishable_key_fingerprint: publishableKeyFingerprint,
64
- user_api_key_fingerprint: userApiKeyFingerprint,
83
+ auth_binding_fingerprint: authBindingFingerprint,
65
84
  user_email: userEmail,
66
85
  access_token: accessToken,
67
86
  refresh_token: refreshToken,
68
87
  expires_at: expiresAt,
88
+ granted_scopes: grantedScopes,
69
89
  updated_at_utc: updatedAtUtc,
70
90
  };
71
91
  }
@@ -85,7 +105,7 @@ function resolveDefaultSessionFilePath(options) {
85
105
  return path.resolve('.tiangong-lca-session.json');
86
106
  }
87
107
  function resolveSessionFilePath(runtime) {
88
- if (runtime.disableSessionCache) {
108
+ if (runtime.authMode === 'access_token' || runtime.disableSessionCache) {
89
109
  return null;
90
110
  }
91
111
  if (runtime.sessionFile) {
@@ -101,17 +121,23 @@ function resolveSessionFilePath(runtime) {
101
121
  function buildRuntimeIdentity(runtime) {
102
122
  const projectBaseUrl = deriveSupabaseProjectBaseUrl(runtime.apiBaseUrl);
103
123
  const publishableKeyFingerprint = fingerprintSecret(runtime.publishableKey);
104
- const userApiKeyFingerprint = fingerprintUserApiKey(runtime.userApiKey);
124
+ const authBindingFingerprint = runtime.authMode === 'oauth'
125
+ ? fingerprintSecret(`oauth-client:${runtime.oauthClientId}`)
126
+ : runtime.authMode === 'access_token'
127
+ ? fingerprintSecret(runtime.accessToken)
128
+ : fingerprintUserApiKey(runtime.userApiKey);
105
129
  const sessionFilePath = resolveSessionFilePath(runtime);
106
130
  return {
107
131
  projectBaseUrl,
108
132
  publishableKeyFingerprint,
109
- userApiKeyFingerprint,
133
+ authMethod: runtime.authMode,
134
+ authBindingFingerprint,
110
135
  sessionFilePath,
111
136
  memoKey: [
112
137
  projectBaseUrl,
113
138
  publishableKeyFingerprint,
114
- userApiKeyFingerprint,
139
+ runtime.authMode,
140
+ authBindingFingerprint,
115
141
  sessionFilePath ?? 'memory-only',
116
142
  ].join('|'),
117
143
  };
@@ -119,7 +145,8 @@ function buildRuntimeIdentity(runtime) {
119
145
  function recordMatchesRuntime(record, runtime) {
120
146
  return (record.supabase_url === runtime.projectBaseUrl &&
121
147
  record.publishable_key_fingerprint === runtime.publishableKeyFingerprint &&
122
- record.user_api_key_fingerprint === runtime.userApiKeyFingerprint);
148
+ record.auth_method === runtime.authMethod &&
149
+ record.auth_binding_fingerprint === runtime.authBindingFingerprint);
123
150
  }
124
151
  function isSessionFresh(record, now) {
125
152
  return (typeof record.expires_at === 'number' &&
@@ -136,14 +163,16 @@ function buildCachedSessionRecord(options) {
136
163
  });
137
164
  }
138
165
  return {
139
- schema_version: 1,
166
+ schema_version: 2,
167
+ auth_method: options.runtime.authMethod === 'oauth' ? 'oauth' : 'legacy_user_api_key',
140
168
  supabase_url: options.runtime.projectBaseUrl,
141
169
  publishable_key_fingerprint: options.runtime.publishableKeyFingerprint,
142
- user_api_key_fingerprint: options.runtime.userApiKeyFingerprint,
170
+ auth_binding_fingerprint: options.runtime.authBindingFingerprint,
143
171
  user_email: options.userEmail,
144
172
  access_token: accessToken,
145
173
  refresh_token: refreshToken,
146
174
  expires_at: computeExpiresAt(options.session, options.now),
175
+ granted_scopes: [...new Set(options.grantedScopes ?? [])].sort(),
147
176
  updated_at_utc: options.now.toISOString(),
148
177
  };
149
178
  }
@@ -155,11 +184,16 @@ function toResolvedSession(record, runtime, source) {
155
184
  userEmail: record.user_email,
156
185
  projectBaseUrl: runtime.projectBaseUrl,
157
186
  sessionFile: runtime.sessionFilePath,
187
+ authMethod: runtime.authMethod,
158
188
  source,
159
189
  };
160
190
  }
161
- function readCachedSessionRecord(sessionFilePath) {
191
+ function readCachedSessionRecord(sessionFilePath, platform = process.platform) {
162
192
  try {
193
+ const stat = statSync(sessionFilePath);
194
+ if (!stat.isFile() || (platform !== 'win32' && (stat.mode & 0o077) !== 0)) {
195
+ return null;
196
+ }
163
197
  const text = readFileSync(sessionFilePath, 'utf8').trim();
164
198
  if (!text) {
165
199
  return null;
@@ -170,15 +204,19 @@ function readCachedSessionRecord(sessionFilePath) {
170
204
  return null;
171
205
  }
172
206
  }
173
- function writeCachedSessionRecord(sessionFilePath, record) {
207
+ function writeCachedSessionRecord(sessionFilePath, record, platform = process.platform) {
174
208
  mkdirSync(path.dirname(sessionFilePath), {
175
209
  recursive: true,
176
210
  mode: 0o700,
177
211
  });
212
+ if (platform !== 'win32') {
213
+ chmodSync(path.dirname(sessionFilePath), 0o700);
214
+ }
178
215
  const tempPath = `${sessionFilePath}.${process.pid}.${Date.now()}.tmp`;
179
216
  try {
180
217
  writeFileSync(tempPath, `${JSON.stringify(record, null, 2)}\n`, {
181
218
  encoding: 'utf8',
219
+ flag: 'wx',
182
220
  mode: 0o600,
183
221
  });
184
222
  renameSync(tempPath, sessionFilePath);
@@ -247,6 +285,7 @@ async function signInWithUserApiKey(options) {
247
285
  runtime: options.runtimeIdentity,
248
286
  session: data.session,
249
287
  userEmail: trimToken(data.user?.email) || credentials.email,
288
+ grantedScopes: [],
250
289
  now: options.now,
251
290
  });
252
291
  }
@@ -256,6 +295,36 @@ async function refreshWithRefreshToken(options) {
256
295
  return null;
257
296
  }
258
297
  try {
298
+ if (options.runtime.authMode === 'oauth') {
299
+ const tokens = await refreshOAuthTokens({
300
+ projectBaseUrl: options.runtimeIdentity.projectBaseUrl,
301
+ clientId: options.runtime.oauthClientId,
302
+ refreshToken: normalizedRefreshToken,
303
+ fetchImpl: options.fetchImpl,
304
+ timeoutMs: options.timeoutMs,
305
+ });
306
+ const userInfo = await fetchOAuthUserInfo({
307
+ projectBaseUrl: options.runtimeIdentity.projectBaseUrl,
308
+ accessToken: tokens.accessToken,
309
+ fetchImpl: options.fetchImpl,
310
+ timeoutMs: options.timeoutMs,
311
+ });
312
+ return buildCachedSessionRecord({
313
+ runtime: options.runtimeIdentity,
314
+ session: {
315
+ access_token: tokens.accessToken,
316
+ refresh_token: tokens.refreshToken,
317
+ expires_at: undefined,
318
+ expires_in: tokens.expiresIn,
319
+ },
320
+ userEmail: userInfo.email,
321
+ grantedScopes: tokens.scope,
322
+ now: options.now,
323
+ });
324
+ }
325
+ if (options.runtime.authMode !== 'legacy_user_api_key') {
326
+ return null;
327
+ }
259
328
  const authClient = createSupabaseAuthClient(options.runtimeIdentity, options.runtime.publishableKey, options.fetchImpl, options.timeoutMs);
260
329
  const { data, error } = await authClient.auth.refreshSession({
261
330
  refresh_token: normalizedRefreshToken,
@@ -267,6 +336,7 @@ async function refreshWithRefreshToken(options) {
267
336
  runtime: options.runtimeIdentity,
268
337
  session: data.session,
269
338
  userEmail: trimToken(data.user?.email) || options.userEmail,
339
+ grantedScopes: [],
270
340
  now: options.now,
271
341
  });
272
342
  }
@@ -274,8 +344,45 @@ async function refreshWithRefreshToken(options) {
274
344
  return null;
275
345
  }
276
346
  }
347
+ async function resolveExplicitAccessToken(options) {
348
+ const cached = ACCESS_TOKEN_MEMORY_CACHE.get(options.runtimeIdentity.memoKey);
349
+ if (cached) {
350
+ return cached;
351
+ }
352
+ const accessToken = trimToken(options.runtime.accessToken);
353
+ const authClient = createSupabaseAuthClient(options.runtimeIdentity, options.runtime.publishableKey, options.fetchImpl, options.timeoutMs);
354
+ const { data, error } = await authClient.auth.getUser(accessToken);
355
+ const userEmail = trimToken(data.user?.email);
356
+ if (error || !data.user?.id || data.user.role !== 'authenticated' || !userEmail) {
357
+ throw new CliError('TIANGONG_LCA_ACCESS_TOKEN did not resolve to an authenticated user.', {
358
+ code: 'SUPABASE_ACCESS_TOKEN_INVALID',
359
+ exitCode: 1,
360
+ details: error?.message ?? 'Authenticated user identity missing from access token.',
361
+ });
362
+ }
363
+ const resolved = {
364
+ accessToken,
365
+ refreshToken: '',
366
+ expiresAt: null,
367
+ userEmail,
368
+ projectBaseUrl: options.runtimeIdentity.projectBaseUrl,
369
+ sessionFile: null,
370
+ authMethod: 'access_token',
371
+ source: 'access_token',
372
+ };
373
+ ACCESS_TOKEN_MEMORY_CACHE.set(options.runtimeIdentity.memoKey, resolved);
374
+ return resolved;
375
+ }
277
376
  async function resolveAndPersistSession(options) {
278
377
  const { runtime, runtimeIdentity } = options;
378
+ if (runtime.authMode === 'access_token') {
379
+ return resolveExplicitAccessToken({
380
+ runtime,
381
+ runtimeIdentity,
382
+ fetchImpl: options.fetchImpl,
383
+ timeoutMs: options.timeoutMs,
384
+ });
385
+ }
279
386
  const memoized = getMemoizedRecord(runtimeIdentity);
280
387
  if (!options.forceRefresh &&
281
388
  !runtime.forceReauth &&
@@ -324,6 +431,13 @@ async function resolveAndPersistSession(options) {
324
431
  }
325
432
  }
326
433
  }
434
+ if (runtime.authMode === 'oauth') {
435
+ dropMemoizedRecord(runtimeIdentity);
436
+ throw new CliError('No usable OAuth session is available. Run `tiangong-lca auth login` in a trusted terminal.', {
437
+ code: 'SUPABASE_OAUTH_LOGIN_REQUIRED',
438
+ exitCode: 1,
439
+ });
440
+ }
327
441
  const signedIn = await signInWithUserApiKey({
328
442
  runtime,
329
443
  runtimeIdentity,
@@ -335,13 +449,21 @@ async function resolveAndPersistSession(options) {
335
449
  writeCachedSessionRecord(runtimeIdentity.sessionFilePath, signedIn);
336
450
  }
337
451
  memoizeRecord(runtimeIdentity, signedIn);
338
- return toResolvedSession(signedIn, runtimeIdentity, 'signin');
452
+ return toResolvedSession(signedIn, runtimeIdentity, 'legacy_signin');
339
453
  }
340
454
  export async function resolveSupabaseUserSession(options) {
341
455
  const runtimeIdentity = buildRuntimeIdentity(options.runtime);
342
456
  const now = options.now ?? new Date();
343
457
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
344
458
  const forceRefresh = Boolean(options.forceRefresh);
459
+ if (options.runtime.authMode === 'access_token') {
460
+ return resolveExplicitAccessToken({
461
+ runtime: options.runtime,
462
+ runtimeIdentity,
463
+ fetchImpl: options.fetchImpl,
464
+ timeoutMs,
465
+ });
466
+ }
345
467
  if (!forceRefresh && !options.runtime.forceReauth) {
346
468
  const memoized = getMemoizedRecord(runtimeIdentity);
347
469
  if (memoized &&
@@ -380,8 +502,176 @@ export async function resolveSupabaseUserSession(options) {
380
502
  });
381
503
  });
382
504
  }
383
- export function createSupabaseDataRuntime(options) {
505
+ export function inspectSupabaseAuthStatus(options) {
506
+ const runtimeIdentity = buildRuntimeIdentity(options.runtime);
507
+ const now = options.now ?? new Date();
508
+ if (options.runtime.authMode === 'access_token') {
509
+ return {
510
+ schemaVersion: 'tiangong.cli-auth-status.v1',
511
+ status: 'ready',
512
+ authMethod: 'access_token',
513
+ sessionState: 'memory-only',
514
+ sessionCache: 'memory-only',
515
+ expiresAt: null,
516
+ grantedScopes: [],
517
+ onlineVerified: false,
518
+ };
519
+ }
520
+ if (options.runtime.authMode === 'legacy_user_api_key') {
521
+ return {
522
+ schemaVersion: 'tiangong.cli-auth-status.v1',
523
+ status: 'ready',
524
+ authMethod: 'legacy_user_api_key',
525
+ sessionState: 'transition-only',
526
+ sessionCache: runtimeIdentity.sessionFilePath ? 'private-file' : 'disabled',
527
+ expiresAt: null,
528
+ grantedScopes: [],
529
+ onlineVerified: false,
530
+ };
531
+ }
532
+ const memoized = getMemoizedRecord(runtimeIdentity);
533
+ const cached = memoized && recordMatchesRuntime(memoized, runtimeIdentity)
534
+ ? memoized
535
+ : runtimeIdentity.sessionFilePath
536
+ ? readCachedSessionRecord(runtimeIdentity.sessionFilePath)
537
+ : null;
538
+ const matching = cached && recordMatchesRuntime(cached, runtimeIdentity) ? cached : null;
539
+ if (!matching || !trimToken(matching.refresh_token)) {
540
+ return {
541
+ schemaVersion: 'tiangong.cli-auth-status.v1',
542
+ status: 'login-required',
543
+ authMethod: 'oauth',
544
+ sessionState: 'missing',
545
+ sessionCache: runtimeIdentity.sessionFilePath ? 'private-file' : 'disabled',
546
+ expiresAt: null,
547
+ grantedScopes: [],
548
+ onlineVerified: false,
549
+ };
550
+ }
384
551
  return {
552
+ schemaVersion: 'tiangong.cli-auth-status.v1',
553
+ status: 'ready',
554
+ authMethod: 'oauth',
555
+ sessionState: isSessionFresh(matching, now) ? 'fresh' : 'refresh-required',
556
+ sessionCache: 'private-file',
557
+ expiresAt: matching.expires_at,
558
+ grantedScopes: matching.granted_scopes,
559
+ onlineVerified: false,
560
+ };
561
+ }
562
+ export async function loginWithSupabaseOAuth(options) {
563
+ if (options.runtime.authMode !== 'oauth' ||
564
+ !options.runtime.oauthClientId ||
565
+ !options.runtime.oauthRedirectUri) {
566
+ throw new CliError('OAuth login requires TIANGONG_LCA_AUTH_MODE=oauth and a client ID.', {
567
+ code: 'SUPABASE_OAUTH_RUNTIME_REQUIRED',
568
+ exitCode: 2,
569
+ });
570
+ }
571
+ const runtimeIdentity = buildRuntimeIdentity(options.runtime);
572
+ if (!runtimeIdentity.sessionFilePath) {
573
+ throw new CliError('OAuth login requires the private session cache to be enabled.', {
574
+ code: 'SUPABASE_OAUTH_SESSION_CACHE_REQUIRED',
575
+ exitCode: 2,
576
+ });
577
+ }
578
+ const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_TIMEOUT_MS;
579
+ const loginTimeoutMs = options.loginTimeoutMs ?? 180_000;
580
+ const now = options.now ?? new Date();
581
+ const createPkceValuesImpl = options.createPkceValuesImpl ?? createOAuthPkceValues;
582
+ const receiveCallbackImpl = options.receiveCallbackImpl ?? receiveOAuthLoopbackCallback;
583
+ const browserOptions = options.browserOptions ?? SYSTEM_BROWSER_OPTIONS;
584
+ const openBrowserImpl = options.openBrowserImpl ??
585
+ ((authorizationUrl) => openSystemBrowser(authorizationUrl, browserOptions));
586
+ const exchangeCodeImpl = options.exchangeCodeImpl ?? exchangeOAuthAuthorizationCode;
587
+ const fetchUserInfoImpl = options.fetchUserInfoImpl ?? fetchOAuthUserInfo;
588
+ return withSessionOperationLock(runtimeIdentity.memoKey, () => withStateFileLock(runtimeIdentity.sessionFilePath, { reason: 'supabase_oauth_pkce_login' }, async () => {
589
+ const pkce = createPkceValuesImpl();
590
+ const authorizationUrl = buildOAuthAuthorizationUrl({
591
+ projectBaseUrl: runtimeIdentity.projectBaseUrl,
592
+ clientId: options.runtime.oauthClientId,
593
+ redirectUri: options.runtime.oauthRedirectUri,
594
+ codeChallenge: pkce.codeChallenge,
595
+ state: pkce.state,
596
+ });
597
+ const authorizationCode = await receiveCallbackImpl({
598
+ redirectUri: options.runtime.oauthRedirectUri,
599
+ expectedState: pkce.state,
600
+ timeoutMs: loginTimeoutMs,
601
+ onListening: () => openBrowserImpl(authorizationUrl),
602
+ });
603
+ const tokens = await exchangeCodeImpl({
604
+ projectBaseUrl: runtimeIdentity.projectBaseUrl,
605
+ clientId: options.runtime.oauthClientId,
606
+ redirectUri: options.runtime.oauthRedirectUri,
607
+ authorizationCode,
608
+ codeVerifier: pkce.codeVerifier,
609
+ fetchImpl: options.fetchImpl,
610
+ timeoutMs: requestTimeoutMs,
611
+ });
612
+ const userInfo = await fetchUserInfoImpl({
613
+ projectBaseUrl: runtimeIdentity.projectBaseUrl,
614
+ accessToken: tokens.accessToken,
615
+ fetchImpl: options.fetchImpl,
616
+ timeoutMs: requestTimeoutMs,
617
+ });
618
+ const grantedScopes = tokens.scope.length > 0 ? tokens.scope : [...DEFAULT_OAUTH_SCOPES];
619
+ const record = buildCachedSessionRecord({
620
+ runtime: runtimeIdentity,
621
+ session: {
622
+ access_token: tokens.accessToken,
623
+ refresh_token: tokens.refreshToken,
624
+ expires_at: undefined,
625
+ expires_in: tokens.expiresIn,
626
+ },
627
+ userEmail: userInfo.email,
628
+ grantedScopes,
629
+ now,
630
+ });
631
+ writeCachedSessionRecord(runtimeIdentity.sessionFilePath, record);
632
+ memoizeRecord(runtimeIdentity, record);
633
+ return {
634
+ schemaVersion: 'tiangong.cli-oauth-login.v1',
635
+ status: 'authenticated',
636
+ authMethod: 'oauth',
637
+ expiresAt: record.expires_at,
638
+ grantedScopes: record.granted_scopes,
639
+ sessionCache: 'private-file',
640
+ };
641
+ }));
642
+ }
643
+ export async function logoutSupabaseUserSession(options) {
644
+ const runtimeIdentity = buildRuntimeIdentity(options.runtime);
645
+ return withSessionOperationLock(runtimeIdentity.memoKey, async () => {
646
+ let removed = false;
647
+ const removeCurrent = () => {
648
+ const cached = runtimeIdentity.sessionFilePath
649
+ ? readCachedSessionRecord(runtimeIdentity.sessionFilePath)
650
+ : null;
651
+ if (runtimeIdentity.sessionFilePath &&
652
+ cached &&
653
+ recordMatchesRuntime(cached, runtimeIdentity)) {
654
+ rmSync(runtimeIdentity.sessionFilePath, { force: true });
655
+ removed = true;
656
+ }
657
+ dropMemoizedRecord(runtimeIdentity);
658
+ ACCESS_TOKEN_MEMORY_CACHE.delete(runtimeIdentity.memoKey);
659
+ };
660
+ if (runtimeIdentity.sessionFilePath) {
661
+ await withStateFileLock(runtimeIdentity.sessionFilePath, { reason: 'supabase_oauth_local_logout' }, removeCurrent);
662
+ }
663
+ else {
664
+ removeCurrent();
665
+ }
666
+ return {
667
+ schemaVersion: 'tiangong.cli-oauth-logout.v1',
668
+ status: 'logged-out',
669
+ removed,
670
+ };
671
+ });
672
+ }
673
+ export function createSupabaseDataRuntime(options) {
674
+ const runtime = {
385
675
  apiBaseUrl: options.runtime.apiBaseUrl,
386
676
  publishableKey: options.runtime.publishableKey,
387
677
  getAccessToken: async () => (await resolveSupabaseUserSession({
@@ -390,17 +680,21 @@ export function createSupabaseDataRuntime(options) {
390
680
  timeoutMs: options.timeoutMs,
391
681
  now: options.now,
392
682
  })).accessToken,
393
- refreshAccessToken: async () => (await resolveSupabaseUserSession({
683
+ };
684
+ if (options.runtime.authMode !== 'access_token') {
685
+ runtime.refreshAccessToken = async () => (await resolveSupabaseUserSession({
394
686
  runtime: options.runtime,
395
687
  fetchImpl: options.fetchImpl,
396
688
  timeoutMs: options.timeoutMs,
397
689
  now: options.now,
398
690
  forceRefresh: true,
399
- })).accessToken,
400
- };
691
+ })).accessToken;
692
+ }
693
+ return runtime;
401
694
  }
402
695
  export const __testInternals = {
403
696
  SESSION_MEMORY_CACHE,
697
+ ACCESS_TOKEN_MEMORY_CACHE,
404
698
  SESSION_OPERATION_CHAINS,
405
699
  buildCachedSessionRecord,
406
700
  buildRuntimeIdentity,