@tryvitris/website-admin-contracts 1.4.0 → 1.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.
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Vitris contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vitris contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,7 @@
1
1
  # Website Admin Contracts
2
2
 
3
3
  Validated installation manifests, roles, permissions and module contracts for `@tryvitris/website-admin-harness`. Node.js 24+. MIT licensed.
4
+
5
+ The maintenance contract exports the stable admin package family and typed installed/locked/published-version reports and preparation plans. Its policy is prepare-and-test under existing publication permissions. It does not execute updates or create schedules; use the runtime maintenance CLI and its agent guide.
6
+
7
+ The `./email-campaigns` entry exports validated contacts, evidence and preferences, bounded audience rules, structured templates, communication purposes, guided workflow steps, website events, account cost calculations, and workspace types. Explicit email permissions remain excluded from existing non-owner role defaults.
@@ -0,0 +1,209 @@
1
+ /** Cloudflare-only customer communications. Newsletter content is draft-only. */
2
+ export declare const emailCampaignPermissions: readonly ["email-campaigns:read", "email-reports:read", "email-contacts:read", "email-contacts:write", "email-contacts:export", "email-content:write", "email-campaigns:send", "email-automations:manage", "email-settings:manage"];
3
+ export type EmailPermission = typeof emailCampaignPermissions[number];
4
+ export declare const emailPurposes: readonly ["customer-report", "service-update", "enquiry-response", "newsletter-draft"];
5
+ export type EmailPurpose = typeof emailPurposes[number];
6
+ export type EmailTopic = 'reports' | 'service' | 'newsletter';
7
+ export interface EmailModuleConfig {
8
+ feedbackQueue: string;
9
+ accountId?: string;
10
+ zoneId?: string;
11
+ sendingDomain?: string;
12
+ eventSubscriptionId?: string;
13
+ dailyLimit?: number;
14
+ }
15
+ export interface EmailSettings {
16
+ enabled: boolean;
17
+ mode: 'capture' | 'send';
18
+ senderName: string;
19
+ logoUrl: string;
20
+ replyTo: string;
21
+ postalAddress: string;
22
+ accent: string;
23
+ timezone: string;
24
+ perMinute: number;
25
+ dailyLimit: number;
26
+ monthlyLimit: number;
27
+ recipientCap: number;
28
+ spacingHours: number;
29
+ otherAccountEmails: number;
30
+ reservedDaily: number;
31
+ clickTracking: boolean;
32
+ }
33
+ export declare const defaultEmailSettings: () => EmailSettings;
34
+ export interface EmailContact {
35
+ id: string;
36
+ revision: number;
37
+ email: string;
38
+ name: string;
39
+ company: string;
40
+ stage: string;
41
+ tags: string[];
42
+ fields: Record<string, string>;
43
+ source: string;
44
+ interactionRef: string;
45
+ serviceRef: string;
46
+ topics: Partial<Record<EmailTopic, boolean>>;
47
+ evidence: string;
48
+ deleted?: boolean;
49
+ }
50
+ export interface EmailRule {
51
+ field: string;
52
+ op: 'eq' | 'neq' | 'contains' | 'exists' | 'not_exists';
53
+ value: string;
54
+ }
55
+ export interface EmailSegment {
56
+ id: string;
57
+ revision: number;
58
+ name: string;
59
+ match: 'all' | 'any';
60
+ rules: EmailRule[];
61
+ }
62
+ export interface EmailBlock {
63
+ type: 'heading' | 'paragraph' | 'button' | 'image' | 'divider';
64
+ text: string;
65
+ url?: string;
66
+ alt?: string;
67
+ }
68
+ export interface EmailTemplate {
69
+ id: string;
70
+ revision: number;
71
+ name: string;
72
+ layout: 'letter' | 'newsletter' | 'report' | 'update';
73
+ subject: string;
74
+ preheader: string;
75
+ blocks: EmailBlock[];
76
+ }
77
+ export interface EmailCampaign {
78
+ id: string;
79
+ revision: number;
80
+ name: string;
81
+ purpose: EmailPurpose;
82
+ templateId: string;
83
+ segmentId: string;
84
+ excludeIds: string[];
85
+ scheduledAt: string;
86
+ status: 'draft' | 'preparing' | 'scheduled' | 'running' | 'paused' | 'cancelled' | 'completed';
87
+ snapshot?: EmailTemplate;
88
+ audienceCutoff?: number;
89
+ cursor?: string;
90
+ approvedBy?: string;
91
+ approvalRevision?: number;
92
+ recurrenceDays?: number;
93
+ nextIssueAt?: number;
94
+ sourceArticle?: string;
95
+ }
96
+ export type EmailStep = {
97
+ type: 'wait';
98
+ minutes: number;
99
+ } | {
100
+ type: 'send';
101
+ templateId: string;
102
+ } | {
103
+ type: 'condition';
104
+ rule: EmailRule;
105
+ skip: number;
106
+ } | {
107
+ type: 'tag';
108
+ value: string;
109
+ } | {
110
+ type: 'stage';
111
+ value: string;
112
+ } | {
113
+ type: 'task';
114
+ title: string;
115
+ } | {
116
+ type: 'exit';
117
+ };
118
+ export interface EmailAutomation {
119
+ id: string;
120
+ revision: number;
121
+ name: string;
122
+ trigger: 'lead.created' | 'service.updated' | 'report.ready' | 'manual' | 'subscription.confirmed';
123
+ purpose: EmailPurpose;
124
+ status: 'draft' | 'active' | 'paused';
125
+ steps: EmailStep[];
126
+ version?: number;
127
+ publisherId?: string;
128
+ hasDraft?: boolean;
129
+ }
130
+ export interface EmailWebsiteEvent {
131
+ version: 1;
132
+ mode: 'production' | 'test';
133
+ id: string;
134
+ installationId: string;
135
+ siteId: string;
136
+ type: 'lead.created' | 'service.updated' | 'report.ready' | 'customer.converted' | 'enquiry.resolved' | 'subscription.confirmed';
137
+ occurredAt: string;
138
+ /** Optional campaign attribution, verified against an actual live message to this contact. */
139
+ campaignId?: string;
140
+ contact: {
141
+ email: string;
142
+ name: string;
143
+ company?: string;
144
+ interactionRef?: string;
145
+ serviceRef?: string;
146
+ };
147
+ report?: {
148
+ title: string;
149
+ period: string;
150
+ summary: string;
151
+ url?: string;
152
+ };
153
+ }
154
+ export interface EmailRecord<T> {
155
+ id: string;
156
+ revision: number;
157
+ data: T;
158
+ createdAt: number;
159
+ updatedAt: number;
160
+ }
161
+ export interface EmailJob {
162
+ id: string;
163
+ campaignId: string;
164
+ contactId: string;
165
+ state: string;
166
+ reason: string | null;
167
+ mode: 'capture' | 'send';
168
+ dueAt: number;
169
+ createdAt: number;
170
+ providerMessageId: string | null;
171
+ }
172
+ export interface EmailWorkspace {
173
+ settings: EmailRecord<EmailSettings>;
174
+ readiness: {
175
+ schemaReady: boolean;
176
+ keysReady: boolean;
177
+ senderReady: boolean;
178
+ liveReady: boolean;
179
+ feedbackReady: boolean;
180
+ websiteReady: boolean;
181
+ deliveryPaused: boolean;
182
+ deliveryReason: string;
183
+ from: string;
184
+ issues: string[];
185
+ };
186
+ permissions: EmailPermission[];
187
+ counts: Record<string, number>;
188
+ monthly: {
189
+ counted: number;
190
+ estimatedOverage: number;
191
+ included: number;
192
+ };
193
+ templates: EmailTemplate[];
194
+ }
195
+ export declare class EmailInputError extends Error {
196
+ constructor(message: string);
197
+ }
198
+ export declare const validEmail: (v: unknown) => v is string;
199
+ export declare const emailId: (v: unknown) => v is string;
200
+ export declare function safeEmailUrl(v: unknown): string;
201
+ export declare function parseEmailRule(v: unknown): EmailRule;
202
+ export declare function parseEmailContact(v: unknown): EmailContact;
203
+ export declare function parseEmailSegment(v: unknown): EmailSegment;
204
+ export declare function parseEmailTemplate(v: unknown): EmailTemplate;
205
+ export declare function parseEmailCampaign(v: unknown): EmailCampaign;
206
+ export declare function parseEmailAutomation(v: unknown): EmailAutomation;
207
+ export declare function parseEmailSettings(v: unknown): EmailSettings;
208
+ export declare function emailOverage(count: number): number;
209
+ export declare function starterEmailTemplates(): EmailTemplate[];
@@ -0,0 +1,166 @@
1
+ // packages/contracts/src/email-campaigns.ts
2
+ var emailCampaignPermissions = ["email-campaigns:read", "email-reports:read", "email-contacts:read", "email-contacts:write", "email-contacts:export", "email-content:write", "email-campaigns:send", "email-automations:manage", "email-settings:manage"];
3
+ var emailPurposes = ["customer-report", "service-update", "enquiry-response", "newsletter-draft"];
4
+ var defaultEmailSettings = () => ({ enabled: false, mode: "capture", senderName: "", logoUrl: "", replyTo: "", postalAddress: "", accent: "#245c50", timezone: "America/New_York", perMinute: 50, dailyLimit: 500, monthlyLimit: 3e3, recipientCap: 5e3, spacingHours: 24, otherAccountEmails: 0, reservedDaily: 50, clickTracking: false });
5
+ var EmailInputError = class extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "EmailInputError";
9
+ }
10
+ };
11
+ var object = (v) => !!v && typeof v === "object" && !Array.isArray(v);
12
+ var validEmail = (v) => typeof v === "string" && v.length <= 254 && /^[^\s@<>]+@[^\s@<>]+\.[^\s@<>]+$/.test(v) && !/[\u0000-\u001f\u007f]/.test(v);
13
+ var emailId = (v) => typeof v === "string" && /^[a-zA-Z0-9_-]{1,100}$/.test(v);
14
+ function string(v, max, empty = true) {
15
+ if (typeof v !== "string" || v.length > max || !empty && !v.trim() || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(v))
16
+ throw new EmailInputError("Enter a valid text value.");
17
+ return v.trim();
18
+ }
19
+ function integer(v, min, max) {
20
+ if (!Number.isInteger(v) || Number(v) < min || Number(v) > max)
21
+ throw new EmailInputError(`Use a whole number from ${min} to ${max}.`);
22
+ return Number(v);
23
+ }
24
+ function choice(v, values) {
25
+ if (!values.includes(v))
26
+ throw new EmailInputError("Choose a supported option.");
27
+ return v;
28
+ }
29
+ function common(v) {
30
+ return { id: v.id === void 0 ? "" : v.id === "" ? "" : emailId(v.id) ? v.id : (() => {
31
+ throw new EmailInputError("Invalid record ID.");
32
+ })(), revision: integer(v.revision ?? 0, 0, 1e8) };
33
+ }
34
+ function safeEmailUrl(v) {
35
+ const value = string(v, 2e3, false);
36
+ try {
37
+ const u = new URL(value);
38
+ if (u.protocol !== "https:" && !(u.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(u.hostname)) || u.username || u.password)
39
+ throw Error();
40
+ return u.href;
41
+ } catch {
42
+ throw new EmailInputError("Use an absolute HTTPS link.");
43
+ }
44
+ }
45
+ function parseEmailRule(v) {
46
+ if (!object(v) || typeof v.field !== "string" || !/^((stage|tag|source|company|service|interaction|topic|event)|custom\.[a-zA-Z][a-zA-Z0-9_-]{0,30})$/.test(v.field))
47
+ throw new EmailInputError("Choose a supported segment field.");
48
+ return { field: v.field, op: choice(v.op, ["eq", "neq", "contains", "exists", "not_exists"]), value: string(v.value ?? "", 300) };
49
+ }
50
+ function parseEmailContact(v) {
51
+ if (!object(v) || !validEmail(v.email))
52
+ throw new EmailInputError("Enter a valid contact email.");
53
+ if (!Array.isArray(v.tags) || v.tags.length > 30 || !object(v.fields) || Object.keys(v.fields).length > 20 || !object(v.topics))
54
+ throw new EmailInputError("Use at most 30 tags and 20 custom fields.");
55
+ const fields = {};
56
+ for (const [key, value] of Object.entries(v.fields)) {
57
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]{0,30}$/.test(key))
58
+ throw new EmailInputError("Invalid custom field name.");
59
+ fields[key] = string(value, 500);
60
+ }
61
+ const topics = {};
62
+ for (const [key, value] of Object.entries(v.topics)) {
63
+ if (!["reports", "service", "newsletter"].includes(key) || typeof value !== "boolean")
64
+ throw new EmailInputError("Invalid topic preference.");
65
+ topics[key] = value;
66
+ }
67
+ return { ...common(v), email: v.email.trim(), name: string(v.name ?? "", 150), company: string(v.company ?? "", 150), stage: string(v.stage ?? "lead", 50, false), tags: [...new Set(v.tags.map((t) => string(t, 60, false)))], fields, source: string(v.source ?? "manual", 100), interactionRef: string(v.interactionRef ?? "", 200), serviceRef: string(v.serviceRef ?? "", 200), topics, evidence: string(v.evidence ?? "", 1e3) };
68
+ }
69
+ function parseEmailSegment(v) {
70
+ if (!object(v) || !Array.isArray(v.rules) || v.rules.length > 16)
71
+ throw new EmailInputError("Use at most 16 segment rules.");
72
+ return { ...common(v), name: string(v.name, 150, false), match: choice(v.match, ["all", "any"]), rules: v.rules.map(parseEmailRule) };
73
+ }
74
+ function parseEmailTemplate(v) {
75
+ if (!object(v) || !Array.isArray(v.blocks) || v.blocks.length < 1 || v.blocks.length > 40)
76
+ throw new EmailInputError("Use 1 to 40 content sections.");
77
+ const subject = string(v.subject, 250, false);
78
+ if (/[\r\n]/.test(subject))
79
+ throw new EmailInputError("Subject must be one line.");
80
+ return { ...common(v), name: string(v.name, 150, false), layout: choice(v.layout, ["letter", "newsletter", "report", "update"]), subject, preheader: string(v.preheader ?? "", 250), blocks: v.blocks.map((b) => {
81
+ if (!object(b))
82
+ throw new EmailInputError("Invalid content section.");
83
+ const type = choice(b.type, ["heading", "paragraph", "button", "image", "divider"]);
84
+ const url = b.url ? safeEmailUrl(b.url) : void 0;
85
+ if ((type === "button" || type === "image") && !url)
86
+ throw new EmailInputError("Buttons and images need an HTTPS link.");
87
+ return { type, text: string(b.text ?? "", 5e3), ...url ? { url } : {}, ...type === "image" ? { alt: string(b.alt ?? "", 300, false) } : {} };
88
+ }) };
89
+ }
90
+ function parseEmailCampaign(v) {
91
+ if (!object(v) || !emailId(v.templateId) || v.segmentId !== "" && !emailId(v.segmentId) || !Array.isArray(v.excludeIds) || v.excludeIds.length > 500 || v.excludeIds.some((x) => !emailId(x)))
92
+ throw new EmailInputError("Choose a template, audience, and valid exclusions.");
93
+ let scheduledAt = string(v.scheduledAt ?? "", 40);
94
+ if (scheduledAt && (!/Z$/.test(scheduledAt) || !Number.isFinite(Date.parse(scheduledAt))))
95
+ throw new EmailInputError("Schedule with an absolute UTC date.");
96
+ return { ...common(v), name: string(v.name, 150, false), purpose: choice(v.purpose, emailPurposes), templateId: v.templateId, segmentId: v.segmentId, excludeIds: v.excludeIds, scheduledAt, status: "draft", recurrenceDays: integer(v.recurrenceDays ?? 0, 0, 365), ...v.sourceArticle ? { sourceArticle: safeEmailUrl(v.sourceArticle) } : {} };
97
+ }
98
+ function parseEmailAutomation(v) {
99
+ if (!object(v) || !Array.isArray(v.steps) || !v.steps.length || v.steps.length > 12)
100
+ throw new EmailInputError("Use 1 to 12 forward-only steps.");
101
+ const steps = v.steps.map((s, index) => {
102
+ if (!object(s))
103
+ throw new EmailInputError("Invalid automation step.");
104
+ switch (s.type) {
105
+ case "wait":
106
+ return { type: s.type, minutes: integer(s.minutes, 1, 43200) };
107
+ case "send":
108
+ if (!emailId(s.templateId))
109
+ throw new EmailInputError("Choose a send template.");
110
+ return { type: s.type, templateId: s.templateId };
111
+ case "condition":
112
+ return { type: s.type, rule: parseEmailRule(s.rule), skip: integer(s.skip, 1, 12 - index) };
113
+ case "tag":
114
+ case "stage":
115
+ return { type: s.type, value: string(s.value, 60, false) };
116
+ case "task":
117
+ return { type: s.type, title: string(s.title, 180, false) };
118
+ case "exit":
119
+ return { type: s.type };
120
+ default:
121
+ throw new EmailInputError("Unsupported automation step.");
122
+ }
123
+ });
124
+ return { ...common(v), name: string(v.name, 150, false), trigger: choice(v.trigger, ["lead.created", "service.updated", "report.ready", "manual", "subscription.confirmed"]), purpose: choice(v.purpose, emailPurposes), status: "draft", steps };
125
+ }
126
+ function parseEmailSettings(v) {
127
+ if (!object(v) || typeof v.enabled !== "boolean" || typeof v.clickTracking !== "boolean" || typeof v.accent !== "string" || !/^#[\da-f]{6}$/i.test(v.accent) || v.replyTo && !validEmail(v.replyTo))
128
+ throw new EmailInputError("Check email settings and reply address.");
129
+ if (typeof v.senderName === "string" && /[\r\n]/.test(v.senderName)) throw new EmailInputError("Use a single line for the sender name.");
130
+ const timezone = string(v.timezone, 100, false);
131
+ try {
132
+ new Intl.DateTimeFormat("en", { timeZone: timezone });
133
+ } catch {
134
+ throw new EmailInputError("Choose a valid timezone.");
135
+ }
136
+ return { enabled: v.enabled, mode: choice(v.mode, ["capture", "send"]), senderName: string(v.senderName, 150), logoUrl: v.logoUrl ? safeEmailUrl(v.logoUrl) : "", replyTo: string(v.replyTo ?? "", 254), postalAddress: string(v.postalAddress ?? "", 500), accent: v.accent, timezone, perMinute: integer(v.perMinute, 1, 100), dailyLimit: integer(v.dailyLimit, 1, 1e5), monthlyLimit: integer(v.monthlyLimit, 1, 1e6), recipientCap: integer(v.recipientCap, 1, 1e4), spacingHours: integer(v.spacingHours, 0, 720), otherAccountEmails: integer(v.otherAccountEmails, 0, 1e7), reservedDaily: integer(v.reservedDaily, 0, 1e5), clickTracking: v.clickTracking };
137
+ }
138
+ function emailOverage(count) {
139
+ return Math.round(Math.max(0, count - 3e3) * 0.035) / 100;
140
+ }
141
+ function starterEmailTemplates() {
142
+ return [
143
+ { id: "starter-report", revision: 0, name: "Customer report", layout: "report", subject: "Your {{report.period|latest}} report", preheader: "An update on your agreed service.", blocks: [{ type: "heading", text: "{{report.title|Your service report}}" }, { type: "paragraph", text: "Hello {{firstName|there}},\n\n{{report.summary|Your requested report is ready.}}" }] },
144
+ { id: "starter-response", revision: 0, name: "Personal response", layout: "letter", subject: "We received your enquiry", preheader: "Your enquiry has reached our team.", blocks: [{ type: "paragraph", text: "Hello {{firstName|there}},\n\nThank you for contacting us. Our team has received your request and will respond with the information you asked for." }] },
145
+ { id: "starter-update", revision: 0, name: "Service update", layout: "update", subject: "An update on your service", preheader: "The latest status of your agreed service.", blocks: [{ type: "heading", text: "Your service update" }, { type: "paragraph", text: "Hello {{firstName|there}},\n\n{{report.summary|We have an update about your agreed service.}}" }] },
146
+ { id: "starter-newsletter", revision: 0, name: "Newsletter draft", layout: "newsletter", subject: "This month\u2019s notes", preheader: "Stories and updates.", blocks: [{ type: "heading", text: "This month\u2019s notes" }, { type: "paragraph", text: "Write your opening note here." }] }
147
+ ];
148
+ }
149
+ export {
150
+ EmailInputError,
151
+ defaultEmailSettings,
152
+ emailCampaignPermissions,
153
+ emailId,
154
+ emailOverage,
155
+ emailPurposes,
156
+ parseEmailAutomation,
157
+ parseEmailCampaign,
158
+ parseEmailContact,
159
+ parseEmailRule,
160
+ parseEmailSegment,
161
+ parseEmailSettings,
162
+ parseEmailTemplate,
163
+ safeEmailUrl,
164
+ starterEmailTemplates,
165
+ validEmail
166
+ };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './email-campaigns.js';
1
2
  export * from './billing.js';
2
3
  export * from './stripe-billing.js';
3
4
  export * from './client-portfolio.js';
@@ -8,7 +9,7 @@ export * from './pairing.js';
8
9
  export * from './signon.js';
9
10
  export declare const contractVersion = 1;
10
11
  export declare const coreSections: readonly ["overview", "leads", "analytics", "settings", "access", "activity"];
11
- export declare const permissions: readonly ["leads:read", "leads:write", "analytics:read", "settings:write", "access:read", "access:write", "activity:read", "tasks:read", "tasks:write", "appointments:read", "appointments:write", "availability:write", "scheduling:configure", "calendar-connections:manage", "clients:read", "client-changes:request", "client-releases:publish", "prospects:read", "prospects:write", "billing:read", "billing:customers:write", "billing:catalog:read", "billing:subscriptions:write", "billing:invoices:write", "billing:collect", "billing:connection:manage", "account-billing:read", "account-billing:manage"];
12
+ export declare const permissions: readonly ["leads:read", "leads:write", "analytics:read", "settings:write", "access:read", "access:write", "activity:read", "tasks:read", "tasks:write", "appointments:read", "appointments:write", "availability:write", "scheduling:configure", "calendar-connections:manage", "clients:read", "client-changes:request", "client-releases:publish", "prospects:read", "prospects:write", "billing:read", "billing:customers:write", "billing:catalog:read", "billing:subscriptions:write", "billing:invoices:write", "billing:collect", "billing:connection:manage", "account-billing:read", "account-billing:manage", "email-campaigns:read", "email-reports:read", "email-contacts:read", "email-contacts:write", "email-contacts:export", "email-content:write", "email-campaigns:send", "email-automations:manage", "email-settings:manage"];
12
13
  export type Permission = typeof permissions[number];
13
14
  export type Role = 'owner' | 'administrator' | 'editor' | 'analyst';
14
15
  export declare const roles: Record<Role, readonly Permission[]>;
@@ -41,7 +42,7 @@ export type WebsiteTheme = Partial<Record<typeof websiteColorKeys[number], strin
41
42
  export declare function isThemeColor(value: unknown): value is string;
42
43
  export declare function isWebsiteTheme(value: unknown): value is WebsiteTheme;
43
44
  export interface ModuleDefinition {
44
- ui?: 'stripe-billing' | 'stripe-account';
45
+ ui?: 'stripe-billing' | 'stripe-account' | 'email-campaigns';
45
46
  id: string;
46
47
  version: number;
47
48
  label: string;
@@ -214,3 +215,4 @@ export declare const workQueueModule: ModuleDefinition;
214
215
  export declare function isLoopback(origin: string): boolean;
215
216
  export declare function parseManifest(value: unknown): AdminManifest;
216
217
  export declare function validateModules(manifest: AdminManifest, modules: ModuleDefinition[]): void;
218
+ export * from './maintenance.js';
package/dist/index.js CHANGED
@@ -2,6 +2,8 @@ import { schedulingPermissions } from "./scheduling.js";
2
2
  import { clientPortfolioPermissions } from "./client-portfolio.js";
3
3
  import { clientOnboardingPermissions } from "./client-onboarding.js";
4
4
  import { billingPermissions } from "./billing.js";
5
+ import { emailCampaignPermissions } from "./email-campaigns.js";
6
+ export * from "./email-campaigns.js";
5
7
  export * from "./billing.js";
6
8
  export * from "./stripe-billing.js";
7
9
  export * from "./client-portfolio.js";
@@ -12,10 +14,10 @@ export * from "./pairing.js";
12
14
  export * from "./signon.js";
13
15
  const contractVersion = 1;
14
16
  const coreSections = ["overview", "leads", "analytics", "settings", "access", "activity"];
15
- const permissions = ["leads:read", "leads:write", "analytics:read", "settings:write", "access:read", "access:write", "activity:read", "tasks:read", "tasks:write", ...schedulingPermissions, ...clientPortfolioPermissions, ...clientOnboardingPermissions, ...billingPermissions];
17
+ const permissions = ["leads:read", "leads:write", "analytics:read", "settings:write", "access:read", "access:write", "activity:read", "tasks:read", "tasks:write", ...schedulingPermissions, ...clientPortfolioPermissions, ...clientOnboardingPermissions, ...billingPermissions, ...emailCampaignPermissions];
16
18
  const roles = {
17
19
  owner: permissions,
18
- administrator: permissions.filter((p) => p !== "access:write" && !clientPortfolioPermissions.includes(p) && !clientOnboardingPermissions.includes(p) && !billingPermissions.includes(p)),
20
+ administrator: permissions.filter((p) => p !== "access:write" && !clientPortfolioPermissions.includes(p) && !clientOnboardingPermissions.includes(p) && !billingPermissions.includes(p) && !emailCampaignPermissions.includes(p)),
19
21
  editor: ["leads:read", "leads:write", "activity:read", "tasks:read", "tasks:write"],
20
22
  analyst: ["analytics:read", "activity:read"]
21
23
  };
@@ -166,6 +168,7 @@ function validateModules(manifest, modules) {
166
168
  }
167
169
  for (const enabled of manifest.modules) if (!ids.has(enabled)) throw new Error(`Module ${enabled} is not installed`);
168
170
  }
171
+ export * from "./maintenance.js";
169
172
  export {
170
173
  analyticsBreakdownKeys,
171
174
  analyticsMetricKeys,
@@ -0,0 +1,47 @@
1
+ /** Package maintenance is an agent workflow; this contract grants no deployment authority. */
2
+ export declare const maintenanceVersion = 1;
3
+ export declare const adminMaintenancePackages: readonly ["@tryvitris/website-admin-harness", "@tryvitris/website-admin-contracts", "@tryvitris/create-website-admin-harness"];
4
+ export declare const maintenancePackages: readonly ["@tryvitris/website-admin-harness", "@tryvitris/website-admin-contracts", "@tryvitris/create-website-admin-harness", "@tryvitris/website-harness"];
5
+ export type MaintenancePackage = typeof maintenancePackages[number];
6
+ export type MaintenanceStatus = 'not-declared' | 'current' | 'update-available' | 'ahead' | 'unmanaged' | 'drift' | 'unavailable';
7
+ export interface MaintenanceRelease {
8
+ name: MaintenancePackage;
9
+ version: string;
10
+ integrity: string;
11
+ tarball: string;
12
+ }
13
+ export interface MaintenanceEntry {
14
+ name: MaintenancePackage;
15
+ declared: string | null;
16
+ installed: string | null;
17
+ locked: string | null;
18
+ status: MaintenanceStatus;
19
+ change: 'none' | 'patch' | 'minor' | 'major' | 'unknown';
20
+ latest: MaintenanceRelease | null;
21
+ }
22
+ export interface MaintenanceReport {
23
+ schemaVersion: 1;
24
+ checkedAt: string;
25
+ policy: 'prepare-and-test';
26
+ adminRelease: {
27
+ version: string | null;
28
+ complete: boolean;
29
+ };
30
+ packages: MaintenanceEntry[];
31
+ baseline: Record<string, string>;
32
+ deployment: 'not-checked';
33
+ }
34
+ export interface MaintenancePlan {
35
+ schemaVersion: 1;
36
+ policy: 'prepare-and-test';
37
+ baseline: Record<string, string>;
38
+ ready: boolean;
39
+ held: MaintenancePackage[];
40
+ updates: MaintenanceRelease[];
41
+ install: {
42
+ command: 'npm';
43
+ args: string[];
44
+ } | null;
45
+ requiredChecks: string[];
46
+ deployment: 'follow-existing-permissions';
47
+ }
@@ -0,0 +1,9 @@
1
+ // packages/contracts/src/maintenance.ts
2
+ var maintenanceVersion = 1;
3
+ var adminMaintenancePackages = ["@tryvitris/website-admin-harness", "@tryvitris/website-admin-contracts", "@tryvitris/create-website-admin-harness"];
4
+ var maintenancePackages = ["@tryvitris/website-admin-harness", "@tryvitris/website-admin-contracts", "@tryvitris/create-website-admin-harness", "@tryvitris/website-harness"];
5
+ export {
6
+ adminMaintenancePackages,
7
+ maintenancePackages,
8
+ maintenanceVersion
9
+ };
package/package.json CHANGED
@@ -1,37 +1,50 @@
1
- {
2
- "name": "@tryvitris/website-admin-contracts",
3
- "version": "1.4.0",
4
- "type": "module",
5
- "license": "MIT",
6
- "description": "Validated installation and module contracts for Cloudflare business admin apps",
7
- "exports": {
8
- ".": {
9
- "types": "./dist/index.d.ts",
10
- "default": "./dist/index.js"
11
- },
12
- "./scheduling": {
13
- "types": "./dist/scheduling.d.ts",
14
- "default": "./dist/scheduling.js"
15
- }
16
- },
17
- "files": [
18
- "dist",
19
- "vitris-release.json"
20
- ],
21
- "publishConfig": {
22
- "access": "public",
23
- "tag": "latest"
24
- },
25
- "engines": {
26
- "node": ">=24"
27
- },
28
- "repository": {
29
- "type": "git",
30
- "url": "git+https://github.com/jackperrotta/website-admin-harness.git",
31
- "directory": "packages/contracts"
32
- },
33
- "homepage": "https://github.com/jackperrotta/website-admin-harness#readme",
34
- "bugs": {
35
- "url": "https://github.com/jackperrotta/website-admin-harness/issues"
36
- }
37
- }
1
+ {
2
+ "name": "@tryvitris/website-admin-contracts",
3
+ "version": "1.6.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "description": "Validated installation and module contracts for Cloudflare business admin apps",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./scheduling": {
13
+ "types": "./dist/scheduling.d.ts",
14
+ "default": "./dist/scheduling.js"
15
+ },
16
+ "./email-campaigns": {
17
+ "types": "./dist/email-campaigns.d.ts",
18
+ "default": "./dist/email-campaigns.js"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "vitris-release.json"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public",
27
+ "tag": "latest"
28
+ },
29
+ "engines": {
30
+ "node": ">=24"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/jackperrotta/website-admin-harness.git",
35
+ "directory": "packages/contracts"
36
+ },
37
+ "homepage": "https://github.com/jackperrotta/website-admin-harness#readme",
38
+ "bugs": {
39
+ "url": "https://github.com/jackperrotta/website-admin-harness/issues"
40
+ },
41
+ "vitris": {
42
+ "maintenance": {
43
+ "version": 1,
44
+ "policy": "prepare-and-test",
45
+ "stableChannel": "latest",
46
+ "command": "vitris-admin updates check --dir . --json",
47
+ "schedule": "reuse-existing-daily-care"
48
+ }
49
+ }
50
+ }
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "name": "@tryvitris/website-admin-contracts",
4
- "version": "1.4.0",
5
- "title": "Optional combined connection approval contract",
4
+ "version": "1.6.0",
5
+ "title": "Customer email purposes audiences and workflows",
6
6
  "notesScope": "package",
7
7
  "notes": [
8
- "Adds versioned combined-setup capability discovery, optional named-subject intent, owner-selected existing-account approval and explicit sign-on result metadata. Legacy reporting-only requests remain supported."
8
+ "Adds typed, validated Cloudflare email settings, contact and permission evidence, segment rules, structured templates, campaign purposes, guided automation steps, website events, workspace responses, and per-account cost calculations.",
9
+ "Adds explicit email permissions excluded from existing non-owner role defaults and identifies the optional compiled email workspace without exposing handlers or credentials in bootstrap metadata."
9
10
  ],
10
11
  "repository": "https://github.com/jackperrotta/website-admin-harness",
11
12
  "readmePath": "packages/contracts/README.md",
12
- "ref": "v1.4.0",
13
- "documentation": "# Website Admin Contracts\r\n\r\nValidated installation manifests, roles, permissions and module contracts for `@tryvitris/website-admin-harness`. Node.js 24+. MIT licensed.\r\n",
14
- "documentationSha256": "dc7fd3ba89b1a0a49046735adc8963115551a0d8cc7a4e64e8fba70c79ca9389"
13
+ "ref": "v1.6.0",
14
+ "documentation": "# Website Admin Contracts\r\n\r\nValidated installation manifests, roles, permissions and module contracts for `@tryvitris/website-admin-harness`. Node.js 24+. MIT licensed.\r\n\r\nThe maintenance contract exports the stable admin package family and typed installed/locked/published-version reports and preparation plans. Its policy is prepare-and-test under existing publication permissions. It does not execute updates or create schedules; use the runtime maintenance CLI and its agent guide.\r\n\nThe `./email-campaigns` entry exports validated contacts, evidence and preferences, bounded audience rules, structured templates, communication purposes, guided workflow steps, website events, account cost calculations, and workspace types. Explicit email permissions remain excluded from existing non-owner role defaults.\n",
15
+ "documentationSha256": "091ba91c28394499ae091ca75665e8e60b8b47fd07e71bb8fabc4a36fbae2073"
15
16
  }