@samva/markup 0.2.0 → 0.3.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.
@@ -1,4 +1,4 @@
1
- import { matchesTemplateFacets, registerPublicCorpusEntry } from "./corpus.js";
1
+ import { brandRecreationProvenance, matchesTemplateFacets, registerPublicCorpusEntry } from "./corpus.js";
2
2
  import { Result } from "effect";
3
3
  //#region presets/public-gallery.ts
4
4
  const templateUseCases = [
@@ -14,7 +14,7 @@ const templateCollectionFamilies = [
14
14
  "stage",
15
15
  "style"
16
16
  ];
17
- const isMadeWithSamvaTemplate = (template) => template.corpus._tag === "OwnedEditablePreset";
17
+ const isMadeWithSamvaTemplate = (template) => template.corpus._tag === "OwnedEditablePreset" || template.corpus._tag === "BrandRecreationPreset";
18
18
  /** Explicit evidence for every owned preset reviewed in the 2026-08-31 catalog pass. */
19
19
  const ownedPresetReviewManifest = {
20
20
  welcome: {
@@ -71,6 +71,12 @@ const ownedPresetReviewManifest = {
71
71
  reviewedAt: "2026-08-31T00:00:00.000Z",
72
72
  notes: "Reviewed team-invite editable SML, public preview data, and gallery identity."
73
73
  },
74
+ "product-newsletter": {
75
+ sourcePath: "examples/email-showcase/emails/product-newsletter.tsx",
76
+ reviewedBy: "Samva catalog review",
77
+ reviewedAt: "2026-09-08T00:00:00.000Z",
78
+ notes: "Reviewed product-newsletter editable SML project, authored assets, public preview data, and gallery identity."
79
+ },
74
80
  "product-digest": {
75
81
  sourcePath: "packages/markup/presets/product-digest.tsx",
76
82
  reviewedBy: "Samva catalog review",
@@ -167,6 +173,12 @@ const ownedPresetReviewManifest = {
167
173
  reviewedAt: "2026-08-31T00:00:00.000Z",
168
174
  notes: "Reviewed order-delivered editable SML, public preview data, and gallery identity."
169
175
  },
176
+ "order-receipt": {
177
+ sourcePath: "examples/email-showcase/emails/order-receipt.tsx",
178
+ reviewedBy: "Samva catalog review",
179
+ reviewedAt: "2026-09-08T00:00:00.000Z",
180
+ notes: "Reviewed order-receipt editable project with stylesheet and original assets, public preview data, and gallery identity."
181
+ },
170
182
  "travel-rewards-promo": {
171
183
  sourcePath: "packages/markup/presets/travel-rewards-promo.tsx",
172
184
  reviewedBy: "Samva catalog review",
@@ -244,8 +256,15 @@ const ownedPresetReviewManifest = {
244
256
  reviewedBy: "Samva catalog review",
245
257
  reviewedAt: "2026-08-31T00:00:00.000Z",
246
258
  notes: "Reviewed season-clearance-promo editable SML, public preview data, and gallery identity."
259
+ },
260
+ "product-launch-promo": {
261
+ sourcePath: "examples/email-showcase/emails/product-launch-promo.tsx",
262
+ reviewedBy: "Samva catalog review",
263
+ reviewedAt: "2026-09-08T00:00:00.000Z",
264
+ notes: "Reviewed product-launch-promo editable project, original Oakline assets, public preview data, and gallery identity."
247
265
  }
248
266
  };
267
+ const isBrandRecreationPresetId = (presetId) => Object.hasOwn(brandRecreationProvenance, presetId);
249
268
  const audienceFor = (useCase) => {
250
269
  switch (useCase) {
251
270
  case "account-access": return ["product-users"];
@@ -261,35 +280,64 @@ const audienceFor = (useCase) => {
261
280
  };
262
281
  const facetValues = (collections, prefix) => collections.filter((id) => id.startsWith(`${prefix}:`)).map((id) => id.slice(prefix.length + 1));
263
282
  const template = (value) => {
264
- const review = ownedPresetReviewManifest[value.presetId];
265
- const corpus = {
266
- _tag: "OwnedEditablePreset",
267
- corpusKind: "owned-editable-sml",
268
- provenance: {
269
- owner: "Samva",
270
- sourcePath: review.sourcePath,
271
- recordedAt: review.reviewedAt
272
- },
273
- review: {
274
- state: "approved",
275
- reviewedBy: review.reviewedBy,
276
- reviewedAt: review.reviewedAt,
277
- notes: review.notes
278
- },
279
- display: {
280
- state: "public",
281
- attributionLabel: "Made with Samva"
282
- },
283
- retention: {
284
- policy: "retain-until-withdrawn",
285
- reviewedAt: review.reviewedAt
286
- },
287
- takedown: {
288
- state: "active",
289
- contact: "legal@samva.dev",
290
- procedureUrl: "https://samva.dev/contact?subject=Template%20takedown"
283
+ const corpus = (() => {
284
+ if (isBrandRecreationPresetId(value.presetId)) {
285
+ const provenance = brandRecreationProvenance[value.presetId];
286
+ return {
287
+ _tag: "BrandRecreationPreset",
288
+ corpusKind: "brand-recreation-sml",
289
+ provenance,
290
+ review: {
291
+ state: "approved",
292
+ reviewedBy: "Samva catalog review",
293
+ reviewedAt: provenance.recordedAt,
294
+ notes: provenance.reviewedNote
295
+ },
296
+ display: {
297
+ state: "public",
298
+ attributionLabel: `Recreated with Samva. ${provenance.brand} does not endorse or have an affiliation with this example.`
299
+ },
300
+ retention: {
301
+ policy: "retain-until-withdrawn",
302
+ reviewedAt: provenance.recordedAt
303
+ },
304
+ takedown: {
305
+ state: "active",
306
+ contact: "legal@samva.dev",
307
+ procedureUrl: "https://samva.dev/contact?subject=Template%20takedown"
308
+ }
309
+ };
291
310
  }
292
- };
311
+ const review = ownedPresetReviewManifest[value.presetId];
312
+ return {
313
+ _tag: "OwnedEditablePreset",
314
+ corpusKind: "owned-editable-sml",
315
+ provenance: {
316
+ owner: "Samva",
317
+ sourcePath: review.sourcePath,
318
+ recordedAt: review.reviewedAt
319
+ },
320
+ review: {
321
+ state: "approved",
322
+ reviewedBy: review.reviewedBy,
323
+ reviewedAt: review.reviewedAt,
324
+ notes: review.notes
325
+ },
326
+ display: {
327
+ state: "public",
328
+ attributionLabel: "Made with Samva"
329
+ },
330
+ retention: {
331
+ policy: "retain-until-withdrawn",
332
+ reviewedAt: review.reviewedAt
333
+ },
334
+ takedown: {
335
+ state: "active",
336
+ contact: "legal@samva.dev",
337
+ procedureUrl: "https://samva.dev/contact?subject=Template%20takedown"
338
+ }
339
+ };
340
+ })();
293
341
  const facets = {
294
342
  jobType: [value.useCase],
295
343
  lifecycle: facetValues(value.collections, "stage"),
@@ -297,7 +345,7 @@ const template = (value) => {
297
345
  season: value.presetId === "seasonal-sale-flyer" || value.presetId === "season-clearance-promo" ? ["seasonal"] : ["evergreen"],
298
346
  audience: audienceFor(value.useCase),
299
347
  design: facetValues(value.collections, "style"),
300
- brandProvenance: ["samva-original"]
348
+ brandProvenance: [corpus._tag === "BrandRecreationPreset" ? "third-party-reference" : "samva-original"]
301
349
  };
302
350
  const registration = registerPublicCorpusEntry({
303
351
  id: value.presetId,
@@ -644,6 +692,24 @@ const publicTemplates = [
644
692
  ],
645
693
  relatedTemplates: ["shipping-update", "receipt"]
646
694
  }),
695
+ template({
696
+ presetId: "order-receipt",
697
+ slug: "order-receipt",
698
+ useCase: "commerce",
699
+ collections: [
700
+ "category:transactional",
701
+ "industry:ecommerce",
702
+ "stage:transaction",
703
+ "style:minimal",
704
+ "style:data-heavy",
705
+ "style:single-cta"
706
+ ],
707
+ relatedTemplates: [
708
+ "receipt",
709
+ "order-delivered",
710
+ "shipping-update"
711
+ ]
712
+ }),
647
713
  template({
648
714
  presetId: "travel-rewards-promo",
649
715
  slug: "travel-rewards-promo",
@@ -812,6 +878,63 @@ const publicTemplates = [
812
878
  "style:single-cta"
813
879
  ],
814
880
  relatedTemplates: ["promo", "announcement"]
881
+ }),
882
+ template({
883
+ presetId: "linear-login-code",
884
+ slug: "linear-login-code",
885
+ useCase: "account-access",
886
+ collections: [
887
+ "category:transactional",
888
+ "industry:saas",
889
+ "industry:developer-tools",
890
+ "stage:activation",
891
+ "style:minimal",
892
+ "style:branded",
893
+ "style:single-cta"
894
+ ],
895
+ relatedTemplates: ["otp", "email-verification"]
896
+ }),
897
+ template({
898
+ presetId: "product-newsletter",
899
+ slug: "product-newsletter",
900
+ useCase: "publishing-campaigns",
901
+ collections: [
902
+ "category:digest-editorial",
903
+ "industry:saas",
904
+ "industry:developer-tools",
905
+ "stage:engagement",
906
+ "style:editorial",
907
+ "style:branded"
908
+ ],
909
+ relatedTemplates: ["newsletter", "product-digest"]
910
+ }),
911
+ template({
912
+ presetId: "product-launch-promo",
913
+ slug: "product-launch-promotion",
914
+ useCase: "commerce",
915
+ collections: [
916
+ "category:marketing",
917
+ "industry:ecommerce",
918
+ "stage:engagement",
919
+ "style:branded",
920
+ "style:single-cta"
921
+ ],
922
+ relatedTemplates: ["promo", "season-clearance-promo"]
923
+ }),
924
+ template({
925
+ presetId: "vercel-invitation",
926
+ slug: "vercel-team-invitation",
927
+ useCase: "product-communication",
928
+ collections: [
929
+ "category:transactional",
930
+ "industry:saas",
931
+ "industry:developer-tools",
932
+ "stage:activation",
933
+ "style:minimal",
934
+ "style:branded",
935
+ "style:single-cta"
936
+ ],
937
+ relatedTemplates: ["team-invite", "welcome"]
815
938
  })
816
939
  ];
817
940
  const publicTemplateIds = publicTemplates.map((item) => item.presetId);
@@ -2,11 +2,18 @@ import { d as Channel } from "../ast-63I-VLQx.js";
2
2
  import { PresetCategory } from "../presets.js";
3
3
  import { TemplateCollectionId, TemplateUseCase } from "./public-gallery.js";
4
4
  import { PublicTemplatePreview } from "./public-preview.js";
5
+ import { PresetProject } from "./sources.js";
5
6
  import { Result } from "effect";
6
7
  //#region presets/public-handoff.d.ts
8
+ /** The brand a clean-room recreation names, and what its email does. */
9
+ interface TemplateBrandRecreation {
10
+ readonly brand: string;
11
+ readonly subject: string;
12
+ }
7
13
  /** Where an approved public artifact came from. */
8
14
  interface TemplateReferenceProvenance {
9
- readonly source: "samva-owned" | "sanitized-synthetic" | "reviewed-public";
15
+ readonly source: "samva-owned" | "sanitized-synthetic" | "reviewed-public" | "brand-recreation";
16
+ readonly brandRecreation?: TemplateBrandRecreation;
10
17
  /** Stable identifier in the producing system; never a third-party URL. */
11
18
  readonly referenceId: string;
12
19
  /** Stable ISO-8601 timestamp supplied by the producer. */
@@ -27,6 +34,7 @@ interface ApprovedPublicTemplateArtifact {
27
34
  readonly description: string;
28
35
  readonly variables: ReadonlyArray<string>;
29
36
  readonly source: string;
37
+ readonly project?: PresetProject;
30
38
  };
31
39
  readonly gallery: {
32
40
  readonly slug: string;
@@ -62,8 +70,9 @@ interface TemplateReferencePacket {
62
70
  readonly provenance: TemplateReferenceProvenance;
63
71
  }
64
72
  interface ValidatedPublicTemplateArtifact {
65
- readonly preset: Omit<ApprovedPublicTemplateArtifact["preset"], "source">;
73
+ readonly preset: Omit<ApprovedPublicTemplateArtifact["preset"], "source" | "project">;
66
74
  readonly source: string;
75
+ readonly project?: PresetProject;
67
76
  readonly gallery: ApprovedPublicTemplateArtifact["gallery"] & {
68
77
  readonly path: `/templates/${string}`;
69
78
  };
@@ -82,6 +91,16 @@ interface PublicHandoffIssue {
82
91
  }
83
92
  /** Bound authored source before parsing or mounting it. */
84
93
  declare const PUBLIC_TEMPLATE_SOURCE_MAX_CHARACTERS = 512000;
94
+ /** Bounds apply to decoded bytes, before evaluation or disk emission. */
95
+ declare const PUBLIC_TEMPLATE_PROJECT_MAX_BYTES: number;
96
+ declare const PUBLIC_TEMPLATE_PROJECT_FILE_MAX_BYTES: number;
97
+ declare const PUBLIC_TEMPLATE_PROJECT_MAX_FILES = 64;
98
+ declare const isSafePublicTemplateProjectPath: (path: string) => boolean;
99
+ /** Reject traversal, collisions, malformed base64 and oversized authored projects. */
100
+ declare const isSafePublicTemplateProject: (project: PresetProject) => boolean;
101
+ /** Digest the complete authored graph independently of caller file ordering. */
102
+ declare const publicTemplateProjectDigest: (project: Pick<PresetProject, "entryPath" | "files">) => Promise<string>;
103
+ declare const isValidTemplateBrandRecreation: (brandRecreation: TemplateBrandRecreation | undefined, project: PresetProject | undefined) => boolean;
85
104
  /** Static syntax inventory; loops, branches and component expansion are not evaluated. */
86
105
  interface PublicTemplateAuthoredStructure {
87
106
  readonly basis: "authored-jsx";
@@ -117,12 +136,17 @@ type TemplateReferenceProjection = {
117
136
  readonly relativePath: "preset.tsx";
118
137
  readonly contents: string;
119
138
  } | {
120
- readonly kind: "catalog" | "gallery" | "preview" | "editorial";
121
- readonly relativePath: `${"catalog" | "gallery" | "preview" | "editorial"}.json`;
139
+ readonly kind: "project-file";
140
+ readonly relativePath: `project/${string}`;
141
+ readonly contents: string;
142
+ readonly encoding: "utf8" | "base64";
143
+ } | {
144
+ readonly kind: "catalog" | "gallery" | "preview" | "editorial" | "project";
145
+ readonly relativePath: `${"catalog" | "gallery" | "preview" | "editorial" | "project"}.json`;
122
146
  readonly contents: string;
123
147
  };
124
148
  /**
125
- * Emits the five destination projections as deterministic bytes. The importer
149
+ * Emits legacy projections plus the complete project as deterministic bytes. The importer
126
150
  * owns no checked-in destination files; callers choose an explicit output dir.
127
151
  */
128
152
  declare const emitTemplateReferencePacket: (packet: TemplateReferencePacket) => ReadonlyArray<TemplateReferenceProjection>;
@@ -136,5 +160,5 @@ declare const emitTemplateReferencePacket: (packet: TemplateReferencePacket) =>
136
160
  */
137
161
  declare const validatePublicTemplateHandoff: (artifacts: ReadonlyArray<ApprovedPublicTemplateArtifact>) => Result.Result<ReadonlyArray<ValidatedPublicTemplateArtifact>, ReadonlyArray<PublicHandoffIssue>>;
138
162
  //#endregion
139
- export { ApprovedPublicTemplateArtifact, PUBLIC_TEMPLATE_SOURCE_MAX_CHARACTERS, PublicHandoffIssue, PublicHandoffIssueCode, PublicTemplateAuthoredStructure, TemplateReferencePacket, TemplateReferenceProjection, TemplateReferenceProvenance, ValidatedPublicTemplateArtifact, emitTemplateReferencePacket, isSafePublicTemplateAsset, publicTemplateAuthoredStructure, toTemplateReferencePacket, unsupportedPublicTemplateFontTokens, validatePublicTemplateHandoff };
163
+ export { ApprovedPublicTemplateArtifact, PUBLIC_TEMPLATE_PROJECT_FILE_MAX_BYTES, PUBLIC_TEMPLATE_PROJECT_MAX_BYTES, PUBLIC_TEMPLATE_PROJECT_MAX_FILES, PUBLIC_TEMPLATE_SOURCE_MAX_CHARACTERS, PublicHandoffIssue, PublicHandoffIssueCode, PublicTemplateAuthoredStructure, TemplateBrandRecreation, TemplateReferencePacket, TemplateReferenceProjection, TemplateReferenceProvenance, ValidatedPublicTemplateArtifact, emitTemplateReferencePacket, isSafePublicTemplateAsset, isSafePublicTemplateProject, isSafePublicTemplateProjectPath, isValidTemplateBrandRecreation, publicTemplateAuthoredStructure, publicTemplateProjectDigest, toTemplateReferencePacket, unsupportedPublicTemplateFontTokens, validatePublicTemplateHandoff };
140
164
  //# sourceMappingURL=public-handoff.d.ts.map
@@ -11,9 +11,62 @@ const MAX_DATA_IMAGE_ASSET_LENGTH = 32768;
11
11
  const DATA_IMAGE = /^data:image\/(?:avif|gif|jpeg|png|svg\+xml|webp)(?:;charset=[a-z0-9-]+)?(?:;base64)?,[^\r\n]+$/i;
12
12
  const PUBLIC_TEMPLATE_ASSET_PATH = /^\/templates\/assets\/[A-Za-z0-9][A-Za-z0-9._-]*\.(?:jpe?g|png|webp)$/;
13
13
  const BLOCKED_PUBLIC_ASSET = /(?:unsplash|placehold|assets\.example)/i;
14
- const isTemplateReferenceProvenanceSource = (value) => value === "samva-owned" || value === "sanitized-synthetic" || value === "reviewed-public";
14
+ /** A sample value is non-empty when every string in it is, and no list or object is empty. */
15
+ const nonEmptySample = (value) => {
16
+ if (typeof value === "string") return nonEmpty(value);
17
+ if (Array.isArray(value)) return value.length > 0 && value.every(nonEmptySample);
18
+ if (Predicate.isObject(value)) {
19
+ const values = Object.values(value);
20
+ return values.length > 0 && values.every(nonEmptySample);
21
+ }
22
+ return true;
23
+ };
24
+ const isTemplateReferenceProvenanceSource = (value) => value === "samva-owned" || value === "sanitized-synthetic" || value === "reviewed-public" || value === "brand-recreation";
15
25
  /** Bound authored source before parsing or mounting it. */
16
26
  const PUBLIC_TEMPLATE_SOURCE_MAX_CHARACTERS = 512e3;
27
+ /** Bounds apply to decoded bytes, before evaluation or disk emission. */
28
+ const PUBLIC_TEMPLATE_PROJECT_MAX_BYTES = 8388608;
29
+ const PUBLIC_TEMPLATE_PROJECT_FILE_MAX_BYTES = 2097152;
30
+ const PUBLIC_TEMPLATE_PROJECT_MAX_FILES = 64;
31
+ const isSafePublicTemplateProjectPath = (path) => path.length <= 240 && path.split("/").every((part) => /^[A-Za-z0-9_][A-Za-z0-9._-]*$/.test(part) && part !== "." && part !== ".." && part !== "node_modules");
32
+ /** Reject traversal, collisions, malformed base64 and oversized authored projects. */
33
+ const isSafePublicTemplateProject = (project) => {
34
+ if (!isSafePublicTemplateProjectPath(project.entryPath) || !project.entryPath.endsWith(".tsx") || !/^[a-f0-9]{64}$/.test(project.digest) || project.files.length === 0 || project.files.length > 64) return false;
35
+ const paths = /* @__PURE__ */ new Set();
36
+ let total = 0;
37
+ for (const file of project.files) {
38
+ if (!isSafePublicTemplateProjectPath(file.path) || paths.has(file.path.toLowerCase())) return false;
39
+ paths.add(file.path.toLowerCase());
40
+ let bytes;
41
+ if (file.encoding === "utf8") {
42
+ if (file.content.length > 512e3) return false;
43
+ bytes = new TextEncoder().encode(file.content).length;
44
+ } else if (file.encoding === "base64") {
45
+ if (file.content.length > Math.ceil(2097152 / 3) * 4 || file.content.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(file.content)) return false;
46
+ if (btoa(atob(file.content)) !== file.content) return false;
47
+ bytes = file.content.length / 4 * 3 - (file.content.endsWith("==") ? 2 : file.content.endsWith("=") ? 1 : 0);
48
+ } else return false;
49
+ if (bytes > 2097152) return false;
50
+ total += bytes;
51
+ if (total > 8388608) return false;
52
+ }
53
+ for (const path of paths) if (path.split("/").slice(0, -1).some((_, index, parts) => paths.has(parts.slice(0, index + 1).join("/")))) return false;
54
+ return project.files.some((file) => file.path === project.entryPath && file.encoding === "utf8");
55
+ };
56
+ /** Digest the complete authored graph independently of caller file ordering. */
57
+ const publicTemplateProjectDigest = async (project) => {
58
+ const serialized = JSON.stringify({
59
+ entryPath: project.entryPath,
60
+ files: [...project.files].sort((a, b) => a.path.localeCompare(b.path)).map(({ path, content, encoding }) => ({
61
+ path,
62
+ content,
63
+ encoding
64
+ }))
65
+ });
66
+ const bytes = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(serialized)));
67
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
68
+ };
69
+ const isValidTemplateBrandRecreation = (brandRecreation, project) => brandRecreation !== void 0 && project !== void 0 && brandRecreation.brand.trim().length > 0 && brandRecreation.subject.trim().length > 0;
17
70
  const PUBLIC_TEMPLATE_FONT_TOKENS = /* @__PURE__ */ new Set([
18
71
  "sans",
19
72
  "serif",
@@ -167,37 +220,51 @@ const jsonProjection = (kind, value) => ({
167
220
  contents: `${JSON.stringify(value, null, 2)}\n`
168
221
  });
169
222
  /**
170
- * Emits the five destination projections as deterministic bytes. The importer
223
+ * Emits legacy projections plus the complete project as deterministic bytes. The importer
171
224
  * owns no checked-in destination files; callers choose an explicit output dir.
172
225
  */
173
- const emitTemplateReferencePacket = (packet) => [
174
- {
175
- kind: "source",
176
- relativePath: "preset.tsx",
177
- contents: packet.preset.source
178
- },
179
- jsonProjection("catalog", {
180
- id: packet.preset.id,
181
- channel: packet.preset.channel,
182
- category: packet.preset.category,
183
- name: packet.preset.name,
184
- description: packet.preset.description,
185
- variables: packet.preset.variables
186
- }),
187
- jsonProjection("gallery", {
188
- presetId: packet.preset.id,
189
- ...packet.gallery,
190
- provenance: packet.provenance
191
- }),
192
- jsonProjection("preview", packet.preview),
193
- jsonProjection("editorial", {
194
- cardTitle: packet.publicMetadata.cardTitle,
195
- cardDescription: packet.publicMetadata.cardDescription,
196
- guidance: packet.publicMetadata.guidance,
197
- variables: packet.publicMetadata.variableDescriptions,
198
- seo: packet.publicMetadata.seo
199
- })
200
- ];
226
+ const emitTemplateReferencePacket = (packet) => {
227
+ const project = packet.preset.project;
228
+ if (project !== void 0 && (!isSafePublicTemplateProject(project) || project.files.find((file) => file.path === project.entryPath)?.content !== packet.preset.source)) throw new Error("Invalid public template project");
229
+ if (packet.provenance.source === "brand-recreation" && !isValidTemplateBrandRecreation(packet.provenance.brandRecreation, project)) throw new Error("Invalid public template brand recreation attribution");
230
+ return [
231
+ ...project === void 0 ? [] : [jsonProjection("project", {
232
+ ...project,
233
+ files: [...project.files].sort((a, b) => a.path.localeCompare(b.path))
234
+ }), ...[...project.files].sort((a, b) => a.path.localeCompare(b.path)).map((file) => ({
235
+ kind: "project-file",
236
+ relativePath: `project/${file.path}`,
237
+ contents: file.content,
238
+ encoding: file.encoding
239
+ }))],
240
+ {
241
+ kind: "source",
242
+ relativePath: "preset.tsx",
243
+ contents: packet.preset.source
244
+ },
245
+ jsonProjection("catalog", {
246
+ id: packet.preset.id,
247
+ channel: packet.preset.channel,
248
+ category: packet.preset.category,
249
+ name: packet.preset.name,
250
+ description: packet.preset.description,
251
+ variables: packet.preset.variables
252
+ }),
253
+ jsonProjection("gallery", {
254
+ presetId: packet.preset.id,
255
+ ...packet.gallery,
256
+ provenance: packet.provenance
257
+ }),
258
+ jsonProjection("preview", packet.preview),
259
+ jsonProjection("editorial", {
260
+ cardTitle: packet.publicMetadata.cardTitle,
261
+ cardDescription: packet.publicMetadata.cardDescription,
262
+ guidance: packet.publicMetadata.guidance,
263
+ variables: packet.publicMetadata.variableDescriptions,
264
+ seo: packet.publicMetadata.seo
265
+ })
266
+ ];
267
+ };
201
268
  /**
202
269
  * Validates metadata and TSX syntax, never executing the template. The Vite importer
203
270
  * verifies schema, fixtures and rendered content before writing projections.
@@ -223,6 +290,9 @@ const validatePublicTemplateHandoff = (artifacts) => {
223
290
  if (existingSlugs.has(artifact.gallery.slug) || suppliedSlugs.has(artifact.gallery.slug)) issues.push(issue(id, "duplicate-slug", `canonical slug "${artifact.gallery.slug}" is already present`));
224
291
  suppliedIds.add(id);
225
292
  suppliedSlugs.add(artifact.gallery.slug);
293
+ const project = artifact.preset.project;
294
+ if (project !== void 0 && (!isSafePublicTemplateProject(project) || project.files.find((file) => file.path === project.entryPath)?.content !== artifact.preset.source)) issues.push(issue(id, "source", "project must contain safe bounded unique files and the matching UTF-8 entry source"));
295
+ if (artifact.provenance?.source === "brand-recreation" && !isValidTemplateBrandRecreation(artifact.provenance.brandRecreation, project)) issues.push(issue(id, "provenance", "brand recreations require a complete project, a referenced brand, and its email subject"));
226
296
  if (parsePublicTemplateSource(artifact.preset.source) === void 0) issues.push(issue(id, "source", "source must be a bounded TSX module with a default export; execution is verified by the importer"));
227
297
  if (artifact.preset.channel !== "email") issues.push(issue(id, "channel", "public template source must be an executable email"));
228
298
  if (new Set(artifact.preset.variables).size !== artifact.preset.variables.length || artifact.preset.variables.some((value) => !nonEmpty(value))) issues.push(issue(id, "variables", "declared input fields must be non-empty and unique"));
@@ -230,13 +300,13 @@ const validatePublicTemplateHandoff = (artifacts) => {
230
300
  if (unsupportedFontTokens.length > 0) issues.push(issue(id, "font-token", `unsupported public font token: ${unsupportedFontTokens.join(", ")}`));
231
301
  const sampleVariables = Object.keys(artifact.preview.sampleData);
232
302
  if (!sameStrings(sampleVariables, artifact.preset.variables)) issues.push(issue(id, "sample-data", "sample data must cover every declared variable exactly"));
233
- if (Object.values(artifact.preview.sampleData).some((value) => !nonEmpty(value))) issues.push(issue(id, "sample-data", "sample data values must be non-empty"));
303
+ if (Object.values(artifact.preview.sampleData).some((value) => !nonEmptySample(value))) issues.push(issue(id, "sample-data", "sample data values must be non-empty"));
234
304
  const assetVariables = artifact.preview.assets.map((asset) => asset.variable);
235
305
  if (new Set(assetVariables).size !== assetVariables.length) issues.push(issue(id, "assets", "asset variable references must be unique"));
236
306
  for (const asset of artifact.preview.assets) {
237
307
  const value = artifact.preview.sampleData[asset.variable];
238
308
  if (!artifact.preset.variables.includes(asset.variable)) issues.push(issue(id, "assets", `asset "${asset.variable}" is not a declared preset variable`));
239
- if (value === void 0 || !isSafePublicTemplateAsset(value)) issues.push(issue(id, "assets", `asset "${asset.variable}" must reference a bounded HTTPS, /templates/assets, or data:image sample URL`));
309
+ if (typeof value !== "string" || !isSafePublicTemplateAsset(value)) issues.push(issue(id, "assets", `asset "${asset.variable}" must reference a bounded HTTPS, /templates/assets, or data:image sample URL`));
240
310
  }
241
311
  const metadata = artifact.publicMetadata;
242
312
  const metadataStrings = [
@@ -275,6 +345,7 @@ const validatePublicTemplateHandoff = (artifacts) => {
275
345
  variables: artifact.preset.variables
276
346
  },
277
347
  source: artifact.preset.source,
348
+ ...artifact.preset.project === void 0 ? {} : { project: artifact.preset.project },
278
349
  gallery: reference.gallery,
279
350
  preview: artifact.preview,
280
351
  publicMetadata: artifact.publicMetadata,
@@ -283,6 +354,6 @@ const validatePublicTemplateHandoff = (artifacts) => {
283
354
  }));
284
355
  };
285
356
  //#endregion
286
- export { PUBLIC_TEMPLATE_SOURCE_MAX_CHARACTERS, emitTemplateReferencePacket, isSafePublicTemplateAsset, publicTemplateAuthoredStructure, toTemplateReferencePacket, unsupportedPublicTemplateFontTokens, validatePublicTemplateHandoff };
357
+ export { PUBLIC_TEMPLATE_PROJECT_FILE_MAX_BYTES, PUBLIC_TEMPLATE_PROJECT_MAX_BYTES, PUBLIC_TEMPLATE_PROJECT_MAX_FILES, PUBLIC_TEMPLATE_SOURCE_MAX_CHARACTERS, emitTemplateReferencePacket, isSafePublicTemplateAsset, isSafePublicTemplateProject, isSafePublicTemplateProjectPath, isValidTemplateBrandRecreation, publicTemplateAuthoredStructure, publicTemplateProjectDigest, toTemplateReferencePacket, unsupportedPublicTemplateFontTokens, validatePublicTemplateHandoff };
287
358
 
288
359
  //# sourceMappingURL=public-handoff.js.map
@@ -1,7 +1,7 @@
1
1
  import { presetCatalog } from "../presets.js";
2
2
  import { registerPublicCorpusEntry } from "./corpus.js";
3
3
  import { publicTemplates } from "./public-gallery.js";
4
- import { presetEmailFixtures, presetModuleSource } from "./sources.js";
4
+ import { presetEmailAssets, presetEmailFixtures, presetModuleSource } from "./sources.js";
5
5
  import { publicTemplatePreviews } from "./public-preview.js";
6
6
  import { Result } from "effect";
7
7
  //#region presets/public-preview-artifacts.ts
@@ -63,14 +63,14 @@ const buildPublicTemplatePreviewArtifact = (template) => {
63
63
  message: "canonical preset source is missing"
64
64
  });
65
65
  const sampleData = fixture.input;
66
- const assets = [];
66
+ const assets = new Set(presetEmailAssets[template.presetId]);
67
67
  for (const asset of preview.assets) {
68
68
  const value = sampleData[asset.variable];
69
- if (value === void 0) return Result.fail({
69
+ if (typeof value !== "string") return Result.fail({
70
70
  presetId: template.presetId,
71
- message: `sample data is missing asset: ${asset.variable}`
71
+ message: `sample data is missing a string asset: ${asset.variable}`
72
72
  });
73
- assets.push(value);
73
+ assets.add(value);
74
74
  }
75
75
  return Result.succeed({
76
76
  presetId: template.presetId,
@@ -80,7 +80,7 @@ const buildPublicTemplatePreviewArtifact = (template) => {
80
80
  quality: preview.quality,
81
81
  source,
82
82
  html: fixture.html,
83
- assets,
83
+ assets: [...assets],
84
84
  renders: renderInputs(template.slug, fixture.html)
85
85
  });
86
86
  };
@@ -1,10 +1,14 @@
1
1
  //#region presets/public-preview.d.ts
2
2
  type PreviewQuality = "ready" | "asset-free";
3
+ /** A declared fixture value: JSON, so a template can take a list of stories as well as a string. */
4
+ type PreviewSampleValue = string | number | boolean | null | ReadonlyArray<PreviewSampleValue> | {
5
+ readonly [key: string]: PreviewSampleValue;
6
+ };
3
7
  interface PublicTemplatePreview {
4
8
  readonly subject: string;
5
9
  readonly preheader: string;
6
10
  readonly quality: PreviewQuality;
7
- readonly sampleData: Readonly<Record<string, string>>;
11
+ readonly sampleData: Readonly<Record<string, PreviewSampleValue>>;
8
12
  readonly assets: ReadonlyArray<{
9
13
  readonly variable: string;
10
14
  readonly kind: "image" | "logo";
@@ -12,6 +16,11 @@ interface PublicTemplatePreview {
12
16
  }
13
17
  /** Gallery metadata augments the actual default fixture; it does not supply render input. */
14
18
  declare const publicTemplatePreviews: {
19
+ readonly "linear-login-code": PublicTemplatePreview;
20
+ readonly "product-newsletter": PublicTemplatePreview;
21
+ readonly "order-receipt": PublicTemplatePreview;
22
+ readonly "vercel-invitation": PublicTemplatePreview;
23
+ readonly "product-launch-promo": PublicTemplatePreview;
15
24
  readonly welcome: PublicTemplatePreview;
16
25
  readonly "email-verification": PublicTemplatePreview;
17
26
  readonly otp: PublicTemplatePreview;
@@ -52,5 +61,5 @@ declare const publicTemplatePreviews: {
52
61
  readonly "season-clearance-promo": PublicTemplatePreview;
53
62
  };
54
63
  //#endregion
55
- export { PreviewQuality, PublicTemplatePreview, publicTemplatePreviews };
64
+ export { PreviewQuality, PreviewSampleValue, PublicTemplatePreview, publicTemplatePreviews };
56
65
  //# sourceMappingURL=public-preview.d.ts.map
@@ -13,6 +13,20 @@ const preview = (id, quality, assets) => {
13
13
  };
14
14
  /** Gallery metadata augments the actual default fixture; it does not supply render input. */
15
15
  const publicTemplatePreviews = {
16
+ "linear-login-code": preview("linear-login-code", "ready", []),
17
+ "product-newsletter": preview("product-newsletter", "ready", [{
18
+ variable: "leadImage",
19
+ kind: "image"
20
+ }]),
21
+ "order-receipt": preview("order-receipt", "ready", []),
22
+ "vercel-invitation": preview("vercel-invitation", "ready", [{
23
+ variable: "userImage",
24
+ kind: "image"
25
+ }, {
26
+ variable: "teamImage",
27
+ kind: "image"
28
+ }]),
29
+ "product-launch-promo": preview("product-launch-promo", "ready", []),
16
30
  welcome: preview("welcome", "ready", [{
17
31
  variable: "logoUrl",
18
32
  kind: "logo"
@@ -1,20 +1,38 @@
1
1
  //#region presets/sources.gen.d.ts
2
+ /** A declared fixture value: JSON, so a template can take a list of stories as well as a string. */
3
+ type PresetEmailFixtureValue = string | number | boolean | null | ReadonlyArray<PresetEmailFixtureValue> | {
4
+ readonly [key: string]: PresetEmailFixtureValue;
5
+ };
2
6
  interface PresetEmailFixture {
3
- readonly input: Readonly<Record<string, string>>;
7
+ readonly input: Readonly<Record<string, PresetEmailFixtureValue>>;
4
8
  readonly subject: string;
5
9
  readonly preheader: string;
6
10
  readonly html: string;
7
11
  readonly text: string;
8
12
  }
13
+ interface PresetProjectFile {
14
+ readonly path: string;
15
+ readonly content: string;
16
+ readonly encoding: "utf8" | "base64";
17
+ }
18
+ interface PresetProject {
19
+ readonly entryPath: string;
20
+ readonly files: ReadonlyArray<PresetProjectFile>;
21
+ readonly digest: string;
22
+ }
23
+ /** Complete editable projects, including styles and assets. */
24
+ declare const presetProjects: Readonly<Record<string, PresetProject>>;
25
+ declare const presetProject: (id: string) => PresetProject | undefined;
26
+ declare const presetEmailAssets: Readonly<Record<string, ReadonlyArray<string>>>;
9
27
  /** Canonical TSX entry source, loaded on demand by authoring surfaces. */
10
28
  declare const presetModuleSources: Readonly<Record<string, string>>;
11
29
  declare const presetModuleDigests: Readonly<Record<string, string>>;
12
- declare const presetModuleManifestDigest = "8c97d809a3ff8d6b1bf49a90ce0275aa98882fc733cc3ed9638e2c38dd78f0fc";
30
+ declare const presetModuleManifestDigest = "b2b55659bcb164b0b0269010d7644aabd17b9b2cae1bcdbaad6e6cfee303945a";
13
31
  /** SML belongs only to the gated SMS and WhatsApp template compilers. */
14
32
  declare const structuredPresetSources: Readonly<Record<string, string>>;
15
33
  /** Gallery snapshots generated by executing each declared fixture with actual input. */
16
34
  declare const presetEmailFixtures: Readonly<Record<string, Readonly<Record<string, PresetEmailFixture>>>>;
17
35
  declare const presetModuleSource: (id: string) => string | undefined;
18
36
  //#endregion
19
- export { PresetEmailFixture, presetEmailFixtures, presetModuleDigests, presetModuleManifestDigest, presetModuleSource, presetModuleSources, structuredPresetSources };
37
+ export { PresetEmailFixture, PresetEmailFixtureValue, PresetProject, PresetProjectFile, presetEmailAssets, presetEmailFixtures, presetModuleDigests, presetModuleManifestDigest, presetModuleSource, presetModuleSources, presetProject, presetProjects, structuredPresetSources };
20
38
  //# sourceMappingURL=sources.d.ts.map