@docsxai/engine 0.2.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.
Files changed (129) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +130 -0
  3. package/dist/auth/api-login.d.ts +69 -0
  4. package/dist/auth/api-login.js +95 -0
  5. package/dist/auth/browser-session.d.ts +28 -0
  6. package/dist/auth/browser-session.js +43 -0
  7. package/dist/auth/cookie-jar.d.ts +58 -0
  8. package/dist/auth/cookie-jar.js +212 -0
  9. package/dist/auth/email-otp.d.ts +210 -0
  10. package/dist/auth/email-otp.js +166 -0
  11. package/dist/auth/http-basic.d.ts +5 -0
  12. package/dist/auth/http-basic.js +17 -0
  13. package/dist/auth/index.d.ts +47 -0
  14. package/dist/auth/index.js +137 -0
  15. package/dist/auth/jwt-injection.d.ts +153 -0
  16. package/dist/auth/jwt-injection.js +136 -0
  17. package/dist/auth/manual-capture.d.ts +35 -0
  18. package/dist/auth/manual-capture.js +30 -0
  19. package/dist/auth/mtls.d.ts +15 -0
  20. package/dist/auth/mtls.js +53 -0
  21. package/dist/auth/pat-header.d.ts +19 -0
  22. package/dist/auth/pat-header.js +34 -0
  23. package/dist/auth/storage-state-cache.d.ts +38 -0
  24. package/dist/auth/storage-state-cache.js +143 -0
  25. package/dist/auth/test-backdoor.d.ts +25 -0
  26. package/dist/auth/test-backdoor.js +51 -0
  27. package/dist/auth/totp.d.ts +39 -0
  28. package/dist/auth/totp.js +108 -0
  29. package/dist/auth/types.d.ts +86 -0
  30. package/dist/auth/types.js +57 -0
  31. package/dist/auth/ui-form.d.ts +204 -0
  32. package/dist/auth/ui-form.js +153 -0
  33. package/dist/auth/webauthn.d.ts +88 -0
  34. package/dist/auth/webauthn.js +67 -0
  35. package/dist/auth.d.ts +1 -0
  36. package/dist/auth.js +3 -0
  37. package/dist/backend-client-contracts.d.ts +88 -0
  38. package/dist/backend-client-contracts.js +19 -0
  39. package/dist/backend-client-oauth-login.d.ts +7 -0
  40. package/dist/backend-client-oauth-login.js +90 -0
  41. package/dist/backend-client-state-cache.d.ts +73 -0
  42. package/dist/backend-client-state-cache.js +185 -0
  43. package/dist/backend-client-token.d.ts +18 -0
  44. package/dist/backend-client-token.js +94 -0
  45. package/dist/backend-client-transport.d.ts +66 -0
  46. package/dist/backend-client-transport.js +181 -0
  47. package/dist/backend-client.d.ts +5 -0
  48. package/dist/backend-client.js +18 -0
  49. package/dist/calibrate.d.ts +31 -0
  50. package/dist/calibrate.js +68 -0
  51. package/dist/cli-commands-authoring.d.ts +5 -0
  52. package/dist/cli-commands-authoring.js +403 -0
  53. package/dist/cli-commands-backend.d.ts +5 -0
  54. package/dist/cli-commands-backend.js +211 -0
  55. package/dist/cli-commands-docpack.d.ts +5 -0
  56. package/dist/cli-commands-docpack.js +280 -0
  57. package/dist/cli-commands-session.d.ts +4 -0
  58. package/dist/cli-commands-session.js +398 -0
  59. package/dist/cli-shared.d.ts +5 -0
  60. package/dist/cli-shared.js +45 -0
  61. package/dist/cli-usage.d.ts +1 -0
  62. package/dist/cli-usage.js +137 -0
  63. package/dist/cli.d.ts +2 -0
  64. package/dist/cli.js +77 -0
  65. package/dist/diagnose.d.ts +50 -0
  66. package/dist/diagnose.js +168 -0
  67. package/dist/diff-compute.d.ts +13 -0
  68. package/dist/diff-compute.js +378 -0
  69. package/dist/diff-report.d.ts +7 -0
  70. package/dist/diff-report.js +125 -0
  71. package/dist/diff-types.d.ts +125 -0
  72. package/dist/diff-types.js +15 -0
  73. package/dist/diff.d.ts +3 -0
  74. package/dist/diff.js +16 -0
  75. package/dist/doc-pack-io.d.ts +30 -0
  76. package/dist/doc-pack-io.js +182 -0
  77. package/dist/doc-pack.d.ts +1814 -0
  78. package/dist/doc-pack.js +328 -0
  79. package/dist/doctor-checks-plugins.d.ts +2 -0
  80. package/dist/doctor-checks-plugins.js +136 -0
  81. package/dist/doctor-checks.d.ts +56 -0
  82. package/dist/doctor-checks.js +367 -0
  83. package/dist/doctor.d.ts +7 -0
  84. package/dist/doctor.js +62 -0
  85. package/dist/export/adf.d.ts +57 -0
  86. package/dist/export/adf.js +323 -0
  87. package/dist/export/playwright-test.d.ts +26 -0
  88. package/dist/export/playwright-test.js +221 -0
  89. package/dist/flow-file.d.ts +21 -0
  90. package/dist/flow-file.js +180 -0
  91. package/dist/flow-lint.d.ts +24 -0
  92. package/dist/flow-lint.js +203 -0
  93. package/dist/flow-runtime.d.ts +113 -0
  94. package/dist/flow-runtime.js +273 -0
  95. package/dist/flow-tree.d.ts +19 -0
  96. package/dist/flow-tree.js +104 -0
  97. package/dist/index.d.ts +27 -0
  98. package/dist/index.js +31 -0
  99. package/dist/playwright-driver.d.ts +105 -0
  100. package/dist/playwright-driver.js +363 -0
  101. package/dist/playwright-instrumented-browser.d.ts +51 -0
  102. package/dist/playwright-instrumented-browser.js +189 -0
  103. package/dist/plugins/load.d.ts +22 -0
  104. package/dist/plugins/load.js +99 -0
  105. package/dist/plugins/lock.d.ts +40 -0
  106. package/dist/plugins/lock.js +122 -0
  107. package/dist/plugins/manifest.d.ts +70 -0
  108. package/dist/plugins/manifest.js +115 -0
  109. package/dist/plugins/plan.d.ts +51 -0
  110. package/dist/plugins/plan.js +279 -0
  111. package/dist/plugins/registry.d.ts +59 -0
  112. package/dist/plugins/registry.js +71 -0
  113. package/dist/plugins/runtime.d.ts +7 -0
  114. package/dist/plugins/runtime.js +27 -0
  115. package/dist/plugins/types.d.ts +58 -0
  116. package/dist/plugins/types.js +4 -0
  117. package/dist/plugins-cli.d.ts +1 -0
  118. package/dist/plugins-cli.js +191 -0
  119. package/dist/redact.d.ts +16 -0
  120. package/dist/redact.js +72 -0
  121. package/dist/style.d.ts +46 -0
  122. package/dist/style.js +151 -0
  123. package/dist/viewer-bin.d.ts +20 -0
  124. package/dist/viewer-bin.js +97 -0
  125. package/dist/workspace.d.ts +60 -0
  126. package/dist/workspace.js +172 -0
  127. package/dist/zip.d.ts +17 -0
  128. package/dist/zip.js +113 -0
  129. package/package.json +64 -0
