@ontrails/core 1.0.0-beta.2 → 1.0.0-beta.21

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.
Files changed (245) hide show
  1. package/CHANGELOG.md +283 -5
  2. package/README.md +113 -33
  3. package/package.json +11 -1
  4. package/src/activation-provenance.ts +111 -0
  5. package/src/activation-source-compatibility.ts +430 -0
  6. package/src/activation-source-projection.ts +221 -0
  7. package/src/activation-source.ts +91 -0
  8. package/src/blob-ref.ts +51 -0
  9. package/src/branded.ts +1 -1
  10. package/src/compose-batch.ts +69 -0
  11. package/src/compose-schema.ts +36 -0
  12. package/src/context.ts +44 -10
  13. package/src/contour.ts +346 -0
  14. package/src/derive.ts +59 -18
  15. package/src/detours.ts +8 -0
  16. package/src/draft.ts +342 -0
  17. package/src/error-projection.ts +51 -0
  18. package/src/errors.ts +309 -48
  19. package/src/execute.ts +1577 -0
  20. package/src/fire.ts +1169 -0
  21. package/src/index.ts +477 -32
  22. package/src/internal/fork-ctx.ts +69 -0
  23. package/src/layer-projection.ts +193 -0
  24. package/src/layer.ts +43 -6
  25. package/src/observe.ts +361 -0
  26. package/src/path-security.ts +1 -1
  27. package/src/permits.ts +12 -0
  28. package/src/redaction/patterns.ts +6 -3
  29. package/src/resilience.ts +11 -11
  30. package/src/resource-config.ts +792 -0
  31. package/src/resource.ts +194 -0
  32. package/src/result.ts +36 -4
  33. package/src/run.ts +76 -0
  34. package/src/schedule-runtime.ts +689 -0
  35. package/src/schedule.ts +326 -0
  36. package/src/serialization.ts +119 -39
  37. package/src/signal-diagnostics.ts +633 -0
  38. package/src/signal-ref.ts +87 -0
  39. package/src/signal.ts +104 -0
  40. package/src/store/accessor-protocol.ts +56 -0
  41. package/src/store/index.ts +4 -0
  42. package/src/structured-examples.ts +248 -0
  43. package/src/surface-derivation.ts +91 -0
  44. package/src/surface-filter.ts +176 -0
  45. package/src/surface-versioning.ts +42 -0
  46. package/src/topo.ts +769 -57
  47. package/src/tracing.ts +345 -0
  48. package/src/trail.ts +1161 -29
  49. package/src/trails/derive-trail.ts +842 -0
  50. package/src/trails/index.ts +9 -0
  51. package/src/trails/ingest.ts +139 -0
  52. package/src/trails-db.ts +208 -0
  53. package/src/transport-error-map.ts +174 -0
  54. package/src/type-utils.ts +87 -0
  55. package/src/types.ts +254 -12
  56. package/src/validate-established-topo.ts +69 -0
  57. package/src/validate-topo.ts +442 -35
  58. package/src/validation.ts +152 -4
  59. package/src/version-marker.ts +716 -0
  60. package/src/version-resolution.ts +308 -0
  61. package/src/version-runtime.ts +120 -0
  62. package/src/webhook.ts +278 -0
  63. package/src/workspace.ts +1 -1
  64. package/src/zod-wrappers.ts +72 -0
  65. package/.turbo/turbo-build.log +0 -1
  66. package/.turbo/turbo-lint.log +0 -3
  67. package/.turbo/turbo-typecheck.log +0 -1
  68. package/dist/adapters.d.ts +0 -39
  69. package/dist/adapters.d.ts.map +0 -1
  70. package/dist/adapters.js +0 -2
  71. package/dist/adapters.js.map +0 -1
  72. package/dist/blob-ref.d.ts +0 -20
  73. package/dist/blob-ref.d.ts.map +0 -1
  74. package/dist/blob-ref.js +0 -22
  75. package/dist/blob-ref.js.map +0 -1
  76. package/dist/branded.d.ts +0 -36
  77. package/dist/branded.d.ts.map +0 -1
  78. package/dist/branded.js +0 -89
  79. package/dist/branded.js.map +0 -1
  80. package/dist/collections.d.ts +0 -31
  81. package/dist/collections.d.ts.map +0 -1
  82. package/dist/collections.js +0 -60
  83. package/dist/collections.js.map +0 -1
  84. package/dist/context.d.ts +0 -10
  85. package/dist/context.d.ts.map +0 -1
  86. package/dist/context.js +0 -15
  87. package/dist/context.js.map +0 -1
  88. package/dist/derive.d.ts +0 -33
  89. package/dist/derive.d.ts.map +0 -1
  90. package/dist/derive.js +0 -122
  91. package/dist/derive.js.map +0 -1
  92. package/dist/errors.d.ts +0 -83
  93. package/dist/errors.d.ts.map +0 -1
  94. package/dist/errors.js +0 -142
  95. package/dist/errors.js.map +0 -1
  96. package/dist/event.d.ts +0 -45
  97. package/dist/event.d.ts.map +0 -1
  98. package/dist/event.js +0 -17
  99. package/dist/event.js.map +0 -1
  100. package/dist/fetch.d.ts +0 -15
  101. package/dist/fetch.d.ts.map +0 -1
  102. package/dist/fetch.js +0 -102
  103. package/dist/fetch.js.map +0 -1
  104. package/dist/guards.d.ts +0 -17
  105. package/dist/guards.d.ts.map +0 -1
  106. package/dist/guards.js +0 -25
  107. package/dist/guards.js.map +0 -1
  108. package/dist/health.d.ts +0 -18
  109. package/dist/health.d.ts.map +0 -1
  110. package/dist/health.js +0 -5
  111. package/dist/health.js.map +0 -1
  112. package/dist/hike.d.ts +0 -36
  113. package/dist/hike.d.ts.map +0 -1
  114. package/dist/hike.js +0 -20
  115. package/dist/hike.js.map +0 -1
  116. package/dist/index.d.ts +0 -34
  117. package/dist/index.d.ts.map +0 -1
  118. package/dist/index.js +0 -38
  119. package/dist/index.js.map +0 -1
  120. package/dist/job.d.ts +0 -24
  121. package/dist/job.d.ts.map +0 -1
  122. package/dist/job.js +0 -17
  123. package/dist/job.js.map +0 -1
  124. package/dist/layer.d.ts +0 -17
  125. package/dist/layer.d.ts.map +0 -1
  126. package/dist/layer.js +0 -21
  127. package/dist/layer.js.map +0 -1
  128. package/dist/path-security.d.ts +0 -28
  129. package/dist/path-security.d.ts.map +0 -1
  130. package/dist/path-security.js +0 -63
  131. package/dist/path-security.js.map +0 -1
  132. package/dist/patterns/bulk.d.ts +0 -15
  133. package/dist/patterns/bulk.d.ts.map +0 -1
  134. package/dist/patterns/bulk.js +0 -14
  135. package/dist/patterns/bulk.js.map +0 -1
  136. package/dist/patterns/change.d.ts +0 -10
  137. package/dist/patterns/change.d.ts.map +0 -1
  138. package/dist/patterns/change.js +0 -10
  139. package/dist/patterns/change.js.map +0 -1
  140. package/dist/patterns/date-range.d.ts +0 -10
  141. package/dist/patterns/date-range.d.ts.map +0 -1
  142. package/dist/patterns/date-range.js +0 -10
  143. package/dist/patterns/date-range.js.map +0 -1
  144. package/dist/patterns/index.d.ts +0 -9
  145. package/dist/patterns/index.d.ts.map +0 -1
  146. package/dist/patterns/index.js +0 -9
  147. package/dist/patterns/index.js.map +0 -1
  148. package/dist/patterns/pagination.d.ts +0 -18
  149. package/dist/patterns/pagination.d.ts.map +0 -1
  150. package/dist/patterns/pagination.js +0 -18
  151. package/dist/patterns/pagination.js.map +0 -1
  152. package/dist/patterns/progress.d.ts +0 -11
  153. package/dist/patterns/progress.d.ts.map +0 -1
  154. package/dist/patterns/progress.js +0 -11
  155. package/dist/patterns/progress.js.map +0 -1
  156. package/dist/patterns/sorting.d.ts +0 -13
  157. package/dist/patterns/sorting.d.ts.map +0 -1
  158. package/dist/patterns/sorting.js +0 -10
  159. package/dist/patterns/sorting.js.map +0 -1
  160. package/dist/patterns/status.d.ts +0 -15
  161. package/dist/patterns/status.d.ts.map +0 -1
  162. package/dist/patterns/status.js +0 -9
  163. package/dist/patterns/status.js.map +0 -1
  164. package/dist/patterns/timestamps.d.ts +0 -10
  165. package/dist/patterns/timestamps.d.ts.map +0 -1
  166. package/dist/patterns/timestamps.js +0 -10
  167. package/dist/patterns/timestamps.js.map +0 -1
  168. package/dist/redaction/index.d.ts +0 -4
  169. package/dist/redaction/index.d.ts.map +0 -1
  170. package/dist/redaction/index.js +0 -3
  171. package/dist/redaction/index.js.map +0 -1
  172. package/dist/redaction/patterns.d.ts +0 -9
  173. package/dist/redaction/patterns.d.ts.map +0 -1
  174. package/dist/redaction/patterns.js +0 -39
  175. package/dist/redaction/patterns.js.map +0 -1
  176. package/dist/redaction/redactor.d.ts +0 -27
  177. package/dist/redaction/redactor.d.ts.map +0 -1
  178. package/dist/redaction/redactor.js +0 -89
  179. package/dist/redaction/redactor.js.map +0 -1
  180. package/dist/resilience.d.ts +0 -34
  181. package/dist/resilience.d.ts.map +0 -1
  182. package/dist/resilience.js +0 -164
  183. package/dist/resilience.js.map +0 -1
  184. package/dist/result.d.ts +0 -57
  185. package/dist/result.d.ts.map +0 -1
  186. package/dist/result.js +0 -145
  187. package/dist/result.js.map +0 -1
  188. package/dist/serialization.d.ts +0 -27
  189. package/dist/serialization.d.ts.map +0 -1
  190. package/dist/serialization.js +0 -115
  191. package/dist/serialization.js.map +0 -1
  192. package/dist/topo.d.ts +0 -18
  193. package/dist/topo.d.ts.map +0 -1
  194. package/dist/topo.js +0 -74
  195. package/dist/topo.js.map +0 -1
  196. package/dist/trail.d.ts +0 -83
  197. package/dist/trail.d.ts.map +0 -1
  198. package/dist/trail.js +0 -16
  199. package/dist/trail.js.map +0 -1
  200. package/dist/types.d.ts +0 -46
  201. package/dist/types.d.ts.map +0 -1
  202. package/dist/types.js +0 -2
  203. package/dist/types.js.map +0 -1
  204. package/dist/validate-topo.d.ts +0 -24
  205. package/dist/validate-topo.d.ts.map +0 -1
  206. package/dist/validate-topo.js +0 -108
  207. package/dist/validate-topo.js.map +0 -1
  208. package/dist/validation.d.ts +0 -27
  209. package/dist/validation.d.ts.map +0 -1
  210. package/dist/validation.js +0 -134
  211. package/dist/validation.js.map +0 -1
  212. package/dist/workspace.d.ts +0 -25
  213. package/dist/workspace.d.ts.map +0 -1
  214. package/dist/workspace.js +0 -57
  215. package/dist/workspace.js.map +0 -1
  216. package/src/__tests__/blob-ref.test.ts +0 -103
  217. package/src/__tests__/branded.test.ts +0 -148
  218. package/src/__tests__/collections.test.ts +0 -126
  219. package/src/__tests__/context.test.ts +0 -66
  220. package/src/__tests__/derive.test.ts +0 -159
  221. package/src/__tests__/errors.test.ts +0 -309
  222. package/src/__tests__/event.test.ts +0 -82
  223. package/src/__tests__/fetch.test.ts +0 -217
  224. package/src/__tests__/guards.test.ts +0 -102
  225. package/src/__tests__/hike.test.ts +0 -117
  226. package/src/__tests__/job.test.ts +0 -98
  227. package/src/__tests__/layer.test.ts +0 -224
  228. package/src/__tests__/path-security.test.ts +0 -114
  229. package/src/__tests__/patterns.test.ts +0 -273
  230. package/src/__tests__/redaction.test.ts +0 -244
  231. package/src/__tests__/resilience.test.ts +0 -246
  232. package/src/__tests__/result.test.ts +0 -155
  233. package/src/__tests__/serialization.test.ts +0 -236
  234. package/src/__tests__/topo.test.ts +0 -184
  235. package/src/__tests__/trail.test.ts +0 -179
  236. package/src/__tests__/validate-topo.test.ts +0 -201
  237. package/src/__tests__/validation.test.ts +0 -283
  238. package/src/__tests__/workspace.test.ts +0 -183
  239. package/src/adapters.ts +0 -68
  240. package/src/event.ts +0 -77
  241. package/src/health.ts +0 -23
  242. package/src/hike.ts +0 -77
  243. package/src/job.ts +0 -20
  244. package/tsconfig.json +0 -9
  245. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,308 @@
