Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions lib/moduleEncoding.js
Original file line number Diff line number Diff line change
Expand Up @@ -1260,10 +1260,11 @@ async function validateAssetLimits(application, assets) {
*
* @param {Buffer|string} application Application binary buffer/path
* @param {Array.<object>|Array.<string>} assets Asset data or paths
* @param {Object} [vars] Env vars to include in the bundle as a JSON file and as an asset
* @param {string} output output filename
* @returns {Promise<Buffer>}
*/
async function createApplicationAndAssetBundle(application, assets) {
async function createApplicationAndAssetBundle(application, assets, vars) {
let app = {};
if (typeof application === 'string') {
app.name = path.basename(application);
Expand All @@ -1277,6 +1278,15 @@ async function createApplicationAndAssetBundle(application, assets) {
};
}

if (vars) {
const varsAsset = await createEnvVarsAssetModule(vars);
assets ||= [];
assets.push({
data: varsAsset,
name: 'env-vars'
});
}

const processedAssets = [];
if (assets) {
for (let asset of assets) {
Expand Down Expand Up @@ -1317,6 +1327,9 @@ async function createApplicationAndAssetBundle(application, assets) {
for (let asset of processedAssets) {
archive.append(asset.data, { name: `assets/${asset.name}` });
}
if (vars) {
archive.append(JSON.stringify(vars, null, 2), { name: 'env/env-vars.json' });
}
output.on('close', resolve);
archive.on('error', reject);
archive.pipe(output);
Expand Down Expand Up @@ -1351,7 +1364,7 @@ async function streamToBuffer(s) {
* Unpack application/asset zip bundle
*
* @param {Buffer} bundle Bundle data
* @returns {Promise<Buffer>}
* @returns {Promise<Object>}
*/
async function unpackApplicationAndAssetBundle(bundle) {
let stream = null;
Expand All @@ -1370,12 +1383,25 @@ async function unpackApplicationAndAssetBundle(bundle) {
stream.on('entry', async (entry) => {
if (entry.type === 'File') {
const data = await streamToBuffer(entry);
if (path.dirname(entry.path) === 'assets') {
const dir = path.dirname(entry.path);
if (dir === 'assets') {
result.assets.push({
name: path.basename(entry.path),
data: data
});
} else if (path.dirname(entry.path) === '.') {
} else if (dir === 'env') {
if (result.vars) {
throw new RangeError('More env vars than expected in a bundle');
}
try {
result.vars = {
name: entry.path,
data: JSON.parse(data.toString('utf-8'))
};
} catch (err) {
reject(new Error(`Env vars file at ${entry.path} is invalid: ${err.message}`));
}
} else if (dir === '.') {
if (result.application) {
throw new RangeError('More applications than expected in a bundle');
}
Expand Down Expand Up @@ -1625,7 +1651,7 @@ function isAssetValid(asset, assetInfo) {
const assetHashHex = assetHash.digest('hex');

return assetHashHex === assetInfo.hash;
};
}

/**
* Add security mode extension to module with protected type.
Expand Down Expand Up @@ -1667,7 +1693,7 @@ async function createEnvVarsAssetModule(vars, snapshot = undefined) {
if (name.length > 128) {
throw new Error(`Variable name is too long: ${name.slice(0, 30)}...`);
}
if (!/^[a-z_][a-z0-9_]*$/i.test(name)) {
if (!/^[A-Z_][A-Z0-9_]*$/.test(name)) {
throw new Error(`Invalid variable name: ${name}`);
}
if (typeof val !== 'string') {
Expand Down
23 changes: 17 additions & 6 deletions specs/lib/moduleEncoding.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,16 @@ describe('moduleEncoding', () => {
}
return expect(createApplicationAndAssetBundle(application, assets)).to.be.rejectedWith(AssetLimitError, /Total size of assets exceeds/);
});

it('includes env vars into the bundle', async () => {
const vars = { KEY: 'value' };
const application = fs.readFileSync(path.join(TEST_BINARIES_PATH, 'tracker-tinker@5.3.1.bin'));
const app = path.join(TEST_BINARIES_PATH, 'tracker-tinker@5.3.1.bin');
const bundle = await createApplicationAndAssetBundle(app, [], vars);
const unpacked = await unpackApplicationAndAssetBundle(bundle);
expect(unpacked.vars.data).be.eql(vars);
expect(unpacked.assets.length).be.eql(1);
});
});

describe('isAssetValid', () => {
Expand Down Expand Up @@ -987,6 +997,7 @@ describe('moduleEncoding', () => {
it('invalid variable name', async () => {
const msg = 'Invalid variable name: ';
await expect(createEnvVarsAssetModule({ '': '123' })).to.be.eventually.rejectedWith(Error, msg);
await expect(createEnvVarsAssetModule({ 'abc': '123' })).to.be.eventually.rejectedWith(Error, msg + 'abc');
await expect(createEnvVarsAssetModule({ '1abc': '123' })).to.be.eventually.rejectedWith(Error, msg + '1abc');
await expect(createEnvVarsAssetModule({ 'abc$': '123' })).to.be.eventually.rejectedWith(Error, msg + 'abc$');
});
Expand All @@ -998,24 +1009,24 @@ describe('moduleEncoding', () => {

it('variable value is not a string', async () => {
const msg = 'Variable value is not a string: ';
await expect(createEnvVarsAssetModule({ 'abc': 123 })).to.be.eventually.rejectedWith(Error, msg + 'abc');
await expect(createEnvVarsAssetModule({ 'ABC': 123 })).to.be.eventually.rejectedWith(Error, msg + 'ABC');
});

it('invalid snapshot hash', async () => {
const msg = 'Invalid snapshot hash';
await expect(createEnvVarsAssetModule({ 'abc': '123' }, { hash: '', updatedAt: 1 })).to.be.eventually.rejectedWith(Error, msg);
await expect(createEnvVarsAssetModule({ 'abc': '123' }, { hash: Buffer.from('not a hash', 'hex'), updatedAt: 1 })).to.be.eventually.rejectedWith(Error, msg);
await expect(createEnvVarsAssetModule({ 'ABC': '123' }, { hash: '', updatedAt: 1 })).to.be.eventually.rejectedWith(Error, msg);
await expect(createEnvVarsAssetModule({ 'ABC': '123' }, { hash: Buffer.from('not a hash', 'hex'), updatedAt: 1 })).to.be.eventually.rejectedWith(Error, msg);
});

it('invalid snapshot timestamp', async () => {
const msg = 'Invalid snapshot timestamp';
await expect(createEnvVarsAssetModule({ 'abc': '123' }, { hash: '11111111', updatedAt: -1 })).to.be.eventually.rejectedWith(Error, msg);
await expect(createEnvVarsAssetModule({ 'abc': '123' }, { hash: '11111111', updatedAt: '2025-12-31T23:59:60.999Z' })).to.be.eventually.rejectedWith(Error, msg);
await expect(createEnvVarsAssetModule({ 'ABC': '123' }, { hash: '11111111', updatedAt: -1 })).to.be.eventually.rejectedWith(Error, msg);
await expect(createEnvVarsAssetModule({ 'ABC': '123' }, { hash: '11111111', updatedAt: '2025-12-31T23:59:60.999Z' })).to.be.eventually.rejectedWith(Error, msg);
});

it('asset exceeds the maximum size', async () => {
const msg = 'Asset exceeds the maximum size';
await expect(createEnvVarsAssetModule({ 'abc': '1'.repeat(20000) })).to.be.eventually.rejectedWith(Error, msg);
await expect(createEnvVarsAssetModule({ 'ABC': '1'.repeat(20000) })).to.be.eventually.rejectedWith(Error, msg);
});
});
});
Expand Down