@sonordev/contracts 2.17.0 → 2.20.0
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 +40 -0
- package/README.md +514 -1
- package/dist/broadcast-layouts.d.ts +8 -0
- package/dist/broadcast-layouts.js +4 -0
- package/dist/broadcast-layouts.mjs +3 -1
- package/dist/decision-roles.d.ts +39 -0
- package/dist/decision-roles.js +31 -0
- package/dist/decision-roles.mjs +28 -0
- package/dist/funnels.d.ts +46 -0
- package/dist/funnels.js +35 -4
- package/dist/funnels.mjs +33 -5
- package/dist/resonance-constants.d.ts +4 -0
- package/dist/resonance-constants.js +10 -0
- package/dist/resonance-constants.mjs +6 -0
- package/dist/resonance.d.ts +6962 -0
- package/dist/resonance.js +1809 -0
- package/dist/resonance.mjs +1650 -0
- package/dist/signal-experiments.d.ts +219 -0
- package/dist/signal-experiments.js +270 -0
- package/dist/signal-experiments.mjs +260 -0
- package/dist/wire-schema.d.ts +36 -0
- package/docs.json +21 -0
- package/package.json +24 -3
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// src/contracts/wire-schema.ts
|
|
2
|
+
function createWireSchema(fail2, isValidationError) {
|
|
3
|
+
const own = (v, k) => Object.prototype.hasOwnProperty.call(v, k);
|
|
4
|
+
const field2 = (schema, read) => ({ schema, read });
|
|
5
|
+
const text2 = (max = 200, pattern, min = 1) => field2({ type: "string", minLength: min, maxLength: max, ...pattern ? { pattern: pattern.source } : {} }, (v, p) => typeof v === "string" && v.length >= min && v.length <= max && (!pattern || pattern.test(v)) ? v : fail2(p, "invalid string"));
|
|
6
|
+
const choice2 = (...values) => field2({ type: "string", enum: values }, (v, p) => typeof v === "string" && values.includes(v) ? v : fail2(p, "unsupported value"));
|
|
7
|
+
const literal2 = (value) => field2({ const: value }, (v, p) => v === value ? value : fail2(p, "unexpected value"));
|
|
8
|
+
const integer2 = (min = 0, max = Number.MAX_SAFE_INTEGER) => field2({ type: "integer", minimum: min, maximum: max }, (v, p) => typeof v === "number" && Number.isSafeInteger(v) && v >= min && v <= max ? v : fail2(p, "invalid integer"));
|
|
9
|
+
const bool = field2({ type: "boolean" }, (v, p) => typeof v === "boolean" ? v : fail2(p, "must be boolean"));
|
|
10
|
+
const optional2 = (f) => ({ ...f, optional: true });
|
|
11
|
+
const nullable2 = (f) => field2({ anyOf: [f.schema, { type: "null" }] }, (v, p) => v === null ? null : f.read(v, p));
|
|
12
|
+
const list2 = (f, max = 100, min = 0) => field2({ type: "array", items: f.schema, minItems: min, maxItems: max }, (v, p) => {
|
|
13
|
+
if (!Array.isArray(v) || v.length < min || v.length > max) fail2(p, "invalid array");
|
|
14
|
+
return v.map((x, i) => f.read(x, `${p}[${i}]`));
|
|
15
|
+
});
|
|
16
|
+
const object2 = (fields2) => field2({ type: "object", additionalProperties: false, properties: Object.fromEntries(Object.entries(fields2).map(([k, f]) => [k, f.schema])), required: Object.keys(fields2).filter((k) => !("optional" in fields2[k])) }, (v, p) => {
|
|
17
|
+
if (!v || typeof v !== "object" || Array.isArray(v) || ![Object.prototype, null].includes(Object.getPrototypeOf(v))) fail2(p, "must be a plain object");
|
|
18
|
+
const source = v, result = {};
|
|
19
|
+
for (const key of Reflect.ownKeys(source)) if (typeof key !== "string" || !own(fields2, key)) fail2(p, "unknown field");
|
|
20
|
+
for (const [key, f] of Object.entries(fields2)) {
|
|
21
|
+
if (!own(source, key)) {
|
|
22
|
+
if ("optional" in f) continue;
|
|
23
|
+
fail2(`${p}.${key}`, "required");
|
|
24
|
+
}
|
|
25
|
+
const descriptor = Object.getOwnPropertyDescriptor(source, key);
|
|
26
|
+
if (!own(descriptor, "value")) fail2(`${p}.${key}`, "accessors are not wire data");
|
|
27
|
+
result[key] = f.read(descriptor.value, `${p}.${key}`);
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
});
|
|
31
|
+
const union2 = (...fields2) => field2({ oneOf: fields2.map((f) => f.schema) }, (v, p) => {
|
|
32
|
+
const found = [];
|
|
33
|
+
for (const f of fields2) {
|
|
34
|
+
try {
|
|
35
|
+
found.push(f.read(v, p));
|
|
36
|
+
} catch (e) {
|
|
37
|
+
if (!isValidationError(e)) throw e;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return found.length === 1 ? found[0] : fail2(p, "must match exactly one supported shape");
|
|
41
|
+
});
|
|
42
|
+
const refine2 = (f, check) => field2(f.schema, (v, p) => {
|
|
43
|
+
const result = f.read(v, p);
|
|
44
|
+
check(result, p);
|
|
45
|
+
return result;
|
|
46
|
+
});
|
|
47
|
+
return { field: field2, text: text2, choice: choice2, literal: literal2, integer: integer2, bool, optional: optional2, nullable: nullable2, list: list2, object: object2, union: union2, refine: refine2 };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/contracts/sites-normalize.ts
|
|
51
|
+
function normalizeSiteHost(site) {
|
|
52
|
+
if (!site || typeof site !== "string") return null;
|
|
53
|
+
let s = site.trim().toLowerCase();
|
|
54
|
+
if (!s) return null;
|
|
55
|
+
s = s.replace(/^https?:\/\//, "");
|
|
56
|
+
const slash = s.indexOf("/");
|
|
57
|
+
if (slash >= 0) s = s.slice(0, slash);
|
|
58
|
+
const colon = s.indexOf(":");
|
|
59
|
+
if (colon >= 0) s = s.slice(0, colon);
|
|
60
|
+
s = s.replace(/\.+$/, "");
|
|
61
|
+
return s || null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/contracts/sites.ts
|
|
65
|
+
function isNormalizedSiteHost(value) {
|
|
66
|
+
return typeof value === "string" && value.length > 0 && normalizeSiteHost(value) === value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/contracts/public-url-host.ts
|
|
70
|
+
function isPrivateIpv4(a, b) {
|
|
71
|
+
return a === 127 || a === 10 || a === 0 || a === 192 && b === 168 || a === 172 && b >= 16 && b <= 31 || a === 169 && b === 254 || a === 100 && b >= 64 && b <= 127 || a >= 224;
|
|
72
|
+
}
|
|
73
|
+
function ipv6Words(host) {
|
|
74
|
+
if (!host.includes(":") || !/^[a-f\d:.]+$/.test(host)) return null;
|
|
75
|
+
let canonical;
|
|
76
|
+
try {
|
|
77
|
+
canonical = new URL(`http://[${host}]/`).hostname.slice(1, -1);
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const sides = canonical.split("::");
|
|
82
|
+
const left = sides[0] ? sides[0].split(":") : [];
|
|
83
|
+
const right = sides.length > 1 && sides[1] ? sides[1].split(":") : [];
|
|
84
|
+
return [...left, ...Array(sides.length > 1 ? 8 - left.length - right.length : 0).fill("0"), ...right].map((word) => parseInt(word, 16));
|
|
85
|
+
}
|
|
86
|
+
function isBlockedPublicUrlHost(rawHost) {
|
|
87
|
+
const host = rawHost.toLowerCase().replace(/^\[|\]$/g, "");
|
|
88
|
+
const blockedName = host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal");
|
|
89
|
+
if (host.includes(":") && host.includes("%")) return true;
|
|
90
|
+
const ipv4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
91
|
+
const blockedIp = !!ipv4 && isPrivateIpv4(Number(ipv4[1]), Number(ipv4[2]));
|
|
92
|
+
const words = ipv6Words(host);
|
|
93
|
+
const blockedIpv6 = !!words && // Unspecified/loopback, unique-local, link-local, deprecated site-local
|
|
94
|
+
// and multicast destinations aren't public logo/webhook endpoints.
|
|
95
|
+
(words.every((word) => word === 0) || words.slice(0, 7).every((word) => word === 0) && words[7] === 1 || (words[0] & 65024) === 64512 || (words[0] & 65472) === 65152 || (words[0] & 65472) === 65216 || (words[0] & 65280) === 65280 || // IPv4-mapped and older IPv4-compatible forms carry an IPv4 destination.
|
|
96
|
+
words.slice(0, 5).every((word) => word === 0) && [0, 65535].includes(words[5]) && isPrivateIpv4(words[6] >> 8, words[6] & 255));
|
|
97
|
+
return blockedName || blockedIp || blockedIpv6;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/contracts/signal-experiments.ts
|
|
101
|
+
var SignalExperimentValidationError = class extends Error {
|
|
102
|
+
constructor(path, reason) {
|
|
103
|
+
super(`${path}: ${reason}`);
|
|
104
|
+
this.path = path;
|
|
105
|
+
this.name = "SignalExperimentValidationError";
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
var fail = (path, reason) => {
|
|
109
|
+
throw new SignalExperimentValidationError(path, reason);
|
|
110
|
+
};
|
|
111
|
+
var { field, text, choice, literal, integer, optional, nullable, list, object, union, refine } = createWireSchema(fail, (error) => error instanceof SignalExperimentValidationError);
|
|
112
|
+
var percent = field({ type: "number", minimum: 0, maximum: 100 }, (v, p) => typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= 100 ? v : fail(p, "must be a finite percentage from 0 to 100"));
|
|
113
|
+
var page = refine(text(2048, /^\/(?!\/)[^?#\s\\]*$/), (v, p) => {
|
|
114
|
+
const parsed = new URL(v, "https://example.com");
|
|
115
|
+
if (parsed.pathname !== v) fail(p, "must be a canonical page path");
|
|
116
|
+
});
|
|
117
|
+
var variant = object({ key: text(80), name: text(160), weight: percent, description: optional(text(1e3, void 0, 0)) });
|
|
118
|
+
var variants = refine(list(variant, 50, 2), (v, p) => {
|
|
119
|
+
if (new Set(v.map((x) => x.key)).size !== v.length) fail(p, "variant keys must be unique");
|
|
120
|
+
if (Math.abs(v.reduce((sum, x) => sum + x.weight, 0) - 100) > 0.01) fail(p, "variant weights must sum to 100");
|
|
121
|
+
});
|
|
122
|
+
var targeting = object({ pages: optional(list(page, 100)), segments: optional(list(text(160), 100)), devices: optional(list(choice("desktop", "mobile", "tablet"), 3)), geo: optional(list(text(160), 100)) });
|
|
123
|
+
var fields = { name: text(160), description: text(1e4, void 0, 0), variants, traffic_allocation: percent, targeting_rules: targeting };
|
|
124
|
+
var create = object({ ...fields, description: optional(fields.description), traffic_allocation: optional(fields.traffic_allocation), targeting_rules: optional(fields.targeting_rules) });
|
|
125
|
+
var update = object({ name: optional(fields.name), description: optional(fields.description), variants: optional(fields.variants), traffic_allocation: optional(fields.traffic_allocation), targeting_rules: optional(fields.targeting_rules) });
|
|
126
|
+
var parseSignalExperimentCreate = (v) => create.read(v, "experimentCreate");
|
|
127
|
+
var parseSignalExperimentUpdate = (v) => update.read(v, "experimentUpdate");
|
|
128
|
+
var SIGNAL_EXPERIMENT_CREATE_JSON_SCHEMA = create.schema;
|
|
129
|
+
var SIGNAL_EXPERIMENT_UPDATE_JSON_SCHEMA = update.schema;
|
|
130
|
+
var measurementUuid = text(36, /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
|
|
131
|
+
var measurementRevision = text(128, /^[a-zA-Z0-9._:-]+$/);
|
|
132
|
+
var measurementInstant = refine(text(30, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/), (v, p) => {
|
|
133
|
+
if (!Number.isFinite(Date.parse(v)) || new Date(v).toISOString().replace(".000Z", "Z") !== v.replace(".000Z", "Z")) fail(p, "invalid UTC instant");
|
|
134
|
+
});
|
|
135
|
+
var measurementSite = refine(text(253), (v, p) => {
|
|
136
|
+
if (!isNormalizedSiteHost(v) || isBlockedPublicUrlHost(v)) fail(p, "must be a normalized public site host");
|
|
137
|
+
});
|
|
138
|
+
var measurementCount = text(30, /^(?:0|[1-9][0-9]*)$/);
|
|
139
|
+
var measurementScope = object({
|
|
140
|
+
projectId: measurementUuid,
|
|
141
|
+
experimentId: measurementUuid,
|
|
142
|
+
site: measurementSite,
|
|
143
|
+
experimentRevision: integer(1),
|
|
144
|
+
goalRevision: measurementRevision,
|
|
145
|
+
campaignId: measurementUuid,
|
|
146
|
+
campaignVersionId: measurementUuid,
|
|
147
|
+
assignedFrom: measurementInstant,
|
|
148
|
+
assignedTo: measurementInstant
|
|
149
|
+
});
|
|
150
|
+
var assignmentCounts = {
|
|
151
|
+
variantKey: text(80),
|
|
152
|
+
assignedUnits: measurementCount,
|
|
153
|
+
exposureEvents: measurementCount,
|
|
154
|
+
exposedUnits: measurementCount,
|
|
155
|
+
matureAssignedUnits: measurementCount,
|
|
156
|
+
immatureAssignedUnits: measurementCount,
|
|
157
|
+
assignmentsWithoutExposure: measurementCount
|
|
158
|
+
};
|
|
159
|
+
var availableVariant = object({ ...assignmentCounts, acceptedFormEvents: measurementCount, acceptedFormUnits: measurementCount, matureAcceptedFormUnits: measurementCount });
|
|
160
|
+
var unsupportedVariant = object({ ...assignmentCounts, acceptedFormEvents: literal(null), acceptedFormUnits: literal(null), matureAcceptedFormUnits: literal(null) });
|
|
161
|
+
var businessMetrics = object({
|
|
162
|
+
uniqueQualifiedContacts: object({ state: literal("unavailable"), reasonCode: literal("original_qualified_lineage_not_bound") }),
|
|
163
|
+
wonValue: object({ state: literal("unavailable"), reasonCode: literal("original_deal_lineage_not_bound") }),
|
|
164
|
+
paidRevenue: object({ state: literal("unavailable"), reasonCode: literal("original_paid_source_lineage_not_bound") })
|
|
165
|
+
});
|
|
166
|
+
var goalCountingUnit = choice("unique_source", "unique_qualified_contact", "net_order");
|
|
167
|
+
var originalGoalFields = { sourceId: measurementUuid, sourceRevision: measurementRevision, countingUnit: goalCountingUnit };
|
|
168
|
+
var formSourceKind = choice("accepted_form", "qualified_lead");
|
|
169
|
+
var unsupportedSourceKind = choice("completed_booking", "net_paid_order", "approved_application");
|
|
170
|
+
var observedFields = {
|
|
171
|
+
schemaVersion: literal(1),
|
|
172
|
+
mode: literal("trusted_source"),
|
|
173
|
+
state: literal("observed"),
|
|
174
|
+
observedAt: measurementInstant,
|
|
175
|
+
scope: measurementScope,
|
|
176
|
+
countingUnit: literal("consent_subject"),
|
|
177
|
+
coverage: literal("assigned_population_only"),
|
|
178
|
+
semanticRole: literal("accepted_form_diagnostic"),
|
|
179
|
+
businessMetrics
|
|
180
|
+
};
|
|
181
|
+
var observedMeasurement = refine(union(
|
|
182
|
+
object({
|
|
183
|
+
...observedFields,
|
|
184
|
+
originalGoal: object({ ...originalGoalFields, sourceKind: formSourceKind }),
|
|
185
|
+
diagnosticType: literal("accepted_form"),
|
|
186
|
+
diagnosticState: literal("available"),
|
|
187
|
+
variants: list(availableVariant, 50)
|
|
188
|
+
}),
|
|
189
|
+
object({
|
|
190
|
+
...observedFields,
|
|
191
|
+
originalGoal: object({ ...originalGoalFields, sourceKind: unsupportedSourceKind }),
|
|
192
|
+
diagnosticType: literal(null),
|
|
193
|
+
diagnosticState: literal("unsupported_original_source"),
|
|
194
|
+
variants: list(unsupportedVariant, 50)
|
|
195
|
+
})
|
|
196
|
+
), (value, p) => {
|
|
197
|
+
if (Date.parse(value.scope.assignedFrom) >= Date.parse(value.scope.assignedTo) || Date.parse(value.scope.assignedTo) > Date.parse(value.observedAt))
|
|
198
|
+
fail(p + ".scope", "requires an increasing assignment cohort ending by the native observation");
|
|
199
|
+
if (new Set(value.variants.map((v) => v.variantKey)).size !== value.variants.length) fail(p + ".variants", "variant keys must be unique");
|
|
200
|
+
for (const [index, v] of value.variants.entries()) {
|
|
201
|
+
const assigned = BigInt(v.assignedUnits), exposed = BigInt(v.exposedUnits), mature = BigInt(v.matureAssignedUnits);
|
|
202
|
+
const at = `${p}.variants[${index}]`;
|
|
203
|
+
if (exposed > assigned || BigInt(v.exposureEvents) < exposed || BigInt(v.assignmentsWithoutExposure) !== assigned - exposed || mature + BigInt(v.immatureAssignedUnits) !== assigned)
|
|
204
|
+
fail(at, "assignment, exposure and maturity counts are inconsistent");
|
|
205
|
+
if (v.acceptedFormUnits !== null && v.acceptedFormEvents !== null && v.matureAcceptedFormUnits !== null) {
|
|
206
|
+
const accepted = BigInt(v.acceptedFormUnits), matureAccepted = BigInt(v.matureAcceptedFormUnits);
|
|
207
|
+
if (accepted > exposed || accepted > BigInt(v.acceptedFormEvents) || matureAccepted > mature || matureAccepted > accepted)
|
|
208
|
+
fail(at, "accepted form units must remain within their exposed, event and mature cohorts");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
var measurement = union(
|
|
213
|
+
object({ schemaVersion: literal(1), mode: literal("legacy"), state: literal("unavailable"), reasonCode: literal("legacy_assignment_outcome_events_only") }),
|
|
214
|
+
object({ schemaVersion: literal(1), mode: literal("trusted_source"), state: literal("unavailable"), reasonCode: literal("measurement_scope_required") }),
|
|
215
|
+
observedMeasurement
|
|
216
|
+
);
|
|
217
|
+
var parseSignalExperimentMeasurement = (v) => measurement.read(v, "experimentMeasurement");
|
|
218
|
+
var SIGNAL_EXPERIMENT_MEASUREMENT_JSON_SCHEMA = measurement.schema;
|
|
219
|
+
var scopeOption = object({
|
|
220
|
+
campaignId: measurementUuid,
|
|
221
|
+
campaignVersionId: measurementUuid,
|
|
222
|
+
campaignDefinitionRevision: integer(1),
|
|
223
|
+
acceptedAt: measurementInstant,
|
|
224
|
+
site: measurementSite,
|
|
225
|
+
experimentId: measurementUuid,
|
|
226
|
+
experimentRevision: integer(1),
|
|
227
|
+
goalRevision: measurementRevision,
|
|
228
|
+
goalLabel: nullable(text(200)),
|
|
229
|
+
assignmentExtent: nullable(object({ firstAssignedAt: measurementInstant, lastAssignedAt: measurementInstant })),
|
|
230
|
+
suggestedCohort: nullable(object({ assignedFrom: measurementInstant, assignedTo: measurementInstant }))
|
|
231
|
+
});
|
|
232
|
+
var scopeOptions = refine(object({ observedAt: measurementInstant, items: list(scopeOption, 100), nextCursor: nullable(text(4096)) }), (value, p) => {
|
|
233
|
+
const observed = Date.parse(value.observedAt), tuples = /* @__PURE__ */ new Set();
|
|
234
|
+
for (const [index, item] of value.items.entries()) {
|
|
235
|
+
const at = `${p}.items[${index}]`, accepted = Date.parse(item.acceptedAt);
|
|
236
|
+
const tuple = JSON.stringify([
|
|
237
|
+
item.campaignId.toLowerCase(),
|
|
238
|
+
item.campaignVersionId.toLowerCase(),
|
|
239
|
+
item.site,
|
|
240
|
+
item.experimentId.toLowerCase(),
|
|
241
|
+
item.experimentRevision,
|
|
242
|
+
item.goalRevision
|
|
243
|
+
]);
|
|
244
|
+
if (tuples.has(tuple)) fail(at, "original scope tuples must be unique");
|
|
245
|
+
tuples.add(tuple);
|
|
246
|
+
if (accepted > observed) fail(at + ".acceptedAt", "must not follow the native observation");
|
|
247
|
+
if (item.assignmentExtent) {
|
|
248
|
+
const first = Date.parse(item.assignmentExtent.firstAssignedAt), last = Date.parse(item.assignmentExtent.lastAssignedAt);
|
|
249
|
+
if (first > last || last > observed) fail(at + ".assignmentExtent", "requires first <= last <= native observation");
|
|
250
|
+
}
|
|
251
|
+
if (item.suggestedCohort) {
|
|
252
|
+
const from = Date.parse(item.suggestedCohort.assignedFrom), to = Date.parse(item.suggestedCohort.assignedTo);
|
|
253
|
+
if (from !== accepted || from >= to || to > observed) fail(at + ".suggestedCohort", "requires original acceptance start and increasing cohort ending by native observation");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
var parseSignalExperimentScopeOptions = (v) => scopeOptions.read(v, "experimentScopeOptions");
|
|
258
|
+
var SIGNAL_EXPERIMENT_SCOPE_OPTIONS_JSON_SCHEMA = scopeOptions.schema;
|
|
259
|
+
|
|
260
|
+
export { SIGNAL_EXPERIMENT_CREATE_JSON_SCHEMA, SIGNAL_EXPERIMENT_MEASUREMENT_JSON_SCHEMA, SIGNAL_EXPERIMENT_SCOPE_OPTIONS_JSON_SCHEMA, SIGNAL_EXPERIMENT_UPDATE_JSON_SCHEMA, SignalExperimentValidationError, parseSignalExperimentCreate, parseSignalExperimentMeasurement, parseSignalExperimentScopeOptions, parseSignalExperimentUpdate };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Internal shared wire-schema primitives. Parser and JSON Schema share one field definition. */
|
|
2
|
+
export type JsonSchema = {
|
|
3
|
+
[key: string]: unknown;
|
|
4
|
+
};
|
|
5
|
+
export interface Field<T> {
|
|
6
|
+
schema: JsonSchema;
|
|
7
|
+
read(value: unknown, path: string): T;
|
|
8
|
+
}
|
|
9
|
+
export type Value<F> = F extends Field<infer T> ? T : never;
|
|
10
|
+
type Fields = Record<string, Field<unknown>>;
|
|
11
|
+
type ObjectValue<S extends Fields> = {
|
|
12
|
+
[K in keyof S as S[K] extends {
|
|
13
|
+
optional: true;
|
|
14
|
+
} ? never : K]: Value<S[K]>;
|
|
15
|
+
} & {
|
|
16
|
+
[K in keyof S as S[K] extends {
|
|
17
|
+
optional: true;
|
|
18
|
+
} ? K : never]?: Value<S[K]>;
|
|
19
|
+
};
|
|
20
|
+
export declare function createWireSchema(fail: (path: string, reason: string) => never, isValidationError: (error: unknown) => boolean): {
|
|
21
|
+
field: <T>(schema: JsonSchema, read: Field<T>['read']) => Field<T>;
|
|
22
|
+
text: (max?: number, pattern?: RegExp, min?: number) => Field<string>;
|
|
23
|
+
choice: <const T extends readonly string[]>(...values: T) => Field<T[number]>;
|
|
24
|
+
literal: <T extends string | number | boolean | null>(value: T) => Field<T>;
|
|
25
|
+
integer: (min?: number, max?: number) => Field<number>;
|
|
26
|
+
bool: Field<boolean>;
|
|
27
|
+
optional: <T>(f: Field<T>) => Field<T> & {
|
|
28
|
+
optional: true;
|
|
29
|
+
};
|
|
30
|
+
nullable: <T>(f: Field<T>) => Field<T | null>;
|
|
31
|
+
list: <T>(f: Field<T>, max?: number, min?: number) => Field<T[]>;
|
|
32
|
+
object: <S extends Fields>(fields: S) => Field<ObjectValue<S>>;
|
|
33
|
+
union: <const F extends readonly Field<unknown>[]>(...fields: F) => Field<Value<F[number]>>;
|
|
34
|
+
refine: <T>(f: Field<T>, check: (v: T, p: string) => void) => Field<T>;
|
|
35
|
+
};
|
|
36
|
+
export {};
|
package/docs.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"title": "contracts",
|
|
4
|
+
"summary": "Shared validation rules, wire types and JSON schemas for Sonor applications.",
|
|
5
|
+
"groups": [
|
|
6
|
+
{
|
|
7
|
+
"title": "Start here",
|
|
8
|
+
"pages": [
|
|
9
|
+
{
|
|
10
|
+
"slug": "",
|
|
11
|
+
"title": "Overview and contracts",
|
|
12
|
+
"file": "README.md"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"changelog": {
|
|
18
|
+
"file": "CHANGELOG.md",
|
|
19
|
+
"since": "2.18.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sonordev/contracts",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "The rules Sonor's sites, APIs and dashboard share: popup blocks and site design tokens, site hosts, SEO page identity, title quality, llms.txt sanitizers,
|
|
3
|
+
"version": "2.20.0",
|
|
4
|
+
"description": "The rules Sonor's sites, APIs and dashboard share: popup blocks and site design tokens, site hosts, SEO page identity, title quality, llms.txt sanitizers, shared form validation, fleet heartbeats, slot content and signing, site cache tags, portfolio metrics, the house voice's contraction rule, how a proposal's site plan counts its pages, what separates an error page from a page's content, which stored JSON-LD is a template's unfilled placeholder, where prose splits into sentences, which Sonor records Signal reads into a project's knowledge and the address each one is stored under, what outbound email and text may claim about a business and whether a sent email arrived intact, what a segment of outreach is and who may turn one on, a page's own name taken from its title or its slug, which Broadcast card layouts and review compositions are drawn and what a retired one draws as, what a publication's branding may hold, which literal hosts are refused as public fetch destinations, which inbox cards are decisions a bulk action leaves alone, what a rep's call outcome (positive, negative or left voicemail) changes on a prospect, and which buying role a job title says (owner, decision maker, influencer or gatekeeper). Pure functions, no dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -160,6 +160,26 @@
|
|
|
160
160
|
"types": "./dist/crm-disposition.d.ts",
|
|
161
161
|
"import": "./dist/crm-disposition.mjs",
|
|
162
162
|
"require": "./dist/crm-disposition.js"
|
|
163
|
+
},
|
|
164
|
+
"./decision-roles": {
|
|
165
|
+
"types": "./dist/decision-roles.d.ts",
|
|
166
|
+
"import": "./dist/decision-roles.mjs",
|
|
167
|
+
"require": "./dist/decision-roles.js"
|
|
168
|
+
},
|
|
169
|
+
"./resonance": {
|
|
170
|
+
"types": "./dist/resonance.d.ts",
|
|
171
|
+
"import": "./dist/resonance.mjs",
|
|
172
|
+
"require": "./dist/resonance.js"
|
|
173
|
+
},
|
|
174
|
+
"./resonance-constants": {
|
|
175
|
+
"types": "./dist/resonance-constants.d.ts",
|
|
176
|
+
"import": "./dist/resonance-constants.mjs",
|
|
177
|
+
"require": "./dist/resonance-constants.js"
|
|
178
|
+
},
|
|
179
|
+
"./signal-experiments": {
|
|
180
|
+
"types": "./dist/signal-experiments.d.ts",
|
|
181
|
+
"import": "./dist/signal-experiments.mjs",
|
|
182
|
+
"require": "./dist/signal-experiments.js"
|
|
163
183
|
}
|
|
164
184
|
},
|
|
165
185
|
"typesVersions": {
|
|
@@ -172,7 +192,8 @@
|
|
|
172
192
|
"files": [
|
|
173
193
|
"dist",
|
|
174
194
|
"README.md",
|
|
175
|
-
"CHANGELOG.md"
|
|
195
|
+
"CHANGELOG.md",
|
|
196
|
+
"docs.json"
|
|
176
197
|
],
|
|
177
198
|
"engines": {
|
|
178
199
|
"node": ">=20"
|