@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.
- package/CHANGELOG.md +283 -5
- package/README.md +113 -33
- package/package.json +11 -1
- package/src/activation-provenance.ts +111 -0
- package/src/activation-source-compatibility.ts +430 -0
- package/src/activation-source-projection.ts +221 -0
- package/src/activation-source.ts +91 -0
- package/src/blob-ref.ts +51 -0
- package/src/branded.ts +1 -1
- package/src/compose-batch.ts +69 -0
- package/src/compose-schema.ts +36 -0
- package/src/context.ts +44 -10
- package/src/contour.ts +346 -0
- package/src/derive.ts +59 -18
- package/src/detours.ts +8 -0
- package/src/draft.ts +342 -0
- package/src/error-projection.ts +51 -0
- package/src/errors.ts +309 -48
- package/src/execute.ts +1577 -0
- package/src/fire.ts +1169 -0
- package/src/index.ts +477 -32
- package/src/internal/fork-ctx.ts +69 -0
- package/src/layer-projection.ts +193 -0
- package/src/layer.ts +43 -6
- package/src/observe.ts +361 -0
- package/src/path-security.ts +1 -1
- package/src/permits.ts +12 -0
- package/src/redaction/patterns.ts +6 -3
- package/src/resilience.ts +11 -11
- package/src/resource-config.ts +792 -0
- package/src/resource.ts +194 -0
- package/src/result.ts +36 -4
- package/src/run.ts +76 -0
- package/src/schedule-runtime.ts +689 -0
- package/src/schedule.ts +326 -0
- package/src/serialization.ts +119 -39
- package/src/signal-diagnostics.ts +633 -0
- package/src/signal-ref.ts +87 -0
- package/src/signal.ts +104 -0
- package/src/store/accessor-protocol.ts +56 -0
- package/src/store/index.ts +4 -0
- package/src/structured-examples.ts +248 -0
- package/src/surface-derivation.ts +91 -0
- package/src/surface-filter.ts +176 -0
- package/src/surface-versioning.ts +42 -0
- package/src/topo.ts +769 -57
- package/src/tracing.ts +345 -0
- package/src/trail.ts +1161 -29
- package/src/trails/derive-trail.ts +842 -0
- package/src/trails/index.ts +9 -0
- package/src/trails/ingest.ts +139 -0
- package/src/trails-db.ts +208 -0
- package/src/transport-error-map.ts +174 -0
- package/src/type-utils.ts +87 -0
- package/src/types.ts +254 -12
- package/src/validate-established-topo.ts +69 -0
- package/src/validate-topo.ts +442 -35
- package/src/validation.ts +152 -4
- package/src/version-marker.ts +716 -0
- package/src/version-resolution.ts +308 -0
- package/src/version-runtime.ts +120 -0
- package/src/webhook.ts +278 -0
- package/src/workspace.ts +1 -1
- package/src/zod-wrappers.ts +72 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/adapters.d.ts +0 -39
- package/dist/adapters.d.ts.map +0 -1
- package/dist/adapters.js +0 -2
- package/dist/adapters.js.map +0 -1
- package/dist/blob-ref.d.ts +0 -20
- package/dist/blob-ref.d.ts.map +0 -1
- package/dist/blob-ref.js +0 -22
- package/dist/blob-ref.js.map +0 -1
- package/dist/branded.d.ts +0 -36
- package/dist/branded.d.ts.map +0 -1
- package/dist/branded.js +0 -89
- package/dist/branded.js.map +0 -1
- package/dist/collections.d.ts +0 -31
- package/dist/collections.d.ts.map +0 -1
- package/dist/collections.js +0 -60
- package/dist/collections.js.map +0 -1
- package/dist/context.d.ts +0 -10
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -15
- package/dist/context.js.map +0 -1
- package/dist/derive.d.ts +0 -33
- package/dist/derive.d.ts.map +0 -1
- package/dist/derive.js +0 -122
- package/dist/derive.js.map +0 -1
- package/dist/errors.d.ts +0 -83
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -142
- package/dist/errors.js.map +0 -1
- package/dist/event.d.ts +0 -45
- package/dist/event.d.ts.map +0 -1
- package/dist/event.js +0 -17
- package/dist/event.js.map +0 -1
- package/dist/fetch.d.ts +0 -15
- package/dist/fetch.d.ts.map +0 -1
- package/dist/fetch.js +0 -102
- package/dist/fetch.js.map +0 -1
- package/dist/guards.d.ts +0 -17
- package/dist/guards.d.ts.map +0 -1
- package/dist/guards.js +0 -25
- package/dist/guards.js.map +0 -1
- package/dist/health.d.ts +0 -18
- package/dist/health.d.ts.map +0 -1
- package/dist/health.js +0 -5
- package/dist/health.js.map +0 -1
- package/dist/hike.d.ts +0 -36
- package/dist/hike.d.ts.map +0 -1
- package/dist/hike.js +0 -20
- package/dist/hike.js.map +0 -1
- package/dist/index.d.ts +0 -34
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -38
- package/dist/index.js.map +0 -1
- package/dist/job.d.ts +0 -24
- package/dist/job.d.ts.map +0 -1
- package/dist/job.js +0 -17
- package/dist/job.js.map +0 -1
- package/dist/layer.d.ts +0 -17
- package/dist/layer.d.ts.map +0 -1
- package/dist/layer.js +0 -21
- package/dist/layer.js.map +0 -1
- package/dist/path-security.d.ts +0 -28
- package/dist/path-security.d.ts.map +0 -1
- package/dist/path-security.js +0 -63
- package/dist/path-security.js.map +0 -1
- package/dist/patterns/bulk.d.ts +0 -15
- package/dist/patterns/bulk.d.ts.map +0 -1
- package/dist/patterns/bulk.js +0 -14
- package/dist/patterns/bulk.js.map +0 -1
- package/dist/patterns/change.d.ts +0 -10
- package/dist/patterns/change.d.ts.map +0 -1
- package/dist/patterns/change.js +0 -10
- package/dist/patterns/change.js.map +0 -1
- package/dist/patterns/date-range.d.ts +0 -10
- package/dist/patterns/date-range.d.ts.map +0 -1
- package/dist/patterns/date-range.js +0 -10
- package/dist/patterns/date-range.js.map +0 -1
- package/dist/patterns/index.d.ts +0 -9
- package/dist/patterns/index.d.ts.map +0 -1
- package/dist/patterns/index.js +0 -9
- package/dist/patterns/index.js.map +0 -1
- package/dist/patterns/pagination.d.ts +0 -18
- package/dist/patterns/pagination.d.ts.map +0 -1
- package/dist/patterns/pagination.js +0 -18
- package/dist/patterns/pagination.js.map +0 -1
- package/dist/patterns/progress.d.ts +0 -11
- package/dist/patterns/progress.d.ts.map +0 -1
- package/dist/patterns/progress.js +0 -11
- package/dist/patterns/progress.js.map +0 -1
- package/dist/patterns/sorting.d.ts +0 -13
- package/dist/patterns/sorting.d.ts.map +0 -1
- package/dist/patterns/sorting.js +0 -10
- package/dist/patterns/sorting.js.map +0 -1
- package/dist/patterns/status.d.ts +0 -15
- package/dist/patterns/status.d.ts.map +0 -1
- package/dist/patterns/status.js +0 -9
- package/dist/patterns/status.js.map +0 -1
- package/dist/patterns/timestamps.d.ts +0 -10
- package/dist/patterns/timestamps.d.ts.map +0 -1
- package/dist/patterns/timestamps.js +0 -10
- package/dist/patterns/timestamps.js.map +0 -1
- package/dist/redaction/index.d.ts +0 -4
- package/dist/redaction/index.d.ts.map +0 -1
- package/dist/redaction/index.js +0 -3
- package/dist/redaction/index.js.map +0 -1
- package/dist/redaction/patterns.d.ts +0 -9
- package/dist/redaction/patterns.d.ts.map +0 -1
- package/dist/redaction/patterns.js +0 -39
- package/dist/redaction/patterns.js.map +0 -1
- package/dist/redaction/redactor.d.ts +0 -27
- package/dist/redaction/redactor.d.ts.map +0 -1
- package/dist/redaction/redactor.js +0 -89
- package/dist/redaction/redactor.js.map +0 -1
- package/dist/resilience.d.ts +0 -34
- package/dist/resilience.d.ts.map +0 -1
- package/dist/resilience.js +0 -164
- package/dist/resilience.js.map +0 -1
- package/dist/result.d.ts +0 -57
- package/dist/result.d.ts.map +0 -1
- package/dist/result.js +0 -145
- package/dist/result.js.map +0 -1
- package/dist/serialization.d.ts +0 -27
- package/dist/serialization.d.ts.map +0 -1
- package/dist/serialization.js +0 -115
- package/dist/serialization.js.map +0 -1
- package/dist/topo.d.ts +0 -18
- package/dist/topo.d.ts.map +0 -1
- package/dist/topo.js +0 -74
- package/dist/topo.js.map +0 -1
- package/dist/trail.d.ts +0 -83
- package/dist/trail.d.ts.map +0 -1
- package/dist/trail.js +0 -16
- package/dist/trail.js.map +0 -1
- package/dist/types.d.ts +0 -46
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/validate-topo.d.ts +0 -24
- package/dist/validate-topo.d.ts.map +0 -1
- package/dist/validate-topo.js +0 -108
- package/dist/validate-topo.js.map +0 -1
- package/dist/validation.d.ts +0 -27
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js +0 -134
- package/dist/validation.js.map +0 -1
- package/dist/workspace.d.ts +0 -25
- package/dist/workspace.d.ts.map +0 -1
- package/dist/workspace.js +0 -57
- package/dist/workspace.js.map +0 -1
- package/src/__tests__/blob-ref.test.ts +0 -103
- package/src/__tests__/branded.test.ts +0 -148
- package/src/__tests__/collections.test.ts +0 -126
- package/src/__tests__/context.test.ts +0 -66
- package/src/__tests__/derive.test.ts +0 -159
- package/src/__tests__/errors.test.ts +0 -309
- package/src/__tests__/event.test.ts +0 -82
- package/src/__tests__/fetch.test.ts +0 -217
- package/src/__tests__/guards.test.ts +0 -102
- package/src/__tests__/hike.test.ts +0 -117
- package/src/__tests__/job.test.ts +0 -98
- package/src/__tests__/layer.test.ts +0 -224
- package/src/__tests__/path-security.test.ts +0 -114
- package/src/__tests__/patterns.test.ts +0 -273
- package/src/__tests__/redaction.test.ts +0 -244
- package/src/__tests__/resilience.test.ts +0 -246
- package/src/__tests__/result.test.ts +0 -155
- package/src/__tests__/serialization.test.ts +0 -236
- package/src/__tests__/topo.test.ts +0 -184
- package/src/__tests__/trail.test.ts +0 -179
- package/src/__tests__/validate-topo.test.ts +0 -201
- package/src/__tests__/validation.test.ts +0 -283
- package/src/__tests__/workspace.test.ts +0 -183
- package/src/adapters.ts +0 -68
- package/src/event.ts +0 -77
- package/src/health.ts +0 -23
- package/src/hike.ts +0 -77
- package/src/job.ts +0 -20
- package/tsconfig.json +0 -9
- 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
|
|
74
|
+
export const deriveRelativePath = (
|
|
75
75
|
filePath: string,
|
|
76
76
|
workspaceRoot: string
|
|
77
77
|
): string => relative(resolve(workspaceRoot), resolve(filePath));
|