@pixelated-tech/components 3.9.15 → 3.9.17
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/components/config/config.js +35 -1
- package/dist/components/general/tiles.css +11 -1
- package/dist/components/general/tiles.js +5 -2
- package/dist/components/general/utilities.js +0 -8
- package/dist/config/pixelated.config.json.enc +1 -1
- package/dist/scripts/config-vault.js +134 -23
- package/dist/scripts/config-vault.ts +136 -27
- package/dist/scripts/create-pixelated-app.js +338 -38
- package/dist/scripts/pixelated-eslint-plugin.js +52 -3
- package/dist/scripts/release.sh +49 -5
- package/dist/scripts/setup-remotes.sh +35 -10
- package/dist/scripts/validate-exports.js +1 -1
- package/dist/types/components/config/config.d.ts.map +1 -1
- package/dist/types/components/general/tiles.d.ts +2 -0
- package/dist/types/components/general/tiles.d.ts.map +1 -1
- package/dist/types/components/general/utilities.d.ts +0 -6
- package/dist/types/components/general/utilities.d.ts.map +1 -1
- package/dist/types/scripts/create-pixelated-app.d.ts +8 -0
- package/dist/types/scripts/create-pixelated-app.d.ts.map +1 -1
- package/dist/types/scripts/pixelated-eslint-plugin.d.ts +24 -0
- package/dist/types/scripts/pixelated-eslint-plugin.d.ts.map +1 -1
- package/dist/types/tests/config-vault.test.d.ts.map +1 -1
- package/dist/types/tests/create-pixelated-app.github.test.d.ts +2 -0
- package/dist/types/tests/create-pixelated-app.github.test.d.ts.map +1 -0
- package/package.json +11 -8
|
@@ -22,6 +22,7 @@ export function getFullPixelatedConfig() {
|
|
|
22
22
|
// If this library is installed as a package, check its dist/config as a fallback
|
|
23
23
|
path.join(process.cwd(), 'node_modules', '@pixelated-tech', 'components', 'dist', 'config', filename),
|
|
24
24
|
];
|
|
25
|
+
// First, look for plaintext config files
|
|
25
26
|
for (const configPath of paths) {
|
|
26
27
|
if (fs.existsSync(configPath)) {
|
|
27
28
|
try {
|
|
@@ -34,13 +35,46 @@ export function getFullPixelatedConfig() {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
}
|
|
38
|
+
// If not found, look for encrypted variants in the same locations (e.g., pixelated.config.json.enc)
|
|
39
|
+
const doIt = false;
|
|
40
|
+
if (!raw && doIt) {
|
|
41
|
+
for (const configPath of paths) {
|
|
42
|
+
const encPath = `${configPath}.enc`;
|
|
43
|
+
if (fs.existsSync(encPath)) {
|
|
44
|
+
try {
|
|
45
|
+
raw = fs.readFileSync(encPath, 'utf8');
|
|
46
|
+
source = encPath;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
console.error(`Failed to read encrypted config file at ${encPath}`, err);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
37
55
|
if (!raw) {
|
|
38
56
|
console.error('pixelated.config.json not found. Searched in src/app/config/, src/config/, and root.');
|
|
39
57
|
return {};
|
|
40
58
|
}
|
|
41
59
|
// Handle decryption if the content is encrypted
|
|
42
60
|
if (isEncrypted(raw)) {
|
|
43
|
-
|
|
61
|
+
// Allow key to come from env or a local .env.local fallback (useful for local/CI debugging)
|
|
62
|
+
let key = process.env.PIXELATED_CONFIG_KEY;
|
|
63
|
+
if (!key) {
|
|
64
|
+
const envPath = path.join(process.cwd(), '.env.local');
|
|
65
|
+
if (fs.existsSync(envPath)) {
|
|
66
|
+
try {
|
|
67
|
+
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
68
|
+
const match = envContent.match(/^PIXELATED_CONFIG_KEY=(.*)$/m);
|
|
69
|
+
if (match && match[1]) {
|
|
70
|
+
key = match[1].trim();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
// ignore
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
44
78
|
if (!key) {
|
|
45
79
|
console.error('PIXELATED_CONFIG is encrypted but PIXELATED_CONFIG_KEY is not set in the environment.');
|
|
46
80
|
return {};
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
margin: 10px;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
.tile-image.clickable {
|
|
23
|
+
cursor: pointer;
|
|
24
|
+
}
|
|
25
|
+
|
|
22
26
|
.tile-image img {
|
|
23
27
|
width: 100%;
|
|
24
28
|
height: 100% !important;
|
|
@@ -44,6 +48,12 @@
|
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
.tile-image-overlay,
|
|
52
|
+
.tile-image-overlay * {
|
|
53
|
+
pointer-events: none;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
}
|
|
56
|
+
|
|
47
57
|
.tile-image:hover .tile-image-overlay {
|
|
48
58
|
visibility: visible;
|
|
49
59
|
width: 100% !important;
|
|
@@ -67,7 +77,7 @@
|
|
|
67
77
|
transition-delay: 0.25s;
|
|
68
78
|
}
|
|
69
79
|
|
|
70
|
-
.tile-image-overlay-title{
|
|
80
|
+
.tile-image-overlay-title {
|
|
71
81
|
margin-bottom: 10px;
|
|
72
82
|
}
|
|
73
83
|
|
|
@@ -9,11 +9,12 @@ import "./tiles.css";
|
|
|
9
9
|
Tiles.propTypes = {
|
|
10
10
|
cards: PropTypes.array.isRequired,
|
|
11
11
|
rowCount: PropTypes.number,
|
|
12
|
+
imgClick: PropTypes.func,
|
|
12
13
|
};
|
|
13
14
|
export function Tiles(props) {
|
|
14
15
|
const rowCount = props.rowCount ?? 2;
|
|
15
16
|
if (props.cards && props.cards.length > 0) {
|
|
16
|
-
return (_jsx("div", { className: "tiles-container", children: _jsx("div", { className: `tile-container row-${rowCount}col`, children: props.cards.map((card, i) => (_jsx("div", { className: "gridItem", children: _jsx(Tile, { index: i, cardLength: props.cards.length, link: card.link, image: card.image, imageAlt: card.imageAlt, bodyText: card.bodyText }) }, i))) }) }));
|
|
17
|
+
return (_jsx("div", { className: "tiles-container", children: _jsx("div", { className: `tile-container row-${rowCount}col`, children: props.cards.map((card, i) => (_jsx("div", { className: "gridItem", children: _jsx(Tile, { index: i, cardLength: props.cards.length, link: card.link, image: card.image, imageAlt: card.imageAlt, bodyText: card.bodyText, imgClick: props.imgClick }) }, i))) }) }));
|
|
17
18
|
}
|
|
18
19
|
else {
|
|
19
20
|
return (_jsx(Loading, {}));
|
|
@@ -27,10 +28,12 @@ Tile.propTypes = {
|
|
|
27
28
|
image: PropTypes.string.isRequired,
|
|
28
29
|
imageAlt: PropTypes.string,
|
|
29
30
|
bodyText: PropTypes.string,
|
|
31
|
+
imgClick: PropTypes.func,
|
|
30
32
|
};
|
|
31
33
|
function Tile(props) {
|
|
32
34
|
const config = usePixelatedConfig();
|
|
33
|
-
const
|
|
35
|
+
const imgClick = props.imgClick;
|
|
36
|
+
const tileBody = _jsxs("div", { className: "tile-image" + (imgClick ? " clickable" : ""), children: [_jsx(SmartImage, { src: props.image, title: props?.imageAlt ?? undefined, alt: props?.imageAlt ?? "", onClick: imgClick ? (event) => imgClick(event, props.image) : undefined, cloudinaryEnv: config?.cloudinary?.product_env ?? undefined }), _jsx("div", { className: "tile-image-overlay", children: _jsxs("div", { className: "tile-image-overlay-text", children: [_jsx("div", { className: "tile-image-overlay-title", children: props.imageAlt }), _jsx("div", { className: "tile-image-overlay-body", children: props.bodyText })] }) })] });
|
|
34
37
|
return (_jsx("div", { className: "tile", id: 'tile-' + props.index, children: props.link ?
|
|
35
38
|
_jsx("a", { href: props.link, className: "tileLink", children: tileBody })
|
|
36
39
|
:
|
|
@@ -151,14 +151,6 @@ export const CLIENT_ONLY_PATTERNS = [
|
|
|
151
151
|
/\bwindow\./,
|
|
152
152
|
/["']use client["']/ // Client directive
|
|
153
153
|
];
|
|
154
|
-
/**
|
|
155
|
-
* Determines if a component file contains client-only code that requires browser execution
|
|
156
|
-
* @param fileContent - The source code content of the file
|
|
157
|
-
* @returns true if the file contains client-only patterns
|
|
158
|
-
*/
|
|
159
|
-
export function isClientComponent(fileContent) {
|
|
160
|
-
return CLIENT_ONLY_PATTERNS.some(pattern => pattern.test(fileContent));
|
|
161
|
-
}
|
|
162
154
|
/* ===== COMPONENT FILE DETECTION ===== */
|
|
163
155
|
/**
|
|
164
156
|
* Glob patterns for finding component files
|
|
@@ -1 +1 @@
|
|
|
1
|
-
pxl:v1:
|
|
1
|
+
pxl:v1:c70cbe09dc837373c6f97231:63d645cc86fe3d8ab9ed584e12567337:de5401e0f619e7bc50e6333ad1dce44aa01813cf21d55ad9a607b7492a46295e65915a988c3809c1cef0048c3a6a08d6ac73e35b951bfd442a4d8a0db29f0e004d244f7a01dfce7e92fc3b099f5d9f16cb1783ce383d9b4e20b4ec1dfe28761ccc1dde660d0e53be13c7adc4421b9ba775be16ad44368d4f45f821e9b03823c41a49434823e33a0b61e2886b85fa68f73bf2f1789e8096796d32438a116051d99e121795aae13365f27a68ea07f4261bb63ba5e3ce52033efc2a64d2681962bbe5ae76e5f0b1c265815ef651557727ec362c9fd0f92eeda070c1468578e0b30c23d764dd9ca0be56ffc28d61834702aa601dfb5df093d25f71e4255c9f34cae0f8a09512220468642249e477d53bfa1cd22b2c5b83fc5f995812bdf5f18056e87551e6f29f72a8c37fe119f3cf895cd88f4eda02c509a1ee469c48043a00f01e69320454262165aeb499c981cf765ecebc40a5a0b99d9dbd991e11fbc1a1c4140e1fa21652edeaaa569e064bfe063e72521b4bb43d6603be1f1ead52536a52beeac985d91ab3b6e20b1d0e0bfefbe1a56340801736cf3a82fe6dc7b663d9cd70d68669894cc79ff0b992809a0910f7f2d42abfb86b64e4cf2e126a4ebab8976308ff61ea414cc33153c5feaf40f68cb7b61952f048761d75878cb57aeaf3a4ef04e754c560e268192749c6dc07b4488caafd5e1dcd09ac94523f7e9a0242a4ae85de3d045d98ba5a96af9d5068a8054728cd99f37cda73ccf8b46a5cb3978444e1e7d2a5b9a2ba90bb855c7e0ab9f13d78e9b234777109061565185775d369037c84505ab5bd224e91eea59ad32b64fd56d5451c9ac490c6cc479f4e6d76a02484a1aa9735979134a3b8b966817cd518e1d930d668f0f7fdfdda39e577bdb9a8332fd9dc1c60de418e51842965ab88d48469e6122c96e46a4302916a59cfae10957c91a7ac45f059fdb9429b993ac1705ab1a6fc43218f46c1b968fd00e41e11e7e2e4b7b69df78156cad91d30e7ab0d332fd99d1a6883d6a83d0cd713fa39cac6483ffef4f8603b8f9522d4f06968dd04eda944b152e056b173bed76609d1a9894bf6e35c1368f17e76ecc794792ed4b4444b924b6867599da45b208fe50ef540f99ac1fbe22b8ac4ceaee2ad069e96726c4740bb15fd12d50b9718cfed9a6eefc5db4bf86c78ab847d00043033f9991dc978ca5c5f1555e77ef04ff00d83da505bc8141ae1539b0e1dd3f42730fee6ba5d6fc02788df8a0489ba4f1b2542f6fd87b76c2bbb93330c165230d7c767bd51387ec8730bae8d435187da55cfb1deb10be60a80be7d29aa27772b2ecbb4340c0901574c8fee44d2c23b4c9d5f012671a260ad62dde2f1f95d6d6892c90a5fd57b20f3c36e10ad28083981685aea41774f828f5b379a0ae5041ea0d29aebf09e62f0ffaa789b3e6616270a3f6e74a5edd7ed18716f157af8af96c959021ba8553be53db638e3663c21a40d4932702163dbfd5834fb4a7f113bdc4d29346b7238e4461324ed2b614ee4a6e434d1207a824864aea955b24090f8e82320b395dc724defa313b3711ca8dedb1ede348367c8879a5c46aaa90bb57581b7643012fa19ca0c6d8956d4c8b8b50b1a077d4c3e3f5ee802084939b5c7a0c9113bcc06d787411d0c3e4a9f8177f0ec89bba4d0d7ff0a30751daadf6e762ac910f0298e4d3ca0d278f27dfaf16ec0d13533aeab259aa3f896f5018b4fd6b7a8b8b4002c3008e4cf8fb7f550e228579cb459c167095da7eb1930a1504325ad30b08de63df8854f7b47becf4e82d19629ba5ee2ae0a43d74f4af216b38db702b4d7325abc47dc9e9ead359f5f892dee204ec12a4dba509c2fe4892d2fe6fcac1911116fd01a5901af1c60b107ae53a588fb2bfadfa88fdcf2b7ddbd14c4d658ad2b580004cf888379c3339a95e93b7e03c41068b6b2c27c8bc29cf8c075f794f33ee5cff0ad08320c06c28b43308015c3ec4084062714aee486db9e0aa492637917f8a68d544515ec38546f9535f24d4fe44190a4cd7b75a8986f789fa083975f031339af222e6ad84da67d633c4afc7bfcc88c01ba16dc3d9f81952ebd4f2e940d427213fbe7628d1fb53c280c14599cfc6fa500e67abd3a989a275c605c0f2993d15a21a887df3e1d508cc4e5089eb3f5ab9f8a0f8e6b04f9f630025adb8986367f14a2523ddd12e46a9baf761a161dd1e28037d701e7f452784ceaf6e804d021ff29ab1043c6d088fc2942c8fa8c66bcc0c76ba52c4ec527d3327f598992a30390d3dcefc64d626b4a60c5320a7e02f10e6d55a685153cf677d519e2c521c36e35a5f64dc3a4bd05fc5a4201e2f04354646b8e539723eaaf112899f0d9b34a1b3370e6e65a41b474503d85f1052064430939732e7b1212df608011d9e220965ab48c7c4925f81437d7d3c93995f0d6f9f92ec74d4faf4ffdf3a25c8eb4df5140e1afa3f5409edf52eb2a2c14e0b65d9d0ccf9f3c435fcd51676d0db022705a517a732634b2869e01ad3a2669aa84c9ced95dbba2203bc0134944b568a0d9dce086b7dfbd11b4066e2572d1d4c00854cd476fcb27a7b8dd262ba9da2de4da1db90f47ad3053967e24de047fde2e7dbd8933176cf47b2cd51297d5aafbbfd801b71df15768d159a69556acf580f012b6230891b6b5fd61c9c80ef7f7a76a12b4018cad6e2d419b072e9622811253cfa73cc780a310cedd439797bf2aceb7d9855c8ad06e6e2b7daacc0c93d89cdc265b27d8d46d1101ba1518bfad0d5dc869b0c1d8879ee8ba85ff41aae3adb7a3a4c85b939b15591b022ffa43b89ec305668f755a230e59362007a035421f4ddd0b1ee4a4d4f18db66d758a9bfb8d26610f0e340153d551dfc11c48ae816de02e2aaae4d4b2b6a2aaa2c5c11d4d2ef5410bd0b230c8e9d0bacce1906fffdb0e28d5ccdfb54682845bbe59906d3e50b896f888a5eb91120c1192046228a27e9e8a13d525c7d1df963d947a71bb95cd07a4526bf61c8b3986e8963871e94bc8e7db723cc9646fed49b5f823c601aa12dc2c61c2f4035e6a2f68793d31285f1d54b195b8a3126677ebcba699ba055beef1adae9c29213faf38a6e7cd889bb7ff37e6cda3e0ce5bc1274dd2b373da6fff9e9cae18e714b9a43698b1e3ebd31b3a2abd8bdcf500834b5aad1ec8220e1375b558cf53459b4f77d1746abfb6311c9b0f12035642bd307ca926622c07ab5b68487a41cb90f503303b33f828c48dabc4076b0a72e4aee69e367e4a8e145ad5955afa4a901ba32acd46f75b7f7cf9f9e8f0557a5687f8097ca8f0af694cee2f2adff0d31683c0526f99c29e6bf54c62f71f61315b566fd9aa15131722e07a27b014632a82b9f6746ab8c9e2826d37056aff5658303f0c6e5d3a08616a3c49be7ddc1f6b7e0f2bed7a9d17b5fdbc9e99aae9e3ccf683da49b5123ea90cc25887751c2bbc3b931b0fa2891cbd2619aad145898a71105e393284311f39a642570540c7d8936e0284c269b178f1aca07a7d378caeabb7273ac2b114120caf05080d27485f11515313561b4b7cc573da43745b623b56ac4facaa6e970f2f191422ba45d12744754d290034694b2f7eb950c225e0b53907ea68985e3b2c27eaba4ecf774a24aba842826d73c2fc5b1e2671c80a392bbe72c45c0bdca3e62d3a87fddf6684187b577e9d3da21833747e6d25d15b06e035d892acf1242e4a739e844c9b1a62592ba83eaacdcef19cbeda4c9f5fa152a8219f735b8c3d43553b799fbedf1013883f9aee7b3069650f53b117790261d2b48e1fe289061aaca736dbaa8118a836b5b1045bfe4e2707829ee2d9fb978814f79fd2be61f36c3d490e149201731fc3c38685de1620306340a5259d62550560c9e36f1b60c0264b5a64e865f5cf21756fdda21270111d02e13947a651b14056296d268c1b9b89edfc983233541797a70c7831a361b94d830c96f02765ed2bd27cb15c4f06228d723db97c61ef679074960db5bcd533d88485589224412cbd02676d63a70588c4d864c9eb20703d8413749d4ec2195a118524ff63097750c68e991b6a53971ced2ad22d22c13300af131a8133b64c68095e4f4a52689a9368d339e0ec0b769f0a83e7002fa02ad4c954499680fd93c42d0e8aa7864eac7bad8aea8c560815ba620e55f6157b20e937db6d02a6abc3f04588c07e1f02445a0c01230316def1f7f4fb7bedecf8d49034dcb27aeabf3a733e8c02160ef1a91909edc20a49266a38eae88e83f66a6809a0a9edefc8feeba2bced621cbba28a3a400df83f152292e3a9ad0074599a90387fbd67bbe9a9c64611176ee81623283839e3959304b5addca8e5875b32f84a0b446bbbe391c323c41f7aeee5a992419da68f5fd47da1b3a227c8532eebe46fd3491f02ea9e11ab4932a74a0770800301c311afb2af8918511fe6d2051a4af053e34629543e23870b064173b0b8bde9ef6745b62b6f6ff9f6a54d138ff3fcc6fd710d3f136a3780269d2d92babd145319aa64e441c516426640dfde81d70a07f24c8956d512a9db47c1d3d928d56bf183206b8f67f2fb91dba8bab52f23e477db35d633e5a63e22daa78b33d28dcceb7820bb43ec99cc8a1a30164c5a0a090bc2065df37f184ab3075618917222cc4f2bbf8680e881507124b5f0b7460a1a590ee142d9fd4097cd9bd375cf4f14b9b7b21243456752f100e07d25818e7e7383637fc6c96ecd60c91ac6bd8ad82b3efcf60ad0f874ae681f896ea249478e1fc1eda90417d7352bad6b06a67cfa70a0b2d6f5063da403c182eebd05896e20e4dd13f409f34a6afc8c378bb42966c0ade4368275feb11b642d5038e8cf01a9738bd09f10a49fb4daf9d84bdcf5703bf575125369b15e59a980511187a9a5365fe5aa43fd68a012d673f6899e919d0e7ef5223bd594c1b0035ed1bed609f2e38e46e8311b58b9706c7dbeec8e0d3887e6f5dfb611df3ee8a67a122cda70d8dbf5dcf299048c85408d32765c0fb9fd2febb8c1d067f6d2fe8d82ce499197943f22d375f8cc4c5546a23136b08bc90b288abea36e22b87e8bd37bf3aa9dbeb1f643da7a42fb01a245a892db4ae375365d3eda2a0ffe83fe953d0cdfbfd46a6869dd13b5953d4343c452f0f66b2825cb6a819e586fd53ea7c3b8dc7a8bf2e09c28747e7e9883be9a50ae
|
|
@@ -13,30 +13,22 @@ import { encrypt, decrypt, isEncrypted } from '../components/config/crypto';
|
|
|
13
13
|
* - `decrypt` writes atomically to the plain filename (when given a `.enc` file it writes to the base name)
|
|
14
14
|
*/
|
|
15
15
|
const [, , command, targetPath, argKey] = process.argv;
|
|
16
|
+
// Helper: obtain key from arg or env or .env.local (only used for encrypt/decrypt)
|
|
16
17
|
let key = argKey || process.env.PIXELATED_CONFIG_KEY;
|
|
17
|
-
// If key is still missing, try to load it from .env.local
|
|
18
18
|
if (!key) {
|
|
19
19
|
const envPath = path.join(process.cwd(), '.env.local');
|
|
20
20
|
if (fs.existsSync(envPath)) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
try {
|
|
22
|
+
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
23
|
+
const match = envContent.match(/^PIXELATED_CONFIG_KEY=(.*)$/m);
|
|
24
|
+
if (match && match[1])
|
|
25
|
+
key = match[1].trim();
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
// ignore
|
|
25
29
|
}
|
|
26
30
|
}
|
|
27
31
|
}
|
|
28
|
-
if (!command || !targetPath || !key) {
|
|
29
|
-
console.log('Usage:');
|
|
30
|
-
console.log(' encrypt <filePath> [key] - Encrypts the file and writes `<filePath>.enc`');
|
|
31
|
-
console.log(' decrypt <filePath> [key] - Decrypts the file and writes the plaintext file (atomic write)');
|
|
32
|
-
console.log('\nNote: Key can be passed as argument or via PIXELATED_CONFIG_KEY env var.');
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
35
|
-
const fullPath = path.isAbsolute(targetPath) ? targetPath : path.resolve(process.cwd(), targetPath);
|
|
36
|
-
if (!fs.existsSync(fullPath)) {
|
|
37
|
-
console.error(`File not found: ${fullPath}`);
|
|
38
|
-
process.exit(1);
|
|
39
|
-
}
|
|
40
32
|
const atomicWrite = (destPath, data) => {
|
|
41
33
|
const dir = path.dirname(destPath);
|
|
42
34
|
const base = path.basename(destPath);
|
|
@@ -44,29 +36,148 @@ const atomicWrite = (destPath, data) => {
|
|
|
44
36
|
fs.writeFileSync(tmp, data, 'utf8');
|
|
45
37
|
fs.renameSync(tmp, destPath);
|
|
46
38
|
};
|
|
39
|
+
// Helper: print usage/help
|
|
40
|
+
function printUsage() {
|
|
41
|
+
console.log('Usage:');
|
|
42
|
+
console.log(' npx tsx src/scripts/config-vault.ts encrypt <filePath> [key] - Encrypts <filePath> (writes <filePath>.enc)');
|
|
43
|
+
console.log(' npx tsx src/scripts/config-vault.ts decrypt <filePath> [key] - Decrypts <filePath>.enc and writes plaintext');
|
|
44
|
+
console.log(' npx tsx src/scripts/config-vault.ts postbuild - CI helper: decrypts and injects into .next/server');
|
|
45
|
+
console.log('\nNotes:');
|
|
46
|
+
console.log(' - Key can be passed as argument or via PIXELATED_CONFIG_KEY env var.');
|
|
47
|
+
console.log(' - Use PIXELATED_CONFIG_DEBUG=1 for verbose output during postbuild.');
|
|
48
|
+
}
|
|
49
|
+
// Helpful messages when arguments are missing
|
|
50
|
+
if (!command) {
|
|
51
|
+
console.log('No command provided.');
|
|
52
|
+
printUsage();
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
if (command === 'help' || command === '--help' || command === '-h') {
|
|
56
|
+
printUsage();
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
// If encrypt/decrypt are requested, ensure targetPath and key exist; log informative messages
|
|
60
|
+
if (command === 'encrypt' || command === 'decrypt') {
|
|
61
|
+
if (!targetPath) {
|
|
62
|
+
console.log('No target path provided for encrypt/decrypt.');
|
|
63
|
+
printUsage();
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
if (!key) {
|
|
67
|
+
// we attempted to resolve key from env/.env.local earlier; if still missing, inform the user
|
|
68
|
+
console.log('No key provided for encrypt/decrypt (argument, PIXELATED_CONFIG_KEY, or .env.local).');
|
|
69
|
+
printUsage();
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Post-build behavior used by CI (Amplify):
|
|
75
|
+
* - Look for an encrypted config file in standard candidate locations
|
|
76
|
+
* - Validate PIXELATED_CONFIG_KEY (from env or .env.local)
|
|
77
|
+
* - Decrypt in-place and copy plaintext to .next/server/pixelated.config.json
|
|
78
|
+
* - Validate JSON and emit a concise success message
|
|
79
|
+
*/
|
|
80
|
+
function decryptPostBuild() {
|
|
81
|
+
const DEBUG = process.env.PIXELATED_CONFIG_DEBUG === '1';
|
|
82
|
+
const candidates = [
|
|
83
|
+
path.join(process.cwd(), 'src/app/config/pixelated.config.json.enc'),
|
|
84
|
+
path.join(process.cwd(), 'src/config/pixelated.config.json.enc'),
|
|
85
|
+
path.join(process.cwd(), 'src/pixelated.config.json.enc'),
|
|
86
|
+
];
|
|
87
|
+
let foundEnc = null;
|
|
88
|
+
for (const p of candidates) {
|
|
89
|
+
if (fs.existsSync(p)) {
|
|
90
|
+
foundEnc = p;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!foundEnc) {
|
|
95
|
+
if (DEBUG)
|
|
96
|
+
console.log('No encrypted config found; nothing to do.');
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
// Resolve key (env preferred, then .env.local)
|
|
100
|
+
let keyLocal = process.env.PIXELATED_CONFIG_KEY;
|
|
101
|
+
if (!keyLocal) {
|
|
102
|
+
const envPath = path.join(process.cwd(), '.env.local');
|
|
103
|
+
if (fs.existsSync(envPath)) {
|
|
104
|
+
try {
|
|
105
|
+
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
106
|
+
const match = envContent.match(/^PIXELATED_CONFIG_KEY=(.*)$/m);
|
|
107
|
+
if (match && match[1])
|
|
108
|
+
keyLocal = match[1].trim();
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
// ignore
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!keyLocal) {
|
|
116
|
+
console.error('PIXELATED_CONFIG_KEY not set; cannot decrypt config.');
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
if (!/^[0-9a-fA-F]{64}$/.test(keyLocal)) {
|
|
120
|
+
console.error('PIXELATED_CONFIG_KEY invalid: must be 64 hex characters.');
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const raw = fs.readFileSync(foundEnc, 'utf8');
|
|
125
|
+
if (!isEncrypted(raw)) {
|
|
126
|
+
console.error('Found file is not in encrypted format.');
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
const decrypted = decrypt(raw, keyLocal);
|
|
130
|
+
const dest = foundEnc.endsWith('.enc') ? foundEnc.slice(0, -4) : `${foundEnc}.plain`;
|
|
131
|
+
atomicWrite(dest, decrypted);
|
|
132
|
+
// Copy to .next/server for SSR to pick up
|
|
133
|
+
const injectPath = path.join(process.cwd(), '.next', 'server', 'pixelated.config.json');
|
|
134
|
+
fs.mkdirSync(path.dirname(injectPath), { recursive: true });
|
|
135
|
+
fs.copyFileSync(dest, injectPath);
|
|
136
|
+
// Validate JSON
|
|
137
|
+
JSON.parse(decrypted);
|
|
138
|
+
console.log('Config injected into .next/server/pixelated.config.json');
|
|
139
|
+
if (DEBUG)
|
|
140
|
+
console.log(`Decrypted ${path.basename(foundEnc)} -> ${injectPath}`);
|
|
141
|
+
process.exit(0);
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
console.error(`Post-build decrypt failed: ${err.message}`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
47
148
|
try {
|
|
48
149
|
if (command === 'encrypt') {
|
|
49
|
-
|
|
150
|
+
if (!key) {
|
|
151
|
+
console.error('Encryption key is required.');
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
const content = fs.readFileSync(targetPath, 'utf8');
|
|
50
155
|
if (isEncrypted(content)) {
|
|
51
156
|
console.log('File is already encrypted. No action taken.');
|
|
52
157
|
process.exit(0);
|
|
53
158
|
}
|
|
54
159
|
const encrypted = encrypt(content, key);
|
|
55
|
-
const encPath =
|
|
160
|
+
const encPath = targetPath.endsWith('.enc') ? targetPath : `${targetPath}.enc`;
|
|
56
161
|
atomicWrite(encPath, encrypted);
|
|
57
162
|
console.log(`Successfully encrypted ${targetPath} -> ${path.basename(encPath)}`);
|
|
58
163
|
}
|
|
59
164
|
else if (command === 'decrypt') {
|
|
60
|
-
|
|
165
|
+
if (!key) {
|
|
166
|
+
console.error('Decryption key is required.');
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
const content = fs.readFileSync(targetPath, 'utf8');
|
|
61
170
|
if (!isEncrypted(content)) {
|
|
62
171
|
console.log('File is not encrypted. No action taken.');
|
|
63
172
|
process.exit(0);
|
|
64
173
|
}
|
|
65
174
|
const decrypted = decrypt(content, key);
|
|
66
|
-
|
|
67
|
-
const destPath = fullPath.endsWith('.enc') ? fullPath.slice(0, -4) : fullPath;
|
|
175
|
+
const destPath = targetPath.endsWith('.enc') ? targetPath.slice(0, -4) : targetPath;
|
|
68
176
|
atomicWrite(destPath, decrypted);
|
|
69
|
-
console.log(`Successfully decrypted ${path.basename(
|
|
177
|
+
console.log(`Successfully decrypted ${path.basename(targetPath)} -> ${path.basename(destPath)}`);
|
|
178
|
+
}
|
|
179
|
+
else if (command === 'postbuild' || command === 'post-build' || command === 'inject') {
|
|
180
|
+
decryptPostBuild();
|
|
70
181
|
}
|
|
71
182
|
else {
|
|
72
183
|
console.error(`Unknown command: ${command}`);
|
|
@@ -15,35 +15,22 @@ import { encrypt, decrypt, isEncrypted } from '../components/config/crypto';
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
const [,, command, targetPath, argKey] = process.argv;
|
|
18
|
-
let key = argKey || process.env.PIXELATED_CONFIG_KEY;
|
|
19
18
|
|
|
20
|
-
//
|
|
19
|
+
// Helper: obtain key from arg or env or .env.local (only used for encrypt/decrypt)
|
|
20
|
+
let key = argKey || process.env.PIXELATED_CONFIG_KEY;
|
|
21
21
|
if (!key) {
|
|
22
22
|
const envPath = path.join(process.cwd(), '.env.local');
|
|
23
23
|
if (fs.existsSync(envPath)) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
key = match[1].trim();
|
|
24
|
+
try {
|
|
25
|
+
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
26
|
+
const match = envContent.match(/^PIXELATED_CONFIG_KEY=(.*)$/m);
|
|
27
|
+
if (match && match[1]) key = match[1].trim();
|
|
28
|
+
} catch (e) {
|
|
29
|
+
// ignore
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
if (!command || !targetPath || !key) {
|
|
33
|
-
console.log('Usage:');
|
|
34
|
-
console.log(' encrypt <filePath> [key] - Encrypts the file and writes `<filePath>.enc`');
|
|
35
|
-
console.log(' decrypt <filePath> [key] - Decrypts the file and writes the plaintext file (atomic write)');
|
|
36
|
-
console.log('\nNote: Key can be passed as argument or via PIXELATED_CONFIG_KEY env var.');
|
|
37
|
-
process.exit(1);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const fullPath = path.isAbsolute(targetPath) ? targetPath : path.resolve(process.cwd(), targetPath);
|
|
41
|
-
|
|
42
|
-
if (!fs.existsSync(fullPath)) {
|
|
43
|
-
console.error(`File not found: ${fullPath}`);
|
|
44
|
-
process.exit(1);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
34
|
const atomicWrite = (destPath: string, data: string) => {
|
|
48
35
|
const dir = path.dirname(destPath);
|
|
49
36
|
const base = path.basename(destPath);
|
|
@@ -52,28 +39,150 @@ const atomicWrite = (destPath: string, data: string) => {
|
|
|
52
39
|
fs.renameSync(tmp, destPath);
|
|
53
40
|
};
|
|
54
41
|
|
|
42
|
+
// Helper: print usage/help
|
|
43
|
+
function printUsage(): void {
|
|
44
|
+
console.log('Usage:');
|
|
45
|
+
console.log(' npx tsx src/scripts/config-vault.ts encrypt <filePath> [key] - Encrypts <filePath> (writes <filePath>.enc)');
|
|
46
|
+
console.log(' npx tsx src/scripts/config-vault.ts decrypt <filePath> [key] - Decrypts <filePath>.enc and writes plaintext');
|
|
47
|
+
console.log(' npx tsx src/scripts/config-vault.ts postbuild - CI helper: decrypts and injects into .next/server');
|
|
48
|
+
console.log('\nNotes:');
|
|
49
|
+
console.log(' - Key can be passed as argument or via PIXELATED_CONFIG_KEY env var.');
|
|
50
|
+
console.log(' - Use PIXELATED_CONFIG_DEBUG=1 for verbose output during postbuild.');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Helpful messages when arguments are missing
|
|
54
|
+
if (!command) {
|
|
55
|
+
console.log('No command provided.');
|
|
56
|
+
printUsage();
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
if (command === 'help' || command === '--help' || command === '-h') {
|
|
60
|
+
printUsage();
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// If encrypt/decrypt are requested, ensure targetPath and key exist; log informative messages
|
|
65
|
+
if (command === 'encrypt' || command === 'decrypt') {
|
|
66
|
+
if (!targetPath) {
|
|
67
|
+
console.log('No target path provided for encrypt/decrypt.');
|
|
68
|
+
printUsage();
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
if (!key) {
|
|
72
|
+
// we attempted to resolve key from env/.env.local earlier; if still missing, inform the user
|
|
73
|
+
console.log('No key provided for encrypt/decrypt (argument, PIXELATED_CONFIG_KEY, or .env.local).');
|
|
74
|
+
printUsage();
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Post-build behavior used by CI (Amplify):
|
|
81
|
+
* - Look for an encrypted config file in standard candidate locations
|
|
82
|
+
* - Validate PIXELATED_CONFIG_KEY (from env or .env.local)
|
|
83
|
+
* - Decrypt in-place and copy plaintext to .next/server/pixelated.config.json
|
|
84
|
+
* - Validate JSON and emit a concise success message
|
|
85
|
+
*/
|
|
86
|
+
function decryptPostBuild(): void {
|
|
87
|
+
const DEBUG = process.env.PIXELATED_CONFIG_DEBUG === '1';
|
|
88
|
+
const candidates = [
|
|
89
|
+
path.join(process.cwd(), 'src/app/config/pixelated.config.json.enc'),
|
|
90
|
+
path.join(process.cwd(), 'src/config/pixelated.config.json.enc'),
|
|
91
|
+
path.join(process.cwd(), 'src/pixelated.config.json.enc'),
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
let foundEnc: string | null = null;
|
|
95
|
+
for (const p of candidates) {
|
|
96
|
+
if (fs.existsSync(p)) {
|
|
97
|
+
foundEnc = p;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!foundEnc) {
|
|
103
|
+
if (DEBUG) console.log('No encrypted config found; nothing to do.');
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Resolve key (env preferred, then .env.local)
|
|
108
|
+
let keyLocal = process.env.PIXELATED_CONFIG_KEY;
|
|
109
|
+
if (!keyLocal) {
|
|
110
|
+
const envPath = path.join(process.cwd(), '.env.local');
|
|
111
|
+
if (fs.existsSync(envPath)) {
|
|
112
|
+
try {
|
|
113
|
+
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
114
|
+
const match = envContent.match(/^PIXELATED_CONFIG_KEY=(.*)$/m);
|
|
115
|
+
if (match && match[1]) keyLocal = match[1].trim();
|
|
116
|
+
} catch (e) {
|
|
117
|
+
// ignore
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!keyLocal) {
|
|
123
|
+
console.error('PIXELATED_CONFIG_KEY not set; cannot decrypt config.');
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
if (!/^[0-9a-fA-F]{64}$/.test(keyLocal)) {
|
|
127
|
+
console.error('PIXELATED_CONFIG_KEY invalid: must be 64 hex characters.');
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const raw = fs.readFileSync(foundEnc, 'utf8');
|
|
133
|
+
if (!isEncrypted(raw)) {
|
|
134
|
+
console.error('Found file is not in encrypted format.');
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
const decrypted = decrypt(raw, keyLocal);
|
|
138
|
+
const dest = foundEnc.endsWith('.enc') ? foundEnc.slice(0, -4) : `${foundEnc}.plain`;
|
|
139
|
+
atomicWrite(dest, decrypted);
|
|
140
|
+
// Copy to .next/server for SSR to pick up
|
|
141
|
+
const injectPath = path.join(process.cwd(), '.next', 'server', 'pixelated.config.json');
|
|
142
|
+
fs.mkdirSync(path.dirname(injectPath), { recursive: true });
|
|
143
|
+
fs.copyFileSync(dest, injectPath);
|
|
144
|
+
// Validate JSON
|
|
145
|
+
JSON.parse(decrypted);
|
|
146
|
+
console.log('Config injected into .next/server/pixelated.config.json');
|
|
147
|
+
if (DEBUG) console.log(`Decrypted ${path.basename(foundEnc)} -> ${injectPath}`);
|
|
148
|
+
process.exit(0);
|
|
149
|
+
} catch (err: any) {
|
|
150
|
+
console.error(`Post-build decrypt failed: ${err.message}`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
55
155
|
try {
|
|
56
156
|
if (command === 'encrypt') {
|
|
57
|
-
|
|
157
|
+
if (!key) {
|
|
158
|
+
console.error('Encryption key is required.');
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
const content = fs.readFileSync(targetPath, 'utf8');
|
|
58
162
|
if (isEncrypted(content)) {
|
|
59
163
|
console.log('File is already encrypted. No action taken.');
|
|
60
164
|
process.exit(0);
|
|
61
165
|
}
|
|
62
166
|
const encrypted = encrypt(content, key);
|
|
63
|
-
const encPath =
|
|
167
|
+
const encPath = targetPath.endsWith('.enc') ? targetPath : `${targetPath}.enc`;
|
|
64
168
|
atomicWrite(encPath, encrypted);
|
|
65
169
|
console.log(`Successfully encrypted ${targetPath} -> ${path.basename(encPath)}`);
|
|
66
170
|
} else if (command === 'decrypt') {
|
|
67
|
-
|
|
171
|
+
if (!key) {
|
|
172
|
+
console.error('Decryption key is required.');
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
const content = fs.readFileSync(targetPath, 'utf8');
|
|
68
176
|
if (!isEncrypted(content)) {
|
|
69
177
|
console.log('File is not encrypted. No action taken.');
|
|
70
178
|
process.exit(0);
|
|
71
179
|
}
|
|
72
180
|
const decrypted = decrypt(content, key);
|
|
73
|
-
|
|
74
|
-
const destPath = fullPath.endsWith('.enc') ? fullPath.slice(0, -4) : fullPath;
|
|
181
|
+
const destPath = targetPath.endsWith('.enc') ? targetPath.slice(0, -4) : targetPath;
|
|
75
182
|
atomicWrite(destPath, decrypted);
|
|
76
|
-
console.log(`Successfully decrypted ${path.basename(
|
|
183
|
+
console.log(`Successfully decrypted ${path.basename(targetPath)} -> ${path.basename(destPath)}`);
|
|
184
|
+
} else if (command === 'postbuild' || command === 'post-build' || command === 'inject') {
|
|
185
|
+
decryptPostBuild();
|
|
77
186
|
} else {
|
|
78
187
|
console.error(`Unknown command: ${command}`);
|
|
79
188
|
process.exit(1);
|