@@ -0,0 +1,212 @@
1
+ // Minimal RFC-6265 cookie jar for the pure-HTTP strategies (api-login, test-backdoor).
2
+ //
3
+ // Strategies that log in over plain `fetch` must collect `Set-Cookie` headers across the login's
4
+ // redirect chain themselves (fetch's `redirect: "follow"` drops them). The jar parses the
5
+ // attributes Playwright's `storageState` needs and replays matching cookies on each hop.
6
+ import { AuthStrategyConfigError } from "./types.js";
7
+ function isLoopback(hostname) {
8
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
9
+ }
10
+ /** RFC 6265 §5.1.4 default-path: the request path's directory. */
11
+ function defaultPath(requestUrl) {
12
+ const p = requestUrl.pathname;
13
+ if (!p.startsWith("/"))
14
+ return "/";
15
+ const lastSlash = p.lastIndexOf("/");
16
+ return lastSlash <= 0 ? "/" : p.slice(0, lastSlash);
17
+ }
18
+ /**
19
+ * Parse one `Set-Cookie` header into a {@link StorageStateCookie}.
20
+ * Domain defaults to the request host (host-only); a `Domain` attribute gets the conventional
21
+ * leading dot (subdomain-matching, mirroring Playwright's representation). `Max-Age` beats
22
+ * `Expires`; absent both, the cookie is a session cookie (`expires: -1`).
23
+ * Returns `null` for an unparsable header.
24
+ */
25
+ export function parseSetCookie(header, requestUrl, now = Date.now()) {
26
+ const segments = header.split(";");
27
+ const nameValue = segments[0] ?? "";
28
+ const eq = nameValue.indexOf("=");
29
+ if (eq <= 0)
30
+ return null;
31
+ const name = nameValue.slice(0, eq).trim();
32
+ const value = nameValue.slice(eq + 1).trim();
33
+ if (!name)
34
+ return null;
35
+ let domain = requestUrl.hostname;
36
+ let path;
37
+ let expires = -1;
38
+ let maxAge;
39
+ let httpOnly = false;
40
+ let secure = false;
41
+ let sameSite = "Lax";
42
+ for (const segment of segments.slice(1)) {
43
+ const sepIdx = segment.indexOf("=");
44
+ const key = (sepIdx === -1 ? segment : segment.slice(0, sepIdx)).trim().toLowerCase();
45
+ const val = sepIdx === -1 ? "" : segment.slice(sepIdx + 1).trim();
46
+ switch (key) {
47
+ case "domain":
48
+ if (val)
49
+ domain = "." + val.replace(/^\./, "").toLowerCase();
50
+ break;
51
+ case "path":
52
+ if (val.startsWith("/"))
53
+ path = val;
54
+ break;
55
+ case "expires": {
56
+ const t = Date.parse(val);
57
+ if (!Number.isNaN(t))
58
+ expires = Math.floor(t / 1000);
59
+ break;
60
+ }
61
+ case "max-age": {
62
+ const n = Number(val);
63
+ if (Number.isFinite(n))
64
+ maxAge = n;
65
+ break;
66
+ }
67
+ case "httponly":
68
+ httpOnly = true;
69
+ break;
70
+ case "secure":
71
+ secure = true;
72
+ break;
73
+ case "samesite": {
74
+ const v = val.toLowerCase();
75
+ sameSite = v === "strict" ? "Strict" : v === "none" ? "None" : "Lax";
76
+ break;
77
+ }
78
+ }
79
+ }
80
+ if (maxAge !== undefined)
81
+ expires = maxAge <= 0 ? 0 : Math.floor(now / 1000) + maxAge;
82
+ return {
83
+ name,
84
+ value,
85
+ domain,
86
+ path: path ?? defaultPath(requestUrl),
87
+ expires,
88
+ httpOnly,
89
+ secure,
90
+ sameSite,
91
+ };
92
+ }
93
+ function domainMatches(hostname, cookieDomain) {
94
+ if (cookieDomain.startsWith(".")) {
95
+ const bare = cookieDomain.slice(1);
96
+ return hostname === bare || hostname.endsWith(cookieDomain);
97
+ }
98
+ return hostname === cookieDomain;
99
+ }
100
+ function pathMatches(requestPath, cookiePath) {
101
+ if (requestPath === cookiePath)
102
+ return true;
103
+ if (!requestPath.startsWith(cookiePath))
104
+ return false;
105
+ return cookiePath.endsWith("/") || requestPath[cookiePath.length] === "/";
106
+ }
107
+ export class CookieJar {
108
+ store = new Map();
109
+ key(c) {
110
+ return `${c.name}\u0000${c.domain}\u0000${c.path}`;
111
+ }
112
+ /** Store a cookie; an already-expired one (`Max-Age=0` deletion) removes the match instead. */
113
+ add(cookie, now = Date.now()) {
114
+ // `expires: -1` is a session cookie (kept); `0` is the Max-Age<=0 deletion marker.
115
+ if (cookie.expires >= 0 && cookie.expires * 1000 <= now) {
116
+ this.store.delete(this.key(cookie));
117
+ return;
118
+ }
119
+ this.store.set(this.key(cookie), cookie);
120
+ }
121
+ /** Parse + store every `Set-Cookie` header from one response. */
122
+ addFromHeaders(setCookieHeaders, requestUrl, now = Date.now()) {
123
+ for (const header of setCookieHeaders) {
124
+ const cookie = parseSetCookie(header, requestUrl, now);
125
+ if (cookie)
126
+ this.add(cookie, now);
127
+ }
128
+ }
129
+ /** First stored cookie with this name, regardless of domain/path. */
130
+ get(name) {
131
+ for (const c of this.store.values())
132
+ if (c.name === name)
133
+ return c;
134
+ return undefined;
135
+ }
136
+ has(name) {
137
+ return this.get(name) !== undefined;
138
+ }
139
+ cookies() {
140
+ return [...this.store.values()];
141
+ }
142
+ /** `Cookie:` header value for a request to `url` — domain/path/secure/expiry-matched. Empty string if none apply. */
143
+ cookieHeaderFor(url, now = Date.now()) {
144
+ const sendable = this.cookies().filter((c) => {
145
+ if (c.expires > 0 && c.expires * 1000 <= now)
146
+ return false;
147
+ if (c.secure && url.protocol !== "https:" && !isLoopback(url.hostname))
148
+ return false;
149
+ return domainMatches(url.hostname, c.domain) && pathMatches(url.pathname || "/", c.path);
150
+ });
151
+ return sendable.map((c) => `${c.name}=${c.value}`).join("; ");
152
+ }
153
+ toStorageState() {
154
+ return { cookies: this.cookies(), origins: [] };
155
+ }
156
+ }
157
+ /**
158
+ * Fetch with `redirect: "manual"`, following up to `maxRedirects` hops (default 5) while
159
+ * collecting `Set-Cookie` headers into a {@link CookieJar} and replaying matching cookies on
160
+ * each hop. 303 — and 301/302 on a non-GET — switch the method to GET and drop the body,
161
+ * matching browser behaviour.
162
+ */
163
+ export async function fetchCollectingCookies(url, init = {}, opts = {}) {
164
+ const maxRedirects = opts.maxRedirects ?? 5;
165
+ const jar = opts.jar ?? new CookieJar();
166
+ const fetchImpl = opts.fetchImpl ?? fetch;
167
+ const now = opts.now ?? Date.now;
168
+ let current = new URL(url);
169
+ let method = (init.method ?? "GET").toUpperCase();
170
+ let body = init.body;
171
+ let headers = { ...(init.headers ?? {}) };
172
+ for (let hops = 0;; hops++) {
173
+ const cookieHeader = jar.cookieHeaderFor(current, now());
174
+ const response = await fetchImpl(current, {
175
+ method,
176
+ headers: { ...headers, ...(cookieHeader ? { cookie: cookieHeader } : {}) },
177
+ ...(body !== undefined ? { body } : {}),
178
+ redirect: "manual",
179
+ });
180
+ jar.addFromHeaders(response.headers.getSetCookie(), current, now());
181
+ const location = response.headers.get("location");
182
+ const isRedirect = response.status >= 300 && response.status < 400 && location !== null;
183
+ if (!isRedirect) {
184
+ return { jar, status: response.status, url: current.href, body: await response.text(), hops };
185
+ }
186
+ await response.body?.cancel();
187
+ if (hops >= maxRedirects) {
188
+ throw new AuthStrategyConfigError(`redirect chain from ${new URL(url).href} exceeded ${maxRedirects} hops (stuck at ${current.href})`);
189
+ }
190
+ current = new URL(location, current);
191
+ if (response.status === 303 ||
192
+ ((response.status === 301 || response.status === 302) && method !== "GET")) {
193
+ method = "GET";
194
+ body = undefined;
195
+ const { ["content-type"]: _dropped, ...rest } = Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]));
196
+ headers = rest;
197
+ }
198
+ }
199
+ }
200
+ /**
201
+ * The expiry a login strategy can credibly report: the named cookie's expiry when the caller
202
+ * knows which cookie is the session, else — only when the jar has exactly one real-expiry
203
+ * cookie — that one. Ambiguous jars report `undefined` (the cache's `ttl` takes over).
204
+ */
205
+ export function jarAuthExpiry(state, preferredCookie) {
206
+ if (preferredCookie) {
207
+ const c = state.cookies.find((x) => x.name === preferredCookie && x.expires > 0);
208
+ return c ? c.expires * 1000 : undefined;
209
+ }
210
+ const real = state.cookies.filter((c) => c.expires > 0);
211
+ return real.length === 1 ? real[0].expires * 1000 : undefined;
212
+ }
@@ -0,0 +1,210 @@
1
+ import { z } from "zod";
2
+ import { type AuthPageLauncher } from "./browser-session.js";
3
+ import { type AuthContext, type AuthResult, type AuthStrategy } from "./types.js";
4
+ export interface InboxMessage {
5
+ to: string;
6
+ /** Epoch ms the inbox recorded the message. */
7
+ receivedAt: number;
8
+ /** Plain-text body the code is extracted from. */
9
+ body: string;
10
+ }
11
+ export interface InboxProvider {
12
+ /** Resolve with the newest message for `to` received at/after `since`; reject after `timeoutMs`. */
13
+ waitForMessage(query: {
14
+ to: string;
15
+ since: number;
16
+ timeoutMs: number;
17
+ }): Promise<InboxMessage>;
18
+ }
19
+ export type InboxProviderFactory = (options: Record<string, unknown>) => InboxProvider;
20
+ export declare const HttpJsonInboxOptions: z.ZodObject<{
21
+ /** Inbox endpoint answering `{ messages: [{ to, received_at, body }] }`. */
22
+ url: z.ZodString;
23
+ poll_interval_ms: z.ZodDefault<z.ZodNumber>;
24
+ }, "strict", z.ZodTypeAny, {
25
+ url: string;
26
+ poll_interval_ms: number;
27
+ }, {
28
+ url: string;
29
+ poll_interval_ms?: number | undefined;
30
+ }>;
31
+ export type HttpJsonInboxOptions = z.infer<typeof HttpJsonInboxOptions>;
32
+ /** Built-in provider: poll a Mailpit-style JSON inbox until the code mail shows up. */
33
+ export declare function httpJsonInboxProvider(options: Record<string, unknown>): InboxProvider;
34
+ /** Register (or override) an inbox provider under `name` — the email-otp plugin hook. */
35
+ export declare function registerInboxProvider(name: string, factory: InboxProviderFactory): void;
36
+ /** Remove a registered provider (test/plugin teardown). Removing `http-json` restores the built-in. */
37
+ export declare function unregisterInboxProvider(name: string): void;
38
+ export declare function makeInboxProvider(name: string, options: Record<string, unknown>): InboxProvider;
39
+ export declare const EmailOtpInboxOptions: z.ZodObject<{
40
+ provider: z.ZodDefault<z.ZodString>;
41
+ /** Provider-specific options (for `http-json`: `{ url, poll_interval_ms? }`). */
42
+ options: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
43
+ /** Env var *name* holding the inbox address to watch. Default: the role's `username` credential. */
44
+ to_env: z.ZodOptional<z.ZodString>;
45
+ /** Code-extraction regex (first capture group, else the whole match). */
46
+ code_pattern: z.ZodDefault<z.ZodString>;
47
+ /** How long to wait for the code mail. Default 30000. */
48
+ timeout_ms: z.ZodDefault<z.ZodNumber>;
49
+ }, "strict", z.ZodTypeAny, {
50
+ options: Record<string, unknown>;
51
+ timeout_ms: number;
52
+ provider: string;
53
+ code_pattern: string;
54
+ to_env?: string | undefined;
55
+ }, {
56
+ options?: Record<string, unknown> | undefined;
57
+ timeout_ms?: number | undefined;
58
+ provider?: string | undefined;
59
+ to_env?: string | undefined;
60
+ code_pattern?: string | undefined;
61
+ }>;
62
+ export type EmailOtpInboxOptions = z.infer<typeof EmailOtpInboxOptions>;
63
+ export declare const EmailOtpOptions: z.ZodEffects<z.ZodObject<{
64
+ login_url: z.ZodString;
65
+ username_selector: z.ZodString;
66
+ password_selector: z.ZodString;
67
+ submit_selector: z.ZodString;
68
+ otp_selector: z.ZodString;
69
+ otp_submit_selector: z.ZodOptional<z.ZodString>;
70
+ success_selector: z.ZodOptional<z.ZodString>;
71
+ url_matches: z.ZodOptional<z.ZodString>;
72
+ timeout_ms: z.ZodDefault<z.ZodNumber>;
73
+ ignore_https_errors: z.ZodDefault<z.ZodBoolean>;
74
+ pre_steps: z.ZodDefault<z.ZodArray<z.ZodObject<{
75
+ action: z.ZodEnum<["click", "fill"]>;
76
+ selector: z.ZodString;
77
+ value_env: z.ZodOptional<z.ZodString>;
78
+ }, "strict", z.ZodTypeAny, {
79
+ selector: string;
80
+ action: "fill" | "click";
81
+ value_env?: string | undefined;
82
+ }, {
83
+ selector: string;
84
+ action: "fill" | "click";
85
+ value_env?: string | undefined;
86
+ }>, "many">>;
87
+ inbox: z.ZodObject<{
88
+ provider: z.ZodDefault<z.ZodString>;
89
+ /** Provider-specific options (for `http-json`: `{ url, poll_interval_ms? }`). */
90
+ options: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
91
+ /** Env var *name* holding the inbox address to watch. Default: the role's `username` credential. */
92
+ to_env: z.ZodOptional<z.ZodString>;
93
+ /** Code-extraction regex (first capture group, else the whole match). */
94
+ code_pattern: z.ZodDefault<z.ZodString>;
95
+ /** How long to wait for the code mail. Default 30000. */
96
+ timeout_ms: z.ZodDefault<z.ZodNumber>;
97
+ }, "strict", z.ZodTypeAny, {
98
+ options: Record<string, unknown>;
99
+ timeout_ms: number;
100
+ provider: string;
101
+ code_pattern: string;
102
+ to_env?: string | undefined;
103
+ }, {
104
+ options?: Record<string, unknown> | undefined;
105
+ timeout_ms?: number | undefined;
106
+ provider?: string | undefined;
107
+ to_env?: string | undefined;
108
+ code_pattern?: string | undefined;
109
+ }>;
110
+ }, "strict", z.ZodTypeAny, {
111
+ timeout_ms: number;
112
+ login_url: string;
113
+ ignore_https_errors: boolean;
114
+ otp_selector: string;
115
+ submit_selector: string;
116
+ username_selector: string;
117
+ password_selector: string;
118
+ pre_steps: {
119
+ selector: string;
120
+ action: "fill" | "click";
121
+ value_env?: string | undefined;
122
+ }[];
123
+ inbox: {
124
+ options: Record<string, unknown>;
125
+ timeout_ms: number;
126
+ provider: string;
127
+ code_pattern: string;
128
+ to_env?: string | undefined;
129
+ };
130
+ url_matches?: string | undefined;
131
+ success_selector?: string | undefined;
132
+ otp_submit_selector?: string | undefined;
133
+ }, {
134
+ login_url: string;
135
+ otp_selector: string;
136
+ submit_selector: string;
137
+ username_selector: string;
138
+ password_selector: string;
139
+ inbox: {
140
+ options?: Record<string, unknown> | undefined;
141
+ timeout_ms?: number | undefined;
142
+ provider?: string | undefined;
143
+ to_env?: string | undefined;
144
+ code_pattern?: string | undefined;
145
+ };
146
+ timeout_ms?: number | undefined;
147
+ url_matches?: string | undefined;
148
+ ignore_https_errors?: boolean | undefined;
149
+ success_selector?: string | undefined;
150
+ pre_steps?: {
151
+ selector: string;
152
+ action: "fill" | "click";
153
+ value_env?: string | undefined;
154
+ }[] | undefined;
155
+ otp_submit_selector?: string | undefined;
156
+ }>, {
157
+ timeout_ms: number;
158
+ login_url: string;
159
+ ignore_https_errors: boolean;
160
+ otp_selector: string;
161
+ submit_selector: string;
162
+ username_selector: string;
163
+ password_selector: string;
164
+ pre_steps: {
165
+ selector: string;
166
+ action: "fill" | "click";
167
+ value_env?: string | undefined;
168
+ }[];
169
+ inbox: {
170
+ options: Record<string, unknown>;
171
+ timeout_ms: number;
172
+ provider: string;
173
+ code_pattern: string;
174
+ to_env?: string | undefined;
175
+ };
176
+ url_matches?: string | undefined;
177
+ success_selector?: string | undefined;
178
+ otp_submit_selector?: string | undefined;
179
+ }, {
180
+ login_url: string;
181
+ otp_selector: string;
182
+ submit_selector: string;
183
+ username_selector: string;
184
+ password_selector: string;
185
+ inbox: {
186
+ options?: Record<string, unknown> | undefined;
187
+ timeout_ms?: number | undefined;
188
+ provider?: string | undefined;
189
+ to_env?: string | undefined;
190
+ code_pattern?: string | undefined;
191
+ };
192
+ timeout_ms?: number | undefined;
193
+ url_matches?: string | undefined;
194
+ ignore_https_errors?: boolean | undefined;
195
+ success_selector?: string | undefined;
196
+ pre_steps?: {
197
+ selector: string;
198
+ action: "fill" | "click";
199
+ value_env?: string | undefined;
200
+ }[] | undefined;
201
+ otp_submit_selector?: string | undefined;
202
+ }>;
203
+ export type EmailOtpOptions = z.infer<typeof EmailOtpOptions>;
204
+ export declare class EmailOtpStrategy implements AuthStrategy {
205
+ private readonly launcher;
206
+ private readonly env;
207
+ readonly name: "email-otp";
208
+ constructor(launcher?: AuthPageLauncher, env?: NodeJS.ProcessEnv);
209
+ authenticate(ctx: AuthContext): Promise<AuthResult>;
210
+ }
@@ -0,0 +1,166 @@
1
+ // `email-otp` — a ui-form login whose second factor arrives by email: fill + submit the
2
+ // credential form, wait for the OTP prompt, poll an inbox for the code mail, extract the code
3
+ // with `code_pattern`, submit it, capture storageState.
4
+ //
5
+ // The inbox side is pluggable: an `InboxProvider` answers "the first message for <address>
6
+ // received after <instant>". The built-in `http-json` provider polls a Mailpit-style JSON
7
+ // endpoint; test inboxes with other shapes register theirs via `registerInboxProvider` (the
8
+ // plugins runtime exposes the same hook).
9
+ import { z } from "zod";
10
+ import { launchAuthPage } from "./browser-session.js";
11
+ import { jarAuthExpiry } from "./cookie-jar.js";
12
+ import { PreStep, requireEnvVar, resolvePreStepValues, runPreSteps, waitForLoginSuccess, } from "./ui-form.js";
13
+ import { AuthStrategyConfigError, maskSecret, parseStrategyOptions, } from "./types.js";
14
+ export const HttpJsonInboxOptions = z
15
+ .object({
16
+ /** Inbox endpoint answering `{ messages: [{ to, received_at, body }] }`. */
17
+ url: z.string().min(1),
18
+ poll_interval_ms: z.number().int().positive().default(250),
19
+ })
20
+ .strict();
21
+ /** Built-in provider: poll a Mailpit-style JSON inbox until the code mail shows up. */
22
+ export function httpJsonInboxProvider(options) {
23
+ const opts = parseStrategyOptions("email-otp (inbox http-json)", HttpJsonInboxOptions, options);
24
+ return {
25
+ async waitForMessage({ to, since, timeoutMs }) {
26
+ const deadline = Date.now() + timeoutMs;
27
+ for (;;) {
28
+ const res = await fetch(opts.url);
29
+ if (res.ok) {
30
+ const parsed = (await res.json());
31
+ const hit = (parsed.messages ?? [])
32
+ .filter((m) => m.to === to && Date.parse(m.received_at ?? "") >= since)
33
+ .at(-1);
34
+ if (hit) {
35
+ return { to: hit.to, receivedAt: Date.parse(hit.received_at), body: hit.body ?? "" };
36
+ }
37
+ }
38
+ if (Date.now() >= deadline) {
39
+ throw new AuthStrategyConfigError(`email-otp: no message for the watched address arrived within ${timeoutMs}ms`);
40
+ }
41
+ await new Promise((r) => setTimeout(r, opts.poll_interval_ms));
42
+ }
43
+ },
44
+ };
45
+ }
46
+ const inboxProviders = new Map([
47
+ ["http-json", httpJsonInboxProvider],
48
+ ]);
49
+ /** Register (or override) an inbox provider under `name` — the email-otp plugin hook. */
50
+ export function registerInboxProvider(name, factory) {
51
+ inboxProviders.set(name, factory);
52
+ }
53
+ /** Remove a registered provider (test/plugin teardown). Removing `http-json` restores the built-in. */
54
+ export function unregisterInboxProvider(name) {
55
+ inboxProviders.delete(name);
56
+ if (name === "http-json")
57
+ inboxProviders.set("http-json", httpJsonInboxProvider);
58
+ }
59
+ export function makeInboxProvider(name, options) {
60
+ const factory = inboxProviders.get(name);
61
+ if (!factory) {
62
+ throw new AuthStrategyConfigError(`email-otp: unknown inbox provider "${name}" (known: ${[...inboxProviders.keys()].join(", ")})`);
63
+ }
64
+ return factory(options);
65
+ }
66
+ // ---------------------------------------------------------------------------
67
+ // Strategy
68
+ // ---------------------------------------------------------------------------
69
+ export const EmailOtpInboxOptions = z
70
+ .object({
71
+ provider: z.string().min(1).default("http-json"),
72
+ /** Provider-specific options (for `http-json`: `{ url, poll_interval_ms? }`). */
73
+ options: z.record(z.string(), z.unknown()).default({}),
74
+ /** Env var *name* holding the inbox address to watch. Default: the role's `username` credential. */
75
+ to_env: z.string().min(1).optional(),
76
+ /** Code-extraction regex (first capture group, else the whole match). */
77
+ code_pattern: z.string().min(1).default("\\b(\\d{6})\\b"),
78
+ /** How long to wait for the code mail. Default 30000. */
79
+ timeout_ms: z.number().int().positive().default(30_000),
80
+ })
81
+ .strict();
82
+ export const EmailOtpOptions = z
83
+ .object({
84
+ login_url: z.string().min(1),
85
+ username_selector: z.string().min(1),
86
+ password_selector: z.string().min(1),
87
+ submit_selector: z.string().min(1),
88
+ otp_selector: z.string().min(1),
89
+ otp_submit_selector: z.string().min(1).optional(),
90
+ success_selector: z.string().min(1).optional(),
91
+ url_matches: z.string().min(1).optional(),
92
+ timeout_ms: z.number().int().positive().default(15_000),
93
+ ignore_https_errors: z.boolean().default(false),
94
+ pre_steps: z.array(PreStep).default([]),
95
+ inbox: EmailOtpInboxOptions,
96
+ })
97
+ .strict()
98
+ .refine((o) => o.success_selector !== undefined || o.url_matches !== undefined, {
99
+ message: "one of success_selector or url_matches is required",
100
+ });
101
+ export class EmailOtpStrategy {
102
+ launcher;
103
+ env;
104
+ name = "email-otp";
105
+ constructor(launcher = launchAuthPage, env = process.env) {
106
+ this.launcher = launcher;
107
+ this.env = env;
108
+ }
109
+ async authenticate(ctx) {
110
+ const opts = parseStrategyOptions(this.name, EmailOtpOptions, ctx.options);
111
+ const { username, password } = ctx.creds;
112
+ if (!username || !password) {
113
+ throw new AuthStrategyConfigError(`email-otp: creds_env must map "username" (${maskSecret(username)}) and "password" (${maskSecret(password)})`);
114
+ }
115
+ const to = opts.inbox.to_env
116
+ ? requireEnvVar(this.name, "inbox.to_env", opts.inbox.to_env, this.env)
117
+ : username;
118
+ let codePattern;
119
+ try {
120
+ codePattern = new RegExp(opts.inbox.code_pattern);
121
+ }
122
+ catch {
123
+ throw new AuthStrategyConfigError(`email-otp: inbox.code_pattern /${opts.inbox.code_pattern}/ is not a valid regex`);
124
+ }
125
+ const preStepValues = resolvePreStepValues(this.name, opts.pre_steps, this.env);
126
+ const provider = makeInboxProvider(opts.inbox.provider, opts.inbox.options);
127
+ const page = await this.launcher({
128
+ baseURL: ctx.baseURL,
129
+ ...(opts.ignore_https_errors ? { ignoreHTTPSErrors: true } : {}),
130
+ });
131
+ try {
132
+ await page.goto(new URL(opts.login_url, ctx.baseURL).href);
133
+ await runPreSteps(page, opts.pre_steps, preStepValues);
134
+ await page.fill(opts.username_selector, username);
135
+ await page.fill(opts.password_selector, password);
136
+ const since = Date.now(); // the code mail is sent in response to this submit
137
+ await page.click(opts.submit_selector);
138
+ try {
139
+ await page.waitForSelector(opts.otp_selector, { timeoutMs: opts.timeout_ms });
140
+ }
141
+ catch {
142
+ throw new AuthStrategyConfigError(`email-otp: the OTP prompt ("${opts.otp_selector}") did not appear within ${opts.timeout_ms}ms — check the credential env vars (values not shown) and the selectors`);
143
+ }
144
+ const message = await provider.waitForMessage({
145
+ to,
146
+ since,
147
+ timeoutMs: opts.inbox.timeout_ms,
148
+ });
149
+ const match = codePattern.exec(message.body);
150
+ const code = match ? (match[1] ?? match[0]) : undefined;
151
+ if (!code) {
152
+ throw new AuthStrategyConfigError(`email-otp: inbox.code_pattern /${opts.inbox.code_pattern}/ did not match the received message`);
153
+ }
154
+ await page.fill(opts.otp_selector, code);
155
+ if (opts.otp_submit_selector)
156
+ await page.click(opts.otp_submit_selector);
157
+ await waitForLoginSuccess(page, opts, this.name);
158
+ const storageState = await page.storageState();
159
+ const expiresAt = jarAuthExpiry(storageState);
160
+ return { storageState, ...(expiresAt !== undefined ? { expiresAt } : {}) };
161
+ }
162
+ finally {
163
+ await page.close();
164
+ }
165
+ }
166
+ }
@@ -0,0 +1,5 @@
1
+ import { type AuthContext, type AuthResult, type AuthStrategy } from "./types.js";
2
+ export declare class HttpBasicStrategy implements AuthStrategy {
3
+ readonly name: "http-basic";
4
+ authenticate(ctx: AuthContext): Promise<AuthResult>;
5
+ }
@@ -0,0 +1,17 @@
1
+ // `http-basic` — connection-level auth: the browser context answers 401 challenges with the
2
+ // role's credentials. Nothing to capture; the strategy emits `contextOptions.httpCredentials`
3
+ // and an empty storageState.
4
+ import { AuthStrategyConfigError, emptyStorageState, maskSecret, } from "./types.js";
5
+ export class HttpBasicStrategy {
6
+ name = "http-basic";
7
+ authenticate(ctx) {
8
+ const { username, password } = ctx.creds;
9
+ if (!username || !password) {
10
+ return Promise.reject(new AuthStrategyConfigError(`http-basic: creds_env must map "username" (${maskSecret(username)}) and "password" (${maskSecret(password)})`));
11
+ }
12
+ return Promise.resolve({
13
+ storageState: emptyStorageState(),
14
+ contextOptions: { httpCredentials: { username, password } },
15
+ });
16
+ }
17
+ }
@@ -0,0 +1,47 @@
1
+ import { AuthStrategyDescriptor, type RoleAuth } from "../doc-pack.js";
2
+ import { type InstrumentedBrowser } from "./manual-capture.js";
3
+ import { type AuthStrategy } from "./types.js";
4
+ export * from "./types.js";
5
+ export * from "./cookie-jar.js";
6
+ export * from "./api-login.js";
7
+ export * from "./browser-session.js";
8
+ export * from "./email-otp.js";
9
+ export * from "./http-basic.js";
10
+ export * from "./jwt-injection.js";
11
+ export * from "./manual-capture.js";
12
+ export * from "./mtls.js";
13
+ export * from "./pat-header.js";
14
+ export * from "./storage-state-cache.js";
15
+ export * from "./test-backdoor.js";
16
+ export * from "./totp.js";
17
+ export * from "./ui-form.js";
18
+ export * from "./webauthn.js";
19
+ /** Parse + validate an `auth/strategy.yaml` descriptor from YAML text. */
20
+ export declare function parseAuthStrategyFile(yamlText: string, source?: string): AuthStrategyDescriptor;
21
+ /** Resolve a role's `creds_env` name map into actual values from an env source (defaults to `process.env`). */
22
+ export declare function resolveCredsEnv(roleAuth: RoleAuth, env?: NodeJS.ProcessEnv): Record<string, string>;
23
+ /**
24
+ * Like {@link resolveCredsEnv}, with **user-pool** support: any credential env value may be a
25
+ * comma-separated pool (`user1,user2,user3`); each parallel worker picks `pool[workerIndex % len]`,
26
+ * consistently across every pooled variable, so worker N always gets user N's username *and* password.
27
+ */
28
+ export declare function resolveCreds(roleAuth: RoleAuth, opts?: {
29
+ workerIndex?: number;
30
+ env?: NodeJS.ProcessEnv;
31
+ }): Record<string, string>;
32
+ export interface StrategyDeps {
33
+ /** Factory for the instrumented browser `manual-capture` drives. Required if any role uses `manual-capture`. */
34
+ instrumentedBrowser?: () => InstrumentedBrowser;
35
+ /** Env source for strategies that read env-var *names* out of options (`token_env`, `totp.secret_env`). Defaults to `process.env`. */
36
+ env?: NodeJS.ProcessEnv;
37
+ }
38
+ /**
39
+ * Register (or override) an auth strategy under `name`. The plugins-runtime hook: registered
40
+ * strategies are consulted *before* the built-ins, so a plugin can both add new schemes and
41
+ * replace a built-in for a quirky target.
42
+ */
43
+ export declare function registerAuthStrategy(name: string, impl: AuthStrategy): void;
44
+ /** Remove a registered strategy (test/plugin teardown). */
45
+ export declare function unregisterAuthStrategy(name: string): void;
46
+ /** Build the {@link AuthStrategy} for a role: registry first, then the built-in catalogue. */
47
+ export declare function makeStrategy(roleAuth: RoleAuth, deps: StrategyDeps): AuthStrategy;