@hraness/design-kit 0.31.1
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/ARTICLE_COPY.md +208 -0
- package/HERO_FIELDS.md +55 -0
- package/ICONS.md +56 -0
- package/LANTERN_MATERIAL.md +60 -0
- package/LICENSE +21 -0
- package/MARKETING_COPY.md +49 -0
- package/MARKETING_PRESET.md +108 -0
- package/PALETTES.md +90 -0
- package/PAPER_THEME.md +128 -0
- package/README.md +745 -0
- package/STATUS_PAGES.md +67 -0
- package/STYLE.md +245 -0
- package/WRITING.md +72 -0
- package/dist/browser/index.js +2345 -0
- package/dist/chunk-44djxk16.js +6872 -0
- package/dist/chunk-5gtx3pza.js +9 -0
- package/dist/chunk-77391vmq.js +454 -0
- package/dist/chunk-7b1586eb.js +1209 -0
- package/dist/chunk-8834fh4n.js +6 -0
- package/dist/chunk-9t8xyqte.js +870 -0
- package/dist/chunk-cejpzyfh.js +446 -0
- package/dist/chunk-eh71jz57.js +429 -0
- package/dist/chunk-evb02bc1.js +516 -0
- package/dist/chunk-h4k7yv6x.js +368 -0
- package/dist/chunk-he8eznb1.js +760 -0
- package/dist/chunk-mr1vdcjq.js +404 -0
- package/dist/chunk-wzvdn8ey.js +116 -0
- package/dist/fonts/nebula-sans/social-fonts.generated.js +53 -0
- package/dist/icons.js +48 -0
- package/dist/index.js +237 -0
- package/dist/launch.js +41 -0
- package/dist/mockups/client.js +422 -0
- package/dist/mockups/index.js +1850 -0
- package/dist/portfolio.js +1549 -0
- package/dist/provider-marks.js +19 -0
- package/dist/react/charts.js +11 -0
- package/dist/react/hero-backdrop.js +8 -0
- package/dist/react/index.js +7252 -0
- package/dist/react/platform-install.js +10 -0
- package/dist/react/server.js +137 -0
- package/dist/stylex-manifest.json +1 -0
- package/dist/stylex.css +5392 -0
- package/dist/syntax-highlighting.js +15 -0
- package/dist/testing.js +250 -0
- package/package.json +377 -0
- package/portfolio-inventory.json +31 -0
- package/scripts/check-lantern-material-snapshot.d.mts +26 -0
- package/scripts/check-lantern-material-snapshot.mjs +126 -0
- package/scripts/check-marketing-snapshot.d.mts +12 -0
- package/scripts/check-marketing-snapshot.mjs +58 -0
- package/scripts/lantern-material-snapshot.ts +101 -0
- package/scripts/marketing-textures.ts +48 -0
- package/scripts/paper-theme-snapshot.ts +97 -0
- package/scripts/product-marketing-snapshot.ts +149 -0
- package/src/appearance-menu.css +266 -0
- package/src/appearance.ts +32 -0
- package/src/article-html.ts +269 -0
- package/src/article.ts +686 -0
- package/src/browser/appearance-menu.ts +604 -0
- package/src/browser/artifact-share.ts +164 -0
- package/src/browser/design-palette.ts +197 -0
- package/src/browser/foil.ts +124 -0
- package/src/browser/hero-light.ts +11 -0
- package/src/browser/index.ts +29 -0
- package/src/browser/status-field.ts +432 -0
- package/src/browser/status-page.ts +100 -0
- package/src/browser/sticky-offset.ts +82 -0
- package/src/browser/theme-color-sync.ts +213 -0
- package/src/charts.css +7 -0
- package/src/compiler-components.css +34 -0
- package/src/compiler-foundation.css +18 -0
- package/src/compiler-palettes.css +7 -0
- package/src/compiler-tokens.css +342 -0
- package/src/components.css +3 -0
- package/src/design-gallery.css +431 -0
- package/src/effects.css +14 -0
- package/src/fonts/geist-mono/GeistMono[wght].woff2 +0 -0
- package/src/fonts/geist-mono/OFL.txt +93 -0
- package/src/fonts/geist-mono/PROVENANCE.md +12 -0
- package/src/fonts/instrument-serif/OFL.txt +93 -0
- package/src/fonts/instrument-serif/UPSTREAM.md +14 -0
- package/src/fonts/instrument-serif/instrument-serif-latin-400.woff2 +0 -0
- package/src/fonts/nebula-sans/LICENSE.txt +96 -0
- package/src/fonts/nebula-sans/NebulaSans-Black.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-BlackItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Bold.otf +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Bold.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-BoldItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Book.otf +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-BookItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Light.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-LightItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Medium.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-MediumItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-Semibold.woff2 +0 -0
- package/src/fonts/nebula-sans/NebulaSans-SemiboldItalic.woff2 +0 -0
- package/src/fonts/nebula-sans/PROVENANCE.md +14 -0
- package/src/fonts/nebula-sans/social-fonts.generated.ts +3873 -0
- package/src/fonts.css +106 -0
- package/src/icons/act60/act60.svg +5 -0
- package/src/icons/act60/application.svg +9 -0
- package/src/icons/act60/chapter-compare.svg +3 -0
- package/src/icons/act60/decree-residence.svg +6 -0
- package/src/icons/act60/estimate.svg +7 -0
- package/src/icons/act60/export-services.svg +4 -0
- package/src/icons/act60/investor.svg +13 -0
- package/src/icons/act60/presence.svg +7 -0
- package/src/icons/aicharts/aicharts.svg +6 -0
- package/src/icons/aicharts/cost-compare.svg +4 -0
- package/src/icons/aicharts/task-audio.svg +7 -0
- package/src/icons/aicharts/task-coding.svg +6 -0
- package/src/icons/aicharts/task-images.svg +3 -0
- package/src/icons/aicharts/task-reasoning.svg +9 -0
- package/src/icons/aicharts/task-research.svg +5 -0
- package/src/icons/aicharts/task-video.svg +4 -0
- package/src/icons/manifest.json +1371 -0
- package/src/icons/platonik/conversation.svg +3 -0
- package/src/icons/platonik/creature.svg +7 -0
- package/src/icons/platonik/economy.svg +4 -0
- package/src/icons/platonik/keep-light.svg +6 -0
- package/src/icons/platonik/platonik.svg +7 -0
- package/src/icons/platonik/spark-route.svg +15 -0
- package/src/icons/receipts/act60-retry.receipt.json +888 -0
- package/src/icons/receipts/act60.receipt.json +1366 -0
- package/src/icons/receipts/aicharts.receipt.json +1546 -0
- package/src/icons/receipts/platonik.receipt.json +1011 -0
- package/src/icons/receipts/roughday.receipt.json +770 -0
- package/src/icons/receipts/shared.receipt.json +1296 -0
- package/src/icons/receipts/slopcamera.receipt.json +1495 -0
- package/src/icons/receipts/soundfish-retry.receipt.json +460 -0
- package/src/icons/receipts/soundfish.receipt.json +1709 -0
- package/src/icons/receipts/sponge.receipt.json +1743 -0
- package/src/icons/receipts/stripe-history-retry.receipt.json +145 -0
- package/src/icons/receipts/stripe-history.receipt.json +1202 -0
- package/src/icons/receipts/wordcell.receipt.json +1924 -0
- package/src/icons/roughday/finance.svg +7 -0
- package/src/icons/roughday/roughday.svg +6 -0
- package/src/icons/roughday/technology.svg +4 -0
- package/src/icons/roughday/world.svg +4 -0
- package/src/icons/sets/act60.json +63 -0
- package/src/icons/sets/aicharts.json +63 -0
- package/src/icons/sets/platonik.json +39 -0
- package/src/icons/sets/roughday.json +25 -0
- package/src/icons/sets/shared.json +12 -0
- package/src/icons/sets/slopcamera.json +47 -0
- package/src/icons/sets/soundfish.json +43 -0
- package/src/icons/sets/sponge.json +51 -0
- package/src/icons/sets/stripe-history.json +39 -0
- package/src/icons/sets/wordcell.json +51 -0
- package/src/icons/shared/agent-skill.svg +4 -0
- package/src/icons/shared/cli.svg +5 -0
- package/src/icons/shared/privacy.svg +3 -0
- package/src/icons/shared/research.svg +7 -0
- package/src/icons/shared/sdk.svg +3 -0
- package/src/icons/slopcamera/diagram.svg +3 -0
- package/src/icons/slopcamera/direct-scene.svg +5 -0
- package/src/icons/slopcamera/edit-video.svg +17 -0
- package/src/icons/slopcamera/mcp.svg +3 -0
- package/src/icons/slopcamera/native-world.svg +3 -0
- package/src/icons/slopcamera/slopcamera.svg +11 -0
- package/src/icons/soundfish/agent-ui.svg +7 -0
- package/src/icons/soundfish/collaborate.svg +5 -0
- package/src/icons/soundfish/compose.svg +3 -0
- package/src/icons/soundfish/hear.svg +6 -0
- package/src/icons/soundfish/human-ui.svg +6 -0
- package/src/icons/soundfish/share.svg +3 -0
- package/src/icons/soundfish/soundfish.svg +6 -0
- package/src/icons/sponge/api.svg +3 -0
- package/src/icons/sponge/documents.svg +3 -0
- package/src/icons/sponge/library.svg +3 -0
- package/src/icons/sponge/one-question.svg +5 -0
- package/src/icons/sponge/people-direct.svg +5 -0
- package/src/icons/sponge/read.svg +3 -0
- package/src/icons/sponge/save.svg +4 -0
- package/src/icons/sponge/sponge.svg +7 -0
- package/src/icons/stripe-history/company-history.svg +3 -0
- package/src/icons/stripe-history/evidence.svg +4 -0
- package/src/icons/stripe-history/independence.svg +7 -0
- package/src/icons/stripe-history/publications.svg +11 -0
- package/src/icons/stripe-history/sources.svg +6 -0
- package/src/icons/stripe-history/stripe-history.svg +3 -0
- package/src/icons/wordcell/backlinks.svg +6 -0
- package/src/icons/wordcell/capture.svg +6 -0
- package/src/icons/wordcell/git-provenance.svg +4 -0
- package/src/icons/wordcell/kb.svg +4 -0
- package/src/icons/wordcell/markdown.svg +4 -0
- package/src/icons/wordcell/scopes.svg +3 -0
- package/src/icons/wordcell/search.svg +7 -0
- package/src/icons.generated.ts +734 -0
- package/src/icons.ts +51 -0
- package/src/index.ts +275 -0
- package/src/lantern-material.css +213 -0
- package/src/launch.ts +624 -0
- package/src/marketing-assets/UPSTREAM.md +18 -0
- package/src/marketing-assets/cells.svg +1 -0
- package/src/marketing-assets/grain.svg +1 -0
- package/src/marketing-forced-colors.css +12 -0
- package/src/mockups/client.tsx +415 -0
- package/src/mockups/core.tsx +318 -0
- package/src/mockups/frames.tsx +368 -0
- package/src/mockups/index.ts +14 -0
- package/src/mockups/surfaces.tsx +681 -0
- package/src/mockups.css +2607 -0
- package/src/palette-appearance.ts +45 -0
- package/src/palette-bridge.css +124 -0
- package/src/palette-color.ts +41 -0
- package/src/palette-system.css +721 -0
- package/src/palette-themes.ts +23 -0
- package/src/palette-tokens.stylex.ts +519 -0
- package/src/palettes.css +3 -0
- package/src/palettes.ts +161 -0
- package/src/paper-theme.css +244 -0
- package/src/plain-publication.css +1241 -0
- package/src/plain-site.css +397 -0
- package/src/platforms.ts +151 -0
- package/src/portfolio.generated.json +1595 -0
- package/src/portfolio.generated.ts +1596 -0
- package/src/portfolio.ts +226 -0
- package/src/product-marketing-foundation.css +433 -0
- package/src/product-marketing-preset.css +261 -0
- package/src/product-marketing.css +2186 -0
- package/src/provider-marks.generated.ts +112 -0
- package/src/provider-marks.ts +143 -0
- package/src/react/animated-rail-stage.stylex.ts +15 -0
- package/src/react/animated-rail-stage.tsx +51 -0
- package/src/react/app-shell.stylex.ts +104 -0
- package/src/react/app-shell.tsx +121 -0
- package/src/react/article.tsx +602 -0
- package/src/react/aurora-dots-background.tsx +34 -0
- package/src/react/charts.stylex.ts +563 -0
- package/src/react/charts.tsx +610 -0
- package/src/react/chat.stylex.ts +34 -0
- package/src/react/chat.tsx +155 -0
- package/src/react/design-gallery.tsx +943 -0
- package/src/react/design-palette.stylex.ts +124 -0
- package/src/react/design-palette.tsx +196 -0
- package/src/react/design-theme-context.tsx +41 -0
- package/src/react/effects.stylex.ts +325 -0
- package/src/react/fader.stylex.ts +71 -0
- package/src/react/fader.tsx +151 -0
- package/src/react/foil-card-math.ts +100 -0
- package/src/react/foil-card-surface.stylex.ts +326 -0
- package/src/react/foil-card-surface.tsx +742 -0
- package/src/react/foil-mark.tsx +35 -0
- package/src/react/foil.stylex.ts +163 -0
- package/src/react/haptics.ts +228 -0
- package/src/react/hero-backdrop.tsx +22 -0
- package/src/react/index.ts +38 -0
- package/src/react/keyboard-shortcuts.ts +228 -0
- package/src/react/lantern-material-gallery.tsx +166 -0
- package/src/react/lantern-material.stylex.ts +39 -0
- package/src/react/launch-beats.tsx +73 -0
- package/src/react/navigation-rail.stylex.ts +130 -0
- package/src/react/navigation-rail.tsx +228 -0
- package/src/react/particle-halo.tsx +115 -0
- package/src/react/phaser-dots.tsx +501 -0
- package/src/react/platform-icons.tsx +94 -0
- package/src/react/platform-install.stylex.ts +385 -0
- package/src/react/platform-install.tsx +389 -0
- package/src/react/playback-transport.stylex.ts +14 -0
- package/src/react/playback-transport.tsx +112 -0
- package/src/react/procedural-backdrop.tsx +192 -0
- package/src/react/procedural-recipe.ts +323 -0
- package/src/react/product-marketing.stylex.ts +4469 -0
- package/src/react/product-marketing.tsx +1611 -0
- package/src/react/production-data-preview-notice.stylex.ts +40 -0
- package/src/react/production-data-preview-notice.tsx +34 -0
- package/src/react/provider-mark.stylex.ts +132 -0
- package/src/react/provider-mark.tsx +120 -0
- package/src/react/relative-time.tsx +117 -0
- package/src/react/route-state.stylex.ts +39 -0
- package/src/react/route-state.tsx +350 -0
- package/src/react/server.ts +11 -0
- package/src/react/social-kit-panel.tsx +103 -0
- package/src/react/sticky-offset.tsx +33 -0
- package/src/react/surfaces.stylex.ts +179 -0
- package/src/react/surfaces.tsx +330 -0
- package/src/react/syntax-code.tsx +33 -0
- package/src/react/theme-color-sync.ts +1 -0
- package/src/react/theme-resolution.ts +11 -0
- package/src/react/theme.stylex.ts +276 -0
- package/src/react/theme.tsx +402 -0
- package/src/reading.css +72 -0
- package/src/relative-time.ts +185 -0
- package/src/reset.css +2 -0
- package/src/site-shell.css +16 -0
- package/src/status-page-html.ts +63 -0
- package/src/status-page.css +276 -0
- package/src/status-page.ts +323 -0
- package/src/styles.css +21 -0
- package/src/syntax-code-lexers.ts +303 -0
- package/src/syntax-highlighting.css +78 -0
- package/src/syntax-highlighting.ts +477 -0
- package/src/syntax-tokens.ts +33 -0
- package/src/testing.ts +307 -0
- package/src/tokens.css +3 -0
- package/src/typography.css +86 -0
- package/vendor/evilcharts/LICENSE +21 -0
- package/vendor/evilcharts/UPSTREAM.md +14 -0
- package/vendor/platform-marks/LICENSE +30 -0
- package/vendor/platform-marks/UPSTREAM.md +15 -0
- package/vendor/platform-marks/apple.svg +1 -0
- package/vendor/platform-marks/linux.svg +1 -0
- package/vendor/provider-marks/CRUSH-LICENSE.md +134 -0
- package/vendor/provider-marks/LICENSE +21 -0
- package/vendor/provider-marks/UPSTREAM.md +27 -0
- package/vendor/provider-marks/aider.svg +3 -0
- package/vendor/provider-marks/crush-heartbit.svg +8 -0
- package/vendor/provider-marks/lobehub/alibabacloud-color.svg +1 -0
- package/vendor/provider-marks/lobehub/alibabacloud.svg +1 -0
- package/vendor/provider-marks/lobehub/amp-color.svg +1 -0
- package/vendor/provider-marks/lobehub/amp.svg +1 -0
- package/vendor/provider-marks/lobehub/anthropic.svg +1 -0
- package/vendor/provider-marks/lobehub/claude-color.svg +1 -0
- package/vendor/provider-marks/lobehub/claude.svg +1 -0
- package/vendor/provider-marks/lobehub/claudecode-color.svg +1 -0
- package/vendor/provider-marks/lobehub/claudecode.svg +1 -0
- package/vendor/provider-marks/lobehub/codex-color.svg +1 -0
- package/vendor/provider-marks/lobehub/codex.svg +1 -0
- package/vendor/provider-marks/lobehub/cursor.svg +1 -0
- package/vendor/provider-marks/lobehub/deepseek-color.svg +1 -0
- package/vendor/provider-marks/lobehub/deepseek.svg +1 -0
- package/vendor/provider-marks/lobehub/devin-color.svg +1 -0
- package/vendor/provider-marks/lobehub/devin.svg +1 -0
- package/vendor/provider-marks/lobehub/gemini-color.svg +1 -0
- package/vendor/provider-marks/lobehub/gemini.svg +1 -0
- package/vendor/provider-marks/lobehub/geminicli-color.svg +1 -0
- package/vendor/provider-marks/lobehub/geminicli.svg +1 -0
- package/vendor/provider-marks/lobehub/goose.svg +1 -0
- package/vendor/provider-marks/lobehub/meta-color.svg +1 -0
- package/vendor/provider-marks/lobehub/meta.svg +1 -0
- package/vendor/provider-marks/lobehub/mistral-color.svg +1 -0
- package/vendor/provider-marks/lobehub/mistral.svg +1 -0
- package/vendor/provider-marks/lobehub/moonshot.svg +1 -0
- package/vendor/provider-marks/lobehub/nvidia-color.svg +1 -0
- package/vendor/provider-marks/lobehub/nvidia.svg +1 -0
- package/vendor/provider-marks/lobehub/openai.svg +1 -0
- package/vendor/provider-marks/lobehub/opencode.svg +1 -0
- package/vendor/provider-marks/lobehub/perplexity-color.svg +1 -0
- package/vendor/provider-marks/lobehub/perplexity.svg +1 -0
- package/vendor/provider-marks/lobehub/qwen-color.svg +1 -0
- package/vendor/provider-marks/lobehub/qwen.svg +1 -0
- package/vendor/provider-marks/lobehub/xai.svg +1 -0
- package/vendor/provider-marks/lobehub/zai.svg +1 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Finite, asset-free portable presentation contract. */
|
|
2
|
+
export const lanternSnapshotPaths: Readonly<{
|
|
3
|
+
"lantern-material.css": "src/lantern-material.css";
|
|
4
|
+
"check.mjs": "scripts/check-lantern-material-snapshot.mjs";
|
|
5
|
+
"check.d.mts": "scripts/check-lantern-material-snapshot.d.mts";
|
|
6
|
+
LICENSE: "LICENSE";
|
|
7
|
+
}>;
|
|
8
|
+
export type LanternSnapshotArtifact = keyof typeof lanternSnapshotPaths;
|
|
9
|
+
export const lanternSnapshotFileLimits: Readonly<Record<LanternSnapshotArtifact, number>>;
|
|
10
|
+
export interface LanternMaterialSnapshot {
|
|
11
|
+
readonly schemaVersion: 1;
|
|
12
|
+
readonly contractVersion: 1;
|
|
13
|
+
readonly source: {
|
|
14
|
+
readonly repository: "https://github.com/hraness/design-kit";
|
|
15
|
+
readonly commit: string;
|
|
16
|
+
readonly export: "@hraness/design-kit/lantern-material.css";
|
|
17
|
+
};
|
|
18
|
+
readonly files: Readonly<Record<LanternSnapshotArtifact, {
|
|
19
|
+
readonly path: string;
|
|
20
|
+
readonly sha256: string;
|
|
21
|
+
}>>;
|
|
22
|
+
}
|
|
23
|
+
export function createLanternMaterialSnapshot(commit: string, files: Readonly<Record<LanternSnapshotArtifact, Uint8Array>>): LanternMaterialSnapshot;
|
|
24
|
+
export function parseLanternMaterialSnapshot(value: unknown): LanternMaterialSnapshot;
|
|
25
|
+
/** Offline byte/provenance consistency check. This does not authenticate a GitHub release. */
|
|
26
|
+
export function checkLanternMaterialSnapshot(directory?: string): Promise<LanternMaterialSnapshot>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { log } from "node:console";
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { constants } from "node:fs";
|
|
5
|
+
import { lstat, open, readdir, realpath } from "node:fs/promises";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import process from "node:process";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { TextDecoder } from "node:util";
|
|
10
|
+
|
|
11
|
+
// This finite, asset-free inventory is owned by the checker, never the manifest.
|
|
12
|
+
export const lanternSnapshotPaths = Object.freeze({
|
|
13
|
+
"lantern-material.css": "src/lantern-material.css",
|
|
14
|
+
"check.mjs": "scripts/check-lantern-material-snapshot.mjs",
|
|
15
|
+
"check.d.mts": "scripts/check-lantern-material-snapshot.d.mts",
|
|
16
|
+
LICENSE: "LICENSE",
|
|
17
|
+
});
|
|
18
|
+
export const lanternSnapshotFileLimits = Object.freeze({
|
|
19
|
+
"lantern-material.css": 256 * 1024,
|
|
20
|
+
"check.mjs": 128 * 1024,
|
|
21
|
+
"check.d.mts": 64 * 1024,
|
|
22
|
+
LICENSE: 64 * 1024,
|
|
23
|
+
});
|
|
24
|
+
const names = Object.keys(lanternSnapshotPaths);
|
|
25
|
+
const repository = "https://github.com/hraness/design-kit";
|
|
26
|
+
const exportedCss = "@hraness/design-kit/lantern-material.css";
|
|
27
|
+
const checkerDirectory = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29
|
+
const keysEqual = (value, keys) => record(value) && Object.keys(value).sort().join("\n") === [...keys].sort().join("\n");
|
|
30
|
+
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
31
|
+
const commitPattern = /^[a-f0-9]{40}$/u;
|
|
32
|
+
const hashPattern = /^[a-f0-9]{64}$/u;
|
|
33
|
+
|
|
34
|
+
export function createLanternMaterialSnapshot(commit, files) {
|
|
35
|
+
if (typeof commit !== "string" || !commitPattern.test(commit)) throw new Error("Snapshot source must be a full lowercase Git commit.");
|
|
36
|
+
if (!keysEqual(files, names)) throw new Error("Snapshot has missing or unowned source files.");
|
|
37
|
+
for (const name of names) {
|
|
38
|
+
if (!(files[name] instanceof Uint8Array) || files[name].byteLength === 0 || files[name].byteLength > lanternSnapshotFileLimits[name]) {
|
|
39
|
+
throw new Error(`Invalid ${name} source bytes.`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
schemaVersion: 1,
|
|
44
|
+
contractVersion: 1,
|
|
45
|
+
source: { repository, commit, export: exportedCss },
|
|
46
|
+
files: Object.fromEntries(names.map((name) => [name, { path: lanternSnapshotPaths[name], sha256: sha256(files[name]) }])),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function parseLanternMaterialSnapshot(value) {
|
|
51
|
+
if (!keysEqual(value, ["schemaVersion", "contractVersion", "source", "files"])
|
|
52
|
+
|| value.schemaVersion !== 1 || value.contractVersion !== 1
|
|
53
|
+
|| !keysEqual(value.source, ["repository", "commit", "export"])
|
|
54
|
+
|| value.source.repository !== repository || value.source.export !== exportedCss
|
|
55
|
+
|| typeof value.source.commit !== "string" || !commitPattern.test(value.source.commit)
|
|
56
|
+
|| !keysEqual(value.files, names)) throw new Error("Invalid Lantern material snapshot provenance.");
|
|
57
|
+
for (const name of names) {
|
|
58
|
+
const receipt = value.files[name];
|
|
59
|
+
if (!keysEqual(receipt, ["path", "sha256"]) || receipt.path !== lanternSnapshotPaths[name]
|
|
60
|
+
|| typeof receipt.sha256 !== "string" || !hashPattern.test(receipt.sha256)) throw new Error(`Invalid ${name} provenance.`);
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function sameIdentity(left, right) {
|
|
66
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function physicalFile(path, maximum) {
|
|
70
|
+
const before = await lstat(path);
|
|
71
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1 || before.size === 0 || before.size > maximum) {
|
|
72
|
+
throw new Error("Snapshot files must be bounded physical files with one link.");
|
|
73
|
+
}
|
|
74
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
75
|
+
try {
|
|
76
|
+
const opened = await file.stat();
|
|
77
|
+
if (!sameIdentity(before, opened) || !opened.isFile() || opened.nlink !== 1 || opened.size !== before.size) {
|
|
78
|
+
throw new Error("Snapshot file changed while opening.");
|
|
79
|
+
}
|
|
80
|
+
// Read only the admitted length plus one sentinel byte. A concurrent
|
|
81
|
+
// append must fail without allocating or reading the growing file to EOF.
|
|
82
|
+
const bytes = Buffer.alloc(before.size + 1);
|
|
83
|
+
let length = 0;
|
|
84
|
+
while (length < bytes.length) {
|
|
85
|
+
const { bytesRead } = await file.read(bytes, length, bytes.length - length, length);
|
|
86
|
+
if (bytesRead === 0) break;
|
|
87
|
+
length += bytesRead;
|
|
88
|
+
}
|
|
89
|
+
const after = await file.stat();
|
|
90
|
+
const current = await lstat(path);
|
|
91
|
+
if (length !== before.size || !sameIdentity(before, current) || current.isSymbolicLink()
|
|
92
|
+
|| current.nlink !== 1 || after.nlink !== 1 || after.size !== before.size
|
|
93
|
+
|| after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs) throw new Error("Snapshot file changed while reading.");
|
|
94
|
+
return bytes.subarray(0, length);
|
|
95
|
+
} finally {
|
|
96
|
+
await file.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function checkLanternMaterialSnapshot(root = checkerDirectory) {
|
|
101
|
+
root = resolve(root);
|
|
102
|
+
const directory = await lstat(root);
|
|
103
|
+
if (!directory.isDirectory() || directory.isSymbolicLink()) throw new Error("Snapshot requires a physical directory.");
|
|
104
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
105
|
+
if (entries.map((entry) => entry.name).sort().join("\n") !== [...names, "provenance.json"].sort().join("\n")) {
|
|
106
|
+
throw new Error("Snapshot has missing or unowned files.");
|
|
107
|
+
}
|
|
108
|
+
if (entries.some((entry) => !entry.isFile() || entry.isSymbolicLink())) throw new Error("Snapshot requires only physical files.");
|
|
109
|
+
const provenanceBytes = await physicalFile(join(root, "provenance.json"), 16 * 1024);
|
|
110
|
+
const manifest = parseLanternMaterialSnapshot(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(provenanceBytes)));
|
|
111
|
+
for (const name of names) {
|
|
112
|
+
const bytes = await physicalFile(join(root, name), lanternSnapshotFileLimits[name]);
|
|
113
|
+
if (sha256(bytes) !== manifest.files[name].sha256) throw new Error(`${name} differs from its immutable snapshot.`);
|
|
114
|
+
}
|
|
115
|
+
if (!provenanceBytes.equals(await physicalFile(join(root, "provenance.json"), 16 * 1024))) throw new Error("Snapshot provenance changed while checking.");
|
|
116
|
+
const after = await lstat(root);
|
|
117
|
+
if (!sameIdentity(directory, after) || !after.isDirectory() || after.isSymbolicLink()
|
|
118
|
+
|| (await readdir(root)).sort().join("\n") !== [...names, "provenance.json"].sort().join("\n")) throw new Error("Snapshot directory changed while checking.");
|
|
119
|
+
return manifest;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (process.argv[1] !== undefined && await realpath(process.argv[1]).catch(() => undefined) === fileURLToPath(import.meta.url)) {
|
|
123
|
+
if (process.argv.length > 3) throw new Error("Usage: node check.mjs [SNAPSHOT_DIRECTORY]");
|
|
124
|
+
const manifest = await checkLanternMaterialSnapshot(process.argv[2] === undefined ? checkerDirectory : resolve(process.argv[2]));
|
|
125
|
+
log(`Lantern material snapshot verified at ${manifest.source.commit}.`);
|
|
126
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Offline validation; throws for altered or unowned files and returns the admitted inventory. */
|
|
2
|
+
export interface MarketingSnapshot {
|
|
3
|
+
readonly schemaVersion: 1;
|
|
4
|
+
readonly contractVersion: 1;
|
|
5
|
+
readonly source: {
|
|
6
|
+
readonly repository: "https://github.com/hraness/design-kit";
|
|
7
|
+
readonly commit: string;
|
|
8
|
+
readonly export: "@hraness/design-kit/product-marketing-preset.css";
|
|
9
|
+
};
|
|
10
|
+
readonly files: Readonly<Record<string, { readonly path: string; readonly sha256: string }>>;
|
|
11
|
+
}
|
|
12
|
+
export function checkMarketingSnapshot(directory?: string): Promise<MarketingSnapshot>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { log } from "node:console";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { lstat, readFile, readdir, realpath } from "node:fs/promises";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
// Finite contract: never accept paths supplied by an untrusted manifest.
|
|
9
|
+
const paths = {
|
|
10
|
+
"product-marketing-preset.css": "src/product-marketing-preset.css",
|
|
11
|
+
"fonts/instrument-serif/instrument-serif-latin-400.woff2": "src/fonts/instrument-serif/instrument-serif-latin-400.woff2",
|
|
12
|
+
"fonts/instrument-serif/OFL.txt": "src/fonts/instrument-serif/OFL.txt",
|
|
13
|
+
"fonts/instrument-serif/UPSTREAM.md": "src/fonts/instrument-serif/UPSTREAM.md",
|
|
14
|
+
"marketing-assets/grain.svg": "src/marketing-assets/grain.svg",
|
|
15
|
+
"marketing-assets/cells.svg": "src/marketing-assets/cells.svg",
|
|
16
|
+
"marketing-assets/UPSTREAM.md": "src/marketing-assets/UPSTREAM.md",
|
|
17
|
+
"check.mjs": "scripts/check-marketing-snapshot.mjs",
|
|
18
|
+
"check.d.mts": "scripts/check-marketing-snapshot.d.mts",
|
|
19
|
+
LICENSE: "LICENSE",
|
|
20
|
+
};
|
|
21
|
+
const checkerDirectory = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
|
+
async function inventory(directory, prefix = "") {
|
|
24
|
+
const details = await lstat(directory);
|
|
25
|
+
if (!details.isDirectory() || details.isSymbolicLink()) throw new Error("Snapshot requires physical directories.");
|
|
26
|
+
const files = [];
|
|
27
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
28
|
+
const path = prefix + entry.name;
|
|
29
|
+
if (entry.isSymbolicLink()) throw new Error(`Snapshot contains a symbolic link: ${path}`);
|
|
30
|
+
if (entry.isDirectory()) {
|
|
31
|
+
if (!Object.keys(paths).some((name) => name.startsWith(`${path}/`))) throw new Error(`Snapshot contains an unowned directory: ${path}`);
|
|
32
|
+
files.push(...await inventory(join(directory, entry.name), `${path}/`));
|
|
33
|
+
} else if (entry.isFile()) files.push(path);
|
|
34
|
+
else throw new Error(`Snapshot contains a non-file: ${path}`);
|
|
35
|
+
}
|
|
36
|
+
return files.sort();
|
|
37
|
+
}
|
|
38
|
+
export async function checkMarketingSnapshot(root = checkerDirectory) {
|
|
39
|
+
if ((await inventory(root)).join("\n") !== [...Object.keys(paths), "provenance.json"].sort().join("\n")) throw new Error("Snapshot has missing or unowned files.");
|
|
40
|
+
const manifest = JSON.parse(await readFile(join(root, "provenance.json"), "utf8"));
|
|
41
|
+
if (!record(manifest) || manifest.schemaVersion !== 1 || manifest.contractVersion !== 1
|
|
42
|
+
|| !record(manifest.source) || manifest.source.repository !== "https://github.com/hraness/design-kit"
|
|
43
|
+
|| manifest.source.export !== "@hraness/design-kit/product-marketing-preset.css"
|
|
44
|
+
|| typeof manifest.source.commit !== "string" || !/^[a-f0-9]{40}$/u.test(manifest.source.commit)
|
|
45
|
+
|| !record(manifest.files) || Object.keys(manifest.files).sort().join("\n") !== Object.keys(paths).sort().join("\n")) throw new Error("Invalid marketing snapshot provenance.");
|
|
46
|
+
for (const [name, sourcePath] of Object.entries(paths)) {
|
|
47
|
+
const receipt = manifest.files[name];
|
|
48
|
+
if (!record(receipt) || receipt.path !== sourcePath || typeof receipt.sha256 !== "string" || !/^[a-f0-9]{64}$/u.test(receipt.sha256)) throw new Error(`Invalid ${name} provenance.`);
|
|
49
|
+
if (createHash("sha256").update(await readFile(join(root, name))).digest("hex") !== receipt.sha256) throw new Error(`${name} differs from its immutable snapshot.`);
|
|
50
|
+
}
|
|
51
|
+
return manifest;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (process.argv[1] !== undefined && await realpath(process.argv[1]).catch(() => undefined) === fileURLToPath(import.meta.url)) {
|
|
55
|
+
if (process.argv.length > 3) throw new Error("Usage: node check.mjs [SNAPSHOT_DIRECTORY]");
|
|
56
|
+
const manifest = await checkMarketingSnapshot(process.argv[2] === undefined ? checkerDirectory : resolve(process.argv[2]));
|
|
57
|
+
log(`Marketing snapshot verified at ${manifest.source.commit}.`);
|
|
58
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { lstat, mkdir, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
checkLanternMaterialSnapshot,
|
|
8
|
+
createLanternMaterialSnapshot,
|
|
9
|
+
lanternSnapshotFileLimits,
|
|
10
|
+
lanternSnapshotPaths,
|
|
11
|
+
} from "./check-lantern-material-snapshot.mjs";
|
|
12
|
+
import type { LanternMaterialSnapshot, LanternSnapshotArtifact } from "./check-lantern-material-snapshot.mjs";
|
|
13
|
+
|
|
14
|
+
export { checkLanternMaterialSnapshot, createLanternMaterialSnapshot, lanternSnapshotPaths, parseLanternMaterialSnapshot } from "./check-lantern-material-snapshot.mjs";
|
|
15
|
+
export type { LanternMaterialSnapshot } from "./check-lantern-material-snapshot.mjs";
|
|
16
|
+
|
|
17
|
+
const names = Object.keys(lanternSnapshotPaths) as LanternSnapshotArtifact[];
|
|
18
|
+
const missing = (error: unknown) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
19
|
+
type DirectoryIdentity = { readonly dev: number; readonly ino: number };
|
|
20
|
+
type Previous = { readonly identity: DirectoryIdentity; readonly manifest: LanternMaterialSnapshot | undefined };
|
|
21
|
+
|
|
22
|
+
async function previousSnapshot(directory: string): Promise<Previous | undefined> {
|
|
23
|
+
let identity;
|
|
24
|
+
try { identity = await lstat(directory); } catch (error) { if (missing(error)) return undefined; throw error; }
|
|
25
|
+
if (!identity.isDirectory() || identity.isSymbolicLink()) throw new Error("Snapshot destination must be a physical directory.");
|
|
26
|
+
const entries = await readdir(directory);
|
|
27
|
+
return { identity, manifest: entries.length === 0 ? undefined : await checkLanternMaterialSnapshot(directory) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function assertPrevious(directory: string, previous: Previous): Promise<void> {
|
|
31
|
+
const current = await previousSnapshot(directory);
|
|
32
|
+
if (current === undefined || current.identity.dev !== previous.identity.dev || current.identity.ino !== previous.identity.ino
|
|
33
|
+
|| JSON.stringify(current.manifest) !== JSON.stringify(previous.manifest)) throw new Error("Snapshot changed during installation.");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function writeLanternMaterialSnapshot(directory: string, commit: string, sourceRoot = resolve(import.meta.dir, "..")): Promise<void> {
|
|
37
|
+
if (!/^[a-f0-9]{40}$/u.test(commit)) throw new Error("Snapshot source must be a full lowercase Git commit.");
|
|
38
|
+
const git = (args: readonly string[], maximum = 1024) => execFileSync("git", ["--no-pager", "--no-optional-locks", "--no-replace-objects", ...args], {
|
|
39
|
+
cwd: sourceRoot, maxBuffer: maximum, timeout: 30_000, stdio: ["ignore", "pipe", "pipe"],
|
|
40
|
+
});
|
|
41
|
+
if (git(["cat-file", "-t", commit]).toString("utf8").trim() !== "commit") {
|
|
42
|
+
throw new Error("Snapshot source must name a commit object, not a tree or tag object.");
|
|
43
|
+
}
|
|
44
|
+
// Every source mode and byte is read from the exact Git object before any
|
|
45
|
+
// destination mutation. A dirty checkout cannot change the admitted payload.
|
|
46
|
+
const files = Object.fromEntries(names.map((name) => {
|
|
47
|
+
const path = lanternSnapshotPaths[name];
|
|
48
|
+
const entry = git(["ls-tree", commit, "--", path]).toString("utf8").trim();
|
|
49
|
+
if (!/^100644 blob [a-f0-9]{40}\t/u.test(entry) || entry.split("\t")[1] !== path) throw new Error(`Snapshot source ${path} must be one regular Git blob.`);
|
|
50
|
+
return [name, git(["show", "--no-textconv", `${commit}:${path}`], lanternSnapshotFileLimits[name])];
|
|
51
|
+
})) as Record<LanternSnapshotArtifact, Buffer>;
|
|
52
|
+
const manifest = createLanternMaterialSnapshot(commit, files);
|
|
53
|
+
directory = resolve(directory);
|
|
54
|
+
const previous = await previousSnapshot(directory);
|
|
55
|
+
await mkdir(dirname(directory), { recursive: true });
|
|
56
|
+
// Resolve an existing parent once; do not stage through a changing symlink.
|
|
57
|
+
directory = join(await realpath(dirname(directory)), basename(directory));
|
|
58
|
+
if (previous !== undefined) await assertPrevious(directory, previous);
|
|
59
|
+
else if (await previousSnapshot(directory) !== undefined) throw new Error("Snapshot destination appeared during installation.");
|
|
60
|
+
const stage = `${directory}.stage-${randomUUID()}`;
|
|
61
|
+
const backup = `${directory}.previous-${randomUUID()}`;
|
|
62
|
+
let backedUp = false;
|
|
63
|
+
try {
|
|
64
|
+
await mkdir(stage, { mode: 0o700 });
|
|
65
|
+
for (const name of names) await writeFile(join(stage, name), files[name], { flag: "wx", mode: 0o600 });
|
|
66
|
+
await writeFile(join(stage, "provenance.json"), `${JSON.stringify(manifest, null, 2)}\n`, { flag: "wx", mode: 0o600 });
|
|
67
|
+
await checkLanternMaterialSnapshot(stage);
|
|
68
|
+
if (previous !== undefined) {
|
|
69
|
+
await assertPrevious(directory, previous);
|
|
70
|
+
await rename(directory, backup);
|
|
71
|
+
backedUp = true;
|
|
72
|
+
await assertPrevious(backup, previous);
|
|
73
|
+
} else if (await previousSnapshot(directory) !== undefined) throw new Error("Snapshot destination appeared during installation.");
|
|
74
|
+
await rename(stage, directory);
|
|
75
|
+
const installed = await checkLanternMaterialSnapshot(directory);
|
|
76
|
+
if (JSON.stringify(installed) !== JSON.stringify(manifest)) throw new Error("Installed snapshot differs from its immutable source.");
|
|
77
|
+
if (backedUp && previous !== undefined) {
|
|
78
|
+
await assertPrevious(backup, previous);
|
|
79
|
+
await rm(backup, { recursive: true });
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (backedUp) {
|
|
83
|
+
// Never inspect or remove an unexpected destination while recovering the
|
|
84
|
+
// admitted previous directory. Keep the backup if any path now exists.
|
|
85
|
+
try { await lstat(directory); } catch (absent) { if (missing(absent)) await rename(backup, directory); }
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
} finally {
|
|
89
|
+
await rm(stage, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (import.meta.main) {
|
|
94
|
+
const [operation, directory, flag, commit, ...extra] = process.argv.slice(2);
|
|
95
|
+
if (operation === "--check" && directory !== undefined && flag === undefined) {
|
|
96
|
+
console.log(`Lantern material snapshot verified at ${(await checkLanternMaterialSnapshot(resolve(directory))).source.commit}.`);
|
|
97
|
+
} else if (operation === "--write" && directory !== undefined && flag === "--source-commit" && commit !== undefined && extra.length === 0) {
|
|
98
|
+
await writeLanternMaterialSnapshot(directory, commit);
|
|
99
|
+
console.log(`Lantern material snapshot installed from ${commit}.`);
|
|
100
|
+
} else throw new Error("Usage: bun scripts/lantern-material-snapshot.ts --check DIRECTORY | --write DIRECTORY --source-commit FULL_COMMIT");
|
|
101
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Original deterministic Hraness textures. Grain stays behind content. */
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
export function marketingTextures(): Readonly<Record<"grain.svg" | "cells.svg", string>> {
|
|
7
|
+
let seed = 4652026;
|
|
8
|
+
function random() {
|
|
9
|
+
seed ^= seed << 13;
|
|
10
|
+
seed ^= seed >>> 17;
|
|
11
|
+
seed ^= seed << 5;
|
|
12
|
+
return (seed >>> 0) / 4294967296;
|
|
13
|
+
}
|
|
14
|
+
const stipple = ["", ""];
|
|
15
|
+
for (let y = 0; y < 128; y++) {
|
|
16
|
+
for (let x = 0; x < 128; x++) {
|
|
17
|
+
const value = random();
|
|
18
|
+
if (value < .65) stipple[value < .325 ? 0 : 1] += `M${x} ${y}h1v1h-1z`;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const grain = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><g opacity=".03"><path fill="#000" d="${stipple[0]}"/><path fill="#fff" d="${stipple[1]}"/></g></svg>\n`;
|
|
22
|
+
const gradients: string[] = [];
|
|
23
|
+
const faces: string[] = [];
|
|
24
|
+
// Eight by eight original faces form a seamless repeating field. Each pane
|
|
25
|
+
// has its own light direction and transmission; no visible grid is drawn.
|
|
26
|
+
for (let row = 0; row < 8; row++) {
|
|
27
|
+
for (let column = 0; column < 8; column++) {
|
|
28
|
+
const id = `p${row * 8 + column}`;
|
|
29
|
+
const angle = Math.round(20 + random() * 65);
|
|
30
|
+
const light = (0.025 + random() * 0.045).toFixed(3);
|
|
31
|
+
const shade = (0.008 + random() * 0.026).toFixed(3);
|
|
32
|
+
gradients.push(`<linearGradient id="${id}" x2="${angle}%" y2="100%"><stop stop-color="#fff" stop-opacity="${light}"/><stop offset=".46" stop-color="#fff" stop-opacity="0"/><stop offset="1" stop-color="#000" stop-opacity="${shade}"/></linearGradient>`);
|
|
33
|
+
faces.push(`<path d="M${column * 96} ${row * 96}h96v96h-96z" fill="url(#${id})"/>`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const cells = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768"><defs>${gradients.join("")}</defs>${faces.join("")}</svg>\n`;
|
|
37
|
+
|
|
38
|
+
return { "grain.svg": grain, "cells.svg": cells };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (import.meta.main) {
|
|
42
|
+
const writing = process.argv.slice(2).join(" ") === "--write";
|
|
43
|
+
for (const [name, contents] of Object.entries(marketingTextures())) {
|
|
44
|
+
const path = resolve(import.meta.dir, "../src/marketing-assets", name);
|
|
45
|
+
if (writing) await writeFile(path, contents);
|
|
46
|
+
else assert.equal(await readFile(path, "utf8"), contents, `${name} differs from its checked source.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
const repository = "https://github.com/hraness/design-kit";
|
|
7
|
+
const artifacts = ["paper-theme.css", "LICENSE"] as const;
|
|
8
|
+
const digest = (value: string) => createHash("sha256").update(value).digest("hex");
|
|
9
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
|
|
11
|
+
export interface PaperThemeSnapshot {
|
|
12
|
+
readonly schemaVersion: 1;
|
|
13
|
+
readonly contractVersion: 1;
|
|
14
|
+
readonly source: {
|
|
15
|
+
readonly repository: typeof repository;
|
|
16
|
+
readonly commit: string;
|
|
17
|
+
readonly export: "@hraness/design-kit/paper-theme.css";
|
|
18
|
+
};
|
|
19
|
+
readonly files: Readonly<Record<(typeof artifacts)[number], { readonly path: string; readonly sha256: string }>>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createPaperThemeSnapshot(commit: string, css: string, license: string): PaperThemeSnapshot {
|
|
23
|
+
if (!/^[a-f0-9]{40}$/u.test(commit)) throw new Error("Snapshot source must be a full lowercase Git commit.");
|
|
24
|
+
return {
|
|
25
|
+
schemaVersion: 1, contractVersion: 1,
|
|
26
|
+
source: { repository, commit, export: "@hraness/design-kit/paper-theme.css" },
|
|
27
|
+
files: {
|
|
28
|
+
"paper-theme.css": { path: "src/paper-theme.css", sha256: digest(css) },
|
|
29
|
+
LICENSE: { path: "LICENSE", sha256: digest(license) },
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function parsePaperThemeSnapshot(value: unknown): PaperThemeSnapshot {
|
|
35
|
+
if (!isRecord(value) || value.schemaVersion !== 1 || value.contractVersion !== 1
|
|
36
|
+
|| !isRecord(value.source) || value.source.repository !== repository
|
|
37
|
+
|| value.source.export !== "@hraness/design-kit/paper-theme.css"
|
|
38
|
+
|| typeof value.source.commit !== "string" || !/^[a-f0-9]{40}$/u.test(value.source.commit)
|
|
39
|
+
|| !isRecord(value.files) || Object.keys(value.files).sort().join(",") !== "LICENSE,paper-theme.css") {
|
|
40
|
+
throw new Error("Invalid Paper theme provenance manifest.");
|
|
41
|
+
}
|
|
42
|
+
for (const name of artifacts) {
|
|
43
|
+
const artifact = value.files[name];
|
|
44
|
+
const path = name === "LICENSE" ? "LICENSE" : "src/paper-theme.css";
|
|
45
|
+
if (!isRecord(artifact) || artifact.path !== path || typeof artifact.sha256 !== "string" || !/^[a-f0-9]{64}$/u.test(artifact.sha256)) {
|
|
46
|
+
throw new Error(`Invalid ${name} provenance.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return value as unknown as PaperThemeSnapshot;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function checkPaperThemeSnapshot(directory: string): Promise<PaperThemeSnapshot> {
|
|
53
|
+
const value: unknown = JSON.parse(await readFile(resolve(directory, "provenance.json"), "utf8"));
|
|
54
|
+
const manifest = parsePaperThemeSnapshot(value);
|
|
55
|
+
for (const name of artifacts) {
|
|
56
|
+
if (digest(await readFile(resolve(directory, name), "utf8")) !== manifest.files[name].sha256) {
|
|
57
|
+
throw new Error(`${name} differs from its immutable snapshot; keep local adaptations in a separate stylesheet.`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return manifest;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function writePaperThemeSnapshot(directory: string, commit: string, sourceRoot = resolve(import.meta.dir, "..")): Promise<void> {
|
|
64
|
+
if (!/^[a-f0-9]{40}$/u.test(commit)) throw new Error("Snapshot source must be a full lowercase Git commit.");
|
|
65
|
+
const committed = (path: string) => execFileSync("git", ["show", `${commit}:${path}`], { cwd: sourceRoot, encoding: "utf8", maxBuffer: 1024 * 1024 });
|
|
66
|
+
const css = committed("src/paper-theme.css");
|
|
67
|
+
const license = committed("LICENSE");
|
|
68
|
+
const manifest = createPaperThemeSnapshot(commit, css, license);
|
|
69
|
+
// Existing snapshots must still be intact before an intentional upgrade.
|
|
70
|
+
try {
|
|
71
|
+
await readFile(resolve(directory, "provenance.json"));
|
|
72
|
+
await checkPaperThemeSnapshot(directory);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (!(isRecord(error) && error.code === "ENOENT")) throw error;
|
|
75
|
+
for (const name of artifacts) {
|
|
76
|
+
try { await readFile(resolve(directory, name)); }
|
|
77
|
+
catch (missing) { if (isRecord(missing) && missing.code === "ENOENT") continue; throw missing; }
|
|
78
|
+
throw new Error(`Refusing to overwrite ${name} without snapshot provenance.`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
await mkdir(directory, { recursive: true });
|
|
82
|
+
await writeFile(resolve(directory, "paper-theme.css"), css);
|
|
83
|
+
await writeFile(resolve(directory, "LICENSE"), license);
|
|
84
|
+
await writeFile(resolve(directory, "provenance.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
85
|
+
await checkPaperThemeSnapshot(directory);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (import.meta.main) {
|
|
89
|
+
const [operation, directory, flag, commit, ...extra] = process.argv.slice(2);
|
|
90
|
+
if (operation === "--check" && directory !== undefined && flag === undefined) {
|
|
91
|
+
const result = await checkPaperThemeSnapshot(resolve(directory));
|
|
92
|
+
console.log(`Paper theme snapshot verified at ${result.source.commit}.`);
|
|
93
|
+
} else if (operation === "--write" && directory !== undefined && flag === "--source-commit" && commit !== undefined && extra.length === 0) {
|
|
94
|
+
await writePaperThemeSnapshot(resolve(directory), commit);
|
|
95
|
+
console.log(`Paper theme snapshot installed from ${commit}.`);
|
|
96
|
+
} else throw new Error("Usage: bun scripts/paper-theme-snapshot.ts --check DIRECTORY | --write DIRECTORY --source-commit FULL_COMMIT");
|
|
97
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { lstat, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
const repository = "https://github.com/hraness/design-kit";
|
|
7
|
+
export const marketingSnapshotPaths = {
|
|
8
|
+
"product-marketing-preset.css": "src/product-marketing-preset.css",
|
|
9
|
+
"fonts/instrument-serif/instrument-serif-latin-400.woff2": "src/fonts/instrument-serif/instrument-serif-latin-400.woff2",
|
|
10
|
+
"fonts/instrument-serif/OFL.txt": "src/fonts/instrument-serif/OFL.txt",
|
|
11
|
+
"fonts/instrument-serif/UPSTREAM.md": "src/fonts/instrument-serif/UPSTREAM.md",
|
|
12
|
+
"marketing-assets/grain.svg": "src/marketing-assets/grain.svg",
|
|
13
|
+
"marketing-assets/cells.svg": "src/marketing-assets/cells.svg",
|
|
14
|
+
"marketing-assets/UPSTREAM.md": "src/marketing-assets/UPSTREAM.md",
|
|
15
|
+
"check.mjs": "scripts/check-marketing-snapshot.mjs",
|
|
16
|
+
"check.d.mts": "scripts/check-marketing-snapshot.d.mts",
|
|
17
|
+
LICENSE: "LICENSE",
|
|
18
|
+
} as const;
|
|
19
|
+
type Artifact = keyof typeof marketingSnapshotPaths;
|
|
20
|
+
const artifacts = Object.keys(marketingSnapshotPaths) as Artifact[];
|
|
21
|
+
const digest = (value: Uint8Array) => createHash("sha256").update(value).digest("hex");
|
|
22
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
|
+
const missing = (error: unknown) => isRecord(error) && error.code === "ENOENT";
|
|
24
|
+
|
|
25
|
+
export interface MarketingSnapshot {
|
|
26
|
+
readonly schemaVersion: 1;
|
|
27
|
+
readonly contractVersion: 1;
|
|
28
|
+
readonly source: { readonly repository: typeof repository; readonly commit: string; readonly export: "@hraness/design-kit/product-marketing-preset.css" };
|
|
29
|
+
readonly files: Readonly<Record<Exclude<Artifact, "check.mjs" | "check.d.mts">, { readonly path: string; readonly sha256: string }> & Partial<Record<"check.mjs" | "check.d.mts", { readonly path: string; readonly sha256: string }>>>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createMarketingSnapshot(commit: string, files: Readonly<Record<Artifact, Uint8Array>>): MarketingSnapshot {
|
|
33
|
+
if (!/^[a-f0-9]{40}$/u.test(commit)) throw new Error("Snapshot source must be a full lowercase Git commit.");
|
|
34
|
+
return {
|
|
35
|
+
schemaVersion: 1, contractVersion: 1,
|
|
36
|
+
source: { repository, commit, export: "@hraness/design-kit/product-marketing-preset.css" },
|
|
37
|
+
files: Object.fromEntries(artifacts.map((name) => [name, { path: marketingSnapshotPaths[name], sha256: digest(files[name]) }])) as MarketingSnapshot["files"],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function parseMarketingSnapshot(value: unknown, allowPreviousInventory = false): MarketingSnapshot {
|
|
42
|
+
const expected = allowPreviousInventory && isRecord(value) && isRecord(value.files) && !("check.mjs" in value.files) ? artifacts.filter((name) => name !== "check.mjs" && name !== "check.d.mts") : artifacts;
|
|
43
|
+
if (!isRecord(value) || value.schemaVersion !== 1 || value.contractVersion !== 1
|
|
44
|
+
|| !isRecord(value.source) || value.source.repository !== repository
|
|
45
|
+
|| value.source.export !== "@hraness/design-kit/product-marketing-preset.css"
|
|
46
|
+
|| typeof value.source.commit !== "string" || !/^[a-f0-9]{40}$/u.test(value.source.commit)
|
|
47
|
+
|| !isRecord(value.files) || Object.keys(value.files).sort().join("\n") !== [...expected].sort().join("\n")) {
|
|
48
|
+
throw new Error("Invalid marketing snapshot provenance.");
|
|
49
|
+
}
|
|
50
|
+
for (const name of expected) {
|
|
51
|
+
const file = value.files[name];
|
|
52
|
+
if (!isRecord(file) || file.path !== marketingSnapshotPaths[name] || typeof file.sha256 !== "string" || !/^[a-f0-9]{64}$/u.test(file.sha256)) {
|
|
53
|
+
throw new Error(`Invalid ${name} provenance.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return value as unknown as MarketingSnapshot;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function inventory(directory: string, prefix = ""): Promise<string[]> {
|
|
60
|
+
const details = await lstat(directory);
|
|
61
|
+
if (!details.isDirectory() || details.isSymbolicLink()) throw new Error("Snapshot directories must be physical directories.");
|
|
62
|
+
const files: string[] = [];
|
|
63
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
64
|
+
const path = prefix + entry.name;
|
|
65
|
+
if (entry.isSymbolicLink()) throw new Error(`Snapshot contains a symbolic link: ${path}`);
|
|
66
|
+
if (entry.isDirectory()) {
|
|
67
|
+
if (!artifacts.some((name) => name.startsWith(`${path}/`))) throw new Error(`Snapshot contains an unowned directory: ${path}`);
|
|
68
|
+
files.push(...await inventory(join(directory, entry.name), `${path}/`));
|
|
69
|
+
}
|
|
70
|
+
else if (entry.isFile()) files.push(path);
|
|
71
|
+
else throw new Error(`Snapshot contains a non-file: ${path}`);
|
|
72
|
+
}
|
|
73
|
+
return files.sort();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function checkMarketingSnapshot(directory: string, allowPreviousInventory = false): Promise<MarketingSnapshot> {
|
|
77
|
+
const actual = await inventory(directory);
|
|
78
|
+
const value: unknown = JSON.parse(await readFile(join(directory, "provenance.json"), "utf8"));
|
|
79
|
+
const manifest = parseMarketingSnapshot(value, allowPreviousInventory);
|
|
80
|
+
if (actual.join("\n") !== [...Object.keys(manifest.files), "provenance.json"].sort().join("\n")) throw new Error("Snapshot has missing or unowned files.");
|
|
81
|
+
for (const [name, receipt] of Object.entries(manifest.files)) {
|
|
82
|
+
if (receipt === undefined) throw new Error(`Missing ${name} provenance.`);
|
|
83
|
+
if (digest(await readFile(join(directory, name))) !== receipt.sha256) throw new Error(`${name} differs from its immutable snapshot.`);
|
|
84
|
+
}
|
|
85
|
+
return manifest;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function writeMarketingSnapshot(directory: string, commit: string, sourceRoot = resolve(import.meta.dir, "..")): Promise<void> {
|
|
89
|
+
if (!/^[a-f0-9]{40}$/u.test(commit)) throw new Error("Snapshot source must be a full lowercase Git commit.");
|
|
90
|
+
const objectType = execFileSync("git", ["cat-file", "-t", commit], {
|
|
91
|
+
cwd: sourceRoot, encoding: "utf8", maxBuffer: 1024, timeout: 30_000,
|
|
92
|
+
}).trim();
|
|
93
|
+
if (objectType !== "commit") throw new Error("Snapshot source must name a commit object, not a tree or tag object.");
|
|
94
|
+
directory = resolve(directory);
|
|
95
|
+
// Read immutable bytes before touching the destination. Never copy a dirty tree.
|
|
96
|
+
const files = Object.fromEntries(artifacts.map((name) => [name, execFileSync("git", ["show", `${commit}:${marketingSnapshotPaths[name]}`], {
|
|
97
|
+
cwd: sourceRoot, maxBuffer: 2 * 1024 * 1024, timeout: 30_000,
|
|
98
|
+
})])) as Record<Artifact, Buffer>;
|
|
99
|
+
const manifest = createMarketingSnapshot(commit, files);
|
|
100
|
+
let previous: MarketingSnapshot | undefined;
|
|
101
|
+
let existed = false;
|
|
102
|
+
try {
|
|
103
|
+
const present = await inventory(directory);
|
|
104
|
+
existed = true;
|
|
105
|
+
if (present.length > 0) previous = await checkMarketingSnapshot(directory, true);
|
|
106
|
+
} catch (error) { if (!missing(error)) throw error; }
|
|
107
|
+
await mkdir(dirname(directory), { recursive: true });
|
|
108
|
+
const stage = `${directory}.stage-${randomUUID()}`;
|
|
109
|
+
const backup = `${directory}.previous-${randomUUID()}`;
|
|
110
|
+
let backedUp = false;
|
|
111
|
+
try {
|
|
112
|
+
await mkdir(stage);
|
|
113
|
+
for (const name of artifacts) {
|
|
114
|
+
await mkdir(dirname(join(stage, name)), { recursive: true });
|
|
115
|
+
await writeFile(join(stage, name), files[name]);
|
|
116
|
+
}
|
|
117
|
+
await writeFile(join(stage, "provenance.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
118
|
+
await checkMarketingSnapshot(stage);
|
|
119
|
+
if (existed) {
|
|
120
|
+
if (previous === undefined) {
|
|
121
|
+
if ((await inventory(directory)).length !== 0) throw new Error("Unowned files appeared in the snapshot destination.");
|
|
122
|
+
} else if (JSON.stringify(await checkMarketingSnapshot(directory, true)) !== JSON.stringify(previous)) throw new Error("Snapshot changed during installation.");
|
|
123
|
+
await rename(directory, backup);
|
|
124
|
+
backedUp = true;
|
|
125
|
+
// Check again after moving the exact directory before replacing it.
|
|
126
|
+
if (previous === undefined ? (await inventory(backup)).length !== 0 : JSON.stringify(await checkMarketingSnapshot(backup, true)) !== JSON.stringify(previous)) throw new Error("Snapshot changed during installation.");
|
|
127
|
+
}
|
|
128
|
+
await rename(stage, directory);
|
|
129
|
+
await checkMarketingSnapshot(directory);
|
|
130
|
+
if (backedUp) await rm(backup, { recursive: true });
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (backedUp) {
|
|
133
|
+
try { await lstat(directory); } catch (absent) { if (missing(absent)) await rename(backup, directory); }
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
} finally {
|
|
137
|
+
await rm(stage, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (import.meta.main) {
|
|
142
|
+
const [operation, directory, flag, commit, ...extra] = process.argv.slice(2);
|
|
143
|
+
if (operation === "--check" && directory !== undefined && flag === undefined) {
|
|
144
|
+
console.log(`Marketing snapshot verified at ${(await checkMarketingSnapshot(resolve(directory))).source.commit}.`);
|
|
145
|
+
} else if (operation === "--write" && directory !== undefined && flag === "--source-commit" && commit !== undefined && extra.length === 0) {
|
|
146
|
+
await writeMarketingSnapshot(resolve(directory), commit);
|
|
147
|
+
console.log(`Marketing snapshot installed from ${commit}.`);
|
|
148
|
+
} else throw new Error("Usage: bun scripts/product-marketing-snapshot.ts --check DIRECTORY | --write DIRECTORY --source-commit FULL_COMMIT");
|
|
149
|
+
}
|