@portaidentity/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/browser-flow.d.ts +42 -0
- package/dist/auth/browser-flow.d.ts.map +1 -0
- package/dist/auth/browser-flow.js +193 -0
- package/dist/auth/browser-flow.js.map +1 -0
- package/dist/auth/callback-server.d.ts +81 -0
- package/dist/auth/callback-server.d.ts.map +1 -0
- package/dist/auth/callback-server.js +193 -0
- package/dist/auth/callback-server.js.map +1 -0
- package/dist/auth/metadata.d.ts +43 -0
- package/dist/auth/metadata.d.ts.map +1 -0
- package/dist/auth/metadata.js +66 -0
- package/dist/auth/metadata.js.map +1 -0
- package/dist/auth/pkce.d.ts +42 -0
- package/dist/auth/pkce.d.ts.map +1 -0
- package/dist/auth/pkce.js +52 -0
- package/dist/auth/pkce.js.map +1 -0
- package/dist/auth/types.d.ts +72 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +11 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/client-factory.d.ts +29 -0
- package/dist/client-factory.d.ts.map +1 -0
- package/dist/client-factory.js +43 -0
- package/dist/client-factory.js.map +1 -0
- package/dist/commands/app-claim.d.ts +9 -0
- package/dist/commands/app-claim.d.ts.map +1 -0
- package/dist/commands/app-claim.js +128 -0
- package/dist/commands/app-claim.js.map +1 -0
- package/dist/commands/app-module.d.ts +9 -0
- package/dist/commands/app-module.d.ts.map +1 -0
- package/dist/commands/app-module.js +104 -0
- package/dist/commands/app-module.js.map +1 -0
- package/dist/commands/app-permission.d.ts +9 -0
- package/dist/commands/app-permission.d.ts.map +1 -0
- package/dist/commands/app-permission.js +118 -0
- package/dist/commands/app-permission.js.map +1 -0
- package/dist/commands/app-role.d.ts +9 -0
- package/dist/commands/app-role.d.ts.map +1 -0
- package/dist/commands/app-role.js +166 -0
- package/dist/commands/app-role.js.map +1 -0
- package/dist/commands/app.d.ts +12 -0
- package/dist/commands/app.d.ts.map +1 -0
- package/dist/commands/app.js +255 -0
- package/dist/commands/app.js.map +1 -0
- package/dist/commands/audit.d.ts +12 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +96 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/bulk.d.ts +12 -0
- package/dist/commands/bulk.d.ts.map +1 -0
- package/dist/commands/bulk.js +77 -0
- package/dist/commands/bulk.js.map +1 -0
- package/dist/commands/client-secret.d.ts +18 -0
- package/dist/commands/client-secret.d.ts.map +1 -0
- package/dist/commands/client-secret.js +126 -0
- package/dist/commands/client-secret.js.map +1 -0
- package/dist/commands/client.d.ts +27 -0
- package/dist/commands/client.d.ts.map +1 -0
- package/dist/commands/client.js +385 -0
- package/dist/commands/client.js.map +1 -0
- package/dist/commands/completion.d.ts +27 -0
- package/dist/commands/completion.d.ts.map +1 -0
- package/dist/commands/completion.js +42 -0
- package/dist/commands/completion.js.map +1 -0
- package/dist/commands/config.d.ts +14 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +85 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/doctor.d.ts +25 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +198 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/exports.d.ts +12 -0
- package/dist/commands/exports.d.ts.map +1 -0
- package/dist/commands/exports.js +80 -0
- package/dist/commands/exports.js.map +1 -0
- package/dist/commands/health.d.ts +12 -0
- package/dist/commands/health.d.ts.map +1 -0
- package/dist/commands/health.js +53 -0
- package/dist/commands/health.js.map +1 -0
- package/dist/commands/keys.d.ts +14 -0
- package/dist/commands/keys.d.ts.map +1 -0
- package/dist/commands/keys.js +91 -0
- package/dist/commands/keys.js.map +1 -0
- package/dist/commands/login.d.ts +36 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +78 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +25 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +43 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/org.d.ts +26 -0
- package/dist/commands/org.d.ts.map +1 -0
- package/dist/commands/org.js +396 -0
- package/dist/commands/org.js.map +1 -0
- package/dist/commands/provision.d.ts +47 -0
- package/dist/commands/provision.d.ts.map +1 -0
- package/dist/commands/provision.js +400 -0
- package/dist/commands/provision.js.map +1 -0
- package/dist/commands/sessions.d.ts +14 -0
- package/dist/commands/sessions.d.ts.map +1 -0
- package/dist/commands/sessions.js +122 -0
- package/dist/commands/sessions.js.map +1 -0
- package/dist/commands/stats.d.ts +12 -0
- package/dist/commands/stats.d.ts.map +1 -0
- package/dist/commands/stats.js +46 -0
- package/dist/commands/stats.js.map +1 -0
- package/dist/commands/user-claim.d.ts +17 -0
- package/dist/commands/user-claim.d.ts.map +1 -0
- package/dist/commands/user-claim.js +123 -0
- package/dist/commands/user-claim.js.map +1 -0
- package/dist/commands/user-role.d.ts +17 -0
- package/dist/commands/user-role.d.ts.map +1 -0
- package/dist/commands/user-role.js +118 -0
- package/dist/commands/user-role.js.map +1 -0
- package/dist/commands/user.d.ts +26 -0
- package/dist/commands/user.d.ts.map +1 -0
- package/dist/commands/user.js +352 -0
- package/dist/commands/user.js.map +1 -0
- package/dist/commands/version.d.ts +25 -0
- package/dist/commands/version.d.ts.map +1 -0
- package/dist/commands/version.js +83 -0
- package/dist/commands/version.js.map +1 -0
- package/dist/commands/whoami.d.ts +26 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +66 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/credential-store.d.ts +101 -0
- package/dist/credential-store.d.ts.map +1 -0
- package/dist/credential-store.js +121 -0
- package/dist/credential-store.js.map +1 -0
- package/dist/error-handler.d.ts +47 -0
- package/dist/error-handler.d.ts.map +1 -0
- package/dist/error-handler.js +166 -0
- package/dist/error-handler.js.map +1 -0
- package/dist/global-options.d.ts +50 -0
- package/dist/global-options.d.ts.map +1 -0
- package/dist/global-options.js +62 -0
- package/dist/global-options.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +122 -0
- package/dist/index.js.map +1 -0
- package/dist/output.d.ts +75 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +100 -0
- package/dist/output.js.map +1 -0
- package/dist/parsers.d.ts +74 -0
- package/dist/parsers.d.ts.map +1 -0
- package/dist/parsers.js +125 -0
- package/dist/parsers.js.map +1 -0
- package/dist/prompt.d.ts +50 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +98 -0
- package/dist/prompt.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OIDC Authorization Code + PKCE browser flow orchestration.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the complete CLI login flow:
|
|
5
|
+
* 1. Discover admin metadata (client_id, issuer) from the server
|
|
6
|
+
* 2. Generate PKCE code_verifier + code_challenge (S256)
|
|
7
|
+
* 3. Start a temporary localhost HTTP server to receive the callback
|
|
8
|
+
* 4. Open the user's browser to the authorization endpoint
|
|
9
|
+
* 5. Wait for the callback with the authorization code
|
|
10
|
+
* 6. Exchange the code for tokens at the token endpoint
|
|
11
|
+
* 7. Decode the ID token for user identity
|
|
12
|
+
* 8. Return the complete AuthFlowResult
|
|
13
|
+
*
|
|
14
|
+
* Supports two modes:
|
|
15
|
+
* - **Browser mode**: Opens browser, starts callback server
|
|
16
|
+
* - **Manual mode**: Prints URL, user pastes callback URL
|
|
17
|
+
*
|
|
18
|
+
* @module auth/browser-flow
|
|
19
|
+
*/
|
|
20
|
+
import type { AuthFlowResult } from './types.js';
|
|
21
|
+
/** Options for the browser flow */
|
|
22
|
+
export interface BrowserFlowOptions {
|
|
23
|
+
/** Porta server URL */
|
|
24
|
+
server: string;
|
|
25
|
+
/** Override the auto-discovered client ID */
|
|
26
|
+
clientId?: string;
|
|
27
|
+
/** Force manual mode (print URL instead of opening browser) */
|
|
28
|
+
noBrowser?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Execute the OIDC Authorization Code + PKCE login flow.
|
|
32
|
+
*
|
|
33
|
+
* This is the main entry point for CLI authentication. It handles
|
|
34
|
+
* both browser-based and manual (Docker/headless) login modes.
|
|
35
|
+
*
|
|
36
|
+
* @param options - Login flow options
|
|
37
|
+
* @param log - Logging callback for status messages
|
|
38
|
+
* @returns Complete auth flow result ready to store as credentials
|
|
39
|
+
* @throws Error on any failure (connectivity, auth, token exchange)
|
|
40
|
+
*/
|
|
41
|
+
export declare function executeBrowserFlow(options: BrowserFlowOptions, log?: (message: string) => void): Promise<AuthFlowResult>;
|
|
42
|
+
//# sourceMappingURL=browser-flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-flow.d.ts","sourceRoot":"","sources":["../../src/auth/browser-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAaH,OAAO,KAAK,EAAE,cAAc,EAAiB,MAAM,YAAY,CAAC;AAahE,mCAAmC;AACnC,MAAM,WAAW,kBAAkB;IACjC,uBAAuB;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAsDD;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,GAAG,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAkB,GAC3C,OAAO,CAAC,cAAc,CAAC,CA2HzB"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OIDC Authorization Code + PKCE browser flow orchestration.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the complete CLI login flow:
|
|
5
|
+
* 1. Discover admin metadata (client_id, issuer) from the server
|
|
6
|
+
* 2. Generate PKCE code_verifier + code_challenge (S256)
|
|
7
|
+
* 3. Start a temporary localhost HTTP server to receive the callback
|
|
8
|
+
* 4. Open the user's browser to the authorization endpoint
|
|
9
|
+
* 5. Wait for the callback with the authorization code
|
|
10
|
+
* 6. Exchange the code for tokens at the token endpoint
|
|
11
|
+
* 7. Decode the ID token for user identity
|
|
12
|
+
* 8. Return the complete AuthFlowResult
|
|
13
|
+
*
|
|
14
|
+
* Supports two modes:
|
|
15
|
+
* - **Browser mode**: Opens browser, starts callback server
|
|
16
|
+
* - **Manual mode**: Prints URL, user pastes callback URL
|
|
17
|
+
*
|
|
18
|
+
* @module auth/browser-flow
|
|
19
|
+
*/
|
|
20
|
+
import { URL } from 'node:url';
|
|
21
|
+
import { decodeJwt } from 'jose';
|
|
22
|
+
import { generateCodeVerifier, generateCodeChallenge, generateState } from './pkce.js';
|
|
23
|
+
import { fetchAdminMetadata } from './metadata.js';
|
|
24
|
+
import { startCallbackServer, parseCallbackUrl, MANUAL_REDIRECT_URI, isContainerized, } from './callback-server.js';
|
|
25
|
+
import { question } from '../prompt.js';
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Constants
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
/** OIDC scopes requested during the login flow */
|
|
30
|
+
const SCOPES = 'openid profile email offline_access';
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Token Exchange
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
/**
|
|
35
|
+
* Exchange an authorization code for tokens at the OIDC token endpoint.
|
|
36
|
+
*
|
|
37
|
+
* Sends a POST request with the authorization code, PKCE code_verifier,
|
|
38
|
+
* and redirect URI to complete the Authorization Code flow.
|
|
39
|
+
*
|
|
40
|
+
* @param params - Token exchange parameters
|
|
41
|
+
* @returns Token response with access, refresh, and ID tokens
|
|
42
|
+
* @throws Error if the token exchange fails
|
|
43
|
+
*/
|
|
44
|
+
async function exchangeCode(params) {
|
|
45
|
+
const tokenUrl = `${params.server}/${params.orgSlug}/token`;
|
|
46
|
+
const response = await fetch(tokenUrl, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
49
|
+
body: new URLSearchParams({
|
|
50
|
+
grant_type: 'authorization_code',
|
|
51
|
+
code: params.code,
|
|
52
|
+
redirect_uri: params.redirectUri,
|
|
53
|
+
client_id: params.clientId,
|
|
54
|
+
code_verifier: params.codeVerifier,
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
const errorData = await response.json().catch(() => ({}));
|
|
59
|
+
const desc = errorData.error_description ||
|
|
60
|
+
errorData.error ||
|
|
61
|
+
`HTTP ${response.status}`;
|
|
62
|
+
throw new Error(`Token exchange failed: ${desc}`);
|
|
63
|
+
}
|
|
64
|
+
return response.json();
|
|
65
|
+
}
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Flow Orchestration
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
/**
|
|
70
|
+
* Execute the OIDC Authorization Code + PKCE login flow.
|
|
71
|
+
*
|
|
72
|
+
* This is the main entry point for CLI authentication. It handles
|
|
73
|
+
* both browser-based and manual (Docker/headless) login modes.
|
|
74
|
+
*
|
|
75
|
+
* @param options - Login flow options
|
|
76
|
+
* @param log - Logging callback for status messages
|
|
77
|
+
* @returns Complete auth flow result ready to store as credentials
|
|
78
|
+
* @throws Error on any failure (connectivity, auth, token exchange)
|
|
79
|
+
*/
|
|
80
|
+
export async function executeBrowserFlow(options, log = console.log) {
|
|
81
|
+
const { server, noBrowser } = options;
|
|
82
|
+
// ---------------------------------------------------------------
|
|
83
|
+
// Step 1: Determine login mode (browser vs. manual)
|
|
84
|
+
// ---------------------------------------------------------------
|
|
85
|
+
const manualMode = noBrowser || isContainerized();
|
|
86
|
+
if (manualMode && !noBrowser) {
|
|
87
|
+
log('Container environment detected — using manual login mode.\n');
|
|
88
|
+
}
|
|
89
|
+
// ---------------------------------------------------------------
|
|
90
|
+
// Step 2: Discover admin metadata (client ID + org slug)
|
|
91
|
+
// ---------------------------------------------------------------
|
|
92
|
+
let clientId;
|
|
93
|
+
let orgSlug;
|
|
94
|
+
if (options.clientId) {
|
|
95
|
+
clientId = options.clientId;
|
|
96
|
+
orgSlug = 'porta-admin';
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const metadata = await fetchAdminMetadata(server);
|
|
100
|
+
clientId = metadata.clientId;
|
|
101
|
+
orgSlug = metadata.orgSlug;
|
|
102
|
+
}
|
|
103
|
+
// ---------------------------------------------------------------
|
|
104
|
+
// Step 3: Generate PKCE parameters
|
|
105
|
+
// ---------------------------------------------------------------
|
|
106
|
+
const codeVerifier = generateCodeVerifier();
|
|
107
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
108
|
+
const state = generateState();
|
|
109
|
+
// ---------------------------------------------------------------
|
|
110
|
+
// Step 4: Set up redirect URI — mode-dependent
|
|
111
|
+
// ---------------------------------------------------------------
|
|
112
|
+
let redirectUri;
|
|
113
|
+
let authCode;
|
|
114
|
+
if (manualMode) {
|
|
115
|
+
redirectUri = MANUAL_REDIRECT_URI;
|
|
116
|
+
authCode = undefined;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
const callbackServer = await startCallbackServer(state);
|
|
120
|
+
redirectUri = `http://127.0.0.1:${callbackServer.port}/callback`;
|
|
121
|
+
authCode = callbackServer.authCode;
|
|
122
|
+
}
|
|
123
|
+
// ---------------------------------------------------------------
|
|
124
|
+
// Step 5: Build the authorization URL
|
|
125
|
+
// ---------------------------------------------------------------
|
|
126
|
+
const authUrl = new URL(`${server}/${orgSlug}/auth`);
|
|
127
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
128
|
+
authUrl.searchParams.set('client_id', clientId);
|
|
129
|
+
authUrl.searchParams.set('redirect_uri', redirectUri);
|
|
130
|
+
authUrl.searchParams.set('scope', SCOPES);
|
|
131
|
+
authUrl.searchParams.set('code_challenge', codeChallenge);
|
|
132
|
+
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
133
|
+
authUrl.searchParams.set('state', state);
|
|
134
|
+
// ---------------------------------------------------------------
|
|
135
|
+
// Step 6: Open browser or print URL + collect auth code
|
|
136
|
+
// ---------------------------------------------------------------
|
|
137
|
+
let code;
|
|
138
|
+
if (manualMode) {
|
|
139
|
+
log('Open this URL in your browser to log in:\n');
|
|
140
|
+
log(` ${authUrl.toString()}\n`);
|
|
141
|
+
log('After logging in, your browser will redirect to a page that won\'t load.');
|
|
142
|
+
log('Copy the full URL from your browser\'s address bar and paste it below.\n');
|
|
143
|
+
const pastedUrl = await question('Paste the callback URL: ');
|
|
144
|
+
code = parseCallbackUrl(pastedUrl, state);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
log('Opening browser for authentication...');
|
|
148
|
+
try {
|
|
149
|
+
// Dynamic import — `open` is an ESM-only package
|
|
150
|
+
const { default: openUrl } = await import('open');
|
|
151
|
+
await openUrl(authUrl.toString());
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
log('\nCould not open browser. Open this URL manually:\n');
|
|
155
|
+
log(` ${authUrl.toString()}\n`);
|
|
156
|
+
}
|
|
157
|
+
log('Waiting for authentication...');
|
|
158
|
+
code = await authCode;
|
|
159
|
+
}
|
|
160
|
+
// ---------------------------------------------------------------
|
|
161
|
+
// Step 7: Exchange the authorization code for tokens
|
|
162
|
+
// ---------------------------------------------------------------
|
|
163
|
+
const tokens = await exchangeCode({
|
|
164
|
+
server,
|
|
165
|
+
orgSlug,
|
|
166
|
+
code,
|
|
167
|
+
redirectUri,
|
|
168
|
+
clientId,
|
|
169
|
+
codeVerifier,
|
|
170
|
+
});
|
|
171
|
+
// ---------------------------------------------------------------
|
|
172
|
+
// Step 8: Decode the ID token to extract user identity
|
|
173
|
+
// ---------------------------------------------------------------
|
|
174
|
+
const claims = decodeJwt(tokens.id_token);
|
|
175
|
+
// ---------------------------------------------------------------
|
|
176
|
+
// Step 9: Build and return the auth flow result
|
|
177
|
+
// ---------------------------------------------------------------
|
|
178
|
+
return {
|
|
179
|
+
server,
|
|
180
|
+
orgSlug,
|
|
181
|
+
clientId,
|
|
182
|
+
accessToken: tokens.access_token,
|
|
183
|
+
refreshToken: tokens.refresh_token,
|
|
184
|
+
idToken: tokens.id_token,
|
|
185
|
+
expiresAt: new Date(Date.now() + tokens.expires_in * 1000).toISOString(),
|
|
186
|
+
userInfo: {
|
|
187
|
+
sub: claims.sub ?? '',
|
|
188
|
+
email: claims.email ?? '',
|
|
189
|
+
name: claims.name ?? undefined,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=browser-flow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-flow.js","sourceRoot":"","sources":["../../src/auth/browser-flow.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth callback server and manual URL parsing.
|
|
3
|
+
*
|
|
4
|
+
* Provides two mechanisms for receiving the OAuth authorization code:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Browser mode**: A temporary localhost HTTP server that receives
|
|
7
|
+
* the redirect callback. Binds to 127.0.0.1 on a random port.
|
|
8
|
+
*
|
|
9
|
+
* 2. **Manual mode**: URL parsing for Docker/headless environments where
|
|
10
|
+
* the user pastes the callback URL from their browser's address bar.
|
|
11
|
+
*
|
|
12
|
+
* Security:
|
|
13
|
+
* - Callback server binds to 127.0.0.1 only (loopback)
|
|
14
|
+
* - State parameter validated on every callback (CSRF protection)
|
|
15
|
+
* - 5-minute timeout prevents abandoned login sessions
|
|
16
|
+
*
|
|
17
|
+
* @module auth/callback-server
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Fixed redirect URI used in manual (no-browser) mode.
|
|
21
|
+
*
|
|
22
|
+
* The port number is arbitrary — nothing actually listens on it. The user's
|
|
23
|
+
* browser will fail to load this page after authentication, but the full URL
|
|
24
|
+
* (including the authorization code in the query string) will be visible in
|
|
25
|
+
* the browser's address bar for the user to copy and paste back.
|
|
26
|
+
*
|
|
27
|
+
* Per RFC 8252 §7.3, node-oidc-provider allows flexible port matching for
|
|
28
|
+
* native clients using loopback redirect URIs, so any port works as long as
|
|
29
|
+
* the base pattern (`http://127.0.0.1/callback`) is registered.
|
|
30
|
+
*/
|
|
31
|
+
export declare const MANUAL_REDIRECT_URI = "http://127.0.0.1:11111/callback";
|
|
32
|
+
/**
|
|
33
|
+
* Result from starting the callback server.
|
|
34
|
+
*/
|
|
35
|
+
export interface CallbackServerResult {
|
|
36
|
+
/** Port the server is listening on */
|
|
37
|
+
port: number;
|
|
38
|
+
/** Promise that resolves with the authorization code */
|
|
39
|
+
authCode: Promise<string>;
|
|
40
|
+
/** Close the server (for cleanup on error) */
|
|
41
|
+
close: () => void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Start a temporary HTTP server to receive the OAuth callback.
|
|
45
|
+
*
|
|
46
|
+
* Binds to 127.0.0.1 on a random available port. Validates the
|
|
47
|
+
* state parameter and extracts the authorization code from the
|
|
48
|
+
* callback URL query parameters.
|
|
49
|
+
*
|
|
50
|
+
* The server automatically shuts down after receiving a callback
|
|
51
|
+
* (success or error) or after the 5-minute timeout.
|
|
52
|
+
*
|
|
53
|
+
* @param expectedState - The state parameter to validate against
|
|
54
|
+
* @returns Object with the assigned port and a promise that resolves with the auth code
|
|
55
|
+
*/
|
|
56
|
+
export declare function startCallbackServer(expectedState: string): Promise<CallbackServerResult>;
|
|
57
|
+
/**
|
|
58
|
+
* Parse the authorization code and state from a pasted callback URL.
|
|
59
|
+
*
|
|
60
|
+
* In manual mode the user pastes the full URL from their browser's address
|
|
61
|
+
* bar after authentication. This function extracts the `code` and `state`
|
|
62
|
+
* query parameters and validates the state against the expected value.
|
|
63
|
+
*
|
|
64
|
+
* @param pastedUrl - The full callback URL pasted by the user
|
|
65
|
+
* @param expectedState - The state value to validate against (CSRF protection)
|
|
66
|
+
* @returns The authorization code extracted from the URL
|
|
67
|
+
* @throws Error if the URL is malformed, state mismatches, or code is missing
|
|
68
|
+
*/
|
|
69
|
+
export declare function parseCallbackUrl(pastedUrl: string, expectedState: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* Detect whether the process is running inside a Docker container.
|
|
72
|
+
*
|
|
73
|
+
* Checks for the `/.dockerenv` sentinel file that Docker creates in every
|
|
74
|
+
* container. Also honours the `PORTA_CONTAINER` environment variable so
|
|
75
|
+
* users can force manual mode in other containerized runtimes (Podman,
|
|
76
|
+
* Kubernetes, etc.) by setting `PORTA_CONTAINER=1`.
|
|
77
|
+
*
|
|
78
|
+
* @returns true if running inside a container
|
|
79
|
+
*/
|
|
80
|
+
export declare function isContainerized(): boolean;
|
|
81
|
+
//# sourceMappingURL=callback-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"callback-server.d.ts","sourceRoot":"","sources":["../../src/auth/callback-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAaH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,oCAAoC,CAAC;AAMrE;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1B,8CAA8C;IAC9C,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,oBAAoB,CAAC,CA+F/B;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,GACpB,MAAM,CA8BR;AAMD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,IAAI,OAAO,CAUzC"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth callback server and manual URL parsing.
|
|
3
|
+
*
|
|
4
|
+
* Provides two mechanisms for receiving the OAuth authorization code:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Browser mode**: A temporary localhost HTTP server that receives
|
|
7
|
+
* the redirect callback. Binds to 127.0.0.1 on a random port.
|
|
8
|
+
*
|
|
9
|
+
* 2. **Manual mode**: URL parsing for Docker/headless environments where
|
|
10
|
+
* the user pastes the callback URL from their browser's address bar.
|
|
11
|
+
*
|
|
12
|
+
* Security:
|
|
13
|
+
* - Callback server binds to 127.0.0.1 only (loopback)
|
|
14
|
+
* - State parameter validated on every callback (CSRF protection)
|
|
15
|
+
* - 5-minute timeout prevents abandoned login sessions
|
|
16
|
+
*
|
|
17
|
+
* @module auth/callback-server
|
|
18
|
+
*/
|
|
19
|
+
import { createServer } from 'node:http';
|
|
20
|
+
import { existsSync } from 'node:fs';
|
|
21
|
+
import { URL } from 'node:url';
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Constants
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
/** Login flow timeout — 5 minutes to complete browser authentication */
|
|
26
|
+
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
27
|
+
/**
|
|
28
|
+
* Fixed redirect URI used in manual (no-browser) mode.
|
|
29
|
+
*
|
|
30
|
+
* The port number is arbitrary — nothing actually listens on it. The user's
|
|
31
|
+
* browser will fail to load this page after authentication, but the full URL
|
|
32
|
+
* (including the authorization code in the query string) will be visible in
|
|
33
|
+
* the browser's address bar for the user to copy and paste back.
|
|
34
|
+
*
|
|
35
|
+
* Per RFC 8252 §7.3, node-oidc-provider allows flexible port matching for
|
|
36
|
+
* native clients using loopback redirect URIs, so any port works as long as
|
|
37
|
+
* the base pattern (`http://127.0.0.1/callback`) is registered.
|
|
38
|
+
*/
|
|
39
|
+
export const MANUAL_REDIRECT_URI = 'http://127.0.0.1:11111/callback';
|
|
40
|
+
/**
|
|
41
|
+
* Start a temporary HTTP server to receive the OAuth callback.
|
|
42
|
+
*
|
|
43
|
+
* Binds to 127.0.0.1 on a random available port. Validates the
|
|
44
|
+
* state parameter and extracts the authorization code from the
|
|
45
|
+
* callback URL query parameters.
|
|
46
|
+
*
|
|
47
|
+
* The server automatically shuts down after receiving a callback
|
|
48
|
+
* (success or error) or after the 5-minute timeout.
|
|
49
|
+
*
|
|
50
|
+
* @param expectedState - The state parameter to validate against
|
|
51
|
+
* @returns Object with the assigned port and a promise that resolves with the auth code
|
|
52
|
+
*/
|
|
53
|
+
export function startCallbackServer(expectedState) {
|
|
54
|
+
return new Promise((resolveSetup) => {
|
|
55
|
+
let resolveCode;
|
|
56
|
+
let rejectCode;
|
|
57
|
+
// Promise that resolves when the callback is received with a valid code
|
|
58
|
+
const authCode = new Promise((resolve, reject) => {
|
|
59
|
+
resolveCode = resolve;
|
|
60
|
+
rejectCode = reject;
|
|
61
|
+
});
|
|
62
|
+
const server = createServer((req, res) => {
|
|
63
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
64
|
+
// Only handle the /callback path — reject anything else
|
|
65
|
+
if (url.pathname !== '/callback') {
|
|
66
|
+
res.writeHead(404);
|
|
67
|
+
res.end('Not Found');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const code = url.searchParams.get('code');
|
|
71
|
+
const returnedState = url.searchParams.get('state');
|
|
72
|
+
const errorParam = url.searchParams.get('error');
|
|
73
|
+
// Case 1: OIDC provider returned an error (user cancelled, etc.)
|
|
74
|
+
if (errorParam) {
|
|
75
|
+
const desc = url.searchParams.get('error_description') || errorParam;
|
|
76
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
77
|
+
res.end('<html><body><h1>Login Failed</h1>' +
|
|
78
|
+
'<p>You can close this tab.</p></body></html>');
|
|
79
|
+
server.close();
|
|
80
|
+
rejectCode(new Error(`Authentication failed: ${desc}`));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Case 2: State mismatch — possible CSRF attack
|
|
84
|
+
if (returnedState !== expectedState) {
|
|
85
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
86
|
+
res.end('<html><body><h1>Security Error</h1>' +
|
|
87
|
+
'<p>State mismatch. Please try again.</p></body></html>');
|
|
88
|
+
server.close();
|
|
89
|
+
rejectCode(new Error('Security error: state mismatch. Login aborted.'));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// Case 3: No authorization code in the callback
|
|
93
|
+
if (!code) {
|
|
94
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
95
|
+
res.end('<html><body><h1>Error</h1>' +
|
|
96
|
+
'<p>No authorization code received.</p></body></html>');
|
|
97
|
+
server.close();
|
|
98
|
+
rejectCode(new Error('No authorization code received'));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// Case 4: Success — valid code and matching state
|
|
102
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
103
|
+
res.end('<html><body><h1>Login Successful</h1>' +
|
|
104
|
+
'<p>You can close this tab and return to the terminal.</p></body></html>');
|
|
105
|
+
server.close();
|
|
106
|
+
resolveCode(code);
|
|
107
|
+
});
|
|
108
|
+
// Timeout: abort the login after 5 minutes of inactivity
|
|
109
|
+
const timeout = setTimeout(() => {
|
|
110
|
+
server.close();
|
|
111
|
+
rejectCode(new Error('Login timed out after 5 minutes'));
|
|
112
|
+
}, LOGIN_TIMEOUT_MS);
|
|
113
|
+
// Unref the timeout so it doesn't keep the Node.js process alive
|
|
114
|
+
timeout.unref();
|
|
115
|
+
// Listen on random available port, loopback interface only
|
|
116
|
+
server.listen(0, '127.0.0.1', () => {
|
|
117
|
+
const addr = server.address();
|
|
118
|
+
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
119
|
+
resolveSetup({
|
|
120
|
+
port,
|
|
121
|
+
authCode,
|
|
122
|
+
close: () => server.close(),
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Manual URL Parsing (Docker / headless mode)
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
/**
|
|
131
|
+
* Parse the authorization code and state from a pasted callback URL.
|
|
132
|
+
*
|
|
133
|
+
* In manual mode the user pastes the full URL from their browser's address
|
|
134
|
+
* bar after authentication. This function extracts the `code` and `state`
|
|
135
|
+
* query parameters and validates the state against the expected value.
|
|
136
|
+
*
|
|
137
|
+
* @param pastedUrl - The full callback URL pasted by the user
|
|
138
|
+
* @param expectedState - The state value to validate against (CSRF protection)
|
|
139
|
+
* @returns The authorization code extracted from the URL
|
|
140
|
+
* @throws Error if the URL is malformed, state mismatches, or code is missing
|
|
141
|
+
*/
|
|
142
|
+
export function parseCallbackUrl(pastedUrl, expectedState) {
|
|
143
|
+
let url;
|
|
144
|
+
try {
|
|
145
|
+
url = new URL(pastedUrl.trim());
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
throw new Error('Invalid URL. Please copy the full URL from your browser\'s address bar.');
|
|
149
|
+
}
|
|
150
|
+
// Check for OIDC error response in the callback URL
|
|
151
|
+
const errorParam = url.searchParams.get('error');
|
|
152
|
+
if (errorParam) {
|
|
153
|
+
const desc = url.searchParams.get('error_description') || errorParam;
|
|
154
|
+
throw new Error(`Authentication failed: ${desc}`);
|
|
155
|
+
}
|
|
156
|
+
// Validate state parameter (CSRF protection)
|
|
157
|
+
const returnedState = url.searchParams.get('state');
|
|
158
|
+
if (returnedState !== expectedState) {
|
|
159
|
+
throw new Error('Security error: state mismatch. Login aborted.');
|
|
160
|
+
}
|
|
161
|
+
// Extract the authorization code
|
|
162
|
+
const code = url.searchParams.get('code');
|
|
163
|
+
if (!code) {
|
|
164
|
+
throw new Error('No authorization code found in the URL.');
|
|
165
|
+
}
|
|
166
|
+
return code;
|
|
167
|
+
}
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Environment Detection
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
/**
|
|
172
|
+
* Detect whether the process is running inside a Docker container.
|
|
173
|
+
*
|
|
174
|
+
* Checks for the `/.dockerenv` sentinel file that Docker creates in every
|
|
175
|
+
* container. Also honours the `PORTA_CONTAINER` environment variable so
|
|
176
|
+
* users can force manual mode in other containerized runtimes (Podman,
|
|
177
|
+
* Kubernetes, etc.) by setting `PORTA_CONTAINER=1`.
|
|
178
|
+
*
|
|
179
|
+
* @returns true if running inside a container
|
|
180
|
+
*/
|
|
181
|
+
export function isContainerized() {
|
|
182
|
+
// Explicit override via environment variable
|
|
183
|
+
if (process.env.PORTA_CONTAINER === '1')
|
|
184
|
+
return true;
|
|
185
|
+
// Docker sentinel file — present in every Docker container
|
|
186
|
+
try {
|
|
187
|
+
return existsSync('/.dockerenv');
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=callback-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"callback-server.js","sourceRoot":"","sources":["../../src/auth/callback-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,YAAY,EAAe,MAAM,WAAW,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E;AAE9E,wEAAwE;AACxE,MAAM,gBAAgB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAEvC;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,iCAAiC,CAAC;AAkBrE;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,mBAAmB,CACjC,aAAqB;IAErB,OAAO,IAAI,OAAO,CAAC,CAAC,YAAY,EAAE,EAAE;QAClC,IAAI,WAAmC,CAAC;QACxC,IAAI,UAAgC,CAAC;QAErC,wEAAwE;QACxE,MAAM,QAAQ,GAAG,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACvD,WAAW,GAAG,OAAO,CAAC;YACtB,UAAU,GAAG,MAAM,CAAC;QACtB,CAAC,CAAC,CAAC;QAEH,MAAM,MAAM,GAAW,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YAC/C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,kBAAkB,CAAC,CAAC;YAExD,wDAAwD;YACxD,IAAI,GAAG,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;gBACjC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;gBACnB,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;gBACrB,OAAO;YACT,CAAC;YAED,MAAM,IAAI,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YAC1C,MAAM,aAAa,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACpD,MAAM,UAAU,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YAEjD,iEAAiE;YACjE,IAAI,UAAU,EAAE,CAAC;gBACf,MAAM,IAAI,GACR,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,mBAAmB,CAAC,IAAI,UAAU,CAAC;gBAC1D,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;gBACpD,GAAG,CAAC,GAAG,CACL,mCAAmC;oBACjC,8CAA8C,CACjD,CAAC;gBACF,MAAM,CAAC,KAAK,EAAE,CAAC;gBACf,UAAU,CAAC,IAAI,KAAK,CAAC,0BAA0B,IAAI,EAAE,CAAC,CAAC,CAAC;gBACxD,OAAO;YACT,CAAC;YAED,gDAAgD;YAChD,IAAI,aAAa,KAAK,aAAa,EAAE,CAAC;gBACpC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;gBACpD,GAAG,CAAC,GAAG,CACL,qCAAqC;oBACnC,wDAAwD,CAC3D,CAAC;gBACF,MAAM,CAAC,KAAK,EAAE,CAAC;gBACf,UAAU,CACR,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAC5D,CAAC;gBACF,OAAO;YACT,CAAC;YAED,gDAAgD;YAChD,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;gBACpD,GAAG,CAAC,GAAG,CACL,4BAA4B;oBAC1B,sDAAsD,CACzD,CAAC;gBACF,MAAM,CAAC,KAAK,EAAE,CAAC;gBACf,UAAU,CAAC,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC,CAAC;gBACxD,OAAO;YACT,CAAC;YAED,kDAAkD;YAClD,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;YACpD,GAAG,CAAC,GAAG,CACL,uCAAuC;gBACrC,yEAAyE,CAC5E,CAAC;YACF,MAAM,CAAC,KAAK,EAAE,CAAC;YACf,WAAW,CAAC,IAAI,CAAC,CAAC;QACpB,CAAC,CAAC,CAAC;QAEH,yDAAyD;QACzD,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;YAC9B,MAAM,CAAC,KAAK,EAAE,CAAC;YACf,UAAU,CAAC,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC,CAAC;QAC3D,CAAC,EAAE,gBAAgB,CAAC,CAAC;QAErB,iEAAiE;QACjE,OAAO,CAAC,KAAK,EAAE,CAAC;QAEhB,2DAA2D;QAC3D,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE;YACjC,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;YAC9B,MAAM,IAAI,GAAG,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;YAC9D,YAAY,CAAC;gBACX,IAAI;gBACJ,QAAQ;gBACR,KAAK,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE;aAC5B,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,8EAA8E;AAC9E,8CAA8C;AAC9C,8EAA8E;AAE9E;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,gBAAgB,CAC9B,SAAiB,EACjB,aAAqB;IAErB,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,yEAAyE,CAC1E,CAAC;IACJ,CAAC;IAED,oDAAoD;IACpD,MAAM,UAAU,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACjD,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,IAAI,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,mBAAmB,CAAC,IAAI,UAAU,CAAC;QACrE,MAAM,IAAI,KAAK,CAAC,0BAA0B,IAAI,EAAE,CAAC,CAAC;IACpD,CAAC;IAED,6CAA6C;IAC7C,MAAM,aAAa,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACpD,IAAI,aAAa,KAAK,aAAa,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACpE,CAAC;IAED,iCAAiC;IACjC,MAAM,IAAI,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC1C,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC7D,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,8EAA8E;AAC9E,wBAAwB;AACxB,8EAA8E;AAE9E;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe;IAC7B,6CAA6C;IAC7C,IAAI,OAAO,CAAC,GAAG,CAAC,eAAe,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAErD,2DAA2D;IAC3D,IAAI,CAAC;QACH,OAAO,UAAU,CAAC,aAAa,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin metadata and server discovery.
|
|
3
|
+
*
|
|
4
|
+
* Fetches server metadata from the unauthenticated endpoint
|
|
5
|
+
* `GET /api/admin/metadata` to discover the OIDC client_id,
|
|
6
|
+
* issuer URL, and organization slug needed for the login flow.
|
|
7
|
+
*
|
|
8
|
+
* Also provides a health check fetch for the `doctor` command.
|
|
9
|
+
*
|
|
10
|
+
* @module auth/metadata
|
|
11
|
+
*/
|
|
12
|
+
import type { AdminMetadata } from './types.js';
|
|
13
|
+
/**
|
|
14
|
+
* Fetch admin metadata from the Porta server.
|
|
15
|
+
*
|
|
16
|
+
* Calls `GET /api/admin/metadata` — an unauthenticated endpoint
|
|
17
|
+
* that only exposes public info needed to initiate the login flow.
|
|
18
|
+
*
|
|
19
|
+
* @param server - Porta server base URL (e.g., "https://porta.local:3443")
|
|
20
|
+
* @returns Admin metadata (issuer, clientId, orgSlug)
|
|
21
|
+
* @throws Error if the server is not reachable or not initialized
|
|
22
|
+
*/
|
|
23
|
+
export declare function fetchAdminMetadata(server: string): Promise<AdminMetadata>;
|
|
24
|
+
/**
|
|
25
|
+
* Health check response from `GET /health`.
|
|
26
|
+
*/
|
|
27
|
+
export interface HealthResponse {
|
|
28
|
+
/** Overall status */
|
|
29
|
+
status: string;
|
|
30
|
+
/** Individual service statuses */
|
|
31
|
+
services?: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Check server health via `GET /health`.
|
|
35
|
+
*
|
|
36
|
+
* This is an unauthenticated endpoint — no credentials needed.
|
|
37
|
+
* Used by the `doctor` command to verify server connectivity.
|
|
38
|
+
*
|
|
39
|
+
* @param server - Porta server base URL
|
|
40
|
+
* @returns Health response or null if server is unreachable
|
|
41
|
+
*/
|
|
42
|
+
export declare function fetchHealthStatus(server: string): Promise<HealthResponse | null>;
|
|
43
|
+
//# sourceMappingURL=metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../../src/auth/metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAMhD;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,aAAa,CAAC,CAsBxB;AAMD;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,qBAAqB;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACnC;AAED;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAahC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin metadata and server discovery.
|
|
3
|
+
*
|
|
4
|
+
* Fetches server metadata from the unauthenticated endpoint
|
|
5
|
+
* `GET /api/admin/metadata` to discover the OIDC client_id,
|
|
6
|
+
* issuer URL, and organization slug needed for the login flow.
|
|
7
|
+
*
|
|
8
|
+
* Also provides a health check fetch for the `doctor` command.
|
|
9
|
+
*
|
|
10
|
+
* @module auth/metadata
|
|
11
|
+
*/
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Metadata Fetch
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
/**
|
|
16
|
+
* Fetch admin metadata from the Porta server.
|
|
17
|
+
*
|
|
18
|
+
* Calls `GET /api/admin/metadata` — an unauthenticated endpoint
|
|
19
|
+
* that only exposes public info needed to initiate the login flow.
|
|
20
|
+
*
|
|
21
|
+
* @param server - Porta server base URL (e.g., "https://porta.local:3443")
|
|
22
|
+
* @returns Admin metadata (issuer, clientId, orgSlug)
|
|
23
|
+
* @throws Error if the server is not reachable or not initialized
|
|
24
|
+
*/
|
|
25
|
+
export async function fetchAdminMetadata(server) {
|
|
26
|
+
let response;
|
|
27
|
+
try {
|
|
28
|
+
response = await fetch(`${server}/api/admin/metadata`, {
|
|
29
|
+
signal: AbortSignal.timeout(10_000), // 10s timeout
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new Error(`Cannot connect to ${server}. Is the server running?`);
|
|
34
|
+
}
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
if (response.status === 503) {
|
|
37
|
+
throw new Error('Server not initialized. Run "porta init" on the server first.');
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`Cannot fetch admin metadata: HTTP ${response.status}`);
|
|
40
|
+
}
|
|
41
|
+
return response.json();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Check server health via `GET /health`.
|
|
45
|
+
*
|
|
46
|
+
* This is an unauthenticated endpoint — no credentials needed.
|
|
47
|
+
* Used by the `doctor` command to verify server connectivity.
|
|
48
|
+
*
|
|
49
|
+
* @param server - Porta server base URL
|
|
50
|
+
* @returns Health response or null if server is unreachable
|
|
51
|
+
*/
|
|
52
|
+
export async function fetchHealthStatus(server) {
|
|
53
|
+
try {
|
|
54
|
+
const response = await fetch(`${server}/health`, {
|
|
55
|
+
signal: AbortSignal.timeout(5_000), // 5s timeout
|
|
56
|
+
});
|
|
57
|
+
if (response.ok) {
|
|
58
|
+
return response.json();
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=metadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.js","sourceRoot":"","sources":["../../src/auth/metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,MAAc;IAEd,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,MAAM,qBAAqB,EAAE;YACrD,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,cAAc;SACpD,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,qBAAqB,MAAM,0BAA0B,CACtD,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC5B,MAAM,IAAI,KAAK,CACb,+DAA+D,CAChE,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,qCAAqC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IAC1E,CAAC;IAED,OAAO,QAAQ,CAAC,IAAI,EAA4B,CAAC;AACnD,CAAC;AAgBD;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,MAAc;IAEd,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,MAAM,SAAS,EAAE;YAC/C,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,aAAa;SAClD,CAAC,CAAC;QAEH,IAAI,QAAQ,CAAC,EAAE,EAAE,CAAC;YAChB,OAAO,QAAQ,CAAC,IAAI,EAA6B,CAAC;QACpD,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|