@gobing-ai/ts-rule-engine 0.4.5 → 0.4.7

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.
@@ -23,6 +23,16 @@ export interface LoadExtensionsOptions {
23
23
  logger?: Pick<Logger, 'warn'>;
24
24
  /** Optional module loader seam for tests or embedders with custom import policy. */
25
25
  moduleLoader?: (absPath: string) => Promise<Record<string, unknown>>;
26
+ /**
27
+ * Optional canonical-path resolver for symlink-safe confinement (ADR-022).
28
+ * Forwarded to the shared loader. Defaults to the node filesystem's
29
+ * canonicalizer whenever the default (real dynamic-import) `moduleLoader` is
30
+ * in effect, so a symlink inside the declaring directory cannot escape it.
31
+ * Pass `realPath: undefined` explicitly to opt out (e.g. pnpm/Nix symlinked
32
+ * layouts), or supply a custom resolver. When a custom `moduleLoader` is
33
+ * given, confinement policy is caller-owned and no default applies.
34
+ */
35
+ realPath?: (absPath: string) => string;
26
36
  }
27
37
  /**
28
38
  * Collect extension refs declared by a preset's or rule file's `extensions` block.
@@ -1 +1 @@
1
- {"version":3,"file":"extensions.d.ts","sourceRoot":"","sources":["../../src/config/extensions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAOlD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC/D,2DAA2D;AAC3D,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,YAAY,GAAG,QAAQ,GAAG,YAAY,CAAC;AAEjF,yEAAyE;AACzE,MAAM,WAAW,YAAY;IACzB,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,6CAA6C;IAC7C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC/B;AAED,oDAAoD;AACpD,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,sEAAsE;IACtE,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,oFAAoF;IACpF,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACxE;AAUD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC7B,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,GAAG,SAAS,GAC7E,YAAY,EAAE,CAShB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,sBAAsB,CACxC,IAAI,EAAE,cAAc,EACpB,IAAI,EAAE,SAAS,YAAY,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GACpC,OAAO,CAAC,IAAI,CAAC,CAuDf"}
1
+ {"version":3,"file":"extensions.d.ts","sourceRoot":"","sources":["../../src/config/extensions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAclD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC/D,2DAA2D;AAC3D,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,YAAY,GAAG,QAAQ,GAAG,YAAY,CAAC;AAEjF,yEAAyE;AACzE,MAAM,WAAW,YAAY;IACzB,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,6CAA6C;IAC7C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC/B;AAED,oDAAoD;AACpD,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,sEAAsE;IACtE,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,oFAAoF;IACpF,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACrE;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;CAC1C;AAUD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC7B,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,GAAG,SAAS,GAC7E,YAAY,EAAE,CAShB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,sBAAsB,CACxC,IAAI,EAAE,cAAc,EACpB,IAAI,EAAE,SAAS,YAAY,EAAE,EAC7B,OAAO,GAAE,qBAA0B,GACpC,OAAO,CAAC,IAAI,CAAC,CAiEf"}
@@ -1,5 +1,4 @@
1
- import { fileURLToPath } from 'node:url';
2
- import { basenamePath, dirnamePath, resolvePath, SEP } from '@gobing-ai/ts-runtime';
1
+ import { basenamePath, createNodeFileSystem, dirnamePath, fileUrlToPath, normalizeSeparators, resolvePath, } from '@gobing-ai/ts-runtime';
3
2
  import { loadExtensionModules } from '@gobing-ai/ts-runtime/extension';
4
3
  /** Host registries that can receive extension capabilities (all four kinds). */
