@terreno/api 0.7.0 → 0.7.2
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/dist/__tests__/versionCheck.test.d.ts +1 -0
- package/dist/__tests__/versionCheck.test.js +263 -0
- package/dist/expressServer.js +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/models/versionConfig.d.ts +17 -0
- package/dist/models/versionConfig.js +66 -0
- package/dist/terrenoApp.js +2 -2
- package/dist/vendor/wesleytodd-openapi/index.d.ts +27 -0
- package/dist/vendor/wesleytodd-openapi/index.js +176 -0
- package/dist/vendor/wesleytodd-openapi/lib/convert-yaml.d.ts +2 -0
- package/dist/vendor/wesleytodd-openapi/lib/convert-yaml.js +13 -0
- package/dist/vendor/wesleytodd-openapi/lib/generate-doc.d.ts +2 -0
- package/dist/vendor/wesleytodd-openapi/lib/generate-doc.js +148 -0
- package/dist/vendor/wesleytodd-openapi/lib/layer-schema.d.ts +2 -0
- package/dist/vendor/wesleytodd-openapi/lib/layer-schema.js +12 -0
- package/dist/vendor/wesleytodd-openapi/lib/minimum-doc.d.ts +6 -0
- package/dist/vendor/wesleytodd-openapi/lib/minimum-doc.js +11 -0
- package/dist/vendor/wesleytodd-openapi/lib/ui.d.ts +1 -0
- package/dist/vendor/wesleytodd-openapi/lib/ui.js +37 -0
- package/dist/vendor/wesleytodd-openapi/lib/validate.d.ts +2 -0
- package/dist/vendor/wesleytodd-openapi/lib/validate.js +168 -0
- package/dist/versionCheckPlugin.d.ts +15 -0
- package/dist/versionCheckPlugin.js +106 -0
- package/package.json +9 -2
- package/src/__tests__/versionCheck.test.ts +132 -0
- package/src/expressServer.ts +1 -2
- package/src/index.ts +2 -0
- package/src/models/versionConfig.ts +92 -0
- package/src/terrenoApp.ts +1 -2
- package/src/vendor/wesleytodd-openapi/LICENSE +15 -0
- package/src/vendor/wesleytodd-openapi/index.js +189 -0
- package/src/vendor/wesleytodd-openapi/lib/convert-yaml.js +13 -0
- package/src/vendor/wesleytodd-openapi/lib/generate-doc.js +153 -0
- package/src/vendor/wesleytodd-openapi/lib/layer-schema.js +13 -0
- package/src/vendor/wesleytodd-openapi/lib/minimum-doc.js +12 -0
- package/src/vendor/wesleytodd-openapi/lib/ui.js +71 -0
- package/src/vendor/wesleytodd-openapi/lib/validate.js +152 -0
- package/src/versionCheckPlugin.ts +81 -0
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Vendored from https://github.com/wesleytodd/express-openapi (branch: express-5)
|
|
2
|
+
// License: ISC (see ../LICENSE)
|
|
3
|
+
'use strict'
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const serve = require('serve-static')
|
|
6
|
+
|
|
7
|
+
module.exports.serveSwaggerUI = function serveSwaggerUI (documentUrl, opts = {}) {
|
|
8
|
+
const { plugins, ...options } = opts
|
|
9
|
+
|
|
10
|
+
return [serve(path.resolve(require.resolve('swagger-ui-dist'), '..'), { index: false }),
|
|
11
|
+
function returnUiInit (req, res, next) {
|
|
12
|
+
if (req.path.endsWith('/swagger-ui-init.js')) {
|
|
13
|
+
res.type('.js')
|
|
14
|
+
res.send(`window.onload = function () {
|
|
15
|
+
window.ui = SwaggerUIBundle({
|
|
16
|
+
url: '${documentUrl}',
|
|
17
|
+
dom_id: '#swagger-ui',
|
|
18
|
+
${plugins?.length ? `plugins: [${plugins}],` : ''}
|
|
19
|
+
...${JSON.stringify(options)}
|
|
20
|
+
})
|
|
21
|
+
}`
|
|
22
|
+
)
|
|
23
|
+
} else {
|
|
24
|
+
next()
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
function renderSwaggerHtml (req, res) {
|
|
28
|
+
res.type('html').send(renderHtmlPage('Swagger UI', `
|
|
29
|
+
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" >
|
|
30
|
+
`, `
|
|
31
|
+
<div id="swagger-ui"></div>
|
|
32
|
+
<script src="./swagger-ui-bundle.js"></script>
|
|
33
|
+
<script src="./swagger-ui-standalone-preset.js"></script>
|
|
34
|
+
<script src="./swagger-ui-init.js"></script>
|
|
35
|
+
`))
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderHtmlPage (title, head, body) {
|
|
41
|
+
return `<!DOCTYPE html>
|
|
42
|
+
<html>
|
|
43
|
+
<head>
|
|
44
|
+
<title>${title}</title>
|
|
45
|
+
<meta charset="utf-8"/>
|
|
46
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
47
|
+
<style>
|
|
48
|
+
html {
|
|
49
|
+
box-sizing: border-box;
|
|
50
|
+
overflow: -moz-scrollbars-vertical;
|
|
51
|
+
overflow-y: scroll;
|
|
52
|
+
}
|
|
53
|
+
*,
|
|
54
|
+
*:before,
|
|
55
|
+
*:after {
|
|
56
|
+
box-sizing: inherit;
|
|
57
|
+
}
|
|
58
|
+
body {
|
|
59
|
+
margin: 0;
|
|
60
|
+
padding: 0;
|
|
61
|
+
background: #fafafa;
|
|
62
|
+
}
|
|
63
|
+
</style>
|
|
64
|
+
${head}
|
|
65
|
+
</head>
|
|
66
|
+
<body>
|
|
67
|
+
${body}
|
|
68
|
+
</body>
|
|
69
|
+
</html>
|
|
70
|
+
`
|
|
71
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Vendored from https://github.com/wesleytodd/express-openapi (branch: express-5)
|
|
2
|
+
// License: ISC (see ../LICENSE)
|
|
3
|
+
'use strict'
|
|
4
|
+
const Ajv = require('ajv')
|
|
5
|
+
const addFormats = require('ajv-formats')
|
|
6
|
+
const addKeywords = require('ajv-keywords')
|
|
7
|
+
const httpErrors = require('http-errors')
|
|
8
|
+
|
|
9
|
+
const BASE_REQ_SCHEMA = {
|
|
10
|
+
type: 'object',
|
|
11
|
+
required: ['headers', 'params', 'query'],
|
|
12
|
+
properties: {
|
|
13
|
+
headers: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
required: [],
|
|
16
|
+
properties: {}
|
|
17
|
+
},
|
|
18
|
+
params: {
|
|
19
|
+
type: 'object',
|
|
20
|
+
required: [],
|
|
21
|
+
properties: {}
|
|
22
|
+
},
|
|
23
|
+
query: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
required: [],
|
|
26
|
+
properties: {}
|
|
27
|
+
},
|
|
28
|
+
body: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
required: [],
|
|
31
|
+
properties: {}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = function makeValidatorMiddleware (middleware, schema, opts) {
|
|
37
|
+
let ajv
|
|
38
|
+
let validate
|
|
39
|
+
|
|
40
|
+
function makeValidator () {
|
|
41
|
+
const reqSchema = structuredClone(BASE_REQ_SCHEMA)
|
|
42
|
+
|
|
43
|
+
// Compile req schema on first request
|
|
44
|
+
// Build param validation
|
|
45
|
+
schema.parameters && schema.parameters.forEach((p) => {
|
|
46
|
+
switch (p.in) {
|
|
47
|
+
case 'path':
|
|
48
|
+
reqSchema.properties.params.properties[p.name] = p.schema
|
|
49
|
+
p.required && !reqSchema.properties.params.required.includes(p.name) && reqSchema.properties.params.required.push(p.name)
|
|
50
|
+
break
|
|
51
|
+
case 'query':
|
|
52
|
+
reqSchema.properties.query.properties[p.name] = p.schema
|
|
53
|
+
p.required && !reqSchema.properties.query.required.includes(p.name) && reqSchema.properties.query.required.push(p.name)
|
|
54
|
+
break
|
|
55
|
+
case 'header': {
|
|
56
|
+
const name = p.name.toLowerCase()
|
|
57
|
+
reqSchema.properties.headers.properties[name] = p.schema
|
|
58
|
+
p.required && !reqSchema.properties.headers.required.includes(p.name) && reqSchema.properties.headers.required.push(name)
|
|
59
|
+
break
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// Compile req body schema
|
|
65
|
+
schema.requestBody && Object.entries(schema.requestBody.content)
|
|
66
|
+
.forEach(([contentType, { schema }]) => {
|
|
67
|
+
switch (contentType) {
|
|
68
|
+
case 'application/json':
|
|
69
|
+
reqSchema.properties.body = schema
|
|
70
|
+
break
|
|
71
|
+
default:
|
|
72
|
+
throw new TypeError(`Validation of content type not supported: ${contentType}`)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Add components for references
|
|
77
|
+
reqSchema.components = middleware.document && middleware.document.components
|
|
78
|
+
|
|
79
|
+
return ajv.compile(reqSchema)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return function validateMiddleware (req, res, next) {
|
|
83
|
+
// Restrict validation to only "route" layers
|
|
84
|
+
// This prevents running any validation
|
|
85
|
+
// if we are in a .use call which could
|
|
86
|
+
// be a non-routable request thus
|
|
87
|
+
if (!req.route) {
|
|
88
|
+
return next()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Create ajv instance on first request
|
|
92
|
+
if (!ajv) {
|
|
93
|
+
ajv = new Ajv({
|
|
94
|
+
coerceTypes: opts.coerce === 'false' ? opts.coerce : true,
|
|
95
|
+
strict: opts.strict === true ? opts.strict : false
|
|
96
|
+
})
|
|
97
|
+
addFormats(ajv)
|
|
98
|
+
|
|
99
|
+
if (middleware.router && !middleware.router.handle) {
|
|
100
|
+
return next()
|
|
101
|
+
}
|
|
102
|
+
if (opts.keywords) { addKeywords(ajv, opts.keywords) }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!validate) {
|
|
106
|
+
validate = makeValidator()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Validate request
|
|
110
|
+
let r = req
|
|
111
|
+
if (opts.coerce !== true) {
|
|
112
|
+
r = makeReqCopy(req)
|
|
113
|
+
} else {
|
|
114
|
+
// Redifine query and params as normal objects we can write to, so we can coerce the data
|
|
115
|
+
Object.defineProperty(req, 'query', {
|
|
116
|
+
value: { ...req.query },
|
|
117
|
+
writable: true,
|
|
118
|
+
enumerable: true,
|
|
119
|
+
configurable: true
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
Object.defineProperty(req, 'params', {
|
|
123
|
+
value: { ...req.params },
|
|
124
|
+
writable: true,
|
|
125
|
+
enumerable: true,
|
|
126
|
+
configurable: true
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
const validationStatus = validate(r)
|
|
130
|
+
if (validationStatus === true) {
|
|
131
|
+
return next()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// build error?
|
|
135
|
+
const err = new Error('Request validation failed')
|
|
136
|
+
err.validationErrors = validate.errors
|
|
137
|
+
err.validationSchema = validate.schema
|
|
138
|
+
next(httpErrors(400, err))
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// This is because ajv modifies the original data,
|
|
143
|
+
// preventing this requires that we dont pass the
|
|
144
|
+
// actual req. An issue has been opened (@TODO open the issue)
|
|
145
|
+
function makeReqCopy (req) {
|
|
146
|
+
return JSON.parse(JSON.stringify({
|
|
147
|
+
headers: req.headers,
|
|
148
|
+
params: req.params,
|
|
149
|
+
query: req.query,
|
|
150
|
+
body: req.body
|
|
151
|
+
}))
|
|
152
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type express from "express";
|
|
2
|
+
|
|
3
|
+
import {asyncHandler} from "./api";
|
|
4
|
+
import {VersionConfig} from "./models/versionConfig";
|
|
5
|
+
import type {TerrenoPlugin} from "./terrenoPlugin";
|
|
6
|
+
|
|
7
|
+
export type VersionCheckStatus = "ok" | "warning" | "required";
|
|
8
|
+
|
|
9
|
+
export interface VersionCheckResponse {
|
|
10
|
+
message?: string;
|
|
11
|
+
status: VersionCheckStatus;
|
|
12
|
+
updateUrl?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DEFAULT_WARNING_MESSAGE =
|
|
16
|
+
"A new version is available. Please update for the best experience.";
|
|
17
|
+
const DEFAULT_REQUIRED_MESSAGE = "This version is no longer supported. Please update to continue.";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* TerrenoPlugin that adds a public GET /version-check endpoint for upgrade enforcement.
|
|
21
|
+
* Compares client build number against admin-configured thresholds per platform.
|
|
22
|
+
*/
|
|
23
|
+
export class VersionCheckPlugin implements TerrenoPlugin {
|
|
24
|
+
register(app: express.Application): void {
|
|
25
|
+
app.get(
|
|
26
|
+
"/version-check",
|
|
27
|
+
asyncHandler(async (req, res) => {
|
|
28
|
+
const versionParam = req.query.version;
|
|
29
|
+
const platform = req.query.platform as string | undefined;
|
|
30
|
+
|
|
31
|
+
const version =
|
|
32
|
+
typeof versionParam === "string"
|
|
33
|
+
? parseInt(versionParam, 10)
|
|
34
|
+
: typeof versionParam === "number"
|
|
35
|
+
? versionParam
|
|
36
|
+
: undefined;
|
|
37
|
+
|
|
38
|
+
if (version === undefined || Number.isNaN(version)) {
|
|
39
|
+
return res.json({status: "ok" as VersionCheckStatus});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const platformNormalized = platform === "web" || platform === "mobile" ? platform : "web";
|
|
43
|
+
|
|
44
|
+
const config = await VersionConfig.findOneOrNone({_singleton: "config"});
|
|
45
|
+
|
|
46
|
+
if (!config) {
|
|
47
|
+
return res.json({status: "ok" as VersionCheckStatus});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const requiredVersion =
|
|
51
|
+
platformNormalized === "web"
|
|
52
|
+
? (config.webRequiredVersion ?? 0)
|
|
53
|
+
: (config.mobileRequiredVersion ?? 0);
|
|
54
|
+
const warningVersion =
|
|
55
|
+
platformNormalized === "web"
|
|
56
|
+
? (config.webWarningVersion ?? 0)
|
|
57
|
+
: (config.mobileWarningVersion ?? 0);
|
|
58
|
+
|
|
59
|
+
const response: VersionCheckResponse = {
|
|
60
|
+
status: "ok",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (requiredVersion > 0 && version < requiredVersion) {
|
|
64
|
+
response.status = "required";
|
|
65
|
+
response.message = config.requiredMessage ?? DEFAULT_REQUIRED_MESSAGE;
|
|
66
|
+
if (config.updateUrl) {
|
|
67
|
+
response.updateUrl = config.updateUrl;
|
|
68
|
+
}
|
|
69
|
+
} else if (warningVersion > 0 && version < warningVersion) {
|
|
70
|
+
response.status = "warning";
|
|
71
|
+
response.message = config.warningMessage ?? DEFAULT_WARNING_MESSAGE;
|
|
72
|
+
if (config.updateUrl) {
|
|
73
|
+
response.updateUrl = config.updateUrl;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return res.json(response);
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -56,5 +56,5 @@
|
|
|
56
56
|
"target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
|
|
57
57
|
},
|
|
58
58
|
"exclude": ["node_modules"],
|
|
59
|
-
"include": ["src/**/*.ts*", "src/*.ts", "types.d.ts"]
|
|
59
|
+
"include": ["src/**/*.ts*", "src/*.ts", "src/**/*.js", "types.d.ts"]
|
|
60
60
|
}
|