Skip to content

Commit 7ac08bb

Browse files
committed
first commit
0 parents  commit 7ac08bb

File tree

11 files changed

+237
-0
lines changed

11 files changed

+237
-0
lines changed

.gitignore

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
logs
2+
*.log
3+
npm-debug.log*
4+
pids
5+
*.pid
6+
*.seed
7+
*.pid.lock
8+
coverage
9+
.nyc_output
10+
node_modules/
11+
.env
12+
\.idea/
13+
14+
#*.json
15+
16+
ormconfig\.json
17+
18+
src/cache/
19+
20+
uploads/
21+
22+
dist/
23+
24+
*.pem
25+
acme/
26+
cache/

.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package-lock=false

Dockerfile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
FROM node:10
2+
3+
LABEL version="0.0.1"
4+
5+
WORKDIR /usr/src/app
6+
7+
COPY . .
8+
9+
EXPOSE 443
10+
EXPOSE 80
11+
12+
VOLUME /usr/src/app/cache
13+
VOLUME /usr/src/app/acme
14+
15+
CMD [ "npm", "run", "start" ]

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# self-hosted-unpkg

nodemon.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"watch": [
3+
"src/**/*.ts"
4+
],
5+
"ext": "ts",
6+
"ignore": [
7+
"./test/*.ts",
8+
"./node_modules/**/node_modules"
9+
],
10+
"exec": "node -r ts-node/register src/index.ts",
11+
"env": {
12+
"NODE_ENV": "development"
13+
}
14+
}

package.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"name": "@edgeworkscreative/self-hosted-unpkg",
3+
"version": "0.0.1",
4+
"private": true,
5+
"description": "",
6+
"main": "dist/index.js",
7+
"author": "Interactive Training",
8+
"license": "MIT",
9+
"scripts": {
10+
"start": "node dist/index.js",
11+
"dev": "nodemon --delay 1500ms --signal SIGTERM",
12+
"build": "tsc"
13+
},
14+
"dependencies": {
15+
"@interactivetraining/le-challenge-cloudflare": "1.3.1",
16+
"compression": "^1.7.3",
17+
"cors": "^2.8.5",
18+
"dotenv": "^5.0.1",
19+
"download-npm-package": "^3.1.12",
20+
"download-package-tarball": "^1.0.7",
21+
"express": "^4.16.4",
22+
"get-package-json-from-registry": "^2.2.1",
23+
"got": "^9.6.0",
24+
"greenlock-express": "^2.6.8",
25+
"helmet": "^3.16.0",
26+
"le-challenge-cloudflare": "^1.0.1",
27+
"mime": "^2.4.0",
28+
"redirect-https": "^1.3.0",
29+
"zlib": "^1.0.5"
30+
},
31+
"devDependencies": {
32+
"@types/compression": "0.0.36",
33+
"@types/cors": "^2.8.4",
34+
"@types/dotenv": "^6.1.0",
35+
"@types/express": "^4.16.1",
36+
"@types/helmet": "0.0.43",
37+
"@types/node": "^11.11.1",
38+
"@types/shelljs": "^0.8.3",
39+
"nodemon": "^1.18.10",
40+
"shelljs": "^0.8.3",
41+
"ts-node": "^8.0.2",
42+
"typescript": "^3.3.3333"
43+
}
44+
}

sample.env

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
DOMAIN=my-self-hosted-unpkg-domain.com
2+
NPM_REGISTRY=https://private.npm.registry
3+
NPM_USER=
4+
NPM_PASSWORD=
5+
NPM_TOKEN=
6+
CLOUDFLARE_EMAIL=
7+
CLOUDFLARE_API_KEY=
8+
LETS_ENCRYPT_EMAIL=
9+
LETS_ENCRYPT_AGREE_TO_TOS=true