5
4
  const HOST_REGISTRY_BY_KIND = {
@@ -47,12 +46,14 @@ export async function loadExtensionsIntoHost(host, refs, options = {}) {
47
46
  return;
48
47
  // Normalize file:// URLs to file paths so dirname/basename produce valid
49
48
  // path components that isAbsolute() accepts (e.g. import.meta.url in tests).
50
- const toFilePath = (p) => (p.startsWith('file://') ? fileURLToPath(p) : p);
49
+ const toFilePath = (p) => (p.startsWith('file://') ? fileUrlToPath(p) : p);
51
50
  // Defense-in-depth: pre-validate `..` traversal in caller-supplied absPath
52
51
  // before the basename adaptation strips it. The shared loader's
53
52
  // assertRelativeExtensionPath also runs on the derived (basename) path.
54
53
  for (const ref of refs) {
55
- const segments = toFilePath(ref.absPath).split(SEP);
54
+ // Split on normalized separators: fileUrlToPath returns forward slashes on
55
+ // every platform, so a platform SEP split would miss segments on Windows.
56
+ const segments = normalizeSeparators(toFilePath(ref.absPath)).split('/');
56
57
  if (segments.includes('..')) {
57
58
  throw new Error(`extension path "${ref.absPath}" declared by "${ref.presetName}" must not contain ".." traversal`);
58
59
  }
@@ -71,10 +72,17 @@ export async function loadExtensionsIntoHost(host, refs, options = {}) {
71
72
  };
72
73
  });
73
74
  const moduleLoader = options.moduleLoader ?? defaultModuleLoader;
75
+ // Real import policy gets real confinement by default (ADR-022 addendum): a
76
+ // caller relying on the default dynamic-import loader that has not set
77
+ // `realPath` — even explicitly to undefined — gets node canonicalization, so a
78
+ // symlink inside the declaring directory cannot escape it. A custom
79
+ // moduleLoader owns its confinement policy; no default applies there.
80
+ const realPath = 'realPath' in options ? options.realPath : options.moduleLoader === undefined ? defaultRealPath() : undefined;
74
81
  const sharedOptions = {
75
82
  allowExtensions: options.allowExtensions,
76
83
  logger: options.logger,
77
84
  moduleLoader,
85
+ realPath,
78
86
  };
79
87
  await loadExtensionModules(sharedRefs, sharedOptions, async (sharedRef, extension) => {
80
88
  const name = extension.name;
@@ -92,3 +100,7 @@ export async function loadExtensionsIntoHost(host, refs, options = {}) {
92
100
  async function defaultModuleLoader(absPath) {
93
101
  return (await import(absPath));
94
102
  }
103
+ function defaultRealPath() {
104
+ const fs = createNodeFileSystem();
105
+ return (absPath) => (fs.realPath ? fs.realPath(absPath) : absPath);
106
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-rule-engine",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "@gobing-ai/ts-rule-engine — Constraint rule schemas, loading, evaluation, and result formatting.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -49,10 +49,10 @@
49
49
  "release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-rule-engine-v<version> && git push --tags' && exit 1"
50
50
  },
51
51
  "dependencies": {
52
- "@gobing-ai/ts-ai-runner": "^0.4.5",
53
- "@gobing-ai/ts-infra": "^0.4.5",
54
- "@gobing-ai/ts-runtime": "^0.4.5",
55
- "@gobing-ai/ts-db": "^0.4.5",
52
+ "@gobing-ai/ts-ai-runner": "^0.4.7",
53
+ "@gobing-ai/ts-infra": "^0.4.7",
54
+ "@gobing-ai/ts-runtime": "^0.4.7",
55
+ "@gobing-ai/ts-db": "^0.4.7",
56
56
  "yaml": "^2.7.0",
57
57
  "zod": "^4.1.0"
58
58
  },
@@ -1,6 +1,12 @@
1
- import { fileURLToPath } from 'node:url';
2
1
  import type { Logger } from '@gobing-ai/ts-infra';
3
- import { basenamePath, dirnamePath, resolvePath, SEP } from '@gobing-ai/ts-runtime';
2
+ import {
3
+ basenamePath,
4
+ createNodeFileSystem,
5
+ dirnamePath,
6
+ fileUrlToPath,
7
+ normalizeSeparators,
8
+ resolvePath,
9
+ } from '@gobing-ai/ts-runtime';
4
10
  import type {
5
11
  ExtensionRef as SharedExtensionRef,
6
12
  LoadExtensionsOptions as SharedLoadExtensionsOptions,
@@ -32,6 +38,16 @@ export interface LoadExtensionsOptions {
32
38
  logger?: Pick<Logger, 'warn'>;
33
39
  /** Optional module loader seam for tests or embedders with custom import policy. */
34
40
  moduleLoader?: (absPath: string) => Promise<Record<string, unknown>>;
41
+ /**
42
+ * Optional canonical-path resolver for symlink-safe confinement (ADR-022).
43
+ * Forwarded to the shared loader. Defaults to the node filesystem's
44
+ * canonicalizer whenever the default (real dynamic-import) `moduleLoader` is
45
+ * in effect, so a symlink inside the declaring directory cannot escape it.
46
+ * Pass `realPath: undefined` explicitly to opt out (e.g. pnpm/Nix symlinked
47
+ * layouts), or supply a custom resolver. When a custom `moduleLoader` is
48
+ * given, confinement policy is caller-owned and no default applies.
49
+ */
50
+ realPath?: (absPath: string) => string;
35
51
  }
36
52
 
37
53
  /** Host registries that can receive extension capabilities (all four kinds). */
@@ -89,13 +105,15 @@ export async function loadExtensionsIntoHost(
89
105
 
90
106
  // Normalize file:// URLs to file paths so dirname/basename produce valid
91
107
  // path components that isAbsolute() accepts (e.g. import.meta.url in tests).
92
- const toFilePath = (p: string): string => (p.startsWith('file://') ? fileURLToPath(p) : p);
108
+ const toFilePath = (p: string): string => (p.startsWith('file://') ? fileUrlToPath(p) : p);
93
109
 
94
110
  // Defense-in-depth: pre-validate `..` traversal in caller-supplied absPath
95
111
  // before the basename adaptation strips it. The shared loader's
96
112
  // assertRelativeExtensionPath also runs on the derived (basename) path.
97
113
  for (const ref of refs) {
98
- const segments = toFilePath(ref.absPath).split(SEP);
114
+ // Split on normalized separators: fileUrlToPath returns forward slashes on
115
+ // every platform, so a platform SEP split would miss segments on Windows.
116
+ const segments = normalizeSeparators(toFilePath(ref.absPath)).split('/');
99
117
  if (segments.includes('..')) {
100
118
  throw new Error(
101
119
  `extension path "${ref.absPath}" declared by "${ref.presetName}" must not contain ".." traversal`,
@@ -118,10 +136,18 @@ export async function loadExtensionsIntoHost(
118
136
  });
119
137
 
120
138
  const moduleLoader = options.moduleLoader ?? defaultModuleLoader;
139
+ // Real import policy gets real confinement by default (ADR-022 addendum): a
140
+ // caller relying on the default dynamic-import loader that has not set
141
+ // `realPath` — even explicitly to undefined — gets node canonicalization, so a
142
+ // symlink inside the declaring directory cannot escape it. A custom
143
+ // moduleLoader owns its confinement policy; no default applies there.
144
+ const realPath =
145
+ 'realPath' in options ? options.realPath : options.moduleLoader === undefined ? defaultRealPath() : undefined;
121
146
  const sharedOptions: SharedLoadExtensionsOptions = {
122
147
  allowExtensions: options.allowExtensions,
123
148
  logger: options.logger,
124
149
  moduleLoader,
150
+ realPath,
125
151
  };
126
152
 
127
153
  await loadExtensionModules<ExtensionKind>(sharedRefs, sharedOptions, async (sharedRef, extension) => {
@@ -144,3 +170,8 @@ export async function loadExtensionsIntoHost(
144
170
  async function defaultModuleLoader(absPath: string): Promise<Record<string, unknown>> {
145
171
  return (await import(absPath)) as Record<string, unknown>;
146
172
  }
173
+
174
+ function defaultRealPath(): (absPath: string) => string {
175
+ const fs = createNodeFileSystem();
176
+ return (absPath) => (fs.realPath ? fs.realPath(absPath) : absPath);
177
+ }