@tiangong-lca/cli 0.1.3 → 0.1.6
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 +47 -11
- package/dist/src/cli.d.ts +4 -0
- package/dist/src/cli.js +264 -7
- package/dist/src/cli.js.map +1 -1
- package/dist/src/lib/auth-identity-receipt.d.ts +2 -2
- package/dist/src/lib/auth-identity-receipt.js +20 -12
- package/dist/src/lib/auth-identity-receipt.js.map +1 -1
- package/dist/src/lib/batch/types.js.map +1 -1
- package/dist/src/lib/dataset-bilingual.js.map +1 -1
- package/dist/src/lib/dataset-curation-queue.js.map +1 -1
- package/dist/src/lib/dataset-local.js.map +1 -1
- package/dist/src/lib/dataset-maintenance-apply.js.map +1 -1
- package/dist/src/lib/dataset-maintenance-clear-account.js.map +1 -1
- package/dist/src/lib/dataset-maintenance-contract.js.map +1 -1
- package/dist/src/lib/dataset-maintenance-verify.js.map +1 -1
- package/dist/src/lib/dataset-save-draft-run.js.map +1 -1
- package/dist/src/lib/env.d.ts +8 -0
- package/dist/src/lib/env.js +36 -3
- package/dist/src/lib/env.js.map +1 -1
- package/dist/src/lib/flow-fetch-rows.js.map +1 -1
- package/dist/src/lib/flow-read.js.map +1 -1
- package/dist/src/lib/identity-preflight.js.map +1 -1
- package/dist/src/lib/lca-release.d.ts +1 -1
- package/dist/src/lib/lca-release.js +3 -4
- package/dist/src/lib/lca-release.js.map +1 -1
- package/dist/src/lib/lifecyclemodel-resulting-process.js +6 -2
- package/dist/src/lib/lifecyclemodel-resulting-process.js.map +1 -1
- package/dist/src/lib/oauth-loopback.d.ts +52 -0
- package/dist/src/lib/oauth-loopback.js +229 -0
- package/dist/src/lib/oauth-loopback.js.map +1 -0
- package/dist/src/lib/oauth-pkce.d.ts +63 -0
- package/dist/src/lib/oauth-pkce.js +268 -0
- package/dist/src/lib/oauth-pkce.js.map +1 -0
- package/dist/src/lib/process-dedup-review.js.map +1 -1
- package/dist/src/lib/process-flow-build-plan.js.map +1 -1
- package/dist/src/lib/process-refresh-references.js +2 -2
- package/dist/src/lib/process-refresh-references.js.map +1 -1
- package/dist/src/lib/process-scope-statistics.js +8 -3
- package/dist/src/lib/process-scope-statistics.js.map +1 -1
- package/dist/src/lib/process-verify-rows.js.map +1 -1
- package/dist/src/lib/publish.js.map +1 -1
- package/dist/src/lib/state-lock.js +10 -4
- package/dist/src/lib/state-lock.js.map +1 -1
- package/dist/src/lib/supabase-client.d.ts +13 -1
- package/dist/src/lib/supabase-client.js +64 -3
- package/dist/src/lib/supabase-client.js.map +1 -1
- package/dist/src/lib/supabase-json-ordered-write.js +7 -3
- package/dist/src/lib/supabase-json-ordered-write.js.map +1 -1
- package/dist/src/lib/supabase-rest.js.map +1 -1
- package/dist/src/lib/supabase-session.d.ts +63 -8
- package/dist/src/lib/supabase-session.js +315 -21
- package/dist/src/lib/supabase-session.js.map +1 -1
- package/dist/src/lib/tidas-sdk-package-validator.js.map +1 -1
- package/package.json +8 -7
|
@@ -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
|
|
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
|
-
!
|
|
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:
|
|
79
|
+
schema_version: 2,
|
|
80
|
+
auth_method: authMethod,
|
|
62
81
|
supabase_url: supabaseUrl,
|
|
63
82
|
publishable_key_fingerprint: publishableKeyFingerprint,
|
|
64
|
-
|
|
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
|
|
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
|
-
|
|
133
|
+
authMethod: runtime.authMode,
|
|
134
|
+
authBindingFingerprint,
|
|
110
135
|
sessionFilePath,
|
|
111
136
|
memoKey: [
|
|
112
137
|
projectBaseUrl,
|
|
113
138
|
publishableKeyFingerprint,
|
|
114
|
-
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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, '
|
|
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
|
|
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
|
-
|
|
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,
|