@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.
- package/dist/compiler.d.ts +7 -5
- package/dist/compiler.js +7 -6
- package/dist/{corpus-Dz5irJ9x.d.ts → corpus-qrUD5O6r.d.ts} +96 -3
- package/dist/{stylesheets-oDRAnYA3.d.ts → css-ar2lU3NC.d.ts} +2 -12
- package/dist/email/components.d.ts +8 -2
- package/dist/email/components.js +8 -5
- package/dist/email/render.d.ts +8 -1
- package/dist/email/render.js +2 -2
- package/dist/email/stylesheets.d.ts +15 -2
- package/dist/email/stylesheets.js +3 -1
- package/dist/email/text.js +9 -1
- package/dist/presets/corpus.d.ts +2 -2
- package/dist/presets/corpus.js +65 -4
- package/dist/presets/public-gallery.d.ts +23 -5
- package/dist/presets/public-gallery.js +154 -31
- package/dist/presets/public-handoff.d.ts +30 -6
- package/dist/presets/public-handoff.js +104 -33
- package/dist/presets/public-preview-artifacts.js +6 -6
- package/dist/presets/public-preview.d.ts +11 -2
- package/dist/presets/public-preview.js +14 -0
- package/dist/presets/sources.d.ts +21 -3
- package/dist/presets/sources.js +2164 -1048
- package/dist/presets.d.ts +37 -3
- package/dist/presets.js +96 -0
- package/dist/{render-Cddsy_XF.js → render-ykQhtMs_.js} +13 -6
- package/package.json +1 -1
|
@@ -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
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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: "
|
|
121
|
-
readonly relativePath:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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) => !
|
|
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
|
|
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
|
|
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.
|
|
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,
|
|
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,
|
|
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 = "
|
|
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
|