@vibe-agent-toolkit/utils 0.1.39-rc.1 → 0.1.39-rc.2
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/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/link-auth/build-headers.d.ts +34 -0
- package/dist/link-auth/build-headers.d.ts.map +1 -0
- package/dist/link-auth/build-headers.js +58 -0
- package/dist/link-auth/build-headers.js.map +1 -0
- package/dist/link-auth/expand-macro.d.ts +38 -0
- package/dist/link-auth/expand-macro.d.ts.map +1 -0
- package/dist/link-auth/expand-macro.js +131 -0
- package/dist/link-auth/expand-macro.js.map +1 -0
- package/dist/link-auth/macros.yaml +50 -0
- package/dist/link-auth/resolve-token.d.ts +49 -0
- package/dist/link-auth/resolve-token.d.ts.map +1 -0
- package/dist/link-auth/resolve-token.js +72 -0
- package/dist/link-auth/resolve-token.js.map +1 -0
- package/dist/link-auth/resolve.d.ts +58 -0
- package/dist/link-auth/resolve.d.ts.map +1 -0
- package/dist/link-auth/resolve.js +55 -0
- package/dist/link-auth/resolve.js.map +1 -0
- package/dist/link-auth/rewrite.d.ts +52 -0
- package/dist/link-auth/rewrite.d.ts.map +1 -0
- package/dist/link-auth/rewrite.js +102 -0
- package/dist/link-auth/rewrite.js.map +1 -0
- package/dist/link-auth/select-provider.d.ts +30 -0
- package/dist/link-auth/select-provider.d.ts.map +1 -0
- package/dist/link-auth/select-provider.js +55 -0
- package/dist/link-auth/select-provider.js.map +1 -0
- package/dist/link-auth/template.d.ts +40 -0
- package/dist/link-auth/template.d.ts.map +1 -0
- package/dist/link-auth/template.js +89 -0
- package/dist/link-auth/template.js.map +1 -0
- package/dist/link-auth/transforms.d.ts +46 -0
- package/dist/link-auth/transforms.d.ts.map +1 -0
- package/dist/link-auth/transforms.js +52 -0
- package/dist/link-auth/transforms.js.map +1 -0
- package/package.json +4 -3
package/dist/index.d.ts
CHANGED
|
@@ -17,4 +17,9 @@ export * from './test-helpers.js';
|
|
|
17
17
|
export * from './zod-introspection.js';
|
|
18
18
|
export * from './template.js';
|
|
19
19
|
export * from './skill-targets.js';
|
|
20
|
+
export { type LinkAuthConfig, type Provider, type ProviderAuth, type ProviderCheck, resolveAuthenticatedUrl, type ResolveOutcome, } from './link-auth/resolve.js';
|
|
21
|
+
export type { ProviderMatch } from './link-auth/select-provider.js';
|
|
22
|
+
export type { RewriteRule } from './link-auth/rewrite.js';
|
|
23
|
+
export type { TokenSource } from './link-auth/resolve-token.js';
|
|
24
|
+
export { expandMacro, UnknownMacroError } from './link-auth/expand-macro.js';
|
|
20
25
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,gBAAgB,CAAC;AAG/B,cAAc,iBAAiB,CAAC;AAGhC,cAAc,sBAAsB,CAAC;AAGrC,cAAc,eAAe,CAAC;AAG9B,cAAc,mBAAmB,CAAC;AAGlC,cAAc,wBAAwB,CAAC;AAGvC,cAAc,gBAAgB,CAAC;AAI/B,cAAc,oBAAoB,CAAC;AAGnC,cAAc,kBAAkB,CAAC;AAGjC,cAAc,mBAAmB,CAAC;AAGlC,cAAc,wBAAwB,CAAC;AAGvC,cAAc,eAAe,CAAC;AAG9B,cAAc,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,gBAAgB,CAAC;AAG/B,cAAc,iBAAiB,CAAC;AAGhC,cAAc,sBAAsB,CAAC;AAGrC,cAAc,eAAe,CAAC;AAG9B,cAAc,mBAAmB,CAAC;AAGlC,cAAc,wBAAwB,CAAC;AAGvC,cAAc,gBAAgB,CAAC;AAI/B,cAAc,oBAAoB,CAAC;AAGnC,cAAc,kBAAkB,CAAC;AAGjC,cAAc,mBAAmB,CAAC;AAGlC,cAAc,wBAAwB,CAAC;AAGvC,cAAc,eAAe,CAAC;AAG9B,cAAc,oBAAoB,CAAC;AAInC,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,uBAAuB,EACvB,KAAK,cAAc,GACpB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AACpE,YAAY,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -31,4 +31,8 @@ export * from './zod-introspection.js';
|
|
|
31
31
|
export * from './template.js';
|
|
32
32
|
// Skill target resolution (cross-platform flat skill install paths)
|
|
33
33
|
export * from './skill-targets.js';
|
|
34
|
+
// linkAuth pure engine — public API only (issue #113).
|
|
35
|
+
// Internal helpers (rewrite, build-headers, etc.) stay module-private.
|
|
36
|
+
export { resolveAuthenticatedUrl, } from './link-auth/resolve.js';
|
|
37
|
+
export { expandMacro, UnknownMacroError } from './link-auth/expand-macro.js';
|
|
34
38
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,8DAA8D;AAC9D,cAAc,gBAAgB,CAAC;AAE/B,gCAAgC;AAChC,cAAc,iBAAiB,CAAC;AAEhC,2DAA2D;AAC3D,cAAc,sBAAsB,CAAC;AAErC,uBAAuB;AACvB,cAAc,eAAe,CAAC;AAE9B,wCAAwC;AACxC,cAAc,mBAAmB,CAAC;AAElC,sBAAsB;AACtB,cAAc,wBAAwB,CAAC;AAEvC,8CAA8C;AAC9C,cAAc,gBAAgB,CAAC;AAE/B,2DAA2D;AAC3D,iEAAiE;AACjE,cAAc,oBAAoB,CAAC;AAEnC,yDAAyD;AACzD,cAAc,kBAAkB,CAAC;AAEjC,oDAAoD;AACpD,cAAc,mBAAmB,CAAC;AAElC,4CAA4C;AAC5C,cAAc,wBAAwB,CAAC;AAEvC,2DAA2D;AAC3D,cAAc,eAAe,CAAC;AAE9B,oEAAoE;AACpE,cAAc,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,8DAA8D;AAC9D,cAAc,gBAAgB,CAAC;AAE/B,gCAAgC;AAChC,cAAc,iBAAiB,CAAC;AAEhC,2DAA2D;AAC3D,cAAc,sBAAsB,CAAC;AAErC,uBAAuB;AACvB,cAAc,eAAe,CAAC;AAE9B,wCAAwC;AACxC,cAAc,mBAAmB,CAAC;AAElC,sBAAsB;AACtB,cAAc,wBAAwB,CAAC;AAEvC,8CAA8C;AAC9C,cAAc,gBAAgB,CAAC;AAE/B,2DAA2D;AAC3D,iEAAiE;AACjE,cAAc,oBAAoB,CAAC;AAEnC,yDAAyD;AACzD,cAAc,kBAAkB,CAAC;AAEjC,oDAAoD;AACpD,cAAc,mBAAmB,CAAC;AAElC,4CAA4C;AAC5C,cAAc,wBAAwB,CAAC;AAEvC,2DAA2D;AAC3D,cAAc,eAAe,CAAC;AAE9B,oEAAoE;AACpE,cAAc,oBAAoB,CAAC;AAEnC,uDAAuD;AACvD,uEAAuE;AACvE,OAAO,EAKL,uBAAuB,GAExB,MAAM,wBAAwB,CAAC;AAIhC,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render auth-header templates with rendered values, and a structural
|
|
3
|
+
* redaction helper for serialization.
|
|
4
|
+
*
|
|
5
|
+
* `buildHeaders` renders each header value template against a context that
|
|
6
|
+
* carries `${token}` plus any named captures / vars from the rewrite step.
|
|
7
|
+
* `redactHeaders` masks `Authorization` values for any caller that needs to
|
|
8
|
+
* serialize headers into logs, errors, or cache entries — the design's §8
|
|
9
|
+
* "tokens never leak" claim depends on every such site routing through this.
|
|
10
|
+
*
|
|
11
|
+
* Per design issue #113 §4 (auth.headers vocabulary) and §8 (redaction is
|
|
12
|
+
* structural; Authorization values never appear in serialized output).
|
|
13
|
+
*/
|
|
14
|
+
export declare const REDACTED_VALUE = "<redacted>";
|
|
15
|
+
/**
|
|
16
|
+
* Render a map of header templates into a map of concrete header values.
|
|
17
|
+
*
|
|
18
|
+
* @throws {TemplateMissingVarError} if a header template references an
|
|
19
|
+
* unknown context key
|
|
20
|
+
* @throws {TemplateSyntaxError} from a malformed template expression
|
|
21
|
+
* @throws {UnknownTransformError} from a template calling an unknown transform
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildHeaders(templates: Record<string, string>, context: Record<string, string>): Record<string, string>;
|
|
24
|
+
/**
|
|
25
|
+
* Return a copy of `headers` with sensitive values replaced by `REDACTED_VALUE`.
|
|
26
|
+
* Header-name matching is case-insensitive but exact (no prefix matching) — a
|
|
27
|
+
* header like `X-Authorization-Foo` is NOT considered sensitive.
|
|
28
|
+
*
|
|
29
|
+
* **Input must be a plain key-value object.** A `Headers` instance (Web Fetch
|
|
30
|
+
* API) or a `Map` yields `[]` from `Object.entries` and would silently no-op
|
|
31
|
+
* redaction — converting to a plain object is the caller's responsibility.
|
|
32
|
+
*/
|
|
33
|
+
export declare function redactHeaders(headers: Record<string, string>): Record<string, string>;
|
|
34
|
+
//# sourceMappingURL=build-headers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-headers.d.ts","sourceRoot":"","sources":["../../src/link-auth/build-headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,eAAO,MAAM,cAAc,eAAe,CAAC;AAc3C;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMxB;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMrF"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render auth-header templates with rendered values, and a structural
|
|
3
|
+
* redaction helper for serialization.
|
|
4
|
+
*
|
|
5
|
+
* `buildHeaders` renders each header value template against a context that
|
|
6
|
+
* carries `${token}` plus any named captures / vars from the rewrite step.
|
|
7
|
+
* `redactHeaders` masks `Authorization` values for any caller that needs to
|
|
8
|
+
* serialize headers into logs, errors, or cache entries — the design's §8
|
|
9
|
+
* "tokens never leak" claim depends on every such site routing through this.
|
|
10
|
+
*
|
|
11
|
+
* Per design issue #113 §4 (auth.headers vocabulary) and §8 (redaction is
|
|
12
|
+
* structural; Authorization values never appear in serialized output).
|
|
13
|
+
*/
|
|
14
|
+
import { renderTemplate } from './template.js';
|
|
15
|
+
export const REDACTED_VALUE = '<redacted>';
|
|
16
|
+
/**
|
|
17
|
+
* Header names whose values must be masked when serialized.
|
|
18
|
+
*
|
|
19
|
+
* v1 ships only `authorization` because that is the only secret-bearing
|
|
20
|
+
* header the current macros emit. **Omission is the security risk** — any
|
|
21
|
+
* future macro that emits a header carrying a secret (`Cookie`,
|
|
22
|
+
* `Proxy-Authorization`, `X-API-Key`, custom bearer-style headers) must add
|
|
23
|
+
* that name here, or the token will silently leak through serialization.
|
|
24
|
+
* Extending the set is not the dangerous edit; forgetting to extend it is.
|
|
25
|
+
*/
|
|
26
|
+
const SENSITIVE_HEADER_NAMES = new Set(['authorization']);
|
|
27
|
+
/**
|
|
28
|
+
* Render a map of header templates into a map of concrete header values.
|
|
29
|
+
*
|
|
30
|
+
* @throws {TemplateMissingVarError} if a header template references an
|
|
31
|
+
* unknown context key
|
|
32
|
+
* @throws {TemplateSyntaxError} from a malformed template expression
|
|
33
|
+
* @throws {UnknownTransformError} from a template calling an unknown transform
|
|
34
|
+
*/
|
|
35
|
+
export function buildHeaders(templates, context) {
|
|
36
|
+
const headers = Object.create(null);
|
|
37
|
+
for (const [name, template] of Object.entries(templates)) {
|
|
38
|
+
headers[name] = renderTemplate(template, context);
|
|
39
|
+
}
|
|
40
|
+
return headers;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Return a copy of `headers` with sensitive values replaced by `REDACTED_VALUE`.
|
|
44
|
+
* Header-name matching is case-insensitive but exact (no prefix matching) — a
|
|
45
|
+
* header like `X-Authorization-Foo` is NOT considered sensitive.
|
|
46
|
+
*
|
|
47
|
+
* **Input must be a plain key-value object.** A `Headers` instance (Web Fetch
|
|
48
|
+
* API) or a `Map` yields `[]` from `Object.entries` and would silently no-op
|
|
49
|
+
* redaction — converting to a plain object is the caller's responsibility.
|
|
50
|
+
*/
|
|
51
|
+
export function redactHeaders(headers) {
|
|
52
|
+
const redacted = Object.create(null);
|
|
53
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
54
|
+
redacted[name] = SENSITIVE_HEADER_NAMES.has(name.toLowerCase()) ? REDACTED_VALUE : value;
|
|
55
|
+
}
|
|
56
|
+
return redacted;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=build-headers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-headers.js","sourceRoot":"","sources":["../../src/link-auth/build-headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,MAAM,CAAC,MAAM,cAAc,GAAG,YAAY,CAAC;AAE3C;;;;;;;;;GASG;AACH,MAAM,sBAAsB,GAAwB,IAAI,GAAG,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC;AAE/E;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAC1B,SAAiC,EACjC,OAA+B;IAE/B,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAA2B,CAAC;IAC9D,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QACzD,OAAO,CAAC,IAAI,CAAC,GAAG,cAAc,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACpD,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,OAA+B;IAC3D,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAA2B,CAAC;IAC/D,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACpD,QAAQ,CAAC,IAAI,CAAC,GAAG,sBAAsB,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC;IAC3F,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Macro loader + expander.
|
|
3
|
+
*
|
|
4
|
+
* Loads the bundled `macros.yaml` once at module init and exposes
|
|
5
|
+
* `expandMacro(name, overrides?)` which deep-merges the named macro with any
|
|
6
|
+
* adopter overrides. The merge is "adopter wins": objects merge recursively,
|
|
7
|
+
* arrays and primitives are replaced wholesale (no element-wise array merge).
|
|
8
|
+
*
|
|
9
|
+
* The shipped macro file is at `link-auth/macros.yaml`, copied into the dist
|
|
10
|
+
* tree by `packages/dev-tools/src/copy-yaml-assets.ts` during build so the
|
|
11
|
+
* runtime `fs.readFileSync(new URL('./macros.yaml', import.meta.url))`
|
|
12
|
+
* resolves in both source-mode (vitest) and built-mode (dist).
|
|
13
|
+
*
|
|
14
|
+
* Per design issue #113 §5 (macros are config, not a privileged code path).
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Thrown when a `use: <name>` references a macro not in the shipped set.
|
|
18
|
+
* Message lists the available macros so a typo surfaces clearly.
|
|
19
|
+
*/
|
|
20
|
+
export declare class UnknownMacroError extends Error {
|
|
21
|
+
constructor(name: string, available: readonly string[]);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Look up a macro by name and deep-merge optional adopter overrides on top.
|
|
25
|
+
*
|
|
26
|
+
* Merge semantics:
|
|
27
|
+
* - Plain objects merge recursively (sibling keys preserved).
|
|
28
|
+
* - Arrays are replaced wholesale (override's array wins; no concat).
|
|
29
|
+
* - Primitives are replaced.
|
|
30
|
+
* - `undefined` in an override is treated as "not provided" (base wins).
|
|
31
|
+
*
|
|
32
|
+
* The returned object and all nested plain objects use null prototypes so
|
|
33
|
+
* `__proto__` / `constructor` keys can never poison consumers.
|
|
34
|
+
*
|
|
35
|
+
* @throws {UnknownMacroError} if `name` is not in the shipped macro set
|
|
36
|
+
*/
|
|
37
|
+
export declare function expandMacro(name: string, overrides?: Record<string, unknown>): Record<string, unknown>;
|
|
38
|
+
//# sourceMappingURL=expand-macro.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expand-macro.d.ts","sourceRoot":"","sources":["../../src/link-auth/expand-macro.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAwCH;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,MAAM,EAAE;CAIvD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAYzB"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Macro loader + expander.
|
|
3
|
+
*
|
|
4
|
+
* Loads the bundled `macros.yaml` once at module init and exposes
|
|
5
|
+
* `expandMacro(name, overrides?)` which deep-merges the named macro with any
|
|
6
|
+
* adopter overrides. The merge is "adopter wins": objects merge recursively,
|
|
7
|
+
* arrays and primitives are replaced wholesale (no element-wise array merge).
|
|
8
|
+
*
|
|
9
|
+
* The shipped macro file is at `link-auth/macros.yaml`, copied into the dist
|
|
10
|
+
* tree by `packages/dev-tools/src/copy-yaml-assets.ts` during build so the
|
|
11
|
+
* runtime `fs.readFileSync(new URL('./macros.yaml', import.meta.url))`
|
|
12
|
+
* resolves in both source-mode (vitest) and built-mode (dist).
|
|
13
|
+
*
|
|
14
|
+
* Per design issue #113 §5 (macros are config, not a privileged code path).
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { parse as parseYaml } from 'yaml';
|
|
19
|
+
const macrosPath = fileURLToPath(new URL('./macros.yaml', import.meta.url));
|
|
20
|
+
let macrosCache;
|
|
21
|
+
/**
|
|
22
|
+
* Load macros lazily on first use rather than at module init.
|
|
23
|
+
*
|
|
24
|
+
* Why lazy: vitest tests elsewhere in the repo mock `node:fs` (replacing
|
|
25
|
+
* `readFileSync` with `vi.fn()` that returns `undefined`). If we eagerly read
|
|
26
|
+
* at module load, any unrelated test that imports `@vibe-agent-toolkit/utils`
|
|
27
|
+
* and mocks fs crashes inside `parseYaml(undefined)` — module-init code runs
|
|
28
|
+
* unconditionally, before the mock-setup intent reaches our file.
|
|
29
|
+
*
|
|
30
|
+
* Lazy load makes module import side-effect-free; only callers of
|
|
31
|
+
* `expandMacro` pay the fs cost.
|
|
32
|
+
*/
|
|
33
|
+
function getMacros() {
|
|
34
|
+
if (macrosCache !== undefined)
|
|
35
|
+
return macrosCache;
|
|
36
|
+
// Path is derived from `import.meta.url`, not user input — points at the
|
|
37
|
+
// shipped macros.yaml asset next to this module in both src and dist trees.
|
|
38
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
39
|
+
const macrosFileContent = readFileSync(macrosPath, 'utf8');
|
|
40
|
+
const parsed = parseYaml(macrosFileContent);
|
|
41
|
+
if (!isPlainObject(parsed)) {
|
|
42
|
+
throw new Error('macros.yaml: expected top-level object mapping macro name → provider config.');
|
|
43
|
+
}
|
|
44
|
+
macrosCache = freezeMacros(parsed);
|
|
45
|
+
return macrosCache;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Thrown when a `use: <name>` references a macro not in the shipped set.
|
|
49
|
+
* Message lists the available macros so a typo surfaces clearly.
|
|
50
|
+
*/
|
|
51
|
+
export class UnknownMacroError extends Error {
|
|
52
|
+
constructor(name, available) {
|
|
53
|
+
super(`Unknown macro "${name}". Available: ${available.join(', ')}.`);
|
|
54
|
+
this.name = 'UnknownMacroError';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Look up a macro by name and deep-merge optional adopter overrides on top.
|
|
59
|
+
*
|
|
60
|
+
* Merge semantics:
|
|
61
|
+
* - Plain objects merge recursively (sibling keys preserved).
|
|
62
|
+
* - Arrays are replaced wholesale (override's array wins; no concat).
|
|
63
|
+
* - Primitives are replaced.
|
|
64
|
+
* - `undefined` in an override is treated as "not provided" (base wins).
|
|
65
|
+
*
|
|
66
|
+
* The returned object and all nested plain objects use null prototypes so
|
|
67
|
+
* `__proto__` / `constructor` keys can never poison consumers.
|
|
68
|
+
*
|
|
69
|
+
* @throws {UnknownMacroError} if `name` is not in the shipped macro set
|
|
70
|
+
*/
|
|
71
|
+
export function expandMacro(name, overrides) {
|
|
72
|
+
const macros = getMacros();
|
|
73
|
+
const base = macros[name];
|
|
74
|
+
if (base === undefined) {
|
|
75
|
+
throw new UnknownMacroError(name, Object.keys(macros));
|
|
76
|
+
}
|
|
77
|
+
if (overrides === undefined) {
|
|
78
|
+
return cloneWithNullProto(base);
|
|
79
|
+
}
|
|
80
|
+
const merged = deepMerge(base, overrides);
|
|
81
|
+
// Top-level result is guaranteed object here because base is.
|
|
82
|
+
return merged;
|
|
83
|
+
}
|
|
84
|
+
function deepMerge(base, override) {
|
|
85
|
+
if (override === undefined)
|
|
86
|
+
return base;
|
|
87
|
+
if (!isPlainObject(base) || !isPlainObject(override))
|
|
88
|
+
return cloneValue(override);
|
|
89
|
+
const result = Object.create(null);
|
|
90
|
+
for (const [key, value] of Object.entries(base)) {
|
|
91
|
+
result[key] = cloneValue(value);
|
|
92
|
+
}
|
|
93
|
+
for (const [key, value] of Object.entries(override)) {
|
|
94
|
+
const existing = Object.hasOwn(result, key) ? result[key] : undefined;
|
|
95
|
+
if (isPlainObject(existing) && isPlainObject(value)) {
|
|
96
|
+
result[key] = deepMerge(existing, value);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
result[key] = cloneValue(value);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
function cloneValue(value) {
|
|
105
|
+
if (Array.isArray(value))
|
|
106
|
+
return value.map(cloneValue);
|
|
107
|
+
if (isPlainObject(value))
|
|
108
|
+
return cloneWithNullProto(value);
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
function cloneWithNullProto(obj) {
|
|
112
|
+
const cloned = Object.create(null);
|
|
113
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
114
|
+
cloned[key] = cloneValue(value);
|
|
115
|
+
}
|
|
116
|
+
return cloned;
|
|
117
|
+
}
|
|
118
|
+
function freezeMacros(parsed) {
|
|
119
|
+
const frozen = Object.create(null);
|
|
120
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
121
|
+
if (!isPlainObject(value)) {
|
|
122
|
+
throw new Error(`macros.yaml: macro "${name}" is not an object.`);
|
|
123
|
+
}
|
|
124
|
+
frozen[name] = cloneWithNullProto(value);
|
|
125
|
+
}
|
|
126
|
+
return frozen;
|
|
127
|
+
}
|
|
128
|
+
function isPlainObject(v) {
|
|
129
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=expand-macro.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expand-macro.js","sourceRoot":"","sources":["../../src/link-auth/expand-macro.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# linkAuth provider macros — shipped defaults referenced by `use: <name>` in
|
|
2
|
+
# adopter `vibe-agent-toolkit.config.yaml` files.
|
|
3
|
+
#
|
|
4
|
+
# Each entry is a full provider config in the §4 vocabulary (match, rewrite,
|
|
5
|
+
# auth, token, check). Adopters reference a macro by name and may deep-merge
|
|
6
|
+
# overrides on top — see design issue #113 §5.
|
|
7
|
+
#
|
|
8
|
+
# Adding a new host is intended to be a config-only PR: append an entry here
|
|
9
|
+
# and add unit tests. Only `vocabulary` changes (e.g. a new transform) require
|
|
10
|
+
# engine code.
|
|
11
|
+
|
|
12
|
+
github:
|
|
13
|
+
match:
|
|
14
|
+
host: github.com
|
|
15
|
+
rewrite:
|
|
16
|
+
- when: '^https://github\.com/(?<owner>[^/]+)/(?<repo>[^/]+)/(?:blob|tree)/(?<ref>[^/]+)/(?<path>.+)$'
|
|
17
|
+
to: 'https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${ref}'
|
|
18
|
+
auth:
|
|
19
|
+
headers:
|
|
20
|
+
Authorization: 'Bearer ${token}'
|
|
21
|
+
Accept: application/vnd.github+json
|
|
22
|
+
token:
|
|
23
|
+
- command: gh auth token
|
|
24
|
+
- env: GITHUB_TOKEN
|
|
25
|
+
check:
|
|
26
|
+
method: GET
|
|
27
|
+
aliveStatus: [200]
|
|
28
|
+
notFoundMeaning: ambiguous
|
|
29
|
+
|
|
30
|
+
sharepoint:
|
|
31
|
+
match:
|
|
32
|
+
host: '*.sharepoint.com'
|
|
33
|
+
excludeHost:
|
|
34
|
+
- '*-my.sharepoint.com'
|
|
35
|
+
rewrite:
|
|
36
|
+
- when: '^(?<u>https://.+)$'
|
|
37
|
+
vars:
|
|
38
|
+
shareId: 'u!${base64url(u)}'
|
|
39
|
+
to: 'https://graph.microsoft.com/v1.0/shares/${shareId}/driveItem'
|
|
40
|
+
auth:
|
|
41
|
+
headers:
|
|
42
|
+
Authorization: 'Bearer ${token}'
|
|
43
|
+
# SharePoint has no zero-config token source (design §5.1 note): a SharePoint-
|
|
44
|
+
# scoped token requires an Entra app registration; the stock `az` token does
|
|
45
|
+
# not work. Adopter must supply their own token command.
|
|
46
|
+
token: []
|
|
47
|
+
check:
|
|
48
|
+
method: GET
|
|
49
|
+
aliveStatus: [200]
|
|
50
|
+
notFoundMeaning: dead
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token resolution — ordered, first-non-empty-wins.
|
|
3
|
+
*
|
|
4
|
+
* Iterates a provider's `token` source list and returns the first source that
|
|
5
|
+
* yields a non-empty value. Two source shapes:
|
|
6
|
+
* - `{ env: "NAME" }` — read from process env (no trimming)
|
|
7
|
+
* - `{ command: argv }` — run a command, return trimmed stdout
|
|
8
|
+
* - `{ command: "gh auth token" }` — convenience: whitespace-tokenized into
|
|
9
|
+
* argv. **Not** passed through a shell — operators (`|`, `&&`, `$(...)`)
|
|
10
|
+
* become literal argv elements, per the design's §6.1 sharp-edge note.
|
|
11
|
+
*
|
|
12
|
+
* Returns `undefined` if every source fails or yields an empty/whitespace
|
|
13
|
+
* value — the caller's `resolveAuthenticatedUrl` translates that to the
|
|
14
|
+
* `unverified` outcome (LINK_AUTH_UNVERIFIED in slice 2).
|
|
15
|
+
*
|
|
16
|
+
* Per design issue #113 §4 (token vocabulary) and §6.1 (command execution,
|
|
17
|
+
* `safeExecSync`-backed, `shell: false`).
|
|
18
|
+
*/
|
|
19
|
+
export type TokenSource = {
|
|
20
|
+
readonly env: string;
|
|
21
|
+
} | {
|
|
22
|
+
readonly command: string | readonly string[];
|
|
23
|
+
};
|
|
24
|
+
export interface TokenResolutionDeps {
|
|
25
|
+
/**
|
|
26
|
+
* Environment lookup map. Defaults to `process.env`. Injectable for tests so
|
|
27
|
+
* unit tests don't depend on ambient environment state.
|
|
28
|
+
*/
|
|
29
|
+
readonly env: Record<string, string | undefined>;
|
|
30
|
+
/**
|
|
31
|
+
* Command runner. Defaults to `safeExecResult`-wrapped invocation. Injectable
|
|
32
|
+
* for tests. Receives argv; returns `success` + `stdout`. Should NOT throw
|
|
33
|
+
* for normal exec failures (return `success: false` instead). Throws are
|
|
34
|
+
* propagated by `resolveToken` — they indicate operator-level bugs.
|
|
35
|
+
*/
|
|
36
|
+
readonly runCommand: (argv: readonly string[]) => {
|
|
37
|
+
success: boolean;
|
|
38
|
+
stdout: string;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolve a token from an ordered list of sources.
|
|
43
|
+
*
|
|
44
|
+
* @returns the first non-empty value, or `undefined` if every source failed.
|
|
45
|
+
* @throws whatever the injected `runCommand` throws (operator-level bug; not
|
|
46
|
+
* swallowed). Standard `safeExecResult` does not throw under normal use.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveToken(sources: readonly TokenSource[], deps?: Partial<TokenResolutionDeps>): string | undefined;
|
|
49
|
+
//# sourceMappingURL=resolve-token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-token.d.ts","sourceRoot":"","sources":["../../src/link-auth/resolve-token.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,MAAM,MAAM,WAAW,GAAG;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;AAEtG,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAEjD;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACxF;AAWD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,SAAS,WAAW,EAAE,EAC/B,IAAI,CAAC,EAAE,OAAO,CAAC,mBAAmB,CAAC,GAClC,MAAM,GAAG,SAAS,CASpB"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token resolution — ordered, first-non-empty-wins.
|
|
3
|
+
*
|
|
4
|
+
* Iterates a provider's `token` source list and returns the first source that
|
|
5
|
+
* yields a non-empty value. Two source shapes:
|
|
6
|
+
* - `{ env: "NAME" }` — read from process env (no trimming)
|
|
7
|
+
* - `{ command: argv }` — run a command, return trimmed stdout
|
|
8
|
+
* - `{ command: "gh auth token" }` — convenience: whitespace-tokenized into
|
|
9
|
+
* argv. **Not** passed through a shell — operators (`|`, `&&`, `$(...)`)
|
|
10
|
+
* become literal argv elements, per the design's §6.1 sharp-edge note.
|
|
11
|
+
*
|
|
12
|
+
* Returns `undefined` if every source fails or yields an empty/whitespace
|
|
13
|
+
* value — the caller's `resolveAuthenticatedUrl` translates that to the
|
|
14
|
+
* `unverified` outcome (LINK_AUTH_UNVERIFIED in slice 2).
|
|
15
|
+
*
|
|
16
|
+
* Per design issue #113 §4 (token vocabulary) and §6.1 (command execution,
|
|
17
|
+
* `safeExecSync`-backed, `shell: false`).
|
|
18
|
+
*/
|
|
19
|
+
import { safeExecResult } from '../safe-exec.js';
|
|
20
|
+
const DEFAULT_RUN_COMMAND = (argv) => {
|
|
21
|
+
if (argv.length === 0)
|
|
22
|
+
return { success: false, stdout: '' };
|
|
23
|
+
const [bin, ...args] = argv;
|
|
24
|
+
if (bin === undefined)
|
|
25
|
+
return { success: false, stdout: '' };
|
|
26
|
+
const result = safeExecResult(bin, [...args], { encoding: 'utf8' });
|
|
27
|
+
const stdout = typeof result.stdout === 'string' ? result.stdout : result.stdout.toString('utf8');
|
|
28
|
+
return { success: result.success, stdout };
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a token from an ordered list of sources.
|
|
32
|
+
*
|
|
33
|
+
* @returns the first non-empty value, or `undefined` if every source failed.
|
|
34
|
+
* @throws whatever the injected `runCommand` throws (operator-level bug; not
|
|
35
|
+
* swallowed). Standard `safeExecResult` does not throw under normal use.
|
|
36
|
+
*/
|
|
37
|
+
export function resolveToken(sources, deps) {
|
|
38
|
+
const env = deps?.env ?? process.env;
|
|
39
|
+
const runCommand = deps?.runCommand ?? DEFAULT_RUN_COMMAND;
|
|
40
|
+
for (const source of sources) {
|
|
41
|
+
const value = tryResolveSource(source, env, runCommand);
|
|
42
|
+
if (value !== undefined && value.length > 0)
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
function tryResolveSource(source, env, runCommand) {
|
|
48
|
+
if ('env' in source) {
|
|
49
|
+
// Object.hasOwn defends against env names like "__proto__" returning
|
|
50
|
+
// Object.prototype via the prototype chain.
|
|
51
|
+
if (!Object.hasOwn(env, source.env))
|
|
52
|
+
return undefined;
|
|
53
|
+
return env[source.env];
|
|
54
|
+
}
|
|
55
|
+
const argv = toArgv(source.command);
|
|
56
|
+
if (argv.length === 0)
|
|
57
|
+
return undefined;
|
|
58
|
+
const result = runCommand(argv);
|
|
59
|
+
if (!result.success)
|
|
60
|
+
return undefined;
|
|
61
|
+
const trimmed = result.stdout.trim();
|
|
62
|
+
return trimmed.length === 0 ? undefined : trimmed;
|
|
63
|
+
}
|
|
64
|
+
function toArgv(command) {
|
|
65
|
+
if (Array.isArray(command))
|
|
66
|
+
return command;
|
|
67
|
+
// Whitespace-tokenize the string form. Empty segments (from multiple spaces)
|
|
68
|
+
// are filtered. Shell operators ('|', '&&', etc.) become literal argv, NOT
|
|
69
|
+
// pipes — see design §6.1.
|
|
70
|
+
return command.split(/\s+/).filter((s) => s.length > 0);
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=resolve-token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-token.js","sourceRoot":"","sources":["../../src/link-auth/resolve-token.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAoBjD,MAAM,mBAAmB,GAAsC,CAAC,IAAI,EAAE,EAAE;IACtE,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IAC7D,MAAM,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IAC5B,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IAC7D,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IACpE,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAClG,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;AAC7C,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAC1B,OAA+B,EAC/B,IAAmC;IAEnC,MAAM,GAAG,GAAG,IAAI,EAAE,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACrC,MAAM,UAAU,GAAG,IAAI,EAAE,UAAU,IAAI,mBAAmB,CAAC;IAE3D,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE,UAAU,CAAC,CAAC;QACxD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;IAC5D,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,gBAAgB,CACvB,MAAmB,EACnB,GAAuC,EACvC,UAA6C;IAE7C,IAAI,KAAK,IAAI,MAAM,EAAE,CAAC;QACpB,qEAAqE;QACrE,4CAA4C;QAC5C,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC;YAAE,OAAO,SAAS,CAAC;QACtD,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACzB,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACpC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAExC,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,CAAC,MAAM,CAAC,OAAO;QAAE,OAAO,SAAS,CAAC;IACtC,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IACrC,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;AACpD,CAAC;AAED,SAAS,MAAM,CAAC,OAAmC;IACjD,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IAC3C,6EAA6E;IAC7E,2EAA2E;IAC3E,2BAA2B;IAC3B,OAAQ,OAAkB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACtE,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API for the linkAuth pure engine.
|
|
3
|
+
*
|
|
4
|
+
* `resolveAuthenticatedUrl(url, config)` is the single entry point per design
|
|
5
|
+
* §6: select the first provider whose `match.host` claims the URL, run its
|
|
6
|
+
* rewrite pipeline, resolve a token, build the auth headers, and return
|
|
7
|
+
* everything the caller needs to issue an authenticated fetch.
|
|
8
|
+
*
|
|
9
|
+
* Three outcomes:
|
|
10
|
+
* - `{ fetchUrl, headers }` — ready to fetch (provider claimed,
|
|
11
|
+
* rewrite matched, token resolved)
|
|
12
|
+
* - `{ outcome: 'unsupported' }` — no provider claims the host, OR
|
|
13
|
+
* host matched but no rewrite did
|
|
14
|
+
* (per §4: "the provider does not
|
|
15
|
+
* claim the URL for rewriting")
|
|
16
|
+
* - `{ outcome: 'unverified', reason }` — claimed and rewrote, but no token
|
|
17
|
+
* source resolved a non-empty value
|
|
18
|
+
*
|
|
19
|
+
* Per design issue #113 §6.
|
|
20
|
+
*/
|
|
21
|
+
import { type TokenResolutionDeps, type TokenSource } from './resolve-token.js';
|
|
22
|
+
import { type RewriteRule } from './rewrite.js';
|
|
23
|
+
import { type ProviderMatch } from './select-provider.js';
|
|
24
|
+
export interface ProviderAuth {
|
|
25
|
+
readonly headers: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
export interface ProviderCheck {
|
|
28
|
+
readonly method: 'GET' | 'HEAD';
|
|
29
|
+
readonly aliveStatus: readonly number[];
|
|
30
|
+
readonly notFoundMeaning: 'ambiguous' | 'dead';
|
|
31
|
+
}
|
|
32
|
+
export interface Provider {
|
|
33
|
+
readonly match: ProviderMatch;
|
|
34
|
+
readonly rewrite: readonly RewriteRule[];
|
|
35
|
+
readonly auth: ProviderAuth;
|
|
36
|
+
readonly token: readonly TokenSource[];
|
|
37
|
+
readonly check: ProviderCheck;
|
|
38
|
+
}
|
|
39
|
+
export interface LinkAuthConfig {
|
|
40
|
+
readonly providers: readonly Provider[];
|
|
41
|
+
}
|
|
42
|
+
export type ResolveOutcome = {
|
|
43
|
+
readonly fetchUrl: string;
|
|
44
|
+
readonly headers: Record<string, string>;
|
|
45
|
+
} | {
|
|
46
|
+
readonly outcome: 'unsupported';
|
|
47
|
+
} | {
|
|
48
|
+
readonly outcome: 'unverified';
|
|
49
|
+
readonly reason: string;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Resolve an authenticated fetch plan for `url` against the configured providers.
|
|
53
|
+
*
|
|
54
|
+
* @param deps - Optional dependency injection for token resolution (`env` map
|
|
55
|
+
* + `runCommand`). Production callers omit this; tests supply mocks.
|
|
56
|
+
*/
|
|
57
|
+
export declare function resolveAuthenticatedUrl(url: string, config: LinkAuthConfig, deps?: Partial<TokenResolutionDeps>): ResolveOutcome;
|
|
58
|
+
//# sourceMappingURL=resolve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/link-auth/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,WAAW,EACjB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,KAAK,WAAW,EAAc,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,KAAK,aAAa,EAAkB,MAAM,sBAAsB,CAAC;AAE1E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1C;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,QAAQ,CAAC,eAAe,EAAE,WAAW,GAAG,MAAM,CAAC;CAChD;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;IACzC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;IACvC,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,EAAE,SAAS,QAAQ,EAAE,CAAC;CAGzC;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GACvE;IAAE,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAA;CAAE,GACnC;IAAE,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhE;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,cAAc,EACtB,IAAI,CAAC,EAAE,OAAO,CAAC,mBAAmB,CAAC,GAClC,cAAc,CAyBhB"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API for the linkAuth pure engine.
|
|
3
|
+
*
|
|
4
|
+
* `resolveAuthenticatedUrl(url, config)` is the single entry point per design
|
|
5
|
+
* §6: select the first provider whose `match.host` claims the URL, run its
|
|
6
|
+
* rewrite pipeline, resolve a token, build the auth headers, and return
|
|
7
|
+
* everything the caller needs to issue an authenticated fetch.
|
|
8
|
+
*
|
|
9
|
+
* Three outcomes:
|
|
10
|
+
* - `{ fetchUrl, headers }` — ready to fetch (provider claimed,
|
|
11
|
+
* rewrite matched, token resolved)
|
|
12
|
+
* - `{ outcome: 'unsupported' }` — no provider claims the host, OR
|
|
13
|
+
* host matched but no rewrite did
|
|
14
|
+
* (per §4: "the provider does not
|
|
15
|
+
* claim the URL for rewriting")
|
|
16
|
+
* - `{ outcome: 'unverified', reason }` — claimed and rewrote, but no token
|
|
17
|
+
* source resolved a non-empty value
|
|
18
|
+
*
|
|
19
|
+
* Per design issue #113 §6.
|
|
20
|
+
*/
|
|
21
|
+
import { buildHeaders } from './build-headers.js';
|
|
22
|
+
import { resolveToken, } from './resolve-token.js';
|
|
23
|
+
import { rewriteUrl } from './rewrite.js';
|
|
24
|
+
import { selectProvider } from './select-provider.js';
|
|
25
|
+
/**
|
|
26
|
+
* Resolve an authenticated fetch plan for `url` against the configured providers.
|
|
27
|
+
*
|
|
28
|
+
* @param deps - Optional dependency injection for token resolution (`env` map
|
|
29
|
+
* + `runCommand`). Production callers omit this; tests supply mocks.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveAuthenticatedUrl(url, config, deps) {
|
|
32
|
+
const provider = selectProvider(url, config.providers);
|
|
33
|
+
if (provider === undefined)
|
|
34
|
+
return { outcome: 'unsupported' };
|
|
35
|
+
const rewrite = rewriteUrl(url, provider.rewrite);
|
|
36
|
+
if (!rewrite.matched)
|
|
37
|
+
return { outcome: 'unsupported' };
|
|
38
|
+
const token = resolveToken(provider.token, deps);
|
|
39
|
+
// eslint-disable-next-line security/detect-possible-timing-attacks -- compare to undefined sentinel, not secret content
|
|
40
|
+
if (token === undefined) {
|
|
41
|
+
return {
|
|
42
|
+
outcome: 'unverified',
|
|
43
|
+
reason: 'No token source resolved a non-empty value — configure `token` or log in.',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Headers see captures + vars + the resolved token. The resolved token wins
|
|
47
|
+
// over any regex capture named "token" (later in Object.assign wins), so
|
|
48
|
+
// URL-derived data never leaks into Authorization values.
|
|
49
|
+
const headerContext = Object.create(null);
|
|
50
|
+
Object.assign(headerContext, rewrite.context);
|
|
51
|
+
headerContext['token'] = token;
|
|
52
|
+
const headers = buildHeaders(provider.auth.headers, headerContext);
|
|
53
|
+
return { fetchUrl: rewrite.rewrittenUrl, headers };
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/link-auth/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EACL,YAAY,GAGb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAoB,UAAU,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAsB,cAAc,EAAE,MAAM,sBAAsB,CAAC;AA+B1E;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CACrC,GAAW,EACX,MAAsB,EACtB,IAAmC;IAEnC,MAAM,QAAQ,GAAG,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;IACvD,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC;IAE9D,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAClD,IAAI,CAAC,OAAO,CAAC,OAAO;QAAE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC;IAExD,MAAM,KAAK,GAAG,YAAY,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACjD,wHAAwH;IACxH,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO;YACL,OAAO,EAAE,YAAY;YACrB,MAAM,EAAE,2EAA2E;SACpF,CAAC;IACJ,CAAC;IAED,4EAA4E;IAC5E,yEAAyE;IACzE,0DAA0D;IAC1D,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAA2B,CAAC;IACpE,MAAM,CAAC,MAAM,CAAC,aAAa,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9C,aAAa,CAAC,OAAO,CAAC,GAAG,KAAK,CAAC;IAE/B,MAAM,OAAO,GAAG,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;IACnE,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,YAAY,EAAE,OAAO,EAAE,CAAC;AACrD,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rewrite pipeline: ordered when → vars → to.
|
|
3
|
+
*
|
|
4
|
+
* Each provider declares one or more rewrite rules. For an input URL the
|
|
5
|
+
* pipeline strips `?query` and `#fragment`, finds the first rule whose
|
|
6
|
+
* `when` regex matches, computes `vars` (templates over captures), and
|
|
7
|
+
* renders the `to` template against captures + vars merged. The merged
|
|
8
|
+
* context flows out so downstream header templates can interpolate the
|
|
9
|
+
* same variables.
|
|
10
|
+
*
|
|
11
|
+
* Per design issue #113 §4 (vocabulary) and §5.2 (fragment/query
|
|
12
|
+
* stripping must precede a greedy `(?<path>.+)` capture).
|
|
13
|
+
*/
|
|
14
|
+
export interface RewriteRule {
|
|
15
|
+
readonly when: string;
|
|
16
|
+
readonly vars?: Record<string, string>;
|
|
17
|
+
readonly to: string;
|
|
18
|
+
}
|
|
19
|
+
export type RewriteOutcome = {
|
|
20
|
+
readonly matched: true;
|
|
21
|
+
readonly rewrittenUrl: string;
|
|
22
|
+
readonly context: Record<string, string>;
|
|
23
|
+
} | {
|
|
24
|
+
readonly matched: false;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Thrown when a rule's `when` field is not a compilable regex.
|
|
28
|
+
*/
|
|
29
|
+
export declare class InvalidRewriteRuleError extends Error {
|
|
30
|
+
constructor(pattern: string, cause: unknown);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Thrown when a `vars` key collides with a named capture in `when`.
|
|
34
|
+
* Adopter must rename one — vars cannot shadow captures (the design's
|
|
35
|
+
* intent is captures + vars in a single namespace).
|
|
36
|
+
*/
|
|
37
|
+
export declare class VarCaptureCollisionError extends Error {
|
|
38
|
+
constructor(name: string);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Apply an ordered list of rewrite rules to a URL.
|
|
42
|
+
*
|
|
43
|
+
* @returns `{ matched: true, rewrittenUrl, context }` for the first matching
|
|
44
|
+
* rule, or `{ matched: false }` if no rule claims the URL.
|
|
45
|
+
* @throws {InvalidRewriteRuleError} if a `when` field does not compile
|
|
46
|
+
* @throws {VarCaptureCollisionError} if a vars name shadows a capture name
|
|
47
|
+
* @throws {TemplateMissingVarError} from a template referencing an unknown name
|
|
48
|
+
* @throws {TemplateSyntaxError} from a malformed template expression
|
|
49
|
+
* @throws {UnknownTransformError} from a template calling an unknown transform
|
|
50
|
+
*/
|
|
51
|
+
export declare function rewriteUrl(url: string, rules: readonly RewriteRule[]): RewriteOutcome;
|
|
52
|
+
//# sourceMappingURL=rewrite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rewrite.d.ts","sourceRoot":"","sources":["../../src/link-auth/rewrite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,cAAc,GACtB;IACE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1C,GACD;IAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAA;CAAE,CAAC;AAEhC;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;CAK5C;AAED;;;;GAIG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;gBACrC,IAAI,EAAE,MAAM;CAMzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,WAAW,EAAE,GAAG,cAAc,CAerF"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rewrite pipeline: ordered when → vars → to.
|
|
3
|
+
*
|
|
4
|
+
* Each provider declares one or more rewrite rules. For an input URL the
|
|
5
|
+
* pipeline strips `?query` and `#fragment`, finds the first rule whose
|
|
6
|
+
* `when` regex matches, computes `vars` (templates over captures), and
|
|
7
|
+
* renders the `to` template against captures + vars merged. The merged
|
|
8
|
+
* context flows out so downstream header templates can interpolate the
|
|
9
|
+
* same variables.
|
|
10
|
+
*
|
|
11
|
+
* Per design issue #113 §4 (vocabulary) and §5.2 (fragment/query
|
|
12
|
+
* stripping must precede a greedy `(?<path>.+)` capture).
|
|
13
|
+
*/
|
|
14
|
+
import { renderTemplate } from './template.js';
|
|
15
|
+
/**
|
|
16
|
+
* Thrown when a rule's `when` field is not a compilable regex.
|
|
17
|
+
*/
|
|
18
|
+
export class InvalidRewriteRuleError extends Error {
|
|
19
|
+
constructor(pattern, cause) {
|
|
20
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
21
|
+
super(`Invalid rewrite rule "when" regex: ${pattern}. ${reason}`);
|
|
22
|
+
this.name = 'InvalidRewriteRuleError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Thrown when a `vars` key collides with a named capture in `when`.
|
|
27
|
+
* Adopter must rename one — vars cannot shadow captures (the design's
|
|
28
|
+
* intent is captures + vars in a single namespace).
|
|
29
|
+
*/
|
|
30
|
+
export class VarCaptureCollisionError extends Error {
|
|
31
|
+
constructor(name) {
|
|
32
|
+
super(`Rewrite var "${name}" collides with a regex capture of the same name. Rename one.`);
|
|
33
|
+
this.name = 'VarCaptureCollisionError';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Apply an ordered list of rewrite rules to a URL.
|
|
38
|
+
*
|
|
39
|
+
* @returns `{ matched: true, rewrittenUrl, context }` for the first matching
|
|
40
|
+
* rule, or `{ matched: false }` if no rule claims the URL.
|
|
41
|
+
* @throws {InvalidRewriteRuleError} if a `when` field does not compile
|
|
42
|
+
* @throws {VarCaptureCollisionError} if a vars name shadows a capture name
|
|
43
|
+
* @throws {TemplateMissingVarError} from a template referencing an unknown name
|
|
44
|
+
* @throws {TemplateSyntaxError} from a malformed template expression
|
|
45
|
+
* @throws {UnknownTransformError} from a template calling an unknown transform
|
|
46
|
+
*/
|
|
47
|
+
export function rewriteUrl(url, rules) {
|
|
48
|
+
const stripped = stripFragmentAndQuery(url);
|
|
49
|
+
for (const rule of rules) {
|
|
50
|
+
const regex = compileWhen(rule.when);
|
|
51
|
+
const match = regex.exec(stripped);
|
|
52
|
+
if (match === null)
|
|
53
|
+
continue;
|
|
54
|
+
const captures = collectCaptures(match);
|
|
55
|
+
const context = computeContext(captures, rule.vars);
|
|
56
|
+
const rewrittenUrl = renderTemplate(rule.to, context);
|
|
57
|
+
return { matched: true, rewrittenUrl, context };
|
|
58
|
+
}
|
|
59
|
+
return { matched: false };
|
|
60
|
+
}
|
|
61
|
+
function stripFragmentAndQuery(url) {
|
|
62
|
+
const idx = url.search(/[?#]/);
|
|
63
|
+
return idx === -1 ? url : url.slice(0, idx);
|
|
64
|
+
}
|
|
65
|
+
function compileWhen(pattern) {
|
|
66
|
+
try {
|
|
67
|
+
// Rule patterns originate in trusted config (see design §8); runtime
|
|
68
|
+
// compilation is intentional, not user-input regex injection.
|
|
69
|
+
// eslint-disable-next-line security/detect-non-literal-regexp
|
|
70
|
+
return new RegExp(pattern);
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
throw new InvalidRewriteRuleError(pattern, e);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function collectCaptures(match) {
|
|
77
|
+
const captures = Object.create(null);
|
|
78
|
+
// The lib typedef for RegExpMatchArray.groups says values are `string`, but
|
|
79
|
+
// optional named groups that did not match show up at runtime as `undefined`.
|
|
80
|
+
// Cast to reflect runtime reality so the filter below is type-meaningful.
|
|
81
|
+
const groups = (match.groups ?? {});
|
|
82
|
+
for (const [key, value] of Object.entries(groups)) {
|
|
83
|
+
if (value !== undefined) {
|
|
84
|
+
captures[key] = value;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return captures;
|
|
88
|
+
}
|
|
89
|
+
function computeContext(captures, vars) {
|
|
90
|
+
const context = Object.create(null);
|
|
91
|
+
Object.assign(context, captures);
|
|
92
|
+
if (vars === undefined)
|
|
93
|
+
return context;
|
|
94
|
+
for (const [name, template] of Object.entries(vars)) {
|
|
95
|
+
if (Object.hasOwn(captures, name)) {
|
|
96
|
+
throw new VarCaptureCollisionError(name);
|
|
97
|
+
}
|
|
98
|
+
context[name] = renderTemplate(template, captures);
|
|
99
|
+
}
|
|
100
|
+
return context;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=rewrite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rewrite.js","sourceRoot":"","sources":["../../src/link-auth/rewrite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAgB/C;;GAEG;AACH,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAChD,YAAY,OAAe,EAAE,KAAc;QACzC,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACtE,KAAK,CAAC,sCAAsC,OAAO,KAAK,MAAM,EAAE,CAAC,CAAC;QAClE,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,OAAO,wBAAyB,SAAQ,KAAK;IACjD,YAAY,IAAY;QACtB,KAAK,CACH,gBAAgB,IAAI,+DAA+D,CACpF,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;IACzC,CAAC;CACF;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,UAAU,CAAC,GAAW,EAAE,KAA6B;IACnE,MAAM,QAAQ,GAAG,qBAAqB,CAAC,GAAG,CAAC,CAAC;IAE5C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrC,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACnC,IAAI,KAAK,KAAK,IAAI;YAAE,SAAS;QAE7B,MAAM,QAAQ,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;QACxC,MAAM,OAAO,GAAG,cAAc,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QACpD,MAAM,YAAY,GAAG,cAAc,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;QACtD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC;IAClD,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAC5B,CAAC;AAED,SAAS,qBAAqB,CAAC,GAAW;IACxC,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC/B,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAC9C,CAAC;AAED,SAAS,WAAW,CAAC,OAAe;IAClC,IAAI,CAAC;QACH,qEAAqE;QACrE,8DAA8D;QAC9D,8DAA8D;QAC9D,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,IAAI,uBAAuB,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAChD,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,KAAsB;IAC7C,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAA2B,CAAC;IAC/D,4EAA4E;IAC5E,8EAA8E;IAC9E,0EAA0E;IAC1E,MAAM,MAAM,GAAG,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,CAAuC,CAAC;IAC1E,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,QAAQ,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACxB,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,cAAc,CACrB,QAAgC,EAChC,IAAwC;IAExC,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAA2B,CAAC;IAC9D,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAEjC,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,OAAO,CAAC;IAEvC,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACpD,IAAI,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,wBAAwB,CAAC,IAAI,CAAC,CAAC;QAC3C,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,GAAG,cAAc,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACrD,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider selection by URL host.
|
|
3
|
+
*
|
|
4
|
+
* Iterates the configured providers in order; the first one whose `match.host`
|
|
5
|
+
* glob matches the URL hostname (and no `match.excludeHost` glob excludes it)
|
|
6
|
+
* claims the URL. Returns `undefined` if no provider claims it — the caller
|
|
7
|
+
* then falls back to anonymous link-checking.
|
|
8
|
+
*
|
|
9
|
+
* Hostname matching is case-insensitive per RFC 3986. Pattern syntax follows
|
|
10
|
+
* `picomatch` (already a utils dep): `*` matches any character, `**` matches
|
|
11
|
+
* across `/`. For hostnames (no slashes) the practical effect is that `*`
|
|
12
|
+
* happily matches dots — e.g. `*.sharepoint.com` claims both
|
|
13
|
+
* `contoso.sharepoint.com` and `foo.bar.sharepoint.com`.
|
|
14
|
+
*
|
|
15
|
+
* Per design issue #113 §4 (vocabulary item 1: match.host + excludeHost).
|
|
16
|
+
*/
|
|
17
|
+
export interface ProviderMatch {
|
|
18
|
+
readonly host: string;
|
|
19
|
+
readonly excludeHost?: readonly string[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Find the first provider whose `match` claims the given URL.
|
|
23
|
+
*
|
|
24
|
+
* @returns the matching provider, or `undefined` if none claim the URL (no
|
|
25
|
+
* pattern matched, all candidates were excluded, or the URL is malformed).
|
|
26
|
+
*/
|
|
27
|
+
export declare function selectProvider<P extends {
|
|
28
|
+
readonly match: ProviderMatch;
|
|
29
|
+
}>(url: string, providers: readonly P[]): P | undefined;
|
|
30
|
+
//# sourceMappingURL=select-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"select-provider.d.ts","sourceRoot":"","sources":["../../src/link-auth/select-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS;IAAE,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAA;CAAE,EACxE,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,SAAS,CAAC,EAAE,GACtB,CAAC,GAAG,SAAS,CAUf"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider selection by URL host.
|
|
3
|
+
*
|
|
4
|
+
* Iterates the configured providers in order; the first one whose `match.host`
|
|
5
|
+
* glob matches the URL hostname (and no `match.excludeHost` glob excludes it)
|
|
6
|
+
* claims the URL. Returns `undefined` if no provider claims it — the caller
|
|
7
|
+
* then falls back to anonymous link-checking.
|
|
8
|
+
*
|
|
9
|
+
* Hostname matching is case-insensitive per RFC 3986. Pattern syntax follows
|
|
10
|
+
* `picomatch` (already a utils dep): `*` matches any character, `**` matches
|
|
11
|
+
* across `/`. For hostnames (no slashes) the practical effect is that `*`
|
|
12
|
+
* happily matches dots — e.g. `*.sharepoint.com` claims both
|
|
13
|
+
* `contoso.sharepoint.com` and `foo.bar.sharepoint.com`.
|
|
14
|
+
*
|
|
15
|
+
* Per design issue #113 §4 (vocabulary item 1: match.host + excludeHost).
|
|
16
|
+
*/
|
|
17
|
+
import picomatch from 'picomatch';
|
|
18
|
+
/**
|
|
19
|
+
* Find the first provider whose `match` claims the given URL.
|
|
20
|
+
*
|
|
21
|
+
* @returns the matching provider, or `undefined` if none claim the URL (no
|
|
22
|
+
* pattern matched, all candidates were excluded, or the URL is malformed).
|
|
23
|
+
*/
|
|
24
|
+
export function selectProvider(url, providers) {
|
|
25
|
+
const host = extractHostname(url);
|
|
26
|
+
if (host === undefined)
|
|
27
|
+
return undefined;
|
|
28
|
+
for (const provider of providers) {
|
|
29
|
+
if (matches(host, provider.match)) {
|
|
30
|
+
return provider;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
function extractHostname(url) {
|
|
36
|
+
try {
|
|
37
|
+
return new URL(url).hostname;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function matches(host, match) {
|
|
44
|
+
const lowerHost = host.toLowerCase();
|
|
45
|
+
if (!picomatch.isMatch(lowerHost, match.host.toLowerCase()))
|
|
46
|
+
return false;
|
|
47
|
+
if (match.excludeHost !== undefined) {
|
|
48
|
+
for (const pattern of match.excludeHost) {
|
|
49
|
+
if (picomatch.isMatch(lowerHost, pattern.toLowerCase()))
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=select-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"select-provider.js","sourceRoot":"","sources":["../../src/link-auth/select-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,SAAS,MAAM,WAAW,CAAC;AAOlC;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAC5B,GAAW,EACX,SAAuB;IAEvB,MAAM,IAAI,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAEzC,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,IAAI,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YAClC,OAAO,QAAQ,CAAC;QAClB,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,eAAe,CAAC,GAAW;IAClC,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,SAAS,OAAO,CAAC,IAAY,EAAE,KAAoB;IACjD,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IACrC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;QAAE,OAAO,KAAK,CAAC;IAE1E,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QACpC,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC;YACxC,IAAI,SAAS,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC;gBAAE,OAAO,KAAK,CAAC;QACxE,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny template renderer for linkAuth rewrite + header templates.
|
|
3
|
+
*
|
|
4
|
+
* Syntax (strict, no escapes, no nesting):
|
|
5
|
+
* - `${name}` → context[name]
|
|
6
|
+
* - `${transform(name)}` → applyTransform(transform, context[name])
|
|
7
|
+
*
|
|
8
|
+
* Anything else inside `${...}` throws `TemplateSyntaxError`. Missing context
|
|
9
|
+
* keys throw `TemplateMissingVarError`. Unknown transform names propagate
|
|
10
|
+
* `UnknownTransformError` from the transforms allowlist.
|
|
11
|
+
*
|
|
12
|
+
* This is NOT the project's general-purpose Handlebars renderer
|
|
13
|
+
* (`utils/template.ts`) — that one is `{{...}}` and consumed widely. linkAuth
|
|
14
|
+
* needs different syntax and a closed transform set; the two coexist by
|
|
15
|
+
* namespace (`link-auth/template.ts`).
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Thrown when a template references a variable not present in the context.
|
|
19
|
+
* Message names the missing variable and includes the full template for
|
|
20
|
+
* caller-side debugging.
|
|
21
|
+
*/
|
|
22
|
+
export declare class TemplateMissingVarError extends Error {
|
|
23
|
+
constructor(varName: string, template: string);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Thrown when a template contains syntactically invalid content inside `${...}`
|
|
27
|
+
* (empty expression, whitespace, unrecognized form) or an unterminated `${`.
|
|
28
|
+
*/
|
|
29
|
+
export declare class TemplateSyntaxError extends Error {
|
|
30
|
+
constructor(detail: string, template: string);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Render a template by substituting `${...}` expressions from a context map.
|
|
34
|
+
*
|
|
35
|
+
* @throws {TemplateMissingVarError} if a referenced variable is absent
|
|
36
|
+
* @throws {TemplateSyntaxError} for invalid expressions or unterminated `${`
|
|
37
|
+
* @throws {UnknownTransformError} from a `${transform(name)}` call
|
|
38
|
+
*/
|
|
39
|
+
export declare function renderTemplate(template: string, context: Record<string, string>): string;
|
|
40
|
+
//# sourceMappingURL=template.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../src/link-auth/template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAOH;;;;GAIG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;CAI9C;AAED;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;CAI7C;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAUxF"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny template renderer for linkAuth rewrite + header templates.
|
|
3
|
+
*
|
|
4
|
+
* Syntax (strict, no escapes, no nesting):
|
|
5
|
+
* - `${name}` → context[name]
|
|
6
|
+
* - `${transform(name)}` → applyTransform(transform, context[name])
|
|
7
|
+
*
|
|
8
|
+
* Anything else inside `${...}` throws `TemplateSyntaxError`. Missing context
|
|
9
|
+
* keys throw `TemplateMissingVarError`. Unknown transform names propagate
|
|
10
|
+
* `UnknownTransformError` from the transforms allowlist.
|
|
11
|
+
*
|
|
12
|
+
* This is NOT the project's general-purpose Handlebars renderer
|
|
13
|
+
* (`utils/template.ts`) — that one is `{{...}}` and consumed widely. linkAuth
|
|
14
|
+
* needs different syntax and a closed transform set; the two coexist by
|
|
15
|
+
* namespace (`link-auth/template.ts`).
|
|
16
|
+
*/
|
|
17
|
+
import { applyTransform } from './transforms.js';
|
|
18
|
+
const IDENTIFIER = /^[a-zA-Z_]\w*$/;
|
|
19
|
+
const TRANSFORM_CALL = /^(?<fn>[a-zA-Z_]\w*)\((?<arg>[a-zA-Z_]\w*)\)$/;
|
|
20
|
+
/**
|
|
21
|
+
* Thrown when a template references a variable not present in the context.
|
|
22
|
+
* Message names the missing variable and includes the full template for
|
|
23
|
+
* caller-side debugging.
|
|
24
|
+
*/
|
|
25
|
+
export class TemplateMissingVarError extends Error {
|
|
26
|
+
constructor(varName, template) {
|
|
27
|
+
super(`Template variable "${varName}" not in context. Template: ${template}`);
|
|
28
|
+
this.name = 'TemplateMissingVarError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Thrown when a template contains syntactically invalid content inside `${...}`
|
|
33
|
+
* (empty expression, whitespace, unrecognized form) or an unterminated `${`.
|
|
34
|
+
*/
|
|
35
|
+
export class TemplateSyntaxError extends Error {
|
|
36
|
+
constructor(detail, template) {
|
|
37
|
+
super(`Invalid template syntax — ${detail}. Template: ${template}`);
|
|
38
|
+
this.name = 'TemplateSyntaxError';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Render a template by substituting `${...}` expressions from a context map.
|
|
43
|
+
*
|
|
44
|
+
* @throws {TemplateMissingVarError} if a referenced variable is absent
|
|
45
|
+
* @throws {TemplateSyntaxError} for invalid expressions or unterminated `${`
|
|
46
|
+
* @throws {UnknownTransformError} from a `${transform(name)}` call
|
|
47
|
+
*/
|
|
48
|
+
export function renderTemplate(template, context) {
|
|
49
|
+
const rendered = template.replaceAll(/\$\{([^}]*)\}/g, (_match, body) => resolveExpression(body, template, context));
|
|
50
|
+
if (rendered.includes('${')) {
|
|
51
|
+
throw new TemplateSyntaxError('unterminated "${" with no matching "}"', template);
|
|
52
|
+
}
|
|
53
|
+
return rendered;
|
|
54
|
+
}
|
|
55
|
+
function resolveExpression(body, template, context) {
|
|
56
|
+
if (body !== body.trim()) {
|
|
57
|
+
throw new TemplateSyntaxError(`whitespace in "${body}"`, template);
|
|
58
|
+
}
|
|
59
|
+
if (IDENTIFIER.test(body)) {
|
|
60
|
+
return lookupContextVar(context, body, template);
|
|
61
|
+
}
|
|
62
|
+
const callMatch = TRANSFORM_CALL.exec(body);
|
|
63
|
+
if (callMatch?.groups !== undefined) {
|
|
64
|
+
const { fn, arg } = callMatch.groups;
|
|
65
|
+
if (fn === undefined || arg === undefined) {
|
|
66
|
+
throw new TemplateSyntaxError(`unexpected regex result for "${body}"`, template);
|
|
67
|
+
}
|
|
68
|
+
return applyTransform(fn, lookupContextVar(context, arg, template));
|
|
69
|
+
}
|
|
70
|
+
throw new TemplateSyntaxError(`unrecognized expression "${body}"`, template);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Fetch a variable from the context map, blocking prototype-chain bypass.
|
|
74
|
+
*
|
|
75
|
+
* Without `Object.hasOwn`, `context["__proto__"]` would return `Object.prototype`
|
|
76
|
+
* (a non-undefined object) and the renderer would substitute `[object Object]`
|
|
77
|
+
* into the output. Mirrors the closed-allowlist discipline in `transforms.ts`.
|
|
78
|
+
*/
|
|
79
|
+
function lookupContextVar(context, key, template) {
|
|
80
|
+
if (!Object.hasOwn(context, key)) {
|
|
81
|
+
throw new TemplateMissingVarError(key, template);
|
|
82
|
+
}
|
|
83
|
+
const value = context[key];
|
|
84
|
+
if (value === undefined) {
|
|
85
|
+
throw new TemplateMissingVarError(key, template);
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=template.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template.js","sourceRoot":"","sources":["../../src/link-auth/template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD,MAAM,UAAU,GAAG,gBAAgB,CAAC;AACpC,MAAM,cAAc,GAAG,+CAA+C,CAAC;AAEvE;;;;GAIG;AACH,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAChD,YAAY,OAAe,EAAE,QAAgB;QAC3C,KAAK,CAAC,sBAAsB,OAAO,+BAA+B,QAAQ,EAAE,CAAC,CAAC;QAC9E,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC5C,YAAY,MAAc,EAAE,QAAgB;QAC1C,KAAK,CAAC,6BAA6B,MAAM,eAAe,QAAQ,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,QAAgB,EAAE,OAA+B;IAC9E,MAAM,QAAQ,GAAG,QAAQ,CAAC,UAAU,CAAC,gBAAgB,EAAE,CAAC,MAAM,EAAE,IAAY,EAAE,EAAE,CAC9E,iBAAiB,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,CAC3C,CAAC;IAEF,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,mBAAmB,CAAC,wCAAwC,EAAE,QAAQ,CAAC,CAAC;IACpF,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,iBAAiB,CACxB,IAAY,EACZ,QAAgB,EAChB,OAA+B;IAE/B,IAAI,IAAI,KAAK,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;QACzB,MAAM,IAAI,mBAAmB,CAAC,kBAAkB,IAAI,GAAG,EAAE,QAAQ,CAAC,CAAC;IACrE,CAAC;IAED,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1B,OAAO,gBAAgB,CAAC,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;IACnD,CAAC;IAED,MAAM,SAAS,GAAG,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5C,IAAI,SAAS,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;QACpC,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,SAAS,CAAC,MAAM,CAAC;QACrC,IAAI,EAAE,KAAK,SAAS,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YAC1C,MAAM,IAAI,mBAAmB,CAAC,gCAAgC,IAAI,GAAG,EAAE,QAAQ,CAAC,CAAC;QACnF,CAAC;QACD,OAAO,cAAc,CAAC,EAAE,EAAE,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC;IACtE,CAAC;IAED,MAAM,IAAI,mBAAmB,CAAC,4BAA4B,IAAI,GAAG,EAAE,QAAQ,CAAC,CAAC;AAC/E,CAAC;AAED;;;;;;GAMG;AACH,SAAS,gBAAgB,CACvB,OAA+B,EAC/B,GAAW,EACX,QAAgB;IAEhB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,uBAAuB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IACnD,CAAC;IACD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IAC3B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,uBAAuB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IACnD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform allowlist for linkAuth rewrite templates.
|
|
3
|
+
*
|
|
4
|
+
* Rewrite `to:` templates may call transforms over named regex captures —
|
|
5
|
+
* e.g. `${base64url(u)}` for Microsoft Graph share-ids. The allowed set is
|
|
6
|
+
* frozen in this file: adding a transform requires a VAT PR (see issue #113
|
|
7
|
+
* design §4.1). There is no arbitrary-evaluation path.
|
|
8
|
+
*
|
|
9
|
+
* v1 inputs are assumed to be ASCII URL host/path captures. Non-ASCII edge
|
|
10
|
+
* cases (lone surrogates) are pinned by tests so future regressions surface.
|
|
11
|
+
*/
|
|
12
|
+
declare const TRANSFORMS: {
|
|
13
|
+
readonly base64url: (s: string) => string;
|
|
14
|
+
readonly urlencode: (s: string) => string;
|
|
15
|
+
readonly lower: (s: string) => string;
|
|
16
|
+
};
|
|
17
|
+
export type TransformName = keyof typeof TRANSFORMS;
|
|
18
|
+
/**
|
|
19
|
+
* The closed allowlist of transform names. Callers parsing rewrite templates
|
|
20
|
+
* can validate transform calls (e.g. `${base64url(u)}`) against this list at
|
|
21
|
+
* config-load time, surfacing typos before any URL is rewritten.
|
|
22
|
+
*/
|
|
23
|
+
export declare const ALLOWED_TRANSFORMS: readonly TransformName[];
|
|
24
|
+
/**
|
|
25
|
+
* Thrown by `applyTransform` when called with a name not in the closed
|
|
26
|
+
* allowlist. The message names the bad transform and the full allowlist so
|
|
27
|
+
* a misconfigured macro surfaces a clear error at load time.
|
|
28
|
+
*/
|
|
29
|
+
export declare class UnknownTransformError extends Error {
|
|
30
|
+
constructor(name: string);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Dispatch a transform by name on a string input.
|
|
34
|
+
*
|
|
35
|
+
* Security claim: only the names in `ALLOWED_TRANSFORMS` resolve; any other
|
|
36
|
+
* input throws `UnknownTransformError`. Lookup uses `Object.hasOwn` to block
|
|
37
|
+
* prototype-chain keys (`toString`, `__proto__`, `constructor`, …) that a
|
|
38
|
+
* `name in TRANSFORMS` check would let through.
|
|
39
|
+
*
|
|
40
|
+
* @throws {UnknownTransformError} if `name` is not in the allowlist
|
|
41
|
+
* @throws {URIError} from `urlencode` on lone-surrogate inputs — deliberate;
|
|
42
|
+
* v1 callers pass URL-derived captures that cannot legally contain these
|
|
43
|
+
*/
|
|
44
|
+
export declare function applyTransform(name: string, input: string): string;
|
|
45
|
+
export {};
|
|
46
|
+
//# sourceMappingURL=transforms.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transforms.d.ts","sourceRoot":"","sources":["../../src/link-auth/transforms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,QAAA,MAAM,UAAU;4BACC,MAAM;4BACN,MAAM;wBACV,MAAM;CAC6B,CAAC;AAEjD,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,UAAU,CAAC;AAEpD;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAAS,aAAa,EAEtD,CAAC;AAEF;;;;GAIG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,IAAI,EAAE,MAAM;CAIzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKlE"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform allowlist for linkAuth rewrite templates.
|
|
3
|
+
*
|
|
4
|
+
* Rewrite `to:` templates may call transforms over named regex captures —
|
|
5
|
+
* e.g. `${base64url(u)}` for Microsoft Graph share-ids. The allowed set is
|
|
6
|
+
* frozen in this file: adding a transform requires a VAT PR (see issue #113
|
|
7
|
+
* design §4.1). There is no arbitrary-evaluation path.
|
|
8
|
+
*
|
|
9
|
+
* v1 inputs are assumed to be ASCII URL host/path captures. Non-ASCII edge
|
|
10
|
+
* cases (lone surrogates) are pinned by tests so future regressions surface.
|
|
11
|
+
*/
|
|
12
|
+
const TRANSFORMS = {
|
|
13
|
+
base64url: (s) => Buffer.from(s, 'utf8').toString('base64url'),
|
|
14
|
+
urlencode: (s) => encodeURIComponent(s),
|
|
15
|
+
lower: (s) => s.toLowerCase(),
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The closed allowlist of transform names. Callers parsing rewrite templates
|
|
19
|
+
* can validate transform calls (e.g. `${base64url(u)}`) against this list at
|
|
20
|
+
* config-load time, surfacing typos before any URL is rewritten.
|
|
21
|
+
*/
|
|
22
|
+
export const ALLOWED_TRANSFORMS = Object.freeze(Object.keys(TRANSFORMS));
|
|
23
|
+
/**
|
|
24
|
+
* Thrown by `applyTransform` when called with a name not in the closed
|
|
25
|
+
* allowlist. The message names the bad transform and the full allowlist so
|
|
26
|
+
* a misconfigured macro surfaces a clear error at load time.
|
|
27
|
+
*/
|
|
28
|
+
export class UnknownTransformError extends Error {
|
|
29
|
+
constructor(name) {
|
|
30
|
+
super(`Unknown transform "${name}". Allowed: ${ALLOWED_TRANSFORMS.join(', ')}.`);
|
|
31
|
+
this.name = 'UnknownTransformError';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Dispatch a transform by name on a string input.
|
|
36
|
+
*
|
|
37
|
+
* Security claim: only the names in `ALLOWED_TRANSFORMS` resolve; any other
|
|
38
|
+
* input throws `UnknownTransformError`. Lookup uses `Object.hasOwn` to block
|
|
39
|
+
* prototype-chain keys (`toString`, `__proto__`, `constructor`, …) that a
|
|
40
|
+
* `name in TRANSFORMS` check would let through.
|
|
41
|
+
*
|
|
42
|
+
* @throws {UnknownTransformError} if `name` is not in the allowlist
|
|
43
|
+
* @throws {URIError} from `urlencode` on lone-surrogate inputs — deliberate;
|
|
44
|
+
* v1 callers pass URL-derived captures that cannot legally contain these
|
|
45
|
+
*/
|
|
46
|
+
export function applyTransform(name, input) {
|
|
47
|
+
if (!Object.hasOwn(TRANSFORMS, name)) {
|
|
48
|
+
throw new UnknownTransformError(name);
|
|
49
|
+
}
|
|
50
|
+
return TRANSFORMS[name](input);
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=transforms.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transforms.js","sourceRoot":"","sources":["../../src/link-auth/transforms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,MAAM,UAAU,GAAG;IACjB,SAAS,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC;IACtE,SAAS,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,kBAAkB,CAAC,CAAC,CAAC;IAC/C,KAAK,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE;CACS,CAAC;AAIjD;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAA6B,MAAM,CAAC,MAAM,CACvE,MAAM,CAAC,IAAI,CAAC,UAAU,CAAoB,CAC3C,CAAC;AAEF;;;;GAIG;AACH,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAC9C,YAAY,IAAY;QACtB,KAAK,CAAC,sBAAsB,IAAI,eAAe,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjF,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,KAAa;IACxD,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,qBAAqB,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC;IACD,OAAO,UAAU,CAAC,IAAqB,CAAC,CAAC,KAAK,CAAC,CAAC;AAClD,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibe-agent-toolkit/utils",
|
|
3
|
-
"version": "0.1.39-rc.
|
|
3
|
+
"version": "0.1.39-rc.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Core utility functions with no external dependencies",
|
|
6
6
|
"keywords": [
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"README.md"
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
|
-
"build": "tsc",
|
|
26
|
+
"build": "tsc && tsx ../dev-tools/src/copy-yaml-assets.ts",
|
|
27
27
|
"test": "vitest run",
|
|
28
28
|
"test:watch": "vitest",
|
|
29
29
|
"typecheck": "tsc --noEmit",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"handlebars": "^4.7.8",
|
|
34
34
|
"ignore": "^7.0.5",
|
|
35
35
|
"picomatch": "^4.0.3",
|
|
36
|
-
"which": "^5.0.0"
|
|
36
|
+
"which": "^5.0.0",
|
|
37
|
+
"yaml": "^2.6.1"
|
|
37
38
|
},
|
|
38
39
|
"peerDependencies": {
|
|
39
40
|
"zod": "^3.25.0 || ^4.0.0"
|