Skip to content

Commit

Permalink
Ensure project can be published as an NPM module
Browse files Browse the repository at this point in the history
Change-type: patch
  • Loading branch information
dfunckt committed Oct 26, 2018
1 parent 3bc2cbf commit ba1e99a
Show file tree
Hide file tree
Showing 21 changed files with 7,060 additions and 130 deletions.
Empty file added .npmignore
Empty file.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ FROM balena/open-balena-base:v4.4.2
EXPOSE 80

COPY package.json package-lock.json /usr/src/app/
RUN npm ci --unsafe-perm --production && npm cache clean --force
RUN npm ci --unsafe-perm --production --ignore-scripts && npm cache clean --force

COPY . /usr/src/app

Expand Down
6,914 changes: 6,914 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

21 changes: 19 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
{
"name": "@balena/open-balena-api",
"private": true,
"description": "Internet of things, Made Simple",
"version": "0.0.1",
"repository": {
"type": "git",
"url": "https://github.com/balena-io/open-balena-api"
},
"main": "dist/index",
"files": [
"dist/"
],
"scripts": {
"build": "npm run clean && tsc --project . && copyup \"src/**/*.sbvr\" \"src/**/*.sql\" dist/",
"clean": "rimraf dist/",
"lint": "resin-lint --typescript src/ && tsc --noEmit --project .",
"prettify": "prettier --config ./node_modules/resin-lint/config/.prettierrc --write \"src/**/*.ts\" \"typings/**/*.ts\"",
"prepare": "npm run build"
},
"dependencies": {
"@resin.io/device-types": "^10.4.1",
"@resin/pinejs": "^9.0.2",
Expand Down Expand Up @@ -66,7 +76,14 @@
"typed-error": "^2.0.0",
"typescript": "^3.1.3"
},
"optionalDependencies": {},
"devDependencies": {
"copyfiles": "^2.1.0",
"husky": "^0.14.3",
"lint-staged": "^7.3.0",
"prettier": "^1.14.3",
"resin-lint": "^2.0.0",
"rimraf": "^2.6.1"
},
"engines": {
"node": ">=8.0"
}
Expand Down
8 changes: 6 additions & 2 deletions src/commands/create-superuser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as _express from 'express';
import * as Promise from 'bluebird';

import { runInTransaction } from '../platform';
import { registerUser } from '../platform/auth';
Expand All @@ -7,12 +8,15 @@ function usage() {
return 'create-superuser USERNAME EMAIL PASSWORD';
}

export function execute(_app: _express.Application, args: string[]) {
export function execute(
_app: _express.Application,
args: string[],
): Promise<void> {
const [username, email, password] = args;
if (username == null || email == null || password == null) {
throw new Error(usage());
}
return runInTransaction(tx =>
registerUser({ username, email, password }, tx),
);
).return();
}
20 changes: 11 additions & 9 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import * as Promise from 'bluebird';
import { readdir } from 'fs';
import { sbvrUtils } from '../platform';
import { retrieveAPIKey } from '../platform/api-keys';

const readdirAsync = Promise.promisify(readdir);

sbvrUtils.addPureHook('all', 'all', 'all', {
PREPARSE: ({ req }) => {
// Extend Pine's default behavior of calling apiKeyMiddleware()
Expand All @@ -13,8 +9,14 @@ sbvrUtils.addPureHook('all', 'all', 'all', {
},
});

readdirAsync(__dirname + '/resources').each(initScript => {
if (/\.ts$/.test(initScript)) {
return require(__dirname + '/resources/' + initScript);
}
});
import './resources/api_key';
import './resources/application';
import './resources/device';
import './resources/envvars';
import './resources/image__is_part_of__release';
import './resources/image';
import './resources/release';
import './resources/service_install';
import './resources/service_instance';
import './resources/service';
import './resources/user';
5 changes: 3 additions & 2 deletions src/lib/application-types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as _ from 'lodash';
import * as Promise from 'bluebird';
import * as resinSemver from 'resin-semver';

import { sbvrUtils } from '@resin/pinejs';
Expand Down Expand Up @@ -50,7 +51,7 @@ export class WebUrlNotSupportedError extends sbvrUtils.ForbiddenError {
export const checkDevicesCanHaveDeviceURL = (
api: sbvrUtils.PinejsClient,
deviceIDs: number[],
) => {
): Promise<void> => {
return api
.get({
resource: 'application_type/$count',
Expand Down Expand Up @@ -92,7 +93,7 @@ export const checkDevicesCanBeInApplication = (
api: sbvrUtils.PinejsClient,
appId: number,
deviceIds: number[],
) => {
): Promise<void> => {
return api
.get({
resource: 'application_type',
Expand Down
16 changes: 0 additions & 16 deletions src/lib/device-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,6 @@ import {
DELTA_HOST,
} from './config';

if (REGISTRY_HOST == null) {
throw new Error('REGISTRY_HOST is required');
}
if (REGISTRY2_HOST == null) {
throw new Error('REGISTRY2_HOST is required');
}
if (API_HOST == null) {
throw new Error('API_HOST is required');
}
if (DELTA_HOST == null) {
throw new Error('DELTA_HOST is required');
}
if (VPN_HOST == null) {
throw new Error('VPN_HOST is required');
}

export const generateConfig = (req: Request, app: AnyObject) => {
const osVersion = req.param('version');
const userPromise = getUser(req);
Expand Down
13 changes: 3 additions & 10 deletions src/lib/device-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,15 @@ import { PinejsClientCoreFactory } from 'pinejs-client-core';

import { resinApi, root } from '../platform';

import { UriOptions, CoreOptions, UrlOptions } from 'request';

const DELAY_BETWEEN_DEVICE_REQUEST = 50;

import request = require('./request');
import { RequestResponse, requestAsync } from './request';
import { API_VPN_SERVICE_API_KEY } from './config';
const requestAsync = (Promise.promisify(request, {
multiArgs: true,
}) as any) as (
arg1: (UriOptions & CoreOptions) | (UrlOptions & CoreOptions),
) => Promise<RequestResponse>;

// Degraded network, slow devices, compressed docker binaries and any combination of these factors
// can cause proxied device requests to surpass the default timeout.
const DEVICE_REQUEST_TIMEOUT = 50000;

const DELAY_BETWEEN_DEVICE_REQUEST = 50;

const { BadRequestError } = sbvrUtils;

const badSupervisorResponse = (
Expand Down
10 changes: 7 additions & 3 deletions src/lib/device-types/build-info-facade.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as Promise from 'bluebird';
import * as memoizee from 'memoizee';
import * as deviceTypesLib from '@resin.io/device-types';
import { fileExists, getFile, getFolderSize, getImageKey } from './storage';
Expand All @@ -8,14 +9,17 @@ const BUILD_PROPERTY_CACHE_EXPIRATION = 10 * 60 * 1000; // 10 mins
const BUILD_COMPRESSED_SIZE_CACHE_EXPIRATION = 20 * 60 * 1000; // 20 mins

export const getIsIgnored = memoizee(
(normalizedSlug: string, buildId: string) => {
(normalizedSlug: string, buildId: string): Promise<boolean> => {
return fileExists(getImageKey(normalizedSlug, buildId, 'IGNORE'));
},
{ promise: true, preFetch: true, maxAge: BUILD_PROPERTY_CACHE_EXPIRATION },
);

export const getDeviceTypeJson = memoizee(
(normalizedSlug: string, buildId: string) => {
(
normalizedSlug: string,
buildId: string,
): Promise<deviceTypesLib.DeviceType | undefined> => {
return getFile(
getImageKey(normalizedSlug, buildId, 'device-type.json'),
).then(response => {
Expand All @@ -33,7 +37,7 @@ export const getDeviceTypeJson = memoizee(
);

export const getCompressedSize = memoizee(
(normalizedSlug: string, buildId: string) => {
(normalizedSlug: string, buildId: string): Promise<number> => {
return getFolderSize(getImageKey(normalizedSlug, buildId, 'compressed'));
},
{
Expand Down
2 changes: 1 addition & 1 deletion src/lib/device-types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from './build-info-facade';
import { getImageKey, IMAGE_STORAGE_PREFIX, listFolders } from './storage';

const { BadRequestError, NotFoundError } = sbvrUtils;
export const { BadRequestError, NotFoundError } = sbvrUtils;

export type DeviceType = deviceTypesLib.DeviceType;

Expand Down
2 changes: 1 addition & 1 deletion src/lib/errors.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import TypedError = require('typed-error');
import { sbvrUtils } from '../platform';

const { NotFoundError } = sbvrUtils;
export const { NotFoundError } = sbvrUtils;

export class NoDevicesFoundError extends NotFoundError {}

Expand Down
17 changes: 14 additions & 3 deletions src/lib/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import * as Promise from 'bluebird';
import { EXTERNAL_HTTP_TIMEOUT_MS } from './config';

type Request = typeof request;
request.get;

export type RequestResponse = [request.Response, any];

interface PromisifiedRequest extends Request {
getAsync(
Expand Down Expand Up @@ -33,10 +34,20 @@ interface PromisifiedRequest extends Request {
) => Promise<RequestResponse>;
}

const defaultedRequest = request.defaults({
export const defaultRequest = request.defaults({
timeout: EXTERNAL_HTTP_TIMEOUT_MS,
});

export = (Promise.promisifyAll(defaultedRequest, {
const promisifiedRequest = (Promise.promisifyAll(defaultRequest, {
multiArgs: true,
}) as any) as PromisifiedRequest;

export const requestAsync = (Promise.promisify(promisifiedRequest, {
multiArgs: true,
}) as any) as (
arg1:
| (request.UriOptions & request.CoreOptions)
| (request.UrlOptions & request.CoreOptions),
) => Promise<RequestResponse>;

export default promisifiedRequest;
2 changes: 1 addition & 1 deletion src/platform/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as _ from 'lodash';
import { createJwt, SignOptions } from './jwt';
import { createJwt, SignOptions, User } from './jwt';
import { retrieveAPIKey } from './api-keys';
import { Tx, sbvrUtils, resinApi, root } from './index';
import * as Promise from 'bluebird';
Expand Down
2 changes: 2 additions & 0 deletions src/platform/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ interface HookReqCaptureOptions {
req?: sbvrUtils.HookReq | Raven.CaptureOptions['req'];
}

type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U;

// Raven is actually fine with our trimmed down `req` from hooks, but it isn't typed that way
// so we have to overwrite and then cast later
interface CaptureOptions
Expand Down
10 changes: 10 additions & 0 deletions src/platform/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ export const updateOrInsertModel = (
type TxFn = (tx: Tx, ...args: any[]) => Promise<any>;
type TxFnArgs<T> = T extends (tx: Tx, ...args: infer U) => any ? U : any[];

// This gives the resolved return type, eg
// - `Promise<R>` -> `R`
// - `Bluebird<R>` -> `R`
// - `R` -> `R`
type ResolvableReturnType<T extends (...args: any[]) => any> = T extends (
...args: any[]
) => Promise<infer R>
? R
: T extends (...args: any[]) => Promise<infer R> ? R : ReturnType<T>;

// wrapInTransaction(someOperation) => fn
//
// Wraps a function to run inside a
Expand Down
62 changes: 44 additions & 18 deletions src/platform/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as Promise from 'bluebird';
import * as jsonwebtoken from 'jsonwebtoken';
import * as randomstring from 'randomstring';
import * as passport from 'passport';
import { sbvrUtils } from '@resin/pinejs';
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
import TypedError = require('typed-error');
import { captureException } from './errors';
Expand All @@ -16,16 +17,54 @@ import {
JSON_WEB_TOKEN_SECRET,
JSON_WEB_TOKEN_EXPIRY_MINUTES,
} from '../lib/config';

const EXPIRY_SECONDS = JSON_WEB_TOKEN_EXPIRY_MINUTES * 60;

if (JSON_WEB_TOKEN_SECRET == null) {
throw new Error('JWT secret must be provided');
class InvalidJwtSecretError extends TypedError {}

export interface ScopedAccessToken {
access: ScopedToken;
}
if (_.isNaN(EXPIRY_SECONDS)) {
throw new Error(`Invalid JWT expiry: '${JSON_WEB_TOKEN_EXPIRY_MINUTES}'`);

export interface ScopedAccessTokenOptions {
// The actor of the resulting token
actor: number;
// A list of permissions
permissions: string[];
// expires in x seconds
expiresIn: number;
}

class InvalidJwtSecretError extends TypedError {}
export interface ServiceToken extends sbvrUtils.Actor {
service: string;
apikey: string;
permissions: string[];
}

export interface ScopedToken extends sbvrUtils.Actor {
actor: number;
permissions: string[];
}

export interface ApiKey extends sbvrUtils.ApiKey {
key: string;
}

export interface User extends sbvrUtils.User {
id: number;
actor: number;
username: string;
email: string;
created_at: string;
jwt_secret?: string;
permissions: string[];

twoFactorRequired?: boolean;
authTime?: number;
}

export type Creds = ServiceToken | User | ScopedToken;
export type JwtUser = Creds | ScopedAccessToken;

export const strategy = new JwtStrategy(
{
Expand Down Expand Up @@ -147,19 +186,6 @@ export const middleware: RequestHandler = (req, res, next) => {

export const isJWT = (token: string): boolean => !!jsonwebtoken.decode(token);

export interface ScopedAccessToken {
access: ScopedToken;
}

export interface ScopedAccessTokenOptions {
// The actor of the resulting token
actor: number;
// A list of permissions
permissions: string[];
// expires in x seconds
expiresIn: number;
}

export function createScopedAccessToken(
options: ScopedAccessTokenOptions,
): string {
Expand Down
Loading

0 comments on commit ba1e99a

Please sign in to comment.