@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
@@ -0,0 +1,229 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { timingSafeEqual } from 'node:crypto';
3
+ import { createServer } from 'node:http';
4
+ import { CliError } from './errors.js';
5
+ export const DEFAULT_OAUTH_REDIRECT_URI = 'http://127.0.0.1:49191/oauth/callback';
6
+ export const OAUTH_LOGIN_TIMEOUT_MAX_MS = 5 * 60 * 1000;
7
+ const CALLBACK_PATH = '/oauth/callback';
8
+ const STATE_PATTERN = /^[A-Za-z0-9_-]{32,128}$/u;
9
+ const OAUTH_ERROR_PATTERN = /^[a-z][a-z0-9_]{0,63}$/u;
10
+ export const SYSTEM_BROWSER_OPTIONS = {
11
+ platform: process.platform,
12
+ spawnImpl: spawn,
13
+ };
14
+ function trimString(value) {
15
+ return typeof value === 'string' ? value.trim() : '';
16
+ }
17
+ export function requireOAuthLoopbackRedirectUri(value) {
18
+ let parsed;
19
+ try {
20
+ parsed = new URL(value);
21
+ }
22
+ catch {
23
+ throw new CliError('OAuth redirect URI is invalid.', {
24
+ code: 'OAUTH_REDIRECT_URI_INVALID',
25
+ exitCode: 2,
26
+ });
27
+ }
28
+ const port = Number(parsed.port);
29
+ if (parsed.protocol !== 'http:' ||
30
+ parsed.hostname !== '127.0.0.1' ||
31
+ parsed.username ||
32
+ parsed.password ||
33
+ parsed.pathname !== CALLBACK_PATH ||
34
+ parsed.search ||
35
+ parsed.hash ||
36
+ !Number.isSafeInteger(port) ||
37
+ port < 1024 ||
38
+ port > 65535) {
39
+ throw new CliError('OAuth redirect URI must be an exact http://127.0.0.1:<port>/oauth/callback URL.', {
40
+ code: 'OAUTH_REDIRECT_URI_INVALID',
41
+ exitCode: 2,
42
+ });
43
+ }
44
+ return {
45
+ redirectUri: parsed.toString(),
46
+ hostname: '127.0.0.1',
47
+ port,
48
+ pathname: CALLBACK_PATH,
49
+ };
50
+ }
51
+ function browserCommand(platform, authorizationUrl) {
52
+ switch (platform) {
53
+ case 'darwin':
54
+ return { command: 'open', args: [authorizationUrl] };
55
+ case 'win32':
56
+ return {
57
+ command: 'rundll32.exe',
58
+ args: ['url.dll,FileProtocolHandler', authorizationUrl],
59
+ };
60
+ case 'linux':
61
+ return { command: 'xdg-open', args: [authorizationUrl] };
62
+ default:
63
+ throw new CliError(`Cannot open a browser automatically on ${platform}.`, {
64
+ code: 'OAUTH_BROWSER_UNSUPPORTED',
65
+ exitCode: 1,
66
+ });
67
+ }
68
+ }
69
+ export async function openSystemBrowser(authorizationUrl, options) {
70
+ const target = browserCommand(options.platform, authorizationUrl);
71
+ const spawnImpl = options.spawnImpl;
72
+ await new Promise((resolve, reject) => {
73
+ let child;
74
+ try {
75
+ child = spawnImpl(target.command, target.args, {
76
+ detached: true,
77
+ shell: false,
78
+ stdio: 'ignore',
79
+ });
80
+ }
81
+ catch (error) {
82
+ reject(new CliError('Failed to start the system browser.', {
83
+ code: 'OAUTH_BROWSER_OPEN_FAILED',
84
+ exitCode: 1,
85
+ details: error instanceof Error ? error.message : String(error),
86
+ }));
87
+ return;
88
+ }
89
+ let settled = false;
90
+ child.once('error', (error) => {
91
+ if (settled)
92
+ return;
93
+ settled = true;
94
+ reject(new CliError('Failed to start the system browser.', {
95
+ code: 'OAUTH_BROWSER_OPEN_FAILED',
96
+ exitCode: 1,
97
+ details: error.message,
98
+ }));
99
+ });
100
+ child.once('spawn', () => {
101
+ if (settled)
102
+ return;
103
+ settled = true;
104
+ child.unref();
105
+ resolve();
106
+ });
107
+ });
108
+ }
109
+ function safeStateEqual(actual, expected) {
110
+ const actualBytes = Buffer.from(actual, 'utf8');
111
+ const expectedBytes = Buffer.from(expected, 'utf8');
112
+ return (actualBytes.byteLength === expectedBytes.byteLength &&
113
+ timingSafeEqual(actualBytes, expectedBytes));
114
+ }
115
+ function writeResponse(response, status, body) {
116
+ response.writeHead(status, {
117
+ 'Cache-Control': 'no-store',
118
+ Connection: 'close',
119
+ 'Content-Type': 'text/html; charset=utf-8',
120
+ 'Content-Security-Policy': "default-src 'none'; style-src 'unsafe-inline'",
121
+ 'Referrer-Policy': 'no-referrer',
122
+ 'X-Content-Type-Options': 'nosniff',
123
+ });
124
+ response.end(body);
125
+ }
126
+ function callbackError(message, code, details) {
127
+ return new CliError(message, { code, exitCode: 1, details });
128
+ }
129
+ export async function receiveOAuthLoopbackCallback(options) {
130
+ const binding = requireOAuthLoopbackRedirectUri(options.redirectUri);
131
+ if (!STATE_PATTERN.test(options.expectedState)) {
132
+ throw callbackError('OAuth state is invalid.', 'OAUTH_STATE_INVALID');
133
+ }
134
+ if (!Number.isSafeInteger(options.timeoutMs) ||
135
+ options.timeoutMs <= 0 ||
136
+ options.timeoutMs > OAUTH_LOGIN_TIMEOUT_MAX_MS) {
137
+ throw new CliError(`OAuth login timeout must be an integer between 1 and ${OAUTH_LOGIN_TIMEOUT_MAX_MS}.`, { code: 'OAUTH_LOGIN_TIMEOUT_INVALID', exitCode: 2 });
138
+ }
139
+ return await new Promise((resolve, reject) => {
140
+ let settled = false;
141
+ let timer;
142
+ const server = createServer((request, response) => {
143
+ const callbackUrl = new URL(request.url, binding.redirectUri);
144
+ if (request.method !== 'GET' || callbackUrl.pathname !== binding.pathname) {
145
+ writeResponse(response, 404, '<h1>Not found</h1>');
146
+ return;
147
+ }
148
+ const stateValues = callbackUrl.searchParams.getAll('state');
149
+ const codeValues = callbackUrl.searchParams.getAll('code');
150
+ const errorValue = trimString(callbackUrl.searchParams.get('error'));
151
+ if (stateValues.length !== 1 ||
152
+ !STATE_PATTERN.test(stateValues[0]) ||
153
+ !safeStateEqual(stateValues[0], options.expectedState)) {
154
+ writeResponse(response, 400, '<h1>OAuth state mismatch</h1>');
155
+ finish(callbackError('OAuth callback state did not match.', 'OAUTH_STATE_MISMATCH'));
156
+ return;
157
+ }
158
+ if (errorValue) {
159
+ const safeError = OAUTH_ERROR_PATTERN.test(errorValue)
160
+ ? errorValue
161
+ : 'authorization_failed';
162
+ writeResponse(response, 400, '<h1>Authorization was not completed</h1>');
163
+ finish(callbackError('OAuth authorization was not completed.', 'OAUTH_AUTHORIZATION_DENIED', {
164
+ error: safeError,
165
+ }));
166
+ return;
167
+ }
168
+ const authorizationCode = codeValues.length === 1 ? trimString(codeValues[0]) : '';
169
+ if (!authorizationCode || authorizationCode.length > 4096) {
170
+ writeResponse(response, 400, '<h1>Authorization code missing</h1>');
171
+ finish(callbackError('OAuth callback did not contain one authorization code.', 'OAUTH_AUTHORIZATION_CODE_INVALID'));
172
+ return;
173
+ }
174
+ writeResponse(response, 200, '<!doctype html><meta charset="utf-8"><title>TianGong LCA CLI</title><h1>Authorization complete</h1><p>You can close this window and return to the CLI.</p>');
175
+ finish(null, authorizationCode);
176
+ });
177
+ const complete = (error, authorizationCode) => {
178
+ if (error)
179
+ reject(error);
180
+ else
181
+ resolve(authorizationCode);
182
+ };
183
+ const finish = (error, authorizationCode) => {
184
+ if (settled)
185
+ return;
186
+ settled = true;
187
+ if (timer)
188
+ clearTimeout(timer);
189
+ if (server.listening) {
190
+ server.close(() => complete(error, authorizationCode));
191
+ }
192
+ else {
193
+ complete(error, authorizationCode);
194
+ }
195
+ };
196
+ server.once('error', (error) => {
197
+ finish(new CliError('Could not bind the OAuth loopback callback.', {
198
+ code: 'OAUTH_LOOPBACK_BIND_FAILED',
199
+ exitCode: 1,
200
+ details: error.message,
201
+ }));
202
+ });
203
+ server.listen(binding.port, binding.hostname, async () => {
204
+ timer = setTimeout(() => {
205
+ finish(callbackError('OAuth login timed out.', 'OAUTH_LOGIN_TIMEOUT'));
206
+ }, options.timeoutMs);
207
+ try {
208
+ await options.onListening();
209
+ }
210
+ catch (error) {
211
+ finish(error instanceof CliError
212
+ ? error
213
+ : new CliError('Could not start OAuth authorization.', {
214
+ code: 'OAUTH_AUTHORIZATION_START_FAILED',
215
+ exitCode: 1,
216
+ details: error instanceof Error ? error.message : String(error),
217
+ }));
218
+ }
219
+ });
220
+ });
221
+ }
222
+ export const __testInternals = {
223
+ browserCommand,
224
+ callbackError,
225
+ safeStateEqual,
226
+ trimString,
227
+ writeResponse,
228
+ };
229
+ //# sourceMappingURL=oauth-loopback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-loopback.js","sourceRoot":"","sources":["../../../src/lib/oauth-loopback.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport { timingSafeEqual } from 'node:crypto';\nimport { createServer, type ServerResponse } from 'node:http';\nimport { CliError } from './errors.js';\n\nexport const DEFAULT_OAUTH_REDIRECT_URI = 'http://127.0.0.1:49191/oauth/callback';\nexport const OAUTH_LOGIN_TIMEOUT_MAX_MS = 5 * 60 * 1000;\n\nconst CALLBACK_PATH = '/oauth/callback';\nconst STATE_PATTERN = /^[A-Za-z0-9_-]{32,128}$/u;\nconst OAUTH_ERROR_PATTERN = /^[a-z][a-z0-9_]{0,63}$/u;\n\ntype SpawnedBrowser = {\n once(event: 'error', listener: (error: Error) => void): SpawnedBrowser;\n once(event: 'spawn', listener: () => void): SpawnedBrowser;\n unref(): void;\n};\n\nexport type BrowserSpawn = (\n command: string,\n args: string[],\n options: { detached: true; shell: false; stdio: 'ignore' },\n) => SpawnedBrowser;\n\nexport type OAuthLoopbackBinding = {\n redirectUri: string;\n hostname: '127.0.0.1';\n port: number;\n pathname: typeof CALLBACK_PATH;\n};\n\nexport const SYSTEM_BROWSER_OPTIONS = {\n platform: process.platform,\n spawnImpl: spawn as unknown as BrowserSpawn,\n} satisfies { platform: NodeJS.Platform; spawnImpl: BrowserSpawn };\n\nfunction trimString(value: unknown): string {\n return typeof value === 'string' ? value.trim() : '';\n}\n\nexport function requireOAuthLoopbackRedirectUri(value: string): OAuthLoopbackBinding {\n let parsed: URL;\n try {\n parsed = new URL(value);\n } catch {\n throw new CliError('OAuth redirect URI is invalid.', {\n code: 'OAUTH_REDIRECT_URI_INVALID',\n exitCode: 2,\n });\n }\n\n const port = Number(parsed.port);\n if (\n parsed.protocol !== 'http:' ||\n parsed.hostname !== '127.0.0.1' ||\n parsed.username ||\n parsed.password ||\n parsed.pathname !== CALLBACK_PATH ||\n parsed.search ||\n parsed.hash ||\n !Number.isSafeInteger(port) ||\n port < 1024 ||\n port > 65535\n ) {\n throw new CliError(\n 'OAuth redirect URI must be an exact http://127.0.0.1:<port>/oauth/callback URL.',\n {\n code: 'OAUTH_REDIRECT_URI_INVALID',\n exitCode: 2,\n },\n );\n }\n\n return {\n redirectUri: parsed.toString(),\n hostname: '127.0.0.1',\n port,\n pathname: CALLBACK_PATH,\n };\n}\n\nfunction browserCommand(platform: NodeJS.Platform, authorizationUrl: string) {\n switch (platform) {\n case 'darwin':\n return { command: 'open', args: [authorizationUrl] };\n case 'win32':\n return {\n command: 'rundll32.exe',\n args: ['url.dll,FileProtocolHandler', authorizationUrl],\n };\n case 'linux':\n return { command: 'xdg-open', args: [authorizationUrl] };\n default:\n throw new CliError(`Cannot open a browser automatically on ${platform}.`, {\n code: 'OAUTH_BROWSER_UNSUPPORTED',\n exitCode: 1,\n });\n }\n}\n\nexport async function openSystemBrowser(\n authorizationUrl: string,\n options: {\n platform: NodeJS.Platform;\n spawnImpl: BrowserSpawn;\n },\n): Promise<void> {\n const target = browserCommand(options.platform, authorizationUrl);\n const spawnImpl = options.spawnImpl;\n\n await new Promise<void>((resolve, reject) => {\n let child: SpawnedBrowser;\n try {\n child = spawnImpl(target.command, target.args, {\n detached: true,\n shell: false,\n stdio: 'ignore',\n });\n } catch (error) {\n reject(\n new CliError('Failed to start the system browser.', {\n code: 'OAUTH_BROWSER_OPEN_FAILED',\n exitCode: 1,\n details: error instanceof Error ? error.message : String(error),\n }),\n );\n return;\n }\n\n let settled = false;\n child.once('error', (error) => {\n if (settled) return;\n settled = true;\n reject(\n new CliError('Failed to start the system browser.', {\n code: 'OAUTH_BROWSER_OPEN_FAILED',\n exitCode: 1,\n details: error.message,\n }),\n );\n });\n child.once('spawn', () => {\n if (settled) return;\n settled = true;\n child.unref();\n resolve();\n });\n });\n}\n\nfunction safeStateEqual(actual: string, expected: string): boolean {\n const actualBytes = Buffer.from(actual, 'utf8');\n const expectedBytes = Buffer.from(expected, 'utf8');\n return (\n actualBytes.byteLength === expectedBytes.byteLength &&\n timingSafeEqual(actualBytes, expectedBytes)\n );\n}\n\nfunction writeResponse(response: ServerResponse, status: number, body: string): void {\n response.writeHead(status, {\n 'Cache-Control': 'no-store',\n Connection: 'close',\n 'Content-Type': 'text/html; charset=utf-8',\n 'Content-Security-Policy': \"default-src 'none'; style-src 'unsafe-inline'\",\n 'Referrer-Policy': 'no-referrer',\n 'X-Content-Type-Options': 'nosniff',\n });\n response.end(body);\n}\n\nfunction callbackError(message: string, code: string, details?: unknown): CliError {\n return new CliError(message, { code, exitCode: 1, details });\n}\n\nexport async function receiveOAuthLoopbackCallback(options: {\n redirectUri: string;\n expectedState: string;\n timeoutMs: number;\n onListening: () => void | Promise<void>;\n}): Promise<string> {\n const binding = requireOAuthLoopbackRedirectUri(options.redirectUri);\n if (!STATE_PATTERN.test(options.expectedState)) {\n throw callbackError('OAuth state is invalid.', 'OAUTH_STATE_INVALID');\n }\n if (\n !Number.isSafeInteger(options.timeoutMs) ||\n options.timeoutMs <= 0 ||\n options.timeoutMs > OAUTH_LOGIN_TIMEOUT_MAX_MS\n ) {\n throw new CliError(\n `OAuth login timeout must be an integer between 1 and ${OAUTH_LOGIN_TIMEOUT_MAX_MS}.`,\n { code: 'OAUTH_LOGIN_TIMEOUT_INVALID', exitCode: 2 },\n );\n }\n\n return await new Promise<string>((resolve, reject) => {\n let settled = false;\n let timer: NodeJS.Timeout | undefined;\n const server = createServer((request, response) => {\n const callbackUrl = new URL(request.url as string, binding.redirectUri);\n\n if (request.method !== 'GET' || callbackUrl.pathname !== binding.pathname) {\n writeResponse(response, 404, '<h1>Not found</h1>');\n return;\n }\n\n const stateValues = callbackUrl.searchParams.getAll('state');\n const codeValues = callbackUrl.searchParams.getAll('code');\n const errorValue = trimString(callbackUrl.searchParams.get('error'));\n if (\n stateValues.length !== 1 ||\n !STATE_PATTERN.test(stateValues[0] as string) ||\n !safeStateEqual(stateValues[0] as string, options.expectedState)\n ) {\n writeResponse(response, 400, '<h1>OAuth state mismatch</h1>');\n finish(callbackError('OAuth callback state did not match.', 'OAUTH_STATE_MISMATCH'));\n return;\n }\n\n if (errorValue) {\n const safeError = OAUTH_ERROR_PATTERN.test(errorValue)\n ? errorValue\n : 'authorization_failed';\n writeResponse(response, 400, '<h1>Authorization was not completed</h1>');\n finish(\n callbackError('OAuth authorization was not completed.', 'OAUTH_AUTHORIZATION_DENIED', {\n error: safeError,\n }),\n );\n return;\n }\n\n const authorizationCode = codeValues.length === 1 ? trimString(codeValues[0]) : '';\n if (!authorizationCode || authorizationCode.length > 4096) {\n writeResponse(response, 400, '<h1>Authorization code missing</h1>');\n finish(\n callbackError(\n 'OAuth callback did not contain one authorization code.',\n 'OAUTH_AUTHORIZATION_CODE_INVALID',\n ),\n );\n return;\n }\n\n writeResponse(\n response,\n 200,\n '<!doctype html><meta charset=\"utf-8\"><title>TianGong LCA CLI</title><h1>Authorization complete</h1><p>You can close this window and return to the CLI.</p>',\n );\n finish(null, authorizationCode);\n });\n\n const complete = (error: CliError | null, authorizationCode?: string) => {\n if (error) reject(error);\n else resolve(authorizationCode as string);\n };\n\n const finish = (error: CliError | null, authorizationCode?: string) => {\n if (settled) return;\n settled = true;\n if (timer) clearTimeout(timer);\n if (server.listening) {\n server.close(() => complete(error, authorizationCode));\n } else {\n complete(error, authorizationCode);\n }\n };\n\n server.once('error', (error) => {\n finish(\n new CliError('Could not bind the OAuth loopback callback.', {\n code: 'OAUTH_LOOPBACK_BIND_FAILED',\n exitCode: 1,\n details: error.message,\n }),\n );\n });\n\n server.listen(binding.port, binding.hostname, async () => {\n timer = setTimeout(() => {\n finish(callbackError('OAuth login timed out.', 'OAUTH_LOGIN_TIMEOUT'));\n }, options.timeoutMs);\n try {\n await options.onListening();\n } catch (error) {\n finish(\n error instanceof CliError\n ? error\n : new CliError('Could not start OAuth authorization.', {\n code: 'OAUTH_AUTHORIZATION_START_FAILED',\n exitCode: 1,\n details: error instanceof Error ? error.message : String(error),\n }),\n );\n }\n });\n });\n}\n\nexport const __testInternals = {\n browserCommand,\n callbackError,\n safeStateEqual,\n trimString,\n writeResponse,\n};\n"]}
@@ -0,0 +1,63 @@
1
+ import type { FetchLike, ResponseLike } from './http.js';
2
+ export declare const OAUTH_TOKEN_RESPONSE_MAX_BYTES: number;
3
+ export declare const DEFAULT_OAUTH_SCOPES: readonly ['openid', 'email', 'profile'];
4
+ export type OAuthPkceValues = {
5
+ codeVerifier: string;
6
+ codeChallenge: string;
7
+ state: string;
8
+ };
9
+ export type OAuthTokenSet = {
10
+ accessToken: string;
11
+ refreshToken: string;
12
+ expiresIn: number;
13
+ scope: string[];
14
+ };
15
+ export type OAuthUserInfo = {
16
+ userId: string;
17
+ email: string;
18
+ };
19
+ type RandomBytes = (size: number) => Uint8Array;
20
+ declare function base64Url(bytes: Uint8Array): string;
21
+ declare function trimString(value: unknown): string;
22
+ declare function requireProjectBaseUrl(value: string): string;
23
+ export declare function requireOAuthClientId(value: string): string;
24
+ export declare function createOAuthPkceValues(randomBytesImpl?: RandomBytes): OAuthPkceValues;
25
+ export declare function buildOAuthAuthorizationUrl(options: {
26
+ projectBaseUrl: string;
27
+ clientId: string;
28
+ redirectUri: string;
29
+ codeChallenge: string;
30
+ state: string;
31
+ }): string;
32
+ declare function parseOAuthTokenSet(value: unknown, previousRefreshToken?: string): OAuthTokenSet;
33
+ declare function readBoundedJson(response: ResponseLike, url: string): Promise<unknown>;
34
+ export declare function exchangeOAuthAuthorizationCode(options: {
35
+ projectBaseUrl: string;
36
+ clientId: string;
37
+ redirectUri: string;
38
+ authorizationCode: string;
39
+ codeVerifier: string;
40
+ fetchImpl: FetchLike;
41
+ timeoutMs: number;
42
+ }): Promise<OAuthTokenSet>;
43
+ export declare function refreshOAuthTokens(options: {
44
+ projectBaseUrl: string;
45
+ clientId: string;
46
+ refreshToken: string;
47
+ fetchImpl: FetchLike;
48
+ timeoutMs: number;
49
+ }): Promise<OAuthTokenSet>;
50
+ export declare function fetchOAuthUserInfo(options: {
51
+ projectBaseUrl: string;
52
+ accessToken: string;
53
+ fetchImpl: FetchLike;
54
+ timeoutMs: number;
55
+ }): Promise<OAuthUserInfo>;
56
+ export declare const __testInternals: {
57
+ base64Url: typeof base64Url;
58
+ parseOAuthTokenSet: typeof parseOAuthTokenSet;
59
+ readBoundedJson: typeof readBoundedJson;
60
+ requireProjectBaseUrl: typeof requireProjectBaseUrl;
61
+ trimString: typeof trimString;
62
+ };
63
+ export {};
@@ -0,0 +1,268 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { CliError } from './errors.js';
3
+ export const OAUTH_TOKEN_RESPONSE_MAX_BYTES = 64 * 1024;
4
+ export const DEFAULT_OAUTH_SCOPES = ['openid', 'email', 'profile'];
5
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
6
+ const PKCE_VALUE_PATTERN = /^[A-Za-z0-9._~-]{43,128}$/u;
7
+ const STATE_PATTERN = /^[A-Za-z0-9_-]{32,128}$/u;
8
+ const OAUTH_ERROR_PATTERN = /^[a-z][a-z0-9_]{0,63}$/u;
9
+ function base64Url(bytes) {
10
+ return Buffer.from(bytes).toString('base64url');
11
+ }
12
+ function trimString(value) {
13
+ return typeof value === 'string' ? value.trim() : '';
14
+ }
15
+ function isRecord(value) {
16
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
17
+ }
18
+ function requireProjectBaseUrl(value) {
19
+ let parsed;
20
+ try {
21
+ parsed = new URL(value);
22
+ }
23
+ catch {
24
+ throw new CliError('OAuth project URL is invalid.', {
25
+ code: 'OAUTH_PROJECT_URL_INVALID',
26
+ exitCode: 2,
27
+ });
28
+ }
29
+ const isLoopback = ['127.0.0.1', 'localhost', '[::1]'].includes(parsed.hostname);
30
+ if (parsed.username ||
31
+ parsed.password ||
32
+ parsed.search ||
33
+ parsed.hash ||
34
+ parsed.pathname.replace(/\/+$/u, '') ||
35
+ (parsed.protocol !== 'https:' && !(parsed.protocol === 'http:' && isLoopback))) {
36
+ throw new CliError('OAuth project URL must be an HTTPS origin or a loopback HTTP origin.', {
37
+ code: 'OAUTH_PROJECT_URL_INVALID',
38
+ exitCode: 2,
39
+ });
40
+ }
41
+ return parsed.origin;
42
+ }
43
+ export function requireOAuthClientId(value) {
44
+ const normalized = value.trim().toLowerCase();
45
+ if (!UUID_PATTERN.test(normalized)) {
46
+ throw new CliError('TIANGONG_LCA_OAUTH_CLIENT_ID must be a canonical UUID.', {
47
+ code: 'OAUTH_CLIENT_ID_INVALID',
48
+ exitCode: 2,
49
+ });
50
+ }
51
+ return normalized;
52
+ }
53
+ export function createOAuthPkceValues(randomBytesImpl = (size) => randomBytes(size)) {
54
+ const verifierBytes = randomBytesImpl(64);
55
+ const stateBytes = randomBytesImpl(32);
56
+ if (verifierBytes.byteLength !== 64 || stateBytes.byteLength !== 32) {
57
+ throw new CliError('Secure OAuth randomness source returned an invalid length.', {
58
+ code: 'OAUTH_RANDOMNESS_INVALID',
59
+ exitCode: 1,
60
+ });
61
+ }
62
+ const codeVerifier = base64Url(verifierBytes);
63
+ const state = base64Url(stateBytes);
64
+ return {
65
+ codeVerifier,
66
+ codeChallenge: createHash('sha256').update(codeVerifier, 'ascii').digest('base64url'),
67
+ state,
68
+ };
69
+ }
70
+ export function buildOAuthAuthorizationUrl(options) {
71
+ const projectBaseUrl = requireProjectBaseUrl(options.projectBaseUrl);
72
+ const clientId = requireOAuthClientId(options.clientId);
73
+ if (!PKCE_VALUE_PATTERN.test(options.codeChallenge)) {
74
+ throw new CliError('OAuth PKCE code challenge is invalid.', {
75
+ code: 'OAUTH_PKCE_CHALLENGE_INVALID',
76
+ exitCode: 1,
77
+ });
78
+ }
79
+ if (!STATE_PATTERN.test(options.state)) {
80
+ throw new CliError('OAuth state is invalid.', {
81
+ code: 'OAUTH_STATE_INVALID',
82
+ exitCode: 1,
83
+ });
84
+ }
85
+ const authorizationUrl = new URL(`${projectBaseUrl}/auth/v1/oauth/authorize`);
86
+ authorizationUrl.search = new URLSearchParams({
87
+ response_type: 'code',
88
+ client_id: clientId,
89
+ redirect_uri: options.redirectUri,
90
+ scope: DEFAULT_OAUTH_SCOPES.join(' '),
91
+ code_challenge: options.codeChallenge,
92
+ code_challenge_method: 'S256',
93
+ state: options.state,
94
+ }).toString();
95
+ return authorizationUrl.toString();
96
+ }
97
+ function parseOAuthTokenSet(value, previousRefreshToken = '') {
98
+ if (!isRecord(value)) {
99
+ throw new CliError('OAuth token response was not an object.', {
100
+ code: 'OAUTH_TOKEN_RESPONSE_INVALID',
101
+ exitCode: 1,
102
+ });
103
+ }
104
+ const accessToken = trimString(value.access_token);
105
+ const refreshToken = trimString(value.refresh_token) || trimString(previousRefreshToken);
106
+ const tokenType = trimString(value.token_type).toLowerCase();
107
+ const expiresIn = value.expires_in;
108
+ const scopeText = trimString(value.scope);
109
+ const scope = scopeText ? [...new Set(scopeText.split(/\s+/u).filter(Boolean))].sort() : [];
110
+ if (!accessToken ||
111
+ !refreshToken ||
112
+ tokenType !== 'bearer' ||
113
+ typeof expiresIn !== 'number' ||
114
+ !Number.isSafeInteger(expiresIn) ||
115
+ expiresIn <= 0) {
116
+ throw new CliError('OAuth token response did not contain a usable bearer session.', {
117
+ code: 'OAUTH_TOKEN_RESPONSE_INVALID',
118
+ exitCode: 1,
119
+ });
120
+ }
121
+ return { accessToken, refreshToken, expiresIn, scope };
122
+ }
123
+ async function readBoundedJson(response, url) {
124
+ const contentLengthText = response.headers.get('content-length');
125
+ if (contentLengthText) {
126
+ const contentLength = Number(contentLengthText);
127
+ if (!Number.isSafeInteger(contentLength) || contentLength < 0) {
128
+ throw new CliError('OAuth response Content-Length is invalid.', {
129
+ code: 'OAUTH_RESPONSE_INVALID',
130
+ exitCode: 1,
131
+ });
132
+ }
133
+ if (contentLength > OAUTH_TOKEN_RESPONSE_MAX_BYTES) {
134
+ throw new CliError('OAuth response exceeded the byte limit.', {
135
+ code: 'OAUTH_RESPONSE_TOO_LARGE',
136
+ exitCode: 1,
137
+ });
138
+ }
139
+ }
140
+ const rawText = await response.text();
141
+ if (Buffer.byteLength(rawText, 'utf8') > OAUTH_TOKEN_RESPONSE_MAX_BYTES) {
142
+ throw new CliError('OAuth response exceeded the byte limit.', {
143
+ code: 'OAUTH_RESPONSE_TOO_LARGE',
144
+ exitCode: 1,
145
+ });
146
+ }
147
+ let parsed;
148
+ try {
149
+ parsed = JSON.parse(rawText);
150
+ }
151
+ catch {
152
+ throw new CliError(`OAuth response was not valid JSON for ${url}.`, {
153
+ code: 'OAUTH_RESPONSE_INVALID',
154
+ exitCode: 1,
155
+ });
156
+ }
157
+ if (!response.ok) {
158
+ const oauthError = isRecord(parsed) && OAUTH_ERROR_PATTERN.test(trimString(parsed.error))
159
+ ? trimString(parsed.error)
160
+ : 'oauth_request_failed';
161
+ throw new CliError(`OAuth endpoint returned HTTP ${response.status}.`, {
162
+ code: 'OAUTH_REQUEST_FAILED',
163
+ exitCode: 1,
164
+ details: { status: response.status, error: oauthError },
165
+ });
166
+ }
167
+ return parsed;
168
+ }
169
+ async function requestOAuthToken(options) {
170
+ const projectBaseUrl = requireProjectBaseUrl(options.projectBaseUrl);
171
+ const url = `${projectBaseUrl}/auth/v1/oauth/token`;
172
+ const response = await options.fetchImpl(url, {
173
+ method: 'POST',
174
+ headers: {
175
+ Accept: 'application/json',
176
+ 'Content-Type': 'application/x-www-form-urlencoded',
177
+ },
178
+ body: options.body.toString(),
179
+ signal: AbortSignal.timeout(options.timeoutMs),
180
+ });
181
+ return parseOAuthTokenSet(await readBoundedJson(response, url), options.previousRefreshToken);
182
+ }
183
+ export async function exchangeOAuthAuthorizationCode(options) {
184
+ const clientId = requireOAuthClientId(options.clientId);
185
+ const authorizationCode = trimString(options.authorizationCode);
186
+ if (!authorizationCode || authorizationCode.length > 4096) {
187
+ throw new CliError('OAuth authorization code is invalid.', {
188
+ code: 'OAUTH_AUTHORIZATION_CODE_INVALID',
189
+ exitCode: 1,
190
+ });
191
+ }
192
+ if (!PKCE_VALUE_PATTERN.test(options.codeVerifier)) {
193
+ throw new CliError('OAuth PKCE verifier is invalid.', {
194
+ code: 'OAUTH_PKCE_VERIFIER_INVALID',
195
+ exitCode: 1,
196
+ });
197
+ }
198
+ return requestOAuthToken({
199
+ projectBaseUrl: options.projectBaseUrl,
200
+ fetchImpl: options.fetchImpl,
201
+ timeoutMs: options.timeoutMs,
202
+ body: new URLSearchParams({
203
+ grant_type: 'authorization_code',
204
+ code: authorizationCode,
205
+ client_id: clientId,
206
+ redirect_uri: options.redirectUri,
207
+ code_verifier: options.codeVerifier,
208
+ }),
209
+ });
210
+ }
211
+ export async function refreshOAuthTokens(options) {
212
+ const clientId = requireOAuthClientId(options.clientId);
213
+ const refreshToken = trimString(options.refreshToken);
214
+ if (!refreshToken) {
215
+ throw new CliError('OAuth refresh token is missing.', {
216
+ code: 'OAUTH_REFRESH_TOKEN_REQUIRED',
217
+ exitCode: 1,
218
+ });
219
+ }
220
+ return requestOAuthToken({
221
+ projectBaseUrl: options.projectBaseUrl,
222
+ fetchImpl: options.fetchImpl,
223
+ timeoutMs: options.timeoutMs,
224
+ previousRefreshToken: refreshToken,
225
+ body: new URLSearchParams({
226
+ grant_type: 'refresh_token',
227
+ refresh_token: refreshToken,
228
+ client_id: clientId,
229
+ }),
230
+ });
231
+ }
232
+ export async function fetchOAuthUserInfo(options) {
233
+ const projectBaseUrl = requireProjectBaseUrl(options.projectBaseUrl);
234
+ const accessToken = trimString(options.accessToken);
235
+ if (!accessToken) {
236
+ throw new CliError('OAuth access token is missing.', {
237
+ code: 'OAUTH_ACCESS_TOKEN_REQUIRED',
238
+ exitCode: 1,
239
+ });
240
+ }
241
+ const url = `${projectBaseUrl}/auth/v1/oauth/userinfo`;
242
+ const response = await options.fetchImpl(url, {
243
+ method: 'GET',
244
+ headers: {
245
+ Accept: 'application/json',
246
+ Authorization: `Bearer ${accessToken}`,
247
+ },
248
+ signal: AbortSignal.timeout(options.timeoutMs),
249
+ });
250
+ const value = await readBoundedJson(response, url);
251
+ const userId = isRecord(value) ? trimString(value.sub) : '';
252
+ const email = isRecord(value) ? trimString(value.email) : '';
253
+ if (!UUID_PATTERN.test(userId) || !email || email.length > 320) {
254
+ throw new CliError('OAuth UserInfo response did not contain a usable user identity.', {
255
+ code: 'OAUTH_USERINFO_INVALID',
256
+ exitCode: 1,
257
+ });
258
+ }
259
+ return { userId: userId.toLowerCase(), email };
260
+ }
261
+ export const __testInternals = {
262
+ base64Url,
263
+ parseOAuthTokenSet,
264
+ readBoundedJson,
265
+ requireProjectBaseUrl,
266
+ trimString,
267
+ };
268
+ //# sourceMappingURL=oauth-pkce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-pkce.js","sourceRoot":"","sources":["../../../src/lib/oauth-pkce.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomBytes } from 'node:crypto';\nimport { CliError } from './errors.js';\nimport type { FetchLike, ResponseLike } from './http.js';\n\nexport const OAUTH_TOKEN_RESPONSE_MAX_BYTES = 64 * 1024;\nexport const DEFAULT_OAUTH_SCOPES = ['openid', 'email', 'profile'] as const;\n\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;\nconst PKCE_VALUE_PATTERN = /^[A-Za-z0-9._~-]{43,128}$/u;\nconst STATE_PATTERN = /^[A-Za-z0-9_-]{32,128}$/u;\nconst OAUTH_ERROR_PATTERN = /^[a-z][a-z0-9_]{0,63}$/u;\n\nexport type OAuthPkceValues = {\n codeVerifier: string;\n codeChallenge: string;\n state: string;\n};\n\nexport type OAuthTokenSet = {\n accessToken: string;\n refreshToken: string;\n expiresIn: number;\n scope: string[];\n};\n\nexport type OAuthUserInfo = {\n userId: string;\n email: string;\n};\n\ntype RandomBytes = (size: number) => Uint8Array;\n\nfunction base64Url(bytes: Uint8Array): string {\n return Buffer.from(bytes).toString('base64url');\n}\n\nfunction trimString(value: unknown): string {\n return typeof value === 'string' ? value.trim() : '';\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction requireProjectBaseUrl(value: string): string {\n let parsed: URL;\n try {\n parsed = new URL(value);\n } catch {\n throw new CliError('OAuth project URL is invalid.', {\n code: 'OAUTH_PROJECT_URL_INVALID',\n exitCode: 2,\n });\n }\n\n const isLoopback = ['127.0.0.1', 'localhost', '[::1]'].includes(parsed.hostname);\n if (\n parsed.username ||\n parsed.password ||\n parsed.search ||\n parsed.hash ||\n parsed.pathname.replace(/\\/+$/u, '') ||\n (parsed.protocol !== 'https:' && !(parsed.protocol === 'http:' && isLoopback))\n ) {\n throw new CliError('OAuth project URL must be an HTTPS origin or a loopback HTTP origin.', {\n code: 'OAUTH_PROJECT_URL_INVALID',\n exitCode: 2,\n });\n }\n\n return parsed.origin;\n}\n\nexport function requireOAuthClientId(value: string): string {\n const normalized = value.trim().toLowerCase();\n if (!UUID_PATTERN.test(normalized)) {\n throw new CliError('TIANGONG_LCA_OAUTH_CLIENT_ID must be a canonical UUID.', {\n code: 'OAUTH_CLIENT_ID_INVALID',\n exitCode: 2,\n });\n }\n return normalized;\n}\n\nexport function createOAuthPkceValues(\n randomBytesImpl: RandomBytes = (size) => randomBytes(size),\n): OAuthPkceValues {\n const verifierBytes = randomBytesImpl(64);\n const stateBytes = randomBytesImpl(32);\n if (verifierBytes.byteLength !== 64 || stateBytes.byteLength !== 32) {\n throw new CliError('Secure OAuth randomness source returned an invalid length.', {\n code: 'OAUTH_RANDOMNESS_INVALID',\n exitCode: 1,\n });\n }\n\n const codeVerifier = base64Url(verifierBytes);\n const state = base64Url(stateBytes);\n return {\n codeVerifier,\n codeChallenge: createHash('sha256').update(codeVerifier, 'ascii').digest('base64url'),\n state,\n };\n}\n\nexport function buildOAuthAuthorizationUrl(options: {\n projectBaseUrl: string;\n clientId: string;\n redirectUri: string;\n codeChallenge: string;\n state: string;\n}): string {\n const projectBaseUrl = requireProjectBaseUrl(options.projectBaseUrl);\n const clientId = requireOAuthClientId(options.clientId);\n if (!PKCE_VALUE_PATTERN.test(options.codeChallenge)) {\n throw new CliError('OAuth PKCE code challenge is invalid.', {\n code: 'OAUTH_PKCE_CHALLENGE_INVALID',\n exitCode: 1,\n });\n }\n if (!STATE_PATTERN.test(options.state)) {\n throw new CliError('OAuth state is invalid.', {\n code: 'OAUTH_STATE_INVALID',\n exitCode: 1,\n });\n }\n\n const authorizationUrl = new URL(`${projectBaseUrl}/auth/v1/oauth/authorize`);\n authorizationUrl.search = new URLSearchParams({\n response_type: 'code',\n client_id: clientId,\n redirect_uri: options.redirectUri,\n scope: DEFAULT_OAUTH_SCOPES.join(' '),\n code_challenge: options.codeChallenge,\n code_challenge_method: 'S256',\n state: options.state,\n }).toString();\n return authorizationUrl.toString();\n}\n\nfunction parseOAuthTokenSet(value: unknown, previousRefreshToken = ''): OAuthTokenSet {\n if (!isRecord(value)) {\n throw new CliError('OAuth token response was not an object.', {\n code: 'OAUTH_TOKEN_RESPONSE_INVALID',\n exitCode: 1,\n });\n }\n\n const accessToken = trimString(value.access_token);\n const refreshToken = trimString(value.refresh_token) || trimString(previousRefreshToken);\n const tokenType = trimString(value.token_type).toLowerCase();\n const expiresIn = value.expires_in;\n const scopeText = trimString(value.scope);\n const scope = scopeText ? [...new Set(scopeText.split(/\\s+/u).filter(Boolean))].sort() : [];\n\n if (\n !accessToken ||\n !refreshToken ||\n tokenType !== 'bearer' ||\n typeof expiresIn !== 'number' ||\n !Number.isSafeInteger(expiresIn) ||\n expiresIn <= 0\n ) {\n throw new CliError('OAuth token response did not contain a usable bearer session.', {\n code: 'OAUTH_TOKEN_RESPONSE_INVALID',\n exitCode: 1,\n });\n }\n\n return { accessToken, refreshToken, expiresIn, scope };\n}\n\nasync function readBoundedJson(response: ResponseLike, url: string): Promise<unknown> {\n const contentLengthText = response.headers.get('content-length');\n if (contentLengthText) {\n const contentLength = Number(contentLengthText);\n if (!Number.isSafeInteger(contentLength) || contentLength < 0) {\n throw new CliError('OAuth response Content-Length is invalid.', {\n code: 'OAUTH_RESPONSE_INVALID',\n exitCode: 1,\n });\n }\n if (contentLength > OAUTH_TOKEN_RESPONSE_MAX_BYTES) {\n throw new CliError('OAuth response exceeded the byte limit.', {\n code: 'OAUTH_RESPONSE_TOO_LARGE',\n exitCode: 1,\n });\n }\n }\n\n const rawText = await response.text();\n if (Buffer.byteLength(rawText, 'utf8') > OAUTH_TOKEN_RESPONSE_MAX_BYTES) {\n throw new CliError('OAuth response exceeded the byte limit.', {\n code: 'OAUTH_RESPONSE_TOO_LARGE',\n exitCode: 1,\n });\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawText);\n } catch {\n throw new CliError(`OAuth response was not valid JSON for ${url}.`, {\n code: 'OAUTH_RESPONSE_INVALID',\n exitCode: 1,\n });\n }\n\n if (!response.ok) {\n const oauthError =\n isRecord(parsed) && OAUTH_ERROR_PATTERN.test(trimString(parsed.error))\n ? trimString(parsed.error)\n : 'oauth_request_failed';\n throw new CliError(`OAuth endpoint returned HTTP ${response.status}.`, {\n code: 'OAUTH_REQUEST_FAILED',\n exitCode: 1,\n details: { status: response.status, error: oauthError },\n });\n }\n\n return parsed;\n}\n\nasync function requestOAuthToken(options: {\n projectBaseUrl: string;\n body: URLSearchParams;\n fetchImpl: FetchLike;\n timeoutMs: number;\n previousRefreshToken?: string;\n}): Promise<OAuthTokenSet> {\n const projectBaseUrl = requireProjectBaseUrl(options.projectBaseUrl);\n const url = `${projectBaseUrl}/auth/v1/oauth/token`;\n const response = await options.fetchImpl(url, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: options.body.toString(),\n signal: AbortSignal.timeout(options.timeoutMs),\n });\n return parseOAuthTokenSet(await readBoundedJson(response, url), options.previousRefreshToken);\n}\n\nexport async function exchangeOAuthAuthorizationCode(options: {\n projectBaseUrl: string;\n clientId: string;\n redirectUri: string;\n authorizationCode: string;\n codeVerifier: string;\n fetchImpl: FetchLike;\n timeoutMs: number;\n}): Promise<OAuthTokenSet> {\n const clientId = requireOAuthClientId(options.clientId);\n const authorizationCode = trimString(options.authorizationCode);\n if (!authorizationCode || authorizationCode.length > 4096) {\n throw new CliError('OAuth authorization code is invalid.', {\n code: 'OAUTH_AUTHORIZATION_CODE_INVALID',\n exitCode: 1,\n });\n }\n if (!PKCE_VALUE_PATTERN.test(options.codeVerifier)) {\n throw new CliError('OAuth PKCE verifier is invalid.', {\n code: 'OAUTH_PKCE_VERIFIER_INVALID',\n exitCode: 1,\n });\n }\n\n return requestOAuthToken({\n projectBaseUrl: options.projectBaseUrl,\n fetchImpl: options.fetchImpl,\n timeoutMs: options.timeoutMs,\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code: authorizationCode,\n client_id: clientId,\n redirect_uri: options.redirectUri,\n code_verifier: options.codeVerifier,\n }),\n });\n}\n\nexport async function refreshOAuthTokens(options: {\n projectBaseUrl: string;\n clientId: string;\n refreshToken: string;\n fetchImpl: FetchLike;\n timeoutMs: number;\n}): Promise<OAuthTokenSet> {\n const clientId = requireOAuthClientId(options.clientId);\n const refreshToken = trimString(options.refreshToken);\n if (!refreshToken) {\n throw new CliError('OAuth refresh token is missing.', {\n code: 'OAUTH_REFRESH_TOKEN_REQUIRED',\n exitCode: 1,\n });\n }\n\n return requestOAuthToken({\n projectBaseUrl: options.projectBaseUrl,\n fetchImpl: options.fetchImpl,\n timeoutMs: options.timeoutMs,\n previousRefreshToken: refreshToken,\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: clientId,\n }),\n });\n}\n\nexport async function fetchOAuthUserInfo(options: {\n projectBaseUrl: string;\n accessToken: string;\n fetchImpl: FetchLike;\n timeoutMs: number;\n}): Promise<OAuthUserInfo> {\n const projectBaseUrl = requireProjectBaseUrl(options.projectBaseUrl);\n const accessToken = trimString(options.accessToken);\n if (!accessToken) {\n throw new CliError('OAuth access token is missing.', {\n code: 'OAUTH_ACCESS_TOKEN_REQUIRED',\n exitCode: 1,\n });\n }\n\n const url = `${projectBaseUrl}/auth/v1/oauth/userinfo`;\n const response = await options.fetchImpl(url, {\n method: 'GET',\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${accessToken}`,\n },\n signal: AbortSignal.timeout(options.timeoutMs),\n });\n const value = await readBoundedJson(response, url);\n const userId = isRecord(value) ? trimString(value.sub) : '';\n const email = isRecord(value) ? trimString(value.email) : '';\n if (!UUID_PATTERN.test(userId) || !email || email.length > 320) {\n throw new CliError('OAuth UserInfo response did not contain a usable user identity.', {\n code: 'OAUTH_USERINFO_INVALID',\n exitCode: 1,\n });\n }\n return { userId: userId.toLowerCase(), email };\n}\n\nexport const __testInternals = {\n base64Url,\n parseOAuthTokenSet,\n readBoundedJson,\n requireProjectBaseUrl,\n trimString,\n};\n"]}
@@ -8,7 +8,7 @@ import { validateProcessPayload, } from './process-payload-validation.js';
8
8
  import { deriveSupabaseProjectBaseUrl, deriveSupabaseRestBaseUrl, requireSupabaseRestRuntime, } from './supabase-client.js';
9
9
  import { applyDataApiProfileHeaders, buildDataApiUrl, resolveDataApiCapability, } from './supabase-data-api-contract.js';
10
10
  import { resolveSupabaseUserSession } from './supabase-session.js';
11
- import { redactEmail, requireUserApiKeyCredentials } from './user-api-key.js';
11
+ import { redactEmail } from './user-api-key.js';
12
12
  const DEFAULT_TIMEOUT_MS = 15_000;
13
13
  const DEFAULT_MAX_RETRIES = 3;
14
14
  const DEFAULT_PAGE_SIZE = 500;
@@ -395,7 +395,7 @@ async function resolveRemoteAuth(options) {
395
395
  publishableKey: runtime.publishableKey,
396
396
  accessToken: session.accessToken,
397
397
  userId,
398
- maskedUserEmail: redactEmail(requireUserApiKeyCredentials(runtime.userApiKey).email),
398
+ maskedUserEmail: redactEmail(session.userEmail),
399
399
  };
400
400
  }
401
401
  function normalizeManifestRow(value) {