@ontrails/core 1.0.0-beta.30 → 1.0.0-beta.39
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/CHANGELOG.md +63 -0
- package/package.json +1 -1
- package/src/context.ts +18 -4
- package/src/derive.ts +30 -3
- package/src/diagnostics.ts +21 -0
- package/src/draft.ts +18 -10
- package/src/glob.ts +81 -0
- package/src/index.ts +79 -4
- package/src/path-scope.ts +66 -0
- package/src/path-security.ts +10 -2
- package/src/resource-config.ts +16 -4
- package/src/runtime-builtins.ts +69 -0
- package/src/sha256.ts +136 -0
- package/src/signal-diagnostics.ts +4 -4
- package/src/surface-filter.ts +3 -78
- package/src/surface-overlay.ts +694 -0
- package/src/trail-id-glob.ts +15 -0
- package/src/trail.ts +78 -0
- package/src/trails-db.ts +28 -22
- package/src/validate-established-topo.ts +2 -2
- package/src/validate-topo.ts +215 -64
- package/src/webhook.ts +194 -11
- package/src/workspace.ts +182 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
# @ontrails/core
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.39
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`f42ca6e`](https://github.com/outfitter-dev/trails/commit/f42ca6e40b29155acec446e5bf44e52e014466bd): Hard cutover: the CLI consumes `cli` bindings from the app-authored surfaces overlay. Scalar bindings behave identically to the removed cliAliases (parity-tested) — the binding name splits on `.` into a transparent synonym command path for exactly one trail. List bindings arrive as command groups: each expanded member trail gets a group-prefixed route that dispatches the member trail with its identity preserved, and a singleton list stays a group. Expansion is fail-fast boundary validation: a scalar binding resolving to zero or multiple trails, or a group with an empty member union, is a `ValidationError` naming the binding. `DeriveTopoGraphOptions.cliAliases`, the `cliAliases`/`trailsCliAliases` app-module export convention, and the per-kind compile lift are deleted; `deriveCliCommands`/`createProgram` take `overlays` instead of `aliases`, and both topo-graph derivation pipelines expand the same bindings through one shared helper so runtime CLI routes and lock routes come from one semantic. A leftover legacy export is now a Warden error (`no-legacy-cli-alias-export`) naming the `surfaceOverlay({ cli: { ... } })` rewrite.
|
|
8
|
+
|
|
9
|
+
This is a breaking API removal shipped under the lockstep beta patch convention (pre-1.0 hard-cutover posture, zero external adoption); the removed options have no deprecation window by design.
|
|
10
|
+
|
|
11
|
+
- [`6b75a46`](https://github.com/outfitter-dev/trails/commit/6b75a46ab6210237d306cceade833bf9ce6e7431): The core barrel is now execution-portable: no eager `bun:`/`node:` builtin imports remain on its module graph (TRL-1198). `trails-db`, workspace discovery, and path security load `bun:sqlite`, `node:fs`, `node:os`, and `node:path` lazily through `process.getBuiltinModule` at first use, and signal payload summaries plus per-project store keys use a pure SHA-256 (output-identical to `node:crypto`). A Worker bundle no longer needs a `bun:sqlite` stub plugin or the `nodejs_compat` flag to serve trails; the Cloudflare adapter's miniflare lane now bundles without externals and boots workerd without `nodejs_compat` as the structural regression gate, and its README stub instructions are replaced with the portable posture. Tooling helpers throw a clear `InternalError` naming the missing builtin when called on runtimes without it.
|
|
12
|
+
- [`81373bc`](https://github.com/outfitter-dev/trails/commit/81373bc5e980bb06d56fb06af4f0986f72e318c7): Wave-2 MCP cutover to the app-authored `surfaces` overlay. The overlay's `mcp` bindings are now the authored, lockable default for the MCP surface: a list binding derives one grouped trailhead tool (member selection in `{ trail, input }`, member identity preserved in `{ trail, output }`, deterministic derived description), and a scalar binding derives an additional tool synonym whose MCP-safe name is published verbatim and must expand to exactly one trail. `deriveMcpTools`/`createServer` accept the new `overlays` option; `@ontrails/core` gains `expandMcpSurfaceBindings` and `deriveMcpTrailheadDescription`.
|
|
13
|
+
|
|
14
|
+
The call-site `CreateServerOptions.trailheads` map survives as permanent override-in-context design, not a compatibility bridge: when both channels are present, the call-site map wins at runtime. Warden's new `trailhead-override-divergence` rule (warn) names both sides when a call-site map's binding names or member selectors diverge from the authored overlay default.
|
|
15
|
+
|
|
16
|
+
Topographer now derives `graph.trailheads` from the overlay's `mcp` list bindings in both `deriveTopoGraph` and the store-side graph build, so trailhead facts flow from compiled locks into Wayfinder reads for the first time. The never-wired `DeriveTopoGraphOptions.trailheads` option and the `TopoGraphTrailheadDeclaration`/`TopoGraphTrailheadTrailSelector` types are removed — a beta-window hard cutover of an option no caller could reach; author the equivalent `mcp` list binding in `surfaceOverlay({ mcp })` instead.
|
|
17
|
+
|
|
18
|
+
- [`820b4ad`](https://github.com/outfitter-dev/trails/commit/820b4ad9c40ea383b3c489a05fe7e4b2328e324f): Add `surfaceOverlay` — the shared surface-naming schema (scalar binding = synonym, list binding = grouped entry, singleton list stays a group) with app-authored/adapter-derived overlay provenance enforced at collection and consumption, and the `surface-overlay-coherence` Warden rule. MCP tool-name derivation moves to `@ontrails/core` (`deriveMcpToolName`) so the surface and governance read one projection; `@ontrails/mcp`'s `deriveToolName` now delegates to it. The coherence rule activates on standard warden runs once fresh derivations collect app-module overlays through the shared compile channel (TRL-1209, next in this stack).
|
|
19
|
+
- [`28d75fb`](https://github.com/outfitter-dev/trails/commit/28d75fbadecc62794f43957bc3aca11a4cf39c51): Add `forkVersion()` so fork version entries get typed blazes (TRL-1180). `TrailVersions` fixes every entry's generics to `unknown`, which left fork blazes with `unknown` input and forced authors to re-parse the already-validated value just to narrow it. `forkVersion({ input, output, blaze, ... })` threads the entry's own schemas into the blaze signature (including merged `composeInput` fields) and enforces the entry's output shape at compile time; the erasure back to `TrailVersionEntry` is sound because the fork pipeline validates raw input against the entry's own schema before dispatch. `TrailVersionForkSpec` is exported alongside it.
|
|
20
|
+
- [`b9e82a3`](https://github.com/outfitter-dev/trails/commit/b9e82a33546356c93fbc302fb934a83f19f1c2c5): Webhook ingress v2 (TRL-1194, absorbing TRL-1174 and TRL-1175): store-verified, per-endpoint webhook ingress becomes framework-expressible. `webhook()` accepts dynamic path segments (`path: '/hooks/:endpoint'`) whose values are delivered as envelope fields, opt-in `rawBody: true` delivery (a non-JSON body is no longer a surface-level failure — the trail owns payload interpretation), an allowlisted `headers` list delivered lowercased, and `resources` that make `verify` resource-capable: the HTTP surface resolves the declared resources into a context for the verifier and releases them afterwards, so signature checks can reach stores holding per-endpoint secrets. Envelope-mode ingress responds 202 Accepted; classic static webhooks keep their exact-match, JSON-gated, 200 behavior. Core exports `parseWebhookPathParams`, `matchWebhookPath`, `webhookPathPatternsOverlap`, and `createResources`. The `webhook-route-collision` Warden rule now also flags dynamic patterns that overlap other webhook or derived routes, not just exact method/path duplicates.
|
|
21
|
+
|
|
22
|
+
## 1.0.0-beta.38
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- [`a105127`](https://github.com/outfitter-dev/trails/commit/a105127e5662ed9a6c245125f791fb0182da3f5e): Add the `@ontrails/cloudflare` adapter collection with its first two service subpaths. `@ontrails/cloudflare/workers` exports `createWorkersHandler`, a materializer producing the `{ fetch(request, env, ctx) }` Worker export on the shared HTTP fetch kernel, with an env bridge that re-resolves env-bound resources whenever a new Worker `env` arrives so no resource instance serves a request with a stale env. `@ontrails/cloudflare/kv` exports `cloudflareKv`, a resource definition wrapping a KV namespace binding (`get`/`put`/`delete`/`list` with TTL options) plus an in-memory `createMemoryKv` mock so `testAll` runs configuration-free.
|
|
27
|
+
|
|
28
|
+
`@ontrails/core` now guards the default trail context fields: `requestId` falls back to `crypto.randomUUID()` when the `Bun` global is absent, and `cwd`/`env` fall back to `'/'`/`{}` when `process` is absent, so trail execution works on runtimes like Cloudflare Workers.
|
|
29
|
+
|
|
30
|
+
`@ontrails/warden` registers the `@ontrails/cloudflare` public barrel in the repo-local `public-export-example-coverage` policy, requiring `@example` TSDoc coverage on `createWorkersHandler` and `cloudflareKv`.
|
|
31
|
+
|
|
32
|
+
## 1.0.0-beta.37
|
|
33
|
+
|
|
34
|
+
## 1.0.0-beta.36
|
|
35
|
+
|
|
36
|
+
## 1.0.0-beta.35
|
|
37
|
+
|
|
38
|
+
### Patch Changes
|
|
39
|
+
|
|
40
|
+
- [`417bd84`](https://github.com/outfitter-dev/trails/commit/417bd8471d0f0f47ad5f33cd2ac1c606eccd72f8): Promote signal trace helpers from tracing compatibility code to core exports, and make tracing's memory sink wrapper use the observe-owned implementation.
|
|
41
|
+
- [`a88114b`](https://github.com/outfitter-dev/trails/commit/a88114b4dd0772db6b58ecdb7671e4169e6bdca5): Expose stable typed topo diagnostics for missing references so downstream
|
|
42
|
+
migration tooling can consume validation results without parsing human messages.
|
|
43
|
+
|
|
44
|
+
## 1.0.0-beta.34
|
|
45
|
+
|
|
46
|
+
## 1.0.0-beta.33
|
|
47
|
+
|
|
48
|
+
## 1.0.0-beta.32
|
|
49
|
+
|
|
50
|
+
### Patch Changes
|
|
51
|
+
|
|
52
|
+
- 3e5c0fc: Export shared diagnostic base types from core and align governance diagnostic
|
|
53
|
+
severity vocabulary across adapter checks, permits, and Warden.
|
|
54
|
+
- f3c4fef: Export a shared `escapeRegExp` helper from core and migrate first-party callers off local copies.
|
|
55
|
+
- cb0a9d8: Export shared workspace package discovery helpers from core and migrate first-party discovery callers.
|
|
56
|
+
- 21c6dda: Rename topo and draft report types to `TopoDiagnostic` and `DraftDiagnostic`, with deprecated `TopoIssue` and `DraftFinding` aliases preserved for source compatibility.
|
|
57
|
+
- fe72b84: Fold remaining Regrade and Warden scan-target surfaces onto the shared path-scope vocabulary.
|
|
58
|
+
|
|
59
|
+
## 1.0.0-beta.31
|
|
60
|
+
|
|
61
|
+
### Patch Changes
|
|
62
|
+
|
|
63
|
+
- 4cd5d4e: Add shared glob, path-scope, and trail-id glob contracts for downstream Trails tooling.
|
|
64
|
+
- 38907cc: Adopt the shared trail-id glob engine for surface filtering and Wayfinder entity filters so dotted `*`, `**`, and `?` patterns behave consistently across graph inspection and surface selection.
|
|
65
|
+
|
|
3
66
|
## 1.0.0-beta.30
|
|
4
67
|
|
|
5
68
|
## 1.0.0-beta.29
|
package/package.json
CHANGED
package/src/context.ts
CHANGED
|
@@ -21,11 +21,25 @@ export const passthroughTrace: TraceFn = async <T>(
|
|
|
21
21
|
fn: () => T | Promise<T>
|
|
22
22
|
): Promise<T> => await fn();
|
|
23
23
|
|
|
24
|
+
const defaultCwd = (): string =>
|
|
25
|
+
typeof process === 'undefined' ? '/' : process.cwd();
|
|
26
|
+
|
|
27
|
+
const defaultEnv = (): Record<string, string | undefined> =>
|
|
28
|
+
typeof process === 'undefined'
|
|
29
|
+
? {}
|
|
30
|
+
: (process.env as Record<string, string | undefined>);
|
|
31
|
+
|
|
32
|
+
const defaultRequestId = (): string =>
|
|
33
|
+
typeof Bun === 'undefined' ? crypto.randomUUID() : Bun.randomUUIDv7();
|
|
34
|
+
|
|
24
35
|
/**
|
|
25
36
|
* Create a TrailContext with sensible defaults.
|
|
26
37
|
*
|
|
27
|
-
* - `requestId` defaults to `Bun.randomUUIDv7()` (sortable v7 UUID)
|
|
38
|
+
* - `requestId` defaults to `Bun.randomUUIDv7()` (sortable v7 UUID), falling
|
|
39
|
+
* back to `crypto.randomUUID()` on runtimes without the `Bun` global
|
|
28
40
|
* - `abortSignal` defaults to a fresh, non-aborted `AbortSignal`
|
|
41
|
+
* - `cwd`/`env` default to the `process` globals when available, and to
|
|
42
|
+
* `'/'`/`{}` on runtimes without `process` (for example Cloudflare Workers)
|
|
29
43
|
* - All other fields come from `overrides`
|
|
30
44
|
*/
|
|
31
45
|
export const createTrailContext = (
|
|
@@ -33,10 +47,10 @@ export const createTrailContext = (
|
|
|
33
47
|
): TrailContext => {
|
|
34
48
|
const ctx = {
|
|
35
49
|
abortSignal: new AbortController().signal,
|
|
36
|
-
cwd:
|
|
50
|
+
cwd: defaultCwd(),
|
|
37
51
|
dryRun: false,
|
|
38
|
-
env:
|
|
39
|
-
requestId:
|
|
52
|
+
env: defaultEnv(),
|
|
53
|
+
requestId: defaultRequestId(),
|
|
40
54
|
trace: passthroughTrace,
|
|
41
55
|
...overrides,
|
|
42
56
|
} as MutableTrailContext;
|
package/src/derive.ts
CHANGED
|
@@ -145,7 +145,7 @@ const propagateDescription = (
|
|
|
145
145
|
}
|
|
146
146
|
};
|
|
147
147
|
|
|
148
|
-
/** Step one level of
|
|
148
|
+
/** Step one level of transparent wrapper unwrapping. Returns null if not a wrapper type. */
|
|
149
149
|
const unwrapStep = (
|
|
150
150
|
current: ZodInternals,
|
|
151
151
|
state: {
|
|
@@ -155,10 +155,16 @@ const unwrapStep = (
|
|
|
155
155
|
}
|
|
156
156
|
): ZodInternals | null => {
|
|
157
157
|
const defType = current._zod.def['type'] as string;
|
|
158
|
-
if (
|
|
158
|
+
if (
|
|
159
|
+
defType !== 'optional' &&
|
|
160
|
+
defType !== 'default' &&
|
|
161
|
+
defType !== 'readonly'
|
|
162
|
+
) {
|
|
159
163
|
return null;
|
|
160
164
|
}
|
|
161
|
-
|
|
165
|
+
if (defType !== 'readonly') {
|
|
166
|
+
state.required = false;
|
|
167
|
+
}
|
|
162
168
|
if (defType === 'default') {
|
|
163
169
|
state.defaultValue = current._zod.def['defaultValue'];
|
|
164
170
|
}
|
|
@@ -376,6 +382,27 @@ const normalizeCliAlias = ({
|
|
|
376
382
|
};
|
|
377
383
|
};
|
|
378
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Convert app name + trail ID to an MCP-safe tool name.
|
|
387
|
+
*
|
|
388
|
+
* MCP tool names must be `[a-z0-9_]+`: the app name prefixes the trail id,
|
|
389
|
+
* dots and hyphens collapse to underscores, and everything lowercases. This
|
|
390
|
+
* is the one owner for the projection — the MCP surface renders tools with
|
|
391
|
+
* it and Warden checks binding-name collisions against it, so the two
|
|
392
|
+
* readers cannot drift.
|
|
393
|
+
*
|
|
394
|
+
* @example
|
|
395
|
+
* ```ts
|
|
396
|
+
* deriveMcpToolName('myapp', 'entity.show'); // "myapp_entity_show"
|
|
397
|
+
* deriveMcpToolName('dispatch', 'patch.search'); // "dispatch_patch_search"
|
|
398
|
+
* ```
|
|
399
|
+
*/
|
|
400
|
+
export const deriveMcpToolName = (appName: string, trailId: string): string => {
|
|
401
|
+
const prefix = appName.toLowerCase().replaceAll(/[.-]/g, '_');
|
|
402
|
+
const suffix = trailId.toLowerCase().replaceAll(/[.-]/g, '_');
|
|
403
|
+
return `${prefix}_${suffix}`;
|
|
404
|
+
};
|
|
405
|
+
|
|
379
406
|
/** Derive resolved CLI command routes for one trail. */
|
|
380
407
|
export const deriveTrailCliCommandProjection = (
|
|
381
408
|
trail: TrailCliProjectionInput,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared diagnostic vocabulary for governance-style findings.
|
|
3
|
+
*
|
|
4
|
+
* Runtime side-channel records and field-state reports can still define their
|
|
5
|
+
* own shapes. This base exists for tools that report rule or check failures.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type DiagnosticSeverity = 'error' | 'warn';
|
|
9
|
+
|
|
10
|
+
export interface DiagnosticBase<TCode extends string = string> {
|
|
11
|
+
readonly code?: TCode | undefined;
|
|
12
|
+
readonly message: string;
|
|
13
|
+
readonly severity: DiagnosticSeverity;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface RuleDiagnosticBase<
|
|
17
|
+
TCode extends string = string,
|
|
18
|
+
TRule extends string = string,
|
|
19
|
+
> extends DiagnosticBase<TCode> {
|
|
20
|
+
readonly rule: TRule;
|
|
21
|
+
}
|
package/src/draft.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface DraftDependency {
|
|
|
25
25
|
readonly toId: string;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export interface
|
|
28
|
+
export interface DraftDiagnostic {
|
|
29
29
|
readonly id: string;
|
|
30
30
|
readonly kind: 'contour' | 'resource' | 'signal' | 'trail' | 'unknown';
|
|
31
31
|
readonly message: string;
|
|
@@ -34,11 +34,19 @@ export interface DraftFinding {
|
|
|
34
34
|
readonly dependsOn?: string | undefined;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* @deprecated Use {@link DraftDiagnostic}. Kept as a source-compatible alias
|
|
39
|
+
* during the v1 vocabulary cutover.
|
|
40
|
+
*/
|
|
41
|
+
export interface DraftFinding extends DraftDiagnostic {
|
|
42
|
+
readonly id: DraftDiagnostic['id'];
|
|
43
|
+
}
|
|
44
|
+
|
|
37
45
|
export interface DraftReport {
|
|
38
46
|
readonly contaminatedIds: ReadonlySet<string>;
|
|
39
47
|
readonly declaredDraftIds: ReadonlySet<string>;
|
|
40
48
|
readonly dependencies: readonly DraftDependency[];
|
|
41
|
-
readonly findings: readonly
|
|
49
|
+
readonly findings: readonly DraftDiagnostic[];
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
interface DraftReason {
|
|
@@ -120,7 +128,7 @@ const nodeKind = (
|
|
|
120
128
|
trails: ReadonlyMap<string, AnyTrail>,
|
|
121
129
|
signals: ReadonlyMap<string, AnySignal>,
|
|
122
130
|
resources: ReadonlyMap<string, AnyResource>
|
|
123
|
-
):
|
|
131
|
+
): DraftDiagnostic['kind'] => {
|
|
124
132
|
if (contours.has(id)) {
|
|
125
133
|
return 'contour';
|
|
126
134
|
}
|
|
@@ -136,7 +144,7 @@ const nodeKind = (
|
|
|
136
144
|
return 'unknown';
|
|
137
145
|
};
|
|
138
146
|
|
|
139
|
-
const displayKind = (kind:
|
|
147
|
+
const displayKind = (kind: DraftDiagnostic['kind']): string =>
|
|
140
148
|
kind === 'unknown' ? 'Node' : kind[0]?.toUpperCase() + kind.slice(1);
|
|
141
149
|
|
|
142
150
|
const draftIdsFromKeys = (keys: Iterable<string>): string[] =>
|
|
@@ -152,8 +160,8 @@ const collectDeclaredDraftIds = (topo: Topo): ReadonlySet<string> =>
|
|
|
152
160
|
|
|
153
161
|
const findingForDraftId = (
|
|
154
162
|
id: string,
|
|
155
|
-
kind:
|
|
156
|
-
):
|
|
163
|
+
kind: DraftDiagnostic['kind']
|
|
164
|
+
): DraftDiagnostic => ({
|
|
157
165
|
id,
|
|
158
166
|
kind,
|
|
159
167
|
message: `${displayKind(id ? kind : 'unknown')} "${id}" is draft and cannot appear in the established graph.`,
|
|
@@ -162,9 +170,9 @@ const findingForDraftId = (
|
|
|
162
170
|
|
|
163
171
|
const findingForContamination = (
|
|
164
172
|
id: string,
|
|
165
|
-
kind:
|
|
173
|
+
kind: DraftDiagnostic['kind'],
|
|
166
174
|
reason: DraftReason
|
|
167
|
-
):
|
|
175
|
+
): DraftDiagnostic => {
|
|
168
176
|
const dependencyLabel = isDraftId(reason.dependsOn)
|
|
169
177
|
? `draft "${reason.dependsOn}"`
|
|
170
178
|
: `draft-contaminated "${reason.dependsOn}"`;
|
|
@@ -244,7 +252,7 @@ const contaminationFindingForId = (
|
|
|
244
252
|
trails: ReadonlyMap<string, AnyTrail>,
|
|
245
253
|
signals: ReadonlyMap<string, AnySignal>,
|
|
246
254
|
resources: ReadonlyMap<string, AnyResource>
|
|
247
|
-
):
|
|
255
|
+
): DraftDiagnostic | undefined => {
|
|
248
256
|
if (declaredDraftIds.has(id)) {
|
|
249
257
|
return undefined;
|
|
250
258
|
}
|
|
@@ -269,7 +277,7 @@ const collectFindings = (
|
|
|
269
277
|
trails: ReadonlyMap<string, AnyTrail>,
|
|
270
278
|
signals: ReadonlyMap<string, AnySignal>,
|
|
271
279
|
resources: ReadonlyMap<string, AnyResource>
|
|
272
|
-
):
|
|
280
|
+
): DraftDiagnostic[] => [
|
|
273
281
|
...[...declaredDraftIds]
|
|
274
282
|
.toSorted()
|
|
275
283
|
.map((id) =>
|
package/src/glob.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export interface GlobConfig {
|
|
2
|
+
readonly separator: '/' | '.';
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Escape a literal string so it can be embedded safely in a RegExp source.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* const pattern = new RegExp(`^${escapeRegExp('@ontrails/core')}$`);
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export const escapeRegExp = (value: string): string =>
|
|
14
|
+
value.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
15
|
+
|
|
16
|
+
const separatorRegExp = (separator: GlobConfig['separator']): string =>
|
|
17
|
+
escapeRegExp(separator);
|
|
18
|
+
|
|
19
|
+
export const globToRegExp = (pattern: string, config: GlobConfig): RegExp => {
|
|
20
|
+
const separator = separatorRegExp(config.separator);
|
|
21
|
+
const parts: string[] = ['^'];
|
|
22
|
+
|
|
23
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
24
|
+
const char = pattern[index];
|
|
25
|
+
const next = pattern[index + 1];
|
|
26
|
+
|
|
27
|
+
if (char === '*' && next === '*') {
|
|
28
|
+
if (pattern[index + 2] === config.separator) {
|
|
29
|
+
parts.push(`(?:.*${separator})?`);
|
|
30
|
+
index += 2;
|
|
31
|
+
} else {
|
|
32
|
+
parts.push('.*');
|
|
33
|
+
index += 1;
|
|
34
|
+
}
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (char === '*') {
|
|
39
|
+
parts.push(`[^${separator}]*`);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (char === '?') {
|
|
44
|
+
parts.push(`[^${separator}]`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
parts.push(escapeRegExp(char ?? ''));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
parts.push('$');
|
|
52
|
+
return new RegExp(parts.join(''));
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const matchesGlob = (
|
|
56
|
+
value: string,
|
|
57
|
+
pattern: string,
|
|
58
|
+
config: GlobConfig
|
|
59
|
+
): boolean => {
|
|
60
|
+
if (value === pattern) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const terminalDoubleStar = `${config.separator}**`;
|
|
65
|
+
if (pattern.endsWith(terminalDoubleStar)) {
|
|
66
|
+
const parent = pattern.slice(0, -terminalDoubleStar.length);
|
|
67
|
+
if (value === parent) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return globToRegExp(pattern, config).test(value);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const matchesAnyGlob = (
|
|
76
|
+
value: string,
|
|
77
|
+
patterns: readonly string[] | undefined,
|
|
78
|
+
config: GlobConfig
|
|
79
|
+
): boolean =>
|
|
80
|
+
patterns !== undefined &&
|
|
81
|
+
patterns.some((pattern) => matchesGlob(value, pattern, config));
|
package/src/index.ts
CHANGED
|
@@ -68,6 +68,11 @@ export {
|
|
|
68
68
|
redactErrorString,
|
|
69
69
|
} from './error-projection.js';
|
|
70
70
|
export type { ErrorDiagnosticsProjection } from './error-projection.js';
|
|
71
|
+
export type {
|
|
72
|
+
DiagnosticBase,
|
|
73
|
+
DiagnosticSeverity,
|
|
74
|
+
RuleDiagnosticBase,
|
|
75
|
+
} from './diagnostics.js';
|
|
71
76
|
|
|
72
77
|
// Types
|
|
73
78
|
export type {
|
|
@@ -112,6 +117,25 @@ export type {
|
|
|
112
117
|
// Context factory
|
|
113
118
|
export { createTrailContext, passthroughTrace } from './context.js';
|
|
114
119
|
|
|
120
|
+
// Glob and path scope
|
|
121
|
+
export {
|
|
122
|
+
escapeRegExp,
|
|
123
|
+
globToRegExp,
|
|
124
|
+
matchesAnyGlob,
|
|
125
|
+
matchesGlob,
|
|
126
|
+
} from './glob.js';
|
|
127
|
+
export type { GlobConfig } from './glob.js';
|
|
128
|
+
export {
|
|
129
|
+
includedByPathScope,
|
|
130
|
+
matchesAnyPathGlob,
|
|
131
|
+
matchesPathGlob,
|
|
132
|
+
normalizePathScopePath,
|
|
133
|
+
pathScopeSchema,
|
|
134
|
+
} from './path-scope.js';
|
|
135
|
+
export type { PathGlob, PathScope, ScanTargets } from './path-scope.js';
|
|
136
|
+
export { matchesAnyTrailIdGlob, matchesTrailIdGlob } from './trail-id-glob.js';
|
|
137
|
+
export type { TrailIdGlob } from './trail-id-glob.js';
|
|
138
|
+
|
|
115
139
|
// Resource
|
|
116
140
|
export {
|
|
117
141
|
createResourceLookup,
|
|
@@ -119,8 +143,13 @@ export {
|
|
|
119
143
|
isResource,
|
|
120
144
|
resource,
|
|
121
145
|
} from './resource.js';
|
|
122
|
-
export {
|
|
146
|
+
export {
|
|
147
|
+
createResources,
|
|
148
|
+
drainResources,
|
|
149
|
+
resolveResourceConfig,
|
|
150
|
+
} from './resource-config.js';
|
|
123
151
|
export type {
|
|
152
|
+
ResolvedResourceScope,
|
|
124
153
|
ResourceConfigValues,
|
|
125
154
|
ResourceDrainReport,
|
|
126
155
|
} from './resource-config.js';
|
|
@@ -162,6 +191,7 @@ export {
|
|
|
162
191
|
export type { ActivationSourceProjection } from './activation-source-projection.js';
|
|
163
192
|
export {
|
|
164
193
|
deriveSupportedTrailVersions,
|
|
194
|
+
forkVersion,
|
|
165
195
|
getTrailVersionEntryKind,
|
|
166
196
|
hasDeprecatedTrailVersionGuidance,
|
|
167
197
|
intentValues,
|
|
@@ -200,6 +230,7 @@ export type {
|
|
|
200
230
|
TrailVersionArchivedStatus,
|
|
201
231
|
TrailVersionDeprecatedStatus,
|
|
202
232
|
TrailVersionForkEntry,
|
|
233
|
+
TrailVersionForkSpec,
|
|
203
234
|
TrailVersionRevisionEntry,
|
|
204
235
|
TrailVersions,
|
|
205
236
|
TrailVersionStatus,
|
|
@@ -278,10 +309,13 @@ export type {
|
|
|
278
309
|
export {
|
|
279
310
|
getWebhookHeader,
|
|
280
311
|
getWebhookHeaders,
|
|
312
|
+
matchWebhookPath,
|
|
313
|
+
parseWebhookPathParams,
|
|
281
314
|
validateWebhookSource,
|
|
282
315
|
verifyWebhookRequest,
|
|
283
316
|
webhook,
|
|
284
317
|
webhookMethods,
|
|
318
|
+
webhookPathPatternsOverlap,
|
|
285
319
|
} from './webhook.js';
|
|
286
320
|
export type {
|
|
287
321
|
WebhookMethod,
|
|
@@ -386,6 +420,31 @@ export type {
|
|
|
386
420
|
export { topo } from './topo.js';
|
|
387
421
|
export type { Topo, TopoIdentity } from './topo.js';
|
|
388
422
|
|
|
423
|
+
// Surface overlay
|
|
424
|
+
export {
|
|
425
|
+
SURFACES_OVERLAY_NAMESPACE,
|
|
426
|
+
classifySurfaceBinding,
|
|
427
|
+
deriveMcpTrailheadDescription,
|
|
428
|
+
expandCliSurfaceBindings,
|
|
429
|
+
expandMcpSurfaceBindings,
|
|
430
|
+
resolveSurfaceOverlayBindings,
|
|
431
|
+
surfaceBindingsFromLockOverlays,
|
|
432
|
+
surfaceOverlay,
|
|
433
|
+
surfaceOverlayBindingsSchema,
|
|
434
|
+
} from './surface-overlay.js';
|
|
435
|
+
export type {
|
|
436
|
+
CliSurfaceBindingAliases,
|
|
437
|
+
McpSurfaceBindingExpansion,
|
|
438
|
+
OverlayEnvelopeLike,
|
|
439
|
+
OverlayProvenance,
|
|
440
|
+
SurfaceBindingRef,
|
|
441
|
+
SurfaceBindingShape,
|
|
442
|
+
SurfaceBindingValue,
|
|
443
|
+
SurfaceBindings,
|
|
444
|
+
SurfaceOverlay,
|
|
445
|
+
SurfaceOverlayBindings,
|
|
446
|
+
} from './surface-overlay.js';
|
|
447
|
+
|
|
389
448
|
// Generic trails-db helpers (shared framework infrastructure per ADR-0014).
|
|
390
449
|
// The topo-store public API that previously lived here moved to
|
|
391
450
|
// `@ontrails/topographer` per ADR-0042.
|
|
@@ -417,13 +476,21 @@ export {
|
|
|
417
476
|
export type {
|
|
418
477
|
DraftDependency,
|
|
419
478
|
DraftDependencyKind,
|
|
479
|
+
DraftDiagnostic,
|
|
420
480
|
DraftFinding,
|
|
421
481
|
DraftReport,
|
|
422
482
|
} from './draft.js';
|
|
423
483
|
|
|
424
484
|
// Topo validation
|
|
425
|
-
export { validateTopo } from './validate-topo.js';
|
|
426
|
-
export type {
|
|
485
|
+
export { getTopoDiagnostics, validateTopo } from './validate-topo.js';
|
|
486
|
+
export type {
|
|
487
|
+
TopoDiagnostic,
|
|
488
|
+
TopoDiagnosticCode,
|
|
489
|
+
TopoIssue,
|
|
490
|
+
TopoMissingReference,
|
|
491
|
+
TopoReferenceKind,
|
|
492
|
+
TopoReferenceOwnerKind,
|
|
493
|
+
} from './validate-topo.js';
|
|
427
494
|
export { validateEstablishedTopo } from './validate-established-topo.js';
|
|
428
495
|
|
|
429
496
|
// Layer
|
|
@@ -448,6 +515,7 @@ export type {
|
|
|
448
515
|
export {
|
|
449
516
|
deriveCliPath,
|
|
450
517
|
deriveFields,
|
|
518
|
+
deriveMcpToolName,
|
|
451
519
|
deriveTrailCliCommandProjection,
|
|
452
520
|
normalizeCliCommandPath,
|
|
453
521
|
} from './derive.js';
|
|
@@ -481,6 +549,7 @@ export type { ExecuteTrailOptions } from './execute.js';
|
|
|
481
549
|
export {
|
|
482
550
|
clearTraceSink,
|
|
483
551
|
createActivationTraceRecord,
|
|
552
|
+
createSignalTraceRecord,
|
|
484
553
|
createTraceRecord,
|
|
485
554
|
getTraceContext,
|
|
486
555
|
getTraceSink,
|
|
@@ -489,6 +558,7 @@ export {
|
|
|
489
558
|
TRACE_CONTEXT_KEY,
|
|
490
559
|
traceContextFromRecord,
|
|
491
560
|
writeActivationTraceRecord,
|
|
561
|
+
writeSignalTraceRecord,
|
|
492
562
|
} from './tracing.js';
|
|
493
563
|
export type {
|
|
494
564
|
ActivationTraceRecordName,
|
|
@@ -581,10 +651,15 @@ export { securePath, isPathSafe, deriveSafePath } from './path-security.js';
|
|
|
581
651
|
|
|
582
652
|
// Workspace
|
|
583
653
|
export {
|
|
654
|
+
deriveRelativePath,
|
|
655
|
+
findWorkspacePackage,
|
|
584
656
|
findWorkspaceRoot,
|
|
585
657
|
isInsideWorkspace,
|
|
586
|
-
|
|
658
|
+
listWorkspacePackageDirs,
|
|
659
|
+
listWorkspacePackages,
|
|
660
|
+
listWorkspacePatterns,
|
|
587
661
|
} from './workspace.js';
|
|
662
|
+
export type { WorkspacePackage, WorkspaceRootManifest } from './workspace.js';
|
|
588
663
|
|
|
589
664
|
// Blob
|
|
590
665
|
export {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import { matchesAnyGlob, matchesGlob } from './glob.js';
|
|
4
|
+
|
|
5
|
+
declare const pathGlobBrand: unique symbol;
|
|
6
|
+
|
|
7
|
+
export type PathGlob = string & {
|
|
8
|
+
readonly [pathGlobBrand]: 'PathGlob';
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const normalizePathScopePath = (value: string): string =>
|
|
12
|
+
value.replaceAll('\\', '/').replace(/^\.\//, '');
|
|
13
|
+
|
|
14
|
+
export const matchesPathGlob = (path: string, pattern: string): boolean =>
|
|
15
|
+
matchesGlob(normalizePathScopePath(path), normalizePathScopePath(pattern), {
|
|
16
|
+
separator: '/',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const matchesAnyPathGlob = (
|
|
20
|
+
path: string,
|
|
21
|
+
patterns: readonly string[] | undefined
|
|
22
|
+
): boolean =>
|
|
23
|
+
matchesAnyGlob(
|
|
24
|
+
normalizePathScopePath(path),
|
|
25
|
+
patterns?.map(normalizePathScopePath),
|
|
26
|
+
{ separator: '/' }
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const pathGlobArraySchema = z.array(z.string()).readonly();
|
|
30
|
+
|
|
31
|
+
export const pathScopeSchema = z
|
|
32
|
+
.object({
|
|
33
|
+
exclude: pathGlobArraySchema.optional(),
|
|
34
|
+
extensions: z.array(z.string()).readonly().optional(),
|
|
35
|
+
include: pathGlobArraySchema.optional(),
|
|
36
|
+
})
|
|
37
|
+
.strict();
|
|
38
|
+
|
|
39
|
+
export type PathScope = z.output<typeof pathScopeSchema>;
|
|
40
|
+
|
|
41
|
+
export type ScanTargets = Pick<PathScope, 'exclude' | 'extensions'>;
|
|
42
|
+
|
|
43
|
+
const extensionOf = (path: string): string => {
|
|
44
|
+
const normalized = normalizePathScopePath(path);
|
|
45
|
+
const name = normalized.slice(normalized.lastIndexOf('/') + 1);
|
|
46
|
+
const dot = name.lastIndexOf('.');
|
|
47
|
+
return dot <= 0 ? '' : name.slice(dot);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const normalizeExtension = (extension: string): string =>
|
|
51
|
+
extension === '' || extension.startsWith('.') ? extension : `.${extension}`;
|
|
52
|
+
|
|
53
|
+
const includedByExtension = (
|
|
54
|
+
path: string,
|
|
55
|
+
extensions: readonly string[] | undefined
|
|
56
|
+
): boolean =>
|
|
57
|
+
extensions === undefined ||
|
|
58
|
+
extensions.length === 0 ||
|
|
59
|
+
extensions.map(normalizeExtension).includes(extensionOf(path));
|
|
60
|
+
|
|
61
|
+
export const includedByPathScope = (path: string, scope?: PathScope): boolean =>
|
|
62
|
+
(scope?.include === undefined ||
|
|
63
|
+
scope.include.length === 0 ||
|
|
64
|
+
matchesAnyPathGlob(path, scope.include)) &&
|
|
65
|
+
!matchesAnyPathGlob(path, scope?.exclude) &&
|
|
66
|
+
includedByExtension(path, scope?.extensions);
|
package/src/path-security.ts
CHANGED
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
* All functions are runtime-agnostic (Node / Bun compatible).
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { resolve, relative, normalize, isAbsolute } from 'node:path';
|
|
8
|
-
|
|
9
7
|
import { PermissionError } from './errors.js';
|
|
10
8
|
import { Result } from './result.js';
|
|
9
|
+
// Path security guards filesystem access on tooling paths: node:path
|
|
10
|
+
// loads lazily at first use so the core barrel's module graph stays
|
|
11
|
+
// execution-portable on runtimes without node: builtins (TRL-1198).
|
|
12
|
+
import { loadRuntimeBuiltin } from './runtime-builtins.js';
|
|
13
|
+
|
|
14
|
+
const nodePath = () => loadRuntimeBuiltin('node:path');
|
|
11
15
|
|
|
12
16
|
// ---------------------------------------------------------------------------
|
|
13
17
|
// Internal
|
|
@@ -15,6 +19,7 @@ import { Result } from './result.js';
|
|
|
15
19
|
|
|
16
20
|
/** Returns true when `target` is equal to or a descendant of `base`. */
|
|
17
21
|
const isWithin = (base: string, target: string): boolean => {
|
|
22
|
+
const { isAbsolute, relative } = nodePath();
|
|
18
23
|
const rel = relative(base, target);
|
|
19
24
|
// Empty string means they are the same directory.
|
|
20
25
|
// A relative path starting with ".." means it escapes.
|
|
@@ -39,6 +44,7 @@ export const securePath = (
|
|
|
39
44
|
basePath: string,
|
|
40
45
|
userPath: string
|
|
41
46
|
): Result<string, PermissionError> => {
|
|
47
|
+
const { resolve } = nodePath();
|
|
42
48
|
const base = resolve(basePath);
|
|
43
49
|
const resolved = resolve(base, userPath);
|
|
44
50
|
|
|
@@ -61,6 +67,7 @@ export const securePath = (
|
|
|
61
67
|
* `basePath`.
|
|
62
68
|
*/
|
|
63
69
|
export const isPathSafe = (basePath: string, userPath: string): boolean => {
|
|
70
|
+
const { resolve } = nodePath();
|
|
64
71
|
const base = resolve(basePath);
|
|
65
72
|
const resolved = resolve(base, userPath);
|
|
66
73
|
return isWithin(base, resolved);
|
|
@@ -74,6 +81,7 @@ export const deriveSafePath = (
|
|
|
74
81
|
basePath: string,
|
|
75
82
|
...segments: string[]
|
|
76
83
|
): Result<string, PermissionError> => {
|
|
84
|
+
const { normalize, resolve } = nodePath();
|
|
77
85
|
const base = resolve(basePath);
|
|
78
86
|
const joined = resolve(base, ...segments.map((s) => normalize(s)));
|
|
79
87
|
|