@ontrails/core 1.0.0-beta.4 → 1.0.0-beta.41
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 +412 -12
- package/README.md +112 -28
- package/package.json +11 -1
- package/src/activation-provenance.ts +116 -0
- package/src/activation-source-compatibility.ts +430 -0
- package/src/activation-source-projection.ts +227 -0
- package/src/activation-source.ts +93 -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 +59 -11
- package/src/derive.ts +271 -19
- package/src/detours.ts +8 -0
- package/src/diagnostics.ts +21 -0
- package/src/draft.ts +350 -0
- package/src/entity.ts +346 -0
- package/src/error-projection.ts +51 -0
- package/src/errors.ts +335 -48
- package/src/execute.ts +1577 -0
- package/src/fire.ts +1172 -0
- package/src/glob.ts +81 -0
- package/src/index.ts +568 -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-scope.ts +66 -0
- package/src/path-security.ts +11 -3
- package/src/permits.ts +12 -0
- package/src/queue.ts +163 -0
- package/src/redaction/patterns.ts +6 -3
- package/src/resilience.ts +11 -11
- package/src/resource-config.ts +804 -0
- package/src/resource.ts +194 -0
- package/src/result.ts +18 -0
- package/src/run.ts +76 -0
- package/src/runtime-builtins.ts +69 -0
- package/src/schedule-runtime.ts +689 -0
- package/src/schedule.ts +326 -0
- package/src/serialization.ts +92 -55
- package/src/sha256.ts +136 -0
- package/src/signal-diagnostics.ts +633 -0
- package/src/signal-ref.ts +111 -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 +101 -0
- package/src/surface-overlay.ts +694 -0
- package/src/surface-versioning.ts +42 -0
- package/src/topo.ts +762 -38
- package/src/tracing.ts +346 -0
- package/src/trail-id-glob.ts +15 -0
- package/src/trail.ts +1244 -34
- package/src/trails/derive-trail.ts +835 -0
- package/src/trails/index.ts +9 -0
- package/src/trails/ingest.ts +152 -0
- package/src/trails-db.ts +212 -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 +73 -0
- package/src/validate-topo.ts +569 -55
- 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 +461 -0
- package/src/workspace.ts +183 -16
- 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 -125
- 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/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 -156
- 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 -153
- package/dist/serialization.js.map +0 -1
- package/dist/topo.d.ts +0 -16
- package/dist/topo.d.ts.map +0 -1
- package/dist/topo.js +0 -66
- package/dist/topo.js.map +0 -1
- package/dist/trail.d.ts +0 -89
- package/dist/trail.d.ts.map +0 -1
- package/dist/trail.js +0 -18
- 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 -158
- 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 -203
- 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__/job.test.ts +0 -98
- package/src/__tests__/layer.test.ts +0 -212
- 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 -401
- package/src/__tests__/topo.test.ts +0 -181
- package/src/__tests__/trail.test.ts +0 -217
- package/src/__tests__/validate-topo.test.ts +0 -278
- 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/job.ts +0 -20
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
package/src/schedule.ts
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { ValidationError } from './errors.js';
|
|
2
|
+
import type {
|
|
3
|
+
ActivationSource,
|
|
4
|
+
ActivationSourceMeta,
|
|
5
|
+
} from './activation-source.js';
|
|
6
|
+
|
|
7
|
+
type ScheduleInputDefault = Record<string, never>;
|
|
8
|
+
|
|
9
|
+
export interface ScheduleSpec<TInput = unknown> {
|
|
10
|
+
readonly cron: string;
|
|
11
|
+
readonly input?: TInput | undefined;
|
|
12
|
+
readonly meta?: ActivationSourceMeta | undefined;
|
|
13
|
+
readonly timezone?: string | undefined;
|
|
14
|
+
/** Reserved for future schedule-specific design; trail versioning is trail-only. */
|
|
15
|
+
readonly version?: never;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ScheduleSource<TInput = unknown> extends ActivationSource {
|
|
19
|
+
readonly cron: string;
|
|
20
|
+
readonly input: TInput;
|
|
21
|
+
readonly kind: 'schedule';
|
|
22
|
+
readonly meta?: ActivationSourceMeta | undefined;
|
|
23
|
+
readonly timezone?: string | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ScheduleValidationIssue {
|
|
27
|
+
readonly field: 'cron' | 'input' | 'timezone';
|
|
28
|
+
readonly message: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const CRON_FIELD_BOUNDS = Object.freeze([
|
|
32
|
+
{ max: 59, min: 0, name: 'minute' },
|
|
33
|
+
{ max: 23, min: 0, name: 'hour' },
|
|
34
|
+
{ max: 31, min: 1, name: 'day of month' },
|
|
35
|
+
{ max: 12, min: 1, name: 'month' },
|
|
36
|
+
{ max: 7, min: 0, name: 'day of week' },
|
|
37
|
+
] as const);
|
|
38
|
+
|
|
39
|
+
const EMPTY_INPUT = Object.freeze({}) as ScheduleInputDefault;
|
|
40
|
+
|
|
41
|
+
const normalizeCron = (cron: string): string =>
|
|
42
|
+
cron.trim().replaceAll(/\s+/g, ' ');
|
|
43
|
+
|
|
44
|
+
const isIntegerToken = (value: string): boolean => /^\d+$/.test(value);
|
|
45
|
+
|
|
46
|
+
const parseIntegerToken = (value: string): number | undefined =>
|
|
47
|
+
isIntegerToken(value) ? Number.parseInt(value, 10) : undefined;
|
|
48
|
+
|
|
49
|
+
const isNumberInRange = (
|
|
50
|
+
value: string,
|
|
51
|
+
bounds: (typeof CRON_FIELD_BOUNDS)[number]
|
|
52
|
+
): boolean => {
|
|
53
|
+
const parsed = parseIntegerToken(value);
|
|
54
|
+
return parsed !== undefined && parsed >= bounds.min && parsed <= bounds.max;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const isCronAtomValid = (
|
|
58
|
+
atom: string,
|
|
59
|
+
bounds: (typeof CRON_FIELD_BOUNDS)[number]
|
|
60
|
+
): boolean => {
|
|
61
|
+
if (atom === '*') {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
if (isNumberInRange(atom, bounds)) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const range = atom.split('-');
|
|
69
|
+
if (range.length !== 2) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const [rangeStart, rangeEnd] = range;
|
|
73
|
+
if (rangeStart === undefined || rangeEnd === undefined) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
if (
|
|
77
|
+
!isNumberInRange(rangeStart, bounds) ||
|
|
78
|
+
!isNumberInRange(rangeEnd, bounds)
|
|
79
|
+
) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
const start = Number.parseInt(rangeStart, 10);
|
|
83
|
+
const end = Number.parseInt(rangeEnd, 10);
|
|
84
|
+
return start <= end;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const isCronPartValid = (
|
|
88
|
+
part: string,
|
|
89
|
+
bounds: (typeof CRON_FIELD_BOUNDS)[number]
|
|
90
|
+
): boolean => {
|
|
91
|
+
const stepped = part.split('/');
|
|
92
|
+
if (stepped.length > 2) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
const [atom, step] = stepped;
|
|
96
|
+
if (!atom || !isCronAtomValid(atom, bounds)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
if (step === undefined) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
const parsedStep = parseIntegerToken(step);
|
|
103
|
+
return parsedStep !== undefined && parsedStep > 0;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const validateCronField = (
|
|
107
|
+
field: string,
|
|
108
|
+
bounds: (typeof CRON_FIELD_BOUNDS)[number]
|
|
109
|
+
): string | undefined => {
|
|
110
|
+
if (field.length === 0) {
|
|
111
|
+
return `${bounds.name} field is empty`;
|
|
112
|
+
}
|
|
113
|
+
const parts = field.split(',');
|
|
114
|
+
return parts.every((part) => isCronPartValid(part, bounds))
|
|
115
|
+
? undefined
|
|
116
|
+
: `${bounds.name} field is not a supported cron expression`;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const validateCron = (cron: unknown): ScheduleValidationIssue[] => {
|
|
120
|
+
if (typeof cron !== 'string' || cron.trim().length === 0) {
|
|
121
|
+
return [
|
|
122
|
+
{ field: 'cron', message: 'Cron expression must be a non-empty string' },
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const normalized = normalizeCron(cron);
|
|
127
|
+
const fields = normalized.split(' ');
|
|
128
|
+
if (fields.length !== CRON_FIELD_BOUNDS.length) {
|
|
129
|
+
return [
|
|
130
|
+
{
|
|
131
|
+
field: 'cron',
|
|
132
|
+
message: 'Cron expression must contain exactly five fields',
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return fields.flatMap((field, index) => {
|
|
138
|
+
const bounds = CRON_FIELD_BOUNDS[index];
|
|
139
|
+
if (!bounds) {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
const issue = validateCronField(field, bounds);
|
|
143
|
+
return issue === undefined
|
|
144
|
+
? []
|
|
145
|
+
: [{ field: 'cron' as const, message: issue }];
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const isTimezoneValid = (timezone: string): boolean => {
|
|
150
|
+
try {
|
|
151
|
+
new Intl.DateTimeFormat('en-US', { timeZone: timezone }).format();
|
|
152
|
+
return true;
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const validateTimezone = (
|
|
159
|
+
timezone: unknown
|
|
160
|
+
): readonly ScheduleValidationIssue[] => {
|
|
161
|
+
if (timezone === undefined) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
if (typeof timezone !== 'string' || timezone.trim().length === 0) {
|
|
165
|
+
return [
|
|
166
|
+
{
|
|
167
|
+
field: 'timezone',
|
|
168
|
+
message: 'Timezone must be a non-empty IANA timezone string',
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
}
|
|
172
|
+
return isTimezoneValid(timezone.trim())
|
|
173
|
+
? []
|
|
174
|
+
: [
|
|
175
|
+
{
|
|
176
|
+
field: 'timezone',
|
|
177
|
+
message: `Timezone "${timezone}" is not supported by Intl.DateTimeFormat`,
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const isNonJsonLeaf = (value: unknown): boolean => {
|
|
183
|
+
if (value === undefined) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
const kind = typeof value;
|
|
187
|
+
if (kind === 'function' || kind === 'symbol' || kind === 'bigint') {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
return (
|
|
191
|
+
value instanceof Date ||
|
|
192
|
+
value instanceof RegExp ||
|
|
193
|
+
value instanceof Map ||
|
|
194
|
+
value instanceof Set
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const containsNonSerializableLeaf = (
|
|
199
|
+
value: unknown,
|
|
200
|
+
seen: WeakSet<object>
|
|
201
|
+
): boolean => {
|
|
202
|
+
if (isNonJsonLeaf(value)) {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
if (value === null || typeof value !== 'object') {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
if (seen.has(value)) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
seen.add(value);
|
|
212
|
+
if (Array.isArray(value)) {
|
|
213
|
+
return value.some((entry) => containsNonSerializableLeaf(entry, seen));
|
|
214
|
+
}
|
|
215
|
+
return Object.values(value).some((entry) =>
|
|
216
|
+
containsNonSerializableLeaf(entry, seen)
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const validateInput = (input: unknown): readonly ScheduleValidationIssue[] => {
|
|
221
|
+
if (containsNonSerializableLeaf(input, new WeakSet<object>())) {
|
|
222
|
+
return [
|
|
223
|
+
{
|
|
224
|
+
field: 'input',
|
|
225
|
+
message: 'Schedule input must be JSON-serializable data',
|
|
226
|
+
},
|
|
227
|
+
];
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
JSON.stringify(input);
|
|
231
|
+
return [];
|
|
232
|
+
} catch {
|
|
233
|
+
return [
|
|
234
|
+
{
|
|
235
|
+
field: 'input',
|
|
236
|
+
message: 'Schedule input must be JSON-serializable data',
|
|
237
|
+
},
|
|
238
|
+
];
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const scheduleIssuesMessage = (
|
|
243
|
+
id: string,
|
|
244
|
+
issues: readonly ScheduleValidationIssue[]
|
|
245
|
+
): string =>
|
|
246
|
+
`schedule("${id}") is invalid: ${issues.map((issue) => `${issue.field}: ${issue.message}`).join('; ')}`;
|
|
247
|
+
|
|
248
|
+
const assertScheduleSpec = (
|
|
249
|
+
id: string,
|
|
250
|
+
spec: ScheduleSpec
|
|
251
|
+
): {
|
|
252
|
+
readonly cron: string;
|
|
253
|
+
readonly input: unknown;
|
|
254
|
+
readonly timezone?: string | undefined;
|
|
255
|
+
} => {
|
|
256
|
+
const input = spec.input === undefined ? EMPTY_INPUT : spec.input;
|
|
257
|
+
const timezone = spec.timezone?.trim();
|
|
258
|
+
const issues = [
|
|
259
|
+
...validateCron(spec.cron),
|
|
260
|
+
...validateTimezone(timezone),
|
|
261
|
+
...validateInput(input),
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
if (issues.length > 0) {
|
|
265
|
+
throw new ValidationError(scheduleIssuesMessage(id, issues), {
|
|
266
|
+
context: { issues },
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
cron: normalizeCron(spec.cron),
|
|
272
|
+
input,
|
|
273
|
+
...(timezone === undefined ? {} : { timezone }),
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
export const validateScheduleSource = (
|
|
278
|
+
source: ActivationSource
|
|
279
|
+
): readonly ScheduleValidationIssue[] => {
|
|
280
|
+
if (source.kind !== 'schedule') {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
const input = Object.hasOwn(source, 'input') ? source.input : EMPTY_INPUT;
|
|
284
|
+
return [
|
|
285
|
+
...validateCron(source.cron),
|
|
286
|
+
...validateTimezone(source.timezone),
|
|
287
|
+
...validateInput(input),
|
|
288
|
+
];
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
export function schedule<TInput>(
|
|
292
|
+
id: string,
|
|
293
|
+
spec: ScheduleSpec<TInput> & { readonly input: TInput }
|
|
294
|
+
): ScheduleSource<TInput>;
|
|
295
|
+
export function schedule(
|
|
296
|
+
id: string,
|
|
297
|
+
spec: ScheduleSpec
|
|
298
|
+
): ScheduleSource<ScheduleInputDefault>;
|
|
299
|
+
export function schedule<TInput>(
|
|
300
|
+
spec: ScheduleSpec<TInput> & { readonly id: string; readonly input: TInput }
|
|
301
|
+
): ScheduleSource<TInput>;
|
|
302
|
+
export function schedule(
|
|
303
|
+
spec: ScheduleSpec & { readonly id: string }
|
|
304
|
+
): ScheduleSource<ScheduleInputDefault>;
|
|
305
|
+
export function schedule<TInput>(
|
|
306
|
+
idOrSpec: string | (ScheduleSpec<TInput> & { readonly id: string }),
|
|
307
|
+
maybeSpec?: ScheduleSpec<TInput>
|
|
308
|
+
): ScheduleSource<TInput | ScheduleInputDefault> {
|
|
309
|
+
const id = typeof idOrSpec === 'string' ? idOrSpec : idOrSpec.id;
|
|
310
|
+
// oxlint-disable-next-line no-non-null-assertion -- overload guarantees maybeSpec when idOrSpec is string
|
|
311
|
+
const spec = typeof idOrSpec === 'string' ? maybeSpec! : idOrSpec;
|
|
312
|
+
const normalized = assertScheduleSpec(id, spec);
|
|
313
|
+
|
|
314
|
+
return Object.freeze({
|
|
315
|
+
cron: normalized.cron,
|
|
316
|
+
id,
|
|
317
|
+
input: normalized.input as TInput | ScheduleInputDefault,
|
|
318
|
+
kind: 'schedule' as const,
|
|
319
|
+
...(spec.meta === undefined
|
|
320
|
+
? {}
|
|
321
|
+
: { meta: Object.freeze({ ...spec.meta }) }),
|
|
322
|
+
...(normalized.timezone === undefined
|
|
323
|
+
? {}
|
|
324
|
+
: { timezone: normalized.timezone }),
|
|
325
|
+
});
|
|
326
|
+
}
|
package/src/serialization.ts
CHANGED
|
@@ -7,21 +7,26 @@
|
|
|
7
7
|
|
|
8
8
|
import type { ErrorCategory, TrailsError } from './errors.js';
|
|
9
9
|
import {
|
|
10
|
+
AuthError,
|
|
11
|
+
CancelledError,
|
|
12
|
+
ConflictError,
|
|
10
13
|
ValidationError,
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
InternalError,
|
|
15
|
+
NetworkError,
|
|
13
16
|
NotFoundError,
|
|
14
|
-
AlreadyExistsError,
|
|
15
|
-
ConflictError,
|
|
16
17
|
PermissionError,
|
|
17
|
-
TimeoutError,
|
|
18
18
|
RateLimitError,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
RetryExhaustedError,
|
|
20
|
+
TimeoutError,
|
|
21
|
+
WorkspaceShiftError,
|
|
22
|
+
errorClasses,
|
|
23
23
|
isTrailsError,
|
|
24
24
|
} from './errors.js';
|
|
25
|
+
import {
|
|
26
|
+
redactErrorContext,
|
|
27
|
+
redactErrorStack,
|
|
28
|
+
redactErrorString,
|
|
29
|
+
} from './error-projection.js';
|
|
25
30
|
import { Result } from './result.js';
|
|
26
31
|
|
|
27
32
|
// ---------------------------------------------------------------------------
|
|
@@ -31,7 +36,10 @@ import { Result } from './result.js';
|
|
|
31
36
|
export interface SerializedError {
|
|
32
37
|
readonly name: string;
|
|
33
38
|
readonly message: string;
|
|
39
|
+
readonly attempts?: number | undefined;
|
|
34
40
|
readonly category?: ErrorCategory | undefined;
|
|
41
|
+
readonly cause?: SerializedError | undefined;
|
|
42
|
+
readonly detour?: string | undefined;
|
|
35
43
|
readonly retryable?: boolean | undefined;
|
|
36
44
|
readonly retryAfter?: number | undefined;
|
|
37
45
|
readonly context?: Record<string, unknown> | undefined;
|
|
@@ -56,10 +64,15 @@ const buildOpts = (
|
|
|
56
64
|
|
|
57
65
|
type ErrorFactory = (
|
|
58
66
|
message: string,
|
|
59
|
-
opts: { context?: Record<string, unknown> },
|
|
67
|
+
opts: { cause?: Error; context?: Record<string, unknown> },
|
|
60
68
|
retryAfter: number | undefined
|
|
61
69
|
) => TrailsError;
|
|
62
70
|
|
|
71
|
+
type FixedErrorConstructor = new (
|
|
72
|
+
message: string,
|
|
73
|
+
options?: { cause?: Error; context?: Record<string, unknown> }
|
|
74
|
+
) => TrailsError;
|
|
75
|
+
|
|
63
76
|
const errorFactories: Record<ErrorCategory, ErrorFactory> = {
|
|
64
77
|
auth: (msg, opts) => new AuthError(msg, opts),
|
|
65
78
|
cancelled: (msg, opts) => new CancelledError(msg, opts),
|
|
@@ -77,6 +90,7 @@ const errorFactories: Record<ErrorCategory, ErrorFactory> = {
|
|
|
77
90
|
}
|
|
78
91
|
return new RateLimitError(msg, rlOpts);
|
|
79
92
|
},
|
|
93
|
+
shift: (msg, opts) => new WorkspaceShiftError(msg, opts),
|
|
80
94
|
timeout: (msg, opts) => new TimeoutError(msg, opts),
|
|
81
95
|
validation: (msg, opts) => new ValidationError(msg, opts),
|
|
82
96
|
};
|
|
@@ -92,30 +106,42 @@ const createErrorByCategory = (
|
|
|
92
106
|
return factory(message, opts, retryAfter);
|
|
93
107
|
};
|
|
94
108
|
|
|
95
|
-
/** Map error class names to
|
|
96
|
-
const errorConstructorsByName: Record<string, ErrorFactory
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
109
|
+
/** Map fixed error class names to constructors for precise round-tripping. */
|
|
110
|
+
const errorConstructorsByName: Readonly<Record<string, ErrorFactory>> =
|
|
111
|
+
Object.fromEntries(
|
|
112
|
+
errorClasses.flatMap((entry): [string, ErrorFactory][] => {
|
|
113
|
+
if (entry.category === 'dynamic') {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
const ctor = entry.ctor as FixedErrorConstructor;
|
|
117
|
+
return [
|
|
118
|
+
[
|
|
119
|
+
entry.name,
|
|
120
|
+
(message, opts, retryAfter) => {
|
|
121
|
+
if (ctor === RateLimitError) {
|
|
122
|
+
const rateLimitOptions:
|
|
123
|
+
| {
|
|
124
|
+
cause?: Error;
|
|
125
|
+
context?: Record<string, unknown>;
|
|
126
|
+
retryAfter?: number;
|
|
127
|
+
}
|
|
128
|
+
| undefined =
|
|
129
|
+
opts.context === undefined &&
|
|
130
|
+
opts.cause === undefined &&
|
|
131
|
+
retryAfter === undefined
|
|
132
|
+
? undefined
|
|
133
|
+
: {
|
|
134
|
+
...opts,
|
|
135
|
+
...(retryAfter === undefined ? {} : { retryAfter }),
|
|
136
|
+
};
|
|
137
|
+
return new RateLimitError(message, rateLimitOptions);
|
|
138
|
+
}
|
|
139
|
+
return new ctor(message, opts);
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
];
|
|
143
|
+
})
|
|
144
|
+
);
|
|
119
145
|
|
|
120
146
|
// ---------------------------------------------------------------------------
|
|
121
147
|
// Error serialization
|
|
@@ -124,16 +150,23 @@ const errorConstructorsByName: Record<string, ErrorFactory> = {
|
|
|
124
150
|
/** Extract structured data from an Error for transport. */
|
|
125
151
|
export const serializeError = (error: Error): SerializedError => {
|
|
126
152
|
const result: SerializedError = {
|
|
127
|
-
message: error.message,
|
|
153
|
+
message: redactErrorString(error.message),
|
|
128
154
|
name: error.name,
|
|
129
|
-
stack: error.stack,
|
|
155
|
+
stack: redactErrorStack(error.stack),
|
|
130
156
|
};
|
|
131
157
|
|
|
132
158
|
if (isTrailsError(error)) {
|
|
133
159
|
return {
|
|
134
160
|
...result,
|
|
135
161
|
category: error.category,
|
|
136
|
-
context: error.context,
|
|
162
|
+
context: redactErrorContext(error.context),
|
|
163
|
+
...(error instanceof RetryExhaustedError
|
|
164
|
+
? {
|
|
165
|
+
attempts: error.attempts,
|
|
166
|
+
cause: serializeError(error.cause),
|
|
167
|
+
detour: error.detour,
|
|
168
|
+
}
|
|
169
|
+
: {}),
|
|
137
170
|
retryAfter:
|
|
138
171
|
error instanceof RateLimitError ? error.retryAfter : undefined,
|
|
139
172
|
retryable: error.retryable,
|
|
@@ -146,6 +179,28 @@ export const serializeError = (error: Error): SerializedError => {
|
|
|
146
179
|
/** Reconstruct a TrailsError from serialized data. */
|
|
147
180
|
export const deserializeError = (data: SerializedError): TrailsError => {
|
|
148
181
|
const opts = buildOpts(data.context);
|
|
182
|
+
if (data.name === 'RetryExhaustedError') {
|
|
183
|
+
const wrapped =
|
|
184
|
+
data.cause === undefined
|
|
185
|
+
? createErrorByCategory(
|
|
186
|
+
data.category ?? 'internal',
|
|
187
|
+
data.message,
|
|
188
|
+
data.context,
|
|
189
|
+
data.retryAfter
|
|
190
|
+
)
|
|
191
|
+
: deserializeError(data.cause);
|
|
192
|
+
const error = new RetryExhaustedError(wrapped, {
|
|
193
|
+
attempts: data.attempts ?? 0,
|
|
194
|
+
detour: data.detour ?? 'unknown',
|
|
195
|
+
});
|
|
196
|
+
if (data.message !== error.message) {
|
|
197
|
+
error.message = data.message;
|
|
198
|
+
}
|
|
199
|
+
if (data.stack) {
|
|
200
|
+
error.stack = data.stack;
|
|
201
|
+
}
|
|
202
|
+
return error;
|
|
203
|
+
}
|
|
149
204
|
const nameFactory = errorConstructorsByName[data.name];
|
|
150
205
|
|
|
151
206
|
const error = nameFactory
|
|
@@ -164,24 +219,6 @@ export const deserializeError = (data: SerializedError): TrailsError => {
|
|
|
164
219
|
return error;
|
|
165
220
|
};
|
|
166
221
|
|
|
167
|
-
// ---------------------------------------------------------------------------
|
|
168
|
-
// Safe JSON
|
|
169
|
-
// ---------------------------------------------------------------------------
|
|
170
|
-
|
|
171
|
-
/** Parse a JSON string, returning a Result instead of throwing. */
|
|
172
|
-
export const safeParse = (json: string): Result<unknown, ValidationError> => {
|
|
173
|
-
try {
|
|
174
|
-
return Result.ok(JSON.parse(json) as unknown);
|
|
175
|
-
} catch (error) {
|
|
176
|
-
return Result.err(
|
|
177
|
-
new ValidationError('Invalid JSON', {
|
|
178
|
-
cause: error instanceof Error ? error : new Error(String(error)),
|
|
179
|
-
context: { input: json.slice(0, 200) },
|
|
180
|
-
})
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
};
|
|
184
|
-
|
|
185
222
|
/** Stringify a value, returning a Result. Handles circular references. */
|
|
186
223
|
export const safeStringify = (
|
|
187
224
|
value: unknown
|
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)));
|