@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.
@@ -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 trail.
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
- const hashText = (text: string): string =>
353
- createHash('sha256').update(text).digest('hex');
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
@@ -1,4 +1,5 @@
1
1
  import type { Intent, Trail } from './trail.js';
2
+ import { matchesAnyTrailIdGlob, matchesTrailIdGlob } from './trail-id-glob.js';
2
3
  import type { SurfaceSelectionOptions } from './surface-derivation.js';
3
4
 
4
5
  // ---------------------------------------------------------------------------
@@ -7,84 +8,10 @@ import type { SurfaceSelectionOptions } from './surface-derivation.js';
7
8
 
8
9
  export type SurfaceFilterOptions = SurfaceSelectionOptions;
9
10
 
10
- // ---------------------------------------------------------------------------
11
- // Glob matching
12
- // ---------------------------------------------------------------------------
13
-
14
- const trailIdSegments = (trailId: string): readonly string[] =>
15
- trailId.split('.');
16
-
17
- const patternSegments = (pattern: string): readonly string[] =>
18
- pattern.split('.');
19
-
20
- type SegmentMatcher = (
21
- trail: readonly string[],
22
- pattern: readonly string[],
23
- trailIndex: number,
24
- patternIndex: number
25
- ) => boolean;
26
-
27
- const matchesDoubleStar = (
28
- trail: readonly string[],
29
- pattern: readonly string[],
30
- trailIndex: number,
31
- patternIndex: number,
32
- matchSegments: SegmentMatcher
33
- ): boolean =>
34
- patternIndex === pattern.length - 1 ||
35
- Array.from(
36
- { length: trail.length - trailIndex + 1 },
37
- (_, offset) => trailIndex + offset
38
- ).some((index) => matchSegments(trail, pattern, index, patternIndex + 1));
39
-
40
- const matchesSingleSegment = (
41
- trail: readonly string[],
42
- pattern: readonly string[],
43
- trailIndex: number,
44
- patternIndex: number,
45
- current: string,
46
- matchSegments: SegmentMatcher
47
- ): boolean =>
48
- trailIndex < trail.length &&
49
- (current === '*' || current === trail[trailIndex]) &&
50
- matchSegments(trail, pattern, trailIndex + 1, patternIndex + 1);
51
-
52
- const matchesFrom = function matchesFrom(
53
- trail: readonly string[],
54
- pattern: readonly string[],
55
- trailIndex: number,
56
- patternIndex: number
57
- ): boolean {
58
- if (patternIndex >= pattern.length) {
59
- return trailIndex >= trail.length;
60
- }
61
-
62
- const current = pattern[patternIndex];
63
- if (current === undefined) {
64
- return false;
65
- }
66
-
67
- return current === '**'
68
- ? matchesDoubleStar(trail, pattern, trailIndex, patternIndex, matchesFrom)
69
- : matchesSingleSegment(
70
- trail,
71
- pattern,
72
- trailIndex,
73
- patternIndex,
74
- current,
75
- matchesFrom
76
- );
77
- };
78
-
79
11
  export const matchesTrailPattern = (
80
12
  trailId: string,
81
13
  pattern: string
82
- ): boolean => {
83
- if (pattern === trailId) {
84
- return true;
85
- }
86
- return matchesFrom(trailIdSegments(trailId), patternSegments(pattern), 0, 0);
87
- };
14
+ ): boolean => matchesTrailIdGlob(trailId, pattern);
88
15
 
89
16
  // ---------------------------------------------------------------------------
90
17
  // Surface filtering
@@ -93,9 +20,7 @@ export const matchesTrailPattern = (
93
20
  const matchesAnyPattern = (
94
21
  trailId: string,
95
22
  patterns: readonly string[] | undefined
96
- ): boolean =>
97
- patterns !== undefined &&
98
- patterns.some((pattern) => matchesTrailPattern(trailId, pattern));
23
+ ): boolean => matchesAnyTrailIdGlob(trailId, patterns);
99
24
 
100
25
  const isExplicitInternalInclude = (
101
26
  trailId: string,