src/helpers.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as downloadTarball from 'download-package-tarball';
2+
import * as got from 'got';
3+
import {IPackageParams} from './interfaces';
4+
5+
export async function downloadPackage(pkg: IPackageParams, directory: string) {
6+
let gotOpts: any = {};
7+
8+
if (process.env.NPM_TOKEN.trim() && process.env.NPM_TOKEN.trim().length > 0) {
9+
gotOpts.headers = {authorization: `Bearer ${process.env.NPM_TOKEN.trim()}`};
10+
} else {
11+
gotOpts.auth = `${process.env.NPM_USER}:${process.env.NPM_PASSWORD}`;
12+
}
13+
14+
if (!pkg.version.includes('.')) {
15+
const {body} = await got(`${process.env.NPM_REGISTRY}/${(pkg.scope) ? `${pkg.scope}/` : ''}${pkg.package}`, {
16+
...gotOpts,
17+
json: true
18+
});
19+
pkg.version = body['dist-tags'][pkg.version];
20+
}
21+
22+
await downloadTarball({
23+
url: `${process.env.NPM_REGISTRY}/${(pkg.scope) ? `${pkg.scope}/` : ''}${pkg.package}/-/${pkg.package}-${pkg.version}.tgz`,
24+
gotOpts: gotOpts,
25+
dir: directory
26+
});
27+
}

src/index.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import * as express from 'express';
2+
import * as fs from 'fs';
3+
import * as cors from 'cors';
4+
import * as compression from 'compression';
5+
import * as helmet from 'helmet';
6+
import {CloudflareChallenge} from '@interactivetraining/le-challenge-cloudflare';
7+
import {IPackageParams} from './interfaces';
8+
import {downloadPackage} from './helpers';
9+
10+
require('dotenv').config();
11+
12+
if (!fs.existsSync('cache')) fs.mkdirSync('cache');
13+
if (!fs.existsSync('acme')) fs.mkdirSync('acme');
14+
15+
const app = express();
16+
17+
app.use(helmet());
18+
app.use(cors());
19+
app.use(compression());
20+
21+
app.get(['/:scope?/:package@:version/*', '/:scope?/:package/*'], async (req, res) => {
22+
try {
23+
let params: IPackageParams = req.params;
24+
25+
// correct the params.package value when there isn't a scope or version provided
26+
if (!params.version && params.scope && !req.url.split('/')[1].includes('@')) {
27+
params.package = params.scope;
28+
params.scope = undefined;
29+
}
30+
31+
if (!params.version) params.version = 'latest';
32+
33+
const packagePath = `cache/${params.version}/${(params.scope) ? `${params.scope}/` : ``}${params.package}`;
34+
const filePath = `${packagePath}/${params['0']}`;
35+
36+
if (!fs.existsSync(packagePath) || params.version === 'latest') {
37+
await downloadPackage({
38+
scope: params.scope,
39+
package: params.package,
40+
version: params.version
41+
}, `cache/${params.version}`);
42+
}
43+
44+
console.log(`${(params.scope) ? `${params.scope}/` : ``}${params.package}@${params.version}: ${params['0']}`);
45+
46+
res.setHeader('Cache-Control', (!params.version.includes('.')) ? 'no-cache' : 'public, max-age=31536000');
47+
res.sendFile(filePath, {root: `./`});
48+
} catch (e) {
49+
console.log(e);
50+
const status = (e.hasOwnProperty('statusCode')) ? e.statusCode : 500;
51+
const message = (e.hasOwnProperty('statusMessage')) ? e.statusMessage : e.message;
52+
res.status(status).send(message);
53+
}
54+
});
55+
56+
require('greenlock-express').create({
57+
version: 'draft-11',
58+
server: 'https://acme-v02.api.letsencrypt.org/directory',
59+
email: process.env.LETS_ENCRYPT_EMAIL,
60+
agreeTos: (process.env.LETS_ENCRYPT_AGREE_TO_TOS.trim() === 'true'),
61+
approveDomains: [
62+
process.env.DOMAIN
63+
],
64+
configDir: 'acme/',
65+
app: app,
66+
challengeType: 'dns-01',
67+
challenge: new CloudflareChallenge({
68+
cloudflare: {
69+
email: process.env.CLOUDFLARE_EMAIL,
70+
key: process.env.CLOUDFLARE_API_KEY
71+
},
72+
acmePrefix: '_acme-challenge',
73+
verifyPropagation: {waitFor: 5000, retries: 50},
74+
useDNSOverHTTPS: false
75+
})
76+
}).listen(80, 443, () => console.log(`Listening...`));

src/interfaces.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface IPackageParams {
2+
scope?: string;
3+
package: string;
4+
version?: string;
5+
0?: string;
6+
}

0 commit comments

Comments
 (0)