@foxtware/mineral 0.1.6 → 0.1.7
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/.creds.yml.sample +4 -1
- package/.env.sample +2 -0
- package/README.md +5 -0
- package/api/shopify/shopifyDecodeSessionToken.js +206 -0
- package/api/slack/slack.constants.js +5 -0
- package/api/slack/slack.utils.js +46 -0
- package/api/slack/slackMessagePost.js +85 -0
- package/api/utils.js +2 -4
- package/hosting/.hosting.yml.sample +3 -3
- package/hosting/deployFromHostingYml.js +2 -1
- package/hosting/generateHosted.js +32 -5
- package/hosting/hosting.utils.js +26 -21
- package/hosting/wrappers.js +1 -1
- package/package.json +1 -1
- package/server.js +1 -3
- package/server.utils.js +41 -7
package/.creds.yml.sample
CHANGED
package/.env.sample
ADDED
package/README.md
CHANGED
|
@@ -52,3 +52,8 @@ The [bedrock](https://github.com/GorgonFreeman/bedrock) middleware, refactored f
|
|
|
52
52
|
Mineral gets pushed to from a larger repo that can also contain private functions. Mineral should be strictly useful stuff for the public, and can be used standalone, but needs to be instantiated for serving, setting stuff like which creds file to use. This allows it to be used as part of another repo in the same HTTP/curl way as by itself. Pass `--workspace` to locate `.creds.yml` and `--api_dirs` to serve additional function directories.
|
|
53
53
|
|
|
54
54
|
For cloud deploy, workspaces use `hosting/.hosting.yml` and `npm run host` (same `--workspace` / `--api_dirs` flags as dev/serve). See `hosting/.hosting.yml.sample`.
|
|
55
|
+
|
|
56
|
+
## What the thang do
|
|
57
|
+
- Server makes functions available from the api/ route, where an export matches the filename. Run `npm run serve`, and they're all curlable.
|
|
58
|
+
- .creds.yml is copied into .env when deploying, so creds can be accessed while hosted. Locally, it reads from the file directly.
|
|
59
|
+
- Cloud deploy reads `hosting/.hosting.yml` for per-function config — `before_wrappers` / `after_wrappers` like `requireHostedApiKey`, `max_instances`, schedules — and deploys each function to Google Cloud.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// https://shopify.dev/docs/apps/build/authentication-authorization/session-tokens/set-up-session-tokens
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { credsFromPayload, ArgsWarden } = require('../utils');
|
|
5
|
+
const { credsValidator } = require('../validators');
|
|
6
|
+
|
|
7
|
+
const sessionTokenValidator = (sessionToken) => {
|
|
8
|
+
return typeof sessionToken === 'string' && Boolean(sessionToken.trim());
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const argsWarden = new ArgsWarden([
|
|
12
|
+
['credsPayload', credsValidator],
|
|
13
|
+
['sessionToken', sessionTokenValidator],
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
const base64UrlDecodeJson = (input) => {
|
|
17
|
+
const base64 = input.replace(/-/g, '+').replace(/_/g, '/');
|
|
18
|
+
const padding = '='.repeat((4 - base64.length % 4) % 4);
|
|
19
|
+
|
|
20
|
+
return JSON.parse(Buffer.from(`${ base64 }${ padding }`, 'base64').toString('utf8'));
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const normalizeSessionToken = (sessionToken) => {
|
|
24
|
+
const trimmedToken = sessionToken.trim();
|
|
25
|
+
|
|
26
|
+
if (trimmedToken.toLowerCase().startsWith('bearer ')) {
|
|
27
|
+
return trimmedToken.slice(7).trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return trimmedToken;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const verifySessionTokenSignature = (token, apiSecret) => {
|
|
34
|
+
const [encodedHeader, encodedPayload, signature] = token.split('.');
|
|
35
|
+
|
|
36
|
+
if (!encodedHeader || !encodedPayload || !signature) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const message = `${ encodedHeader }.${ encodedPayload }`;
|
|
41
|
+
const computedSignature = crypto
|
|
42
|
+
.createHmac('sha256', apiSecret)
|
|
43
|
+
.update(message)
|
|
44
|
+
.digest('base64url');
|
|
45
|
+
|
|
46
|
+
return computedSignature === signature;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const validateSessionTokenClaims = (
|
|
50
|
+
decoded,
|
|
51
|
+
{
|
|
52
|
+
checkAudience = true,
|
|
53
|
+
clientId,
|
|
54
|
+
} = {},
|
|
55
|
+
) => {
|
|
56
|
+
const {
|
|
57
|
+
iss,
|
|
58
|
+
dest,
|
|
59
|
+
aud,
|
|
60
|
+
exp,
|
|
61
|
+
nbf,
|
|
62
|
+
} = decoded;
|
|
63
|
+
|
|
64
|
+
const currentTime = Math.floor(Date.now() / 1000);
|
|
65
|
+
const expValid = exp > currentTime;
|
|
66
|
+
const nbfValid = nbf <= currentTime;
|
|
67
|
+
const domainsValid = dest?.includes('myshopify.com') && iss?.includes('myshopify.com');
|
|
68
|
+
const audValid = !checkAudience || aud === clientId;
|
|
69
|
+
|
|
70
|
+
return expValid && nbfValid && domainsValid && audValid;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const decodeSessionTokenPayload = (token) => {
|
|
74
|
+
const encodedPayload = token.split('.')[1];
|
|
75
|
+
|
|
76
|
+
if (!encodedPayload) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
return base64UrlDecodeJson(encodedPayload);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const shopifyDecodeSessionToken = async (
|
|
88
|
+
credsPayload,
|
|
89
|
+
sessionToken,
|
|
90
|
+
{
|
|
91
|
+
checkAudience = true,
|
|
92
|
+
} = {},
|
|
93
|
+
) => {
|
|
94
|
+
|
|
95
|
+
const rejectResponse = await argsWarden.responseIfRejectingArgs({
|
|
96
|
+
credsPayload,
|
|
97
|
+
sessionToken,
|
|
98
|
+
});
|
|
99
|
+
if (rejectResponse) {
|
|
100
|
+
return rejectResponse;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const creds = await credsFromPayload(credsPayload);
|
|
104
|
+
const {
|
|
105
|
+
API_SECRET,
|
|
106
|
+
CLIENT_ID,
|
|
107
|
+
} = creds;
|
|
108
|
+
|
|
109
|
+
if (!API_SECRET || !CLIENT_ID) {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
error: {
|
|
113
|
+
code: 'INVALID_CREDS',
|
|
114
|
+
message: 'API_SECRET and CLIENT_ID are required.',
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const token = normalizeSessionToken(sessionToken);
|
|
120
|
+
const decoded = decodeSessionTokenPayload(token);
|
|
121
|
+
|
|
122
|
+
if (!decoded) {
|
|
123
|
+
return {
|
|
124
|
+
ok: false,
|
|
125
|
+
error: {
|
|
126
|
+
code: 'INVALID_SESSION_TOKEN',
|
|
127
|
+
message: 'Session token could not be decoded.',
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!verifySessionTokenSignature(token, API_SECRET)) {
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
error: {
|
|
136
|
+
code: 'INVALID_SESSION_TOKEN',
|
|
137
|
+
message: 'Session token signature is invalid.',
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!validateSessionTokenClaims(decoded, {
|
|
143
|
+
checkAudience,
|
|
144
|
+
clientId: CLIENT_ID,
|
|
145
|
+
})) {
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
error: {
|
|
149
|
+
code: 'INVALID_SESSION_TOKEN',
|
|
150
|
+
message: 'Session token claims are invalid.',
|
|
151
|
+
details: decoded,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
ok: true,
|
|
158
|
+
data: decoded,
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const funcApiConfig = {
|
|
163
|
+
argsWarden,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
module.exports = {
|
|
167
|
+
shopifyDecodeSessionToken,
|
|
168
|
+
funcApiConfig,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/*
|
|
172
|
+
curl -X POST "http://localhost:8000/shopifyDecodeSessionToken" \
|
|
173
|
+
-H "Content-Type: application/json" \
|
|
174
|
+
-d '{
|
|
175
|
+
"credsPayload": { "credsPath": "tender.prod" },
|
|
176
|
+
"sessionToken": "<jwt>"
|
|
177
|
+
}'
|
|
178
|
+
*/
|
|
179
|
+
|
|
180
|
+
/*
|
|
181
|
+
Legacy usage:
|
|
182
|
+
const whichApp = req.headers['x-wf-app'];
|
|
183
|
+
|
|
184
|
+
const sessionToken = req?.headers?.authorization?.split(' ')?.pop();
|
|
185
|
+
if (!sessionToken) {
|
|
186
|
+
return respond(res, 401, {
|
|
187
|
+
error: 'Unauthorized: No session token',
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const sessionTokenData = shopifyDecodeSessionToken(sessionToken, {
|
|
192
|
+
...whichApp && { credsPath: whichApp },
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const {
|
|
196
|
+
dest,
|
|
197
|
+
sub: customerGid,
|
|
198
|
+
} = sessionTokenData;
|
|
199
|
+
|
|
200
|
+
const config = domainToConfig(dest);
|
|
201
|
+
if (!config) {
|
|
202
|
+
return respond(res, 401, {
|
|
203
|
+
error: 'No config found for domain',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
*/
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const { SLACK_API_BASE_URL } = require('../slack/slack.constants');
|
|
2
|
+
const {
|
|
3
|
+
FetchClient,
|
|
4
|
+
Chain,
|
|
5
|
+
appendUrlToBase,
|
|
6
|
+
fetchClientCommonSteps,
|
|
7
|
+
} = require('../utils');
|
|
8
|
+
|
|
9
|
+
const addUrlAndAuthHeaders = async (state) => {
|
|
10
|
+
const { requestPayload, context } = state;
|
|
11
|
+
const { creds } = context;
|
|
12
|
+
const {
|
|
13
|
+
BOT_TOKEN,
|
|
14
|
+
} = creds;
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
requestPayload: {
|
|
18
|
+
...requestPayload,
|
|
19
|
+
method: requestPayload.method || 'post',
|
|
20
|
+
url: appendUrlToBase(SLACK_API_BASE_URL, requestPayload.url),
|
|
21
|
+
headers: {
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
Authorization: `Bearer ${ BOT_TOKEN }`,
|
|
24
|
+
...requestPayload.headers,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
const slackClientRequestPreparer = new Chain([
|
|
32
|
+
addUrlAndAuthHeaders,
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const slackClientResponseInterpreter = new Chain([
|
|
36
|
+
fetchClientCommonSteps.exitEarlyOnNotOk,
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const slackClient = new FetchClient({
|
|
40
|
+
requestPreparer: slackClientRequestPreparer,
|
|
41
|
+
responseInterpreter: slackClientResponseInterpreter,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
slackClient,
|
|
46
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// https://docs.slack.dev/reference/methods/chat.postmessage
|
|
2
|
+
|
|
3
|
+
const { credsFromPayload, objHasAny, ArgsWarden } = require('../utils');
|
|
4
|
+
const { credsValidator } = require('../validators');
|
|
5
|
+
const { slackClient } = require('../slack/slack.utils');
|
|
6
|
+
|
|
7
|
+
const channelIdentifierValidator = (channelIdentifier) => {
|
|
8
|
+
return objHasAny(channelIdentifier, ['channelName', 'channelId']);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const messagePayloadValidator = (messagePayload) => {
|
|
12
|
+
return objHasAny(messagePayload, ['text', 'blocks', 'markdownText']);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const argsWarden = new ArgsWarden([
|
|
16
|
+
['credsPayload', credsValidator],
|
|
17
|
+
['channelIdentifier', channelIdentifierValidator],
|
|
18
|
+
['messagePayload', messagePayloadValidator],
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const slackMessagePost = async (
|
|
22
|
+
credsPayload,
|
|
23
|
+
channelIdentifier,
|
|
24
|
+
messagePayload,
|
|
25
|
+
{
|
|
26
|
+
inspect = false,
|
|
27
|
+
} = {},
|
|
28
|
+
) => {
|
|
29
|
+
|
|
30
|
+
const rejectResponse = await argsWarden.responseIfRejectingArgs({
|
|
31
|
+
credsPayload,
|
|
32
|
+
channelIdentifier,
|
|
33
|
+
messagePayload,
|
|
34
|
+
});
|
|
35
|
+
if (rejectResponse) {
|
|
36
|
+
return rejectResponse;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const creds = await credsFromPayload(credsPayload);
|
|
40
|
+
|
|
41
|
+
const {
|
|
42
|
+
channelName,
|
|
43
|
+
channelId,
|
|
44
|
+
} = channelIdentifier;
|
|
45
|
+
|
|
46
|
+
const {
|
|
47
|
+
text,
|
|
48
|
+
blocks,
|
|
49
|
+
markdownText,
|
|
50
|
+
} = messagePayload;
|
|
51
|
+
|
|
52
|
+
const response = await slackClient.fetch({
|
|
53
|
+
url: '/chat.postMessage',
|
|
54
|
+
method: 'post',
|
|
55
|
+
body: {
|
|
56
|
+
channel: channelId || channelName,
|
|
57
|
+
...text && { text },
|
|
58
|
+
...blocks && { blocks },
|
|
59
|
+
...markdownText && { markdown_text: markdownText },
|
|
60
|
+
},
|
|
61
|
+
context: { creds },
|
|
62
|
+
inspect,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return response;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const funcApiConfig = {
|
|
69
|
+
argsWarden,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
slackMessagePost,
|
|
74
|
+
funcApiConfig,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/*
|
|
78
|
+
curl -X POST "http://localhost:8000/slackMessagePost" \
|
|
79
|
+
-H "Content-Type: application/json" \
|
|
80
|
+
-d '{
|
|
81
|
+
"credsPayload": { "credsPath": "slack" },
|
|
82
|
+
"channelIdentifier": { "channelName": "#hidden_testing" },
|
|
83
|
+
"messagePayload": { "text": "new number, who dis?" }
|
|
84
|
+
}'
|
|
85
|
+
*/
|
package/api/utils.js
CHANGED
|
@@ -146,14 +146,14 @@ const customFetch = async (url, {
|
|
|
146
146
|
});
|
|
147
147
|
|
|
148
148
|
const responseContentType = response.headers.get('content-type');
|
|
149
|
-
console.log(responseContentType);
|
|
149
|
+
!HOSTED && console.log('responseContentType', responseContentType);
|
|
150
150
|
|
|
151
151
|
if (!responseParser) {
|
|
152
152
|
responseParser = getResponseParser(responseContentType);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
const parsedResponse = await responseParser(response);
|
|
156
|
-
logDeep({ parsedResponse });
|
|
156
|
+
!HOSTED && logDeep({ parsedResponse });
|
|
157
157
|
|
|
158
158
|
if (response.ok) {
|
|
159
159
|
return {
|
|
@@ -278,7 +278,6 @@ const pathAsArray = (path) => {
|
|
|
278
278
|
|
|
279
279
|
const objectDigNodeAtPath = (obj, path) => {
|
|
280
280
|
let nodes = pathAsArray(path);
|
|
281
|
-
console.log(nodes);
|
|
282
281
|
|
|
283
282
|
let output = obj;
|
|
284
283
|
for (const node of nodes) {
|
|
@@ -1161,7 +1160,6 @@ module.exports = {
|
|
|
1161
1160
|
actionSingleOrMultiple,
|
|
1162
1161
|
Processor,
|
|
1163
1162
|
Getter,
|
|
1164
|
-
capitaliseString,
|
|
1165
1163
|
sentenceCaseString,
|
|
1166
1164
|
ArgsWarden,
|
|
1167
1165
|
};
|
|
@@ -8,14 +8,14 @@ functions:
|
|
|
8
8
|
exampleFunction:
|
|
9
9
|
max_instances: 1
|
|
10
10
|
timeout: 300s
|
|
11
|
-
|
|
12
|
-
- requireHostedApiKey
|
|
11
|
+
before_wrappers:
|
|
13
12
|
- allowCrossOriginCallsAndHandleOptions
|
|
13
|
+
- requireHostedApiKey
|
|
14
14
|
# entry_point: otherHandlerName
|
|
15
15
|
|
|
16
16
|
pokemonPokeballThrow:
|
|
17
17
|
max_instances: 1
|
|
18
|
-
|
|
18
|
+
before_wrappers:
|
|
19
19
|
- checkTrainer
|
|
20
20
|
|
|
21
21
|
groups:
|
|
@@ -5,15 +5,33 @@ const formatResolvedWrapperForHostedJs = ({ modulePath, wrapperName }) => (
|
|
|
5
5
|
`require('${ modulePath }').${ wrapperName }`
|
|
6
6
|
);
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const formatWrapperList = (resolvedWrappers = []) => {
|
|
9
9
|
if (!resolvedWrappers.length) {
|
|
10
10
|
return '';
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const wrapperLines = resolvedWrappers.map((resolvedWrapper) => (
|
|
14
|
-
`
|
|
14
|
+
` ${ formatResolvedWrapperForHostedJs(resolvedWrapper) },`
|
|
15
15
|
));
|
|
16
|
-
|
|
16
|
+
|
|
17
|
+
return `\n${ wrapperLines.join('\n') }\n `;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const formatWrappersArg = ({
|
|
21
|
+
resolvedBeforeWrappers = [],
|
|
22
|
+
resolvedAfterWrappers = [],
|
|
23
|
+
} = {}) => {
|
|
24
|
+
if (!resolvedBeforeWrappers.length && !resolvedAfterWrappers.length) {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const beforeWrappersBlock = formatWrapperList(resolvedBeforeWrappers);
|
|
29
|
+
const afterWrappersBlock = formatWrapperList(resolvedAfterWrappers);
|
|
30
|
+
|
|
31
|
+
return `, {
|
|
32
|
+
beforeWrappers: [${ beforeWrappersBlock }],
|
|
33
|
+
afterWrappers: [${ afterWrappersBlock }],
|
|
34
|
+
}`;
|
|
17
35
|
};
|
|
18
36
|
|
|
19
37
|
const generateHostedJs = ({
|
|
@@ -22,8 +40,17 @@ const generateHostedJs = ({
|
|
|
22
40
|
const exportLines = [];
|
|
23
41
|
|
|
24
42
|
for (const hostedHandler of hostedHandlers) {
|
|
25
|
-
const {
|
|
26
|
-
|
|
43
|
+
const {
|
|
44
|
+
hostedName,
|
|
45
|
+
handlerName,
|
|
46
|
+
resolvedBeforeWrappers = [],
|
|
47
|
+
resolvedAfterWrappers = [],
|
|
48
|
+
requirePath,
|
|
49
|
+
} = hostedHandler;
|
|
50
|
+
const wrappersArg = formatWrappersArg({
|
|
51
|
+
resolvedBeforeWrappers,
|
|
52
|
+
resolvedAfterWrappers,
|
|
53
|
+
});
|
|
27
54
|
|
|
28
55
|
exportLines.push(
|
|
29
56
|
` ${ hostedName }: wrapHostedFunction(() => require('${ requirePath }'), '${ handlerName }'${ wrappersArg }),`,
|
package/hosting/hosting.utils.js
CHANGED
|
@@ -71,13 +71,16 @@ const getHostedEntries = (functions = {}) => (
|
|
|
71
71
|
Object.entries(functions).map(([hostedName, functionConfig = {}]) => ({
|
|
72
72
|
hostedName,
|
|
73
73
|
handlerName: functionConfig.entry_point || functionConfig.entryPoint || hostedName,
|
|
74
|
-
|
|
74
|
+
beforeWrappers: Array.isArray(functionConfig.before_wrappers) ? functionConfig.before_wrappers : [],
|
|
75
|
+
afterWrappers: Array.isArray(functionConfig.after_wrappers) ? functionConfig.after_wrappers : [],
|
|
75
76
|
}))
|
|
76
77
|
);
|
|
77
78
|
|
|
78
|
-
const functionUsesWrapper = (functionConfig = {}, wrapperName) =>
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
const functionUsesWrapper = (functionConfig = {}, wrapperName) => {
|
|
80
|
+
const { before_wrappers = [], after_wrappers = [] } = functionConfig;
|
|
81
|
+
|
|
82
|
+
return before_wrappers.includes(wrapperName) || after_wrappers.includes(wrapperName);
|
|
83
|
+
};
|
|
81
84
|
|
|
82
85
|
const getFuncApiConfig = ({
|
|
83
86
|
moduleExports,
|
|
@@ -100,7 +103,10 @@ const getFuncApiConfig = ({
|
|
|
100
103
|
}
|
|
101
104
|
};
|
|
102
105
|
|
|
103
|
-
const wrapHostedFunction = (loader, exportName,
|
|
106
|
+
const wrapHostedFunction = (loader, exportName, {
|
|
107
|
+
beforeWrappers = [],
|
|
108
|
+
afterWrappers = [],
|
|
109
|
+
} = {}) => {
|
|
104
110
|
let handler = null;
|
|
105
111
|
let usesFuncApi = false;
|
|
106
112
|
|
|
@@ -129,7 +135,10 @@ const wrapHostedFunction = (loader, exportName, wrappers = []) => {
|
|
|
129
135
|
: await handler(...args);
|
|
130
136
|
};
|
|
131
137
|
|
|
132
|
-
const wrappedHandler = wrapFunction(coreHandler,
|
|
138
|
+
const wrappedHandler = wrapFunction(coreHandler, {
|
|
139
|
+
beforeWrappers,
|
|
140
|
+
afterWrappers,
|
|
141
|
+
});
|
|
133
142
|
|
|
134
143
|
return async (req, res) => {
|
|
135
144
|
try {
|
|
@@ -180,17 +189,6 @@ const readHostingYml = (workspace) => {
|
|
|
180
189
|
return hostingConfig;
|
|
181
190
|
};
|
|
182
191
|
|
|
183
|
-
const getCredsJsonForDeploy = (workspace) => {
|
|
184
|
-
const credsPath = `${ workspace }/.creds.yml`;
|
|
185
|
-
|
|
186
|
-
if (!fs.existsSync(credsPath)) {
|
|
187
|
-
throw new Error(`Missing .creds.yml in workspace: ${ workspace }`);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const credsText = fs.readFileSync(credsPath, 'utf8');
|
|
191
|
-
return JSON.stringify(yaml.parse(credsText));
|
|
192
|
-
};
|
|
193
|
-
|
|
194
192
|
const ensureWorkspaceEnvForDeploy = (workspace) => {
|
|
195
193
|
const { copyCredsToEnv } = require('./copyCredsToEnv');
|
|
196
194
|
const envPath = `${ workspace.replace(/\/$/, '') }/.env`;
|
|
@@ -211,9 +209,16 @@ const resolveHostedHandlersForDeploy = ({
|
|
|
211
209
|
const hostedEntries = getHostedEntries(functions);
|
|
212
210
|
|
|
213
211
|
return hostedEntries.map((hostedEntry) => {
|
|
214
|
-
const {
|
|
212
|
+
const {
|
|
213
|
+
handlerName,
|
|
214
|
+
beforeWrappers = [],
|
|
215
|
+
afterWrappers = [],
|
|
216
|
+
} = hostedEntry;
|
|
215
217
|
|
|
216
|
-
const
|
|
218
|
+
const resolvedBeforeWrappers = beforeWrappers.map((wrapperName) => (
|
|
219
|
+
resolveWrapperName(wrapperName, workspaceRequire)
|
|
220
|
+
));
|
|
221
|
+
const resolvedAfterWrappers = afterWrappers.map((wrapperName) => (
|
|
217
222
|
resolveWrapperName(wrapperName, workspaceRequire)
|
|
218
223
|
));
|
|
219
224
|
|
|
@@ -226,7 +231,8 @@ const resolveHostedHandlersForDeploy = ({
|
|
|
226
231
|
|
|
227
232
|
return {
|
|
228
233
|
...hostedEntry,
|
|
229
|
-
|
|
234
|
+
resolvedBeforeWrappers,
|
|
235
|
+
resolvedAfterWrappers,
|
|
230
236
|
requirePath: getRequirePathForHandler(handler, workspace),
|
|
231
237
|
};
|
|
232
238
|
});
|
|
@@ -240,7 +246,6 @@ module.exports = {
|
|
|
240
246
|
getFuncApiConfig,
|
|
241
247
|
wrapHostedFunction,
|
|
242
248
|
readHostingYml,
|
|
243
|
-
getCredsJsonForDeploy,
|
|
244
249
|
ensureWorkspaceEnvForDeploy,
|
|
245
250
|
getHostedEntries,
|
|
246
251
|
resolveHostedHandlersForDeploy,
|
package/hosting/wrappers.js
CHANGED
|
@@ -22,7 +22,7 @@ const allowCrossOriginCallsAndHandleOptions = async (req, res) => {
|
|
|
22
22
|
|
|
23
23
|
res.setHeader('Access-Control-Allow-Origin', origin || '*');
|
|
24
24
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
25
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, x-api-key, x-wf-token, x-wf-value');
|
|
25
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-api-key, x-wf-token, x-wf-value');
|
|
26
26
|
|
|
27
27
|
if (req.method === 'OPTIONS') {
|
|
28
28
|
res.writeHead(204);
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -78,8 +78,6 @@ const directoriesToScan = ({
|
|
|
78
78
|
return [MINERAL_API_DIR, ...extraDirs];
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
-
const getFuncApiConfigFromModule = getFuncApiConfig;
|
|
82
|
-
|
|
83
81
|
const addHandlerFromFile = (filePath, handlers) => {
|
|
84
82
|
const moduleExports = require(filePath);
|
|
85
83
|
if (!moduleExports || typeof moduleExports !== 'object') {
|
|
@@ -92,7 +90,7 @@ const addHandlerFromFile = (filePath, handlers) => {
|
|
|
92
90
|
return;
|
|
93
91
|
}
|
|
94
92
|
|
|
95
|
-
const funcApiConfig =
|
|
93
|
+
const funcApiConfig = getFuncApiConfig({
|
|
96
94
|
moduleExports,
|
|
97
95
|
routeName,
|
|
98
96
|
});
|
package/server.utils.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
const { logDeep } = require('./api/utils');
|
|
2
|
+
const { HOSTED } = require('./api/constants');
|
|
2
3
|
const { StringDecoder } = require('string_decoder');
|
|
3
4
|
|
|
4
5
|
const respondJson = (res, statusCode, payload) => {
|
|
5
|
-
logDeep(payload);
|
|
6
|
+
!HOSTED && logDeep(payload);
|
|
6
7
|
const body = JSON.stringify(payload);
|
|
7
8
|
res.writeHead(statusCode, {
|
|
8
9
|
'Content-Type': 'application/json',
|
|
@@ -120,15 +121,48 @@ const runRequestHandler = async (requestHandler, requestContext) => {
|
|
|
120
121
|
return mergeRequestContext(requestContext, handlerOutput);
|
|
121
122
|
};
|
|
122
123
|
|
|
123
|
-
const wrapFunction = (func,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
const wrapFunction = (func, {
|
|
125
|
+
beforeWrappers = [],
|
|
126
|
+
afterWrappers = [],
|
|
127
|
+
} = {}) => async (req, res, ...rest) => {
|
|
128
|
+
for (const beforeWrapper of beforeWrappers) {
|
|
129
|
+
const beforeResult = await beforeWrapper(req, res);
|
|
130
|
+
if (beforeResult?.handled) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (beforeResult) {
|
|
134
|
+
return beforeResult;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!afterWrappers.length) {
|
|
139
|
+
return func(req, res, ...rest);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let result;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
result = await func(req, res, ...rest);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
result = {
|
|
148
|
+
ok: false,
|
|
149
|
+
error: {
|
|
150
|
+
code: 'UNHANDLED_ERROR',
|
|
151
|
+
message: 'Unhandled server error.',
|
|
152
|
+
details: errorToReadable(error),
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const afterWrapper of afterWrappers) {
|
|
158
|
+
try {
|
|
159
|
+
await afterWrapper(req, res, result);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.log('wrapFunction afterWrapper error', error);
|
|
128
162
|
}
|
|
129
163
|
}
|
|
130
164
|
|
|
131
|
-
return
|
|
165
|
+
return result;
|
|
132
166
|
};
|
|
133
167
|
|
|
134
168
|
const statusCodeFromResult = (result) => {
|