@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.
- package/LICENSE +202 -0
- package/README.md +130 -0
- package/dist/auth/api-login.d.ts +69 -0
- package/dist/auth/api-login.js +95 -0
- package/dist/auth/browser-session.d.ts +28 -0
- package/dist/auth/browser-session.js +43 -0
- package/dist/auth/cookie-jar.d.ts +58 -0
- package/dist/auth/cookie-jar.js +212 -0
- package/dist/auth/email-otp.d.ts +210 -0
- package/dist/auth/email-otp.js +166 -0
- package/dist/auth/http-basic.d.ts +5 -0
- package/dist/auth/http-basic.js +17 -0
- package/dist/auth/index.d.ts +47 -0
- package/dist/auth/index.js +137 -0
- package/dist/auth/jwt-injection.d.ts +153 -0
- package/dist/auth/jwt-injection.js +136 -0
- package/dist/auth/manual-capture.d.ts +35 -0
- package/dist/auth/manual-capture.js +30 -0
- package/dist/auth/mtls.d.ts +15 -0
- package/dist/auth/mtls.js +53 -0
- package/dist/auth/pat-header.d.ts +19 -0
- package/dist/auth/pat-header.js +34 -0
- package/dist/auth/storage-state-cache.d.ts +38 -0
- package/dist/auth/storage-state-cache.js +143 -0
- package/dist/auth/test-backdoor.d.ts +25 -0
- package/dist/auth/test-backdoor.js +51 -0
- package/dist/auth/totp.d.ts +39 -0
- package/dist/auth/totp.js +108 -0
- package/dist/auth/types.d.ts +86 -0
- package/dist/auth/types.js +57 -0
- package/dist/auth/ui-form.d.ts +204 -0
- package/dist/auth/ui-form.js +153 -0
- package/dist/auth/webauthn.d.ts +88 -0
- package/dist/auth/webauthn.js +67 -0
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +3 -0
- package/dist/backend-client-contracts.d.ts +88 -0
- package/dist/backend-client-contracts.js +19 -0
- package/dist/backend-client-oauth-login.d.ts +7 -0
- package/dist/backend-client-oauth-login.js +90 -0
- package/dist/backend-client-state-cache.d.ts +73 -0
- package/dist/backend-client-state-cache.js +185 -0
- package/dist/backend-client-token.d.ts +18 -0
- package/dist/backend-client-token.js +94 -0
- package/dist/backend-client-transport.d.ts +66 -0
- package/dist/backend-client-transport.js +181 -0
- package/dist/backend-client.d.ts +5 -0
- package/dist/backend-client.js +18 -0
- package/dist/calibrate.d.ts +31 -0
- package/dist/calibrate.js +68 -0
- package/dist/cli-commands-authoring.d.ts +5 -0
- package/dist/cli-commands-authoring.js +403 -0
- package/dist/cli-commands-backend.d.ts +5 -0
- package/dist/cli-commands-backend.js +211 -0
- package/dist/cli-commands-docpack.d.ts +5 -0
- package/dist/cli-commands-docpack.js +280 -0
- package/dist/cli-commands-session.d.ts +4 -0
- package/dist/cli-commands-session.js +398 -0
- package/dist/cli-shared.d.ts +5 -0
- package/dist/cli-shared.js +45 -0
- package/dist/cli-usage.d.ts +1 -0
- package/dist/cli-usage.js +137 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +77 -0
- package/dist/diagnose.d.ts +50 -0
- package/dist/diagnose.js +168 -0
- package/dist/diff-compute.d.ts +13 -0
- package/dist/diff-compute.js +378 -0
- package/dist/diff-report.d.ts +7 -0
- package/dist/diff-report.js +125 -0
- package/dist/diff-types.d.ts +125 -0
- package/dist/diff-types.js +15 -0
- package/dist/diff.d.ts +3 -0
- package/dist/diff.js +16 -0
- package/dist/doc-pack-io.d.ts +30 -0
- package/dist/doc-pack-io.js +182 -0
- package/dist/doc-pack.d.ts +1814 -0
- package/dist/doc-pack.js +328 -0
- package/dist/doctor-checks-plugins.d.ts +2 -0
- package/dist/doctor-checks-plugins.js +136 -0
- package/dist/doctor-checks.d.ts +56 -0
- package/dist/doctor-checks.js +367 -0
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +62 -0
- package/dist/export/adf.d.ts +57 -0
- package/dist/export/adf.js +323 -0
- package/dist/export/playwright-test.d.ts +26 -0
- package/dist/export/playwright-test.js +221 -0
- package/dist/flow-file.d.ts +21 -0
- package/dist/flow-file.js +180 -0
- package/dist/flow-lint.d.ts +24 -0
- package/dist/flow-lint.js +203 -0
- package/dist/flow-runtime.d.ts +113 -0
- package/dist/flow-runtime.js +273 -0
- package/dist/flow-tree.d.ts +19 -0
- package/dist/flow-tree.js +104 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +31 -0
- package/dist/playwright-driver.d.ts +105 -0
- package/dist/playwright-driver.js +363 -0
- package/dist/playwright-instrumented-browser.d.ts +51 -0
- package/dist/playwright-instrumented-browser.js +189 -0
- package/dist/plugins/load.d.ts +22 -0
- package/dist/plugins/load.js +99 -0
- package/dist/plugins/lock.d.ts +40 -0
- package/dist/plugins/lock.js +122 -0
- package/dist/plugins/manifest.d.ts +70 -0
- package/dist/plugins/manifest.js +115 -0
- package/dist/plugins/plan.d.ts +51 -0
- package/dist/plugins/plan.js +279 -0
- package/dist/plugins/registry.d.ts +59 -0
- package/dist/plugins/registry.js +71 -0
- package/dist/plugins/runtime.d.ts +7 -0
- package/dist/plugins/runtime.js +27 -0
- package/dist/plugins/types.d.ts +58 -0
- package/dist/plugins/types.js +4 -0
- package/dist/plugins-cli.d.ts +1 -0
- package/dist/plugins-cli.js +191 -0
- package/dist/redact.d.ts +16 -0
- package/dist/redact.js +72 -0
- package/dist/style.d.ts +46 -0
- package/dist/style.js +151 -0
- package/dist/viewer-bin.d.ts +20 -0
- package/dist/viewer-bin.js +97 -0
- package/dist/workspace.d.ts +60 -0
- package/dist/workspace.js +172 -0
- package/dist/zip.d.ts +17 -0
- package/dist/zip.js +113 -0
- package/package.json +64 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Target-site auth layer.
|
|
2
|
+
//
|
|
3
|
+
// Every strategy produces a `storageState` (cookies + localStorage + sessionStorage) — the
|
|
4
|
+
// universal artifact every auth scheme reduces to — plus, for connection-level schemes
|
|
5
|
+
// (HTTP Basic, PAT headers, mTLS), `contextOptions` the session launcher passes through to
|
|
6
|
+
// the browser context. One module per strategy; this index holds the descriptor parsing,
|
|
7
|
+
// the credential resolution, and the strategy registry.
|
|
8
|
+
import { parse as parseYaml } from "yaml";
|
|
9
|
+
import { AuthStrategyDescriptor } from "../doc-pack.js";
|
|
10
|
+
import { ApiLoginStrategy } from "./api-login.js";
|
|
11
|
+
import { launchAuthPage } from "./browser-session.js";
|
|
12
|
+
import { EmailOtpStrategy } from "./email-otp.js";
|
|
13
|
+
import { HttpBasicStrategy } from "./http-basic.js";
|
|
14
|
+
import { JwtInjectionStrategy } from "./jwt-injection.js";
|
|
15
|
+
import { ManualCaptureStrategy } from "./manual-capture.js";
|
|
16
|
+
import { MtlsStrategy } from "./mtls.js";
|
|
17
|
+
import { PatHeaderStrategy } from "./pat-header.js";
|
|
18
|
+
import { TestBackdoorStrategy } from "./test-backdoor.js";
|
|
19
|
+
import { TotpStrategy } from "./totp.js";
|
|
20
|
+
import { UiFormStrategy } from "./ui-form.js";
|
|
21
|
+
import { WebauthnStrategy } from "./webauthn.js";
|
|
22
|
+
import { AuthStrategyConfigError, NotImplementedStrategyError, } from "./types.js";
|
|
23
|
+
export * from "./types.js";
|
|
24
|
+
export * from "./cookie-jar.js";
|
|
25
|
+
export * from "./api-login.js";
|
|
26
|
+
export * from "./browser-session.js";
|
|
27
|
+
export * from "./email-otp.js";
|
|
28
|
+
export * from "./http-basic.js";
|
|
29
|
+
export * from "./jwt-injection.js";
|
|
30
|
+
export * from "./manual-capture.js";
|
|
31
|
+
export * from "./mtls.js";
|
|
32
|
+
export * from "./pat-header.js";
|
|
33
|
+
export * from "./storage-state-cache.js";
|
|
34
|
+
export * from "./test-backdoor.js";
|
|
35
|
+
export * from "./totp.js";
|
|
36
|
+
export * from "./ui-form.js";
|
|
37
|
+
export * from "./webauthn.js";
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Descriptor (`auth/strategy.yaml`)
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
/** Parse + validate an `auth/strategy.yaml` descriptor from YAML text. */
|
|
42
|
+
export function parseAuthStrategyFile(yamlText, source = "<auth/strategy.yaml>") {
|
|
43
|
+
let raw;
|
|
44
|
+
try {
|
|
45
|
+
raw = parseYaml(yamlText);
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
throw new AuthStrategyConfigError(`${source}: not valid YAML — ${e.message}`, e);
|
|
49
|
+
}
|
|
50
|
+
const r = AuthStrategyDescriptor.safeParse(raw);
|
|
51
|
+
if (!r.success) {
|
|
52
|
+
const issues = r.error.issues
|
|
53
|
+
.map((i) => ` • ${i.path.length ? i.path.join(".") : "(root)"}: ${i.message}`)
|
|
54
|
+
.join("\n");
|
|
55
|
+
throw new AuthStrategyConfigError(`${source}: invalid auth-strategy descriptor:\n${issues}`, r.error);
|
|
56
|
+
}
|
|
57
|
+
return r.data;
|
|
58
|
+
}
|
|
59
|
+
/** Resolve a role's `creds_env` name map into actual values from an env source (defaults to `process.env`). */
|
|
60
|
+
export function resolveCredsEnv(roleAuth, env = process.env) {
|
|
61
|
+
const out = {};
|
|
62
|
+
const missing = [];
|
|
63
|
+
for (const [key, varName] of Object.entries(roleAuth.creds_env)) {
|
|
64
|
+
const v = env[varName];
|
|
65
|
+
if (v === undefined || v === "")
|
|
66
|
+
missing.push(`${key} → $${varName}`);
|
|
67
|
+
else
|
|
68
|
+
out[key] = v;
|
|
69
|
+
}
|
|
70
|
+
if (missing.length) {
|
|
71
|
+
throw new AuthStrategyConfigError(`missing credential env vars:\n${missing.map((m) => ` • ${m}`).join("\n")}`);
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Like {@link resolveCredsEnv}, with **user-pool** support: any credential env value may be a
|
|
77
|
+
* comma-separated pool (`user1,user2,user3`); each parallel worker picks `pool[workerIndex % len]`,
|
|
78
|
+
* consistently across every pooled variable, so worker N always gets user N's username *and* password.
|
|
79
|
+
*/
|
|
80
|
+
export function resolveCreds(roleAuth, opts = {}) {
|
|
81
|
+
const workerIndex = opts.workerIndex ?? 0;
|
|
82
|
+
const raw = resolveCredsEnv(roleAuth, opts.env ?? process.env);
|
|
83
|
+
const out = {};
|
|
84
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
85
|
+
const pool = value.split(",").map((v) => v.trim());
|
|
86
|
+
out[key] = pool.length > 1 ? pool[workerIndex % pool.length] : value;
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
const registry = new Map();
|
|
91
|
+
/**
|
|
92
|
+
* Register (or override) an auth strategy under `name`. The plugins-runtime hook: registered
|
|
93
|
+
* strategies are consulted *before* the built-ins, so a plugin can both add new schemes and
|
|
94
|
+
* replace a built-in for a quirky target.
|
|
95
|
+
*/
|
|
96
|
+
export function registerAuthStrategy(name, impl) {
|
|
97
|
+
registry.set(name, impl);
|
|
98
|
+
}
|
|
99
|
+
/** Remove a registered strategy (test/plugin teardown). */
|
|
100
|
+
export function unregisterAuthStrategy(name) {
|
|
101
|
+
registry.delete(name);
|
|
102
|
+
}
|
|
103
|
+
/** Build the {@link AuthStrategy} for a role: registry first, then the built-in catalogue. */
|
|
104
|
+
export function makeStrategy(roleAuth, deps) {
|
|
105
|
+
const registered = registry.get(roleAuth.strategy);
|
|
106
|
+
if (registered)
|
|
107
|
+
return registered;
|
|
108
|
+
switch (roleAuth.strategy) {
|
|
109
|
+
case "manual-capture":
|
|
110
|
+
if (!deps.instrumentedBrowser) {
|
|
111
|
+
throw new AuthStrategyConfigError("strategy `manual-capture` requires an instrumented-browser factory (deps.instrumentedBrowser)");
|
|
112
|
+
}
|
|
113
|
+
return new ManualCaptureStrategy(deps.instrumentedBrowser);
|
|
114
|
+
case "api-login":
|
|
115
|
+
return new ApiLoginStrategy();
|
|
116
|
+
case "jwt-injection":
|
|
117
|
+
return new JwtInjectionStrategy(fetch, deps.env);
|
|
118
|
+
case "http-basic":
|
|
119
|
+
return new HttpBasicStrategy();
|
|
120
|
+
case "pat-header":
|
|
121
|
+
return new PatHeaderStrategy();
|
|
122
|
+
case "mtls":
|
|
123
|
+
return new MtlsStrategy();
|
|
124
|
+
case "test-backdoor":
|
|
125
|
+
return new TestBackdoorStrategy();
|
|
126
|
+
case "totp":
|
|
127
|
+
return new TotpStrategy();
|
|
128
|
+
case "ui-form":
|
|
129
|
+
return new UiFormStrategy(launchAuthPage, deps.env);
|
|
130
|
+
case "email-otp":
|
|
131
|
+
return new EmailOtpStrategy(launchAuthPage, deps.env);
|
|
132
|
+
case "webauthn":
|
|
133
|
+
return new WebauthnStrategy(launchAuthPage, deps.env);
|
|
134
|
+
default:
|
|
135
|
+
throw new NotImplementedStrategyError(roleAuth.strategy);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type AuthContext, type AuthResult, type AuthStrategy } from "./types.js";
|
|
3
|
+
export declare const JwtInjectionOptions: z.ZodEffects<z.ZodObject<{
|
|
4
|
+
/** Env var *name* holding a static token. Mutually exclusive with `token_url`. */
|
|
5
|
+
token_env: z.ZodOptional<z.ZodString>;
|
|
6
|
+
/** OAuth2 token endpoint for a client-credentials grant (creds: `client_id`, `client_secret`). */
|
|
7
|
+
token_url: z.ZodOptional<z.ZodString>;
|
|
8
|
+
/** Where the token goes. At least one of `localStorage` / `cookies`. */
|
|
9
|
+
inject: z.ZodEffects<z.ZodObject<{
|
|
10
|
+
localStorage: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
11
|
+
key: z.ZodString;
|
|
12
|
+
/** `{{token}}` template; default `{{token}}`. */
|
|
13
|
+
value_template: z.ZodDefault<z.ZodString>;
|
|
14
|
+
}, "strict", z.ZodTypeAny, {
|
|
15
|
+
key: string;
|
|
16
|
+
value_template: string;
|
|
17
|
+
}, {
|
|
18
|
+
key: string;
|
|
19
|
+
value_template?: string | undefined;
|
|
20
|
+
}>, "many">>;
|
|
21
|
+
cookies: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
22
|
+
name: z.ZodString;
|
|
23
|
+
value_template: z.ZodDefault<z.ZodString>;
|
|
24
|
+
domain: z.ZodOptional<z.ZodString>;
|
|
25
|
+
path: z.ZodOptional<z.ZodString>;
|
|
26
|
+
}, "strict", z.ZodTypeAny, {
|
|
27
|
+
name: string;
|
|
28
|
+
value_template: string;
|
|
29
|
+
path?: string | undefined;
|
|
30
|
+
domain?: string | undefined;
|
|
31
|
+
}, {
|
|
32
|
+
name: string;
|
|
33
|
+
path?: string | undefined;
|
|
34
|
+
domain?: string | undefined;
|
|
35
|
+
value_template?: string | undefined;
|
|
36
|
+
}>, "many">>;
|
|
37
|
+
}, "strict", z.ZodTypeAny, {
|
|
38
|
+
cookies?: {
|
|
39
|
+
name: string;
|
|
40
|
+
value_template: string;
|
|
41
|
+
path?: string | undefined;
|
|
42
|
+
domain?: string | undefined;
|
|
43
|
+
}[] | undefined;
|
|
44
|
+
localStorage?: {
|
|
45
|
+
key: string;
|
|
46
|
+
value_template: string;
|
|
47
|
+
}[] | undefined;
|
|
48
|
+
}, {
|
|
49
|
+
cookies?: {
|
|
50
|
+
name: string;
|
|
51
|
+
path?: string | undefined;
|
|
52
|
+
domain?: string | undefined;
|
|
53
|
+
value_template?: string | undefined;
|
|
54
|
+
}[] | undefined;
|
|
55
|
+
localStorage?: {
|
|
56
|
+
key: string;
|
|
57
|
+
value_template?: string | undefined;
|
|
58
|
+
}[] | undefined;
|
|
59
|
+
}>, {
|
|
60
|
+
cookies?: {
|
|
61
|
+
name: string;
|
|
62
|
+
value_template: string;
|
|
63
|
+
path?: string | undefined;
|
|
64
|
+
domain?: string | undefined;
|
|
65
|
+
}[] | undefined;
|
|
66
|
+
localStorage?: {
|
|
67
|
+
key: string;
|
|
68
|
+
value_template: string;
|
|
69
|
+
}[] | undefined;
|
|
70
|
+
}, {
|
|
71
|
+
cookies?: {
|
|
72
|
+
name: string;
|
|
73
|
+
path?: string | undefined;
|
|
74
|
+
domain?: string | undefined;
|
|
75
|
+
value_template?: string | undefined;
|
|
76
|
+
}[] | undefined;
|
|
77
|
+
localStorage?: {
|
|
78
|
+
key: string;
|
|
79
|
+
value_template?: string | undefined;
|
|
80
|
+
}[] | undefined;
|
|
81
|
+
}>;
|
|
82
|
+
}, "strict", z.ZodTypeAny, {
|
|
83
|
+
inject: {
|
|
84
|
+
cookies?: {
|
|
85
|
+
name: string;
|
|
86
|
+
value_template: string;
|
|
87
|
+
path?: string | undefined;
|
|
88
|
+
domain?: string | undefined;
|
|
89
|
+
}[] | undefined;
|
|
90
|
+
localStorage?: {
|
|
91
|
+
key: string;
|
|
92
|
+
value_template: string;
|
|
93
|
+
}[] | undefined;
|
|
94
|
+
};
|
|
95
|
+
token_env?: string | undefined;
|
|
96
|
+
token_url?: string | undefined;
|
|
97
|
+
}, {
|
|
98
|
+
inject: {
|
|
99
|
+
cookies?: {
|
|
100
|
+
name: string;
|
|
101
|
+
path?: string | undefined;
|
|
102
|
+
domain?: string | undefined;
|
|
103
|
+
value_template?: string | undefined;
|
|
104
|
+
}[] | undefined;
|
|
105
|
+
localStorage?: {
|
|
106
|
+
key: string;
|
|
107
|
+
value_template?: string | undefined;
|
|
108
|
+
}[] | undefined;
|
|
109
|
+
};
|
|
110
|
+
token_env?: string | undefined;
|
|
111
|
+
token_url?: string | undefined;
|
|
112
|
+
}>, {
|
|
113
|
+
inject: {
|
|
114
|
+
cookies?: {
|
|
115
|
+
name: string;
|
|
116
|
+
value_template: string;
|
|
117
|
+
path?: string | undefined;
|
|
118
|
+
domain?: string | undefined;
|
|
119
|
+
}[] | undefined;
|
|
120
|
+
localStorage?: {
|
|
121
|
+
key: string;
|
|
122
|
+
value_template: string;
|
|
123
|
+
}[] | undefined;
|
|
124
|
+
};
|
|
125
|
+
token_env?: string | undefined;
|
|
126
|
+
token_url?: string | undefined;
|
|
127
|
+
}, {
|
|
128
|
+
inject: {
|
|
129
|
+
cookies?: {
|
|
130
|
+
name: string;
|
|
131
|
+
path?: string | undefined;
|
|
132
|
+
domain?: string | undefined;
|
|
133
|
+
value_template?: string | undefined;
|
|
134
|
+
}[] | undefined;
|
|
135
|
+
localStorage?: {
|
|
136
|
+
key: string;
|
|
137
|
+
value_template?: string | undefined;
|
|
138
|
+
}[] | undefined;
|
|
139
|
+
};
|
|
140
|
+
token_env?: string | undefined;
|
|
141
|
+
token_url?: string | undefined;
|
|
142
|
+
}>;
|
|
143
|
+
export type JwtInjectionOptions = z.infer<typeof JwtInjectionOptions>;
|
|
144
|
+
/** Decode a JWT's payload (base64url, **no signature verification**) — claims-reading only. */
|
|
145
|
+
export declare function decodeJwtPayload(token: string): Record<string, unknown> | undefined;
|
|
146
|
+
export declare class JwtInjectionStrategy implements AuthStrategy {
|
|
147
|
+
private readonly fetchImpl;
|
|
148
|
+
private readonly env;
|
|
149
|
+
readonly name: "jwt-injection";
|
|
150
|
+
constructor(fetchImpl?: typeof fetch, env?: NodeJS.ProcessEnv);
|
|
151
|
+
private obtainToken;
|
|
152
|
+
authenticate(ctx: AuthContext): Promise<AuthResult>;
|
|
153
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// `jwt-injection` — obtain a bearer token (static env var, or an OAuth2 client-credentials
|
|
2
|
+
// grant) and inject it into the browser's storage the way the target SPA expects it:
|
|
3
|
+
// localStorage keys and/or cookies, rendered from `{{token}}` templates. Pure node, no browser.
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { AuthStrategyConfigError, maskSecret, parseStrategyOptions, renderTemplate, } from "./types.js";
|
|
6
|
+
export const JwtInjectionOptions = z
|
|
7
|
+
.object({
|
|
8
|
+
/** Env var *name* holding a static token. Mutually exclusive with `token_url`. */
|
|
9
|
+
token_env: z.string().min(1).optional(),
|
|
10
|
+
/** OAuth2 token endpoint for a client-credentials grant (creds: `client_id`, `client_secret`). */
|
|
11
|
+
token_url: z.string().min(1).optional(),
|
|
12
|
+
/** Where the token goes. At least one of `localStorage` / `cookies`. */
|
|
13
|
+
inject: z
|
|
14
|
+
.object({
|
|
15
|
+
localStorage: z
|
|
16
|
+
.array(z
|
|
17
|
+
.object({
|
|
18
|
+
key: z.string().min(1),
|
|
19
|
+
/** `{{token}}` template; default `{{token}}`. */
|
|
20
|
+
value_template: z.string().min(1).default("{{token}}"),
|
|
21
|
+
})
|
|
22
|
+
.strict())
|
|
23
|
+
.optional(),
|
|
24
|
+
cookies: z
|
|
25
|
+
.array(z
|
|
26
|
+
.object({
|
|
27
|
+
name: z.string().min(1),
|
|
28
|
+
value_template: z.string().min(1).default("{{token}}"),
|
|
29
|
+
domain: z.string().min(1).optional(),
|
|
30
|
+
path: z.string().min(1).optional(),
|
|
31
|
+
})
|
|
32
|
+
.strict())
|
|
33
|
+
.optional(),
|
|
34
|
+
})
|
|
35
|
+
.strict()
|
|
36
|
+
.refine((i) => (i.localStorage?.length ?? 0) + (i.cookies?.length ?? 0) > 0, {
|
|
37
|
+
message: "inject needs at least one localStorage or cookies entry",
|
|
38
|
+
}),
|
|
39
|
+
})
|
|
40
|
+
.strict()
|
|
41
|
+
.refine((o) => (o.token_env !== undefined) !== (o.token_url !== undefined), {
|
|
42
|
+
message: "exactly one of token_env or token_url is required",
|
|
43
|
+
});
|
|
44
|
+
/** Decode a JWT's payload (base64url, **no signature verification**) — claims-reading only. */
|
|
45
|
+
export function decodeJwtPayload(token) {
|
|
46
|
+
const payload = token.split(".")[1];
|
|
47
|
+
if (!payload)
|
|
48
|
+
return undefined;
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
51
|
+
return parsed !== null && typeof parsed === "object"
|
|
52
|
+
? parsed
|
|
53
|
+
: undefined;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export class JwtInjectionStrategy {
|
|
60
|
+
fetchImpl;
|
|
61
|
+
env;
|
|
62
|
+
name = "jwt-injection";
|
|
63
|
+
constructor(fetchImpl = fetch, env = process.env) {
|
|
64
|
+
this.fetchImpl = fetchImpl;
|
|
65
|
+
this.env = env;
|
|
66
|
+
}
|
|
67
|
+
async obtainToken(opts, ctx) {
|
|
68
|
+
if (opts.token_env !== undefined) {
|
|
69
|
+
const token = this.env[opts.token_env];
|
|
70
|
+
if (!token) {
|
|
71
|
+
throw new AuthStrategyConfigError(`jwt-injection: token env var $${opts.token_env} is ${maskSecret(token)}`);
|
|
72
|
+
}
|
|
73
|
+
return { token, expiresAt: expiryFromJwt(token) };
|
|
74
|
+
}
|
|
75
|
+
const { client_id: clientId, client_secret: clientSecret } = ctx.creds;
|
|
76
|
+
if (!clientId || !clientSecret) {
|
|
77
|
+
throw new AuthStrategyConfigError(`jwt-injection: token_url needs creds_env mapping client_id (${maskSecret(clientId)}) and client_secret (${maskSecret(clientSecret)})`);
|
|
78
|
+
}
|
|
79
|
+
const tokenUrl = new URL(opts.token_url, ctx.baseURL);
|
|
80
|
+
const response = await this.fetchImpl(tokenUrl, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
83
|
+
body: new URLSearchParams({
|
|
84
|
+
grant_type: "client_credentials",
|
|
85
|
+
client_id: clientId,
|
|
86
|
+
client_secret: clientSecret,
|
|
87
|
+
}).toString(),
|
|
88
|
+
});
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
throw new AuthStrategyConfigError(`jwt-injection: token endpoint ${tokenUrl.href} answered ${response.status} (client_secret: ${maskSecret(clientSecret)} — value not shown)`);
|
|
91
|
+
}
|
|
92
|
+
let body;
|
|
93
|
+
try {
|
|
94
|
+
body = (await response.json());
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
throw new AuthStrategyConfigError(`jwt-injection: token endpoint ${tokenUrl.href} did not return JSON`);
|
|
98
|
+
}
|
|
99
|
+
if (!body.access_token) {
|
|
100
|
+
throw new AuthStrategyConfigError(`jwt-injection: token endpoint ${tokenUrl.href} returned no access_token`);
|
|
101
|
+
}
|
|
102
|
+
const fromExpiresIn = typeof body.expires_in === "number" ? Date.now() + body.expires_in * 1000 : undefined;
|
|
103
|
+
const expiresAt = fromExpiresIn ?? expiryFromJwt(body.access_token);
|
|
104
|
+
return { token: body.access_token, ...(expiresAt !== undefined ? { expiresAt } : {}) };
|
|
105
|
+
}
|
|
106
|
+
async authenticate(ctx) {
|
|
107
|
+
const opts = parseStrategyOptions(this.name, JwtInjectionOptions, ctx.options);
|
|
108
|
+
const { token, expiresAt } = await this.obtainToken(opts, ctx);
|
|
109
|
+
const base = new URL(ctx.baseURL);
|
|
110
|
+
const vars = { token };
|
|
111
|
+
const storageState = { cookies: [], origins: [] };
|
|
112
|
+
const entries = (opts.inject.localStorage ?? []).map((e) => ({
|
|
113
|
+
name: e.key,
|
|
114
|
+
value: renderTemplate(e.value_template, vars),
|
|
115
|
+
}));
|
|
116
|
+
if (entries.length)
|
|
117
|
+
storageState.origins.push({ origin: base.origin, localStorage: entries });
|
|
118
|
+
for (const c of opts.inject.cookies ?? []) {
|
|
119
|
+
storageState.cookies.push({
|
|
120
|
+
name: c.name,
|
|
121
|
+
value: renderTemplate(c.value_template, vars),
|
|
122
|
+
domain: c.domain ?? base.hostname,
|
|
123
|
+
path: c.path ?? "/",
|
|
124
|
+
expires: expiresAt !== undefined ? Math.floor(expiresAt / 1000) : -1,
|
|
125
|
+
httpOnly: false,
|
|
126
|
+
secure: base.protocol === "https:",
|
|
127
|
+
sameSite: "Lax",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return { storageState, ...(expiresAt !== undefined ? { expiresAt } : {}) };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function expiryFromJwt(token) {
|
|
134
|
+
const exp = decodeJwtPayload(token)?.exp;
|
|
135
|
+
return typeof exp === "number" ? exp * 1000 : undefined;
|
|
136
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type AuthContext, type AuthResult, type AuthStrategy, type StorageState } from "./types.js";
|
|
2
|
+
export type CaptureTrigger = "console" | "button";
|
|
3
|
+
/**
|
|
4
|
+
* The browser the engine drives for `manual-capture` — a security-lowered, *instrumented* Chrome.
|
|
5
|
+
* The Playwright-backed implementation lives in a separate module (so this strategy stays testable);
|
|
6
|
+
* `--disable-web-security` etc. let the injected capture helper work across SSO-redirect origins.
|
|
7
|
+
*/
|
|
8
|
+
export interface InstrumentedBrowser {
|
|
9
|
+
/** Launch the browser and open the target site. */
|
|
10
|
+
open(baseURL: string): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Inject the capture helper (a console function `window.__docsxai.capture()` and/or an on-page button)
|
|
13
|
+
* and resolve once the human triggers it. The human does the interactive login (SSO / MFA / conditional
|
|
14
|
+
* access — anything they can click through) before triggering.
|
|
15
|
+
*/
|
|
16
|
+
waitForCapture(trigger: CaptureTrigger): Promise<void>;
|
|
17
|
+
/** Snapshot the current `storageState`. */
|
|
18
|
+
storageState(): Promise<StorageState>;
|
|
19
|
+
close(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export interface ManualCaptureOptions {
|
|
22
|
+
/** `console` (default): `window.__docsxai.capture()`. `button`: an injected on-page button. */
|
|
23
|
+
capture_trigger?: CaptureTrigger;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* `manual-capture` — the plugin spawns a security-lowered, instrumented Chrome; the engineer logs in
|
|
27
|
+
* interactively; a console command or an injected button snapshots `storageState`. The zero-integration
|
|
28
|
+
* universal fallback for SSO / MFA / conditional access, at the cost of periodic human re-capture.
|
|
29
|
+
*/
|
|
30
|
+
export declare class ManualCaptureStrategy implements AuthStrategy {
|
|
31
|
+
private readonly browserFactory;
|
|
32
|
+
readonly name: "manual-capture";
|
|
33
|
+
constructor(browserFactory: () => InstrumentedBrowser);
|
|
34
|
+
authenticate(ctx: AuthContext): Promise<AuthResult>;
|
|
35
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// `manual-capture` — the zero-integration universal fallback for SSO / MFA / conditional access.
|
|
2
|
+
/**
|
|
3
|
+
* `manual-capture` — the plugin spawns a security-lowered, instrumented Chrome; the engineer logs in
|
|
4
|
+
* interactively; a console command or an injected button snapshots `storageState`. The zero-integration
|
|
5
|
+
* universal fallback for SSO / MFA / conditional access, at the cost of periodic human re-capture.
|
|
6
|
+
*/
|
|
7
|
+
export class ManualCaptureStrategy {
|
|
8
|
+
browserFactory;
|
|
9
|
+
name = "manual-capture";
|
|
10
|
+
constructor(browserFactory) {
|
|
11
|
+
this.browserFactory = browserFactory;
|
|
12
|
+
}
|
|
13
|
+
async authenticate(ctx) {
|
|
14
|
+
const opts = ctx.options;
|
|
15
|
+
const trigger = opts.capture_trigger ?? "console";
|
|
16
|
+
const browser = this.browserFactory();
|
|
17
|
+
try {
|
|
18
|
+
await browser.open(ctx.baseURL);
|
|
19
|
+
await browser.waitForCapture(trigger);
|
|
20
|
+
const storageState = await browser.storageState();
|
|
21
|
+
// Deliberately *not* reporting an `expiresAt`: an interactive SSO login drops ephemeral IdP scratch
|
|
22
|
+
// cookies whose expiry is seconds out, so `min(cookie.expires)` ≈ now and would make the cached
|
|
23
|
+
// session born expired. How long a manually-captured session is trusted is the `cache.ttl` contract.
|
|
24
|
+
return { storageState };
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
await browser.close();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type AuthContext, type AuthResult, type AuthStrategy } from "./types.js";
|
|
3
|
+
export declare const MtlsOptions: z.ZodObject<{
|
|
4
|
+
/** Origin the certificate applies to. Default: the target's baseURL origin. */
|
|
5
|
+
origin: z.ZodOptional<z.ZodString>;
|
|
6
|
+
}, "strict", z.ZodTypeAny, {
|
|
7
|
+
origin?: string | undefined;
|
|
8
|
+
}, {
|
|
9
|
+
origin?: string | undefined;
|
|
10
|
+
}>;
|
|
11
|
+
export type MtlsOptions = z.infer<typeof MtlsOptions>;
|
|
12
|
+
export declare class MtlsStrategy implements AuthStrategy {
|
|
13
|
+
readonly name: "mtls";
|
|
14
|
+
authenticate(ctx: AuthContext): Promise<AuthResult>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// `mtls` — connection-level auth: the browser context presents a client certificate during the
|
|
2
|
+
// TLS handshake. The creds env vars hold *paths* to the PEM files (cert/key stay on disk; the
|
|
3
|
+
// strategy validates they exist and look like PEM but never reads them into logs or errors).
|
|
4
|
+
// An optional `passphrase` cred decrypts an encrypted key. Empty storageState;
|
|
5
|
+
// `contextOptions.clientCertificates` carries the paths through to Playwright.
|
|
6
|
+
import { promises as fs } from "node:fs";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { AuthStrategyConfigError, emptyStorageState, maskSecret, parseStrategyOptions, } from "./types.js";
|
|
9
|
+
export const MtlsOptions = z
|
|
10
|
+
.object({
|
|
11
|
+
/** Origin the certificate applies to. Default: the target's baseURL origin. */
|
|
12
|
+
origin: z.string().min(1).optional(),
|
|
13
|
+
})
|
|
14
|
+
.strict();
|
|
15
|
+
async function assertPemFile(role, filePath) {
|
|
16
|
+
let head;
|
|
17
|
+
try {
|
|
18
|
+
const handle = await fs.open(filePath, "r");
|
|
19
|
+
try {
|
|
20
|
+
const buf = Buffer.alloc(64);
|
|
21
|
+
const { bytesRead } = await handle.read(buf, 0, 64, 0);
|
|
22
|
+
head = buf.subarray(0, bytesRead).toString("utf8");
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
await handle.close();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new AuthStrategyConfigError(`mtls: ${role} file not readable: ${filePath}`);
|
|
30
|
+
}
|
|
31
|
+
if (!head.includes("-----BEGIN ")) {
|
|
32
|
+
throw new AuthStrategyConfigError(`mtls: ${role} file does not look like PEM (expected a "-----BEGIN …-----" header): ${filePath}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export class MtlsStrategy {
|
|
36
|
+
name = "mtls";
|
|
37
|
+
async authenticate(ctx) {
|
|
38
|
+
const opts = parseStrategyOptions(this.name, MtlsOptions, ctx.options);
|
|
39
|
+
const { cert: certPath, key: keyPath, passphrase } = ctx.creds;
|
|
40
|
+
if (!certPath || !keyPath) {
|
|
41
|
+
throw new AuthStrategyConfigError(`mtls: creds_env must map "cert" (${maskSecret(certPath)}) and "key" (${maskSecret(keyPath)}) to env vars holding PEM file *paths*`);
|
|
42
|
+
}
|
|
43
|
+
await assertPemFile("cert", certPath);
|
|
44
|
+
await assertPemFile("key", keyPath);
|
|
45
|
+
const origin = opts.origin ?? new URL(ctx.baseURL).origin;
|
|
46
|
+
return {
|
|
47
|
+
storageState: emptyStorageState(),
|
|
48
|
+
contextOptions: {
|
|
49
|
+
clientCertificates: [{ origin, certPath, keyPath, ...(passphrase ? { passphrase } : {}) }],
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type AuthContext, type AuthResult, type AuthStrategy } from "./types.js";
|
|
3
|
+
export declare const PatHeaderOptions: z.ZodObject<{
|
|
4
|
+
/** Header name. Default `Authorization`. */
|
|
5
|
+
header: z.ZodDefault<z.ZodString>;
|
|
6
|
+
/** `{{token}}` template for the header value. Default `Bearer {{token}}`. */
|
|
7
|
+
value_template: z.ZodDefault<z.ZodString>;
|
|
8
|
+
}, "strict", z.ZodTypeAny, {
|
|
9
|
+
value_template: string;
|
|
10
|
+
header: string;
|
|
11
|
+
}, {
|
|
12
|
+
value_template?: string | undefined;
|
|
13
|
+
header?: string | undefined;
|
|
14
|
+
}>;
|
|
15
|
+
export type PatHeaderOptions = z.infer<typeof PatHeaderOptions>;
|
|
16
|
+
export declare class PatHeaderStrategy implements AuthStrategy {
|
|
17
|
+
readonly name: "pat-header";
|
|
18
|
+
authenticate(ctx: AuthContext): Promise<AuthResult>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// `pat-header` — connection-level auth: every request the browser context makes carries a
|
|
2
|
+
// personal-access-token header (`Authorization: Bearer <token>` by default). Empty storageState;
|
|
3
|
+
// the strategy emits `contextOptions.extraHTTPHeaders`.
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { AuthStrategyConfigError, emptyStorageState, maskSecret, parseStrategyOptions, renderTemplate, } from "./types.js";
|
|
6
|
+
export const PatHeaderOptions = z
|
|
7
|
+
.object({
|
|
8
|
+
/** Header name. Default `Authorization`. */
|
|
9
|
+
header: z.string().min(1).default("Authorization"),
|
|
10
|
+
/** `{{token}}` template for the header value. Default `Bearer {{token}}`. */
|
|
11
|
+
value_template: z.string().min(1).default("Bearer {{token}}"),
|
|
12
|
+
})
|
|
13
|
+
.strict();
|
|
14
|
+
export class PatHeaderStrategy {
|
|
15
|
+
name = "pat-header";
|
|
16
|
+
authenticate(ctx) {
|
|
17
|
+
try {
|
|
18
|
+
const opts = parseStrategyOptions(this.name, PatHeaderOptions, ctx.options);
|
|
19
|
+
const token = ctx.creds.token;
|
|
20
|
+
if (!token) {
|
|
21
|
+
throw new AuthStrategyConfigError(`pat-header: creds_env must map "token" to the env var holding the access token (token: ${maskSecret(token)})`);
|
|
22
|
+
}
|
|
23
|
+
return Promise.resolve({
|
|
24
|
+
storageState: emptyStorageState(),
|
|
25
|
+
contextOptions: {
|
|
26
|
+
extraHTTPHeaders: { [opts.header]: renderTemplate(opts.value_template, { token }) },
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
return Promise.reject(e instanceof Error ? e : new Error(String(e)));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type RoleAuth } from "../doc-pack.js";
|
|
2
|
+
import { type AuthResult, type StorageState } from "./types.js";
|
|
3
|
+
export declare class LocalStorageStateCache {
|
|
4
|
+
private readonly dir;
|
|
5
|
+
/** @param dir the `.auth/` directory (relative paths resolved against cwd). */
|
|
6
|
+
constructor(dir?: string);
|
|
7
|
+
private fileName;
|
|
8
|
+
private file;
|
|
9
|
+
/** Return the cached state for a role if present and not past its expiry; otherwise `null`. */
|
|
10
|
+
load(role: string, now?: number): Promise<StorageState | null>;
|
|
11
|
+
/**
|
|
12
|
+
* Persist a captured session and compute its expiry, in priority order:
|
|
13
|
+
* 1. **The app's auth cookie** — if `auth_cookie` (descriptor or override) names a cookie that's in the
|
|
14
|
+
* captured jar with a real (non-session) expiry, that expiry *is* the bound. This is the right answer;
|
|
15
|
+
* the host agent identifies which cookie it is (it's on the app's domain, long-lived — not an ephemeral
|
|
16
|
+
* IdP scratch cookie). Why not just `min(cookie.expires)`? An interactive SSO login drops scratch cookies
|
|
17
|
+
* that expire seconds out, so the min ≈ now and the session would be born expired.
|
|
18
|
+
* 2. **`ttl`** — a duration (`1h`, `30m`, ms) → `now + ttl`. The fallback when no `auth_cookie` is set/found.
|
|
19
|
+
* 3. **`session` / default** — the strategy's reported `expiresAt` if plausibly in the future, else +1h.
|
|
20
|
+
* Returns the computed `expiresAt` and a human-readable `source`.
|
|
21
|
+
*/
|
|
22
|
+
save(role: string, result: AuthResult, roleAuth: RoleAuth, now?: number, opts?: {
|
|
23
|
+
authCookie?: string;
|
|
24
|
+
}): Promise<{
|
|
25
|
+
expiresAt: number;
|
|
26
|
+
source: string;
|
|
27
|
+
}>;
|
|
28
|
+
clear(role: string): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
/** The cache surface both stores satisfy — `load` / `save` / `clear` per role. */
|
|
31
|
+
export type StorageStateCache = Pick<LocalStorageStateCache, "load" | "save" | "clear">;
|
|
32
|
+
/**
|
|
33
|
+
* Pick the state cache for a role. `store: local` (the default) caches under `<workspace>/.auth/`.
|
|
34
|
+
* `store: backend` relays AES-256-GCM envelopes through the backend (encrypted client-side; the
|
|
35
|
+
* backend never sees plaintext) — it needs a workspace that has been pushed (`backend_url` +
|
|
36
|
+
* `backend_workspace_id` in `.docsxai.json`) and `DOCSX_CACHE_KEY`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveStateCache(roleAuth: RoleAuth, workspaceDir: string): Promise<StorageStateCache>;
|