@nuvlore/extension-access-pagerduty 0.1.1
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/LICENSE +202 -0
- package/README.md +3 -0
- package/package.json +52 -0
- package/tools/pagerduty_sso_incident.mjs +32 -0
- package/tools/pagerduty_sso_incidents.mjs +36 -0
- package/tools/pagerduty_sso_oncalls.mjs +36 -0
- package/tools/pagerduty_sso_read.mjs +34 -0
- package/tools/pagerduty_sso_services.mjs +33 -0
- package/tools/pagerduty_sso_verify.mjs +26 -0
- package/vendor/extension-sso/LICENSE +202 -0
- package/vendor/extension-sso/README.md +35 -0
- package/vendor/extension-sso/commands/okta-login.md +10 -0
- package/vendor/extension-sso/package.json +51 -0
- package/vendor/extension-sso/skills/workday-okta-auth/SKILL.md +20 -0
- package/vendor/extension-sso/src/autoAuth.mjs +25 -0
- package/vendor/extension-sso/src/browserLogin.mjs +1 -0
- package/vendor/extension-sso/src/chromeCookies.mjs +1 -0
- package/vendor/extension-sso/src/cookieJar.mjs +1 -0
- package/vendor/extension-sso/src/extensionAccess.mjs +173 -0
- package/vendor/extension-sso/src/htmlSessionExtract.mjs +1 -0
- package/vendor/extension-sso/src/oktaAppAccess.mjs +133 -0
- package/vendor/extension-sso/src/oktaAppCatalog.mjs +118 -0
- package/vendor/extension-sso/src/oktaAppRead.mjs +119 -0
- package/vendor/extension-sso/src/oktaConfig.mjs +12 -0
- package/vendor/extension-sso/src/oktaSession.mjs +32 -0
- package/vendor/extension-sso/src/serviceOperations.mjs +407 -0
- package/vendor/extension-sso/src/serviceReadSearch.mjs +395 -0
- package/vendor/extension-sso/src/serviceReads.mjs +109 -0
- package/vendor/extension-sso/src/serviceRegistry.mjs +497 -0
- package/vendor/extension-sso/src/sessionAuth.mjs +339 -0
- package/vendor/extension-sso/src/sessionManager.mjs +212 -0
- package/vendor/extension-sso/src/sessionValidate.mjs +74 -0
- package/vendor/extension-sso/src/ssoConfig.mjs +31 -0
- package/vendor/extension-sso/src/ssoHttpRead.mjs +292 -0
- package/vendor/extension-sso/tools/extension-access-tools.shared.mjs +16 -0
- package/vendor/extension-sso/tools/okta-app-tools.shared.mjs +5 -0
- package/vendor/extension-sso/tools/okta-tools.shared.mjs +18 -0
- package/vendor/extension-sso/tools/workday_extension_read.mjs +51 -0
- package/vendor/extension-sso/tools/workday_extension_read_access.mjs +65 -0
- package/vendor/extension-sso/tools/workday_list_extension_access.mjs +39 -0
- package/vendor/extension-sso/tools/workday_list_okta_apps.mjs +18 -0
- package/vendor/extension-sso/tools/workday_list_service_operations.mjs +41 -0
- package/vendor/extension-sso/tools/workday_list_service_read_search_capabilities.mjs +34 -0
- package/vendor/extension-sso/tools/workday_list_services.mjs +31 -0
- package/vendor/extension-sso/tools/workday_okta_app_probe_all.mjs +24 -0
- package/vendor/extension-sso/tools/workday_okta_app_read.mjs +30 -0
- package/vendor/extension-sso/tools/workday_okta_app_read_access.mjs +25 -0
- package/vendor/extension-sso/tools/workday_okta_auth_header.mjs +58 -0
- package/vendor/extension-sso/tools/workday_okta_login.mjs +65 -0
- package/vendor/extension-sso/tools/workday_okta_open_app.mjs +63 -0
- package/vendor/extension-sso/tools/workday_okta_open_service.mjs +63 -0
- package/vendor/extension-sso/tools/workday_okta_status.mjs +35 -0
- package/vendor/extension-sso/tools/workday_service_auth.mjs +65 -0
- package/vendor/extension-sso/tools/workday_service_operation_read.mjs +51 -0
- package/vendor/extension-sso/tools/workday_service_read.mjs +56 -0
- package/vendor/extension-sso/tools/workday_service_search.mjs +62 -0
- package/vendor/extension-sso/tools/workday_sso_cookie_header.mjs +59 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/LICENSE +202 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/README.md +16 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/package.json +47 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/autoAuth.mjs +47 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/browserHttpRead.mjs +76 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/browserLogin.mjs +24 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/chromeCookies.mjs +192 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/cookieJar.mjs +132 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/devops-data-paths.mjs +222 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/devops-diagnostics.mjs +440 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/enterprise-api-read.mjs +180 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/read-only-command-pack.mjs +1 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/read-only-plan.mjs +45 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/read-only-shell-policy.d.mts +26 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/read-only-shell-policy.mjs +170 -0
- package/vendor/extension-sso/vendor/extension-browser-sso/src/htmlSessionExtract.mjs +64 -0
- package/vendor/extension-sso/vendor/extension-read-only-toolkit/LICENSE +202 -0
- package/vendor/extension-sso/vendor/extension-read-only-toolkit/README.md +29 -0
- package/vendor/extension-sso/vendor/extension-read-only-toolkit/package.json +43 -0
- package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/devops-data-paths.mjs +222 -0
- package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/devops-diagnostics.mjs +440 -0
- package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/enterprise-api-read.mjs +180 -0
- package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/read-only-command-pack.mjs +1 -0
- package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/read-only-plan.mjs +45 -0
- package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/read-only-shell-policy.d.mts +26 -0
- package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/read-only-shell-policy.mjs +170 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cookiesForHost,
|
|
3
|
+
jarSummary,
|
|
4
|
+
loadCookieJar,
|
|
5
|
+
mergeCookies,
|
|
6
|
+
resolveBestCookieHost,
|
|
7
|
+
saveCookieJar
|
|
8
|
+
} from "./cookieJar.mjs";
|
|
9
|
+
import { openBrowser, waitFor } from "./browserLogin.mjs";
|
|
10
|
+
|
|
11
|
+
export { openBrowser };
|
|
12
|
+
import {
|
|
13
|
+
isAutoAuthEnabled,
|
|
14
|
+
resolveAutoAuthOptions
|
|
15
|
+
} from "./autoAuth.mjs";
|
|
16
|
+
|
|
17
|
+
export { isAutoAuthEnabled, resolveAutoAuthOptions };
|
|
18
|
+
import {
|
|
19
|
+
ensureSsoSession,
|
|
20
|
+
importCookiesFromChrome,
|
|
21
|
+
syncOktaAppCookies
|
|
22
|
+
} from "./sessionManager.mjs";
|
|
23
|
+
import { getSsoConfig } from "./ssoConfig.mjs";
|
|
24
|
+
import { getServiceDomains, getWorkdayService } from "./serviceRegistry.mjs";
|
|
25
|
+
import { getOktaApp } from "./oktaAppCatalog.mjs";
|
|
26
|
+
import { validateOktaAppHost, validateServiceHost } from "./sessionValidate.mjs";
|
|
27
|
+
|
|
28
|
+
function hasCookiesForDomains(jar, domains) {
|
|
29
|
+
if (!domains.length) return true;
|
|
30
|
+
return domains.some((domain) => cookiesForHost(jar?.cookies ?? [], domain).length > 0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function mergeImportedCookies(config, jar, domains) {
|
|
34
|
+
const imported = await importCookiesFromChrome(config, [config.domain, ...domains]);
|
|
35
|
+
if (!imported?.cookies?.length) return jar;
|
|
36
|
+
const next = {
|
|
37
|
+
...(jar ?? {}),
|
|
38
|
+
cookies: mergeCookies(jar?.cookies ?? [], imported.cookies),
|
|
39
|
+
source: imported.source ?? "chrome"
|
|
40
|
+
};
|
|
41
|
+
await saveCookieJar(config.cookieJarPath, next);
|
|
42
|
+
return next;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function waitForChromeCookies(
|
|
46
|
+
config,
|
|
47
|
+
domains,
|
|
48
|
+
{ timeoutMs, pollMs = 2000, onStatus, label } = {}
|
|
49
|
+
) {
|
|
50
|
+
const deadline = Date.now() + (timeoutMs ?? config.loginTimeoutMs);
|
|
51
|
+
let jar = await loadCookieJar(config.cookieJarPath);
|
|
52
|
+
while (Date.now() < deadline) {
|
|
53
|
+
onStatus?.({ phase: "waiting_for_app_cookies", label, domains });
|
|
54
|
+
jar = await mergeImportedCookies(config, jar, domains);
|
|
55
|
+
if (hasCookiesForDomains(jar, domains)) {
|
|
56
|
+
return { jar, ready: true };
|
|
57
|
+
}
|
|
58
|
+
await waitFor(pollMs);
|
|
59
|
+
}
|
|
60
|
+
return { jar, ready: hasCookiesForDomains(jar, domains) };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function openAuthPageAndWait(config, { authUrl, domains, label, options }) {
|
|
64
|
+
options.onStatus?.({ phase: "opening_auth", label, url: authUrl });
|
|
65
|
+
await openBrowser(authUrl, { chromeProfile: config.chromeProfile });
|
|
66
|
+
const waited = await waitForChromeCookies(config, domains, {
|
|
67
|
+
timeoutMs: options.appAuthTimeoutMs,
|
|
68
|
+
onStatus: options.onStatus,
|
|
69
|
+
label
|
|
70
|
+
});
|
|
71
|
+
return { jar: waited.jar, ready: waited.ready, openedBrowser: true };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function resolveServiceAuthUrl(serviceId) {
|
|
75
|
+
const service = getWorkdayService(serviceId);
|
|
76
|
+
const domains = getServiceDomains(serviceId);
|
|
77
|
+
return service.readAccess?.authUrl || service.readAccess?.defaultBaseUrl || (domains[0] ? `https://${domains[0]}` : getSsoConfig().loginUrl);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function resolveOktaAppAuthUrl(appName) {
|
|
81
|
+
const app = getOktaApp(appName);
|
|
82
|
+
if (app.mode === "bookmark" || app.mode === "oidc-app") {
|
|
83
|
+
return getSsoConfig().loginUrl;
|
|
84
|
+
}
|
|
85
|
+
return app.agentsUrl || app.defaultBaseUrl || getSsoConfig().loginUrl;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Open Chrome at the Okta app URL so the user can sign in (read-only). */
|
|
89
|
+
export async function openOktaAppForAuth(appName, options = {}) {
|
|
90
|
+
const config = options.config ?? getSsoConfig();
|
|
91
|
+
const app = getOktaApp(appName);
|
|
92
|
+
const authUrl = resolveOktaAppAuthUrl(appName);
|
|
93
|
+
const domains = app.domains ?? [];
|
|
94
|
+
let openedBrowser = false;
|
|
95
|
+
|
|
96
|
+
if (app.launchViaUserHome && options.skipUserHome !== true) {
|
|
97
|
+
options.onStatus?.({
|
|
98
|
+
phase: "opening_okta_userhome",
|
|
99
|
+
oktaApp: appName,
|
|
100
|
+
url: config.loginUrl,
|
|
101
|
+
hint:
|
|
102
|
+
appName === "Sana"
|
|
103
|
+
? 'On workday.sana.ai click "Continue with SSO" to finish login'
|
|
104
|
+
: `Click "${appName}" on UserHome if SSO does not auto-launch`
|
|
105
|
+
});
|
|
106
|
+
await openBrowser(config.loginUrl, { chromeProfile: config.chromeProfile });
|
|
107
|
+
openedBrowser = true;
|
|
108
|
+
if (options.userHomeDelayMs !== 0) {
|
|
109
|
+
await waitFor(options.userHomeDelayMs ?? 2500);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const result = await openAuthPageAndWait(config, {
|
|
114
|
+
authUrl,
|
|
115
|
+
domains: domains.length ? domains : [config.domain],
|
|
116
|
+
label: appName,
|
|
117
|
+
options
|
|
118
|
+
});
|
|
119
|
+
return { authUrl, userHomeUrl: app.launchViaUserHome ? config.loginUrl : undefined, openedBrowser: openedBrowser || result.openedBrowser, ...result };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Open Chrome at the Workday service URL so the user can sign in via Okta (read-only). */
|
|
123
|
+
export async function openServiceForAuth(serviceId, options = {}) {
|
|
124
|
+
const config = options.config ?? getSsoConfig();
|
|
125
|
+
const domains = getServiceDomains(serviceId);
|
|
126
|
+
const authUrl = resolveServiceAuthUrl(serviceId);
|
|
127
|
+
const result = await openAuthPageAndWait(config, {
|
|
128
|
+
authUrl,
|
|
129
|
+
domains: domains.length ? domains : [config.domain],
|
|
130
|
+
label: serviceId,
|
|
131
|
+
options
|
|
132
|
+
});
|
|
133
|
+
let serviceReady = false;
|
|
134
|
+
for (const domain of domains) {
|
|
135
|
+
if (await validateServiceHost(result.jar, domain).catch(() => false)) {
|
|
136
|
+
serviceReady = true;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { authUrl, openedBrowser: true, ...result, ready: serviceReady, serviceReady };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function getServiceCookieHeader(serviceId, options = {}) {
|
|
144
|
+
const authOptions = resolveAutoAuthOptions(options);
|
|
145
|
+
const config = authOptions.config ?? getSsoConfig();
|
|
146
|
+
const session = await ensureServiceSession(serviceId, { ...authOptions, config });
|
|
147
|
+
const domains = getServiceDomains(serviceId);
|
|
148
|
+
const { host, cookie, hasCookies } = resolveBestCookieHost(session.jar, domains);
|
|
149
|
+
|
|
150
|
+
if (!host || !session.jar?.cookies?.length || !hasCookies) {
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
summary: session.summary,
|
|
154
|
+
host,
|
|
155
|
+
domains,
|
|
156
|
+
serviceReady: session.serviceReady,
|
|
157
|
+
openedBrowser: session.openedBrowser,
|
|
158
|
+
authUrl: session.authUrl,
|
|
159
|
+
message: `No cookies for ${serviceId}. Open ${session.authUrl} in Chrome via Okta, then retry.`
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const ready = session.summary.authenticated && Boolean(cookie) && session.serviceReady;
|
|
164
|
+
return {
|
|
165
|
+
ok: ready,
|
|
166
|
+
summary: session.summary,
|
|
167
|
+
host,
|
|
168
|
+
cookie,
|
|
169
|
+
domains,
|
|
170
|
+
serviceReady: session.serviceReady,
|
|
171
|
+
openedBrowser: session.openedBrowser,
|
|
172
|
+
authUrl: session.authUrl
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function ensureServiceSession(serviceId, options = {}) {
|
|
177
|
+
const authOptions = resolveAutoAuthOptions(options);
|
|
178
|
+
const config = authOptions.config ?? getSsoConfig();
|
|
179
|
+
const { autoAuth, openBrowserOnMissing } = authOptions;
|
|
180
|
+
const domains = getServiceDomains(serviceId);
|
|
181
|
+
const authUrl = resolveServiceAuthUrl(serviceId);
|
|
182
|
+
let openedBrowser = false;
|
|
183
|
+
|
|
184
|
+
let { jar, summary, openedBrowser: oktaOpened } = await ensureSsoSession({
|
|
185
|
+
config,
|
|
186
|
+
serviceId,
|
|
187
|
+
openBrowserOnMissing,
|
|
188
|
+
forceBrowser: authOptions.forceBrowser === true,
|
|
189
|
+
onStatus: authOptions.onStatus
|
|
190
|
+
});
|
|
191
|
+
summary = summary ?? jarSummary(jar ?? {}, config);
|
|
192
|
+
openedBrowser = openedBrowser || oktaOpened;
|
|
193
|
+
|
|
194
|
+
jar = await mergeImportedCookies(config, jar, domains);
|
|
195
|
+
let hasServiceCookies = hasCookiesForDomains(jar, domains);
|
|
196
|
+
let serviceReady = false;
|
|
197
|
+
if (summary.authenticated && hasServiceCookies) {
|
|
198
|
+
for (const domain of domains) {
|
|
199
|
+
if (await validateServiceHost(jar, domain).catch(() => false)) {
|
|
200
|
+
serviceReady = true;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const needsAuth =
|
|
207
|
+
openBrowserOnMissing && (!hasServiceCookies || (hasServiceCookies && !serviceReady));
|
|
208
|
+
|
|
209
|
+
if (needsAuth) {
|
|
210
|
+
authOptions.onStatus?.({ phase: "opening_service_auth", service: serviceId, url: authUrl });
|
|
211
|
+
const auth = await openAuthPageAndWait(config, {
|
|
212
|
+
authUrl,
|
|
213
|
+
domains,
|
|
214
|
+
label: serviceId,
|
|
215
|
+
options: authOptions
|
|
216
|
+
});
|
|
217
|
+
jar = auth.jar;
|
|
218
|
+
openedBrowser = openedBrowser || auth.openedBrowser;
|
|
219
|
+
hasServiceCookies = hasCookiesForDomains(jar, domains);
|
|
220
|
+
if (!auth.ready && !hasServiceCookies) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
`Service ${serviceId} auth timed out. Complete login in Chrome at ${authUrl}, then retry.`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
summary = jarSummary(jar, config);
|
|
226
|
+
serviceReady = false;
|
|
227
|
+
if (summary.authenticated) {
|
|
228
|
+
for (const domain of domains) {
|
|
229
|
+
if (await validateServiceHost(jar, domain).catch(() => false)) {
|
|
230
|
+
serviceReady = true;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
jar,
|
|
239
|
+
summary,
|
|
240
|
+
openedBrowser,
|
|
241
|
+
serviceReady,
|
|
242
|
+
hasServiceCookies,
|
|
243
|
+
authUrl
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function ensureOktaAppSession(appName, options = {}) {
|
|
248
|
+
const authOptions = resolveAutoAuthOptions(options);
|
|
249
|
+
const config = authOptions.config ?? getSsoConfig();
|
|
250
|
+
const { openBrowserOnMissing } = authOptions;
|
|
251
|
+
const app = getOktaApp(appName);
|
|
252
|
+
const domains = app.domains ?? [];
|
|
253
|
+
const authUrl = resolveOktaAppAuthUrl(appName);
|
|
254
|
+
let openedBrowser = false;
|
|
255
|
+
|
|
256
|
+
if (app.mode === "cli-readonly") {
|
|
257
|
+
const { jar, summary } = await ensureSsoSession({
|
|
258
|
+
config,
|
|
259
|
+
openBrowserOnMissing,
|
|
260
|
+
onStatus: authOptions.onStatus
|
|
261
|
+
});
|
|
262
|
+
return { jar, summary, openedBrowser: false, appReady: true, authUrl };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let { jar, summary, openedBrowser: oktaOpened } = await ensureSsoSession({
|
|
266
|
+
config,
|
|
267
|
+
openBrowserOnMissing,
|
|
268
|
+
forceBrowser: authOptions.forceBrowser === true,
|
|
269
|
+
onStatus: authOptions.onStatus
|
|
270
|
+
});
|
|
271
|
+
openedBrowser = openedBrowser || oktaOpened;
|
|
272
|
+
|
|
273
|
+
await syncOktaAppCookies(appName, { config });
|
|
274
|
+
jar = await loadCookieJar(config.cookieJarPath);
|
|
275
|
+
let hasAppCookies = hasCookiesForDomains(jar, domains);
|
|
276
|
+
let appValidated =
|
|
277
|
+
!domains.length ||
|
|
278
|
+
app.mode === "bookmark" ||
|
|
279
|
+
app.mode === "oidc-app" ||
|
|
280
|
+
(hasAppCookies && (await validateOktaAppHost(jar, app, authOptions.fetchImpl).catch(() => false)));
|
|
281
|
+
|
|
282
|
+
const needsAuth = openBrowserOnMissing && domains.length > 0 && (!hasAppCookies || !appValidated);
|
|
283
|
+
|
|
284
|
+
if (needsAuth) {
|
|
285
|
+
authOptions.onStatus?.({ phase: "opening_okta_app_auth", oktaApp: appName, url: authUrl });
|
|
286
|
+
const auth = app.launchViaUserHome
|
|
287
|
+
? await openOktaAppForAuth(appName, { ...authOptions, config })
|
|
288
|
+
: await openAuthPageAndWait(config, {
|
|
289
|
+
authUrl,
|
|
290
|
+
domains,
|
|
291
|
+
label: appName,
|
|
292
|
+
options: authOptions
|
|
293
|
+
});
|
|
294
|
+
jar = auth.jar;
|
|
295
|
+
openedBrowser = openedBrowser || auth.openedBrowser;
|
|
296
|
+
hasAppCookies = hasCookiesForDomains(jar, domains);
|
|
297
|
+
appValidated = hasAppCookies && (await validateOktaAppHost(jar, app, authOptions.fetchImpl).catch(() => false));
|
|
298
|
+
if (!auth.ready && !hasAppCookies && app.mode !== "bookmark" && app.mode !== "oidc-app") {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`Okta app "${appName}" auth timed out. Complete login in Chrome at ${authUrl}, then retry.`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
summary = jarSummary(jar, config);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const appReady =
|
|
307
|
+
appValidated ||
|
|
308
|
+
app.mode === "bookmark" ||
|
|
309
|
+
app.mode === "oidc-app" ||
|
|
310
|
+
(domains.length === 0 && summary.authenticated);
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
jar,
|
|
314
|
+
summary,
|
|
315
|
+
openedBrowser,
|
|
316
|
+
appReady,
|
|
317
|
+
hasAppCookies,
|
|
318
|
+
authUrl
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export async function openAppsForAuth(appNames = [], options = {}) {
|
|
323
|
+
const config = options.config ?? getSsoConfig();
|
|
324
|
+
const opened = [];
|
|
325
|
+
await ensureSsoSession({
|
|
326
|
+
config,
|
|
327
|
+
openBrowserOnMissing: true,
|
|
328
|
+
forceBrowser: options.forceOkta === true,
|
|
329
|
+
onStatus: options.onStatus
|
|
330
|
+
});
|
|
331
|
+
for (const appName of appNames) {
|
|
332
|
+
const url = resolveOktaAppAuthUrl(appName);
|
|
333
|
+
options.onStatus?.({ phase: "opening_app", oktaApp: appName, url });
|
|
334
|
+
await openBrowser(url);
|
|
335
|
+
opened.push({ app: appName, url });
|
|
336
|
+
if (options.delayMs) await waitFor(options.delayMs);
|
|
337
|
+
}
|
|
338
|
+
return { opened, loginUrl: config.loginUrl };
|
|
339
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
buildCookieHeader,
|
|
5
|
+
cookiesForHost,
|
|
6
|
+
resolveBestCookieHost,
|
|
7
|
+
isValidationFresh,
|
|
8
|
+
jarSummary,
|
|
9
|
+
loadCookieJar,
|
|
10
|
+
mergeCookies,
|
|
11
|
+
saveCookieJar
|
|
12
|
+
} from "./cookieJar.mjs";
|
|
13
|
+
import { hostPatternsForDomains, readChromeCookies, resolveChromeCookiesPath } from "./chromeCookies.mjs";
|
|
14
|
+
import { openBrowser, waitFor } from "./browserLogin.mjs";
|
|
15
|
+
import { validateOktaSession, validateServiceHost } from "./sessionValidate.mjs";
|
|
16
|
+
import { getSsoConfig } from "./ssoConfig.mjs";
|
|
17
|
+
import { getServiceDomains } from "./serviceRegistry.mjs";
|
|
18
|
+
|
|
19
|
+
export async function importCookiesFromChrome(config = getSsoConfig(), domains = []) {
|
|
20
|
+
const patterns = hostPatternsForDomains([config.domain, ...domains]);
|
|
21
|
+
const cookiesPath = resolveChromeCookiesPath(config);
|
|
22
|
+
let cookies = await readChromeCookies(cookiesPath, patterns);
|
|
23
|
+
if (!cookies.length) {
|
|
24
|
+
cookies = await readChromeCookiesFromProfileFallbacks(config, patterns);
|
|
25
|
+
}
|
|
26
|
+
if (!cookies.length) return null;
|
|
27
|
+
return {
|
|
28
|
+
cookies,
|
|
29
|
+
authenticated: false,
|
|
30
|
+
source: "chrome"
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function readChromeCookiesFromProfileFallbacks(config, patterns) {
|
|
35
|
+
if (process.env.WORKDAY_CHROME_PROFILE_FALLBACK === "false") return [];
|
|
36
|
+
for (const cookiesPath of chromeCookieFallbackPaths(config)) {
|
|
37
|
+
const cookies = await readChromeCookies(cookiesPath, patterns);
|
|
38
|
+
if (cookies.length) return cookies;
|
|
39
|
+
}
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function chromeCookieFallbackPaths(config = getSsoConfig(), { readdir = readdirSync, exists = existsSync } = {}) {
|
|
44
|
+
const primary = config.chromeCookiesPath || resolveChromeCookiesPath(config);
|
|
45
|
+
const chromeRoot = dirname(dirname(primary));
|
|
46
|
+
let entries;
|
|
47
|
+
try {
|
|
48
|
+
entries = readdir(chromeRoot);
|
|
49
|
+
} catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
return entries
|
|
53
|
+
.filter((entry) => entry === "Default" || /^Profile \d+$/u.test(entry))
|
|
54
|
+
.map((entry) => join(chromeRoot, entry, "Cookies"))
|
|
55
|
+
.filter((cookiesPath) => cookiesPath !== primary && exists(cookiesPath));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function persistValidatedJar(config, jar) {
|
|
59
|
+
const authenticated = await validateOktaSession(jar, config);
|
|
60
|
+
const next = {
|
|
61
|
+
...jar,
|
|
62
|
+
authenticated,
|
|
63
|
+
validatedAt: authenticated ? new Date().toISOString() : jar.validatedAt
|
|
64
|
+
};
|
|
65
|
+
await saveCookieJar(config.cookieJarPath, next);
|
|
66
|
+
return next;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function hasCookiesForDomains(jar, domains) {
|
|
70
|
+
if (!domains.length) return true;
|
|
71
|
+
return domains.some((domain) => cookiesForHost(jar?.cookies ?? [], domain).length > 0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function importAndMergeJar(config, jar, domains, { validate = true } = {}) {
|
|
75
|
+
const imported = await importCookiesFromChrome(config, [config.domain, ...domains]);
|
|
76
|
+
if (!imported) return jar;
|
|
77
|
+
const next = {
|
|
78
|
+
...(jar ?? {}),
|
|
79
|
+
cookies: mergeCookies(jar?.cookies ?? [], imported.cookies),
|
|
80
|
+
source: imported.source
|
|
81
|
+
};
|
|
82
|
+
if (!validate) {
|
|
83
|
+
await saveCookieJar(config.cookieJarPath, next);
|
|
84
|
+
return next;
|
|
85
|
+
}
|
|
86
|
+
return persistValidatedJar(config, { ...next, authenticated: false });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function ensureSsoSession({
|
|
90
|
+
config = getSsoConfig(),
|
|
91
|
+
openBrowserOnMissing = false,
|
|
92
|
+
forceBrowser = false,
|
|
93
|
+
serviceId,
|
|
94
|
+
onStatus
|
|
95
|
+
} = {}) {
|
|
96
|
+
let jar = await loadCookieJar(config.cookieJarPath);
|
|
97
|
+
const serviceDomains = serviceId ? getServiceDomains(serviceId) : [];
|
|
98
|
+
const needsServiceCookies = !hasCookiesForDomains(jar, serviceDomains);
|
|
99
|
+
|
|
100
|
+
if (
|
|
101
|
+
!forceBrowser &&
|
|
102
|
+
jar &&
|
|
103
|
+
isValidationFresh(jar, config.validateTtlMs) &&
|
|
104
|
+
jar.authenticated &&
|
|
105
|
+
!needsServiceCookies
|
|
106
|
+
) {
|
|
107
|
+
return { jar, summary: jarSummary(jar, config), openedBrowser: false };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!forceBrowser && jar?.authenticated && needsServiceCookies) {
|
|
111
|
+
onStatus?.({ phase: "import_service_cookies" });
|
|
112
|
+
jar = await importAndMergeJar(config, jar, serviceDomains, { validate: false });
|
|
113
|
+
return { jar, summary: jarSummary(jar, config), openedBrowser: false };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!forceBrowser) {
|
|
117
|
+
onStatus?.({ phase: "import_from_chrome" });
|
|
118
|
+
const imported = await importCookiesFromChrome(config, [config.domain, ...serviceDomains]);
|
|
119
|
+
if (imported) {
|
|
120
|
+
jar = {
|
|
121
|
+
cookies: mergeCookies(jar?.cookies ?? [], imported.cookies),
|
|
122
|
+
source: "chrome",
|
|
123
|
+
authenticated: false
|
|
124
|
+
};
|
|
125
|
+
jar = await persistValidatedJar(config, jar);
|
|
126
|
+
if (jar.authenticated) {
|
|
127
|
+
return { jar, summary: jarSummary(jar, config), openedBrowser: false };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!openBrowserOnMissing && !forceBrowser) {
|
|
133
|
+
return {
|
|
134
|
+
jar,
|
|
135
|
+
summary: jarSummary(jar, config),
|
|
136
|
+
openedBrowser: false
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
onStatus?.({ phase: "browser_opened", url: config.loginUrl });
|
|
141
|
+
await openBrowser(config.loginUrl, { chromeProfile: config.chromeProfile });
|
|
142
|
+
|
|
143
|
+
const deadline = Date.now() + config.loginTimeoutMs;
|
|
144
|
+
while (Date.now() < deadline) {
|
|
145
|
+
await waitFor(2000);
|
|
146
|
+
onStatus?.({ phase: "waiting_for_login" });
|
|
147
|
+
const imported = await importCookiesFromChrome(config, [config.domain, ...serviceDomains]);
|
|
148
|
+
if (!imported?.cookies?.length) continue;
|
|
149
|
+
jar = {
|
|
150
|
+
cookies: mergeCookies(jar?.cookies ?? [], imported.cookies),
|
|
151
|
+
source: "chrome-browser-login",
|
|
152
|
+
authenticated: false
|
|
153
|
+
};
|
|
154
|
+
jar = await persistValidatedJar(config, jar);
|
|
155
|
+
if (jar.authenticated) {
|
|
156
|
+
return { jar, summary: jarSummary(jar, config), openedBrowser: true };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
throw new Error(
|
|
161
|
+
`Okta browser login timed out after ${config.loginTimeoutMs}ms. Complete MFA in Chrome, then retry.`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function getOktaCookieHeader(jar, config = getSsoConfig()) {
|
|
166
|
+
if (!jar?.cookies?.length) return "";
|
|
167
|
+
return buildCookieHeader(jar.cookies, config.domain);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function getCookiesForService(jar, serviceId) {
|
|
171
|
+
const domains = getServiceDomains(serviceId);
|
|
172
|
+
const cookies = domains.flatMap((domain) => cookiesForHost(jar?.cookies ?? [], domain));
|
|
173
|
+
return { domains, cookies };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function syncOktaAppCookies(appName, options = {}) {
|
|
177
|
+
const { getOktaApp } = await import("./oktaAppCatalog.mjs");
|
|
178
|
+
const app = getOktaApp(appName);
|
|
179
|
+
const config = options.config ?? getSsoConfig();
|
|
180
|
+
const domains = [...new Set([config.domain, ...(app.domains ?? [])])];
|
|
181
|
+
const imported = await importCookiesFromChrome(config, domains);
|
|
182
|
+
if (!imported?.cookies?.length) return null;
|
|
183
|
+
const jar = await loadCookieJar(config.cookieJarPath);
|
|
184
|
+
const merged = {
|
|
185
|
+
...(jar ?? {}),
|
|
186
|
+
cookies: mergeCookies(jar?.cookies ?? [], imported.cookies),
|
|
187
|
+
source: "chrome-okta-app-sync"
|
|
188
|
+
};
|
|
189
|
+
await saveCookieJar(config.cookieJarPath, merged);
|
|
190
|
+
return merged;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function syncAllOktaCatalogCookies(options = {}) {
|
|
194
|
+
const { OKTA_USER_HOME_APPS } = await import("./oktaAppCatalog.mjs");
|
|
195
|
+
const config = options.config ?? getSsoConfig();
|
|
196
|
+
const domains = new Set([config.domain]);
|
|
197
|
+
for (const app of Object.values(OKTA_USER_HOME_APPS)) {
|
|
198
|
+
for (const domain of app.domains ?? []) domains.add(domain);
|
|
199
|
+
}
|
|
200
|
+
const imported = await importCookiesFromChrome(config, [...domains]);
|
|
201
|
+
if (!imported?.cookies?.length) return null;
|
|
202
|
+
const jar = await loadCookieJar(config.cookieJarPath);
|
|
203
|
+
const merged = {
|
|
204
|
+
...(jar ?? {}),
|
|
205
|
+
cookies: mergeCookies(jar?.cookies ?? [], imported.cookies),
|
|
206
|
+
source: "chrome-okta-catalog-sync"
|
|
207
|
+
};
|
|
208
|
+
await saveCookieJar(config.cookieJarPath, merged);
|
|
209
|
+
return merged;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export { jarSummary, loadCookieJar };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { buildCookieHeader, resolveBestCookieHost } from "./cookieJar.mjs";
|
|
2
|
+
|
|
3
|
+
export async function validateOktaSession(jar, config) {
|
|
4
|
+
if (!jar?.cookies?.length) return false;
|
|
5
|
+
const cookie = buildCookieHeader(jar.cookies, config.domain);
|
|
6
|
+
if (!cookie) return false;
|
|
7
|
+
|
|
8
|
+
const response = await fetch(config.loginUrl, {
|
|
9
|
+
method: "GET",
|
|
10
|
+
redirect: "manual",
|
|
11
|
+
headers: { cookie }
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
if (response.status === 200) return true;
|
|
15
|
+
const location = response.headers.get("location") || "";
|
|
16
|
+
if (response.status >= 300 && response.status < 400) {
|
|
17
|
+
return !/login|signin|authorize/i.test(location);
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function probeReadOnlyUrl(
|
|
23
|
+
jar,
|
|
24
|
+
{ host, path = "/", cookie, fetchImpl = globalThis.fetch } = {}
|
|
25
|
+
) {
|
|
26
|
+
if (!host || !cookie) return { ready: false, status: 0, reason: "no_cookies" };
|
|
27
|
+
const response = await fetchImpl(`https://${host}${path}`, {
|
|
28
|
+
method: "GET",
|
|
29
|
+
redirect: "manual",
|
|
30
|
+
headers: { cookie, accept: "text/html,application/json" }
|
|
31
|
+
});
|
|
32
|
+
const location = response.headers.get("location") || "";
|
|
33
|
+
const loginRedirect = /login|signin|authorize|okta/i.test(location);
|
|
34
|
+
const contentType = response.headers.get("content-type") || undefined;
|
|
35
|
+
let loginPage = false;
|
|
36
|
+
if (response.status === 200 && /text\/html/i.test(contentType ?? "")) {
|
|
37
|
+
const htmlResponse = typeof response.clone === "function" ? response.clone() : response;
|
|
38
|
+
const html = typeof htmlResponse.text === "function" ? await htmlResponse.text().catch(() => "") : "";
|
|
39
|
+
loginPage = /<title[^>]*>\s*(?:log in|login|log into|sign in|signin)\b|data-testid=["']login|name=["']os_username/i.test(html);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
ready:
|
|
43
|
+
(response.status === 200 && !loginPage) ||
|
|
44
|
+
(response.status >= 300 && response.status < 400 && !loginRedirect),
|
|
45
|
+
status: response.status,
|
|
46
|
+
location: location || undefined,
|
|
47
|
+
contentType,
|
|
48
|
+
loginPage
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function validateOktaAppHost(jar, app, fetchImpl = globalThis.fetch) {
|
|
53
|
+
const domains = app?.domains ?? [];
|
|
54
|
+
if (!domains.length) return true;
|
|
55
|
+
if (app.mode === "bookmark" || app.mode === "oidc-app" || app.mode === "cli-readonly") return true;
|
|
56
|
+
const { host, cookie, hasCookies } = resolveBestCookieHost(jar, domains);
|
|
57
|
+
if (!hasCookies || !cookie || !host) return false;
|
|
58
|
+
const probe = await probeReadOnlyUrl(jar, {
|
|
59
|
+
host,
|
|
60
|
+
path: app.probePath ?? "/",
|
|
61
|
+
cookie,
|
|
62
|
+
fetchImpl
|
|
63
|
+
});
|
|
64
|
+
return probe.ready;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function validateServiceHost(jar, host) {
|
|
68
|
+
if (!jar?.cookies?.length || !host) return false;
|
|
69
|
+
const cookie = buildCookieHeader(jar.cookies, host);
|
|
70
|
+
if (!cookie) return false;
|
|
71
|
+
|
|
72
|
+
const probe = await probeReadOnlyUrl(jar, { host, cookie });
|
|
73
|
+
return probe.ready;
|
|
74
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export function getSsoConfig(env = process.env) {
|
|
5
|
+
const domain = env.WORKDAY_OKTA_DOMAIN?.trim() || "workday.okta.com";
|
|
6
|
+
const loginUrl = env.WORKDAY_OKTA_LOGIN_URL?.trim() || `https://${domain}/app/UserHome`;
|
|
7
|
+
const chromeProfileOverride = env.WORKDAY_CHROME_PROFILE?.trim();
|
|
8
|
+
const chromeProfile = chromeProfileOverride || "Default";
|
|
9
|
+
const validateTtlMs = Number(env.WORKDAY_SSO_VALIDATE_TTL_MS ?? 900_000);
|
|
10
|
+
const loginTimeoutMs = Number(env.WORKDAY_SSO_LOGIN_TIMEOUT_MS ?? 300_000);
|
|
11
|
+
const baseDir = join(homedir(), ".Nuvlore", "workday");
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
domain,
|
|
15
|
+
loginUrl,
|
|
16
|
+
chromeProfile,
|
|
17
|
+
chromeProfileExplicit: Boolean(chromeProfileOverride),
|
|
18
|
+
validateTtlMs,
|
|
19
|
+
loginTimeoutMs,
|
|
20
|
+
cookieJarPath: join(baseDir, "cookie-jar.json"),
|
|
21
|
+
chromeCookiesPath: join(
|
|
22
|
+
homedir(),
|
|
23
|
+
"Library",
|
|
24
|
+
"Application Support",
|
|
25
|
+
"Google",
|
|
26
|
+
"Chrome",
|
|
27
|
+
chromeProfile,
|
|
28
|
+
"Cookies"
|
|
29
|
+
)
|
|
30
|
+
};
|
|
31
|
+
}
|