Skip to content

Commit 0b76783

Browse files
authored
feat: export install-source helpers (#393)
* feat: export install-source helpers * fix: freeze exported archive extensions
1 parent d2b02f1 commit 0b76783

File tree

5 files changed

+37
-5
lines changed

5 files changed

+37
-5
lines changed

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
"import": "./dist/src/remote-config.js",
2121
"types": "./dist/src/remote-config.d.ts"
2222
},
23+
"./install-source": {
24+
"import": "./dist/src/install-source.js",
25+
"types": "./dist/src/install-source.d.ts"
26+
},
2327
"./contracts": {
2428
"import": "./dist/src/contracts.js",
2529
"types": "./dist/src/contracts.d.ts"

rslib.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default defineConfig({
1717
source: {
1818
entry: {
1919
index: 'src/index.ts',
20+
'install-source': 'src/install-source.ts',
2021
metro: 'src/metro.ts',
2122
'remote-config': 'src/remote-config.ts',
2223
contracts: 'src/contracts.ts',

src/install-source.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export {
2+
ARCHIVE_EXTENSIONS,
3+
isBlockedIpAddress,
4+
isBlockedSourceHostname,
5+
isTrustedInstallSourceUrl,
6+
materializeInstallablePath,
7+
validateDownloadSourceUrl,
8+
} from './platforms/install-source.ts';
9+
10+
export type {
11+
MaterializeInstallSource,
12+
MaterializedInstallable,
13+
} from './platforms/install-source.ts';

src/platforms/__tests__/install-source.test.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@ import fs from 'node:fs/promises';
66
import os from 'node:os';
77
import path from 'node:path';
88
import {
9+
ARCHIVE_EXTENSIONS,
10+
isBlockedIpAddress,
11+
isBlockedSourceHostname,
912
isTrustedInstallSourceUrl,
1013
materializeInstallablePath,
1114
validateDownloadSourceUrl,
12-
} from '../install-source.ts';
15+
} from '../../install-source.ts';
1316
import { prepareAndroidInstallArtifact } from '../android/install-artifact.ts';
1417
import { prepareIosInstallArtifact } from '../ios/install-artifact.ts';
1518

@@ -46,6 +49,15 @@ test('validateDownloadSourceUrl rejects unsupported protocols', async () => {
4649
);
4750
});
4851

52+
test('public install-source helpers expose the SSRF and archive surface', () => {
53+
assert.deepEqual(ARCHIVE_EXTENSIONS, ['.zip', '.tar', '.tar.gz', '.tgz']);
54+
assert.equal(Object.isFrozen(ARCHIVE_EXTENSIONS), true);
55+
assert.equal(isBlockedSourceHostname('localhost'), true);
56+
assert.equal(isBlockedSourceHostname('example.com'), false);
57+
assert.equal(isBlockedIpAddress('127.0.0.1'), true);
58+
assert.equal(isBlockedIpAddress('203.0.113.10'), false);
59+
});
60+
4961
test('isTrustedInstallSourceUrl recognizes supported artifact services', () => {
5062
assert.equal(
5163
isTrustedInstallSourceUrl('https://api.github.com/repos/acme/app/actions/artifacts/1/zip'),

src/platforms/install-source.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ export type MaterializedInstallable = {
4242
cleanup: () => Promise<void>;
4343
};
4444

45-
const ARCHIVE_EXTENSIONS = ['.zip', '.tar', '.tar.gz', '.tgz'] as const;
45+
const INTERNAL_ARCHIVE_EXTENSIONS = ['.zip', '.tar', '.tar.gz', '.tgz'] as const;
46+
47+
export const ARCHIVE_EXTENSIONS = Object.freeze([...INTERNAL_ARCHIVE_EXTENSIONS] as const);
4648
const MAX_INSTALL_SOURCE_SEARCH_DEPTH = 5;
4749
const DEFAULT_SOURCE_DOWNLOAD_TIMEOUT_MS = resolveTimeoutMs(
4850
process.env.AGENT_DEVICE_SOURCE_DOWNLOAD_TIMEOUT_MS,
@@ -271,13 +273,13 @@ function resolveDownloadFileName(response: Response, parsedUrl: URL): string {
271273
return 'downloaded-artifact.bin';
272274
}
273275

274-
function isBlockedSourceHostname(hostname: string): boolean {
276+
export function isBlockedSourceHostname(hostname: string): boolean {
275277
if (!hostname) return true;
276278
if (hostname === 'localhost' || hostname.endsWith('.localhost')) return true;
277279
return isBlockedIpAddress(hostname);
278280
}
279281

280-
function isBlockedIpAddress(address: string): boolean {
282+
export function isBlockedIpAddress(address: string): boolean {
281283
const family = net.isIP(address);
282284
if (family === 4) return isBlockedIpv4(address);
283285
if (family === 6) return isBlockedIpv6(address);
@@ -449,7 +451,7 @@ async function extractArchive(
449451

450452
function isArchivePath(candidatePath: string): boolean {
451453
const lower = candidatePath.toLowerCase();
452-
return ARCHIVE_EXTENSIONS.some((extension) => lower.endsWith(extension));
454+
return INTERNAL_ARCHIVE_EXTENSIONS.some((extension) => lower.endsWith(extension));
453455
}
454456

455457
async function runCleanupTasks(tasks: Array<() => Promise<void>>): Promise<void> {

0 commit comments

Comments
 (0)