1
+ import {
2
+ AmbiguousError,
3
+ TrailsError,
4
+ ValidationError,
5
+ VersionNotSupportedError,
6
+ } from './errors.js';
7
+ import { Result } from './result.js';
8
+ import {
9
+ deriveSupportedTrailVersions,
10
+ getTrailVersionEntryKind,
11
+ isArchivedTrailVersionEntry,
12
+ } from './trail.js';
13
+ import type {
14
+ AnyTrail,
15
+ TrailVersionForkEntry,
16
+ TrailVersionRevisionEntry,
17
+ } from './trail.js';
18
+ import {
19
+ TRAIL_VERSION_MARKER_LENGTH,
20
+ TRAIL_VERSION_MARKER_MIN_PREFIX_LENGTH,
21
+ deriveTrailVersionMarkers,
22
+ } from './version-marker.js';
23
+
24
+ export type TrailVersionReference = number | string;
25
+
26
+ export interface ParsedTrailVersionReference {
27
+ readonly id: string;
28
+ readonly version?: TrailVersionReference | undefined;
29
+ }
30
+
31
+ export type ResolvedTrailVersion =
32
+ | {
33
+ readonly current: true;
34
+ readonly kind: 'current';
35
+ readonly marker?: string | undefined;
36
+ readonly version?: number | undefined;
37
+ }
38
+ | {
39
+ readonly current: false;
40
+ readonly deprecated: boolean;
41
+ readonly entry: TrailVersionForkEntry;
42
+ readonly kind: 'fork';
43
+ readonly marker?: string | undefined;
44
+ readonly version: number;
45
+ }
46
+ | {
47
+ readonly current: false;
48
+ readonly deprecated: boolean;
49
+ readonly entry: TrailVersionRevisionEntry;
50
+ readonly kind: 'revision';
51
+ readonly marker?: string | undefined;
52
+ readonly version: number;
53
+ };
54
+
55
+ interface ParsedVersionReference {
56
+ readonly display: number | string;
57
+ readonly markerPrefix?: string | undefined;
58
+ readonly number?: number | undefined;
59
+ }
60
+
61
+ type MarkerResolvableTrail = Pick<
62
+ AnyTrail,
63
+ | 'composes'
64
+ | 'detours'
65
+ | 'id'
66
+ | 'input'
67
+ | 'output'
68
+ | 'resources'
69
+ | 'version'
70
+ | 'versions'
71
+ >;
72
+
73
+ const versionRefPattern = /^(.+)@(.+)$/;
74
+ const positiveIntegerPattern = /^[1-9]\d*$/;
75
+ const markerPrefixPattern = /^[0-9a-fA-F]+$/;
76
+
77
+ const parsePositiveInteger = (value: string): number | undefined => {
78
+ if (!positiveIntegerPattern.test(value)) {
79
+ return undefined;
80
+ }
81
+ const version = Number(value);
82
+ return Number.isSafeInteger(version) ? version : undefined;
83
+ };
84
+
85
+ const isInlineVersionReference = (value: string): boolean =>
86
+ parsePositiveInteger(value) !== undefined ||
87
+ (value.length >= TRAIL_VERSION_MARKER_MIN_PREFIX_LENGTH &&
88
+ value.length <= TRAIL_VERSION_MARKER_LENGTH &&
89
+ markerPrefixPattern.test(value));
90
+
91
+ const unsupportedVersion = (
92
+ trail: Pick<AnyTrail, 'id' | 'version' | 'versions'>,
93
+ requested: number | string,
94
+ reason: string
95
+ ): VersionNotSupportedError => {
96
+ const supported = deriveSupportedTrailVersions(trail);
97
+ return new VersionNotSupportedError(trail.id, requested, supported, reason);
98
+ };
99
+
100
+ export const parseTrailIdVersionReference = (
101
+ id: string
102
+ ): Result<ParsedTrailVersionReference, ValidationError> => {
103
+ const match = versionRefPattern.exec(id);
104
+ if (match === null) {
105
+ return Result.ok({ id });
106
+ }
107
+
108
+ const [, baseId, rawVersion] = match;
109
+ if (baseId === undefined || rawVersion === undefined) {
110
+ return Result.err(
111
+ new ValidationError(
112
+ 'Trail version reference must use trail.id@N or trail.id@<marker-prefix>'
113
+ )
114
+ );
115
+ }
116
+ if (!isInlineVersionReference(rawVersion)) {
117
+ return Result.ok({ id });
118
+ }
119
+
120
+ return Result.ok({ id: baseId, version: rawVersion });
121
+ };
122
+
123
+ const parseVersionReference = (
124
+ reference: TrailVersionReference
125
+ ): Result<ParsedVersionReference, Error> => {
126
+ if (typeof reference === 'number') {
127
+ return Number.isSafeInteger(reference) && reference > 0
128
+ ? Result.ok({ display: reference, number: reference })
129
+ : Result.err(
130
+ new ValidationError(
131
+ 'Trail version reference must be a positive integer'
132
+ )
133
+ );
134
+ }
135
+
136
+ const markerRequested = reference.startsWith('@');
137
+ const raw = markerRequested ? reference.slice(1) : reference;
138
+ if (raw.length === 0) {
139
+ return Result.err(
140
+ new ValidationError('Trail version reference must not be empty')
141
+ );
142
+ }
143
+
144
+ const number = parsePositiveInteger(raw);
145
+ if (!markerRequested && number !== undefined) {
146
+ return Result.ok({
147
+ display: reference,
148
+ markerPrefix:
149
+ raw.length >= TRAIL_VERSION_MARKER_MIN_PREFIX_LENGTH &&
150
+ raw.length <= TRAIL_VERSION_MARKER_LENGTH &&
151
+ markerPrefixPattern.test(raw)
152
+ ? raw.toLowerCase()
153
+ : undefined,
154
+ number,
155
+ });
156
+ }
157
+
158
+ if (
159
+ raw.length >= TRAIL_VERSION_MARKER_MIN_PREFIX_LENGTH &&
160
+ raw.length <= TRAIL_VERSION_MARKER_LENGTH &&
161
+ markerPrefixPattern.test(raw)
162
+ ) {
163
+ return Result.ok({
164
+ display: reference,
165
+ markerPrefix: raw.toLowerCase(),
166
+ });
167
+ }
168
+
169
+ return Result.err(
170
+ new ValidationError(
171
+ 'Trail version reference must be a positive integer or marker prefix'
172
+ )
173
+ );
174
+ };
175
+
176
+ const trailHasVersion = (
177
+ trail: Pick<AnyTrail, 'version' | 'versions'>,
178
+ version: number
179
+ ): boolean =>
180
+ trail.version === version || trail.versions?.[version] !== undefined;
181
+
182
+ const resolveMarkerReference = (
183
+ trail: MarkerResolvableTrail,
184
+ requested: number | string,
185
+ prefix: string
186
+ ): Result<{ readonly marker: string; readonly version: number }, Error> => {
187
+ let records: ReturnType<typeof deriveTrailVersionMarkers>;
188
+ try {
189
+ records = deriveTrailVersionMarkers(trail);
190
+ } catch (error: unknown) {
191
+ return Result.err(
192
+ error instanceof TrailsError
193
+ ? error
194
+ : new ValidationError(
195
+ `Trail version marker derivation failed: ${String(error)}`
196
+ )
197
+ );
198
+ }
199
+ const matches = records.filter((record) => record.marker.startsWith(prefix));
200
+
201
+ if (matches.length === 0) {
202
+ return Result.err(unsupportedVersion(trail, requested, 'marker missing'));
203
+ }
204
+ if (matches.length > 1) {
205
+ return Result.err(
206
+ new AmbiguousError(
207
+ `Trail version marker prefix ${prefix} is ambiguous across versions ${matches.map((record) => record.version).join(', ')}`
208
+ )
209
+ );
210
+ }
211
+
212
+ const match = matches[0] as (typeof matches)[number];
213
+
214
+ return Result.ok({ marker: match.marker, version: match.version });
215
+ };
216
+
217
+ const resolveRequestedVersionNumber = (
218
+ trail: MarkerResolvableTrail,
219
+ reference: TrailVersionReference
220
+ ): Result<
221
+ { readonly marker?: string | undefined; readonly version: number },
222
+ Error
223
+ > => {
224
+ const parsed = parseVersionReference(reference);
225
+ if (parsed.isErr()) {
226
+ return parsed;
227
+ }
228
+
229
+ const { display, markerPrefix, number } = parsed.value;
230
+ if (number !== undefined && trailHasVersion(trail, number)) {
231
+ return Result.ok({ version: number });
232
+ }
233
+
234
+ if (markerPrefix !== undefined) {
235
+ const marker = resolveMarkerReference(trail, display, markerPrefix);
236
+ if (marker.isOk()) {
237
+ return marker;
238
+ }
239
+ if (number === undefined) {
240
+ return marker;
241
+ }
242
+ }
243
+
244
+ return Result.err(unsupportedVersion(trail, display, 'missing'));
245
+ };
246
+
247
+ export const resolveTrailVersion = (
248
+ trail: MarkerResolvableTrail,
249
+ reference?: TrailVersionReference | undefined
250
+ ): Result<ResolvedTrailVersion, Error> => {
251
+ if (reference === undefined) {
252
+ return Result.ok({
253
+ current: true,
254
+ kind: 'current',
255
+ ...(trail.version === undefined ? {} : { version: trail.version }),
256
+ });
257
+ }
258
+
259
+ if (trail.version === undefined) {
260
+ return Result.err(unsupportedVersion(trail, reference, 'unversioned'));
261
+ }
262
+
263
+ const requested = resolveRequestedVersionNumber(trail, reference);
264
+ if (requested.isErr()) {
265
+ return requested;
266
+ }
267
+
268
+ if (requested.value.version === trail.version) {
269
+ return Result.ok({
270
+ current: true,
271
+ kind: 'current',
272
+ ...(requested.value.marker === undefined
273
+ ? {}
274
+ : { marker: requested.value.marker }),
275
+ version: trail.version,
276
+ });
277
+ }
278
+
279
+ const entry = trail.versions?.[requested.value.version];
280
+ if (entry === undefined) {
281
+ return Result.err(unsupportedVersion(trail, reference, 'missing'));
282
+ }
283
+ if (isArchivedTrailVersionEntry(entry)) {
284
+ return Result.err(unsupportedVersion(trail, reference, 'archived'));
285
+ }
286
+
287
+ const kind = getTrailVersionEntryKind(entry);
288
+ const base = {
289
+ current: false,
290
+ deprecated: entry.status?.state === 'deprecated',
291
+ ...(requested.value.marker === undefined
292
+ ? {}
293
+ : { marker: requested.value.marker }),
294
+ version: requested.value.version,
295
+ } as const;
296
+
297
+ return kind === 'fork'
298
+ ? Result.ok({
299
+ ...base,
300
+ entry: entry as TrailVersionForkEntry,
301
+ kind,
302
+ })
303
+ : Result.ok({
304
+ ...base,
305
+ entry: entry as TrailVersionRevisionEntry,
306
+ kind,
307
+ });
308
+ };
@@ -0,0 +1,120 @@
1
+ import { InternalError, ValidationError } from './errors.js';
2
+ import { Result } from './result.js';
3
+ import type { ExecuteTrailOptions } from './execute.js';
4
+ import { getTrailVersionEntryKind } from './trail.js';
5
+ import type { AnyTrail, TrailVersionEntry } from './trail.js';
6
+ import { validateInput, validateOutput } from './validation.js';
7
+
8
+ export type TrailVersionCurrentExecutor<
9
+ Options extends ExecuteTrailOptions = ExecuteTrailOptions,
10
+ > = (
11
+ trail: AnyTrail,
12
+ input: unknown,
13
+ options?: Options
14
+ ) => Promise<Result<unknown, Error>>;
15
+
16
+ const normalizeTransposeError = (
17
+ trail: AnyTrail,
18
+ version: number,
19
+ phase: 'input' | 'output',
20
+ error: unknown
21
+ ): InternalError => {
22
+ const message = error instanceof Error ? error.message : String(error);
23
+ const options: { cause?: Error; context: Record<string, unknown> } = {
24
+ context: { phase, trailId: trail.id, version },
25
+ };
26
+ if (error instanceof Error) {
27
+ options.cause = error;
28
+ }
29
+ return new InternalError(
30
+ `Trail "${trail.id}" version ${version} transpose.${phase} failed: ${message}`,
31
+ options
32
+ );
33
+ };
34
+
35
+ const runTransposeInput = async (
36
+ trail: AnyTrail,
37
+ version: number,
38
+ entry: TrailVersionEntry,
39
+ input: unknown
40
+ ): Promise<Result<unknown, Error>> => {
41
+ if (entry.transpose === undefined) {
42
+ return Result.ok(input);
43
+ }
44
+
45
+ try {
46
+ return Result.ok(await entry.transpose.input({ input }));
47
+ } catch (error: unknown) {
48
+ return Result.err(normalizeTransposeError(trail, version, 'input', error));
49
+ }
50
+ };
51
+
52
+ const runTransposeOutput = async (
53
+ trail: AnyTrail,
54
+ version: number,
55
+ entry: TrailVersionEntry,
56
+ output: unknown
57
+ ): Promise<Result<unknown, Error>> => {
58
+ if (entry.transpose === undefined) {
59
+ return Result.ok(output);
60
+ }
61
+
62
+ try {
63
+ return Result.ok(await entry.transpose.output({ output }));
64
+ } catch (error: unknown) {
65
+ return Result.err(normalizeTransposeError(trail, version, 'output', error));
66
+ }
67
+ };
68
+
69
+ export const executeTrailRevision = async <
70
+ Options extends ExecuteTrailOptions = ExecuteTrailOptions,
71
+ >(
72
+ trail: AnyTrail,
73
+ version: number,
74
+ entry: TrailVersionEntry,
75
+ rawInput: unknown,
76
+ options: Options | undefined,
77
+ executeCurrentTrail: TrailVersionCurrentExecutor<Options>
78
+ ): Promise<Result<unknown, Error>> => {
79
+ if (getTrailVersionEntryKind(entry) !== 'revision') {
80
+ return Result.err(
81
+ new ValidationError(
82
+ `Trail "${trail.id}" version ${version} is not a revision entry`
83
+ )
84
+ );
85
+ }
86
+
87
+ const historicalInput = validateInput(entry.input, rawInput);
88
+ if (historicalInput.isErr()) {
89
+ return historicalInput;
90
+ }
91
+
92
+ const currentInput = await runTransposeInput(
93
+ trail,
94
+ version,
95
+ entry,
96
+ historicalInput.value
97
+ );
98
+ if (currentInput.isErr()) {
99
+ return currentInput;
100
+ }
101
+
102
+ const currentOutput = await executeCurrentTrail(
103
+ trail,
104
+ currentInput.value,
105
+ options
106
+ );
107
+ if (currentOutput.isErr()) {
108
+ return currentOutput;
109
+ }
110
+
111
+ const historicalOutput = await runTransposeOutput(
112
+ trail,
113
+ version,
114
+ entry,
115
+ currentOutput.value
116
+ );
117
+ return historicalOutput.isErr()
118
+ ? historicalOutput
119
+ : validateOutput(entry.output, historicalOutput.value);
120
+ };
package/src/webhook.ts ADDED
@@ -0,0 +1,278 @@
1
+ import type {
2
+ ActivationSource,
3
+ ActivationSourceMeta,
4
+ ActivationSourceParse,
5
+ } from './activation-source.js';
6
+ import { InternalError, ValidationError } from './errors.js';
7
+ import { Result } from './result.js';
8
+
9
+ export const webhookMethods = Object.freeze([
10
+ 'DELETE',
11
+ 'GET',
12
+ 'PATCH',
13
+ 'POST',
14
+ 'PUT',
15
+ ] as const);
16
+
17
+ export type WebhookMethod = (typeof webhookMethods)[number];
18
+ export type WebhookMethodInput = WebhookMethod | Lowercase<WebhookMethod>;
19
+
20
+ export type WebhookVerifyHeaders = Readonly<
21
+ Record<string, readonly string[] | string | undefined>
22
+ >;
23
+
24
+ export interface WebhookVerifyRequest {
25
+ readonly body: ArrayBuffer | Uint8Array | string;
26
+ readonly headers: WebhookVerifyHeaders;
27
+ readonly method: string;
28
+ readonly path: string;
29
+ }
30
+
31
+ export type WebhookVerify = (
32
+ request: WebhookVerifyRequest
33
+ ) => Promise<Result<void, Error>> | Result<void, Error>;
34
+
35
+ export interface WebhookSpec<TOutput = unknown> {
36
+ readonly meta?: ActivationSourceMeta | undefined;
37
+ readonly method?: WebhookMethodInput | undefined;
38
+ readonly parse: ActivationSourceParse<TOutput>;
39
+ readonly path: string;
40
+ readonly payload?: ActivationSource['payload'] | undefined;
41
+ readonly verify?: WebhookVerify | undefined;
42
+ /** Reserved for future webhook-specific design; trail versioning is trail-only. */
43
+ readonly version?: never;
44
+ }
45
+
46
+ export interface WebhookSource<TOutput = unknown> extends ActivationSource {
47
+ readonly kind: 'webhook';
48
+ readonly meta?: ActivationSourceMeta | undefined;
49
+ readonly method: WebhookMethod;
50
+ readonly parse: ActivationSourceParse<TOutput>;
51
+ readonly path: string;
52
+ readonly payload?: ActivationSource['payload'] | undefined;
53
+ readonly verify?: WebhookVerify | undefined;
54
+ }
55
+
56
+ export interface WebhookValidationIssue {
57
+ readonly field: 'method' | 'parse' | 'path' | 'verify';
58
+ readonly message: string;
59
+ }
60
+
61
+ const DEFAULT_WEBHOOK_METHOD = 'POST' as const;
62
+
63
+ const normalizeMethod = (
64
+ method: WebhookMethodInput | string | undefined
65
+ ): string => (method ?? DEFAULT_WEBHOOK_METHOD).trim().toUpperCase();
66
+
67
+ const normalizePath = (path: string): string => path.trim();
68
+
69
+ const isWebhookMethod = (method: string): method is WebhookMethod =>
70
+ (webhookMethods as readonly string[]).includes(method);
71
+
72
+ const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
73
+ typeof value === 'object' && value !== null && !Array.isArray(value);
74
+
75
+ const isZodSchema = (value: unknown): boolean =>
76
+ isObjectRecord(value) && typeof value['safeParse'] === 'function';
77
+
78
+ const validateMethod = (
79
+ method: WebhookMethodInput | string | undefined
80
+ ): WebhookValidationIssue[] => {
81
+ const normalized = normalizeMethod(method);
82
+ return isWebhookMethod(normalized)
83
+ ? []
84
+ : [
85
+ {
86
+ field: 'method',
87
+ message: `Webhook method must be one of ${webhookMethods.join(', ')}`,
88
+ },
89
+ ];
90
+ };
91
+
92
+ const validatePath = (path: unknown): WebhookValidationIssue[] => {
93
+ if (typeof path !== 'string' || path.trim().length === 0) {
94
+ return [
95
+ {
96
+ field: 'path',
97
+ message: 'Webhook path must be a non-empty absolute path',
98
+ },
99
+ ];
100
+ }
101
+
102
+ const normalized = normalizePath(path);
103
+ return normalized.startsWith('/')
104
+ ? []
105
+ : [
106
+ {
107
+ field: 'path',
108
+ message: 'Webhook path must start with "/"',
109
+ },
110
+ ];
111
+ };
112
+
113
+ const validateRequiredParse = (parse: unknown): WebhookValidationIssue[] =>
114
+ parse === undefined
115
+ ? [
116
+ {
117
+ field: 'parse',
118
+ message: 'Webhook sources must define parse',
119
+ },
120
+ ]
121
+ : [];
122
+
123
+ const validateParseShape = (parse: unknown): WebhookValidationIssue[] => {
124
+ if (parse === undefined || isZodSchema(parse)) {
125
+ return [];
126
+ }
127
+ if (isObjectRecord(parse) && isZodSchema(parse['output'])) {
128
+ return [];
129
+ }
130
+ return [
131
+ {
132
+ field: 'parse',
133
+ message: 'Webhook parse must be a Zod schema or define parse.output',
134
+ },
135
+ ];
136
+ };
137
+
138
+ const validateVerify = (verify: unknown): WebhookValidationIssue[] =>
139
+ verify === undefined || typeof verify === 'function'
140
+ ? []
141
+ : [
142
+ {
143
+ field: 'verify',
144
+ message: 'Webhook verify must be a function when provided',
145
+ },
146
+ ];
147
+
148
+ const webhookIssuesMessage = (
149
+ id: string,
150
+ issues: readonly WebhookValidationIssue[]
151
+ ): string =>
152
+ `webhook("${id}") is invalid: ${issues.map((issue) => `${issue.field}: ${issue.message}`).join('; ')}`;
153
+
154
+ const assertWebhookSpec = <TOutput>(
155
+ id: string,
156
+ spec: WebhookSpec<TOutput>
157
+ ): {
158
+ readonly method: WebhookMethod;
159
+ readonly path: string;
160
+ } => {
161
+ const issues = [
162
+ ...validateMethod(spec.method),
163
+ ...validatePath(spec.path),
164
+ ...validateRequiredParse(spec.parse),
165
+ ...validateParseShape(spec.parse),
166
+ ...validateVerify(spec.verify),
167
+ ];
168
+
169
+ if (issues.length > 0) {
170
+ throw new ValidationError(webhookIssuesMessage(id, issues), {
171
+ context: { issues },
172
+ });
173
+ }
174
+
175
+ return {
176
+ method: normalizeMethod(spec.method) as WebhookMethod,
177
+ path: normalizePath(spec.path),
178
+ };
179
+ };
180
+
181
+ export const validateWebhookSource = (
182
+ source: ActivationSource
183
+ ): readonly WebhookValidationIssue[] => {
184
+ if (source.kind !== 'webhook') {
185
+ return [];
186
+ }
187
+
188
+ return [
189
+ ...validateMethod(source.method),
190
+ ...validatePath(source.path),
191
+ ...validateRequiredParse(source.parse),
192
+ ...validateParseShape(source.parse),
193
+ ...validateVerify(source.verify),
194
+ ];
195
+ };
196
+
197
+ const errorFromUnknown = (error: unknown): Error =>
198
+ error instanceof Error ? error : new Error(String(error));
199
+
200
+ export const getWebhookHeaders = (
201
+ request: Pick<WebhookVerifyRequest, 'headers'>,
202
+ name: string
203
+ ): readonly string[] => {
204
+ const normalized = name.toLowerCase();
205
+ const matches: string[] = [];
206
+ for (const [headerName, value] of Object.entries(request.headers)) {
207
+ if (headerName.toLowerCase() !== normalized) {
208
+ continue;
209
+ }
210
+ if (value === undefined) {
211
+ continue;
212
+ }
213
+ if (typeof value === 'string') {
214
+ matches.push(value);
215
+ } else {
216
+ matches.push(...value);
217
+ }
218
+ }
219
+ return matches;
220
+ };
221
+
222
+ export const getWebhookHeader = (
223
+ request: Pick<WebhookVerifyRequest, 'headers'>,
224
+ name: string
225
+ ): string | undefined => {
226
+ const [first] = getWebhookHeaders(request, name);
227
+ return first;
228
+ };
229
+
230
+ export const verifyWebhookRequest = async (
231
+ source: Pick<WebhookSource, 'id' | 'verify'>,
232
+ request: WebhookVerifyRequest
233
+ ): Promise<Result<void, Error>> => {
234
+ if (source.verify === undefined) {
235
+ return Result.ok();
236
+ }
237
+
238
+ try {
239
+ return await source.verify(request);
240
+ } catch (error) {
241
+ const cause = errorFromUnknown(error);
242
+ return Result.err(
243
+ new InternalError(`Webhook source "${source.id}" verification threw`, {
244
+ cause,
245
+ })
246
+ );
247
+ }
248
+ };
249
+
250
+ export function webhook<TOutput>(
251
+ id: string,
252
+ spec: WebhookSpec<TOutput>
253
+ ): WebhookSource<TOutput>;
254
+ export function webhook<TOutput>(
255
+ spec: WebhookSpec<TOutput> & { readonly id: string }
256
+ ): WebhookSource<TOutput>;
257
+ export function webhook<TOutput>(
258
+ idOrSpec: string | (WebhookSpec<TOutput> & { readonly id: string }),
259
+ maybeSpec?: WebhookSpec<TOutput>
260
+ ): WebhookSource<TOutput> {
261
+ const id = typeof idOrSpec === 'string' ? idOrSpec : idOrSpec.id;
262
+ // oxlint-disable-next-line no-non-null-assertion -- overload guarantees maybeSpec when idOrSpec is string
263
+ const spec = typeof idOrSpec === 'string' ? maybeSpec! : idOrSpec;
264
+ const normalized = assertWebhookSpec(id, spec);
265
+
266
+ return Object.freeze({
267
+ id,
268
+ kind: 'webhook' as const,
269
+ method: normalized.method,
270
+ parse: spec.parse,
271
+ path: normalized.path,
272
+ ...(spec.meta === undefined
273
+ ? {}
274
+ : { meta: Object.freeze({ ...spec.meta }) }),
275
+ ...(spec.payload === undefined ? {} : { payload: spec.payload }),
276
+ ...(spec.verify === undefined ? {} : { verify: spec.verify }),
277
+ });
278
+ }
package/src/workspace.ts CHANGED
@@ -71,7 +71,7 @@ export const isInsideWorkspace = (
71
71
  /**
72
72
  * Returns the relative path from `workspaceRoot` to `filePath`.
73
73
  */
74
- export const getRelativePath = (
74
+ export const deriveRelativePath = (
75
75
  filePath: string,
76
76
  workspaceRoot: string
77
77
  ): string => relative(resolve(workspaceRoot), resolve(filePath));