@sonordev/contracts 2.13.0 → 2.16.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 +23 -0
- package/README.md +149 -4
- package/dist/campaign-normalize.d.ts +2 -0
- package/dist/frame-ready.d.ts +23 -0
- package/dist/funnels.d.ts +576 -0
- package/dist/funnels.js +617 -0
- package/dist/funnels.mjs +560 -0
- package/dist/inbox.d.ts +42 -0
- package/dist/inbox.js +14 -0
- package/dist/inbox.mjs +9 -0
- package/dist/llms.d.ts +200 -0
- package/dist/llms.js +1002 -8
- package/dist/llms.mjs +989 -9
- package/dist/portfolio.d.ts +197 -14
- package/dist/portfolio.js +421 -12
- package/dist/portfolio.mjs +408 -13
- package/dist/publication-branding.d.ts +6 -1
- package/dist/publication-branding.js +1 -1
- package/dist/publication-branding.mjs +1 -1
- package/dist/site-cache.d.ts +2 -0
- package/dist/site-cache.js +3 -1
- package/dist/site-cache.mjs +3 -1
- package/package.json +13 -4
package/dist/portfolio.mjs
CHANGED
|
@@ -1,8 +1,41 @@
|
|
|
1
|
-
// src/contracts/
|
|
1
|
+
// src/contracts/public-url-host.ts
|
|
2
|
+
function isPrivateIpv4(a, b) {
|
|
3
|
+
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;
|
|
4
|
+
}
|
|
5
|
+
function ipv6Words(host) {
|
|
6
|
+
if (!host.includes(":") || !/^[a-f\d:.]+$/.test(host)) return null;
|
|
7
|
+
let canonical;
|
|
8
|
+
try {
|
|
9
|
+
canonical = new URL(`http://[${host}]/`).hostname.slice(1, -1);
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const sides = canonical.split("::");
|
|
14
|
+
const left = sides[0] ? sides[0].split(":") : [];
|
|
15
|
+
const right = sides.length > 1 && sides[1] ? sides[1].split(":") : [];
|
|
16
|
+
return [...left, ...Array(sides.length > 1 ? 8 - left.length - right.length : 0).fill("0"), ...right].map((word) => parseInt(word, 16));
|
|
17
|
+
}
|
|
18
|
+
function isBlockedPublicUrlHost(rawHost) {
|
|
19
|
+
const host = rawHost.toLowerCase().replace(/^\[|\]$/g, "");
|
|
20
|
+
const blockedName = host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal");
|
|
21
|
+
if (host.includes(":") && host.includes("%")) return true;
|
|
22
|
+
const ipv4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
23
|
+
const blockedIp = !!ipv4 && isPrivateIpv4(Number(ipv4[1]), Number(ipv4[2]));
|
|
24
|
+
const words = ipv6Words(host);
|
|
25
|
+
const blockedIpv6 = !!words && // Unspecified/loopback, unique-local, link-local, deprecated site-local
|
|
26
|
+
// and multicast destinations aren't public logo/webhook endpoints.
|
|
27
|
+
(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.
|
|
28
|
+
words.slice(0, 5).every((word) => word === 0) && [0, 65535].includes(words[5]) && isPrivateIpv4(words[6] >> 8, words[6] & 255));
|
|
29
|
+
return blockedName || blockedIp || blockedIpv6;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/contracts/frame-ready.ts
|
|
2
33
|
var FRAME_READY_MESSAGE = "sonor:frame-ready";
|
|
3
34
|
function isFrameReadyMessage(data) {
|
|
4
35
|
return !!data && typeof data === "object" && data.type === FRAME_READY_MESSAGE && data.v === 1;
|
|
5
36
|
}
|
|
37
|
+
|
|
38
|
+
// src/contracts/portfolio.ts
|
|
6
39
|
var PORTFOLIO_CONTRACT_VERSION = 1;
|
|
7
40
|
var CASE_STUDY_STAGES = ["problem", "build", "proof", "outcome"];
|
|
8
41
|
var CASE_STUDY_STAGE_COPY = {
|
|
@@ -45,6 +78,7 @@ var PORTFOLIO_SECTION_TYPES = [
|
|
|
45
78
|
// proof
|
|
46
79
|
"portfolioPerformance",
|
|
47
80
|
"portfolioSpeedComparison",
|
|
81
|
+
"portfolioVerification",
|
|
48
82
|
// outcome
|
|
49
83
|
"portfolioResults",
|
|
50
84
|
"portfolioMetricsTimeline",
|
|
@@ -73,6 +107,7 @@ var SECTION_STAGE = {
|
|
|
73
107
|
portfolioTechStack: "build",
|
|
74
108
|
portfolioPerformance: "proof",
|
|
75
109
|
portfolioSpeedComparison: "proof",
|
|
110
|
+
portfolioVerification: "proof",
|
|
76
111
|
portfolioResults: "outcome",
|
|
77
112
|
portfolioMetricsTimeline: "outcome",
|
|
78
113
|
portfolioConversionFunnel: "outcome",
|
|
@@ -92,6 +127,18 @@ var STAGE_CORE_SECTION = {
|
|
|
92
127
|
proof: "portfolioPerformance",
|
|
93
128
|
outcome: "portfolioResults"
|
|
94
129
|
};
|
|
130
|
+
var STAGE_CORE_ALTERNATES = {
|
|
131
|
+
proof: ["portfolioVerification"]
|
|
132
|
+
};
|
|
133
|
+
var STAND_IN_HAS_CONTENT = {
|
|
134
|
+
portfolioVerification: (data) => readVerificationChecks(data).length > 0
|
|
135
|
+
};
|
|
136
|
+
var STAND_IN_EMPTY_COPY = {
|
|
137
|
+
portfolioVerification: "The verification is there, but none of its checks can be shown yet. Add a check, and say what it doesn't cover."
|
|
138
|
+
};
|
|
139
|
+
function stageCoreSections(stage) {
|
|
140
|
+
return [STAGE_CORE_SECTION[stage], ...STAGE_CORE_ALTERNATES[stage] ?? []];
|
|
141
|
+
}
|
|
95
142
|
var PORTFOLIO_SECTION_ORDER = Object.fromEntries(
|
|
96
143
|
PORTFOLIO_SECTION_TYPES.map((type, i) => [type, i])
|
|
97
144
|
);
|
|
@@ -135,7 +182,7 @@ var STAGE_MISSING_COPY = {
|
|
|
135
182
|
noCore: "The build is shown but the plan behind it is missing. Add the strategy."
|
|
136
183
|
},
|
|
137
184
|
proof: {
|
|
138
|
-
empty: "Nothing measures the finished build. Add a performance measurement.",
|
|
185
|
+
empty: "Nothing measures the finished build. Add a performance measurement, or a verification when the screens are behind a sign-in.",
|
|
139
186
|
noCore: "The old site is compared but the finished build has no measurement of its own. Add a performance measurement."
|
|
140
187
|
},
|
|
141
188
|
outcome: {
|
|
@@ -146,15 +193,20 @@ var STAGE_MISSING_COPY = {
|
|
|
146
193
|
function caseStudyStageStatus(sections) {
|
|
147
194
|
const ordered = orderSections(sections);
|
|
148
195
|
return CASE_STUDY_STAGES.map((stage) => {
|
|
149
|
-
const
|
|
150
|
-
|
|
196
|
+
const own = ordered.filter(
|
|
197
|
+
(s) => isPortfolioSectionType(s.sectionType) && SECTION_STAGE[s.sectionType] === stage
|
|
198
|
+
);
|
|
199
|
+
const present = own.map((s) => s.sectionType);
|
|
200
|
+
const standIns = STAGE_CORE_ALTERNATES[stage] ?? [];
|
|
201
|
+
const emptyStandIns = own.filter(
|
|
202
|
+
(s) => standIns.includes(s.sectionType) && STAND_IN_HAS_CONTENT[s.sectionType]?.(s.data) === false
|
|
203
|
+
);
|
|
204
|
+
const complete = own.some(
|
|
205
|
+
(s) => s.sectionType === STAGE_CORE_SECTION[stage] || standIns.includes(s.sectionType) && !emptyStandIns.includes(s)
|
|
206
|
+
);
|
|
151
207
|
const copy = STAGE_MISSING_COPY[stage];
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
present,
|
|
155
|
-
complete,
|
|
156
|
-
missing: complete ? null : present.length ? copy.noCore : copy.empty
|
|
157
|
-
};
|
|
208
|
+
const missing = complete ? null : (emptyStandIns.length ? STAND_IN_EMPTY_COPY[emptyStandIns[0].sectionType] : void 0) ?? (present.length ? copy.noCore : copy.empty);
|
|
209
|
+
return { stage, present, complete, missing };
|
|
158
210
|
});
|
|
159
211
|
}
|
|
160
212
|
var NARRATIVE_MAX_CHARS = 320;
|
|
@@ -234,6 +286,350 @@ function caseStudyFlowProblems(input) {
|
|
|
234
286
|
}
|
|
235
287
|
return problems;
|
|
236
288
|
}
|
|
289
|
+
var ISO_DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
290
|
+
function isRealDay(value) {
|
|
291
|
+
if (typeof value !== "string" || !ISO_DAY_RE.test(value)) return false;
|
|
292
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
293
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
294
|
+
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
295
|
+
}
|
|
296
|
+
var VERIFICATION_MAX_CHECKS = 8;
|
|
297
|
+
var VERIFICATION_CHECK_FIELDS = ["label", "result", "measured", "where", "method", "date", "scope"];
|
|
298
|
+
var VERIFICATION_FIELD_MAX_CHARS = {
|
|
299
|
+
label: 80,
|
|
300
|
+
result: 120,
|
|
301
|
+
measured: 200,
|
|
302
|
+
where: 200,
|
|
303
|
+
method: 240,
|
|
304
|
+
scope: 240
|
|
305
|
+
};
|
|
306
|
+
var VERIFICATION_EARLIEST_DAY = "2000-01-01";
|
|
307
|
+
function latestVerificationDay(now = /* @__PURE__ */ new Date()) {
|
|
308
|
+
const tomorrow = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1));
|
|
309
|
+
return tomorrow.toISOString().slice(0, 10);
|
|
310
|
+
}
|
|
311
|
+
function wordsOf(text) {
|
|
312
|
+
return text.toLowerCase().replace(/['’]/g, "").split(/[^a-z0-9]+/).filter(Boolean);
|
|
313
|
+
}
|
|
314
|
+
var FILLER_WORDS = /* @__PURE__ */ new Set([
|
|
315
|
+
"a",
|
|
316
|
+
"an",
|
|
317
|
+
"the",
|
|
318
|
+
"of",
|
|
319
|
+
"to",
|
|
320
|
+
"at",
|
|
321
|
+
"as",
|
|
322
|
+
"in",
|
|
323
|
+
"is",
|
|
324
|
+
"are",
|
|
325
|
+
"be",
|
|
326
|
+
"it",
|
|
327
|
+
"this",
|
|
328
|
+
"that",
|
|
329
|
+
"any",
|
|
330
|
+
"all"
|
|
331
|
+
]);
|
|
332
|
+
var PLACEHOLDER_WORDS = /* @__PURE__ */ new Set([
|
|
333
|
+
...FILLER_WORDS,
|
|
334
|
+
"n",
|
|
335
|
+
"na",
|
|
336
|
+
"nil",
|
|
337
|
+
"null",
|
|
338
|
+
"undefined",
|
|
339
|
+
"none",
|
|
340
|
+
"nothing",
|
|
341
|
+
"else",
|
|
342
|
+
"no",
|
|
343
|
+
"not",
|
|
344
|
+
"yes",
|
|
345
|
+
"ok",
|
|
346
|
+
"okay",
|
|
347
|
+
"x",
|
|
348
|
+
"xx",
|
|
349
|
+
"xxx",
|
|
350
|
+
"tbd",
|
|
351
|
+
"tbc",
|
|
352
|
+
"tba",
|
|
353
|
+
"todo",
|
|
354
|
+
"lorem",
|
|
355
|
+
"ipsum",
|
|
356
|
+
"misc",
|
|
357
|
+
"etc",
|
|
358
|
+
"various",
|
|
359
|
+
"multiple",
|
|
360
|
+
"same",
|
|
361
|
+
"above",
|
|
362
|
+
"see",
|
|
363
|
+
"here",
|
|
364
|
+
"there",
|
|
365
|
+
"applicable",
|
|
366
|
+
"available",
|
|
367
|
+
"relevant",
|
|
368
|
+
"required",
|
|
369
|
+
"needed",
|
|
370
|
+
"specified",
|
|
371
|
+
"unspecified",
|
|
372
|
+
"provided",
|
|
373
|
+
"given",
|
|
374
|
+
"stated",
|
|
375
|
+
"known",
|
|
376
|
+
"unknown",
|
|
377
|
+
"apply",
|
|
378
|
+
"applies",
|
|
379
|
+
"exist",
|
|
380
|
+
"exists",
|
|
381
|
+
"restriction",
|
|
382
|
+
"restrictions",
|
|
383
|
+
"limit",
|
|
384
|
+
"limits",
|
|
385
|
+
"limitation",
|
|
386
|
+
"limitations",
|
|
387
|
+
"caveat",
|
|
388
|
+
"caveats",
|
|
389
|
+
"exclusion",
|
|
390
|
+
"exclusions",
|
|
391
|
+
"exception",
|
|
392
|
+
"exceptions",
|
|
393
|
+
"constraint",
|
|
394
|
+
"constraints",
|
|
395
|
+
"gap",
|
|
396
|
+
"gaps",
|
|
397
|
+
"issue",
|
|
398
|
+
"issues",
|
|
399
|
+
"problem",
|
|
400
|
+
"problems",
|
|
401
|
+
"boundary",
|
|
402
|
+
"boundaries"
|
|
403
|
+
]);
|
|
404
|
+
var LIMIT_WORDS = /* @__PURE__ */ new Set([
|
|
405
|
+
"only",
|
|
406
|
+
"not",
|
|
407
|
+
"never",
|
|
408
|
+
"cannot",
|
|
409
|
+
"except",
|
|
410
|
+
"beyond",
|
|
411
|
+
"outside",
|
|
412
|
+
"just",
|
|
413
|
+
"solely",
|
|
414
|
+
"merely",
|
|
415
|
+
"unless",
|
|
416
|
+
"besides",
|
|
417
|
+
"untested",
|
|
418
|
+
"unmeasured",
|
|
419
|
+
"unchecked",
|
|
420
|
+
"unverified",
|
|
421
|
+
// negative contractions, apostrophe dropped
|
|
422
|
+
"isnt",
|
|
423
|
+
"arent",
|
|
424
|
+
"wasnt",
|
|
425
|
+
"werent",
|
|
426
|
+
"dont",
|
|
427
|
+
"doesnt",
|
|
428
|
+
"didnt",
|
|
429
|
+
"cant",
|
|
430
|
+
"couldnt",
|
|
431
|
+
"wont",
|
|
432
|
+
"wouldnt",
|
|
433
|
+
"hasnt",
|
|
434
|
+
"havent",
|
|
435
|
+
"hadnt",
|
|
436
|
+
"shouldnt",
|
|
437
|
+
"mustnt",
|
|
438
|
+
"neednt"
|
|
439
|
+
]);
|
|
440
|
+
var EXCLUSION_WORDS = /* @__PURE__ */ new Set([
|
|
441
|
+
"exclude",
|
|
442
|
+
"excludes",
|
|
443
|
+
"excluded",
|
|
444
|
+
"excluding",
|
|
445
|
+
"omit",
|
|
446
|
+
"omits",
|
|
447
|
+
"omitted",
|
|
448
|
+
"skip",
|
|
449
|
+
"skips",
|
|
450
|
+
"skipped",
|
|
451
|
+
"limited"
|
|
452
|
+
]);
|
|
453
|
+
var NEGATOR_WORDS = /* @__PURE__ */ new Set(["no", "none", "nothing", "without"]);
|
|
454
|
+
function isPlaceholderText(text) {
|
|
455
|
+
const words = wordsOf(text);
|
|
456
|
+
return words.length === 0 || words.every((word) => PLACEHOLDER_WORDS.has(word));
|
|
457
|
+
}
|
|
458
|
+
function isVacuousScope(scope) {
|
|
459
|
+
const words = wordsOf(scope);
|
|
460
|
+
if (words.length < 2) return true;
|
|
461
|
+
const wordAfter = (i) => {
|
|
462
|
+
for (let j = i + 1; j < words.length; j++) if (!FILLER_WORDS.has(words[j])) return words[j];
|
|
463
|
+
return void 0;
|
|
464
|
+
};
|
|
465
|
+
const wordBefore = (i) => {
|
|
466
|
+
for (let j = i - 1; j >= 0; j--) if (!FILLER_WORDS.has(words[j])) return words[j];
|
|
467
|
+
return void 0;
|
|
468
|
+
};
|
|
469
|
+
const marksLimit = words.map((word, i) => {
|
|
470
|
+
if (NEGATOR_WORDS.has(word)) {
|
|
471
|
+
const next = wordAfter(i);
|
|
472
|
+
return next !== void 0 && !PLACEHOLDER_WORDS.has(next) && !EXCLUSION_WORDS.has(next);
|
|
473
|
+
}
|
|
474
|
+
if (EXCLUSION_WORDS.has(word)) return !NEGATOR_WORDS.has(wordBefore(i) ?? "");
|
|
475
|
+
return LIMIT_WORDS.has(word);
|
|
476
|
+
});
|
|
477
|
+
if (!marksLimit.some(Boolean)) return true;
|
|
478
|
+
return !words.some(
|
|
479
|
+
(word) => !PLACEHOLDER_WORDS.has(word) && !LIMIT_WORDS.has(word) && !EXCLUSION_WORDS.has(word) && !NEGATOR_WORDS.has(word)
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
function carriesCredentials(text) {
|
|
483
|
+
for (let at = text.indexOf("://"); at !== -1; at = text.indexOf("://", at + 3)) {
|
|
484
|
+
for (let i = at + 3; i < text.length; i++) {
|
|
485
|
+
const char = text[i];
|
|
486
|
+
if (char === "@") return true;
|
|
487
|
+
if (char === "/" || char === "?" || char === "#" || /\s/.test(char)) break;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
var WEB_ADDRESS = /^https?:\/\//i;
|
|
493
|
+
var BARE_PRIVATE_ADDRESS = /^(?:localhost|\d{1,3}(?:\.\d{1,3}){3}|\[[0-9a-f:.]+\])(?::\d+)?(?:[/?#]|$)/i;
|
|
494
|
+
function trimPunctuation(word) {
|
|
495
|
+
let start = 0;
|
|
496
|
+
let end = word.length;
|
|
497
|
+
while (start < end && `("'<`.includes(word[start])) start++;
|
|
498
|
+
while (end > start && `)"'>.,;!?`.includes(word[end - 1])) end--;
|
|
499
|
+
return word.slice(start, end);
|
|
500
|
+
}
|
|
501
|
+
function namesPrivateAddress(text) {
|
|
502
|
+
return text.split(/\s+/).some((word) => {
|
|
503
|
+
const token = trimPunctuation(word);
|
|
504
|
+
const address = WEB_ADDRESS.test(token) ? token : BARE_PRIVATE_ADDRESS.test(token) ? `http://${token}` : null;
|
|
505
|
+
if (!address) return false;
|
|
506
|
+
try {
|
|
507
|
+
return isBlockedPublicUrlHost(new URL(address).hostname);
|
|
508
|
+
} catch {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
function looksLikePathOrHost(token) {
|
|
514
|
+
const pieces = token.split(/[/\\.]/).filter(Boolean);
|
|
515
|
+
return pieces.length >= 2 && pieces.every((piece) => /^[\w:-]{2,}$/.test(piece));
|
|
516
|
+
}
|
|
517
|
+
function whereProblem(where) {
|
|
518
|
+
if (carriesCredentials(where)) return "must not carry a username or password";
|
|
519
|
+
if (namesPrivateAddress(where)) return "must be a public address, not localhost or a private network";
|
|
520
|
+
const named = !isPlaceholderText(where) && (verificationLink(where) !== null || where.split(/\s+/).length >= 2 || looksLikePathOrHost(where));
|
|
521
|
+
return named ? null : "must say where the check ran: a public web address, or a screen or file named in words";
|
|
522
|
+
}
|
|
523
|
+
var NOT_PLAIN_LINE = /[\p{Cc}\u2028\u2029\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF]/u;
|
|
524
|
+
function checkProblems(check, at, latestDay, strict) {
|
|
525
|
+
if (!check || typeof check !== "object" || Array.isArray(check)) return [`${at} must be an object`];
|
|
526
|
+
const entry = check;
|
|
527
|
+
const problems = [];
|
|
528
|
+
if (strict) {
|
|
529
|
+
for (const key of Object.keys(entry)) {
|
|
530
|
+
if (!VERIFICATION_CHECK_FIELDS.includes(key)) problems.push(`${at}.${key} is not a field`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
for (const field of VERIFICATION_CHECK_FIELDS) {
|
|
534
|
+
const value = entry[field];
|
|
535
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
536
|
+
problems.push(
|
|
537
|
+
field === "scope" ? `${at}.scope is required: say what the check does not cover` : `${at}.${field} is required`
|
|
538
|
+
);
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (NOT_PLAIN_LINE.test(value)) problems.push(`${at}.${field} must be one line of plain text`);
|
|
542
|
+
if (field === "date") continue;
|
|
543
|
+
const max = VERIFICATION_FIELD_MAX_CHARS[field];
|
|
544
|
+
if (value.trim().length > max) problems.push(`${at}.${field} is over ${max} characters`);
|
|
545
|
+
}
|
|
546
|
+
const date = typeof entry.date === "string" ? entry.date.trim() : "";
|
|
547
|
+
if (date) {
|
|
548
|
+
if (!isRealDay(date)) problems.push(`${at}.date must be a real day, YYYY-MM-DD`);
|
|
549
|
+
else if (date < VERIFICATION_EARLIEST_DAY) problems.push(`${at}.date must be ${VERIFICATION_EARLIEST_DAY} or later`);
|
|
550
|
+
else if (date > latestDay) problems.push(`${at}.date is in the future`);
|
|
551
|
+
}
|
|
552
|
+
const text = (field) => {
|
|
553
|
+
const value = entry[field];
|
|
554
|
+
return typeof value === "string" && value.trim() && value.trim().length <= VERIFICATION_FIELD_MAX_CHARS[field] ? value.trim() : null;
|
|
555
|
+
};
|
|
556
|
+
const where = text("where");
|
|
557
|
+
const problem = where === null ? null : whereProblem(where);
|
|
558
|
+
if (problem) problems.push(`${at}.where ${problem}`);
|
|
559
|
+
const scope = text("scope");
|
|
560
|
+
if (scope !== null && isVacuousScope(scope)) {
|
|
561
|
+
problems.push(`${at}.scope must state a limit: what the check does not cover, using a word like "only", "not", "except" or "without"`);
|
|
562
|
+
}
|
|
563
|
+
return problems;
|
|
564
|
+
}
|
|
565
|
+
function verificationProblems(data, options = {}) {
|
|
566
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return ["verification data must be an object"];
|
|
567
|
+
const { checks, ...rest } = data;
|
|
568
|
+
const problems = Object.keys(rest).map((key) => `${key} is not a field of a verification`);
|
|
569
|
+
if (!Array.isArray(checks) || checks.length === 0) return [...problems, "checks must list at least one check"];
|
|
570
|
+
if (checks.length > VERIFICATION_MAX_CHECKS) {
|
|
571
|
+
problems.push(`checks is over ${VERIFICATION_MAX_CHECKS} entries; keep the ones that matter`);
|
|
572
|
+
}
|
|
573
|
+
const latestDay = options.latestDay ?? latestVerificationDay();
|
|
574
|
+
checks.forEach((check, i) => problems.push(...checkProblems(check, `checks[${i}]`, latestDay, true)));
|
|
575
|
+
return problems;
|
|
576
|
+
}
|
|
577
|
+
function readVerificationChecks(data, options = {}) {
|
|
578
|
+
const checks = data?.checks;
|
|
579
|
+
if (!Array.isArray(checks)) return [];
|
|
580
|
+
const latestDay = options.latestDay ?? latestVerificationDay();
|
|
581
|
+
const shown = [];
|
|
582
|
+
checks.forEach((check, i) => {
|
|
583
|
+
if (shown.length >= VERIFICATION_MAX_CHECKS || checkProblems(check, `checks[${i}]`, latestDay, false).length) return;
|
|
584
|
+
const entry = check;
|
|
585
|
+
shown.push(
|
|
586
|
+
Object.fromEntries(VERIFICATION_CHECK_FIELDS.map((field) => [field, entry[field].trim()]))
|
|
587
|
+
);
|
|
588
|
+
});
|
|
589
|
+
return shown;
|
|
590
|
+
}
|
|
591
|
+
function verificationLink(where) {
|
|
592
|
+
const text = where.trim();
|
|
593
|
+
if (!text || /\s/.test(text)) return null;
|
|
594
|
+
try {
|
|
595
|
+
const url = new URL(text);
|
|
596
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
|
|
597
|
+
if (url.username || url.password || isBlockedPublicUrlHost(url.hostname)) return null;
|
|
598
|
+
return url.toString();
|
|
599
|
+
} catch {
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
var PORTFOLIO_VERIFICATION_JSON_SCHEMA = {
|
|
604
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
605
|
+
$id: "https://sonor.io/schemas/portfolio-verification.v1.json",
|
|
606
|
+
title: "PortfolioVerificationData",
|
|
607
|
+
description: "A short list of checks on the finished build. Each says what was checked, where, how, when, what came out, and what it does not cover.",
|
|
608
|
+
type: "object",
|
|
609
|
+
required: ["checks"],
|
|
610
|
+
properties: {
|
|
611
|
+
checks: {
|
|
612
|
+
type: "array",
|
|
613
|
+
minItems: 1,
|
|
614
|
+
maxItems: VERIFICATION_MAX_CHECKS,
|
|
615
|
+
items: {
|
|
616
|
+
type: "object",
|
|
617
|
+
required: [...VERIFICATION_CHECK_FIELDS],
|
|
618
|
+
properties: {
|
|
619
|
+
label: { type: "string", minLength: 1, maxLength: VERIFICATION_FIELD_MAX_CHARS.label },
|
|
620
|
+
result: { type: "string", minLength: 1, maxLength: VERIFICATION_FIELD_MAX_CHARS.result },
|
|
621
|
+
measured: { type: "string", minLength: 1, maxLength: VERIFICATION_FIELD_MAX_CHARS.measured },
|
|
622
|
+
where: { type: "string", minLength: 1, maxLength: VERIFICATION_FIELD_MAX_CHARS.where },
|
|
623
|
+
method: { type: "string", minLength: 1, maxLength: VERIFICATION_FIELD_MAX_CHARS.method },
|
|
624
|
+
date: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$" },
|
|
625
|
+
scope: { type: "string", minLength: 1, maxLength: VERIFICATION_FIELD_MAX_CHARS.scope }
|
|
626
|
+
},
|
|
627
|
+
additionalProperties: false
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
},
|
|
631
|
+
additionalProperties: false
|
|
632
|
+
};
|
|
237
633
|
var LIGHTHOUSE_RE = /lighthouse/i;
|
|
238
634
|
function isLighthouseKpi(kpi) {
|
|
239
635
|
return LIGHTHOUSE_RE.test(kpi.label || "");
|
|
@@ -279,7 +675,6 @@ var PORTFOLIO_KPI_JSON_SCHEMA = {
|
|
|
279
675
|
additionalProperties: true
|
|
280
676
|
};
|
|
281
677
|
var METRIC_SOURCES = ["measured", "reported", "estimated"];
|
|
282
|
-
var ISO_DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
283
678
|
var MONTHS = [
|
|
284
679
|
"January",
|
|
285
680
|
"February",
|
|
@@ -309,7 +704,7 @@ function metricSourceProblem(metric) {
|
|
|
309
704
|
if (typeof by.organization !== "string" || !by.organization.trim()) {
|
|
310
705
|
return "reportedBy.organization is required";
|
|
311
706
|
}
|
|
312
|
-
if (typeof by.date !== "string" || !
|
|
707
|
+
if (typeof by.date !== "string" || !isRealDay(by.date)) {
|
|
313
708
|
return "reportedBy.date must be a real day, YYYY-MM-DD";
|
|
314
709
|
}
|
|
315
710
|
if (by.role !== void 0 && typeof by.role !== "string") return "reportedBy.role must be text";
|
|
@@ -341,4 +736,4 @@ function carryReportedMetrics(previous, next, keyOf) {
|
|
|
341
736
|
return [...reported, ...generated];
|
|
342
737
|
}
|
|
343
738
|
|
|
344
|
-
export { CASE_STUDY_NARRATIVE_JSON_SCHEMA, CASE_STUDY_STAGES, CASE_STUDY_STAGE_COPY, FRAME_READY_MESSAGE, METRIC_SOURCES, NARRATIVE_MAX_CHARS, PORTFOLIO_CONTRACT_VERSION, PORTFOLIO_KPI_JSON_SCHEMA, PORTFOLIO_SECTION_ORDER, PORTFOLIO_SECTION_TYPES, SECTION_STAGE, STAGE_CORE_SECTION, STAGE_NAME_MAX_CHARS, carryReportedMetrics, caseStudyFlowProblems, caseStudyStageStatus, challengeId, curateHeroKpis, formatAttribution, groupSectionsByStage, isFrameReadyMessage, isHeadlineMetric, isLighthouseKpi, isMetricSource, isPortfolioSectionType, metricSourceLabel, metricSourceProblem, narrativeProblems, orderSections, sectionStage, shortAttribution, stageName, stageNamesProblems };
|
|
739
|
+
export { CASE_STUDY_NARRATIVE_JSON_SCHEMA, CASE_STUDY_STAGES, CASE_STUDY_STAGE_COPY, FRAME_READY_MESSAGE, METRIC_SOURCES, NARRATIVE_MAX_CHARS, PORTFOLIO_CONTRACT_VERSION, PORTFOLIO_KPI_JSON_SCHEMA, PORTFOLIO_SECTION_ORDER, PORTFOLIO_SECTION_TYPES, PORTFOLIO_VERIFICATION_JSON_SCHEMA, SECTION_STAGE, STAGE_CORE_ALTERNATES, STAGE_CORE_SECTION, STAGE_NAME_MAX_CHARS, VERIFICATION_CHECK_FIELDS, VERIFICATION_EARLIEST_DAY, VERIFICATION_FIELD_MAX_CHARS, VERIFICATION_MAX_CHECKS, carryReportedMetrics, caseStudyFlowProblems, caseStudyStageStatus, challengeId, curateHeroKpis, formatAttribution, groupSectionsByStage, isFrameReadyMessage, isHeadlineMetric, isLighthouseKpi, isMetricSource, isPlaceholderText, isPortfolioSectionType, isRealDay, isVacuousScope, latestVerificationDay, metricSourceLabel, metricSourceProblem, narrativeProblems, orderSections, readVerificationChecks, sectionStage, shortAttribution, stageCoreSections, stageName, stageNamesProblems, verificationLink, verificationProblems };
|
|
@@ -9,7 +9,7 @@ export type PublicationPlatform = (typeof PUBLICATION_PLATFORMS)[number];
|
|
|
9
9
|
/** The sections of a profile and of each platform override, in the order they're read. */
|
|
10
10
|
export declare const PUBLICATION_BRANDING_SECTIONS: readonly ['identity', 'colors', 'typography', 'editorial', 'newsletter'];
|
|
11
11
|
export type PublicationBrandingSection = (typeof PUBLICATION_BRANDING_SECTIONS)[number];
|
|
12
|
-
export declare const PUBLICATION_IDENTITY_FIELDS: readonly ['publicationName', 'wordmark', 'tagline', 'publisherName', 'publisherUrl', 'publisherLogoUrl', 'logos'];
|
|
12
|
+
export declare const PUBLICATION_IDENTITY_FIELDS: readonly ['publicationName', 'wordmark', 'tagline', 'publisherName', 'publisherUrl', 'publisherLogoUrl', 'logos', 'squareLogoUrl', 'fullLogoUrl'];
|
|
13
13
|
export declare const PUBLICATION_LOGO_VARIANTS: readonly ['default', 'light', 'dark'];
|
|
14
14
|
export declare const PUBLICATION_COLOR_FIELDS: readonly ['primary', 'secondary', 'accent', 'background', 'surface', 'text', 'muted', 'border'];
|
|
15
15
|
export declare const PUBLICATION_TYPOGRAPHY_FIELDS: readonly ['heading', 'body', 'mono', 'emailHeadingFallback', 'emailBodyFallback'];
|
|
@@ -28,6 +28,11 @@ export interface PublicationIdentity {
|
|
|
28
28
|
publisherUrl?: string;
|
|
29
29
|
/** The publisher's attribution mark, separate from the publication masthead. */
|
|
30
30
|
publisherLogoUrl?: string;
|
|
31
|
+
/** The publication's square mark; independent of theme variants and publisher attribution. */
|
|
32
|
+
squareLogoUrl?: string;
|
|
33
|
+
/** The publication's full/display logo; wordmark remains a text fallback. */
|
|
34
|
+
fullLogoUrl?: string;
|
|
35
|
+
/** Legacy theme variants, not square/full logo roles. */
|
|
31
36
|
logos?: {
|
|
32
37
|
default?: string;
|
|
33
38
|
light?: string;
|
|
@@ -134,7 +134,7 @@ function publicationCharterContext(charter) {
|
|
|
134
134
|
var PUBLICATION_BRANDING_VERSION = 1;
|
|
135
135
|
var PUBLICATION_PLATFORMS = ["publication", "email", "social", "instagram", "facebook", "linkedin", "gbp"];
|
|
136
136
|
var PUBLICATION_BRANDING_SECTIONS = ["identity", "colors", "typography", "editorial", "newsletter"];
|
|
137
|
-
var PUBLICATION_IDENTITY_FIELDS = ["publicationName", "wordmark", "tagline", "publisherName", "publisherUrl", "publisherLogoUrl", "logos"];
|
|
137
|
+
var PUBLICATION_IDENTITY_FIELDS = ["publicationName", "wordmark", "tagline", "publisherName", "publisherUrl", "publisherLogoUrl", "logos", "squareLogoUrl", "fullLogoUrl"];
|
|
138
138
|
var PUBLICATION_LOGO_VARIANTS = ["default", "light", "dark"];
|
|
139
139
|
var PUBLICATION_COLOR_FIELDS = ["primary", "secondary", "accent", "background", "surface", "text", "muted", "border"];
|
|
140
140
|
var PUBLICATION_TYPOGRAPHY_FIELDS = ["heading", "body", "mono", "emailHeadingFallback", "emailBodyFallback"];
|
|
@@ -132,7 +132,7 @@ function publicationCharterContext(charter) {
|
|
|
132
132
|
var PUBLICATION_BRANDING_VERSION = 1;
|
|
133
133
|
var PUBLICATION_PLATFORMS = ["publication", "email", "social", "instagram", "facebook", "linkedin", "gbp"];
|
|
134
134
|
var PUBLICATION_BRANDING_SECTIONS = ["identity", "colors", "typography", "editorial", "newsletter"];
|
|
135
|
-
var PUBLICATION_IDENTITY_FIELDS = ["publicationName", "wordmark", "tagline", "publisherName", "publisherUrl", "publisherLogoUrl", "logos"];
|
|
135
|
+
var PUBLICATION_IDENTITY_FIELDS = ["publicationName", "wordmark", "tagline", "publisherName", "publisherUrl", "publisherLogoUrl", "logos", "squareLogoUrl", "fullLogoUrl"];
|
|
136
136
|
var PUBLICATION_LOGO_VARIANTS = ["default", "light", "dark"];
|
|
137
137
|
var PUBLICATION_COLOR_FIELDS = ["primary", "secondary", "accent", "background", "surface", "text", "muted", "border"];
|
|
138
138
|
var PUBLICATION_TYPOGRAPHY_FIELDS = ["heading", "body", "mono", "emailHeadingFallback", "emailBodyFallback"];
|
package/dist/site-cache.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export declare const SITE_CACHE_TAGS: {
|
|
|
29
29
|
readonly portfolio: 'portfolio';
|
|
30
30
|
/** Products, services, events and membership plans (commerce-kit). */
|
|
31
31
|
readonly commerce: 'commerce';
|
|
32
|
+
/** Published managed landing pages. Draft previews never enter this cache. */
|
|
33
|
+
readonly funnels: 'sonor-funnels';
|
|
32
34
|
};
|
|
33
35
|
export type SiteCacheTag = (typeof SITE_CACHE_TAGS)[keyof typeof SITE_CACHE_TAGS];
|
|
34
36
|
/** The body Sonor POSTs to the revalidation route. */
|
package/dist/site-cache.js
CHANGED
|
@@ -14,7 +14,9 @@ var SITE_CACHE_TAGS = {
|
|
|
14
14
|
/** Portfolio items (agency-site-kit). */
|
|
15
15
|
portfolio: "portfolio",
|
|
16
16
|
/** Products, services, events and membership plans (commerce-kit). */
|
|
17
|
-
commerce: "commerce"
|
|
17
|
+
commerce: "commerce",
|
|
18
|
+
/** Published managed landing pages. Draft previews never enter this cache. */
|
|
19
|
+
funnels: "sonor-funnels"
|
|
18
20
|
};
|
|
19
21
|
function isSiteRevalidatePong(body) {
|
|
20
22
|
return typeof body === "object" && body !== null && body.ok === true && body.ping === true;
|
package/dist/site-cache.mjs
CHANGED
|
@@ -12,7 +12,9 @@ var SITE_CACHE_TAGS = {
|
|
|
12
12
|
/** Portfolio items (agency-site-kit). */
|
|
13
13
|
portfolio: "portfolio",
|
|
14
14
|
/** Products, services, events and membership plans (commerce-kit). */
|
|
15
|
-
commerce: "commerce"
|
|
15
|
+
commerce: "commerce",
|
|
16
|
+
/** Published managed landing pages. Draft previews never enter this cache. */
|
|
17
|
+
funnels: "sonor-funnels"
|
|
16
18
|
};
|
|
17
19
|
function isSiteRevalidatePong(body) {
|
|
18
20
|
return typeof body === "object" && body !== null && body.ok === true && body.ping === true;
|
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, the forms honeypot, 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,
|
|
3
|
+
"version": "2.16.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, the forms honeypot, 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, and which inbox cards are decisions a bulk action leaves alone. Pure functions, no dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -130,8 +130,7 @@
|
|
|
130
130
|
"types": "./dist/page-titles.d.ts",
|
|
131
131
|
"import": "./dist/page-titles.mjs",
|
|
132
132
|
"require": "./dist/page-titles.js"
|
|
133
|
-
}
|
|
134
|
-
,
|
|
133
|
+
},
|
|
135
134
|
"./broadcast-layouts": {
|
|
136
135
|
"types": "./dist/broadcast-layouts.d.ts",
|
|
137
136
|
"import": "./dist/broadcast-layouts.mjs",
|
|
@@ -146,6 +145,16 @@
|
|
|
146
145
|
"types": "./dist/public-url-host.d.ts",
|
|
147
146
|
"import": "./dist/public-url-host.mjs",
|
|
148
147
|
"require": "./dist/public-url-host.js"
|
|
148
|
+
},
|
|
149
|
+
"./inbox": {
|
|
150
|
+
"types": "./dist/inbox.d.ts",
|
|
151
|
+
"import": "./dist/inbox.mjs",
|
|
152
|
+
"require": "./dist/inbox.js"
|
|
153
|
+
},
|
|
154
|
+
"./funnels": {
|
|
155
|
+
"types": "./dist/funnels.d.ts",
|
|
156
|
+
"import": "./dist/funnels.mjs",
|
|
157
|
+
"require": "./dist/funnels.js"
|
|
149
158
|
}
|
|
150
159
|
},
|
|
151
160
|
"typesVersions": {
|