@convert-buddy/importer-contracts 0.1.0-beta.1
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/LICENSE +64 -0
- package/NOTICE +33 -0
- package/README.md +74 -0
- package/dist/index.d.ts +234 -0
- package/dist/index.js +1125 -0
- package/package.json +49 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1125 @@
|
|
|
1
|
+
// src/canonical.ts
|
|
2
|
+
function isPlainRecord(value) {
|
|
3
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
4
|
+
const prototype = Object.getPrototypeOf(value);
|
|
5
|
+
return prototype === Object.prototype || prototype === null;
|
|
6
|
+
}
|
|
7
|
+
function canonicalizeValue(value, ancestors) {
|
|
8
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
9
|
+
if (typeof value === "number") {
|
|
10
|
+
if (!Number.isFinite(value)) throw new TypeError("Canonical JSON numbers must be finite");
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
if (Array.isArray(value)) {
|
|
14
|
+
if (ancestors.has(value)) throw new TypeError("Canonical JSON cannot contain cycles");
|
|
15
|
+
const nextAncestors = new Set(ancestors).add(value);
|
|
16
|
+
return value.map((entry) => canonicalizeValue(entry, nextAncestors));
|
|
17
|
+
}
|
|
18
|
+
if (isPlainRecord(value)) {
|
|
19
|
+
if (ancestors.has(value)) throw new TypeError("Canonical JSON cannot contain cycles");
|
|
20
|
+
const nextAncestors = new Set(ancestors).add(value);
|
|
21
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
22
|
+
for (const key of Object.keys(value).sort()) {
|
|
23
|
+
const child = value[key];
|
|
24
|
+
if (child === void 0) throw new TypeError(`Canonical JSON property ${JSON.stringify(key)} is undefined`);
|
|
25
|
+
result[key] = canonicalizeValue(child, nextAncestors);
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
throw new TypeError("Value is not canonical JSON");
|
|
30
|
+
}
|
|
31
|
+
function canonicalStringify(value) {
|
|
32
|
+
return JSON.stringify(canonicalizeValue(value, /* @__PURE__ */ new Set()));
|
|
33
|
+
}
|
|
34
|
+
function canonicalByteLength(value) {
|
|
35
|
+
return new TextEncoder().encode(canonicalStringify(value)).byteLength;
|
|
36
|
+
}
|
|
37
|
+
function cloneJsonValue(value) {
|
|
38
|
+
return canonicalizeValue(value, /* @__PURE__ */ new Set());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/dates.ts
|
|
42
|
+
function isCalendarDate(value) {
|
|
43
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
44
|
+
if (!match) return false;
|
|
45
|
+
const year = Number(match[1]);
|
|
46
|
+
const month = Number(match[2]);
|
|
47
|
+
const day = Number(match[3]);
|
|
48
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
49
|
+
const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
50
|
+
return year >= 1 && month >= 1 && month <= 12 && day >= 1 && day <= days[month - 1];
|
|
51
|
+
}
|
|
52
|
+
function isOffsetDateTime(value) {
|
|
53
|
+
const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?(Z|[+-]\d{2}:\d{2})$/.exec(value);
|
|
54
|
+
if (!match || !isCalendarDate(match[1])) return false;
|
|
55
|
+
if (Number(match[2]) > 23 || Number(match[3]) > 59 || Number(match[4] ?? 0) > 59) return false;
|
|
56
|
+
const zone = match[6];
|
|
57
|
+
if (zone !== "Z" && (Number(zone.slice(1, 3)) > 23 || Number(zone.slice(4)) > 59)) return false;
|
|
58
|
+
return Number.isFinite(Date.parse(value));
|
|
59
|
+
}
|
|
60
|
+
function parseCalendarFormats(value, formats) {
|
|
61
|
+
const results = /* @__PURE__ */ new Set();
|
|
62
|
+
for (const format of formats) {
|
|
63
|
+
const normalized = format.replace(/yyyy/g, "YYYY").replace(/dd/g, "DD");
|
|
64
|
+
const parts = normalized.split(/(YYYY|MM|DD)/);
|
|
65
|
+
const tokens = [];
|
|
66
|
+
const pattern = parts.map((part) => {
|
|
67
|
+
if (["YYYY", "MM", "DD"].includes(part)) {
|
|
68
|
+
tokens.push(part);
|
|
69
|
+
return part === "YYYY" ? "(\\d{4})" : "(\\d{2})";
|
|
70
|
+
}
|
|
71
|
+
return part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
72
|
+
}).join("");
|
|
73
|
+
if (tokens.length !== 3 || new Set(tokens).size !== 3) continue;
|
|
74
|
+
const match = new RegExp(`^${pattern}$`).exec(value);
|
|
75
|
+
if (!match) continue;
|
|
76
|
+
const values = Object.fromEntries(tokens.map((token, index) => [token, match[index + 1]]));
|
|
77
|
+
const date = `${values.YYYY}-${values.MM}-${values.DD}`;
|
|
78
|
+
if (isCalendarDate(date)) results.add(date);
|
|
79
|
+
}
|
|
80
|
+
return results.size === 1 ? [...results][0] : void 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/limits.ts
|
|
84
|
+
var DEFAULT_CONTRACT_LIMITS = Object.freeze({
|
|
85
|
+
maxAliasesPerField: 32,
|
|
86
|
+
maxCanonicalBytes: 256 * 1024,
|
|
87
|
+
maxDescriptionLength: 2e3,
|
|
88
|
+
maxDictionaryEntries: 256,
|
|
89
|
+
maxFields: 512,
|
|
90
|
+
maxMappings: 512,
|
|
91
|
+
maxOperationsPerMapping: 12,
|
|
92
|
+
maxPatternLength: 256,
|
|
93
|
+
maxRecordPathLength: 512,
|
|
94
|
+
maxSamplesPerField: 3,
|
|
95
|
+
maxSampleLength: 128,
|
|
96
|
+
maxStringLength: 512,
|
|
97
|
+
maxWarnings: 128
|
|
98
|
+
});
|
|
99
|
+
function resolveContractLimits(overrides) {
|
|
100
|
+
const limits = { ...DEFAULT_CONTRACT_LIMITS, ...overrides };
|
|
101
|
+
for (const [key, value] of Object.entries(limits)) {
|
|
102
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
103
|
+
throw new TypeError(`Contract limit ${key} must be a positive safe integer`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return Object.freeze(limits);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/validation-helpers.ts
|
|
110
|
+
var FORBIDDEN_PROPERTY_NAMES = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
111
|
+
var ContractValidationError = class extends TypeError {
|
|
112
|
+
issues;
|
|
113
|
+
constructor(contractName, issues) {
|
|
114
|
+
super(`${contractName} is invalid: ${issues[0]?.message ?? "unknown validation error"}`);
|
|
115
|
+
this.name = "ContractValidationError";
|
|
116
|
+
this.issues = Object.freeze(issues.map((issue) => Object.freeze({ ...issue })));
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var ValidationContext = class {
|
|
120
|
+
constructor(limits) {
|
|
121
|
+
this.limits = limits;
|
|
122
|
+
}
|
|
123
|
+
limits;
|
|
124
|
+
issues = [];
|
|
125
|
+
issue(code, path, message) {
|
|
126
|
+
this.issues.push({ code, path, message });
|
|
127
|
+
}
|
|
128
|
+
finish(contractName, value) {
|
|
129
|
+
if (value === void 0 || this.issues.length > 0) {
|
|
130
|
+
throw new ContractValidationError(contractName, this.issues);
|
|
131
|
+
}
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
function isPlainRecord2(value) {
|
|
136
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
137
|
+
const prototype = Object.getPrototypeOf(value);
|
|
138
|
+
return prototype === Object.prototype || prototype === null;
|
|
139
|
+
}
|
|
140
|
+
function readStrictObject(context, value, path, allowedKeys) {
|
|
141
|
+
if (!isPlainRecord2(value)) {
|
|
142
|
+
context.issue("invalid_type", path, "Expected an object");
|
|
143
|
+
return void 0;
|
|
144
|
+
}
|
|
145
|
+
const allowed = new Set(allowedKeys);
|
|
146
|
+
for (const key of Object.keys(value)) {
|
|
147
|
+
if (!allowed.has(key)) {
|
|
148
|
+
context.issue("unknown_property", `${path}.${key}`, `Unknown property ${JSON.stringify(key)}`);
|
|
149
|
+
}
|
|
150
|
+
if (FORBIDDEN_PROPERTY_NAMES.has(key)) {
|
|
151
|
+
context.issue("invalid_value", `${path}.${key}`, `Forbidden property name ${JSON.stringify(key)}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
function readString(context, value, path, options = {}) {
|
|
157
|
+
if (typeof value !== "string") {
|
|
158
|
+
context.issue(value === void 0 ? "missing_value" : "invalid_type", path, "Expected a string");
|
|
159
|
+
return void 0;
|
|
160
|
+
}
|
|
161
|
+
const maxLength = options.maxLength ?? context.limits.maxStringLength;
|
|
162
|
+
if (!options.allowEmpty && value.trim().length === 0 || value.length > maxLength) {
|
|
163
|
+
context.issue(
|
|
164
|
+
"invalid_value",
|
|
165
|
+
path,
|
|
166
|
+
value.length > maxLength ? `String exceeds ${maxLength} characters` : "String must not be empty"
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (options.pattern && !options.pattern.test(value)) {
|
|
170
|
+
context.issue("invalid_value", path, "String has an invalid format");
|
|
171
|
+
}
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
function readFiniteNumber(context, value, path, options = {}) {
|
|
175
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
176
|
+
context.issue(value === void 0 ? "missing_value" : "invalid_type", path, "Expected a finite number");
|
|
177
|
+
return void 0;
|
|
178
|
+
}
|
|
179
|
+
if (options.integer && !Number.isSafeInteger(value)) {
|
|
180
|
+
context.issue("invalid_value", path, "Expected a safe integer");
|
|
181
|
+
}
|
|
182
|
+
if (options.min !== void 0 && value < options.min) {
|
|
183
|
+
context.issue("invalid_value", path, `Value must be at least ${options.min}`);
|
|
184
|
+
}
|
|
185
|
+
if (options.max !== void 0 && value > options.max) {
|
|
186
|
+
context.issue("invalid_value", path, `Value must be at most ${options.max}`);
|
|
187
|
+
}
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
function readBoolean(context, value, path) {
|
|
191
|
+
if (typeof value !== "boolean") {
|
|
192
|
+
context.issue("invalid_type", path, "Expected a boolean");
|
|
193
|
+
return void 0;
|
|
194
|
+
}
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
function assertUnique(context, values, path) {
|
|
198
|
+
const seen = /* @__PURE__ */ new Set();
|
|
199
|
+
values.forEach((value, index) => {
|
|
200
|
+
if (seen.has(value)) {
|
|
201
|
+
context.issue("duplicate_value", `${path}[${index}]`, `Duplicate value ${JSON.stringify(value)}`);
|
|
202
|
+
}
|
|
203
|
+
seen.add(value);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
function readJsonValue(context, value, path, depth = 0) {
|
|
207
|
+
if (depth > 20) {
|
|
208
|
+
context.issue("limit_exceeded", path, "JSON value exceeds the maximum nesting depth of 20");
|
|
209
|
+
return void 0;
|
|
210
|
+
}
|
|
211
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
212
|
+
if (typeof value === "number") {
|
|
213
|
+
if (!Number.isFinite(value)) {
|
|
214
|
+
context.issue("invalid_value", path, "JSON numbers must be finite");
|
|
215
|
+
return void 0;
|
|
216
|
+
}
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
if (Array.isArray(value)) {
|
|
220
|
+
if (value.length > context.limits.maxDictionaryEntries) {
|
|
221
|
+
context.issue("limit_exceeded", path, "JSON array is too large");
|
|
222
|
+
}
|
|
223
|
+
const result = [];
|
|
224
|
+
value.forEach((entry, index) => {
|
|
225
|
+
const parsed = readJsonValue(context, entry, `${path}[${index}]`, depth + 1);
|
|
226
|
+
if (parsed !== void 0) result.push(parsed);
|
|
227
|
+
});
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
if (isPlainRecord2(value)) {
|
|
231
|
+
const keys = Object.keys(value);
|
|
232
|
+
if (keys.length > context.limits.maxDictionaryEntries) {
|
|
233
|
+
context.issue("limit_exceeded", path, "JSON object has too many properties");
|
|
234
|
+
}
|
|
235
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
236
|
+
for (const key of keys) {
|
|
237
|
+
if (FORBIDDEN_PROPERTY_NAMES.has(key)) {
|
|
238
|
+
context.issue("invalid_value", `${path}.${key}`, `Forbidden property name ${JSON.stringify(key)}`);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const parsed = readJsonValue(context, value[key], `${path}.${key}`, depth + 1);
|
|
242
|
+
if (parsed !== void 0) result[key] = parsed;
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
context.issue("invalid_type", path, "Expected a JSON-serializable value");
|
|
247
|
+
return void 0;
|
|
248
|
+
}
|
|
249
|
+
function assertCanonicalSize(context, value, path) {
|
|
250
|
+
if (canonicalByteLength(value) > context.limits.maxCanonicalBytes) {
|
|
251
|
+
context.issue(
|
|
252
|
+
"limit_exceeded",
|
|
253
|
+
path,
|
|
254
|
+
`Canonical payload exceeds ${context.limits.maxCanonicalBytes} bytes`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function cloneContract(value) {
|
|
259
|
+
return cloneJsonValue(value);
|
|
260
|
+
}
|
|
261
|
+
function validateSafePattern(context, pattern, path) {
|
|
262
|
+
if (pattern.length > context.limits.maxPatternLength) {
|
|
263
|
+
context.issue("limit_exceeded", path, `Pattern exceeds ${context.limits.maxPatternLength} characters`);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
new RegExp(pattern, "u");
|
|
268
|
+
} catch {
|
|
269
|
+
context.issue("invalid_value", path, "Pattern is not a valid Unicode regular expression");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const hasBackReference = /\\[1-9]/u.test(pattern);
|
|
273
|
+
const hasLookaroundOrNamedGroup = /\(\?(?:[=!<]|P?<)/u.test(pattern);
|
|
274
|
+
const hasNestedQuantifier = /\([^)]*(?:\+|\*|\{\d+(?:,\d*)?\})[^)]*\)(?:\+|\*|\{\d+(?:,\d*)?\})/u.test(pattern);
|
|
275
|
+
const hasRepeatedWildcard = /\.\*(?:[^|)]*\.\*)/u.test(pattern);
|
|
276
|
+
if (hasBackReference || hasLookaroundOrNamedGroup || hasNestedQuantifier || hasRepeatedWildcard) {
|
|
277
|
+
context.issue("unsafe_pattern", path, "Pattern uses a disallowed potentially unsafe construct");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function isSafeFieldPath(path) {
|
|
281
|
+
return !path.split(/[.[\]]+/u).filter(Boolean).some((segment) => FORBIDDEN_PROPERTY_NAMES.has(segment));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/delivery.ts
|
|
285
|
+
var DELIVERY_PROTOCOL_VERSION = 1;
|
|
286
|
+
var DEFAULT_MAX_DELIVERY_BATCH_BYTES = 2 * 1024 * 1024;
|
|
287
|
+
var DEFAULT_MAX_DELIVERY_BATCH_RECORDS = 1e4;
|
|
288
|
+
var DEFAULT_DELIVERY_SIGNATURE_TOLERANCE_SECONDS = 5 * 60;
|
|
289
|
+
var DELIVERY_HEADERS = Object.freeze({
|
|
290
|
+
apiKey: "x-convert-buddy-key",
|
|
291
|
+
batchId: "x-convert-buddy-batch-id",
|
|
292
|
+
destination: "x-convert-buddy-destination",
|
|
293
|
+
importId: "x-convert-buddy-import-id",
|
|
294
|
+
signature: "x-convert-buddy-signature",
|
|
295
|
+
signatureTimestamp: "x-convert-buddy-signature-timestamp"
|
|
296
|
+
});
|
|
297
|
+
var IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/u;
|
|
298
|
+
var TIMESTAMP_PATTERN = /^(?:0|[1-9]\d{0,15})$/u;
|
|
299
|
+
var SIGNATURE_PATTERN = /^v1=([0-9a-f]{64})$/u;
|
|
300
|
+
var encoder = new TextEncoder();
|
|
301
|
+
function asArrayBuffer(bytes) {
|
|
302
|
+
return new Uint8Array(bytes).buffer;
|
|
303
|
+
}
|
|
304
|
+
function positiveSafeInteger(value, name) {
|
|
305
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
306
|
+
throw new TypeError(`${name} must be a positive safe integer`);
|
|
307
|
+
}
|
|
308
|
+
return value;
|
|
309
|
+
}
|
|
310
|
+
function signingSecretBytes(secret) {
|
|
311
|
+
const bytes = typeof secret === "string" ? encoder.encode(secret) : new Uint8Array(secret);
|
|
312
|
+
if (bytes.byteLength < 32) throw new TypeError("Delivery signing secrets must contain at least 32 bytes");
|
|
313
|
+
return bytes;
|
|
314
|
+
}
|
|
315
|
+
function signaturePayload(input) {
|
|
316
|
+
if (!IDENTIFIER_PATTERN.test(input.importId)) throw new TypeError("Invalid delivery import ID");
|
|
317
|
+
if (!IDENTIFIER_PATTERN.test(input.batchId)) throw new TypeError("Invalid delivery batch ID");
|
|
318
|
+
if (!TIMESTAMP_PATTERN.test(input.timestamp)) throw new TypeError("Invalid delivery timestamp");
|
|
319
|
+
const prefix = encoder.encode(
|
|
320
|
+
`convert-buddy.delivery.v1
|
|
321
|
+
${input.timestamp}
|
|
322
|
+
${input.importId}
|
|
323
|
+
${input.batchId}
|
|
324
|
+
`
|
|
325
|
+
);
|
|
326
|
+
const payload = new Uint8Array(prefix.byteLength + input.rawBody.byteLength);
|
|
327
|
+
payload.set(prefix);
|
|
328
|
+
payload.set(input.rawBody, prefix.byteLength);
|
|
329
|
+
return payload;
|
|
330
|
+
}
|
|
331
|
+
function bytesToHex(bytes) {
|
|
332
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
333
|
+
}
|
|
334
|
+
function hexToBytes(value) {
|
|
335
|
+
const bytes = new Uint8Array(value.length / 2);
|
|
336
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
337
|
+
bytes[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16);
|
|
338
|
+
}
|
|
339
|
+
return bytes;
|
|
340
|
+
}
|
|
341
|
+
async function importHmacKey(secret, usage) {
|
|
342
|
+
return crypto.subtle.importKey(
|
|
343
|
+
"raw",
|
|
344
|
+
asArrayBuffer(signingSecretBytes(secret)),
|
|
345
|
+
{ hash: "SHA-256", name: "HMAC" },
|
|
346
|
+
false,
|
|
347
|
+
[usage]
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
function parseAcceptedRecordBatchV1(input, options = {}) {
|
|
351
|
+
const limits = resolveContractLimits(options.limits);
|
|
352
|
+
const maxRecords = positiveSafeInteger(
|
|
353
|
+
options.maxRecords ?? DEFAULT_MAX_DELIVERY_BATCH_RECORDS,
|
|
354
|
+
"maxRecords"
|
|
355
|
+
);
|
|
356
|
+
const maxBatchBytes = positiveSafeInteger(
|
|
357
|
+
options.maxBatchBytes ?? DEFAULT_MAX_DELIVERY_BATCH_BYTES,
|
|
358
|
+
"maxBatchBytes"
|
|
359
|
+
);
|
|
360
|
+
const context = new ValidationContext(limits);
|
|
361
|
+
const object = readStrictObject(context, input, "$", ["version", "records"]);
|
|
362
|
+
if (!object) return context.finish("AcceptedRecordBatchV1", void 0);
|
|
363
|
+
if (object.version !== DELIVERY_PROTOCOL_VERSION) {
|
|
364
|
+
context.issue("invalid_value", "$.version", "Expected delivery protocol version 1");
|
|
365
|
+
}
|
|
366
|
+
if (!Array.isArray(object.records)) {
|
|
367
|
+
context.issue(
|
|
368
|
+
object.records === void 0 ? "missing_value" : "invalid_type",
|
|
369
|
+
"$.records",
|
|
370
|
+
"Expected an array of records"
|
|
371
|
+
);
|
|
372
|
+
return context.finish("AcceptedRecordBatchV1", void 0);
|
|
373
|
+
}
|
|
374
|
+
if (object.records.length > maxRecords) {
|
|
375
|
+
context.issue("limit_exceeded", "$.records", `Batch exceeds ${maxRecords} records`);
|
|
376
|
+
}
|
|
377
|
+
const records = [];
|
|
378
|
+
object.records.forEach((record, index) => {
|
|
379
|
+
if (!isPlainRecord2(record)) {
|
|
380
|
+
context.issue("invalid_type", `$.records[${index}]`, "Expected a record object");
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const parsed = readJsonValue(context, record, `$.records[${index}]`);
|
|
384
|
+
if (parsed && !Array.isArray(parsed) && typeof parsed === "object") {
|
|
385
|
+
records.push(parsed);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
const result = { version: 1, records };
|
|
389
|
+
if (canonicalByteLength(result) > maxBatchBytes) {
|
|
390
|
+
context.issue("limit_exceeded", "$", `Batch exceeds ${maxBatchBytes} canonical bytes`);
|
|
391
|
+
}
|
|
392
|
+
return cloneContract(context.finish("AcceptedRecordBatchV1", result));
|
|
393
|
+
}
|
|
394
|
+
async function signDeliveryRequestV1(signingSecret, input) {
|
|
395
|
+
const key = await importHmacKey(signingSecret, "sign");
|
|
396
|
+
const signature = await crypto.subtle.sign("HMAC", key, asArrayBuffer(signaturePayload(input)));
|
|
397
|
+
return `v1=${bytesToHex(new Uint8Array(signature))}`;
|
|
398
|
+
}
|
|
399
|
+
async function verifyDeliverySignatureV1(input) {
|
|
400
|
+
const match = SIGNATURE_PATTERN.exec(input.signature);
|
|
401
|
+
if (!match?.[1]) return false;
|
|
402
|
+
const timestamp = Number(input.timestamp);
|
|
403
|
+
if (!Number.isSafeInteger(timestamp)) return false;
|
|
404
|
+
const now = input.nowEpochSeconds ?? Math.floor(Date.now() / 1e3);
|
|
405
|
+
const tolerance = positiveSafeInteger(
|
|
406
|
+
input.toleranceSeconds ?? DEFAULT_DELIVERY_SIGNATURE_TOLERANCE_SECONDS,
|
|
407
|
+
"toleranceSeconds"
|
|
408
|
+
);
|
|
409
|
+
if (!Number.isSafeInteger(now) || Math.abs(now - timestamp) > tolerance) return false;
|
|
410
|
+
try {
|
|
411
|
+
const key = await importHmacKey(input.signingSecret, "verify");
|
|
412
|
+
return crypto.subtle.verify(
|
|
413
|
+
"HMAC",
|
|
414
|
+
key,
|
|
415
|
+
asArrayBuffer(hexToBytes(match[1])),
|
|
416
|
+
asArrayBuffer(signaturePayload(input))
|
|
417
|
+
);
|
|
418
|
+
} catch {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/fingerprint.ts
|
|
424
|
+
function describeSourceStructure(profile) {
|
|
425
|
+
const descriptor = {
|
|
426
|
+
version: 1,
|
|
427
|
+
format: profile.format,
|
|
428
|
+
fields: profile.fields.map((field) => ({
|
|
429
|
+
path: field.path,
|
|
430
|
+
normalizedName: field.normalizedName,
|
|
431
|
+
inferredTypes: field.inferredTypes.filter((entry) => entry.ratio >= 0.05).map((entry) => entry.type).sort()
|
|
432
|
+
})).sort((left, right) => left.path.localeCompare(right.path, "en"))
|
|
433
|
+
};
|
|
434
|
+
if (profile.recordPath !== void 0) descriptor.recordPath = profile.recordPath;
|
|
435
|
+
return descriptor;
|
|
436
|
+
}
|
|
437
|
+
async function computeStructureFingerprint(profile) {
|
|
438
|
+
if (!globalThis.crypto?.subtle) {
|
|
439
|
+
throw new Error("Web Crypto SubtleCrypto is required to compute a structure fingerprint");
|
|
440
|
+
}
|
|
441
|
+
const descriptor = describeSourceStructure(profile);
|
|
442
|
+
const bytes = new TextEncoder().encode(canonicalStringify(descriptor));
|
|
443
|
+
const digest = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", bytes));
|
|
444
|
+
return Array.from(digest, (value) => value.toString(16).padStart(2, "0")).join("");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/types.ts
|
|
448
|
+
var TARGET_FIELD_TYPES = [
|
|
449
|
+
"string",
|
|
450
|
+
"integer",
|
|
451
|
+
"number",
|
|
452
|
+
"boolean",
|
|
453
|
+
"date",
|
|
454
|
+
"datetime",
|
|
455
|
+
"enum"
|
|
456
|
+
];
|
|
457
|
+
var SOURCE_FORMATS = ["csv", "xml", "json", "ndjson", "xlsx"];
|
|
458
|
+
var SOURCE_INFERRED_TYPES = [
|
|
459
|
+
...TARGET_FIELD_TYPES,
|
|
460
|
+
"null",
|
|
461
|
+
"object",
|
|
462
|
+
"array",
|
|
463
|
+
"unknown"
|
|
464
|
+
];
|
|
465
|
+
|
|
466
|
+
// src/source-profile.ts
|
|
467
|
+
var SOURCE_FORMAT_SET = new Set(SOURCE_FORMATS);
|
|
468
|
+
var SOURCE_TYPE_SET = new Set(SOURCE_INFERRED_TYPES);
|
|
469
|
+
var SHA256_PATTERN = /^[a-f0-9]{64}$/u;
|
|
470
|
+
function parseInferredType(context, value, path) {
|
|
471
|
+
const object = readStrictObject(context, value, path, ["type", "ratio"]);
|
|
472
|
+
if (!object) return void 0;
|
|
473
|
+
const typeValue = readString(context, object.type, `${path}.type`, { maxLength: 32 });
|
|
474
|
+
const type = typeValue && SOURCE_TYPE_SET.has(typeValue) ? typeValue : void 0;
|
|
475
|
+
if (typeValue !== void 0 && type === void 0) {
|
|
476
|
+
context.issue("invalid_value", `${path}.type`, `Unsupported inferred type ${JSON.stringify(typeValue)}`);
|
|
477
|
+
}
|
|
478
|
+
const ratio = readFiniteNumber(context, object.ratio, `${path}.ratio`, { min: 0, max: 1 });
|
|
479
|
+
return type !== void 0 && ratio !== void 0 ? { type, ratio } : void 0;
|
|
480
|
+
}
|
|
481
|
+
function parseSourceField(context, value, path, allowSamples) {
|
|
482
|
+
const object = readStrictObject(
|
|
483
|
+
context,
|
|
484
|
+
value,
|
|
485
|
+
path,
|
|
486
|
+
["path", "normalizedName", "inferredTypes", "nullRatio", "distinctEstimate", "samples"]
|
|
487
|
+
);
|
|
488
|
+
if (!object) return void 0;
|
|
489
|
+
const sourcePath = readString(context, object.path, `${path}.path`, {
|
|
490
|
+
maxLength: context.limits.maxRecordPathLength
|
|
491
|
+
});
|
|
492
|
+
if (sourcePath !== void 0 && !isSafeFieldPath(sourcePath)) {
|
|
493
|
+
context.issue("invalid_value", `${path}.path`, "Source path contains a forbidden segment");
|
|
494
|
+
}
|
|
495
|
+
const normalizedName = readString(context, object.normalizedName, `${path}.normalizedName`);
|
|
496
|
+
const nullRatio = readFiniteNumber(context, object.nullRatio, `${path}.nullRatio`, { min: 0, max: 1 });
|
|
497
|
+
let inferredTypes;
|
|
498
|
+
if (!Array.isArray(object.inferredTypes) || object.inferredTypes.length === 0) {
|
|
499
|
+
context.issue("invalid_type", `${path}.inferredTypes`, "Expected a non-empty inferred type array");
|
|
500
|
+
} else {
|
|
501
|
+
inferredTypes = object.inferredTypes.map((entry, index) => parseInferredType(context, entry, `${path}.inferredTypes[${index}]`)).filter((entry) => entry !== void 0);
|
|
502
|
+
assertUnique(context, inferredTypes.map((entry) => entry.type), `${path}.inferredTypes.*.type`);
|
|
503
|
+
const ratioTotal = inferredTypes.reduce((total, entry) => total + entry.ratio, 0);
|
|
504
|
+
if (ratioTotal > 1.000001) {
|
|
505
|
+
context.issue("invalid_value", `${path}.inferredTypes`, "Inferred type ratios must not total more than 1");
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (sourcePath === void 0 || normalizedName === void 0 || nullRatio === void 0 || !inferredTypes) {
|
|
509
|
+
return void 0;
|
|
510
|
+
}
|
|
511
|
+
const field = { path: sourcePath, normalizedName, inferredTypes, nullRatio };
|
|
512
|
+
if (object.distinctEstimate !== void 0) {
|
|
513
|
+
const distinctEstimate = readFiniteNumber(context, object.distinctEstimate, `${path}.distinctEstimate`, {
|
|
514
|
+
min: 0,
|
|
515
|
+
integer: true
|
|
516
|
+
});
|
|
517
|
+
if (distinctEstimate !== void 0) field.distinctEstimate = distinctEstimate;
|
|
518
|
+
}
|
|
519
|
+
if (object.samples !== void 0) {
|
|
520
|
+
if (!allowSamples) {
|
|
521
|
+
context.issue("invalid_value", `${path}.samples`, "Samples are disabled unless explicitly allowed");
|
|
522
|
+
} else if (!Array.isArray(object.samples)) {
|
|
523
|
+
context.issue("invalid_type", `${path}.samples`, "Expected a string array");
|
|
524
|
+
} else {
|
|
525
|
+
if (object.samples.length > context.limits.maxSamplesPerField) {
|
|
526
|
+
context.issue("limit_exceeded", `${path}.samples`, "Too many sample values");
|
|
527
|
+
}
|
|
528
|
+
field.samples = object.samples.map(
|
|
529
|
+
(entry, index) => readString(context, entry, `${path}.samples[${index}]`, {
|
|
530
|
+
allowEmpty: true,
|
|
531
|
+
maxLength: context.limits.maxSampleLength
|
|
532
|
+
})
|
|
533
|
+
).filter((entry) => entry !== void 0);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return field;
|
|
537
|
+
}
|
|
538
|
+
function parseSourceProfileV1(input, options = {}) {
|
|
539
|
+
const context = new ValidationContext(resolveContractLimits(options.limits));
|
|
540
|
+
const object = readStrictObject(context, input, "$", ["version", "format", "structureFingerprint", "recordPath", "fields"]);
|
|
541
|
+
if (!object) return context.finish("SourceProfileV1", void 0);
|
|
542
|
+
if (object.version !== 1) context.issue("invalid_value", "$.version", "Expected version 1");
|
|
543
|
+
const formatValue = readString(context, object.format, "$.format", { maxLength: 16 });
|
|
544
|
+
const format = formatValue && SOURCE_FORMAT_SET.has(formatValue) ? formatValue : void 0;
|
|
545
|
+
if (formatValue !== void 0 && format === void 0) {
|
|
546
|
+
context.issue("invalid_value", "$.format", `Unsupported source format ${JSON.stringify(formatValue)}`);
|
|
547
|
+
}
|
|
548
|
+
const structureFingerprint = readString(context, object.structureFingerprint, "$.structureFingerprint", {
|
|
549
|
+
maxLength: 64,
|
|
550
|
+
pattern: SHA256_PATTERN
|
|
551
|
+
});
|
|
552
|
+
let fields;
|
|
553
|
+
if (!Array.isArray(object.fields) || object.fields.length === 0) {
|
|
554
|
+
context.issue("invalid_type", "$.fields", "Expected a non-empty source field array");
|
|
555
|
+
} else {
|
|
556
|
+
if (object.fields.length > context.limits.maxFields) {
|
|
557
|
+
context.issue("limit_exceeded", "$.fields", `Profile exceeds ${context.limits.maxFields} fields`);
|
|
558
|
+
}
|
|
559
|
+
fields = object.fields.map((entry, index) => parseSourceField(context, entry, `$.fields[${index}]`, options.allowSamples === true)).filter((entry) => entry !== void 0);
|
|
560
|
+
assertUnique(context, fields.map((field) => field.path), "$.fields.*.path");
|
|
561
|
+
}
|
|
562
|
+
if (format === void 0 || structureFingerprint === void 0 || fields === void 0) {
|
|
563
|
+
return context.finish("SourceProfileV1", void 0);
|
|
564
|
+
}
|
|
565
|
+
const profile = { version: 1, format, structureFingerprint, fields };
|
|
566
|
+
if (object.recordPath !== void 0) {
|
|
567
|
+
const recordPath = readString(context, object.recordPath, "$.recordPath", {
|
|
568
|
+
maxLength: context.limits.maxRecordPathLength
|
|
569
|
+
});
|
|
570
|
+
if (recordPath !== void 0) profile.recordPath = recordPath;
|
|
571
|
+
}
|
|
572
|
+
assertCanonicalSize(context, profile, "$");
|
|
573
|
+
return context.finish("SourceProfileV1", cloneContract(profile));
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/target-schema.ts
|
|
577
|
+
var CONTRACT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
578
|
+
var TARGET_FIELD_TYPE_SET = new Set(TARGET_FIELD_TYPES);
|
|
579
|
+
var UNKNOWN_FIELD_POLICIES = /* @__PURE__ */ new Set(["ignore", "reject", "preserve"]);
|
|
580
|
+
function valueMatchesType(value, type) {
|
|
581
|
+
switch (type) {
|
|
582
|
+
case "string":
|
|
583
|
+
case "enum":
|
|
584
|
+
return typeof value === "string";
|
|
585
|
+
case "date":
|
|
586
|
+
return typeof value === "string" && isCalendarDate(value);
|
|
587
|
+
case "datetime":
|
|
588
|
+
return typeof value === "string" && isOffsetDateTime(value);
|
|
589
|
+
case "integer":
|
|
590
|
+
return typeof value === "number" && Number.isSafeInteger(value);
|
|
591
|
+
case "number":
|
|
592
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
593
|
+
case "boolean":
|
|
594
|
+
return typeof value === "boolean";
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
function parseConstraints(context, value, path, type) {
|
|
598
|
+
if (value === void 0) return void 0;
|
|
599
|
+
const object = readStrictObject(
|
|
600
|
+
context,
|
|
601
|
+
value,
|
|
602
|
+
path,
|
|
603
|
+
["min", "max", "minLength", "maxLength", "pattern", "values"]
|
|
604
|
+
);
|
|
605
|
+
if (!object) return void 0;
|
|
606
|
+
const result = {};
|
|
607
|
+
if (object.min !== void 0) {
|
|
608
|
+
const parsed = readFiniteNumber(context, object.min, `${path}.min`);
|
|
609
|
+
if (parsed !== void 0) result.min = parsed;
|
|
610
|
+
}
|
|
611
|
+
if (object.max !== void 0) {
|
|
612
|
+
const parsed = readFiniteNumber(context, object.max, `${path}.max`);
|
|
613
|
+
if (parsed !== void 0) result.max = parsed;
|
|
614
|
+
}
|
|
615
|
+
if (result.min !== void 0 && result.max !== void 0 && result.min > result.max) {
|
|
616
|
+
context.issue("invalid_value", path, "min must not exceed max");
|
|
617
|
+
}
|
|
618
|
+
if ((result.min !== void 0 || result.max !== void 0) && type !== "integer" && type !== "number") {
|
|
619
|
+
context.issue("invalid_value", path, `Numeric constraints are incompatible with ${type}`);
|
|
620
|
+
}
|
|
621
|
+
if (object.minLength !== void 0) {
|
|
622
|
+
const parsed = readFiniteNumber(context, object.minLength, `${path}.minLength`, {
|
|
623
|
+
min: 0,
|
|
624
|
+
integer: true
|
|
625
|
+
});
|
|
626
|
+
if (parsed !== void 0) result.minLength = parsed;
|
|
627
|
+
}
|
|
628
|
+
if (object.maxLength !== void 0) {
|
|
629
|
+
const parsed = readFiniteNumber(context, object.maxLength, `${path}.maxLength`, {
|
|
630
|
+
min: 0,
|
|
631
|
+
integer: true
|
|
632
|
+
});
|
|
633
|
+
if (parsed !== void 0) result.maxLength = parsed;
|
|
634
|
+
}
|
|
635
|
+
if (result.minLength !== void 0 && result.maxLength !== void 0 && result.minLength > result.maxLength) {
|
|
636
|
+
context.issue("invalid_value", path, "minLength must not exceed maxLength");
|
|
637
|
+
}
|
|
638
|
+
if ((result.minLength !== void 0 || result.maxLength !== void 0) && type !== "string" && type !== "enum") {
|
|
639
|
+
context.issue("invalid_value", path, `Length constraints are incompatible with ${type}`);
|
|
640
|
+
}
|
|
641
|
+
if (object.pattern !== void 0) {
|
|
642
|
+
const parsed = readString(context, object.pattern, `${path}.pattern`, {
|
|
643
|
+
maxLength: context.limits.maxPatternLength
|
|
644
|
+
});
|
|
645
|
+
if (parsed !== void 0) {
|
|
646
|
+
validateSafePattern(context, parsed, `${path}.pattern`);
|
|
647
|
+
result.pattern = parsed;
|
|
648
|
+
}
|
|
649
|
+
if (type !== "string") {
|
|
650
|
+
context.issue("invalid_value", `${path}.pattern`, `pattern is incompatible with ${type}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (object.values !== void 0) {
|
|
654
|
+
if (!Array.isArray(object.values) || object.values.length === 0) {
|
|
655
|
+
context.issue("invalid_type", `${path}.values`, "Expected a non-empty string array");
|
|
656
|
+
} else {
|
|
657
|
+
if (object.values.length > context.limits.maxDictionaryEntries) {
|
|
658
|
+
context.issue("limit_exceeded", `${path}.values`, "Enum contains too many values");
|
|
659
|
+
}
|
|
660
|
+
const values = object.values.map(
|
|
661
|
+
(entry, index) => readString(context, entry, `${path}.values[${index}]`, {
|
|
662
|
+
maxLength: context.limits.maxStringLength
|
|
663
|
+
})
|
|
664
|
+
).filter((entry) => entry !== void 0);
|
|
665
|
+
assertUnique(context, values, `${path}.values`);
|
|
666
|
+
result.values = values;
|
|
667
|
+
}
|
|
668
|
+
if (type !== "enum") {
|
|
669
|
+
context.issue("invalid_value", `${path}.values`, "values is only valid for enum fields");
|
|
670
|
+
}
|
|
671
|
+
} else if (type === "enum") {
|
|
672
|
+
context.issue("missing_value", `${path}.values`, "Enum fields require an allowed values list");
|
|
673
|
+
}
|
|
674
|
+
return result;
|
|
675
|
+
}
|
|
676
|
+
function parseField(context, value, path) {
|
|
677
|
+
const object = readStrictObject(
|
|
678
|
+
context,
|
|
679
|
+
value,
|
|
680
|
+
path,
|
|
681
|
+
["key", "label", "type", "required", "aliases", "description", "defaultValue", "constraints"]
|
|
682
|
+
);
|
|
683
|
+
if (!object) return void 0;
|
|
684
|
+
const key = readString(context, object.key, `${path}.key`, { maxLength: 128 });
|
|
685
|
+
if (key !== void 0 && !isSafeFieldPath(key)) {
|
|
686
|
+
context.issue("invalid_value", `${path}.key`, "Field key contains a forbidden path segment");
|
|
687
|
+
}
|
|
688
|
+
const label = readString(context, object.label, `${path}.label`);
|
|
689
|
+
const typeValue = readString(context, object.type, `${path}.type`, { maxLength: 32 });
|
|
690
|
+
const type = typeValue && TARGET_FIELD_TYPE_SET.has(typeValue) ? typeValue : void 0;
|
|
691
|
+
if (typeValue !== void 0 && type === void 0) {
|
|
692
|
+
context.issue("invalid_value", `${path}.type`, `Unsupported target field type ${JSON.stringify(typeValue)}`);
|
|
693
|
+
}
|
|
694
|
+
if (key === void 0 || label === void 0 || type === void 0) return void 0;
|
|
695
|
+
const field = { key, label, type };
|
|
696
|
+
if (object.required !== void 0) {
|
|
697
|
+
const required = readBoolean(context, object.required, `${path}.required`);
|
|
698
|
+
if (required !== void 0) field.required = required;
|
|
699
|
+
}
|
|
700
|
+
if (object.aliases !== void 0) {
|
|
701
|
+
if (!Array.isArray(object.aliases)) {
|
|
702
|
+
context.issue("invalid_type", `${path}.aliases`, "Expected a string array");
|
|
703
|
+
} else {
|
|
704
|
+
if (object.aliases.length > context.limits.maxAliasesPerField) {
|
|
705
|
+
context.issue("limit_exceeded", `${path}.aliases`, "Field has too many aliases");
|
|
706
|
+
}
|
|
707
|
+
const aliases = object.aliases.map((entry, index) => readString(context, entry, `${path}.aliases[${index}]`)).filter((entry) => entry !== void 0);
|
|
708
|
+
assertUnique(context, aliases, `${path}.aliases`);
|
|
709
|
+
field.aliases = aliases;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (object.description !== void 0) {
|
|
713
|
+
const description = readString(context, object.description, `${path}.description`, {
|
|
714
|
+
maxLength: context.limits.maxDescriptionLength
|
|
715
|
+
});
|
|
716
|
+
if (description !== void 0) field.description = description;
|
|
717
|
+
}
|
|
718
|
+
if (Object.hasOwn(object, "defaultValue")) {
|
|
719
|
+
const defaultValue = readJsonValue(context, object.defaultValue, `${path}.defaultValue`);
|
|
720
|
+
if (defaultValue !== void 0) {
|
|
721
|
+
field.defaultValue = defaultValue;
|
|
722
|
+
if (!valueMatchesType(defaultValue, type)) {
|
|
723
|
+
context.issue("invalid_value", `${path}.defaultValue`, `Default value is incompatible with ${type}`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const constraints = parseConstraints(context, object.constraints, `${path}.constraints`, type);
|
|
728
|
+
if (constraints !== void 0) field.constraints = constraints;
|
|
729
|
+
if (type === "enum" && typeof field.defaultValue === "string" && constraints?.values && !constraints.values.includes(field.defaultValue)) {
|
|
730
|
+
context.issue("invalid_value", `${path}.defaultValue`, "Enum default is not in constraints.values");
|
|
731
|
+
}
|
|
732
|
+
return field;
|
|
733
|
+
}
|
|
734
|
+
function parseTargetSchemaV1(input, options = {}) {
|
|
735
|
+
const context = new ValidationContext(resolveContractLimits(options.limits));
|
|
736
|
+
const object = readStrictObject(context, input, "$", ["version", "id", "name", "fields", "unknownFields"]);
|
|
737
|
+
if (!object) return context.finish("TargetSchemaV1", void 0);
|
|
738
|
+
if (object.version !== 1) context.issue("invalid_value", "$.version", "Expected version 1");
|
|
739
|
+
const id = readString(context, object.id, "$.id", { maxLength: 128, pattern: CONTRACT_ID_PATTERN });
|
|
740
|
+
const name = readString(context, object.name, "$.name");
|
|
741
|
+
const unknownFieldsValue = readString(context, object.unknownFields, "$.unknownFields", { maxLength: 16 });
|
|
742
|
+
const unknownFields = unknownFieldsValue && UNKNOWN_FIELD_POLICIES.has(unknownFieldsValue) ? unknownFieldsValue : void 0;
|
|
743
|
+
if (unknownFieldsValue !== void 0 && unknownFields === void 0) {
|
|
744
|
+
context.issue("invalid_value", "$.unknownFields", "Expected ignore, reject, or preserve");
|
|
745
|
+
}
|
|
746
|
+
let fields;
|
|
747
|
+
if (!Array.isArray(object.fields) || object.fields.length === 0) {
|
|
748
|
+
context.issue("invalid_type", "$.fields", "Expected a non-empty field array");
|
|
749
|
+
} else {
|
|
750
|
+
if (object.fields.length > context.limits.maxFields) {
|
|
751
|
+
context.issue("limit_exceeded", "$.fields", `Schema exceeds ${context.limits.maxFields} fields`);
|
|
752
|
+
}
|
|
753
|
+
fields = object.fields.map((entry, index) => parseField(context, entry, `$.fields[${index}]`)).filter((entry) => entry !== void 0);
|
|
754
|
+
assertUnique(context, fields.map((field) => field.key), "$.fields.*.key");
|
|
755
|
+
}
|
|
756
|
+
if (id === void 0 || name === void 0 || unknownFields === void 0 || fields === void 0) {
|
|
757
|
+
return context.finish("TargetSchemaV1", void 0);
|
|
758
|
+
}
|
|
759
|
+
const schema = { version: 1, id, name, fields, unknownFields };
|
|
760
|
+
assertCanonicalSize(context, schema, "$");
|
|
761
|
+
return context.finish("TargetSchemaV1", cloneContract(schema));
|
|
762
|
+
}
|
|
763
|
+
function isTargetValueCompatible(value, type) {
|
|
764
|
+
return valueMatchesType(value, type);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// src/schema-builder.ts
|
|
768
|
+
var TargetSchemaBuilderV1 = class {
|
|
769
|
+
constructor(options) {
|
|
770
|
+
this.options = options;
|
|
771
|
+
}
|
|
772
|
+
options;
|
|
773
|
+
#fields = [];
|
|
774
|
+
field(field) {
|
|
775
|
+
this.#fields.push(structuredClone(field));
|
|
776
|
+
return this;
|
|
777
|
+
}
|
|
778
|
+
fields(fields) {
|
|
779
|
+
for (const field of fields) this.field(field);
|
|
780
|
+
return this;
|
|
781
|
+
}
|
|
782
|
+
build() {
|
|
783
|
+
return Object.freeze(parseTargetSchemaV1({
|
|
784
|
+
version: 1,
|
|
785
|
+
id: this.options.id,
|
|
786
|
+
name: this.options.name,
|
|
787
|
+
unknownFields: this.options.unknownFields ?? "ignore",
|
|
788
|
+
fields: this.#fields
|
|
789
|
+
}));
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
function defineTargetSchemaV1(schema) {
|
|
793
|
+
return Object.freeze(parseTargetSchemaV1(schema));
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/transform-plan.ts
|
|
797
|
+
var TARGET_TYPE_SET = new Set(TARGET_FIELD_TYPES.filter((type) => type !== "enum"));
|
|
798
|
+
var REASON_CODE_PATTERN = /^[a-z][a-z0-9_.-]{0,63}$/u;
|
|
799
|
+
var WARNING_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/u;
|
|
800
|
+
function operationStage(operation) {
|
|
801
|
+
switch (operation.op) {
|
|
802
|
+
case "trim":
|
|
803
|
+
case "lowercase":
|
|
804
|
+
case "uppercase":
|
|
805
|
+
case "replace":
|
|
806
|
+
case "enum_map":
|
|
807
|
+
return 0;
|
|
808
|
+
case "split":
|
|
809
|
+
case "join":
|
|
810
|
+
return 1;
|
|
811
|
+
case "coerce":
|
|
812
|
+
case "parse_date":
|
|
813
|
+
return 2;
|
|
814
|
+
case "default":
|
|
815
|
+
return 3;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
function parseStringArray(context, value, path, maximum) {
|
|
819
|
+
if (!Array.isArray(value)) {
|
|
820
|
+
context.issue("invalid_type", path, "Expected a string array");
|
|
821
|
+
return void 0;
|
|
822
|
+
}
|
|
823
|
+
if (value.length > maximum) context.issue("limit_exceeded", path, `Array exceeds ${maximum} entries`);
|
|
824
|
+
const result = value.map((entry, index) => readString(context, entry, `${path}[${index}]`)).filter((entry) => entry !== void 0);
|
|
825
|
+
assertUnique(context, result, path);
|
|
826
|
+
return result;
|
|
827
|
+
}
|
|
828
|
+
function parseOperation(context, value, path, targetField) {
|
|
829
|
+
if (!isPlainRecord2(value)) {
|
|
830
|
+
context.issue("invalid_type", path, "Expected an operation object");
|
|
831
|
+
return void 0;
|
|
832
|
+
}
|
|
833
|
+
const op = readString(context, value.op, `${path}.op`, { maxLength: 32 });
|
|
834
|
+
if (!op) return void 0;
|
|
835
|
+
switch (op) {
|
|
836
|
+
case "trim":
|
|
837
|
+
case "lowercase":
|
|
838
|
+
case "uppercase": {
|
|
839
|
+
readStrictObject(context, value, path, ["op"]);
|
|
840
|
+
if (targetField.type !== "string" && targetField.type !== "enum") {
|
|
841
|
+
context.issue("incompatible_operation", path, `${op} is incompatible with ${targetField.type}`);
|
|
842
|
+
}
|
|
843
|
+
return { op };
|
|
844
|
+
}
|
|
845
|
+
case "coerce": {
|
|
846
|
+
const object = readStrictObject(context, value, path, ["op", "type"]);
|
|
847
|
+
if (!object) return void 0;
|
|
848
|
+
const typeValue = readString(context, object.type, `${path}.type`, { maxLength: 16 });
|
|
849
|
+
const type = typeValue && TARGET_TYPE_SET.has(typeValue) ? typeValue : void 0;
|
|
850
|
+
if (typeValue !== void 0 && type === void 0) {
|
|
851
|
+
context.issue("invalid_value", `${path}.type`, `Unsupported coercion type ${JSON.stringify(typeValue)}`);
|
|
852
|
+
}
|
|
853
|
+
const expected = targetField.type === "enum" ? "string" : targetField.type;
|
|
854
|
+
if (type !== void 0 && type !== expected) {
|
|
855
|
+
context.issue("incompatible_operation", path, `Coercion to ${type} cannot produce target type ${targetField.type}`);
|
|
856
|
+
}
|
|
857
|
+
return type === void 0 ? void 0 : { op: "coerce", type };
|
|
858
|
+
}
|
|
859
|
+
case "parse_date": {
|
|
860
|
+
const object = readStrictObject(context, value, path, ["op", "formats"]);
|
|
861
|
+
if (!object) return void 0;
|
|
862
|
+
const formats = parseStringArray(context, object.formats, `${path}.formats`, 12);
|
|
863
|
+
if (formats?.length === 0) context.issue("invalid_value", `${path}.formats`, "At least one date format is required");
|
|
864
|
+
if (targetField.type !== "date" && targetField.type !== "datetime") {
|
|
865
|
+
context.issue("incompatible_operation", path, `parse_date is incompatible with ${targetField.type}`);
|
|
866
|
+
}
|
|
867
|
+
return formats ? { op: "parse_date", formats } : void 0;
|
|
868
|
+
}
|
|
869
|
+
case "replace": {
|
|
870
|
+
const object = readStrictObject(context, value, path, ["op", "pattern", "replacement"]);
|
|
871
|
+
if (!object) return void 0;
|
|
872
|
+
const pattern = readString(context, object.pattern, `${path}.pattern`, {
|
|
873
|
+
maxLength: context.limits.maxPatternLength
|
|
874
|
+
});
|
|
875
|
+
const replacement = readString(context, object.replacement, `${path}.replacement`, {
|
|
876
|
+
allowEmpty: true,
|
|
877
|
+
maxLength: context.limits.maxStringLength
|
|
878
|
+
});
|
|
879
|
+
if (pattern !== void 0) validateSafePattern(context, pattern, `${path}.pattern`);
|
|
880
|
+
if (targetField.type !== "string" && targetField.type !== "enum") {
|
|
881
|
+
context.issue("incompatible_operation", path, `replace is incompatible with ${targetField.type}`);
|
|
882
|
+
}
|
|
883
|
+
return pattern !== void 0 && replacement !== void 0 ? { op: "replace", pattern, replacement } : void 0;
|
|
884
|
+
}
|
|
885
|
+
case "enum_map": {
|
|
886
|
+
const object = readStrictObject(context, value, path, ["op", "values"]);
|
|
887
|
+
if (!object) return void 0;
|
|
888
|
+
if (!isPlainRecord2(object.values)) {
|
|
889
|
+
context.issue("invalid_type", `${path}.values`, "Expected a string dictionary");
|
|
890
|
+
return void 0;
|
|
891
|
+
}
|
|
892
|
+
const entries = Object.entries(object.values);
|
|
893
|
+
if (entries.length > context.limits.maxDictionaryEntries) {
|
|
894
|
+
context.issue("limit_exceeded", `${path}.values`, "Enum map has too many entries");
|
|
895
|
+
}
|
|
896
|
+
const values = /* @__PURE__ */ Object.create(null);
|
|
897
|
+
for (const [key, entry] of entries) {
|
|
898
|
+
const parsedKey = readString(context, key, `${path}.values.${key}.key`);
|
|
899
|
+
const parsedValue = readString(context, entry, `${path}.values.${key}`);
|
|
900
|
+
if (parsedKey !== void 0 && parsedValue !== void 0) values[parsedKey] = parsedValue;
|
|
901
|
+
if (parsedValue !== void 0 && targetField.type === "enum" && targetField.constraints?.values && !targetField.constraints.values.includes(parsedValue)) {
|
|
902
|
+
context.issue("incompatible_operation", `${path}.values.${key}`, "Mapped value is not allowed by the target enum");
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (targetField.type !== "enum" && targetField.type !== "string") {
|
|
906
|
+
context.issue("incompatible_operation", path, `enum_map is incompatible with ${targetField.type}`);
|
|
907
|
+
}
|
|
908
|
+
return { op: "enum_map", values };
|
|
909
|
+
}
|
|
910
|
+
case "split": {
|
|
911
|
+
const object = readStrictObject(context, value, path, ["op", "separator", "index"]);
|
|
912
|
+
if (!object) return void 0;
|
|
913
|
+
const separator = readString(context, object.separator, `${path}.separator`, { maxLength: 32 });
|
|
914
|
+
const index = readFiniteNumber(context, object.index, `${path}.index`, { min: 0, max: 1024, integer: true });
|
|
915
|
+
return separator !== void 0 && index !== void 0 ? { op: "split", separator, index } : void 0;
|
|
916
|
+
}
|
|
917
|
+
case "join": {
|
|
918
|
+
const object = readStrictObject(context, value, path, ["op", "separator"]);
|
|
919
|
+
if (!object) return void 0;
|
|
920
|
+
const separator = readString(context, object.separator, `${path}.separator`, {
|
|
921
|
+
allowEmpty: true,
|
|
922
|
+
maxLength: 32
|
|
923
|
+
});
|
|
924
|
+
return separator === void 0 ? void 0 : { op: "join", separator };
|
|
925
|
+
}
|
|
926
|
+
case "default": {
|
|
927
|
+
const object = readStrictObject(context, value, path, ["op", "value"]);
|
|
928
|
+
if (!object) return void 0;
|
|
929
|
+
const defaultValue = readJsonValue(context, object.value, `${path}.value`);
|
|
930
|
+
if (defaultValue !== void 0 && !isTargetValueCompatible(defaultValue, targetField.type)) {
|
|
931
|
+
context.issue("incompatible_operation", `${path}.value`, `Default is incompatible with ${targetField.type}`);
|
|
932
|
+
}
|
|
933
|
+
return defaultValue === void 0 ? void 0 : { op: "default", value: defaultValue };
|
|
934
|
+
}
|
|
935
|
+
default:
|
|
936
|
+
context.issue("invalid_value", `${path}.op`, `Unknown transform operation ${JSON.stringify(op)}`);
|
|
937
|
+
return void 0;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
function validateOperationSequence(context, operations, sources, path) {
|
|
941
|
+
let previousStage = 0;
|
|
942
|
+
const counts = /* @__PURE__ */ new Map();
|
|
943
|
+
operations.forEach((operation, index) => {
|
|
944
|
+
const stage = operationStage(operation);
|
|
945
|
+
if (stage < previousStage) {
|
|
946
|
+
context.issue("incompatible_operation", `${path}[${index}]`, "Operations are not in deterministic execution order");
|
|
947
|
+
}
|
|
948
|
+
previousStage = stage;
|
|
949
|
+
counts.set(operation.op, (counts.get(operation.op) ?? 0) + 1);
|
|
950
|
+
});
|
|
951
|
+
for (const singleton of ["split", "join", "coerce", "parse_date", "default"]) {
|
|
952
|
+
if ((counts.get(singleton) ?? 0) > 1) {
|
|
953
|
+
context.issue("incompatible_operation", path, `${singleton} may appear at most once`);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
if ((counts.get("split") ?? 0) > 0 && sources.length !== 1) {
|
|
957
|
+
context.issue("incompatible_operation", path, "split requires exactly one source");
|
|
958
|
+
}
|
|
959
|
+
if ((counts.get("join") ?? 0) > 0 && sources.length < 2) {
|
|
960
|
+
context.issue("incompatible_operation", path, "join requires at least two sources");
|
|
961
|
+
}
|
|
962
|
+
if ((counts.get("coerce") ?? 0) > 0 && (counts.get("parse_date") ?? 0) > 0) {
|
|
963
|
+
context.issue("incompatible_operation", path, "coerce and parse_date cannot be combined");
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
function parseMapping(context, value, path, sourcePaths, targetFields) {
|
|
967
|
+
const object = readStrictObject(context, value, path, ["sources", "target", "confidence", "reasonCode", "operations"]);
|
|
968
|
+
if (!object) return void 0;
|
|
969
|
+
const sources = parseStringArray(context, object.sources, `${path}.sources`, 16);
|
|
970
|
+
if (sources?.length === 0) context.issue("invalid_value", `${path}.sources`, "At least one source is required");
|
|
971
|
+
sources?.forEach((source, index) => {
|
|
972
|
+
if (!sourcePaths.has(source)) {
|
|
973
|
+
context.issue("unknown_source", `${path}.sources[${index}]`, `Unknown source path ${JSON.stringify(source)}`);
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
const target = readString(context, object.target, `${path}.target`, { maxLength: 128 });
|
|
977
|
+
const targetField = target ? targetFields.get(target) : void 0;
|
|
978
|
+
if (target !== void 0 && !targetField) {
|
|
979
|
+
context.issue("unknown_target", `${path}.target`, `Unknown target field ${JSON.stringify(target)}`);
|
|
980
|
+
}
|
|
981
|
+
const confidence = readFiniteNumber(context, object.confidence, `${path}.confidence`, { min: 0, max: 1 });
|
|
982
|
+
const reasonCode = readString(context, object.reasonCode, `${path}.reasonCode`, {
|
|
983
|
+
maxLength: 64,
|
|
984
|
+
pattern: REASON_CODE_PATTERN
|
|
985
|
+
});
|
|
986
|
+
let operations;
|
|
987
|
+
if (!Array.isArray(object.operations)) {
|
|
988
|
+
context.issue("invalid_type", `${path}.operations`, "Expected an operation array");
|
|
989
|
+
} else if (targetField) {
|
|
990
|
+
if (object.operations.length > context.limits.maxOperationsPerMapping) {
|
|
991
|
+
context.issue("limit_exceeded", `${path}.operations`, "Mapping has too many operations");
|
|
992
|
+
}
|
|
993
|
+
operations = object.operations.map((entry, index) => parseOperation(context, entry, `${path}.operations[${index}]`, targetField)).filter((entry) => entry !== void 0);
|
|
994
|
+
if (sources) validateOperationSequence(context, operations, sources, `${path}.operations`);
|
|
995
|
+
}
|
|
996
|
+
return sources && target && targetField && confidence !== void 0 && reasonCode && operations ? { sources, target, confidence, reasonCode, operations } : void 0;
|
|
997
|
+
}
|
|
998
|
+
function parseWarning(context, value, path) {
|
|
999
|
+
const object = readStrictObject(context, value, path, ["code", "message"]);
|
|
1000
|
+
if (!object) return void 0;
|
|
1001
|
+
const code = readString(context, object.code, `${path}.code`, { maxLength: 64, pattern: WARNING_CODE_PATTERN });
|
|
1002
|
+
const message = readString(context, object.message, `${path}.message`, {
|
|
1003
|
+
maxLength: context.limits.maxDescriptionLength
|
|
1004
|
+
});
|
|
1005
|
+
return code && message ? { code, message } : void 0;
|
|
1006
|
+
}
|
|
1007
|
+
function parseTransformPlanV1(input, planContext, options = {}) {
|
|
1008
|
+
const context = new ValidationContext(resolveContractLimits(options.limits));
|
|
1009
|
+
const object = readStrictObject(
|
|
1010
|
+
context,
|
|
1011
|
+
input,
|
|
1012
|
+
"$",
|
|
1013
|
+
["version", "sourceFingerprint", "targetSchemaVersionId", "mappings", "unmappedSources", "unsatisfiedTargets", "warnings"]
|
|
1014
|
+
);
|
|
1015
|
+
if (!object) return context.finish("TransformPlanV1", void 0);
|
|
1016
|
+
if (object.version !== 1) context.issue("invalid_value", "$.version", "Expected version 1");
|
|
1017
|
+
const sourceFingerprint = readString(context, object.sourceFingerprint, "$.sourceFingerprint", { maxLength: 64 });
|
|
1018
|
+
if (sourceFingerprint !== void 0 && sourceFingerprint !== planContext.sourceProfile.structureFingerprint) {
|
|
1019
|
+
context.issue("fingerprint_mismatch", "$.sourceFingerprint", "Plan fingerprint does not match the source profile");
|
|
1020
|
+
}
|
|
1021
|
+
const targetSchemaVersionId = readString(context, object.targetSchemaVersionId, "$.targetSchemaVersionId", {
|
|
1022
|
+
maxLength: 128
|
|
1023
|
+
});
|
|
1024
|
+
if (targetSchemaVersionId !== void 0 && targetSchemaVersionId !== planContext.targetSchemaVersionId) {
|
|
1025
|
+
context.issue("schema_version_mismatch", "$.targetSchemaVersionId", "Plan schema version does not match the requested version");
|
|
1026
|
+
}
|
|
1027
|
+
const sourcePaths = new Set(planContext.sourceProfile.fields.map((field) => field.path));
|
|
1028
|
+
const targetFields = new Map(planContext.targetSchema.fields.map((field) => [field.key, field]));
|
|
1029
|
+
let mappings;
|
|
1030
|
+
if (!Array.isArray(object.mappings)) {
|
|
1031
|
+
context.issue("invalid_type", "$.mappings", "Expected a mapping array");
|
|
1032
|
+
} else {
|
|
1033
|
+
if (object.mappings.length > context.limits.maxMappings) {
|
|
1034
|
+
context.issue("limit_exceeded", "$.mappings", "Plan has too many mappings");
|
|
1035
|
+
}
|
|
1036
|
+
mappings = object.mappings.map((entry, index) => parseMapping(context, entry, `$.mappings[${index}]`, sourcePaths, targetFields)).filter((entry) => entry !== void 0);
|
|
1037
|
+
assertUnique(context, mappings.map((mapping) => mapping.target), "$.mappings.*.target");
|
|
1038
|
+
}
|
|
1039
|
+
const unmappedSources = parseStringArray(context, object.unmappedSources, "$.unmappedSources", context.limits.maxFields);
|
|
1040
|
+
unmappedSources?.forEach((source, index) => {
|
|
1041
|
+
if (!sourcePaths.has(source)) {
|
|
1042
|
+
context.issue("unknown_source", `$.unmappedSources[${index}]`, `Unknown source path ${JSON.stringify(source)}`);
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
const unsatisfiedTargets = parseStringArray(
|
|
1046
|
+
context,
|
|
1047
|
+
object.unsatisfiedTargets,
|
|
1048
|
+
"$.unsatisfiedTargets",
|
|
1049
|
+
context.limits.maxFields
|
|
1050
|
+
);
|
|
1051
|
+
unsatisfiedTargets?.forEach((target, index) => {
|
|
1052
|
+
if (!targetFields.has(target)) {
|
|
1053
|
+
context.issue("unknown_target", `$.unsatisfiedTargets[${index}]`, `Unknown target field ${JSON.stringify(target)}`);
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
let warnings;
|
|
1057
|
+
if (!Array.isArray(object.warnings)) {
|
|
1058
|
+
context.issue("invalid_type", "$.warnings", "Expected a warning array");
|
|
1059
|
+
} else {
|
|
1060
|
+
if (object.warnings.length > context.limits.maxWarnings) {
|
|
1061
|
+
context.issue("limit_exceeded", "$.warnings", "Plan has too many warnings");
|
|
1062
|
+
}
|
|
1063
|
+
warnings = object.warnings.map((entry, index) => parseWarning(context, entry, `$.warnings[${index}]`)).filter((entry) => entry !== void 0);
|
|
1064
|
+
}
|
|
1065
|
+
if (mappings && unmappedSources) {
|
|
1066
|
+
const mappedSources = new Set(mappings.flatMap((mapping) => mapping.sources));
|
|
1067
|
+
unmappedSources.forEach((source, index) => {
|
|
1068
|
+
if (mappedSources.has(source)) {
|
|
1069
|
+
context.issue("invalid_value", `$.unmappedSources[${index}]`, "A source cannot be both mapped and unmapped");
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
if (mappings && unsatisfiedTargets) {
|
|
1074
|
+
const mappedTargets = new Set(mappings.map((mapping) => mapping.target));
|
|
1075
|
+
unsatisfiedTargets.forEach((target, index) => {
|
|
1076
|
+
if (mappedTargets.has(target)) {
|
|
1077
|
+
context.issue("invalid_value", `$.unsatisfiedTargets[${index}]`, "A target cannot be both mapped and unsatisfied");
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
if (sourceFingerprint === void 0 || targetSchemaVersionId === void 0 || mappings === void 0 || unmappedSources === void 0 || unsatisfiedTargets === void 0 || warnings === void 0) {
|
|
1082
|
+
return context.finish("TransformPlanV1", void 0);
|
|
1083
|
+
}
|
|
1084
|
+
const plan = {
|
|
1085
|
+
version: 1,
|
|
1086
|
+
sourceFingerprint,
|
|
1087
|
+
targetSchemaVersionId,
|
|
1088
|
+
mappings,
|
|
1089
|
+
unmappedSources,
|
|
1090
|
+
unsatisfiedTargets,
|
|
1091
|
+
warnings
|
|
1092
|
+
};
|
|
1093
|
+
assertCanonicalSize(context, plan, "$");
|
|
1094
|
+
return context.finish("TransformPlanV1", cloneContract(plan));
|
|
1095
|
+
}
|
|
1096
|
+
export {
|
|
1097
|
+
ContractValidationError,
|
|
1098
|
+
DEFAULT_CONTRACT_LIMITS,
|
|
1099
|
+
DEFAULT_DELIVERY_SIGNATURE_TOLERANCE_SECONDS,
|
|
1100
|
+
DEFAULT_MAX_DELIVERY_BATCH_BYTES,
|
|
1101
|
+
DEFAULT_MAX_DELIVERY_BATCH_RECORDS,
|
|
1102
|
+
DELIVERY_HEADERS,
|
|
1103
|
+
DELIVERY_PROTOCOL_VERSION,
|
|
1104
|
+
SOURCE_FORMATS,
|
|
1105
|
+
SOURCE_INFERRED_TYPES,
|
|
1106
|
+
TARGET_FIELD_TYPES,
|
|
1107
|
+
TargetSchemaBuilderV1,
|
|
1108
|
+
canonicalByteLength,
|
|
1109
|
+
canonicalStringify,
|
|
1110
|
+
cloneJsonValue,
|
|
1111
|
+
computeStructureFingerprint,
|
|
1112
|
+
defineTargetSchemaV1,
|
|
1113
|
+
describeSourceStructure,
|
|
1114
|
+
isCalendarDate,
|
|
1115
|
+
isOffsetDateTime,
|
|
1116
|
+
isTargetValueCompatible,
|
|
1117
|
+
parseAcceptedRecordBatchV1,
|
|
1118
|
+
parseCalendarFormats,
|
|
1119
|
+
parseSourceProfileV1,
|
|
1120
|
+
parseTargetSchemaV1,
|
|
1121
|
+
parseTransformPlanV1,
|
|
1122
|
+
resolveContractLimits,
|
|
1123
|
+
signDeliveryRequestV1,
|
|
1124
|
+
verifyDeliverySignatureV1
|
|
1125
|
+
};
|