@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.
@@ -0,0 +1,560 @@
1
+ // src/contracts/sites-normalize.ts
2
+ function normalizeSiteHost(site2) {
3
+ if (!site2 || typeof site2 !== "string") return null;
4
+ let s = site2.trim().toLowerCase();
5
+ if (!s) return null;
6
+ s = s.replace(/^https?:\/\//, "");
7
+ const slash = s.indexOf("/");
8
+ if (slash >= 0) s = s.slice(0, slash);
9
+ const colon = s.indexOf(":");
10
+ if (colon >= 0) s = s.slice(0, colon);
11
+ s = s.replace(/\.+$/, "");
12
+ return s || null;
13
+ }
14
+
15
+ // src/contracts/public-url-host.ts
16
+ function isPrivateIpv4(a, b) {
17
+ 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;
18
+ }
19
+ function ipv6Words(host) {
20
+ if (!host.includes(":") || !/^[a-f\d:.]+$/.test(host)) return null;
21
+ let canonical;
22
+ try {
23
+ canonical = new URL(`http://[${host}]/`).hostname.slice(1, -1);
24
+ } catch {
25
+ return null;
26
+ }
27
+ const sides = canonical.split("::");
28
+ const left = sides[0] ? sides[0].split(":") : [];
29
+ const right = sides.length > 1 && sides[1] ? sides[1].split(":") : [];
30
+ return [...left, ...Array(sides.length > 1 ? 8 - left.length - right.length : 0).fill("0"), ...right].map((word) => parseInt(word, 16));
31
+ }
32
+ function isBlockedPublicUrlHost(rawHost) {
33
+ const host = rawHost.toLowerCase().replace(/^\[|\]$/g, "");
34
+ const blockedName = host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal");
35
+ if (host.includes(":") && host.includes("%")) return true;
36
+ const ipv4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
37
+ const blockedIp = !!ipv4 && isPrivateIpv4(Number(ipv4[1]), Number(ipv4[2]));
38
+ const words = ipv6Words(host);
39
+ const blockedIpv6 = !!words && // Unspecified/loopback, unique-local, link-local, deprecated site-local
40
+ // and multicast destinations aren't public logo/webhook endpoints.
41
+ (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.
42
+ words.slice(0, 5).every((word) => word === 0) && [0, 65535].includes(words[5]) && isPrivateIpv4(words[6] >> 8, words[6] & 255));
43
+ return blockedName || blockedIp || blockedIpv6;
44
+ }
45
+
46
+ // src/contracts/publication-branding.ts
47
+ var PUBLICATION_COLOR_FIELDS = ["primary", "secondary", "accent", "background", "surface", "text", "muted", "border"];
48
+ var GENERIC_FONT_FAMILIES = /* @__PURE__ */ new Set(["serif", "sans-serif", "monospace", "system-ui", "cursive", "fantasy", "ui-serif", "ui-sans-serif", "ui-monospace"]);
49
+ function containsControlCharacters(value) {
50
+ for (const character of value) {
51
+ const code = character.charCodeAt(0);
52
+ if (code <= 31 || code === 127) return true;
53
+ }
54
+ return false;
55
+ }
56
+ function safeFontStack(value) {
57
+ if (typeof value !== "string" || value.length > 512 || containsControlCharacters(value) || /[;{}<>\\]|(?:var|url|expression)\s*\(/i.test(value)) return void 0;
58
+ const names = value.split(",").map((v) => v.trim().replace(/^(['"])(.*)\1$/, "$2")).filter(Boolean).filter((v) => !/^(?:heading|body|mono)$/i.test(v));
59
+ if (!names.length || names.some((v) => !/^[\p{L}\p{N} _.-]+$/u.test(v))) return void 0;
60
+ return names.map((v) => GENERIC_FONT_FAMILIES.has(v.toLowerCase()) ? v.toLowerCase() : `'${v}'`).join(",");
61
+ }
62
+
63
+ // src/contracts/campaign-normalize.ts
64
+ function normalizeCampaignSlug(input) {
65
+ if (!input || typeof input !== "string") return null;
66
+ const slug2 = input.trim().toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
67
+ return slug2 || null;
68
+ }
69
+
70
+ // src/contracts/funnels.ts
71
+ var FUNNEL_SCHEMA_VERSION = 1;
72
+ var MANAGED_FUNNEL_PATH_PREFIX = "/f";
73
+ var FUNNEL_TEMPLATES = ["newsletter", "requested_guide", "inquiry"];
74
+ var string = (maxLength, minLength = 1) => ({ type: "string", minLength, maxLength });
75
+ var literal = (value) => ({ const: value });
76
+ var enumeration = (...values) => ({ type: "string", enum: values });
77
+ var object = (properties, required = Object.keys(properties)) => ({ type: "object", properties, required, additionalProperties: false });
78
+ var array = (items, maxItems, minItems = 0) => ({ type: "array", items, maxItems, minItems });
79
+ var integer = (minimum, maximum = Number.MAX_SAFE_INTEGER) => ({ type: "integer", minimum, maximum });
80
+ var nullable = (shape) => ({ oneOf: [shape, { type: "null" }] });
81
+ var isoDate = { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,3})?Z$" };
82
+ var uuid = { type: "string", pattern: "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" };
83
+ var slug = { ...string(80), pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" };
84
+ var fieldSlug = { ...string(80), pattern: "^[a-zA-Z0-9_-]+$" };
85
+ var path = { ...string(512), pattern: "^/(?!/)[a-zA-Z0-9/_-]*$" };
86
+ var site = { ...string(253), pattern: "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$" };
87
+ var color = { ...string(9), pattern: "^#[a-fA-F0-9]{6}$" };
88
+ var colors = object(Object.fromEntries(PUBLICATION_COLOR_FIELDS.map((key) => [key, color])));
89
+ var typography = object({ headingFamily: string(200), bodyFamily: string(200) });
90
+ var override = object({
91
+ displayName: nullable(string(120)),
92
+ tagline: nullable(string(300, 0)),
93
+ fullLogoAssetId: nullable(uuid),
94
+ squareLogoAssetId: nullable(uuid),
95
+ colors: object(Object.fromEntries(PUBLICATION_COLOR_FIELDS.map((key) => [key, nullable(color)])), []),
96
+ typography: object({ headingFamily: nullable(string(200)), bodyFamily: nullable(string(200)) }, [])
97
+ }, []);
98
+ var bodyBlock = { oneOf: [object({ type: literal("paragraph"), text: string(2e3) }), object({ type: literal("bullets"), items: array(string(500), 8, 1) })] };
99
+ var body = array(bodyBlock, 12);
100
+ var publicBrand = object({ displayName: string(120), tagline: string(300, 0), fullLogoUrl: nullable(string(2048)), squareLogoUrl: nullable(string(2048)), colors, typography, scheme: enumeration("light", "dark") });
101
+ var step = { oneOf: [
102
+ object({ id: uuid, type: literal("wait"), durationSeconds: integer(60, 2592e3) }),
103
+ object({ id: uuid, type: literal("send_email"), templateId: uuid, purpose: enumeration("requested_fulfillment", "marketing"), guideAssetId: nullable(uuid) }, ["id", "type", "templateId", "purpose"]),
104
+ object({ id: uuid, type: literal("add_tag"), target: literal("subscriber"), tag: string(80) }),
105
+ object({ id: uuid, type: literal("create_task"), ownerContactId: uuid, title: string(160), dueAfterSeconds: integer(0, 2592e3) })
106
+ ] };
107
+ var definitionShape = object({
108
+ schemaVersion: literal(1),
109
+ name: string(120),
110
+ template: enumeration(...FUNNEL_TEMPLATES),
111
+ site,
112
+ entry: object({ formId: uuid, mode: literal("future_accepted_form_only") }),
113
+ goal: object({ type: enumeration("newsletter_confirmed", "guide_requested", "inquiry_received", "booking_confirmed"), bookingTypeId: nullable(uuid) }, ["type"]),
114
+ page: object({ mode: enumeration("existing", "managed"), path, slug: nullable(slug), headline: string(160, 0), body, formId: uuid, offerAssetId: nullable(uuid) }, ["mode", "path", "headline", "body", "formId"]),
115
+ permission: object({ mode: enumeration("requested_only", "marketing_opt_in"), optInFieldSlug: nullable(fieldSlug) }, ["mode"]),
116
+ brandOverride: override,
117
+ steps: array(step, 8),
118
+ exits: object({ unsubscribe: literal(true), manualStop: literal(true), confirmedBooking: object({ enabled: { type: "boolean" }, bookingTypeId: nullable(uuid) }) }, ["unsubscribe", "manualStop"])
119
+ }, ["schemaVersion", "name", "template", "site", "entry", "goal", "page", "permission", "steps", "exits"]);
120
+ var draftDefinitionShape = JSON.parse(JSON.stringify(definitionShape));
121
+ var draftProps = draftDefinitionShape.properties;
122
+ draftProps.name = string(120, 0);
123
+ draftProps.site = nullable(site);
124
+ draftProps.entry.properties.formId = nullable(uuid);
125
+ draftProps.page.properties.path = nullable(path);
126
+ draftProps.page.properties.formId = nullable(uuid);
127
+ draftProps.permission.properties.optInFieldSlug = nullable(fieldSlug);
128
+ draftProps.steps.items.oneOf = draftProps.steps.items.oneOf.map((item) => {
129
+ const fields = item.properties;
130
+ if (fields.templateId) fields.templateId = nullable(uuid);
131
+ if (fields.guideAssetId) fields.guideAssetId = nullable(uuid);
132
+ if (fields.ownerContactId) fields.ownerContactId = nullable(uuid);
133
+ if (fields.title) fields.title = string(160, 0);
134
+ if (fields.tag) fields.tag = string(80, 0);
135
+ return item;
136
+ });
137
+ draftProps.page.properties.body = array({ oneOf: [object({ type: literal("paragraph"), text: string(2e3, 0) }), object({ type: literal("bullets"), items: array(string(500, 0), 8, 1) })] }, 12);
138
+ var FUNNEL_DRAFT_DEFINITION_JSON_SCHEMA = { $schema: "http://json-schema.org/draft-07/schema#", ...draftDefinitionShape };
139
+ function parseFunnelDraftDefinition(value) {
140
+ var _a, _b, _c, _d, _e;
141
+ check(value, draftDefinitionShape, "draft");
142
+ const result = JSON.parse(JSON.stringify(value));
143
+ (_a = result.goal).bookingTypeId ?? (_a.bookingTypeId = null);
144
+ (_b = result.page).slug ?? (_b.slug = null);
145
+ (_c = result.page).offerAssetId ?? (_c.offerAssetId = null);
146
+ (_d = result.permission).optInFieldSlug ?? (_d.optInFieldSlug = null);
147
+ result.brandOverride ?? (result.brandOverride = {});
148
+ (_e = result.exits).confirmedBooking ?? (_e.confirmedBooking = { enabled: false, bookingTypeId: null });
149
+ for (const item of result.steps) if (item.type === "send_email") item.guideAssetId ?? (item.guideAssetId = null);
150
+ checkBody(result.page.body, "draft.page.body");
151
+ if (result.brandOverride) checkFonts(result.brandOverride, "draft.brandOverride");
152
+ if (result.page.mode === "existing" && result.page.slug !== null) fail("draft.page.slug", "only managed pages have slugs");
153
+ return result;
154
+ }
155
+ function createFunnelDraft(template) {
156
+ if (!FUNNEL_TEMPLATES.includes(template)) fail("template", "unsupported template");
157
+ return parseFunnelDraftDefinition({
158
+ schemaVersion: 1,
159
+ name: { newsletter: "Newsletter signup", requested_guide: "Requested guide", inquiry: "Inquiry or consultation" }[template],
160
+ template,
161
+ site: null,
162
+ entry: { formId: null, mode: "future_accepted_form_only" },
163
+ goal: { type: { newsletter: "newsletter_confirmed", requested_guide: "guide_requested", inquiry: "inquiry_received" }[template], bookingTypeId: null },
164
+ page: { mode: "existing", path: null, slug: null, headline: "", body: [], formId: null, offerAssetId: null },
165
+ permission: { mode: template === "newsletter" ? "marketing_opt_in" : "requested_only", optInFieldSlug: null },
166
+ brandOverride: {},
167
+ steps: [],
168
+ exits: { unsubscribe: true, manualStop: true, confirmedBooking: { enabled: false, bookingTypeId: null } }
169
+ });
170
+ }
171
+ var FUNNEL_DEFINITION_JSON_SCHEMA = { $schema: "http://json-schema.org/draft-07/schema#", ...definitionShape };
172
+ var publishedShape = object({
173
+ schemaVersion: literal(1),
174
+ id: uuid,
175
+ versionId: uuid,
176
+ revision: integer(1),
177
+ site,
178
+ path,
179
+ content: object({ headline: string(160), body, offer: object({ label: string(120), href: string(2048) }) }, ["headline", "body"]),
180
+ brand: publicBrand,
181
+ conversion: object({ formSlug: fieldSlug }),
182
+ tracking: object({ campaignSlug: slug })
183
+ });
184
+ var PUBLISHED_FUNNEL_PAGE_JSON_SCHEMA = { $schema: "http://json-schema.org/draft-07/schema#", ...publishedShape };
185
+ var FUNNEL_LAUNCH_JSON_SCHEMA = object({ expectedRevision: integer(1), previewId: uuid, idempotencyKey: string(200), confirm: literal(true) });
186
+ var FUNNEL_LIFECYCLE_JSON_SCHEMA = object({ expectedActiveVersionId: uuid, idempotencyKey: string(200), confirm: literal(true) });
187
+ var FUNNEL_ROLLBACK_JSON_SCHEMA = object({ expectedActiveVersionId: uuid, targetVersionId: uuid, idempotencyKey: string(200), confirm: literal(true) });
188
+ var FunnelValidationError = class extends Error {
189
+ constructor(path2, reason) {
190
+ super(`${path2}: ${reason}`);
191
+ this.path = path2;
192
+ this.name = "FunnelValidationError";
193
+ }
194
+ };
195
+ function fail(path2, reason) {
196
+ throw new FunnelValidationError(path2, reason);
197
+ }
198
+ function check(value, shape, at) {
199
+ if (shape.oneOf) {
200
+ let matches = 0;
201
+ for (const candidate of shape.oneOf) {
202
+ try {
203
+ check(value, candidate, at);
204
+ matches++;
205
+ } catch (error) {
206
+ if (!(error instanceof FunnelValidationError)) throw error;
207
+ }
208
+ }
209
+ if (matches !== 1) fail(at, "must match one supported shape");
210
+ return;
211
+ }
212
+ if ("const" in shape && value !== shape.const) fail(at, "unexpected value");
213
+ if (shape.enum && !shape.enum.includes(value)) fail(at, "unsupported value");
214
+ if (shape.type === "null" && value !== null) fail(at, "must be null");
215
+ if (shape.type === "boolean" && typeof value !== "boolean") fail(at, "must be boolean");
216
+ if (shape.type === "string") {
217
+ if (typeof value !== "string") fail(at, "must be a string");
218
+ if (value.length < (shape.minLength ?? 0) || value.length > (shape.maxLength ?? Infinity) || (shape.minLength ?? 0) > 0 && !value.trim() || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(value)) fail(at, "invalid string length or control character");
219
+ if (shape.pattern) {
220
+ const match = new RegExp(shape.pattern).exec(value);
221
+ if (!match || match[0] !== value) fail(at, "invalid format");
222
+ }
223
+ }
224
+ if (shape.type === "integer" && (typeof value !== "number" || !Number.isSafeInteger(value) || value < (shape.minimum ?? -Infinity) || value > (shape.maximum ?? Infinity))) fail(at, "invalid integer");
225
+ if (shape.type === "array") {
226
+ if (!Array.isArray(value) || value.length < (shape.minItems ?? 0) || value.length > (shape.maxItems ?? Infinity)) fail(at, "invalid array length");
227
+ value.forEach((item, i) => check(item, shape.items, `${at}[${i}]`));
228
+ }
229
+ if (shape.type === "object") {
230
+ if (!value || typeof value !== "object" || Array.isArray(value)) fail(at, "must be an object");
231
+ const row = value;
232
+ if (![Object.prototype, null].includes(Object.getPrototypeOf(row))) fail(at, "must be plain JSON");
233
+ for (const key of shape.required ?? []) if (!Object.prototype.hasOwnProperty.call(row, key)) fail(`${at}.${key}`, "required");
234
+ for (const key of Object.keys(row)) {
235
+ if (!Object.prototype.hasOwnProperty.call(shape.properties, key)) fail(`${at}.${key}`, "unknown field");
236
+ check(row[key], shape.properties[key], `${at}.${key}`);
237
+ }
238
+ }
239
+ }
240
+ function isSafeFunnelPublicUrl(value) {
241
+ try {
242
+ const url = new URL(value);
243
+ return value.startsWith("https://") && url.protocol === "https:" && !url.username && !url.password && (!url.port || url.port === "443") && !isBlockedPublicUrlHost(url.hostname) && !/[\s\x00-\x1f\\]/.test(value);
244
+ } catch {
245
+ return false;
246
+ }
247
+ }
248
+ function managedFunnelPath(value) {
249
+ check(value, slug, "slug");
250
+ if (normalizeCampaignSlug(value) !== value) fail("slug", "must be canonical");
251
+ return `${MANAGED_FUNNEL_PATH_PREFIX}/${value}`;
252
+ }
253
+ function checkFonts(value, at) {
254
+ for (const [key, font] of Object.entries(value.typography ?? {})) if (font !== null && safeFontStack(font) === void 0) fail(`${at}.typography.${key}`, "unsafe font stack");
255
+ }
256
+ function checkBody(blocks, at) {
257
+ if (blocks.reduce((total, block) => total + (block.type === "paragraph" ? block.text.length : block.items.join("").length), 0) > 12e3) fail(at, "total text exceeds 12000 characters");
258
+ }
259
+ function parseFunnelDefinition(value) {
260
+ check(value, definitionShape, "definition");
261
+ const result = JSON.parse(JSON.stringify(value));
262
+ if (normalizeSiteHost(result.site) !== result.site) fail("definition.site", "must be canonical");
263
+ if (result.page.formId !== result.entry.formId) fail("definition.page.formId", "must match the entry form");
264
+ if (result.page.mode === "managed") {
265
+ if (!result.page.slug || managedFunnelPath(result.page.slug) !== result.page.path) fail("definition.page.path", "must match the managed slug");
266
+ } else if (result.page.slug != null) fail("definition.page.slug", "only managed pages have slugs");
267
+ if (result.page.mode === "managed" && !result.page.headline.trim()) fail("definition.page.headline", "required for a managed page");
268
+ if (result.exits.confirmedBooking?.enabled && !result.exits.confirmedBooking.bookingTypeId) fail("definition.exits.confirmedBooking.bookingTypeId", "required when booking exit is enabled");
269
+ if (result.goal.type === "booking_confirmed" && !result.goal.bookingTypeId) fail("definition.goal.bookingTypeId", "required for a booking goal");
270
+ if (result.permission.mode === "marketing_opt_in" && !result.permission.optInFieldSlug) fail("definition.permission.optInFieldSlug", "explicit checkbox required");
271
+ if (result.steps.some((item) => item.type === "send_email" && item.purpose === "marketing") && result.permission.mode !== "marketing_opt_in") fail("definition.permission", "marketing requires explicit opt-in");
272
+ if (new Set(result.steps.map((item) => item.id)).size !== result.steps.length) fail("definition.steps", "step identities must be unique");
273
+ checkBody(result.page.body, "definition.page.body");
274
+ if (result.brandOverride) checkFonts(result.brandOverride, "definition.brandOverride");
275
+ return result;
276
+ }
277
+ function parsePublishedFunnelPage(value) {
278
+ check(value, publishedShape, "page");
279
+ const result = JSON.parse(JSON.stringify(value));
280
+ const suffix = result.path.slice(MANAGED_FUNNEL_PATH_PREFIX.length + 1);
281
+ if (managedFunnelPath(suffix) !== result.path) fail("page.path", "invalid managed route");
282
+ for (const key of ["fullLogoUrl", "squareLogoUrl"]) if (result.brand[key] && !isSafeFunnelPublicUrl(result.brand[key])) fail(`page.brand.${key}`, "unsafe public URL");
283
+ checkFonts(result.brand, "page.brand");
284
+ checkBody(result.content.body, "page.content.body");
285
+ if (result.content.offer && !isSafeFunnelPublicUrl(result.content.offer.href)) fail("page.content.offer.href", "unsafe public URL");
286
+ return result;
287
+ }
288
+ function parseFunnelLaunchArgs(value) {
289
+ check(value, FUNNEL_LAUNCH_JSON_SCHEMA, "launch");
290
+ return JSON.parse(JSON.stringify(value));
291
+ }
292
+ function parseFunnelLifecycleArgs(value, operation = "pause") {
293
+ check(value, operation === "rollback" ? FUNNEL_ROLLBACK_JSON_SCHEMA : FUNNEL_LIFECYCLE_JSON_SCHEMA, "lifecycle");
294
+ return JSON.parse(JSON.stringify(value));
295
+ }
296
+ function applyFunnelBrandOverride(inherited, patch = {}, resolvedAssets = {}) {
297
+ check(inherited, publicBrand, "brand");
298
+ check(patch, override, "brandOverride");
299
+ checkFonts(inherited, "brand");
300
+ checkFonts(patch, "brandOverride");
301
+ const brand = JSON.parse(JSON.stringify(inherited));
302
+ const overridePaths = [];
303
+ const assetRefs = [];
304
+ for (const key of ["displayName", "tagline"]) if (patch[key] != null) {
305
+ brand[key] = patch[key];
306
+ overridePaths.push(key);
307
+ }
308
+ for (const section of ["colors", "typography"]) {
309
+ for (const [key, value] of Object.entries(patch[section] ?? {})) if (value != null) {
310
+ Object.assign(brand[section], { [key]: value });
311
+ overridePaths.push(`${section}.${key}`);
312
+ }
313
+ }
314
+ for (const [idKey, urlKey] of [["fullLogoAssetId", "fullLogoUrl"], ["squareLogoAssetId", "squareLogoUrl"]]) {
315
+ const id = patch[idKey];
316
+ if (!id) continue;
317
+ const asset = Object.prototype.hasOwnProperty.call(resolvedAssets, id) ? resolvedAssets[id] : void 0;
318
+ if (!asset || !isSafeFunnelPublicUrl(asset.url)) fail(`brandOverride.${idKey}`, "authorized resolved asset required");
319
+ brand[urlKey] = asset.url;
320
+ overridePaths.push(urlKey);
321
+ assetRefs.push({ id, version: asset.version ?? null });
322
+ }
323
+ for (const key of ["fullLogoUrl", "squareLogoUrl"]) if (brand[key] && !isSafeFunnelPublicUrl(brand[key])) fail(`brand.${key}`, "unsafe public URL");
324
+ return { brand, overridePaths, assetRefs };
325
+ }
326
+ var FUNNEL_CREATE_JSON_SCHEMA = object({ idempotencyKey: string(200), definition: draftDefinitionShape });
327
+ var FUNNEL_UPDATE_JSON_SCHEMA = object({ expectedRevision: integer(1), idempotencyKey: string(200), definition: draftDefinitionShape });
328
+ function parseFunnelCreateArgs(value) {
329
+ check(value, FUNNEL_CREATE_JSON_SCHEMA, "create");
330
+ const args = value;
331
+ return { idempotencyKey: args.idempotencyKey, definition: parseFunnelDraftDefinition(args.definition) };
332
+ }
333
+ function parseFunnelUpdateArgs(value) {
334
+ check(value, FUNNEL_UPDATE_JSON_SCHEMA, "update");
335
+ const args = value;
336
+ return { expectedRevision: args.expectedRevision, idempotencyKey: args.idempotencyKey, definition: parseFunnelDraftDefinition(args.definition) };
337
+ }
338
+ function parseFunnelDraftArgs(value, mode) {
339
+ return mode === "update" ? parseFunnelUpdateArgs(value) : parseFunnelCreateArgs(value);
340
+ }
341
+ var FUNNEL_REVISION_JSON_SCHEMA = object({ expectedRevision: integer(1) });
342
+ var FUNNEL_RETRY_JSON_SCHEMA = object({ expectedAttempt: integer(0, 100), idempotencyKey: string(200), confirm: literal(true) });
343
+ var FUNNEL_DRAFT_JSON_SCHEMAS = { create: FUNNEL_CREATE_JSON_SCHEMA, update: FUNNEL_UPDATE_JSON_SCHEMA };
344
+ function parseFunnelRevisionArgs(value) {
345
+ check(value, FUNNEL_REVISION_JSON_SCHEMA, "revision");
346
+ return JSON.parse(JSON.stringify(value));
347
+ }
348
+ function parseFunnelRetryArgs(value) {
349
+ check(value, FUNNEL_RETRY_JSON_SCHEMA, "retry");
350
+ return JSON.parse(JSON.stringify(value));
351
+ }
352
+ var fingerprint = { type: "string", pattern: "^[a-f0-9]{64}$" };
353
+ var issue = object({ code: { ...string(80), pattern: "^[A-Z][A-Z0-9_]*$" }, path: string(300), message: string(500), fixRef: object({ section: string(80), field: string(80), resourceId: uuid }, ["section"]) }, ["code", "path", "message"]);
354
+ var FUNNEL_READINESS_JSON_SCHEMA = object({ definitionRevision: integer(1), brandFingerprint: fingerprint, resourceFingerprint: fingerprint, ready: { type: "boolean" }, blockers: array(issue, 50), warnings: array(issue, 50), capabilities: object({ managedPage: { type: "boolean" } }) });
355
+ function parseFunnelReadiness(value) {
356
+ check(value, FUNNEL_READINESS_JSON_SCHEMA, "readiness");
357
+ const result = JSON.parse(JSON.stringify(value));
358
+ if (result.ready !== (result.blockers.length === 0)) fail("readiness.ready", "must agree with blockers");
359
+ return result;
360
+ }
361
+ var funnelDetailShape = object({ id: uuid, revision: integer(1), definition: draftDefinitionShape, activeVersionId: nullable(uuid), lifecycle: enumeration("draft", "active", "paused"), health: enumeration("healthy", "waiting", "needs_attention", "unknown"), hasDraftChanges: { type: "boolean" }, readiness: FUNNEL_READINESS_JSON_SCHEMA });
362
+ var FUNNEL_DETAIL_JSON_SCHEMA = funnelDetailShape;
363
+ function parseFunnelDetail(value) {
364
+ check(value, funnelDetailShape, "funnel");
365
+ const detail = JSON.parse(JSON.stringify(value));
366
+ detail.definition = parseFunnelDraftDefinition(detail.definition);
367
+ detail.readiness = parseFunnelReadiness(detail.readiness);
368
+ return detail;
369
+ }
370
+ var FUNNEL_OPERATION_RECEIPT_JSON_SCHEMA = object({
371
+ operationId: uuid,
372
+ projectId: uuid,
373
+ orgId: uuid,
374
+ funnelId: uuid,
375
+ operation: enumeration("create", "update", "launch", "pause", "resume", "retry", "rollback"),
376
+ idempotencyKey: string(200),
377
+ requestDigest: fingerprint,
378
+ versionId: nullable(uuid),
379
+ lifecycle: enumeration("draft", "active", "paused"),
380
+ replayed: { type: "boolean" },
381
+ armedFutureEntrantsOnly: literal(true),
382
+ enrolledExisting: literal(0),
383
+ messagesSent: literal(0),
384
+ pagePublication: enumeration("not_requested", "published", "revalidation_pending"),
385
+ health: enumeration("healthy", "waiting", "needs_attention", "unknown"),
386
+ revision: integer(1),
387
+ commandState: literal("committed"),
388
+ funnel: nullable(funnelDetailShape),
389
+ actor: string(200),
390
+ armedAt: nullable(isoDate),
391
+ effectScope: enumeration("configuration_only", "safe_retry_armed"),
392
+ target: object({ funnelId: nullable(uuid), enrollmentId: nullable(uuid), executionId: nullable(uuid), targetVersionId: nullable(uuid) }),
393
+ ledgerProposalId: uuid
394
+ });
395
+ function parseFunnelOperationReceipt(value) {
396
+ check(value, FUNNEL_OPERATION_RECEIPT_JSON_SCHEMA, "receipt");
397
+ const receipt = JSON.parse(JSON.stringify(value));
398
+ if (receipt.target.funnelId !== (receipt.operation === "create" ? null : receipt.funnelId)) fail("receipt.target.funnelId", "must match operation scope");
399
+ for (const key of ["enrollmentId", "executionId"]) if (receipt.target[key] !== null !== (receipt.operation === "retry")) fail(`receipt.target.${key}`, "must match retry scope");
400
+ if (receipt.target.targetVersionId !== null !== (receipt.operation === "rollback")) fail("receipt.target.targetVersionId", "must match rollback scope");
401
+ if (receipt.funnel !== null !== ["create", "update"].includes(receipt.operation)) fail("receipt.funnel", "detail belongs only to create and update");
402
+ if (receipt.funnel) {
403
+ receipt.funnel = parseFunnelDetail(receipt.funnel);
404
+ if (receipt.funnel.id !== receipt.funnelId || receipt.funnel.revision !== receipt.revision || receipt.funnel.lifecycle !== receipt.lifecycle) fail("receipt.funnel", "must match receipt identity and revision");
405
+ }
406
+ if (!["create", "update"].includes(receipt.operation) && receipt.versionId === null) fail("receipt.versionId", "an active or historical version is required");
407
+ if (receipt.effectScope !== (receipt.operation === "retry" ? "safe_retry_armed" : "configuration_only")) fail("receipt.effectScope", "must match the operation");
408
+ return receipt;
409
+ }
410
+ var provenancePaths = ["displayName", "tagline", "fullLogoUrl", "squareLogoUrl", "scheme", ...PUBLICATION_COLOR_FIELDS.map((key) => `colors.${key}`), "typography.headingFamily", "typography.bodyFamily"];
411
+ var brandSnapshotShape = object({
412
+ page: publicBrand,
413
+ email: publicBrand,
414
+ provenance: object(Object.fromEntries(["page", "email"].flatMap((channel) => provenancePaths.map((key) => [`${channel}.${key}`, enumeration("project", "brand_profile", "site_design", "publication_branding", "platforms.publication", "platforms.email", "funnel_override", "default")]))), []),
415
+ assetRefs: array(object({ id: nullable(uuid), version: nullable(string(128)) }), 32),
416
+ capturedAt: isoDate,
417
+ fingerprint
418
+ });
419
+ var FUNNEL_BRAND_SNAPSHOT_JSON_SCHEMA = brandSnapshotShape;
420
+ function parseFunnelBrandSnapshot(value) {
421
+ check(value, brandSnapshotShape, "brandSnapshot");
422
+ const result = JSON.parse(JSON.stringify(value));
423
+ applyFunnelBrandOverride(result.page);
424
+ applyFunnelBrandOverride(result.email);
425
+ return result;
426
+ }
427
+ var choicePage = (item) => object({ items: array(item, 100), nextCursor: nullable(string(500)) });
428
+ var choiceFields = { id: uuid, label: string(120) };
429
+ var catalogShape = object({
430
+ templates: array(object({ id: enumeration(...FUNNEL_TEMPLATES), label: string(120), defaults: draftDefinitionShape }), 3),
431
+ sites: array(object({ host: site, label: string(200) }), 200),
432
+ brand: nullable(brandSnapshotShape),
433
+ choices: object({
434
+ forms: choicePage(object({ ...choiceFields, slug: string(120), site: nullable(site), formType: enumeration("prospect", "contact", "support", "feedback", "newsletter", "custom"), active: { type: "boolean" }, newsletterListIds: array(uuid, 20), fields: array(object({ slug: fieldSlug, type: string(80), label: string(120), required: { type: "boolean" } }), 100) })),
435
+ lists: choicePage(object({ ...choiceFields, active: { type: "boolean" }, doubleOptIn: { type: "boolean" } })),
436
+ emailTemplates: choicePage(object({ ...choiceFields, subject: string(998, 0), updatedAt: nullable(isoDate) })),
437
+ assets: choicePage(object({ ...choiceFields, label: string(160), mimeType: string(100), sizeBytes: integer(0, 52428800), visibility: literal("public"), publicUrl: string(2048), version: nullable(string(200)) })),
438
+ owners: choicePage(object({ ...choiceFields, active: literal(true) })),
439
+ bookingTypes: choicePage(object({ ...choiceFields, site: nullable(site), active: literal(true), requiresConfirmation: { type: "boolean" } }))
440
+ }, []),
441
+ capabilities: object({ managedPage: { type: "boolean" } })
442
+ });
443
+ var FUNNEL_CATALOG_JSON_SCHEMA = catalogShape;
444
+ function parseFunnelCatalog(value) {
445
+ check(value, catalogShape, "catalog");
446
+ const result = JSON.parse(JSON.stringify(value));
447
+ for (const template of result.templates) template.defaults = parseFunnelDraftDefinition(template.defaults);
448
+ if (result.brand) result.brand = parseFunnelBrandSnapshot(result.brand);
449
+ for (const asset of result.choices.assets?.items ?? []) if (!isSafeFunnelPublicUrl(asset.publicUrl)) fail("catalog.choices.assets", "unsafe public asset URL");
450
+ return result;
451
+ }
452
+ var launchEvidenceShape = object({ projectId: uuid, orgId: uuid, funnelId: uuid, previewId: uuid, versionId: uuid, definitionRevision: integer(1), definitionDigest: fingerprint, brandFingerprint: fingerprint, resourceFingerprint: fingerprint, entryDigest: fingerprint, permissionDigest: fingerprint, expiresAt: isoDate });
453
+ var FUNNEL_LAUNCH_EVIDENCE_JSON_SCHEMA = launchEvidenceShape;
454
+ function parseFunnelLaunchEvidence(value) {
455
+ check(value, launchEvidenceShape, "launchEvidence");
456
+ return JSON.parse(JSON.stringify(value));
457
+ }
458
+ var FUNNEL_ROLLBACK_EVIDENCE_JSON_SCHEMA = object({ orgId: uuid, projectId: uuid, funnelId: uuid, targetVersionId: uuid, definitionRevision: integer(1), definitionDigest: fingerprint, brandFingerprint: fingerprint, resourceFingerprint: fingerprint });
459
+ function parseFunnelRollbackEvidence(value) {
460
+ check(value, FUNNEL_ROLLBACK_EVIDENCE_JSON_SCHEMA, "rollbackEvidence");
461
+ return JSON.parse(JSON.stringify(value));
462
+ }
463
+ var FUNNEL_PREVIEW_JSON_SCHEMA = object({
464
+ previewId: uuid,
465
+ versionId: uuid,
466
+ expiresAt: isoDate,
467
+ definitionRevision: integer(1),
468
+ brand: brandSnapshotShape,
469
+ readiness: FUNNEL_READINESS_JSON_SCHEMA,
470
+ page: nullable(publishedShape),
471
+ launchEvidence: launchEvidenceShape,
472
+ emailPreviews: array(object({ stepId: uuid, ready: { type: "boolean" }, subject: nullable(string(998, 0)), contentHtml: nullable(string(2e5, 0)), issues: array(issue, 50) }), 8)
473
+ });
474
+ function parseFunnelPreview(value) {
475
+ check(value, FUNNEL_PREVIEW_JSON_SCHEMA, "preview");
476
+ const result = JSON.parse(JSON.stringify(value));
477
+ result.brand = parseFunnelBrandSnapshot(result.brand);
478
+ result.readiness = parseFunnelReadiness(result.readiness);
479
+ if (result.page) {
480
+ result.page = parsePublishedFunnelPage(result.page);
481
+ if (result.page.id !== result.launchEvidence.funnelId || result.page.versionId !== result.versionId || result.page.revision !== result.definitionRevision) fail("preview.page", "must match the exact preview version");
482
+ const pageBrand = result.page.brand;
483
+ const snapshotBrand = result.brand.page;
484
+ if (["displayName", "tagline", "fullLogoUrl", "squareLogoUrl", "scheme"].some((key) => pageBrand[key] !== snapshotBrand[key]) || PUBLICATION_COLOR_FIELDS.some((key) => pageBrand.colors[key] !== snapshotBrand.colors[key]) || pageBrand.typography.headingFamily !== snapshotBrand.typography.headingFamily || pageBrand.typography.bodyFamily !== snapshotBrand.typography.bodyFamily) fail("preview.page.brand", "must match the captured brand");
485
+ }
486
+ const evidence = result.launchEvidence;
487
+ if (evidence.previewId !== result.previewId || evidence.versionId !== result.versionId || evidence.definitionRevision !== result.definitionRevision || evidence.brandFingerprint !== result.readiness.brandFingerprint || evidence.resourceFingerprint !== result.readiness.resourceFingerprint || evidence.expiresAt !== result.expiresAt) fail("preview.launchEvidence", "must match the exact preview");
488
+ if (result.definitionRevision !== result.readiness.definitionRevision) fail("preview.definitionRevision", "must match readiness");
489
+ if (result.brand.fingerprint !== result.readiness.brandFingerprint) fail("preview.brand", "must match readiness fingerprint");
490
+ return result;
491
+ }
492
+ var summaryShape = object({
493
+ id: uuid,
494
+ name: string(120, 0),
495
+ template: enumeration(...FUNNEL_TEMPLATES),
496
+ site: nullable(site),
497
+ revision: integer(1),
498
+ activeVersionId: nullable(uuid),
499
+ lifecycle: enumeration("draft", "active", "paused"),
500
+ health: enumeration("healthy", "waiting", "needs_attention", "unknown"),
501
+ hasDraftChanges: { type: "boolean" },
502
+ counts: object({ entered: integer(0), waiting: integer(0), running: integer(0), completed: integer(0), exited: integer(0), needsAttention: integer(0) }),
503
+ countsBasis: literal("all_versions_all_time"),
504
+ lastEventAt: nullable(isoDate)
505
+ });
506
+ var FUNNEL_SUMMARY_JSON_SCHEMA = summaryShape;
507
+ function parseFunnelSummary(value) {
508
+ check(value, summaryShape, "summary");
509
+ return JSON.parse(JSON.stringify(value));
510
+ }
511
+ var FUNNEL_ENROLLMENT_JSON_SCHEMA = object({
512
+ id: uuid,
513
+ versionId: uuid,
514
+ state: enumeration("waiting", "running", "completed", "exited", "needs_attention"),
515
+ effectiveStatus: enumeration("paused", "waiting", "running", "completed", "exited", "needs_attention"),
516
+ source: object({ submissionId: uuid, acceptedAt: isoDate }),
517
+ subject: object({ kind: enumeration("subscriber", "contact", "submission"), id: uuid, displayLabel: string(200) }),
518
+ enteredAt: isoDate,
519
+ nextRunAt: nullable(isoDate),
520
+ exitCode: nullable(string(80)),
521
+ stepsCompleted: integer(0, 8),
522
+ stepsTotal: integer(0, 8),
523
+ lastReceiptAt: nullable(isoDate)
524
+ });
525
+ function parseFunnelEnrollmentSummary(value) {
526
+ check(value, FUNNEL_ENROLLMENT_JSON_SCHEMA, "enrollment");
527
+ const result = JSON.parse(JSON.stringify(value));
528
+ if (result.stepsCompleted > result.stepsTotal) fail("enrollment.stepsCompleted", "cannot exceed total");
529
+ return result;
530
+ }
531
+ var FUNNEL_TIMELINE_JSON_SCHEMA = object({
532
+ items: array(object({ executionId: uuid, stepId: uuid, type: enumeration("wait", "send_email", "add_tag", "create_task"), state: enumeration("pending", "claimed", "queued", "admitted", "succeeded", "skipped", "uncertain", "failed"), dueAt: nullable(isoDate), occurredAt: nullable(isoDate), attempt: integer(0, 100), reasonCode: nullable(string(80)), message: string(500, 0), receipt: nullable(object({ domain: enumeration("campaign_send", "subscriber_tag", "crm_task", "wait"), id: nullable(uuid), state: string(80) })), canRetry: { type: "boolean" } }), 100),
533
+ nextCursor: nullable(string(500)),
534
+ exit: nullable(object({ code: string(80), sourceType: string(80), sourceId: nullable(uuid), occurredAt: isoDate }))
535
+ });
536
+ function parseFunnelTimeline(value) {
537
+ check(value, FUNNEL_TIMELINE_JSON_SCHEMA, "timeline");
538
+ return JSON.parse(JSON.stringify(value));
539
+ }
540
+ function freezeSchema(value) {
541
+ if (!value || typeof value !== "object" || Object.isFrozen(value)) return;
542
+ for (const child of Object.values(value)) freezeSchema(child);
543
+ Object.freeze(value);
544
+ }
545
+ for (const schema of [FUNNEL_DEFINITION_JSON_SCHEMA, FUNNEL_DRAFT_DEFINITION_JSON_SCHEMA, PUBLISHED_FUNNEL_PAGE_JSON_SCHEMA, FUNNEL_LAUNCH_JSON_SCHEMA, FUNNEL_LIFECYCLE_JSON_SCHEMA, FUNNEL_ROLLBACK_JSON_SCHEMA, FUNNEL_CREATE_JSON_SCHEMA, FUNNEL_UPDATE_JSON_SCHEMA, FUNNEL_REVISION_JSON_SCHEMA, FUNNEL_RETRY_JSON_SCHEMA, FUNNEL_DRAFT_JSON_SCHEMAS, FUNNEL_READINESS_JSON_SCHEMA, FUNNEL_DETAIL_JSON_SCHEMA, FUNNEL_OPERATION_RECEIPT_JSON_SCHEMA, FUNNEL_BRAND_SNAPSHOT_JSON_SCHEMA, FUNNEL_CATALOG_JSON_SCHEMA, FUNNEL_PREVIEW_JSON_SCHEMA, FUNNEL_SUMMARY_JSON_SCHEMA, FUNNEL_ENROLLMENT_JSON_SCHEMA, FUNNEL_TIMELINE_JSON_SCHEMA]) freezeSchema(schema);
546
+ var FUNNEL_VERSION_JSON_SCHEMA = object({ versionId: uuid, definitionRevision: integer(1), publishedAt: isoDate, brandFingerprint: fingerprint, resourceFingerprint: fingerprint, active: { type: "boolean" } });
547
+ var FUNNEL_VERSION_DETAIL_JSON_SCHEMA = object({ ...FUNNEL_VERSION_JSON_SCHEMA.properties, rollbackEvidence: FUNNEL_ROLLBACK_EVIDENCE_JSON_SCHEMA });
548
+ function parseFunnelPublishedVersion(value) {
549
+ check(value, FUNNEL_VERSION_JSON_SCHEMA, "version");
550
+ return JSON.parse(JSON.stringify(value));
551
+ }
552
+ function parseFunnelPublishedVersionDetail(value) {
553
+ check(value, FUNNEL_VERSION_DETAIL_JSON_SCHEMA, "versionDetail");
554
+ const result = JSON.parse(JSON.stringify(value));
555
+ if (result.versionId !== result.rollbackEvidence.targetVersionId || result.definitionRevision !== result.rollbackEvidence.definitionRevision || result.brandFingerprint !== result.rollbackEvidence.brandFingerprint) fail("versionDetail.rollbackEvidence", "must match the published target");
556
+ return result;
557
+ }
558
+ for (const schema of [FUNNEL_LAUNCH_EVIDENCE_JSON_SCHEMA, FUNNEL_ROLLBACK_EVIDENCE_JSON_SCHEMA, FUNNEL_VERSION_JSON_SCHEMA, FUNNEL_VERSION_DETAIL_JSON_SCHEMA]) freezeSchema(schema);
559
+
560
+ export { FUNNEL_BRAND_SNAPSHOT_JSON_SCHEMA, FUNNEL_CATALOG_JSON_SCHEMA, FUNNEL_CREATE_JSON_SCHEMA, FUNNEL_DEFINITION_JSON_SCHEMA, FUNNEL_DETAIL_JSON_SCHEMA, FUNNEL_DRAFT_DEFINITION_JSON_SCHEMA, FUNNEL_DRAFT_JSON_SCHEMAS, FUNNEL_ENROLLMENT_JSON_SCHEMA, FUNNEL_LAUNCH_EVIDENCE_JSON_SCHEMA, FUNNEL_LAUNCH_JSON_SCHEMA, FUNNEL_LIFECYCLE_JSON_SCHEMA, FUNNEL_OPERATION_RECEIPT_JSON_SCHEMA, FUNNEL_PREVIEW_JSON_SCHEMA, FUNNEL_READINESS_JSON_SCHEMA, FUNNEL_RETRY_JSON_SCHEMA, FUNNEL_REVISION_JSON_SCHEMA, FUNNEL_ROLLBACK_EVIDENCE_JSON_SCHEMA, FUNNEL_ROLLBACK_JSON_SCHEMA, FUNNEL_SCHEMA_VERSION, FUNNEL_SUMMARY_JSON_SCHEMA, FUNNEL_TEMPLATES, FUNNEL_TIMELINE_JSON_SCHEMA, FUNNEL_UPDATE_JSON_SCHEMA, FUNNEL_VERSION_DETAIL_JSON_SCHEMA, FUNNEL_VERSION_JSON_SCHEMA, FunnelValidationError, MANAGED_FUNNEL_PATH_PREFIX, PUBLISHED_FUNNEL_PAGE_JSON_SCHEMA, applyFunnelBrandOverride, createFunnelDraft, isSafeFunnelPublicUrl, managedFunnelPath, normalizeCampaignSlug, parseFunnelBrandSnapshot, parseFunnelCatalog, parseFunnelCreateArgs, parseFunnelDefinition, parseFunnelDetail, parseFunnelDraftArgs, parseFunnelDraftDefinition, parseFunnelEnrollmentSummary, parseFunnelLaunchArgs, parseFunnelLaunchEvidence, parseFunnelLifecycleArgs, parseFunnelOperationReceipt, parseFunnelPreview, parseFunnelPublishedVersion, parseFunnelPublishedVersionDetail, parseFunnelReadiness, parseFunnelRetryArgs, parseFunnelRevisionArgs, parseFunnelRollbackEvidence, parseFunnelSummary, parseFunnelTimeline, parseFunnelUpdateArgs, parsePublishedFunnelPage };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Which inbox cards are decisions. Single source of truth for the rule the
3
+ * Signal API follows when it writes ("Mark all read" and "Dismiss all" leave a
4
+ * decision alone) and the dashboard follows when it previews that write.
5
+ *
6
+ * A decision is a card still waiting on its person:
7
+ * - an approval (insight type `approval_needed`);
8
+ * - any card carrying a ledger proposal (`data.proposal_id`), which the Inbox
9
+ * turns into Approve / Reject;
10
+ * - the ledger's unconfirmed-action card (`ledger_unconfirmed_action`):
11
+ * Signal started something it never saw finish and won't run again, so a
12
+ * person has to check it. It's raised once per claim, ever, so a bulk
13
+ * dismissal that took it would leave nobody told. It isn't an approval: the
14
+ * claim was approved already, so it has no `proposal_id` and the Inbox
15
+ * offers no Approve / Reject on it.
16
+ *
17
+ * The Signal API states the same rule once more as a database filter. It
18
+ * builds that filter from DECISION_INSIGHT_TYPES, so the list is written here
19
+ * and nowhere else.
20
+ */
21
+ /** The insight type of an approval waiting on a yes. */
22
+ export declare const APPROVAL_NEEDED_INSIGHT_TYPE = "approval_needed";
23
+ /**
24
+ * The insight type of the card a ledger claim nobody can account for raises.
25
+ * The producer writes it, the delivery policy matches it and the decision rule
26
+ * below counts it.
27
+ */
28
+ export declare const LEDGER_UNCONFIRMED_INSIGHT_TYPE = "ledger_unconfirmed_action";
29
+ /** Insight types that are a decision whatever their data. */
30
+ export declare const DECISION_INSIGHT_TYPES: readonly string[];
31
+ /** The card fields the rule reads. A stored row or the dashboard's inbox item both fit. */
32
+ export interface DecisionCardFields {
33
+ insight_type: string;
34
+ data?: Record<string, unknown> | null;
35
+ }
36
+ /**
37
+ * Whether a card is a decision still waiting on its person. `proposal_id` set
38
+ * is what counts, so it's read the way the database reads `data->>proposal_id`:
39
+ * a JSON null, a missing key and a null `data` all mean no proposal; any other
40
+ * value, an empty string included, means one.
41
+ */
42
+ export declare function isDecisionCard(card: DecisionCardFields): boolean;
package/dist/inbox.js ADDED
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ // src/contracts/inbox.ts
4
+ var APPROVAL_NEEDED_INSIGHT_TYPE = "approval_needed";
5
+ var LEDGER_UNCONFIRMED_INSIGHT_TYPE = "ledger_unconfirmed_action";
6
+ var DECISION_INSIGHT_TYPES = [APPROVAL_NEEDED_INSIGHT_TYPE, LEDGER_UNCONFIRMED_INSIGHT_TYPE];
7
+ function isDecisionCard(card) {
8
+ return DECISION_INSIGHT_TYPES.includes(card.insight_type) || card.data?.proposal_id != null;
9
+ }
10
+
11
+ exports.APPROVAL_NEEDED_INSIGHT_TYPE = APPROVAL_NEEDED_INSIGHT_TYPE;
12
+ exports.DECISION_INSIGHT_TYPES = DECISION_INSIGHT_TYPES;
13
+ exports.LEDGER_UNCONFIRMED_INSIGHT_TYPE = LEDGER_UNCONFIRMED_INSIGHT_TYPE;
14
+ exports.isDecisionCard = isDecisionCard;
package/dist/inbox.mjs ADDED
@@ -0,0 +1,9 @@
1
+ // src/contracts/inbox.ts
2
+ var APPROVAL_NEEDED_INSIGHT_TYPE = "approval_needed";
3
+ var LEDGER_UNCONFIRMED_INSIGHT_TYPE = "ledger_unconfirmed_action";
4
+ var DECISION_INSIGHT_TYPES = [APPROVAL_NEEDED_INSIGHT_TYPE, LEDGER_UNCONFIRMED_INSIGHT_TYPE];
5
+ function isDecisionCard(card) {
6
+ return DECISION_INSIGHT_TYPES.includes(card.insight_type) || card.data?.proposal_id != null;
7
+ }
8
+
9
+ export { APPROVAL_NEEDED_INSIGHT_TYPE, DECISION_INSIGHT_TYPES, LEDGER_UNCONFIRMED_INSIGHT_TYPE, isDecisionCard };