@h0tp/shucky 0.1.0 → 0.4.4

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/lib/config.js CHANGED
@@ -1,52 +1,52 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
-
6
- const DEFAULTS = {
7
- policy: 'block',
8
- failOn: ['high', 'critical'],
9
- warnOn: ['medium'],
10
- rules: {
11
- secret_access: true,
12
- agent_state_access: true,
13
- browser_session: true,
14
- network_exfil: true,
15
- obfuscation: true,
16
- destructive: true,
17
- persistence: true,
18
- prompt_injection: true,
19
- supply_chain: true,
20
- undeclared_capability: true,
21
- excessive_scope: true
22
- },
23
- trustedSources: [
24
- 'anthropics', 'vercel-labs', 'microsoft', 'google', 'stripe',
25
- 'cloudflare', 'netlify', 'huggingface', 'sentry', 'expo', 'figma', 'trailofbits'
26
- ],
27
- trustedSourcePolicy: 'relax',
28
- requireAgentReview: true,
29
- allowOverride: true,
30
- overrideRequiresReason: true,
31
- persistApprovals: true,
32
- approvalsFile: 'approved-skills.json'
33
- };
34
-
35
- // Load config from (in order of precedence, lowest first):
36
- // packaged DEFAULTS -> config.json (packaged or --config) -> env vars -> CLI overrides.
37
- function loadConfig(configPath, overrides) {
38
- let cfg = Object.assign({}, DEFAULTS);
39
- const p = configPath || path.join(__dirname, '..', 'config.json');
40
- try {
41
- const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
42
- cfg = Object.assign(cfg, raw);
43
- } catch (e) {
44
- // No/invalid config file — fall back to defaults silently.
45
- }
46
- if (process.env.SHUCKY_POLICY) cfg.policy = process.env.SHUCKY_POLICY;
47
- if (process.env.SHUCKY_SOURCE) cfg.source = process.env.SHUCKY_SOURCE;
48
- if (overrides) Object.assign(cfg, overrides);
49
- return cfg;
50
- }
51
-
52
- module.exports = { loadConfig, DEFAULTS };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const DEFAULTS = {
7
+ policy: 'block',
8
+ failOn: ['high', 'critical'],
9
+ warnOn: ['medium'],
10
+ rules: {
11
+ secret_access: true,
12
+ agent_state_access: true,
13
+ browser_session: true,
14
+ network_exfil: true,
15
+ obfuscation: true,
16
+ destructive: true,
17
+ persistence: true,
18
+ prompt_injection: true,
19
+ supply_chain: true,
20
+ undeclared_capability: true,
21
+ excessive_scope: true
22
+ },
23
+ trustedSources: [
24
+ 'anthropics', 'vercel-labs', 'microsoft', 'google', 'stripe',
25
+ 'cloudflare', 'netlify', 'huggingface', 'sentry', 'expo', 'figma', 'trailofbits'
26
+ ],
27
+ trustedSourcePolicy: 'relax',
28
+ requireAgentReview: true,
29
+ allowOverride: true,
30
+ overrideRequiresReason: true,
31
+ persistApprovals: true,
32
+ approvalsFile: 'approved-skills.json'
33
+ };
34
+
35
+ // Load config from (in order of precedence, lowest first):
36
+ // packaged DEFAULTS -> config.json (packaged or --config) -> env vars -> CLI overrides.
37
+ function loadConfig(configPath, overrides) {
38
+ let cfg = Object.assign({}, DEFAULTS);
39
+ const p = configPath || path.join(__dirname, '..', 'config.json');
40
+ try {
41
+ const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
42
+ cfg = Object.assign(cfg, raw);
43
+ } catch (e) {
44
+ // No/invalid config file — fall back to defaults silently.
45
+ }
46
+ if (process.env.SHUCKY_POLICY) cfg.policy = process.env.SHUCKY_POLICY;
47
+ if (process.env.SHUCKY_SOURCE) cfg.source = process.env.SHUCKY_SOURCE;
48
+ if (overrides) Object.assign(cfg, overrides);
49
+ return cfg;
50
+ }
51
+
52
+ module.exports = { loadConfig, DEFAULTS };
@@ -0,0 +1,143 @@
1
+ 'use strict';
2
+
3
+ // shucky skill discovery — find the SKILL.md packages inside a fetched directory.
4
+ // Plugin-manifest handling ported from vercel-labs/skills `src/plugin-manifest.ts` (MIT). See NOTICE.
5
+ //
6
+ // discoverSkills(rootDir, { subpath?, skillFilter? }) -> [{ name, dir, skillMdPath, description, frontmatterName }]
7
+ //
8
+ // Frontmatter is read with a STRING-ONLY line reader (no `yaml` dep, no anchors/aliases) so a
9
+ // hostile SKILL.md can't trigger a YAML-bomb or type-coercion during discovery. The full file is
10
+ // still handed to scanTarget(). Symlinks are never traversed out of the tree.
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ const MAX_DEPTH = 8;
16
+
17
+ // Strip ANSI/OSC/control sequences (CWE-150) from any string we might echo to a terminal.
18
+ function stripControl(s) {
19
+ return String(s)
20
+ .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '') // OSC
21
+ .replace(/\x1b[@-_][0-?]*[ -/]*[@-~]/g, '') // CSI / other ESC
22
+ .replace(/[\x00-\x08\x0b-\x1f\x7f]/g, ''); // raw control chars
23
+ }
24
+
25
+ // Filesystem-safe install name derived from a (possibly hostile) frontmatter name or dir basename.
26
+ function safeName(raw) {
27
+ let s = stripControl(raw).trim();
28
+ s = s.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^[.-]+/, '').replace(/-+/g, '-').replace(/[.-]+$/, '');
29
+ return s.slice(0, 100);
30
+ }
31
+
32
+ // Minimal, injection-safe frontmatter reader: only name/description/license out of the leading --- block.
33
+ function parseFrontmatter(text) {
34
+ const out = {};
35
+ const m = String(text).match(/^?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/);
36
+ if (!m) return out;
37
+ for (const line of m[1].split(/\r?\n/)) {
38
+ const kv = line.match(/^([A-Za-z0-9_-]+):[ \t]*(.*)$/);
39
+ if (!kv) continue;
40
+ const key = kv[1].toLowerCase();
41
+ if (key !== 'name' && key !== 'description' && key !== 'license') continue;
42
+ let val = kv[2].trim();
43
+ if (val.length >= 2 && ((val[0] === '"' && val[val.length - 1] === '"') || (val[0] === "'" && val[val.length - 1] === "'"))) {
44
+ val = val.slice(1, -1);
45
+ }
46
+ out[key] = val;
47
+ }
48
+ return out;
49
+ }
50
+
51
+ function isSubpathSafe(base, target) {
52
+ const rel = path.relative(base, target);
53
+ return rel === '' || (!rel.startsWith('..' + path.sep) && rel !== '..' && !path.isAbsolute(rel));
54
+ }
55
+
56
+ function walkForSkills(root, out, depth) {
57
+ if (depth > MAX_DEPTH) return;
58
+ let entries;
59
+ try { entries = fs.readdirSync(root, { withFileTypes: true }); }
60
+ catch (e) { return; }
61
+ for (const e of entries) {
62
+ if (e.name === '.git' || e.name === 'node_modules' || e.name === '.github') continue;
63
+ const full = path.join(root, e.name);
64
+ let st;
65
+ try { st = fs.lstatSync(full); } catch (er) { continue; }
66
+ if (st.isSymbolicLink()) continue; // never follow symlinks out of the fetched tree
67
+ if (st.isDirectory()) walkForSkills(full, out, depth + 1);
68
+ else if (st.isFile() && /^skill\.md$/i.test(e.name)) out.push(full);
69
+ }
70
+ }
71
+
72
+ // Extra skill directories declared by Claude-Code plugin manifests (validated within rootDir).
73
+ function getPluginSkillPaths(rootDir) {
74
+ const dirs = [];
75
+ const add = function (rel) {
76
+ if (typeof rel !== 'string' || rel.indexOf('./') !== 0) return; // must be repo-relative
77
+ const abs = path.resolve(rootDir, rel);
78
+ if (isSubpathSafe(rootDir, abs)) dirs.push(abs);
79
+ };
80
+ const readJson = function (p) {
81
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (e) { return null; }
82
+ };
83
+ const plugin = readJson(path.join(rootDir, '.claude-plugin', 'plugin.json'));
84
+ if (plugin && Array.isArray(plugin.skills)) plugin.skills.forEach(add);
85
+ const market = readJson(path.join(rootDir, '.claude-plugin', 'marketplace.json'));
86
+ if (market && Array.isArray(market.plugins)) {
87
+ for (const pl of market.plugins) {
88
+ if (pl && Array.isArray(pl.skills)) pl.skills.forEach(add);
89
+ }
90
+ }
91
+ return dirs;
92
+ }
93
+
94
+ function discoverSkills(rootDir, opts) {
95
+ opts = opts || {};
96
+ let base = rootDir;
97
+ if (opts.subpath) {
98
+ const target = path.resolve(rootDir, opts.subpath);
99
+ if (!isSubpathSafe(rootDir, target)) throw new Error('subpath escapes source: ' + opts.subpath);
100
+ base = target;
101
+ }
102
+
103
+ const mdPaths = [];
104
+ walkForSkills(base, mdPaths, 0);
105
+ for (const pd of getPluginSkillPaths(rootDir)) {
106
+ if (fs.existsSync(pd)) walkForSkills(pd, mdPaths, 0);
107
+ }
108
+
109
+ // Shallowest first → deterministic dedupe by install name.
110
+ mdPaths.sort(function (a, b) { return a.split(path.sep).length - b.split(path.sep).length || a.localeCompare(b); });
111
+
112
+ const byName = new Map();
113
+ for (const mdPath of mdPaths) {
114
+ const dir = path.dirname(mdPath);
115
+ let text = '';
116
+ try { text = fs.readFileSync(mdPath, 'utf8'); } catch (e) { continue; }
117
+ const fm = parseFrontmatter(text);
118
+ const name = safeName(fm.name || path.basename(dir));
119
+ if (!name) continue;
120
+ if (!byName.has(name)) {
121
+ byName.set(name, {
122
+ name: name,
123
+ dir: dir,
124
+ skillMdPath: mdPath,
125
+ description: stripControl(fm.description || '').slice(0, 300),
126
+ frontmatterName: fm.name || null
127
+ });
128
+ }
129
+ }
130
+
131
+ let skills = Array.from(byName.values());
132
+ if (opts.skillFilter) {
133
+ const f = String(opts.skillFilter).toLowerCase();
134
+ skills = skills.filter(function (s) {
135
+ return s.name.toLowerCase() === f ||
136
+ path.basename(s.dir).toLowerCase() === f ||
137
+ (s.frontmatterName || '').toLowerCase() === f;
138
+ });
139
+ }
140
+ return skills;
141
+ }
142
+
143
+ module.exports = { discoverSkills, parseFrontmatter, safeName, stripControl, isSubpathSafe };
package/lib/fetch.js ADDED
@@ -0,0 +1,303 @@
1
+ 'use strict';
2
+
3
+ // shucky universal fetcher — normalise ANY source into a local directory we can scan.
4
+ // Every fetched skill ends up as plain files on disk, which scanTarget() already eats.
5
+ // Zero npm deps: `git` (system binary) for git-type sources, Node `https`/`http` for the rest.
6
+ //
7
+ // fetchSource(parsed, opts) -> { dir, ref, provenance, cleanup }
8
+ // dir absolute path to the fetched skill root (temp dir, or the local path in place)
9
+ // ref resolved commit SHA (git) / content hash (rawfile) / null — feeds the scan gate
10
+ // provenance { type, url, input } for the lockfile + messaging
11
+ // cleanup() removes the temp dir (no-op for local sources); ALWAYS call it in a finally
12
+
13
+ const fs = require('fs');
14
+ const os = require('os');
15
+ const path = require('path');
16
+ const http = require('http');
17
+ const https = require('https');
18
+ const crypto = require('crypto');
19
+ const { execFileSync } = require('child_process');
20
+ const { assertSafeHttpsUrl } = require('./safeurl');
21
+
22
+ const DEFAULT_MAX_BYTES = Number(process.env.SHUCKY_MAX_FETCH_BYTES) || 25 * 1024 * 1024;
23
+ const DEFAULT_HTTP_TIMEOUT = Number(process.env.SHUCKY_HTTP_TIMEOUT) || 20000;
24
+ const DEFAULT_GIT_TIMEOUT = Number(process.env.SHUCKY_GIT_TIMEOUT) || 120000;
25
+ const MAX_REDIRECTS = 5;
26
+
27
+ // ---- temp dir lifecycle --------------------------------------------------
28
+
29
+ function makeTempRoot() {
30
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'shucky-'));
31
+ }
32
+
33
+ // Only ever remove a dir we created under os.tmpdir() with our prefix.
34
+ function removeTempRoot(tempRoot) {
35
+ if (!tempRoot) return;
36
+ try {
37
+ const base = path.basename(tempRoot);
38
+ if (base.indexOf('shucky-') !== 0) return;
39
+ if (tempRoot.indexOf(os.tmpdir()) !== 0) return;
40
+ fs.rmSync(tempRoot, { recursive: true, force: true });
41
+ } catch (e) { /* best effort */ }
42
+ }
43
+
44
+ // ---- git ------------------------------------------------------------------
45
+
46
+ function validateRef(ref) {
47
+ if (ref == null || ref === '') return;
48
+ if (ref[0] === '-' || !/^[\w./-]+$/.test(ref)) {
49
+ throw new Error('unsafe git ref: ' + JSON.stringify(ref));
50
+ }
51
+ }
52
+
53
+ function gitEnv() {
54
+ return Object.assign({}, process.env, {
55
+ GIT_TERMINAL_PROMPT: '0', // never prompt for credentials → fail fast
56
+ GIT_LFS_SKIP_SMUDGE: '1', // don't pull LFS blobs
57
+ GCM_INTERACTIVE: 'never',
58
+ GIT_ASKPASS: 'echo'
59
+ });
60
+ }
61
+
62
+ function runGit(args, opts) {
63
+ return execFileSync('git', args, Object.assign({
64
+ stdio: ['ignore', 'pipe', 'pipe'],
65
+ timeout: DEFAULT_GIT_TIMEOUT,
66
+ maxBuffer: 64 * 1024 * 1024,
67
+ encoding: 'utf8',
68
+ env: gitEnv()
69
+ }, opts || {}));
70
+ }
71
+
72
+ // Clone url@ref into repoDir (shallow). Falls back to full clone + checkout when ref is a
73
+ // commit SHA (which `--branch` can't take). Returns the resolved HEAD SHA.
74
+ function gitCloneInto(url, ref, repoDir) {
75
+ validateRef(ref);
76
+ try {
77
+ const args = ['clone', '--depth', '1', '--no-tags', '--single-branch'];
78
+ if (ref) args.push('--branch', ref);
79
+ args.push('--', url, repoDir);
80
+ runGit(args);
81
+ } catch (e) {
82
+ if (!ref) throw new Error('git clone failed: ' + gitErr(e));
83
+ // ref might be a commit SHA → clone the default branch, then check it out.
84
+ try { fs.rmSync(repoDir, { recursive: true, force: true }); } catch (e2) { /* ignore */ }
85
+ runGit(['clone', '--no-tags', '--', url, repoDir]);
86
+ runGit(['-C', repoDir, 'checkout', ref]);
87
+ }
88
+ return runGit(['-C', repoDir, 'rev-parse', 'HEAD']).trim();
89
+ }
90
+
91
+ function gitErr(e) {
92
+ const s = (e && (e.stderr || e.message) || '').toString().trim();
93
+ if (/Authentication failed|could not read Username|terminal prompts disabled/i.test(s)) {
94
+ return 'authentication required (private repo?) — shucky does not prompt for git credentials';
95
+ }
96
+ return s.split('\n').slice(-3).join(' ') || 'unknown error';
97
+ }
98
+
99
+ // ---- http(s) --------------------------------------------------------------
100
+
101
+ // GET a URL, re-validating SSRF safety on EVERY redirect hop, with size + time caps.
102
+ async function safeGet(url, opts) {
103
+ opts = opts || {};
104
+ const maxBytes = opts.maxBytes || DEFAULT_MAX_BYTES;
105
+ let current = url;
106
+ for (let hop = 0; ; hop++) {
107
+ if (hop > MAX_REDIRECTS) throw new Error('too many redirects fetching ' + url);
108
+ const u = await assertSafeHttpsUrl(current, { resolver: opts.resolver, allowHttp: opts.allowHttp });
109
+ const lib = u.protocol === 'http:' ? http : https;
110
+
111
+ const resp = await new Promise(function (res, rej) {
112
+ const req = lib.get(u, {
113
+ timeout: opts.timeout || DEFAULT_HTTP_TIMEOUT,
114
+ headers: Object.assign({ 'user-agent': 'shucky', 'accept': opts.accept || '*/*' }, opts.headers || {})
115
+ }, res);
116
+ req.on('timeout', function () { req.destroy(new Error('request timed out')); });
117
+ req.on('error', rej);
118
+ });
119
+
120
+ const status = resp.statusCode;
121
+ if (status >= 300 && status < 400 && resp.headers.location) {
122
+ resp.resume(); // drain
123
+ current = new URL(resp.headers.location, u).toString();
124
+ continue;
125
+ }
126
+ if (status !== 200) { resp.resume(); throw new Error('HTTP ' + status + ' fetching ' + current); }
127
+
128
+ const cl = Number(resp.headers['content-length'] || 0);
129
+ if (cl && cl > maxBytes) { resp.destroy(); throw new Error('response too large (' + cl + ' bytes)'); }
130
+
131
+ return await new Promise(function (res, rej) {
132
+ const chunks = [];
133
+ let total = 0;
134
+ resp.on('data', function (d) {
135
+ total += d.length;
136
+ if (total > maxBytes) { resp.destroy(); rej(new Error('response exceeded ' + maxBytes + ' bytes')); return; }
137
+ chunks.push(d);
138
+ });
139
+ resp.on('end', function () { res(Buffer.concat(chunks)); });
140
+ resp.on('error', rej);
141
+ });
142
+ }
143
+ }
144
+
145
+ function shortHash(buf) {
146
+ return 'sha256:' + crypto.createHash('sha256').update(buf).digest('hex').slice(0, 16);
147
+ }
148
+
149
+ // ---- per-type fetchers ----------------------------------------------------
150
+
151
+ function fetchLocal(parsed) {
152
+ const p = parsed.localPath || parsed.url;
153
+ let stat;
154
+ try { stat = fs.statSync(p); }
155
+ catch (e) { throw new Error('local source not found: ' + p); }
156
+ const dir = stat.isDirectory() ? p : path.dirname(p);
157
+ return { dir: dir, ref: null, provenance: { type: 'local', url: p, input: p }, cleanup: function () {} };
158
+ }
159
+
160
+ function fetchGit(parsed, opts) {
161
+ if (/^https?:\/\//.test(parsed.url)) {
162
+ // Validate the clone host (SSRF) before handing it to git.
163
+ return assertSafeHttpsUrl(parsed.url, { resolver: opts && opts.resolver })
164
+ .then(function () { return doGitClone(parsed); });
165
+ }
166
+ // git@ / ssh:// — no http host to validate; trust the explicit URL.
167
+ return Promise.resolve(doGitClone(parsed));
168
+ }
169
+
170
+ function doGitClone(parsed) {
171
+ const tempRoot = makeTempRoot();
172
+ const repoDir = path.join(tempRoot, 'repo');
173
+ try {
174
+ const sha = gitCloneInto(parsed.url, parsed.ref, repoDir);
175
+ return {
176
+ dir: repoDir,
177
+ ref: sha,
178
+ provenance: { type: parsed.type, url: parsed.url, input: parsed.url },
179
+ cleanup: function () { removeTempRoot(tempRoot); }
180
+ };
181
+ } catch (e) {
182
+ removeTempRoot(tempRoot);
183
+ throw e;
184
+ }
185
+ }
186
+
187
+ async function fetchRawfile(parsed, opts) {
188
+ const tempRoot = makeTempRoot();
189
+ const skillDir = path.join(tempRoot, 'skill');
190
+ try {
191
+ fs.mkdirSync(skillDir, { recursive: true });
192
+ const buf = await safeGet(parsed.url, { resolver: opts && opts.resolver, accept: 'text/markdown,text/plain,*/*' });
193
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), buf);
194
+ return {
195
+ dir: skillDir,
196
+ ref: shortHash(buf),
197
+ provenance: { type: 'rawfile', url: parsed.url, input: parsed.url },
198
+ cleanup: function () { removeTempRoot(tempRoot); }
199
+ };
200
+ } catch (e) {
201
+ removeTempRoot(tempRoot);
202
+ throw e;
203
+ }
204
+ }
205
+
206
+ // Minimal RFC-8615 well-known discovery: probe the index, materialise each `skill-md`
207
+ // skill as <name>/SKILL.md. Archive-type entries are skipped (Phase 3).
208
+ async function fetchWellKnown(parsed, opts) {
209
+ const origin = new URL(parsed.url).origin;
210
+ const indexPaths = ['/.well-known/agent-skills/index.json', '/.well-known/skills/index.json'];
211
+ let index = null, indexBase = null;
212
+ for (const ip of indexPaths) {
213
+ try {
214
+ const buf = await safeGet(origin + ip, { resolver: opts && opts.resolver, accept: 'application/json' });
215
+ index = JSON.parse(buf.toString('utf8'));
216
+ indexBase = ip;
217
+ break;
218
+ } catch (e) { /* try next */ }
219
+ }
220
+ if (!index || !Array.isArray(index.skills)) {
221
+ throw new Error('no .well-known skills index at ' + origin);
222
+ }
223
+
224
+ const tempRoot = makeTempRoot();
225
+ try {
226
+ let count = 0;
227
+ for (const sk of index.skills) {
228
+ if (!sk || !sk.name) continue;
229
+ const name = String(sk.name).replace(/[^A-Za-z0-9._-]/g, '-');
230
+ let md = null;
231
+ if (sk.url) {
232
+ if (sk.type && sk.type !== 'skill-md') continue; // archives → Phase 3
233
+ md = await safeGet(new URL(sk.url, origin).toString(), { resolver: opts && opts.resolver });
234
+ } else if (Array.isArray(sk.files)) {
235
+ const base = origin + path.posix.dirname(indexBase) + '/' + name + '/';
236
+ const target = sk.files.find(function (f) { return /SKILL\.md$/i.test(f); }) || sk.files[0];
237
+ if (target) md = await safeGet(new URL(target, base).toString(), { resolver: opts && opts.resolver });
238
+ }
239
+ if (!md) continue;
240
+ const d = path.join(tempRoot, name);
241
+ fs.mkdirSync(d, { recursive: true });
242
+ fs.writeFileSync(path.join(d, 'SKILL.md'), md);
243
+ count++;
244
+ }
245
+ if (!count) throw new Error('well-known index listed no installable skill-md skills');
246
+ return {
247
+ dir: tempRoot,
248
+ ref: null,
249
+ provenance: { type: 'well-known', url: parsed.url, input: parsed.url },
250
+ cleanup: function () { removeTempRoot(tempRoot); }
251
+ };
252
+ } catch (e) {
253
+ removeTempRoot(tempRoot);
254
+ throw e;
255
+ }
256
+ }
257
+
258
+ // Fetch a .tar.gz / .zip (remote URL or local file) and extract it into a temp dir, with all the
259
+ // zip-slip / zip-bomb / symlink guards in lib/archive.js. Untrusted → always fully scanned.
260
+ async function fetchArchive(parsed, opts) {
261
+ const tempRoot = makeTempRoot();
262
+ const extractDir = path.join(tempRoot, 'extract');
263
+ try {
264
+ let buf;
265
+ if (parsed.localPath) buf = fs.readFileSync(parsed.localPath);
266
+ else buf = await safeGet(parsed.url, { resolver: opts && opts.resolver, maxBytes: DEFAULT_MAX_BYTES });
267
+ require('./archive').extractArchive(buf, extractDir, { format: parsed.archiveFormat });
268
+ return {
269
+ dir: extractDir,
270
+ ref: shortHash(buf),
271
+ provenance: { type: 'archive', url: parsed.url, input: parsed.url },
272
+ cleanup: function () { removeTempRoot(tempRoot); }
273
+ };
274
+ } catch (e) {
275
+ removeTempRoot(tempRoot);
276
+ throw e;
277
+ }
278
+ }
279
+
280
+ // ---- dispatcher -----------------------------------------------------------
281
+
282
+ function fetchSource(parsed, opts) {
283
+ opts = opts || {};
284
+ switch (parsed.type) {
285
+ case 'local':
286
+ return Promise.resolve(fetchLocal(parsed));
287
+ case 'github':
288
+ case 'gitlab':
289
+ case 'gist':
290
+ case 'git':
291
+ return fetchGit(parsed, opts);
292
+ case 'rawfile':
293
+ return fetchRawfile(parsed, opts);
294
+ case 'well-known':
295
+ return fetchWellKnown(parsed, opts);
296
+ case 'archive':
297
+ return fetchArchive(parsed, opts);
298
+ default:
299
+ return Promise.reject(new Error('unsupported source type: ' + parsed.type));
300
+ }
301
+ }
302
+
303
+ module.exports = { fetchSource, safeGet, removeTempRoot, validateRef, makeTempRoot };