@sonordev/site-kit 8.5.2 → 8.6.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/agent-manifest.json +1 -1
  3. package/dist/{AnalyticsProvider-HSJMHYBA.js → AnalyticsProvider-GVJYL6XY.js} +4 -4
  4. package/dist/{ArticleViewTracker-XEA6ZY67.js → ArticleViewTracker-AOJXEBYM.js} +5 -5
  5. package/dist/{BlocksPopup-DUTOENMQ.js → BlocksPopup-AJ3CKVQG.js} +4 -4
  6. package/dist/{ChatWidget-PMVQTSEU.js → ChatWidget-EYBVB5SZ.js} +6 -6
  7. package/dist/FileField-UEQYIFRO.js +237 -0
  8. package/dist/{FormSpotlight-RTPJPLQ3.js → FormSpotlight-H73ZZ66T.js} +2 -2
  9. package/dist/{FormStage-UANGD66Z.js → FormStage-2PLWWYBW.js} +2 -2
  10. package/dist/{ManagedForm-4AACRHEY.js → ManagedForm-ZQOFXEWS.js} +8 -8
  11. package/dist/{ManagedNewsletterForm-RWKFWZ72.js → ManagedNewsletterForm-5TE47YZD.js} +10 -10
  12. package/dist/{SignalCore-XULLPKPX.js → SignalCore-FEFNMNYN.js} +3 -3
  13. package/dist/{SiteChat-P4CRBRJZ.js → SiteChat-3MKYL2XM.js} +1 -1
  14. package/dist/{SiteDesignReporter-QDVP4WMK.js → SiteDesignReporter-AJDIBNHI.js} +5 -5
  15. package/dist/{SitePopups-US6VQN7X.js → SitePopups-C44YTQ3Q.js} +4 -4
  16. package/dist/{SitemapSync-5FBRDGHG.js → SitemapSync-UJA6JVPW.js} +4 -4
  17. package/dist/_client/booking-widget.js +5 -5
  18. package/dist/affiliates/index.js +3 -3
  19. package/dist/analytics/index.js +4 -4
  20. package/dist/articles/index.js +1 -1
  21. package/dist/articles/server-ui.js +3 -3
  22. package/dist/articles/server.js +2 -2
  23. package/dist/chat/index.js +7 -7
  24. package/dist/{chunk-N4ZCNYWF.js → chunk-6AWIUO2Z.js} +1 -1
  25. package/dist/{chunk-EQHGPB3J.js → chunk-6NXFA7HE.js} +1 -1
  26. package/dist/{chunk-5BRHTBPO.js → chunk-72F44HKL.js} +1 -1
  27. package/dist/{chunk-3PT6SO65.js → chunk-7R2STOVD.js} +5 -5
  28. package/dist/{chunk-ZSU7KTIE.js → chunk-AJG74ZZV.js} +2 -2
  29. package/dist/{chunk-CFSIGEUW.js → chunk-BUE3Z32O.js} +1 -1
  30. package/dist/{chunk-Q4Y3WIHE.js → chunk-CMUD37WV.js} +1 -1
  31. package/dist/{chunk-TYAVHRWH.js → chunk-CVADNHJ4.js} +8 -2
  32. package/dist/{chunk-A7RZQI6H.js → chunk-DSD3XZQQ.js} +1 -1
  33. package/dist/{chunk-24NM64IV.js → chunk-FTSEP7Q3.js} +2 -2
  34. package/dist/{chunk-HNYECNCU.js → chunk-GFY3E7EQ.js} +1 -1
  35. package/dist/{chunk-27LYZVPP.js → chunk-HMTPRBYJ.js} +2 -2
  36. package/dist/{chunk-ITQND6IB.js → chunk-IGAIQ2B3.js} +2 -2
  37. package/dist/{chunk-GDQKDL3E.js → chunk-IYCSDFPN.js} +2 -2
  38. package/dist/{chunk-T6U6J3QK.js → chunk-JSW42BHL.js} +1 -1
  39. package/dist/{chunk-OFQ3BU52.js → chunk-KTMHRXDT.js} +1 -1
  40. package/dist/{chunk-2QFIYYMP.js → chunk-L6QTERSH.js} +1 -1
  41. package/dist/{chunk-DLTLXYOV.js → chunk-NI6ZTP3D.js} +4 -9
  42. package/dist/{chunk-7HRYSLY2.js → chunk-NUXXFCIT.js} +2 -2
  43. package/dist/{chunk-ANVHRT35.js → chunk-O3Z5HHFP.js} +1 -1
  44. package/dist/{chunk-YW3LNSBM.js → chunk-OFSV5TZY.js} +1 -1
  45. package/dist/{chunk-GLLFIE2R.js → chunk-OWO3RFEX.js} +2 -2
  46. package/dist/{chunk-I4LMZNBN.js → chunk-QNOJSSQT.js} +1 -1
  47. package/dist/{chunk-OEA7U4HJ.js → chunk-QQT2T6JQ.js} +1 -1
  48. package/dist/{chunk-5SQK3D53.js → chunk-TYSIAWC3.js} +1 -0
  49. package/dist/{chunk-ZDUTXHM6.js → chunk-U34O6I7W.js} +2 -2
  50. package/dist/{chunk-FVXCQC7W.js → chunk-VA6IJ55K.js} +1 -1
  51. package/dist/{chunk-LNSF6BJC.js → chunk-VPZJ7ZTL.js} +2 -2
  52. package/dist/{chunk-UGZ3B5AK.js → chunk-YJH6WE76.js} +8 -8
  53. package/dist/{chunk-GTVOB5RN.js → chunk-YNBRVVTX.js} +1 -1
  54. package/dist/{chunk-IXA7ZB6U.js → chunk-ZZSAWXWD.js} +1 -1
  55. package/dist/client/index.js +4 -4
  56. package/dist/contracts/broadcast-layouts.d.ts +133 -0
  57. package/dist/contracts/business-claims.d.ts +456 -0
  58. package/dist/contracts/entries.d.ts +1 -1
  59. package/dist/contracts/outreach.d.ts +267 -0
  60. package/dist/contracts/page-titles.d.ts +140 -0
  61. package/dist/contracts/public-url-host.d.ts +10 -0
  62. package/dist/contracts/publication-branding.d.ts +143 -0
  63. package/dist/contracts/publication-charter.d.ts +14 -0
  64. package/dist/fleet/index.js +4 -4
  65. package/dist/forms/FileField.d.ts +3 -1
  66. package/dist/forms/PrivateFileField.d.ts +3 -0
  67. package/dist/forms/index.js +10 -10
  68. package/dist/forms/private-file-rules.d.ts +8 -0
  69. package/dist/forms/server.js +5 -5
  70. package/dist/forms/static.js +3 -3
  71. package/dist/forms/styles.css +17 -1
  72. package/dist/forms/types.d.ts +4 -0
  73. package/dist/images/index.js +4 -4
  74. package/dist/index.js +1 -1
  75. package/dist/layout/client.js +7 -7
  76. package/dist/layout/index.js +8 -8
  77. package/dist/llms/index.js +4 -4
  78. package/dist/maps/index.js +3 -3
  79. package/dist/mcp/sonor.js +9 -9
  80. package/dist/revalidate/index.js +2 -2
  81. package/dist/runtime/index.js +3 -3
  82. package/dist/seo/client.js +4 -4
  83. package/dist/seo/index.js +4 -4
  84. package/dist/seo/llms.js +4 -4
  85. package/dist/seo/register-sitemap-cli.js +1 -1
  86. package/dist/seo/sitemap.js +2 -2
  87. package/dist/server/index.js +2 -2
  88. package/dist/shared/version.d.ts +1 -1
  89. package/dist/signal/index.js +2 -2
  90. package/dist/sitemap/index.js +2 -2
  91. package/dist/sync/index.js +5 -5
  92. package/dist/website/images.js +4 -4
  93. package/dist/website/index.js +5 -5
  94. package/dist/website/popups.js +4 -4
  95. package/dist/{writeLLMsTxt-BAK5XMRR.js → writeLLMsTxt-KHVHNSDM.js} +1 -1
  96. package/package.json +1 -1
  97. package/src/forms/README.md +22 -0
  98. package/dist/FileField-F7BG4XAZ.js +0 -83
  99. /package/dist/{chunk-URZTBPZO.js → chunk-V67OD7RW.js} +0 -0
