@detiq/crawler 1.1.7 → 1.1.9
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/accessibility-auditor.js +74 -54
- package/dist/ai-login.js +249 -227
- package/dist/auth-token-rotator.js +67 -57
- package/dist/auth.js +336 -299
- package/dist/browser-extension.js +142 -104
- package/dist/captcha-solver.js +125 -98
- package/dist/color-contrast.js +89 -77
- package/dist/content-chunker.js +126 -126
- package/dist/content-dedup.js +71 -54
- package/dist/cookie-consent.js +83 -68
- package/dist/cookie-jar.js +60 -53
- package/dist/crawler.js +5345 -4486
- package/dist/custom-js-injector.js +61 -45
- package/dist/dark-mode-capture.js +81 -67
- package/dist/dead-link-detector.js +55 -47
- package/dist/element-hash.js +12 -8
- package/dist/email-otp.js +195 -154
- package/dist/engine-router.js +98 -87
- package/dist/export/bulk-export.js +55 -70
- package/dist/export/vector-export.js +105 -77
- package/dist/extractors/advanced.d.ts +10 -0
- package/dist/extractors/advanced.js +266 -249
- package/dist/extractors/artifact-envelope.js +58 -59
- package/dist/failure-analysis-agent.js +33 -37
- package/dist/feature-flag-detector.js +66 -51
- package/dist/form-detector.js +75 -55
- package/dist/frontier.js +101 -76
- package/dist/gap-pass.js +89 -68
- package/dist/graphql-detector.js +72 -62
- package/dist/http-engine.js +330 -282
- package/dist/incremental-diff.js +87 -54
- package/dist/index.d.ts +2 -0
- package/dist/index.js +28 -63
- package/dist/index.js.map +2 -2
- package/dist/input-fuzzer.js +97 -93
- package/dist/interactive-probe.d.ts +36 -0
- package/dist/interactive-probe.js +827 -715
- package/dist/job-queue.js +2169 -2151
- package/dist/js-coverage.js +56 -52
- package/dist/js-router-detector.js +117 -86
- package/dist/language-detector.js +63 -51
- package/dist/link-classifier.js +50 -32
- package/dist/live-view.js +108 -91
- package/dist/locator-extractor.d.ts +37 -0
- package/dist/locator-extractor.js +199 -0
- package/dist/locator-extractor.js.map +7 -0
- package/dist/logger.js +7 -9
- package/dist/markdown-extract.js +61 -48
- package/dist/native-crawler.js +946 -780
- package/dist/network-observer.js +156 -135
- package/dist/network-throttle.js +40 -29
- package/dist/openapi-generator.js +92 -77
- package/dist/pdf-extractor.js +90 -72
- package/dist/performance-metrics.js +102 -80
- package/dist/phash.js +144 -125
- package/dist/pii-scrubber.js +68 -65
- package/dist/playwright-recorder.js +58 -70
- package/dist/policies/crawl-safety.js +34 -31
- package/dist/profiles/domain-profile.js +125 -130
- package/dist/proxy.js +172 -146
- package/dist/rate-limiter.js +65 -58
- package/dist/redirect-tracker.js +64 -60
- package/dist/redis-frontier.js +136 -116
- package/dist/redis-queue.js +144 -157
- package/dist/response-cache.js +140 -127
- package/dist/responsive-detector.js +46 -42
- package/dist/retry-policy.js +50 -46
- package/dist/robots-txt.js +83 -69
- package/dist/screen-reader.js +70 -45
- package/dist/screenshot-masker.js +77 -59
- package/dist/seo-checker.js +107 -88
- package/dist/session-capture.js +212 -192
- package/dist/session-export.js +133 -107
- package/dist/sitemap-writer.js +41 -38
- package/dist/sitemap.js +202 -141
- package/dist/skyvern.js +148 -123
- package/dist/sqs-queue.js +42 -42
- package/dist/sso-token.js +90 -74
- package/dist/stealth/human-behavior.js +173 -156
- package/dist/telemetry.js +82 -68
- package/dist/test-executor.js +335 -309
- package/dist/tls-fingerprint.js +88 -77
- package/dist/url-pattern-completer.js +90 -71
- package/dist/visual-diff-verdict.js +38 -23
- package/dist/webhook-notifier.js +69 -71
- package/dist/websocket-detector.js +68 -56
- package/dist/worker.js +38 -29
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/locator-extractor.ts +226 -0
- package/types/index.d.ts +2 -0
- package/types/locator-extractor.d.ts +37 -0
package/dist/skyvern.js
CHANGED
|
@@ -1,129 +1,154 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Skyvern Vision Fallback
|
|
3
|
+
*
|
|
4
|
+
* Skyvern combines computer vision + LLM reasoning to navigate sites that
|
|
5
|
+
* defeat DOM-based approaches: CAPTCHAs, legacy apps, visual-only login flows,
|
|
6
|
+
* government portals. It's the "break glass" option when Stagehand fails.
|
|
7
|
+
*
|
|
8
|
+
* Production setup (Phase 4+):
|
|
9
|
+
* 1. Self-host: https://github.com/Skyvern-AI/skyvern
|
|
10
|
+
* 2. Or use cloud: https://app.skyvern.com
|
|
11
|
+
* 3. Set SKYVERN_API_URL + SKYVERN_API_KEY in .env
|
|
12
|
+
*
|
|
13
|
+
* Pricing note: vision tasks cost more per step than DOM-based Stagehand.
|
|
14
|
+
* Use only when the fast path fails.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Gap 5: Post-login navigation via Skyvern vision for apps where DOM analysis
|
|
18
|
+
* cannot discover screens (canvas, WebGL, legacy Flash-replacement stacks).
|
|
19
|
+
* Skyvern drives the browser via screenshot + LLM, returning visited page list.
|
|
20
|
+
*/
|
|
21
|
+
export async function skyvernNavigate(startUrl, navigationGoals, tenantConfig, triggerReason) {
|
|
22
|
+
const apiUrl = tenantConfig?.apiUrl || process.env.SKYVERN_API_URL;
|
|
23
|
+
const apiKey = tenantConfig?.apiKey || process.env.SKYVERN_API_KEY;
|
|
24
|
+
if (!apiUrl || !apiKey) {
|
|
25
|
+
console.warn('[skyvern] skyvernNavigate: no API config — skipping post-login vision navigation');
|
|
26
|
+
return { screenshotUrls: [], visitedUrls: [], status: 'failed' };
|
|
27
|
+
}
|
|
28
|
+
const visitedUrls = [];
|
|
29
|
+
const screenshotUrls = [];
|
|
30
|
+
const skyvernSteps = [];
|
|
31
|
+
let lastTaskId;
|
|
32
|
+
let totalDurationMs = 0;
|
|
33
|
+
for (const goal of navigationGoals) {
|
|
34
|
+
try {
|
|
35
|
+
const taskStart = Date.now();
|
|
36
|
+
const res = await fetch(`${apiUrl}/api/v1/tasks`, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
url: startUrl,
|
|
41
|
+
navigation_goal: goal,
|
|
42
|
+
...(tenantConfig?.llmKey ? { llm_key: tenantConfig.llmKey } : process.env.SKYVERN_LLM_KEY ? { llm_key: process.env.SKYVERN_LLM_KEY } : {}),
|
|
43
|
+
...(tenantConfig?.llmApiKey ? { llm_api_key: tenantConfig.llmApiKey } : process.env.OPENAI_API_KEY ? { llm_api_key: process.env.OPENAI_API_KEY } : {}),
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
if (!res.ok)
|
|
47
|
+
continue;
|
|
48
|
+
const data = await res.json();
|
|
49
|
+
const taskId = data.task_id;
|
|
50
|
+
if (!taskId)
|
|
51
|
+
continue;
|
|
52
|
+
lastTaskId = taskId;
|
|
53
|
+
// Poll for task completion (max 60s per goal)
|
|
54
|
+
let done = false;
|
|
55
|
+
for (let p = 0; p < 12 && !done; p++) {
|
|
56
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
57
|
+
const poll = await fetch(`${apiUrl}/api/v1/tasks/${taskId}`, {
|
|
58
|
+
headers: { 'x-api-key': apiKey },
|
|
59
|
+
}).catch(() => null);
|
|
60
|
+
if (!poll?.ok)
|
|
61
|
+
continue;
|
|
62
|
+
const pollData = await poll.json();
|
|
63
|
+
if (pollData.status === 'completed' || pollData.status === 'failed') {
|
|
64
|
+
done = true;
|
|
65
|
+
totalDurationMs += Date.now() - taskStart;
|
|
66
|
+
if (pollData.status === 'completed') {
|
|
67
|
+
if (pollData.output?.url)
|
|
68
|
+
visitedUrls.push(pollData.output.url);
|
|
69
|
+
if (pollData.output?.screenshot_url)
|
|
70
|
+
screenshotUrls.push(pollData.output.screenshot_url);
|
|
71
|
+
}
|
|
72
|
+
// Fetch step-level detail from Skyvern
|
|
73
|
+
try {
|
|
74
|
+
const stepsRes = await fetch(`${apiUrl}/api/v1/tasks/${taskId}/steps`, {
|
|
75
|
+
headers: { 'x-api-key': apiKey },
|
|
76
|
+
}).catch(() => null);
|
|
77
|
+
if (stepsRes?.ok) {
|
|
78
|
+
const stepsData = await stepsRes.json();
|
|
79
|
+
const stepsList = Array.isArray(stepsData) ? stepsData : (stepsData?.steps ?? []);
|
|
80
|
+
for (const s of stepsList) {
|
|
81
|
+
if (s.output?.screenshot_url)
|
|
82
|
+
screenshotUrls.push(s.output.screenshot_url);
|
|
83
|
+
skyvernSteps.push({
|
|
84
|
+
order: s.step_order ?? s.order ?? 0,
|
|
85
|
+
action_type: s.action_type ?? s.action ?? 'unknown',
|
|
86
|
+
element_description: s.output?.element_description,
|
|
87
|
+
reasoning: s.output?.llm_response?.action_description ?? s.output?.reasoning,
|
|
88
|
+
success: s.status === 'completed',
|
|
89
|
+
screenshot_url: s.output?.screenshot_url,
|
|
90
|
+
error: s.status === 'failed' ? (s.output?.errors?.[0] ?? 'Step failed') : undefined,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch { /* non-fatal */ }
|
|
96
|
+
}
|
|
65
97
|
}
|
|
66
|
-
} catch {
|
|
67
|
-
}
|
|
68
98
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
99
|
+
catch (err) {
|
|
100
|
+
console.warn(`[skyvern] Navigation goal failed: ${goal}`, err);
|
|
101
|
+
}
|
|
72
102
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
};
|
|
103
|
+
return {
|
|
104
|
+
screenshotUrls,
|
|
105
|
+
visitedUrls,
|
|
106
|
+
status: visitedUrls.length > 0 ? 'success' : screenshotUrls.length > 0 ? 'partial' : 'failed',
|
|
107
|
+
steps: skyvernSteps,
|
|
108
|
+
taskId: lastTaskId,
|
|
109
|
+
durationMs: totalDurationMs || undefined,
|
|
110
|
+
triggerReason,
|
|
111
|
+
};
|
|
83
112
|
}
|
|
84
|
-
async function skyvernFallback(task, tenantConfig) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
export async function skyvernFallback(task, tenantConfig) {
|
|
114
|
+
const apiUrl = tenantConfig?.apiUrl || process.env.SKYVERN_API_URL;
|
|
115
|
+
const apiKey = tenantConfig?.apiKey || process.env.SKYVERN_API_KEY;
|
|
116
|
+
if (!apiUrl || !apiKey) {
|
|
117
|
+
console.warn('[skyvern] Vision fallback disabled — set SKYVERN_API_URL + SKYVERN_API_KEY in .env to enable.', '\n[skyvern] Self-host: https://github.com/Skyvern-AI/skyvern');
|
|
118
|
+
return 'failed';
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const res = await fetch(`${apiUrl}/api/v1/tasks`, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
|
|
124
|
+
body: JSON.stringify({
|
|
125
|
+
url: task.url,
|
|
126
|
+
navigation_goal: task.taskDescription,
|
|
127
|
+
...(task.credentials && {
|
|
128
|
+
navigation_payload: {
|
|
129
|
+
username: task.credentials.username,
|
|
130
|
+
password: task.credentials.password,
|
|
131
|
+
...(task.credentials.oneTimeCode && { otp: task.credentials.oneTimeCode }),
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
...(tenantConfig?.llmKey ? { llm_key: tenantConfig.llmKey } : process.env.SKYVERN_LLM_KEY ? { llm_key: process.env.SKYVERN_LLM_KEY } : {}),
|
|
135
|
+
...(tenantConfig?.llmApiKey ? { llm_api_key: tenantConfig.llmApiKey } : process.env.OPENAI_API_KEY ? { llm_api_key: process.env.OPENAI_API_KEY } : {}),
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
if (!res.ok) {
|
|
139
|
+
const body = await res.text().catch(() => '');
|
|
140
|
+
console.error(`[skyvern] Task creation failed: ${res.status}`, body);
|
|
141
|
+
return 'failed';
|
|
142
|
+
}
|
|
143
|
+
const data = await res.json();
|
|
144
|
+
console.log(`[skyvern] Task queued: ${data.task_id}. Poll GET /api/v1/tasks/${data.task_id} for completion.`);
|
|
145
|
+
// Full implementation: poll until status === "completed" | "failed".
|
|
146
|
+
// For Phase 1 the login already happened in the browser context Skyvern manages,
|
|
147
|
+
// so returning 'success' here means we can proceed in that browser session.
|
|
148
|
+
return 'success';
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
console.error('[skyvern] Error:', err);
|
|
152
|
+
return 'failed';
|
|
116
153
|
}
|
|
117
|
-
const data = await res.json();
|
|
118
|
-
console.log(`[skyvern] Task queued: ${data.task_id}. Poll GET /api/v1/tasks/${data.task_id} for completion.`);
|
|
119
|
-
return "success";
|
|
120
|
-
} catch (err) {
|
|
121
|
-
console.error("[skyvern] Error:", err);
|
|
122
|
-
return "failed";
|
|
123
|
-
}
|
|
124
154
|
}
|
|
125
|
-
export {
|
|
126
|
-
skyvernFallback,
|
|
127
|
-
skyvernNavigate
|
|
128
|
-
};
|
|
129
|
-
//# sourceMappingURL=skyvern.js.map
|
package/dist/sqs-queue.js
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
import { SQSClient, SendMessageCommand, ReceiveMessageCommand, DeleteMessageCommand } from
|
|
2
|
-
const sqs = new SQSClient({ region: process.env.AWS_REGION ??
|
|
3
|
-
async function sendJobToSqs(jobId, tenantId, config) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
MessageBody: JSON.stringify({ jobId, tenantId, config })
|
|
9
|
-
}));
|
|
10
|
-
}
|
|
11
|
-
async function startSqsWorker() {
|
|
12
|
-
const queueUrl = process.env.SQS_CRAWL_QUEUE_URL;
|
|
13
|
-
if (!queueUrl) throw new Error("SQS_CRAWL_QUEUE_URL not configured");
|
|
14
|
-
const { processJob } = await import("./job-queue.js");
|
|
15
|
-
console.log("[sqs-worker] starting, queue:", queueUrl);
|
|
16
|
-
while (true) {
|
|
17
|
-
try {
|
|
18
|
-
const { Messages = [] } = await sqs.send(new ReceiveMessageCommand({
|
|
1
|
+
import { SQSClient, SendMessageCommand, ReceiveMessageCommand, DeleteMessageCommand } from '@aws-sdk/client-sqs';
|
|
2
|
+
const sqs = new SQSClient({ region: process.env.AWS_REGION ?? 'us-east-1' });
|
|
3
|
+
export async function sendJobToSqs(jobId, tenantId, config) {
|
|
4
|
+
const queueUrl = process.env.SQS_CRAWL_QUEUE_URL;
|
|
5
|
+
if (!queueUrl)
|
|
6
|
+
throw new Error('SQS_CRAWL_QUEUE_URL not configured');
|
|
7
|
+
await sqs.send(new SendMessageCommand({
|
|
19
8
|
QueueUrl: queueUrl,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
9
|
+
MessageBody: JSON.stringify({ jobId, tenantId, config }),
|
|
10
|
+
}));
|
|
11
|
+
}
|
|
12
|
+
export async function startSqsWorker() {
|
|
13
|
+
const queueUrl = process.env.SQS_CRAWL_QUEUE_URL;
|
|
14
|
+
if (!queueUrl)
|
|
15
|
+
throw new Error('SQS_CRAWL_QUEUE_URL not configured');
|
|
16
|
+
const { processJob } = await import('./job-queue.js');
|
|
17
|
+
console.log('[sqs-worker] starting, queue:', queueUrl);
|
|
18
|
+
while (true) {
|
|
25
19
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
20
|
+
const { Messages = [] } = await sqs.send(new ReceiveMessageCommand({
|
|
21
|
+
QueueUrl: queueUrl,
|
|
22
|
+
WaitTimeSeconds: 20,
|
|
23
|
+
MaxNumberOfMessages: 2,
|
|
24
|
+
}));
|
|
25
|
+
await Promise.all(Messages.map(async (msg) => {
|
|
26
|
+
let jobId = '?';
|
|
27
|
+
try {
|
|
28
|
+
const data = JSON.parse(msg.Body);
|
|
29
|
+
jobId = data.jobId;
|
|
30
|
+
await processJob(data.jobId, data.config);
|
|
31
|
+
await sqs.send(new DeleteMessageCommand({
|
|
32
|
+
QueueUrl: queueUrl,
|
|
33
|
+
ReceiptHandle: msg.ReceiptHandle,
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
console.error(`[sqs-worker] job ${jobId} failed — leaving for SQS redrive:`, err.message);
|
|
38
|
+
// Do NOT delete — SQS redrive policy + DLQ handles retry
|
|
39
|
+
}
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
console.error('[sqs-worker] receive error:', err.message);
|
|
44
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
35
45
|
}
|
|
36
|
-
}));
|
|
37
|
-
} catch (err) {
|
|
38
|
-
console.error("[sqs-worker] receive error:", err.message);
|
|
39
|
-
await new Promise((r) => setTimeout(r, 2e3));
|
|
40
46
|
}
|
|
41
|
-
}
|
|
42
47
|
}
|
|
43
|
-
export {
|
|
44
|
-
sendJobToSqs,
|
|
45
|
-
startSqsWorker
|
|
46
|
-
};
|
|
47
|
-
//# sourceMappingURL=sqs-queue.js.map
|
package/dist/sso-token.js
CHANGED
|
@@ -1,76 +1,92 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
1
|
+
/**
|
|
2
|
+
* SSO token retrieval for Okta and Azure AD.
|
|
3
|
+
* Uses OAuth2 ROPC (Resource Owner Password Credentials) flow to get access tokens
|
|
4
|
+
* without browser-based login.
|
|
5
|
+
*
|
|
6
|
+
* Note: ROPC is deprecated in OAuth 2.1 but still supported by Okta and Azure AD
|
|
7
|
+
* for service accounts and automated testing scenarios.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Get an access token using ROPC flow (username + password → token).
|
|
11
|
+
* Returns the access token string.
|
|
12
|
+
*/
|
|
13
|
+
export async function getSsoToken(config, username, password) {
|
|
14
|
+
let tokenUrl;
|
|
15
|
+
const params = {
|
|
16
|
+
grant_type: 'password',
|
|
17
|
+
username,
|
|
18
|
+
password,
|
|
19
|
+
scope: config.scope ?? 'openid profile',
|
|
20
|
+
client_id: config.clientId,
|
|
21
|
+
};
|
|
22
|
+
if (config.clientSecret) {
|
|
23
|
+
params.client_secret = config.clientSecret;
|
|
24
|
+
}
|
|
25
|
+
switch (config.provider) {
|
|
26
|
+
case 'okta':
|
|
27
|
+
tokenUrl = `https://${config.domain}/oauth2/v1/token`;
|
|
28
|
+
break;
|
|
29
|
+
case 'azure_ad':
|
|
30
|
+
// domain here is the tenant ID (e.g. XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX or 'mycompany.onmicrosoft.com')
|
|
31
|
+
tokenUrl = `https://login.microsoftonline.com/${config.domain}/oauth2/v2.0/token`;
|
|
32
|
+
break;
|
|
33
|
+
case 'generic_oidc':
|
|
34
|
+
tokenUrl = config.domain.startsWith('http') ? config.domain : `https://${config.domain}/oauth2/token`;
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
throw new Error(`Unknown SSO provider: ${config.provider}`);
|
|
38
|
+
}
|
|
39
|
+
const res = await fetch(tokenUrl, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
42
|
+
body: new URLSearchParams(params).toString(),
|
|
43
|
+
});
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
const body = await res.text().catch(() => res.statusText);
|
|
46
|
+
throw new Error(`SSO token request failed (${res.status}): ${body}`);
|
|
47
|
+
}
|
|
48
|
+
const data = await res.json();
|
|
49
|
+
if (data.error)
|
|
50
|
+
throw new Error(`SSO error: ${data.error} — ${data.error_description}`);
|
|
51
|
+
if (!data.access_token)
|
|
52
|
+
throw new Error('SSO response missing access_token');
|
|
53
|
+
return data.access_token;
|
|
39
54
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Get a client credentials token (no user — for service accounts).
|
|
57
|
+
*/
|
|
58
|
+
export async function getSsoClientCredentialsToken(config) {
|
|
59
|
+
if (!config.clientSecret)
|
|
60
|
+
throw new Error('clientSecret required for client credentials flow');
|
|
61
|
+
let tokenUrl;
|
|
62
|
+
switch (config.provider) {
|
|
63
|
+
case 'okta':
|
|
64
|
+
tokenUrl = `https://${config.domain}/oauth2/v1/token`;
|
|
65
|
+
break;
|
|
66
|
+
case 'azure_ad':
|
|
67
|
+
tokenUrl = `https://login.microsoftonline.com/${config.domain}/oauth2/v2.0/token`;
|
|
68
|
+
break;
|
|
69
|
+
default:
|
|
70
|
+
tokenUrl = `https://${config.domain}/oauth2/token`;
|
|
71
|
+
}
|
|
72
|
+
const res = await fetch(tokenUrl, {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
75
|
+
body: new URLSearchParams({
|
|
76
|
+
grant_type: 'client_credentials',
|
|
77
|
+
client_id: config.clientId,
|
|
78
|
+
client_secret: config.clientSecret,
|
|
79
|
+
scope: config.scope ?? 'openid profile',
|
|
80
|
+
}).toString(),
|
|
81
|
+
});
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
const body = await res.text().catch(() => res.statusText);
|
|
84
|
+
throw new Error(`SSO client credentials failed (${res.status}): ${body}`);
|
|
85
|
+
}
|
|
86
|
+
const data = await res.json();
|
|
87
|
+
if (data.error)
|
|
88
|
+
throw new Error(`SSO error: ${data.error} — ${data.error_description}`);
|
|
89
|
+
if (!data.access_token)
|
|
90
|
+
throw new Error('SSO response missing access_token');
|
|
91
|
+
return data.access_token;
|
|
71
92
|
}
|
|
72
|
-
export {
|
|
73
|
-
getSsoClientCredentialsToken,
|
|
74
|
-
getSsoToken
|
|
75
|
-
};
|
|
76
|
-
//# sourceMappingURL=sso-token.js.map
|