@ontrails/core 1.0.0-beta.32 → 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 +45 -0
- package/package.json +1 -1
- package/src/context.ts +18 -4
- package/src/derive.ts +21 -0
- package/src/index.ts +48 -3
- 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-overlay.ts +694 -0
- package/src/trail.ts +78 -0
- package/src/trails-db.ts +28 -22
- package/src/validate-topo.ts +181 -40
- package/src/webhook.ts +194 -11
- package/src/workspace.ts +39 -33
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
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
|
+
|
|
3
48
|
## 1.0.0-beta.32
|
|
4
49
|
|
|
5
50
|
### Patch Changes
|
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
|
@@ -382,6 +382,27 @@ const normalizeCliAlias = ({
|
|
|
382
382
|
};
|
|
383
383
|
};
|
|
384
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
|
+
|
|
385
406
|
/** Derive resolved CLI command routes for one trail. */
|
|
386
407
|
export const deriveTrailCliCommandProjection = (
|
|
387
408
|
trail: TrailCliProjectionInput,
|
package/src/index.ts
CHANGED
|
@@ -143,8 +143,13 @@ export {
|
|
|
143
143
|
isResource,
|
|
144
144
|
resource,
|
|
145
145
|
} from './resource.js';
|
|
146
|
-
export {
|
|
146
|
+
export {
|
|
147
|
+
createResources,
|
|
148
|
+
drainResources,
|
|
149
|
+
resolveResourceConfig,
|
|
150
|
+
} from './resource-config.js';
|
|
147
151
|
export type {
|
|
152
|
+
ResolvedResourceScope,
|
|
148
153
|
ResourceConfigValues,
|
|
149
154
|
ResourceDrainReport,
|
|
150
155
|
} from './resource-config.js';
|
|
@@ -186,6 +191,7 @@ export {
|
|
|
186
191
|
export type { ActivationSourceProjection } from './activation-source-projection.js';
|
|
187
192
|
export {
|
|
188
193
|
deriveSupportedTrailVersions,
|
|
194
|
+
forkVersion,
|
|
189
195
|
getTrailVersionEntryKind,
|
|
190
196
|
hasDeprecatedTrailVersionGuidance,
|
|
191
197
|
intentValues,
|
|
@@ -224,6 +230,7 @@ export type {
|
|
|
224
230
|
TrailVersionArchivedStatus,
|
|
225
231
|
TrailVersionDeprecatedStatus,
|
|
226
232
|
TrailVersionForkEntry,
|
|
233
|
+
TrailVersionForkSpec,
|
|
227
234
|
TrailVersionRevisionEntry,
|
|
228
235
|
TrailVersions,
|
|
229
236
|
TrailVersionStatus,
|
|
@@ -302,10 +309,13 @@ export type {
|
|
|
302
309
|
export {
|
|
303
310
|
getWebhookHeader,
|
|
304
311
|
getWebhookHeaders,
|
|
312
|
+
matchWebhookPath,
|
|
313
|
+
parseWebhookPathParams,
|
|
305
314
|
validateWebhookSource,
|
|
306
315
|
verifyWebhookRequest,
|
|
307
316
|
webhook,
|
|
308
317
|
webhookMethods,
|
|
318
|
+
webhookPathPatternsOverlap,
|
|
309
319
|
} from './webhook.js';
|
|
310
320
|
export type {
|
|
311
321
|
WebhookMethod,
|
|
@@ -410,6 +420,31 @@ export type {
|
|
|
410
420
|
export { topo } from './topo.js';
|
|
411
421
|
export type { Topo, TopoIdentity } from './topo.js';
|
|
412
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
|
+
|
|
413
448
|
// Generic trails-db helpers (shared framework infrastructure per ADR-0014).
|
|
414
449
|
// The topo-store public API that previously lived here moved to
|
|
415
450
|
// `@ontrails/topographer` per ADR-0042.
|
|
@@ -447,8 +482,15 @@ export type {
|
|
|
447
482
|
} from './draft.js';
|
|
448
483
|
|
|
449
484
|
// Topo validation
|
|
450
|
-
export { validateTopo } from './validate-topo.js';
|
|
451
|
-
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';
|
|
452
494
|
export { validateEstablishedTopo } from './validate-established-topo.js';
|
|
453
495
|
|
|
454
496
|
// Layer
|
|
@@ -473,6 +515,7 @@ export type {
|
|
|
473
515
|
export {
|
|
474
516
|
deriveCliPath,
|
|
475
517
|
deriveFields,
|
|
518
|
+
deriveMcpToolName,
|
|
476
519
|
deriveTrailCliCommandProjection,
|
|
477
520
|
normalizeCliCommandPath,
|
|
478
521
|
} from './derive.js';
|
|
@@ -506,6 +549,7 @@ export type { ExecuteTrailOptions } from './execute.js';
|
|
|
506
549
|
export {
|
|
507
550
|
clearTraceSink,
|
|
508
551
|
createActivationTraceRecord,
|
|
552
|
+
createSignalTraceRecord,
|
|
509
553
|
createTraceRecord,
|
|
510
554
|
getTraceContext,
|
|
511
555
|
getTraceSink,
|
|
@@ -514,6 +558,7 @@ export {
|
|
|
514
558
|
TRACE_CONTEXT_KEY,
|
|
515
559
|
traceContextFromRecord,
|
|
516
560
|
writeActivationTraceRecord,
|
|
561
|
+
writeSignalTraceRecord,
|
|
517
562
|
} from './tracing.js';
|
|
518
563
|
export type {
|
|
519
564
|
ActivationTraceRecordName,
|
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
|
|
package/src/resource-config.ts
CHANGED
|
@@ -585,7 +585,7 @@ const withResolvedResources = (
|
|
|
585
585
|
/**
|
|
586
586
|
* Resolved trail context plus lease release for resources used by the run.
|
|
587
587
|
*/
|
|
588
|
-
interface ResolvedResourceScope {
|
|
588
|
+
export interface ResolvedResourceScope {
|
|
589
589
|
readonly ctx: TrailContext;
|
|
590
590
|
release(): void;
|
|
591
591
|
}
|
|
@@ -593,14 +593,26 @@ interface ResolvedResourceScope {
|
|
|
593
593
|
const releaseNoResources = (): undefined => undefined;
|
|
594
594
|
|
|
595
595
|
/**
|
|
596
|
-
* Resolve all declared resources for a
|
|
596
|
+
* Resolve all declared resources for a resource-bearing declaration.
|
|
597
597
|
*
|
|
598
598
|
* Validates per-resource config, checks overrides and caches, and creates
|
|
599
599
|
* new instances as needed. Returns an enriched context with all resource
|
|
600
|
-
* instances injected into extensions.
|
|
600
|
+
* instances injected into extensions. Callers own the returned `release`.
|
|
601
|
+
*
|
|
602
|
+
* @example
|
|
603
|
+
* ```ts
|
|
604
|
+
* const scope = await createResources(
|
|
605
|
+
* { resources: [secretsStore] },
|
|
606
|
+
* createTrailContext()
|
|
607
|
+
* );
|
|
608
|
+
* if (scope.isOk()) {
|
|
609
|
+
* const secrets = secretsStore.from(scope.value.ctx);
|
|
610
|
+
* scope.value.release();
|
|
611
|
+
* }
|
|
612
|
+
* ```
|
|
601
613
|
*/
|
|
602
614
|
export const createResources = async (
|
|
603
|
-
trail: AnyTrail,
|
|
615
|
+
trail: Pick<AnyTrail, 'resources'>,
|
|
604
616
|
ctx: TrailContext,
|
|
605
617
|
overrides?: ResourceOverrideMap,
|
|
606
618
|
configValues?: ConfigValues
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy runtime-builtin loading for the execution-portable core barrel.
|
|
3
|
+
*
|
|
4
|
+
* Core tooling modules that need Bun or Node capabilities (`bun:sqlite`,
|
|
5
|
+
* `node:fs`, `node:os`, `node:path`) must not import them eagerly: the
|
|
6
|
+
* core barrel sits on the execution path of every surface, and edge
|
|
7
|
+
* runtimes such as workerd refuse module graphs that import `bun:`
|
|
8
|
+
* builtins and gate `node:` builtins behind compatibility flags
|
|
9
|
+
* (TRL-1198). Loading through `process.getBuiltinModule` keeps the
|
|
10
|
+
* specifier out of bundler module graphs entirely and defers the
|
|
11
|
+
* capability requirement to first use, so runtimes that never call a
|
|
12
|
+
* tooling helper never pay for it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type * as BunSqlite from 'bun:sqlite';
|
|
16
|
+
import type * as NodeFs from 'node:fs';
|
|
17
|
+
import type * as NodeOs from 'node:os';
|
|
18
|
+
import type * as NodePath from 'node:path';
|
|
19
|
+
|
|
20
|
+
import { InternalError } from './errors.js';
|
|
21
|
+
|
|
22
|
+
interface BuiltinModules {
|
|
23
|
+
readonly 'bun:sqlite': typeof BunSqlite;
|
|
24
|
+
readonly 'node:fs': typeof NodeFs;
|
|
25
|
+
readonly 'node:os': typeof NodeOs;
|
|
26
|
+
readonly 'node:path': typeof NodePath;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const loadedBuiltins = new Map<keyof BuiltinModules, unknown>();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Load a Bun/Node builtin module at first use.
|
|
33
|
+
*
|
|
34
|
+
* @throws {InternalError} When the runtime does not expose the builtin
|
|
35
|
+
* (for example workerd without `nodejs_compat`). Trail execution never
|
|
36
|
+
* reaches this loader; only tooling helpers (trails-db, workspace
|
|
37
|
+
* discovery) do.
|
|
38
|
+
*/
|
|
39
|
+
export const loadRuntimeBuiltin = <TName extends keyof BuiltinModules>(
|
|
40
|
+
name: TName
|
|
41
|
+
): BuiltinModules[TName] => {
|
|
42
|
+
const cached = loadedBuiltins.get(name);
|
|
43
|
+
if (cached !== undefined) {
|
|
44
|
+
return cached as BuiltinModules[TName];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const proc = (
|
|
48
|
+
globalThis as {
|
|
49
|
+
readonly process?: {
|
|
50
|
+
readonly getBuiltinModule?: (id: string) => unknown;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
).process;
|
|
54
|
+
if (typeof proc?.getBuiltinModule !== 'function') {
|
|
55
|
+
throw new InternalError(
|
|
56
|
+
`Runtime builtin "${name}" is unavailable: this runtime does not expose process.getBuiltinModule. Trails tooling helpers need a Bun or Node runtime; the trail execution path never loads them.`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const loaded = proc.getBuiltinModule(name);
|
|
61
|
+
if (loaded === undefined || loaded === null) {
|
|
62
|
+
throw new InternalError(
|
|
63
|
+
`Runtime builtin "${name}" is unavailable on this runtime.`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
loadedBuiltins.set(name, loaded);
|
|
68
|
+
return loaded as BuiltinModules[TName];
|
|
69
|
+
};
|
package/src/sha256.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure synchronous SHA-256 for runtime-portable fingerprints.
|
|
3
|
+
*
|
|
4
|
+
* `node:crypto`'s `createHash` is unavailable on edge runtimes without
|
|
5
|
+
* compatibility flags, and Web Crypto's `subtle.digest` is asynchronous,
|
|
6
|
+
* which does not fit the synchronous summary paths that need a digest
|
|
7
|
+
* (signal payload summaries, per-project store keys). This implementation
|
|
8
|
+
* follows FIPS 180-4 and produces output identical to
|
|
9
|
+
* `createHash('sha256').update(text).digest('hex')`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/* oxlint-disable no-bitwise, unicorn/prefer-math-trunc, unicorn/number-literal-case -- SHA-256 is defined over 32-bit words: rotations, xor, and `>>> 0` unsigned wrapping are the specified operations, and `Math.trunc()` is not equivalent to `>>> 0` (it neither wraps to 32 bits nor coerces to unsigned). number-literal-case is disabled because oxfmt normalizes hex digits to lowercase, which the rule rejects — the formatter wins. */
|
|
13
|
+
|
|
14
|
+
// FIPS 180-4 §4.2.2: first 32 bits of the fractional parts of the cube
|
|
15
|
+
// roots of the first 64 primes.
|
|
16
|
+
const K = Uint32Array.from([
|
|
17
|
+
0x42_8a_2f_98, 0x71_37_44_91, 0xb5_c0_fb_cf, 0xe9_b5_db_a5, 0x39_56_c2_5b,
|
|
18
|
+
0x59_f1_11_f1, 0x92_3f_82_a4, 0xab_1c_5e_d5, 0xd8_07_aa_98, 0x12_83_5b_01,
|
|
19
|
+
0x24_31_85_be, 0x55_0c_7d_c3, 0x72_be_5d_74, 0x80_de_b1_fe, 0x9b_dc_06_a7,
|
|
20
|
+
0xc1_9b_f1_74, 0xe4_9b_69_c1, 0xef_be_47_86, 0x0f_c1_9d_c6, 0x24_0c_a1_cc,
|
|
21
|
+
0x2d_e9_2c_6f, 0x4a_74_84_aa, 0x5c_b0_a9_dc, 0x76_f9_88_da, 0x98_3e_51_52,
|
|
22
|
+
0xa8_31_c6_6d, 0xb0_03_27_c8, 0xbf_59_7f_c7, 0xc6_e0_0b_f3, 0xd5_a7_91_47,
|
|
23
|
+
0x06_ca_63_51, 0x14_29_29_67, 0x27_b7_0a_85, 0x2e_1b_21_38, 0x4d_2c_6d_fc,
|
|
24
|
+
0x53_38_0d_13, 0x65_0a_73_54, 0x76_6a_0a_bb, 0x81_c2_c9_2e, 0x92_72_2c_85,
|
|
25
|
+
0xa2_bf_e8_a1, 0xa8_1a_66_4b, 0xc2_4b_8b_70, 0xc7_6c_51_a3, 0xd1_92_e8_19,
|
|
26
|
+
0xd6_99_06_24, 0xf4_0e_35_85, 0x10_6a_a0_70, 0x19_a4_c1_16, 0x1e_37_6c_08,
|
|
27
|
+
0x27_48_77_4c, 0x34_b0_bc_b5, 0x39_1c_0c_b3, 0x4e_d8_aa_4a, 0x5b_9c_ca_4f,
|
|
28
|
+
0x68_2e_6f_f3, 0x74_8f_82_ee, 0x78_a5_63_6f, 0x84_c8_78_14, 0x8c_c7_02_08,
|
|
29
|
+
0x90_be_ff_fa, 0xa4_50_6c_eb, 0xbe_f9_a3_f7, 0xc6_71_78_f2,
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const rotr = (value: number, bits: number): number =>
|
|
33
|
+
(value >>> bits) | (value << (32 - bits));
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Total array accessor: every read in the compression loop is provably in
|
|
37
|
+
* range, so the fallback never fires — it only satisfies
|
|
38
|
+
* `noUncheckedIndexedAccess` without an assertion.
|
|
39
|
+
*/
|
|
40
|
+
const wordAt = (words: Uint32Array, index: number): number => words[index] ?? 0;
|
|
41
|
+
|
|
42
|
+
const padMessage = (bytes: Uint8Array): Uint8Array => {
|
|
43
|
+
const bitLength = bytes.length * 8;
|
|
44
|
+
// Message + 0x80 marker, padded to 56 mod 64, then a 64-bit big-endian
|
|
45
|
+
// bit length. Message sizes here are far below 2^32 bits, so the high
|
|
46
|
+
// word of the length is derived from the float division.
|
|
47
|
+
const paddedLength = (Math.floor((bytes.length + 8) / 64) + 1) * 64;
|
|
48
|
+
const padded = new Uint8Array(paddedLength);
|
|
49
|
+
padded.set(bytes);
|
|
50
|
+
padded[bytes.length] = 0x80;
|
|
51
|
+
const view = new DataView(padded.buffer);
|
|
52
|
+
view.setUint32(paddedLength - 8, Math.floor(bitLength / 0x1_00_00_00_00));
|
|
53
|
+
view.setUint32(paddedLength - 4, bitLength >>> 0);
|
|
54
|
+
return padded;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const fillSchedule = (w: Uint32Array, view: DataView, offset: number): void => {
|
|
58
|
+
for (let i = 0; i < 16; i += 1) {
|
|
59
|
+
w[i] = view.getUint32(offset + i * 4);
|
|
60
|
+
}
|
|
61
|
+
for (let i = 16; i < 64; i += 1) {
|
|
62
|
+
const w15 = wordAt(w, i - 15);
|
|
63
|
+
const w2 = wordAt(w, i - 2);
|
|
64
|
+
const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ (w15 >>> 3);
|
|
65
|
+
const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ (w2 >>> 10);
|
|
66
|
+
w[i] = (wordAt(w, i - 16) + s0 + wordAt(w, i - 7) + s1) >>> 0;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// oxlint-disable-next-line max-statements -- the FIPS 180-4 compression loop reads more clearly as one block than split across helpers
|
|
71
|
+
const digestHex = (padded: Uint8Array): string => {
|
|
72
|
+
// FIPS 180-4 §5.3.3 initial hash value.
|
|
73
|
+
let h0 = 0x6a_09_e6_67;
|
|
74
|
+
let h1 = 0xbb_67_ae_85;
|
|
75
|
+
let h2 = 0x3c_6e_f3_72;
|
|
76
|
+
let h3 = 0xa5_4f_f5_3a;
|
|
77
|
+
let h4 = 0x51_0e_52_7f;
|
|
78
|
+
let h5 = 0x9b_05_68_8c;
|
|
79
|
+
let h6 = 0x1f_83_d9_ab;
|
|
80
|
+
let h7 = 0x5b_e0_cd_19;
|
|
81
|
+
|
|
82
|
+
const view = new DataView(padded.buffer);
|
|
83
|
+
const w = new Uint32Array(64);
|
|
84
|
+
|
|
85
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
86
|
+
fillSchedule(w, view, offset);
|
|
87
|
+
|
|
88
|
+
let a = h0;
|
|
89
|
+
let b = h1;
|
|
90
|
+
let c = h2;
|
|
91
|
+
let d = h3;
|
|
92
|
+
let e = h4;
|
|
93
|
+
let f = h5;
|
|
94
|
+
let g = h6;
|
|
95
|
+
let h = h7;
|
|
96
|
+
|
|
97
|
+
for (let i = 0; i < 64; i += 1) {
|
|
98
|
+
const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
99
|
+
const ch = (e & f) ^ (~e & g);
|
|
100
|
+
const temp1 = (h + s1 + ch + wordAt(K, i) + wordAt(w, i)) >>> 0;
|
|
101
|
+
const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
102
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
103
|
+
const temp2 = (s0 + maj) >>> 0;
|
|
104
|
+
|
|
105
|
+
h = g;
|
|
106
|
+
g = f;
|
|
107
|
+
f = e;
|
|
108
|
+
e = (d + temp1) >>> 0;
|
|
109
|
+
d = c;
|
|
110
|
+
c = b;
|
|
111
|
+
b = a;
|
|
112
|
+
a = (temp1 + temp2) >>> 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
h0 = (h0 + a) >>> 0;
|
|
116
|
+
h1 = (h1 + b) >>> 0;
|
|
117
|
+
h2 = (h2 + c) >>> 0;
|
|
118
|
+
h3 = (h3 + d) >>> 0;
|
|
119
|
+
h4 = (h4 + e) >>> 0;
|
|
120
|
+
h5 = (h5 + f) >>> 0;
|
|
121
|
+
h6 = (h6 + g) >>> 0;
|
|
122
|
+
h7 = (h7 + h) >>> 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return [h0, h1, h2, h3, h4, h5, h6, h7]
|
|
126
|
+
.map((word) => word.toString(16).padStart(8, '0'))
|
|
127
|
+
.join('');
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* SHA-256 of the UTF-8 encoding of `text`, as lowercase hex.
|
|
132
|
+
*
|
|
133
|
+
* Output-identical to `createHash('sha256').update(text).digest('hex')`.
|
|
134
|
+
*/
|
|
135
|
+
export const sha256Hex = (text: string): string =>
|
|
136
|
+
digestHex(padMessage(new TextEncoder().encode(text)));
|
|
@@ -5,11 +5,10 @@
|
|
|
5
5
|
* observable without changing the best-effort producer API.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { createHash } from 'node:crypto';
|
|
9
|
-
|
|
10
8
|
import type { z } from 'zod';
|
|
11
9
|
|
|
12
10
|
import type { ActivationProvenance } from './activation-provenance.js';
|
|
11
|
+
import { sha256Hex } from './sha256.js';
|
|
13
12
|
import type { Logger } from './types.js';
|
|
14
13
|
|
|
15
14
|
export const SIGNAL_DIAGNOSTICS_SINK_KEY =
|
|
@@ -349,8 +348,9 @@ const stableJson = (value: unknown): string => {
|
|
|
349
348
|
}
|
|
350
349
|
};
|
|
351
350
|
|
|
352
|
-
|
|
353
|
-
|
|
351
|
+
// Pure sha256 keeps payload summarization on the execution path portable:
|
|
352
|
+
// node:crypto is unavailable on edge runtimes without compat flags.
|
|
353
|
+
const hashText = (text: string): string => sha256Hex(text);
|
|
354
354
|
|
|
355
355
|
export const summarizeSignalPayload = (
|
|
356
356
|
payload: unknown
|