@@ -0,0 +1,267 @@
1
+ /** Bumped when a stored shape here (SegmentGrant, the segment key) changes incompatibly. */
2
+ export declare const OUTREACH_CONTRACT_VERSION = 1;
3
+ /** The badge Signal's outreach writer files and acts under. */
4
+ export declare const OUTREACH_WRITER_BADGE = "signal.outreach.writer";
5
+ /** The ledger kind segments belong to, and its key under settings.autonomy.segments. */
6
+ export declare const OUTREACH_EMAIL_KIND = "outreach_email";
7
+ /**
8
+ * Signal's one graduation bar: a track record earns sending without a click
9
+ * per item once at least minMeasured are measured, at least minWinRate of
10
+ * them won, and none degraded without being undone. Single source of truth
11
+ * for both APIs: Signal measures a kind or a segment against it, and an
12
+ * earned segment grant needs minMeasured receipts wherever
13
+ * grantEvidenceRefusal checks one.
14
+ */
15
+ export declare const GRADUATION_BAR: {
16
+ readonly minMeasured: 5;
17
+ readonly minWinRate: 0.7;
18
+ readonly maxUnrevertedDegradations: 0;
19
+ };
20
+ /**
21
+ * A segment's standing, one vocabulary everywhere:
22
+ * - ungranted: nobody turned it on (its next email is a first email);
23
+ * - granted: a person turned it on and it's sending without a per-email click;
24
+ * - paused: it stopped itself (a quality loss or a list signal) until a person resumes it;
25
+ * - lapsed: the pitch changed since it was turned on, so its next email is a first email again;
26
+ * - revoked: a person stopped it.
27
+ * "Awaiting first approval" is a drip check's answer, never a stored state.
28
+ */
29
+ export declare const SEGMENT_STATES: readonly ['ungranted', 'granted', 'paused', 'lapsed', 'revoked'];
30
+ export type SegmentState = (typeof SEGMENT_STATES)[number];
31
+ export type OutreachSegmentSource = 'drip' | 'mcp';
32
+ /** The vertical of a lead with no industry. Never grantable. */
33
+ export declare const UNSPECIFIED_INDUSTRY = "unspecified";
34
+ /**
35
+ * A prospect's industry as written, in the one order every caller uses: the
36
+ * target company's vertical, then its industry, then the lead's
37
+ * company_industry. Null when none is set.
38
+ */
39
+ export declare function prospectIndustry(prospect: {
40
+ vertical?: unknown;
41
+ industry?: unknown;
42
+ company_industry?: unknown;
43
+ } | null | undefined): string | null;
44
+ /**
45
+ * An industry as it appears in a segment key: NFKC, accents folded, lowercase,
46
+ * "&" read as "and", every run of anything that isn't a-z or 0-9 a single
47
+ * "-", trimmed, at most 48 characters. Empty is 'unspecified'. There's no
48
+ * synonym table: "Roofing" and "Roofers" are two segments.
49
+ */
50
+ export declare function normalizeIndustry(value: unknown): string;
51
+ /** The fields of an outreach_narratives row that are its pitch. Its name, its switch and its forbidden phrases aren't. */
52
+ export declare const NARRATIVE_PITCH_FIELDS: readonly ['positioning_statement', 'target_icp_description', 'tone_profile', 'hook_types', 'example_angles', 'value_prop_bullets'];
53
+ /** A narrative row, or any object carrying its pitch fields; every other column (name, enabled, forbidden_phrases, ...) is read past. */
54
+ export type NarrativePitch = {
55
+ [K in (typeof NARRATIVE_PITCH_FIELDS)[number]]?: unknown;
56
+ } & Record<string, unknown>;
57
+ /**
58
+ * The narrative's pitch, fingerprinted: FNV-1a of canonical JSON of
59
+ * NARRATIVE_PITCH_FIELDS only. Rewriting the pitch changes it, and that
60
+ * lapses a segment's grant. Renaming the narrative, switching it on or off,
61
+ * or tightening its forbidden phrases or avoid_tone doesn't: those take
62
+ * effect at once through the checker and the prompt.
63
+ */
64
+ export declare function narrativeFingerprint(narrative: NarrativePitch | null | undefined): string;
65
+ /** target_ref.segment on an outreach_email row: stamped once, at filing. */
66
+ export interface OutreachSegment {
67
+ key: string;
68
+ source: OutreachSegmentSource;
69
+ narrative_id: string | null;
70
+ mailbox_id: string | null;
71
+ vertical: string;
72
+ label: string;
73
+ angle_fp: string | null;
74
+ }
75
+ /**
76
+ * A segment: which pitch, from which mailbox, to which kind of business.
77
+ * - drip: `drip:<narrative uuid>@<mailbox uuid>|<vertical>`. Null when either id isn't a UUID.
78
+ * - mcp: `mcp:-|<vertical>`. For metrics only; never granted.
79
+ */
80
+ export declare function outreachSegment(input: {
81
+ source: OutreachSegmentSource;
82
+ narrativeId?: string | null;
83
+ mailboxId?: string | null;
84
+ industry?: unknown;
85
+ label?: string | null;
86
+ angleFp?: string | null;
87
+ }): OutreachSegment | null;
88
+ /** A segment's label: "<narrative name> to <industry as written>, from <mailbox address>". */
89
+ export declare function outreachSegmentLabel(input: {
90
+ narrativeName?: unknown;
91
+ industry?: unknown;
92
+ mailboxAddress?: unknown;
93
+ }): string;
94
+ export type ParsedOutreachSegmentKey = {
95
+ source: 'drip';
96
+ narrative_id: string;
97
+ mailbox_id: string;
98
+ vertical: string;
99
+ } | {
100
+ source: 'mcp';
101
+ vertical: string;
102
+ };
103
+ /**
104
+ * A segment key, parsed strictly, or null. Nothing else is ever used as a key
105
+ * in projects.settings, so a caller-chosen key ("__proto__", a long string)
106
+ * is refused before any read or write.
107
+ */
108
+ export declare function parseOutreachSegmentKey(key: unknown): ParsedOutreachSegmentKey | null;
109
+ /**
110
+ * Whether a segment can ever be turned on: a drip segment with a known
111
+ * industry. An mcp segment, or a lead with no industry, always waits for a
112
+ * person. (A row whose recipient Sonor already knows waits too; that's the
113
+ * row's relationship, checked separately.)
114
+ */
115
+ export declare function isGrantableSegmentKey(key: unknown): boolean;
116
+ /**
117
+ * A person's standing approval of one segment, at
118
+ * projects.settings.autonomy.segments.outreach_email[key]. Written only by
119
+ * Signal's AutonomyPolicyService.setSegmentGrant, refused on every generic
120
+ * settings writer, and only ever trusted after grantEvidenceRefusal passes.
121
+ */
122
+ export interface SegmentGrant {
123
+ state: 'granted' | 'revoked';
124
+ /** first_email: a person approved the segment's first email unedited with "Send the rest like this". earned: they accepted Signal's offer once the bar was met. */
125
+ source: 'first_email' | 'earned';
126
+ /** contacts.id of the person who turned it on. */
127
+ by: string;
128
+ /** Where they did it. Never MCP. */
129
+ via: 'dashboard' | 'drafts';
130
+ at: string;
131
+ /** first_email: the ledger id of that email. earned: the offer's dedup key. */
132
+ ref: string;
133
+ /** earned: the ledger ids of the wins that earned it. */
134
+ receipts?: string[];
135
+ /** The mailbox it sends from. A grant never covers another mailbox. */
136
+ mailbox_id: string;
137
+ label: string;
138
+ /** narrativeFingerprint when it was turned on. A rewritten pitch lapses it. */
139
+ angle_fp: string;
140
+ /** Stopped itself. Cleared only by a person's resume. */
141
+ paused?: {
142
+ at: string;
143
+ reason: string;
144
+ loss_ref: string | null;
145
+ };
146
+ resumed?: {
147
+ by: string;
148
+ at: string;
149
+ };
150
+ revoked?: {
151
+ by: string;
152
+ at: string;
153
+ reason: string;
154
+ };
155
+ }
156
+ /**
157
+ * Every well-formed grant, by segment key. Pass projects.settings or its
158
+ * autonomy document. A key that doesn't parse strictly, or a grant that isn't
159
+ * the SegmentGrant shape, is left out: an unreadable grant counts as no grant.
160
+ */
161
+ export declare function readSegmentGrants(source: unknown): Record<string, SegmentGrant>;
162
+ /** The grant for a segment when it's live (granted, not paused, on a grantable key), else null. Evidence is checked separately. */
163
+ export declare function activeSegmentGrant(source: unknown, key: string): SegmentGrant | null;
164
+ export type SegmentGrantCoverage = {
165
+ covers: true;
166
+ } | {
167
+ covers: false;
168
+ code: 'not_granted' | 'revoked' | 'paused' | 'other_person' | 'granted_after' | 'angle_changed' | 'other_mailbox';
169
+ reason: string;
170
+ };
171
+ /**
172
+ * Whether a live grant covers one row: refused when there's no grant, it was
173
+ * revoked or paused, the row names another approver, the row was filed
174
+ * before the grant, the pitch changed since (a different fingerprint), or the
175
+ * row sends from another mailbox.
176
+ */
177
+ export declare function segmentGrantCovers(grant: SegmentGrant | null | undefined, row: {
178
+ by?: string | null;
179
+ createdAt: string | null | undefined;
180
+ angleFp: string | null | undefined;
181
+ mailboxId: string | null | undefined;
182
+ }): SegmentGrantCoverage;
183
+ /** The columns of a signal_proposals row grantEvidenceRefusal reads. */
184
+ export interface GrantEvidenceRow {
185
+ id: string;
186
+ kind: string;
187
+ status: string;
188
+ project_id?: string | null;
189
+ created_by?: string | null;
190
+ reviewed_by?: string | null;
191
+ reviewed_at?: string | null;
192
+ target_ref?: unknown;
193
+ payload?: unknown;
194
+ }
195
+ export interface GrantEvidenceScope {
196
+ /** The segment key the grant is stored under. */
197
+ key: string;
198
+ /** GRADUATION_BAR.minMeasured: how many receipts an earned grant needs. */
199
+ minMeasured: number;
200
+ /** The project the grant is stored on. Rows from another project never count. */
201
+ projectId?: string | null;
202
+ /** contacts.id of reviewers known to be people (not agents). When given, every reviewer must be in it. */
203
+ people?: ReadonlySet<string> | ReadonlyArray<string> | null;
204
+ }
205
+ /**
206
+ * Why a grant can't be trusted, or null when the ledger rows it cites stand
207
+ * behind it. A grant that fails counts as no grant, so a grant forged into
208
+ * settings never sends anything.
209
+ *
210
+ * - first_email: grant.ref is an applied outreach_email the writer filed in
211
+ * this project and segment, from the grant's mailbox, approved by grant.by
212
+ * at or before grant.at, in Sonor (via dashboard or drafts, never MCP),
213
+ * unedited, and read back verified.
214
+ * - earned: at least scope.minMeasured distinct receipts, each an applied
215
+ * writer row in this segment and mailbox, for the same pitch, approved by a
216
+ * person in Sonor at or before grant.at, unedited and read back verified.
217
+ */
218
+ export declare function grantEvidenceRefusal(grant: SegmentGrant | null | undefined, refRows: ReadonlyArray<GrantEvidenceRow> | null | undefined, scope: GrantEvidenceScope): string | null;
219
+ /** The live roles of the person asking, read by the caller. */
220
+ export interface SegmentGranterRoles {
221
+ contact_type?: string | null;
222
+ is_platform_admin?: boolean | null;
223
+ /** Their role in the project's org. */
224
+ org_role?: string | null;
225
+ /** Their role on the project itself. */
226
+ project_role?: string | null;
227
+ /** Their role in the agency that manages the project's org. */
228
+ managing_org_role?: string | null;
229
+ }
230
+ /**
231
+ * Whether this person may turn a segment on (or resume it): never an agent
232
+ * contact; otherwise a platform admin, or an owner or admin of the project,
233
+ * its org, or the agency that manages that org.
234
+ */
235
+ export declare function segmentGranterEligible(roles: SegmentGranterRoles | null | undefined): boolean;
236
+ /**
237
+ * Domains anyone can sign up for. A matching address says nothing about the
238
+ * company someone works for, so a relationship check never matches on one.
239
+ */
240
+ export declare const PUBLIC_MAILBOX_DOMAINS: ReadonlyArray<string>;
241
+ /** Whether an address or a domain is a public mailbox provider's. Case and a leading "mailto:" or "@" don't matter. */
242
+ export declare function isPublicMailboxDomain(value: unknown): boolean;
243
+ /**
244
+ * - live: still waiting on a person or a held send. Leave the lead reserved.
245
+ * - sent: the email went out. The lead moves on.
246
+ * - retire: a person said it shouldn't go to this person. Drop the lead.
247
+ * - declined: a person turned this email down for another reason. The lead goes back in line.
248
+ * - refile: nothing was decided about the lead (wrong timing, expired,
249
+ * superseded, the kind switched off, a send that never went out). It may be
250
+ * filed again after its cooldown.
251
+ */
252
+ export type DripRowDisposition = 'live' | 'sent' | 'retire' | 'declined' | 'refile';
253
+ /** The columns of a signal_proposals row dripRowDisposition reads. */
254
+ export interface DripLedgerRow {
255
+ status: string;
256
+ review_reason?: string | null;
257
+ reviewed_by?: string | null;
258
+ autonomy_decision?: unknown;
259
+ payload?: unknown;
260
+ }
261
+ /**
262
+ * What a drip's ledger row means for its lead. Signal's filing dedup and the
263
+ * Sonor API's reservation release both read it, so "decided" means one thing.
264
+ * A status it doesn't know reads as live: a lead stays reserved rather than
265
+ * being written to twice.
266
+ */
267
+ export declare function dripRowDisposition(row: DripLedgerRow | null | undefined): DripRowDisposition;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Page-titles contract: what a page is called when a list, a sitemap or a
3
+ * case study needs its own name and not its whole <title>. The Sonor APIs and
4
+ * the agency site kit import it as `@sonordev/contracts/page-titles`.
5
+ *
6
+ * Why it exists: three places each turned a stored title or a URL slug into a
7
+ * label with their own logic, and they disagreed. "ac-replacement" became "Ac
8
+ * Replacement", "faq" became "Faq", "smith-and-smith" became "Smith And
9
+ * Smith", and a title with a stray second pipe shipped as "Loans | | Brand".
10
+ * There's one rule now:
11
+ *
12
+ * - `titleHead` is the page's own name: the title without its trailing
13
+ * " | Brand" segment. Prefer it. A page's title is the label it was
14
+ * written with, so it beats anything guessed from a URL.
15
+ * - `slugLabel` is for the rare case a label has to come from a slug, a file
16
+ * name or a path, because the page has no title at all.
17
+ * - `isRoutePlaceholder` says whether a value is a framework route
18
+ * placeholder (":slug", "[slug]", "[...rest]") and not a page's name.
19
+ * - `isPageAddress` says whether a value is a page's address (a path, a
20
+ * URL, a route pattern, or the page's own path without its slashes) and
21
+ * not its name. It's the one "is this title really a path" rule, so the
22
+ * writer of a page list and the reader that prints it can't disagree.
23
+ * - `isPageName` says whether a stored title is a name: something is left
24
+ * once the pipes are set aside, and no piece between them is an address.
25
+ * It's the one rule for a title that gets printed whole, so the writer
26
+ * that stores a name and the reader that prints it can't disagree about
27
+ * a path, a URL or a placeholder behind a pipe.
28
+ *
29
+ * Pure string logic: no I/O, no Node built-ins. The brand split is
30
+ * seo-meta's `stripTitleSuffix`, imported, never copied: only the LAST
31
+ * " | " segment of a title is its brand.
32
+ */
33
+ /**
34
+ * The page's own name: the title without its trailing " | Brand" segment.
35
+ *
36
+ * Whitespace is collapsed first. Stray pipes are repaired before the brand is
37
+ * cut, so "Loans | | Brand" reads as "Loans | Brand" (a run of pipes with
38
+ * only whitespace between them is one separator) and a leading or trailing
39
+ * pipe is dropped ("Pricing |" is "Pricing"). Then the last " | " segment is
40
+ * the brand and goes; anything before it stays, pipes and all:
41
+ * "1 Bedroom | 1BR 1BA | Example Apartments" is "1 Bedroom | 1BR 1BA".
42
+ *
43
+ * Apply it once to a stored title. A title with two or more separators
44
+ * loses one segment per call, so its own output isn't a fixed point.
45
+ *
46
+ * A title with no separator comes back as written (trimmed): this can't tell
47
+ * a brand from a name, so a title that's only a brand stays that brand.
48
+ * Empty, whitespace-only, non-string, null and undefined input give "".
49
+ */
50
+ export declare function titleHead(title: string | null | undefined): string;
51
+ /**
52
+ * A readable label for a slug, a file name or a path: "ac-replacement" is
53
+ * "AC Replacement", "/services/hvac-repair/" is "HVAC Repair".
54
+ *
55
+ * Reach for it only when the page has no title (`titleHead` is the page's own
56
+ * name). It reads the last non-empty path segment of the input, after
57
+ * dropping a "https://host" origin, a query or a hash, and decodes %-escapes
58
+ * without ever throwing. A trailing .html, .htm, .php or .aspx is dropped. The
59
+ * words split on "-", "_", "+" and whitespace.
60
+ *
61
+ * Each word is cased by the first rule that fits:
62
+ * - an acronym is upper case ("ac", "ai", "cms", "faq", "hvac", "seo",
63
+ * "llc", "mls", ...), and "faqs" is "FAQs";
64
+ * - a two-letter US state or DC code is upper case ("ky" is "KY"), apart
65
+ * from the codes that are ordinary words as often as not ("in", "or",
66
+ * "me", "hi", "id", "la", "ma", "pa", "al", "de", "co"). "mt" goes by its
67
+ * position: last, it's Montana ("billings-mt" is "Billings MT"); with a
68
+ * word after it, it's Mount ("mt-adams" is "Mt Adams"). "us" isn't a
69
+ * state: "about-us" is "About Us";
70
+ * - a word with a digit keeps its case and only gains a leading capital
71
+ * when it starts with a letter ("c1" is "C1", "075ct" stays);
72
+ * - a small word ("a", "an", "and", "at", "by", "for", "in", "of", "on",
73
+ * "or", "the", "to", "vs", "with") is lower case, unless it's first;
74
+ * - anything else is capitalized with the rest lower case.
75
+ *
76
+ * Nothing left to label ("", "/", "?x=1") gives "". The home page is the
77
+ * caller's to name. A label isn't a title: "index" isn't special-cased,
78
+ * and a brand's own capitalization ("DeWalt") isn't recoverable from a slug.
79
+ */
80
+ export declare function slugLabel(input: string): string;
81
+ /**
82
+ * True when the value, or any "/"-separated segment of it, is a framework
83
+ * route placeholder: a page's pattern leaked into its name ("/floor-plans/:slug",
84
+ * ":Slug", "[...rest]"). A placeholder is one token with no spaces, so
85
+ * ordinary text that contains a colon or a bracket isn't one ("Update: new
86
+ * hours", "FAQ / [draft] notes"). Nullish, empty and non-string values are
87
+ * false.
88
+ */
89
+ export declare function isRoutePlaceholder(value: string | null | undefined): boolean;
90
+ /**
91
+ * Whether `value` is a page's address and not its name: what a page's title
92
+ * becomes when nothing better was stored (the old sitemap writers saved the
93
+ * path there), or a pattern that leaked out of the router. True for:
94
+ *
95
+ * - a path, or anything starting with a slash ("/services/ac-repair",
96
+ * "//example.com/services");
97
+ * - an absolute URL ("https://example.com/services");
98
+ * - a host and a path with no scheme ("example.com/services");
99
+ * - a route placeholder, as `isRoutePlaceholder` reads it ("/floor-plans/:slug",
100
+ * ":Slug", "[slug]");
101
+ * - the page's own path written without its slashes. Given `path`, the
102
+ * comparison drops leading and trailing slashes from both sides, so for
103
+ * "/services/ac-repair" the values "services/ac-repair" and
104
+ * "services/ac-repair/" are the address, and for "/faq" the value "faq" is.
105
+ * It compares the whole path and it's case-sensitive: "ac-repair" isn't
106
+ * the address of "/services/ac-repair", and "Pricing" for "/pricing" is a
107
+ * name where "pricing" is the path.
108
+ *
109
+ * The value is trimmed first. Empty, whitespace-only, nullish and non-string
110
+ * values give false: they aren't an address, they're nothing, and the caller
111
+ * decides what an empty title means. `path` is optional; without it only the
112
+ * checks that don't need the page's own path run.
113
+ *
114
+ * It judges one string. A title with a " | Brand" tail is `titleHead`'s to
115
+ * cut, and the caller asks about the head: `isPageAddress(titleHead(title), path)`.
116
+ * Pure string logic, linear in the length of the input.
117
+ */
118
+ export declare function isPageAddress(value: string | null | undefined, path?: string | null): boolean;
119
+ /**
120
+ * Whether a stored title is a real name for the page at `path`: something is
121
+ * left once the pipes are set aside, and none of its pieces is a page's
122
+ * address (`isPageAddress`, given the page's path). It's the one rule for a
123
+ * title that gets printed whole, so whoever stores a name and whoever prints
124
+ * it can't disagree.
125
+ *
126
+ * Every piece is judged, the first and the last included, because a title
127
+ * with a pipe in it prints with its pipes: ":slug | Example Apartments" (a
128
+ * placeholder in front of a brand tail) and "Floor Plans | /floor-plans/x" (a
129
+ * path behind the name) are both refused, and so is the unspaced
130
+ * "Floor Plans|/floor-plans/x". A name with a pipe in it stays a name while
131
+ * every piece is text ("1 Bedroom | 1BR 1BA"). A title that's only pipes has
132
+ * no piece and isn't a name. Empty, whitespace-only, nullish and non-string
133
+ * values aren't names either.
134
+ *
135
+ * It judges the title it is given, brand tail and all. A writer cuts the tail
136
+ * first and asks about what it stores: `isPageName(titleHead(title), path)`.
137
+ * `path` is optional, as it is for `isPageAddress`. Pure string logic, linear
138
+ * in the length of the input.
139
+ */
140
+ export declare function isPageName(title: string | null | undefined, path?: string | null): boolean;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The shared literal-host SSRF policy. Pass URL.hostname (which canonicalizes
3
+ * numeric IPv4 spellings), or an address returned by a DNS lookup. IPv6 may
4
+ * have the brackets URL.hostname retains. This isn't a URL validator and
5
+ * doesn't trim input, resolve DNS, follow redirects or open a connection.
6
+ * Callers still enforce their protocol/port rules and check or pin the DNS
7
+ * addresses used by each connection; a false result alone can't prove a
8
+ * destination is public.
9
+ */
10
+ export declare function isBlockedPublicUrlHost(rawHost: string): boolean;
@@ -0,0 +1,143 @@
1
+ import { type BroadcastStyleId, type PublicationCardLayout } from './broadcast-layouts';
2
+ import { type PublicationCharter } from './publication-charter';
3
+ export { PUBLICATION_CHARTER_FIELDS, parsePublicationCharter, parseSiteCharters, publicationCharterContext, publicationCharterFor, type PublicationCharter, } from './publication-charter';
4
+ /** The stored profile's version. A breaking change to the shape bumps it. */
5
+ export declare const PUBLICATION_BRANDING_VERSION: 1;
6
+ /** Where a publication appears. `social` overrides every social network; each network can override again. */
7
+ export declare const PUBLICATION_PLATFORMS: readonly ['publication', 'email', 'social', 'instagram', 'facebook', 'linkedin', 'gbp'];
8
+ export type PublicationPlatform = (typeof PUBLICATION_PLATFORMS)[number];
9
+ /** The sections of a profile and of each platform override, in the order they're read. */
10
+ export declare const PUBLICATION_BRANDING_SECTIONS: readonly ['identity', 'colors', 'typography', 'editorial', 'newsletter'];
11
+ export type PublicationBrandingSection = (typeof PUBLICATION_BRANDING_SECTIONS)[number];
12
+ export declare const PUBLICATION_IDENTITY_FIELDS: readonly ['publicationName', 'wordmark', 'tagline', 'publisherName', 'publisherUrl', 'publisherLogoUrl', 'logos'];
13
+ export declare const PUBLICATION_LOGO_VARIANTS: readonly ['default', 'light', 'dark'];
14
+ export declare const PUBLICATION_COLOR_FIELDS: readonly ['primary', 'secondary', 'accent', 'background', 'surface', 'text', 'muted', 'border'];
15
+ export declare const PUBLICATION_TYPOGRAPHY_FIELDS: readonly ['heading', 'body', 'mono', 'emailHeadingFallback', 'emailBodyFallback'];
16
+ export declare const PUBLICATION_EDITORIAL_FIELDS: readonly ['articleStyle', 'cardLayout', 'charter', 'siteCharters'];
17
+ export declare const PUBLICATION_NEWSLETTER_FIELDS: readonly ['name', 'tagline', 'layout', 'maxWidth'];
18
+ /** Fonts every email client has, the only families an email fallback may name. Lowercase. */
19
+ export declare const EMAIL_SAFE_FONTS: ReadonlySet<string>;
20
+ /** CSS generic families: written bare and lowercase in a font stack, never quoted. */
21
+ export declare const GENERIC_FONT_FAMILIES: ReadonlySet<string>;
22
+ export interface PublicationIdentity {
23
+ publicationName?: string;
24
+ wordmark?: string;
25
+ tagline?: string;
26
+ publisherName?: string;
27
+ publisherUrl?: string;
28
+ /** The publisher's attribution mark, separate from the publication masthead. */
29
+ publisherLogoUrl?: string;
30
+ logos?: {
31
+ default?: string;
32
+ light?: string;
33
+ dark?: string;
34
+ };
35
+ }
36
+ export interface PublicationColors {
37
+ primary?: string;
38
+ secondary?: string;
39
+ accent?: string;
40
+ background?: string;
41
+ surface?: string;
42
+ text?: string;
43
+ muted?: string;
44
+ border?: string;
45
+ }
46
+ export interface PublicationTypography {
47
+ heading?: string;
48
+ body?: string;
49
+ mono?: string;
50
+ emailHeadingFallback?: string;
51
+ emailBodyFallback?: string;
52
+ }
53
+ export interface PublicationEditorial {
54
+ charter?: PublicationCharter;
55
+ siteCharters?: Record<string, PublicationCharter>;
56
+ /** Shared Broadcast style for articles, reviews and generated posts; portfolio is excluded. Legacy wire name. */
57
+ articleStyle?: BroadcastStyleId;
58
+ cardLayout?: PublicationCardLayout;
59
+ }
60
+ export interface PublicationNewsletter {
61
+ name?: string;
62
+ tagline?: string;
63
+ layout?: 'editorial';
64
+ maxWidth?: 600 | 640;
65
+ }
66
+ export interface PublicationBrandingSections {
67
+ identity?: PublicationIdentity;
68
+ colors?: PublicationColors;
69
+ typography?: PublicationTypography;
70
+ editorial?: PublicationEditorial;
71
+ newsletter?: PublicationNewsletter;
72
+ }
73
+ /** Exactly the object stored in projects.settings.publication_branding. */
74
+ export interface PublicationBranding extends PublicationBrandingSections {
75
+ version: typeof PUBLICATION_BRANDING_VERSION;
76
+ platforms?: Partial<Record<PublicationPlatform, PublicationBrandingSections>>;
77
+ }
78
+ export type PublicationBrandingProfile = PublicationBranding;
79
+ type LeafPatch<T> = {
80
+ [K in keyof T]?: NonNullable<T[K]> extends object ? LeafPatch<NonNullable<T[K]>> : T[K] | null;
81
+ };
82
+ export type PublicationBrandingSectionsPatch = LeafPatch<PublicationBrandingSections>;
83
+ /** A change: any leaf set to a value, or to null to clear that override. */
84
+ export interface PublicationBrandingPatch extends PublicationBrandingSectionsPatch {
85
+ version?: typeof PUBLICATION_BRANDING_VERSION;
86
+ platforms?: Partial<Record<PublicationPlatform, PublicationBrandingSectionsPatch>>;
87
+ }
88
+ /**
89
+ * A refusal, naming the field: `publication_branding.colors.primary: use a
90
+ * valid opaque color`. One case throws a plain Error instead: a charter set on
91
+ * a channel override, which belongs to the publication or a site.
92
+ */
93
+ export declare class PublicationBrandingValidationError extends Error {
94
+ readonly path: string;
95
+ constructor(path: string, reason: string);
96
+ }
97
+ /** True for a control character (U+0000-U+001F, U+007F) anywhere in the text. */
98
+ export declare function containsControlCharacters(value: string): boolean;
99
+ /**
100
+ * A CSS font stack as data, never a stylesheet fragment: quoted family names
101
+ * and lowercase generic families joined by commas ("'Brand Display',serif"),
102
+ * or undefined when the value carries rules, variables, url() or anything
103
+ * that isn't a family name. The theme aliases heading, body and mono drop out.
104
+ */
105
+ export declare function safeFontStack(value: unknown): string | undefined;
106
+ export interface PublicationBrandingValidatorOptions {
107
+ /**
108
+ * True when a URL's hostname must never be stored as a logo or publisher
109
+ * URL: loopback, private and link-local ranges, `localhost` and the like.
110
+ * Each API passes the shared `public-url-host` policy. It receives
111
+ * `new URL(value).hostname`, so IPv6 keeps its brackets.
112
+ */
113
+ isBlockedHost: (hostname: string) => boolean;
114
+ }
115
+ export interface PublicationBrandingValidator {
116
+ /** A whole saved profile. Retired card layouts still read. */
117
+ validatePublicationBranding(value: unknown): PublicationBranding;
118
+ /** A change. Leaves may be null (clear that override); a card layout must be one drawn today. */
119
+ validatePublicationBrandingPatch(value: unknown): PublicationBrandingPatch;
120
+ /** The sections of a profile or a platform override, at `path`. */
121
+ validatePublicationBrandingSections(input: unknown, path: string, patch: boolean): PublicationBrandingSections | PublicationBrandingSectionsPatch;
122
+ /**
123
+ * The saved profile with the change applied: a null leaf clears just that
124
+ * override and empty containers are pruned. No profile yet reads as
125
+ * `{ version: 1 }`; a change that sets nothing returns the profile as it was.
126
+ */
127
+ mergePublicationBrandingPatch(current: unknown, patch: unknown): PublicationBranding;
128
+ /** One text leaf: trimmed, 1 to max characters (0 when empty is allowed), no control characters. */
129
+ text(value: unknown, path: string, max: number, empty?: boolean): string;
130
+ /** One URL leaf: a public HTTPS URL without credentials, normalized. */
131
+ url(value: unknown, path: string): string;
132
+ /** One font leaf, normalized by safeFontStack; email fallbacks may name only EMAIL_SAFE_FONTS. */
133
+ font(value: unknown, path: string, emailSafe?: boolean): string;
134
+ }
135
+ /**
136
+ * Leaf-wise merge of a validated change into a profile layer: a null leaf
137
+ * deletes that key, an object merges recursively, and a container left empty
138
+ * is removed. The inputs are never mutated. The Sonor API layers resolved
139
+ * branding with the same merge.
140
+ */
141
+ export declare function mergePublicationBrandingLayers(current: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown>;
142
+ /** Single source of truth for publication-branding validation, bound to the caller's host guard. */
143
+ export declare function createPublicationBrandingValidator(options: PublicationBrandingValidatorOptions): PublicationBrandingValidator;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A publication's editorial charter: data-only direction (who it's for, what
3
+ * they're deciding, the proof it holds itself to), never evidence of
4
+ * expertise or permission to publish private material. Part of
5
+ * `@sonordev/contracts/publication-branding`, which re-exports it; Signal's
6
+ * writers read it into their prompts and the Sonor API stores it in a
7
+ * publication's branding. Pure: no I/O, no Node built-ins.
8
+ */
9
+ export declare const PUBLICATION_CHARTER_FIELDS: readonly ['audience', 'readerDecisions', 'perspective', 'expertise', 'proofStandards', 'tone', 'recurringSeries', 'nonAuthority'];
10
+ export type PublicationCharter = Partial<Record<typeof PUBLICATION_CHARTER_FIELDS[number], string>>;
11
+ export declare function parsePublicationCharter(value: unknown, patch?: boolean): PublicationCharter;
12
+ export declare function parseSiteCharters(value: unknown, patch?: boolean): Record<string, PublicationCharter>;
13
+ export declare function publicationCharterFor(editorial: unknown, site?: string | null): PublicationCharter;
14
+ export declare function publicationCharterContext(charter?: PublicationCharter): string;
@@ -1,11 +1,11 @@
1
1
  'use client';
2
- export { FLEET_HEARTBEAT_PATH, FleetHeartbeat, detectNextVersion, sendFleetHeartbeat } from '../chunk-ZDUTXHM6.js';
2
+ export { FLEET_HEARTBEAT_PATH, FleetHeartbeat, detectNextVersion, sendFleetHeartbeat } from '../chunk-U34O6I7W.js';
3
3
  export { FLEET_CONTRACT_VERSION, FLEET_MODULES, FLEET_MODULES_MAX, FLEET_SITE_MAX_LENGTH, FLEET_VERSION_MAX_LENGTH, FLEET_VERSION_PATTERN, buildFleetHeartbeatPayload, isFleetModule, normalizeFleetModules, normalizeFleetSite, sanitizeFleetVersion } from '../chunk-ZTZQPYP6.js';
4
4
  import '../chunk-YC7ELZS3.js';
5
5
  import '../chunk-DLM4FFNH.js';
6
6
  import '../chunk-EKBEOXTH.js';
7
- import '../chunk-GDQKDL3E.js';
8
- import '../chunk-OEA7U4HJ.js';
7
+ import '../chunk-IYCSDFPN.js';
8
+ import '../chunk-QQT2T6JQ.js';
9
9
  import '../chunk-W7OPEMGJ.js';
10
- import '../chunk-CFSIGEUW.js';
10
+ import '../chunk-BUE3Z32O.js';
11
11
  import '../chunk-PKBMQBKP.js';
@@ -24,5 +24,7 @@ export interface FileFieldProps {
24
24
  classPrefix: string;
25
25
  baseInputStyle: React.CSSProperties;
26
26
  onChange: (value: unknown) => void;
27
+ ariaInvalid?: boolean;
28
+ describedBy?: string;
27
29
  }
28
- export declare function FileField({ field, inputId, classPrefix, baseInputStyle, onChange, }: FileFieldProps): React.JSX.Element;
30
+ export declare function FileField({ field, inputId, classPrefix, baseInputStyle, onChange, ariaInvalid, describedBy, }: FileFieldProps): React.JSX.Element;
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ import type { FileFieldProps } from './FileField';
3
+ export declare function PrivateFileField({ field, inputId, onChange, ariaInvalid, describedBy }: Pick<FileFieldProps, 'field' | 'inputId' | 'onChange' | 'ariaInvalid' | 'describedBy'>): React.JSX.Element;
@@ -1,25 +1,25 @@
1
1
  'use client';
2
- export { configureFormsApi, defineForm, field, formsApi, initializeForms } from '../chunk-2QFIYYMP.js';
3
- export { FormClient, ManagedForm } from '../chunk-3PT6SO65.js';
4
- import '../chunk-DLTLXYOV.js';
2
+ export { configureFormsApi, defineForm, field, formsApi, initializeForms } from '../chunk-L6QTERSH.js';
3
+ export { FormClient, ManagedForm } from '../chunk-7R2STOVD.js';
4
+ import '../chunk-NI6ZTP3D.js';
5
5
  export { DatePicker } from '../chunk-SOD2F6C2.js';
6
6
  export { FormCelebration } from '../chunk-YGGDU6C3.js';
7
- export { useForm, useFormTracking } from '../chunk-27LYZVPP.js';
7
+ export { useForm, useFormTracking } from '../chunk-HMTPRBYJ.js';
8
8
  import '../chunk-Q6E4OPGG.js';
9
9
  import '../chunk-5ROTTMBL.js';
10
- export { FormEnhancer } from '../chunk-GLLFIE2R.js';
10
+ export { FormEnhancer } from '../chunk-OWO3RFEX.js';
11
11
  import '../chunk-C2FZBUSS.js';
12
12
  import '../chunk-YC7ELZS3.js';
13
13
  import '../chunk-DXO6SBMD.js';
14
- export { FormField as FormFieldComponent, autocompleteFor } from '../chunk-TYAVHRWH.js';
15
- export { DEFAULT_FORM_SUCCESS_MESSAGE } from '../chunk-5SQK3D53.js';
14
+ export { FormField as FormFieldComponent, autocompleteFor } from '../chunk-CVADNHJ4.js';
15
+ export { DEFAULT_FORM_SUCCESS_MESSAGE } from '../chunk-TYSIAWC3.js';
16
16
  import '../chunk-HW7B43E5.js';
17
17
  import '../chunk-NJCTQH2P.js';
18
18
  import '../chunk-L2V5PUFN.js';
19
19
  import '../chunk-GJWI74ZZ.js';
20
20
  import '../chunk-5TM3WPCI.js';
21
- import '../chunk-GDQKDL3E.js';
22
- import '../chunk-OEA7U4HJ.js';
21
+ import '../chunk-IYCSDFPN.js';
22
+ import '../chunk-QQT2T6JQ.js';
23
23
  import '../chunk-W7OPEMGJ.js';
24
- import '../chunk-CFSIGEUW.js';
24
+ import '../chunk-BUE3Z32O.js';
25
25
  import '../chunk-PKBMQBKP.js';
@@ -0,0 +1,8 @@
1
+ export declare const PRIVATE_FILE_MAX_BYTES: number;
2
+ export declare const PRIVATE_FILE_TYPES: readonly ['application/pdf', 'image/jpeg', 'image/png', 'image/webp'];
3
+ export declare const PRIVATE_FILE_ACCEPT = ".pdf,.jpg,.jpeg,.png,.webp";
4
+ export declare function validatePrivateFileSelection(files: Array<{
5
+ name: string;
6
+ size: number;
7
+ type: string;
8
+ }>, maxFiles?: number): string | null;