quidproquo-webserver 0.1.1 → 0.1.3
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/commonjs/actionProcessor/api/getHttpApiEventAutoRespondActionProcessor.js +20 -0
- package/lib/commonjs/config/QPQConfig.d.ts +2 -1
- package/lib/commonjs/config/QPQConfig.js +1 -0
- package/lib/commonjs/config/settings/api.d.ts +2 -0
- package/lib/commonjs/config/settings/api.js +1 -0
- package/lib/commonjs/config/settings/fileUploadSettings.d.ts +24 -0
- package/lib/commonjs/config/settings/fileUploadSettings.js +14 -0
- package/lib/commonjs/config/settings/index.d.ts +1 -0
- package/lib/commonjs/config/settings/index.js +1 -0
- package/lib/commonjs/config/settings/serviceFunction.d.ts +2 -0
- package/lib/commonjs/config/settings/serviceFunction.js +1 -0
- package/lib/commonjs/config/settings/websocket.d.ts +2 -0
- package/lib/commonjs/config/settings/websocket.js +1 -0
- package/lib/commonjs/services/webSocketQueue/logic/webSocket/messageProcessors/askProcessOnAuthenticate.js +27 -20
- package/lib/commonjs/types/HTTPEvent.d.ts +12 -0
- package/lib/commonjs/types/HTTPEvent.js +9 -0
- package/lib/commonjs/utils/httpEventUtils.d.ts +8 -0
- package/lib/commonjs/utils/httpEventUtils.js +25 -1
- package/lib/commonjs/utils/networkRequestUtils.js +1 -1
- package/lib/commonjs/utils/qpqConfigAccessorsUtils.d.ts +10 -1
- package/lib/commonjs/utils/qpqConfigAccessorsUtils.js +24 -1
- package/lib/esm/actionProcessor/api/getHttpApiEventAutoRespondActionProcessor.js +20 -0
- package/lib/esm/config/QPQConfig.d.ts +2 -1
- package/lib/esm/config/QPQConfig.js +1 -0
- package/lib/esm/config/settings/api.d.ts +2 -0
- package/lib/esm/config/settings/api.js +1 -0
- package/lib/esm/config/settings/fileUploadSettings.d.ts +24 -0
- package/lib/esm/config/settings/fileUploadSettings.js +10 -0
- package/lib/esm/config/settings/index.d.ts +1 -0
- package/lib/esm/config/settings/index.js +1 -0
- package/lib/esm/config/settings/serviceFunction.d.ts +2 -0
- package/lib/esm/config/settings/serviceFunction.js +1 -0
- package/lib/esm/config/settings/websocket.d.ts +2 -0
- package/lib/esm/config/settings/websocket.js +1 -0
- package/lib/esm/services/webSocketQueue/logic/webSocket/messageProcessors/askProcessOnAuthenticate.js +31 -24
- package/lib/esm/types/HTTPEvent.d.ts +12 -0
- package/lib/esm/types/HTTPEvent.js +8 -1
- package/lib/esm/utils/httpEventUtils.d.ts +8 -0
- package/lib/esm/utils/httpEventUtils.js +22 -0
- package/lib/esm/utils/networkRequestUtils.js +1 -1
- package/lib/esm/utils/qpqConfigAccessorsUtils.d.ts +10 -1
- package/lib/esm/utils/qpqConfigAccessorsUtils.js +24 -0
- package/package.json +3 -3
|
@@ -12,7 +12,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.getHttpApiEventAutoRespondActionProcessor = void 0;
|
|
13
13
|
const quidproquo_core_1 = require("quidproquo-core");
|
|
14
14
|
const askValidateRouteAuth_1 = require("../../stories/askValidateRouteAuth");
|
|
15
|
+
const HTTPEvent_1 = require("../../types/HTTPEvent");
|
|
15
16
|
const headerUtils_1 = require("../../utils/headerUtils");
|
|
17
|
+
const fileUploadErrorHttpStatusMap = {
|
|
18
|
+
[HTTPEvent_1.FileUploadErrorTypeEnum.fileTooLarge]: 413,
|
|
19
|
+
[HTTPEvent_1.FileUploadErrorTypeEnum.tooManyFiles]: 413,
|
|
20
|
+
[HTTPEvent_1.FileUploadErrorTypeEnum.tooManyFields]: 400,
|
|
21
|
+
[HTTPEvent_1.FileUploadErrorTypeEnum.disallowedMimeType]: 415,
|
|
22
|
+
[HTTPEvent_1.FileUploadErrorTypeEnum.malformed]: 400,
|
|
23
|
+
};
|
|
16
24
|
const getProcessAutoRespond = (qpqConfig) => {
|
|
17
25
|
const validateAuth = (0, quidproquo_core_1.getProcessCustomImplementation)(qpqConfig, askValidateRouteAuth_1.askValidateRouteAuth, 'API Auth Validation', null, () => new Date().toISOString(), quidproquo_core_1.generateUuid);
|
|
18
26
|
return (_a, session_1, actionProcessorList_1, logger_1, updateSession_1, dynamicModuleLoader_1) => __awaiter(void 0, [_a, session_1, actionProcessorList_1, logger_1, updateSession_1, dynamicModuleLoader_1], void 0, function* ({ qpqEventRecord, matchResult }, session, actionProcessorList, logger, updateSession, dynamicModuleLoader) {
|
|
@@ -41,6 +49,18 @@ const getProcessAutoRespond = (qpqConfig) => {
|
|
|
41
49
|
headers: (0, headerUtils_1.getCorsHeaders)(qpqConfig, matchResult.config || {}, qpqEventRecord.headers),
|
|
42
50
|
});
|
|
43
51
|
}
|
|
52
|
+
// Reject invalid multipart uploads before the route story runs (after auth, so a 401 wins)
|
|
53
|
+
if (qpqEventRecord.fileUploadError) {
|
|
54
|
+
return (0, quidproquo_core_1.actionResult)({
|
|
55
|
+
status: fileUploadErrorHttpStatusMap[qpqEventRecord.fileUploadError.errorType] || 400,
|
|
56
|
+
isBase64Encoded: false,
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
errorType: qpqEventRecord.fileUploadError.errorType,
|
|
59
|
+
errorText: qpqEventRecord.fileUploadError.message,
|
|
60
|
+
}),
|
|
61
|
+
headers: (0, headerUtils_1.getCorsHeaders)(qpqConfig, matchResult.config || {}, qpqEventRecord.headers),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
44
64
|
return (0, quidproquo_core_1.actionResult)(null);
|
|
45
65
|
});
|
|
46
66
|
};
|
|
@@ -13,7 +13,8 @@ export declare enum QPQWebServerConfigSettingType {
|
|
|
13
13
|
Cache = "@quidproquo-webserver/config/Cache",
|
|
14
14
|
Certificate = "@quidproquo-webserver/config/Certificate",
|
|
15
15
|
DomainProxy = "@quidproquo-webserver/config/DomainProxy",
|
|
16
|
-
StorageDriveCorsSettings = "@quidproquo-webserver/config/StorageDriveCorsSettings"
|
|
16
|
+
StorageDriveCorsSettings = "@quidproquo-webserver/config/StorageDriveCorsSettings",
|
|
17
|
+
FileUploadSettings = "@quidproquo-webserver/config/FileUploadSettings"
|
|
17
18
|
}
|
|
18
19
|
export interface CacheSettings {
|
|
19
20
|
minTTLInSeconds: number;
|
|
@@ -18,4 +18,5 @@ var QPQWebServerConfigSettingType;
|
|
|
18
18
|
QPQWebServerConfigSettingType["Certificate"] = "@quidproquo-webserver/config/Certificate";
|
|
19
19
|
QPQWebServerConfigSettingType["DomainProxy"] = "@quidproquo-webserver/config/DomainProxy";
|
|
20
20
|
QPQWebServerConfigSettingType["StorageDriveCorsSettings"] = "@quidproquo-webserver/config/StorageDriveCorsSettings";
|
|
21
|
+
QPQWebServerConfigSettingType["FileUploadSettings"] = "@quidproquo-webserver/config/FileUploadSettings";
|
|
21
22
|
})(QPQWebServerConfigSettingType || (exports.QPQWebServerConfigSettingType = QPQWebServerConfigSettingType = {}));
|
|
@@ -3,6 +3,7 @@ export interface QPQConfigAdvancedApiSettings extends QPQConfigAdvancedSettings
|
|
|
3
3
|
subDomain?: string;
|
|
4
4
|
cloudflareApiKeySecretName?: string;
|
|
5
5
|
virtualNetworkName?: string;
|
|
6
|
+
maxConcurrentExecutions?: number;
|
|
6
7
|
}
|
|
7
8
|
export interface ApiQPQWebServerConfigSetting extends QPQConfigSetting {
|
|
8
9
|
apiSubdomain: string;
|
|
@@ -11,5 +12,6 @@ export interface ApiQPQWebServerConfigSetting extends QPQConfigSetting {
|
|
|
11
12
|
deprecated: boolean;
|
|
12
13
|
cloudflareApiKeySecretName?: string;
|
|
13
14
|
virtualNetworkName?: string;
|
|
15
|
+
maxConcurrentExecutions?: number;
|
|
14
16
|
}
|
|
15
17
|
export declare const defineApi: (apiName: string, rootDomain: string, options?: QPQConfigAdvancedApiSettings) => ApiQPQWebServerConfigSetting;
|
|
@@ -12,6 +12,7 @@ const defineApi = (apiName, rootDomain, options) => {
|
|
|
12
12
|
deprecated: (options === null || options === void 0 ? void 0 : options.deprecated) || false,
|
|
13
13
|
cloudflareApiKeySecretName: options === null || options === void 0 ? void 0 : options.cloudflareApiKeySecretName,
|
|
14
14
|
virtualNetworkName: options === null || options === void 0 ? void 0 : options.virtualNetworkName,
|
|
15
|
+
maxConcurrentExecutions: options === null || options === void 0 ? void 0 : options.maxConcurrentExecutions,
|
|
15
16
|
};
|
|
16
17
|
};
|
|
17
18
|
exports.defineApi = defineApi;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { QPQConfigSetting } from 'quidproquo-core';
|
|
2
|
+
export interface FileUploadSettings {
|
|
3
|
+
/** Maximum size of any single uploaded file. Uploads with a larger file are rejected with a 413. */
|
|
4
|
+
maxFileSizeBytes: number;
|
|
5
|
+
/** Maximum number of files in a single multipart request. */
|
|
6
|
+
maxFileCount: number;
|
|
7
|
+
/** Maximum number of non-file fields in a single multipart request. */
|
|
8
|
+
maxFieldCount: number;
|
|
9
|
+
/** Maximum size of any single non-file field value. Larger values are truncated by the parser. */
|
|
10
|
+
maxFieldSizeBytes: number;
|
|
11
|
+
/**
|
|
12
|
+
* Content types accepted for uploaded files, e.g. `['image/*', 'application/pdf']`
|
|
13
|
+
* (`type/*` wildcards supported). Omit to accept any content type.
|
|
14
|
+
*/
|
|
15
|
+
allowedMimeTypes?: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface FileUploadSettingsQPQWebServerConfigSetting extends QPQConfigSetting {
|
|
18
|
+
fileUploadSettings: Partial<FileUploadSettings>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Service-wide limits for multipart/form-data file uploads. Sensible defaults
|
|
22
|
+
* apply even when this setting is not declared — declare it only to override them.
|
|
23
|
+
*/
|
|
24
|
+
export declare const defineFileUploadSettings: (fileUploadSettings: Partial<FileUploadSettings>) => FileUploadSettingsQPQWebServerConfigSetting;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.defineFileUploadSettings = void 0;
|
|
4
|
+
const QPQConfig_1 = require("../QPQConfig");
|
|
5
|
+
/**
|
|
6
|
+
* Service-wide limits for multipart/form-data file uploads. Sensible defaults
|
|
7
|
+
* apply even when this setting is not declared — declare it only to override them.
|
|
8
|
+
*/
|
|
9
|
+
const defineFileUploadSettings = (fileUploadSettings) => ({
|
|
10
|
+
configSettingType: QPQConfig_1.QPQWebServerConfigSettingType.FileUploadSettings,
|
|
11
|
+
uniqueKey: 'fileUploadSettings',
|
|
12
|
+
fileUploadSettings,
|
|
13
|
+
});
|
|
14
|
+
exports.defineFileUploadSettings = defineFileUploadSettings;
|
|
@@ -8,6 +8,7 @@ export * from './defineAdminSettings';
|
|
|
8
8
|
export * from './defineAuthSystem';
|
|
9
9
|
export * from './dns';
|
|
10
10
|
export * from './domainProxy';
|
|
11
|
+
export * from './fileUploadSettings';
|
|
11
12
|
export * from './migration';
|
|
12
13
|
export * from './openApi';
|
|
13
14
|
export * from './route';
|
|
@@ -24,6 +24,7 @@ __exportStar(require("./defineAdminSettings"), exports);
|
|
|
24
24
|
__exportStar(require("./defineAuthSystem"), exports);
|
|
25
25
|
__exportStar(require("./dns"), exports);
|
|
26
26
|
__exportStar(require("./domainProxy"), exports);
|
|
27
|
+
__exportStar(require("./fileUploadSettings"), exports);
|
|
27
28
|
__exportStar(require("./migration"), exports);
|
|
28
29
|
__exportStar(require("./openApi"), exports);
|
|
29
30
|
__exportStar(require("./route"), exports);
|
|
@@ -2,11 +2,13 @@ import { CrossModuleOwner, QPQConfigAdvancedSettings, QPQConfigSetting, QpqFunct
|
|
|
2
2
|
export interface QPQConfigAdvancedServiceFunctionSettings extends QPQConfigAdvancedSettings {
|
|
3
3
|
functionName?: string;
|
|
4
4
|
virtualNetworkName?: string;
|
|
5
|
+
maxConcurrentExecutions?: number;
|
|
5
6
|
owner?: CrossModuleOwner<'functionName'>;
|
|
6
7
|
}
|
|
7
8
|
export interface ServiceFunctionQPQWebServerConfigSetting extends QPQConfigSetting {
|
|
8
9
|
runtime: QpqFunctionRuntime;
|
|
9
10
|
functionName: string;
|
|
10
11
|
virtualNetworkName?: string;
|
|
12
|
+
maxConcurrentExecutions?: number;
|
|
11
13
|
}
|
|
12
14
|
export declare const defineServiceFunction: (runtime: QpqFunctionRuntime, options?: QPQConfigAdvancedServiceFunctionSettings) => ServiceFunctionQPQWebServerConfigSetting;
|
|
@@ -11,6 +11,7 @@ const defineServiceFunction = (runtime, options) => {
|
|
|
11
11
|
runtime,
|
|
12
12
|
functionName: functionName,
|
|
13
13
|
virtualNetworkName: options === null || options === void 0 ? void 0 : options.virtualNetworkName,
|
|
14
|
+
maxConcurrentExecutions: options === null || options === void 0 ? void 0 : options.maxConcurrentExecutions,
|
|
14
15
|
owner: quidproquo_core_1.qpqCoreUtils.convertCrossModuleOwnerToGenericResourceNameOverride(options === null || options === void 0 ? void 0 : options.owner),
|
|
15
16
|
};
|
|
16
17
|
};
|
|
@@ -8,6 +8,7 @@ export interface QPQConfigAdvancedWebSocketSettings extends QPQConfigAdvancedSet
|
|
|
8
8
|
onRootDomain?: boolean;
|
|
9
9
|
apiName?: string;
|
|
10
10
|
cloudflareApiKeySecretName?: string;
|
|
11
|
+
maxConcurrentExecutions?: number;
|
|
11
12
|
owner?: CrossModuleOwner<'websocketApiName'>;
|
|
12
13
|
}
|
|
13
14
|
export interface WebSocketQPQWebServerConfigSetting extends QPQConfigSetting {
|
|
@@ -18,5 +19,6 @@ export interface WebSocketQPQWebServerConfigSetting extends QPQConfigSetting {
|
|
|
18
19
|
eventProcessors: QpqWebSocketEventProcessors;
|
|
19
20
|
deprecated: boolean;
|
|
20
21
|
cloudflareApiKeySecretName?: string;
|
|
22
|
+
maxConcurrentExecutions?: number;
|
|
21
23
|
}
|
|
22
24
|
export declare const defineWebsocket: (apiSubdomain: string, rootDomain: string, eventProcessors: QpqWebSocketEventProcessors, options?: QPQConfigAdvancedWebSocketSettings) => WebSocketQPQWebServerConfigSetting;
|
|
@@ -15,6 +15,7 @@ const defineWebsocket = (apiSubdomain, rootDomain, eventProcessors, options) =>
|
|
|
15
15
|
apiName: (options === null || options === void 0 ? void 0 : options.apiName) || 'api',
|
|
16
16
|
deprecated: (options === null || options === void 0 ? void 0 : options.deprecated) || false,
|
|
17
17
|
cloudflareApiKeySecretName: options === null || options === void 0 ? void 0 : options.cloudflareApiKeySecretName,
|
|
18
|
+
maxConcurrentExecutions: options === null || options === void 0 ? void 0 : options.maxConcurrentExecutions,
|
|
18
19
|
owner: quidproquo_core_1.qpqCoreUtils.convertCrossModuleOwnerToGenericResourceNameOverride(options === null || options === void 0 ? void 0 : options.owner),
|
|
19
20
|
};
|
|
20
21
|
};
|
|
@@ -14,28 +14,35 @@ function isWebSocketAuthenticateMessage(event) {
|
|
|
14
14
|
}
|
|
15
15
|
function* askProcessOnAuthenticate(connectionId, accessToken) {
|
|
16
16
|
const connection = yield* data_1.webSocketConnectionData.askGetById(connectionId);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
17
|
+
// No connection record (e.g. the connect event hasn't been processed yet) —
|
|
18
|
+
// tell the client rather than dropping the request silently, so it can
|
|
19
|
+
// distinguish "not authenticated" from "no reply".
|
|
20
|
+
if (!connection) {
|
|
21
|
+
yield* (0, askSendMessage_1.askSendMessage)(connectionId, {
|
|
22
|
+
type: types_1.WebSocketQueueServerMessageEventType.Unauthenticated,
|
|
23
|
+
});
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const apiName = yield* (0, context_1.askWebsocketReadApiNameOrThrow)();
|
|
27
|
+
const userDirectoryName = yield* (0, quidproquo_core_1.askConfigGetGlobal)((0, config_1.getWebSocketQueueGlobalConfigKeyForUserDirectoryName)(apiName));
|
|
28
|
+
if (userDirectoryName) {
|
|
29
|
+
const result = yield* (0, quidproquo_core_1.askCatch)((0, quidproquo_core_1.askUserDirectorySetAccessToken)(userDirectoryName, accessToken));
|
|
30
|
+
if (!result.success) {
|
|
30
31
|
yield* (0, askSendMessage_1.askSendMessage)(connectionId, {
|
|
31
|
-
type: types_1.WebSocketQueueServerMessageEventType.
|
|
32
|
+
type: types_1.WebSocketQueueServerMessageEventType.Unauthenticated,
|
|
32
33
|
});
|
|
33
|
-
|
|
34
|
-
const webSocketQueueClientEventMessageAuthenticate = {
|
|
35
|
-
type: types_1.WebSocketQueueClientMessageEventType.Authenticate,
|
|
36
|
-
payload: {},
|
|
37
|
-
};
|
|
38
|
-
yield* (0, askBroadcastUnknownMessage_1.askBroadcastUnknownMessage)(webSocketQueueClientEventMessageAuthenticate);
|
|
34
|
+
return;
|
|
39
35
|
}
|
|
36
|
+
const decodedAccessToken = result.result;
|
|
37
|
+
yield* data_1.webSocketConnectionData.askUpsert(Object.assign(Object.assign({}, connection), { userId: decodedAccessToken.userId, accessToken }));
|
|
38
|
+
yield* (0, askSendMessage_1.askSendMessage)(connectionId, {
|
|
39
|
+
type: types_1.WebSocketQueueServerMessageEventType.Authenticated,
|
|
40
|
+
});
|
|
41
|
+
// Send a websocket message to the event buss WITHOUT an access token
|
|
42
|
+
const webSocketQueueClientEventMessageAuthenticate = {
|
|
43
|
+
type: types_1.WebSocketQueueClientMessageEventType.Authenticate,
|
|
44
|
+
payload: {},
|
|
45
|
+
};
|
|
46
|
+
yield* (0, askBroadcastUnknownMessage_1.askBroadcastUnknownMessage)(webSocketQueueClientEventMessageAuthenticate);
|
|
40
47
|
}
|
|
41
48
|
}
|
|
@@ -5,6 +5,17 @@ export interface HttpEventHeaders {
|
|
|
5
5
|
export interface HttpEventRouteParams {
|
|
6
6
|
[key: string]: string;
|
|
7
7
|
}
|
|
8
|
+
export declare enum FileUploadErrorTypeEnum {
|
|
9
|
+
fileTooLarge = "fileTooLarge",
|
|
10
|
+
tooManyFiles = "tooManyFiles",
|
|
11
|
+
tooManyFields = "tooManyFields",
|
|
12
|
+
disallowedMimeType = "disallowedMimeType",
|
|
13
|
+
malformed = "malformed"
|
|
14
|
+
}
|
|
15
|
+
export interface HTTPEventFileUploadError {
|
|
16
|
+
errorType: FileUploadErrorTypeEnum;
|
|
17
|
+
message: string;
|
|
18
|
+
}
|
|
8
19
|
export interface HTTPEvent {
|
|
9
20
|
path: string;
|
|
10
21
|
query: {
|
|
@@ -17,6 +28,7 @@ export interface HTTPEvent {
|
|
|
17
28
|
sourceIp: string;
|
|
18
29
|
isBase64Encoded: boolean;
|
|
19
30
|
files?: QPQBinaryData[];
|
|
31
|
+
fileUploadError?: HTTPEventFileUploadError;
|
|
20
32
|
}
|
|
21
33
|
export interface HTTPEventResponse {
|
|
22
34
|
status: number;
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FileUploadErrorTypeEnum = void 0;
|
|
4
|
+
var FileUploadErrorTypeEnum;
|
|
5
|
+
(function (FileUploadErrorTypeEnum) {
|
|
6
|
+
FileUploadErrorTypeEnum["fileTooLarge"] = "fileTooLarge";
|
|
7
|
+
FileUploadErrorTypeEnum["tooManyFiles"] = "tooManyFiles";
|
|
8
|
+
FileUploadErrorTypeEnum["tooManyFields"] = "tooManyFields";
|
|
9
|
+
FileUploadErrorTypeEnum["disallowedMimeType"] = "disallowedMimeType";
|
|
10
|
+
FileUploadErrorTypeEnum["malformed"] = "malformed";
|
|
11
|
+
})(FileUploadErrorTypeEnum || (exports.FileUploadErrorTypeEnum = FileUploadErrorTypeEnum = {}));
|
|
3
12
|
// type ParseParamType<S extends string> = S extends `${infer ParamName}:number`
|
|
4
13
|
// ? { name: ParamName; type: number }
|
|
5
14
|
// : S extends `${infer ParamName}:int`
|
|
@@ -4,6 +4,14 @@ import { HTTPEvent, HTTPEventResponse } from '../types/HTTPEvent';
|
|
|
4
4
|
export declare const rawFromJsonEventRequest: (httpJsonEvent: HTTPEvent) => string | undefined;
|
|
5
5
|
export declare const fromJsonEventRequest: <T>(httpJsonEvent: HTTPEvent) => T;
|
|
6
6
|
export declare function askFromJsonEventRequest<T>(httpJsonEvent: HTTPEvent): AskResponse<T>;
|
|
7
|
+
/**
|
|
8
|
+
* Like `askFromJsonEventRequest`, but the parsed body is run through an app-supplied
|
|
9
|
+
* validator before it is returned - so the `T` is actually checked, not just cast.
|
|
10
|
+
* The validator throws (or returns the typed value); a validation throw becomes an
|
|
11
|
+
* `Invalid` (422) response. Any schema library fits, e.g. zod: `(data) => schema.parse(data)`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function askFromValidJsonEventRequest<T>(httpJsonEvent: HTTPEvent, validate: (data: unknown) => T): AskResponse<T>;
|
|
14
|
+
export declare const readUriQueryParamFromEvent: (event: HTTPEvent, paramName: string) => string | undefined;
|
|
7
15
|
export declare const toJsonEventResponse: (item: any, status?: number) => HTTPEventResponse;
|
|
8
16
|
export declare const toHtmlResponse: (html: string, status?: number) => HTTPEventResponse;
|
|
9
17
|
export declare const toTextResponse: (text: string, status?: number) => HTTPEventResponse;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.toCdnResponse = exports.toMovedTemporarilyRedirectResponse = exports.toMovedPermanentlyRedirectResponse = exports.toTextResponse = exports.toHtmlResponse = exports.toJsonEventResponse = exports.fromJsonEventRequest = exports.rawFromJsonEventRequest = void 0;
|
|
3
|
+
exports.toCdnResponse = exports.toMovedTemporarilyRedirectResponse = exports.toMovedPermanentlyRedirectResponse = exports.toTextResponse = exports.toHtmlResponse = exports.toJsonEventResponse = exports.readUriQueryParamFromEvent = exports.fromJsonEventRequest = exports.rawFromJsonEventRequest = void 0;
|
|
4
4
|
exports.askFromJsonEventRequest = askFromJsonEventRequest;
|
|
5
|
+
exports.askFromValidJsonEventRequest = askFromValidJsonEventRequest;
|
|
5
6
|
const quidproquo_core_1 = require("quidproquo-core");
|
|
6
7
|
const rawFromJsonEventRequest = (httpJsonEvent) => {
|
|
7
8
|
const json = httpJsonEvent.isBase64Encoded && httpJsonEvent.body ? Buffer.from(httpJsonEvent.body, 'base64').toString() : httpJsonEvent.body;
|
|
@@ -39,6 +40,29 @@ function* askFromJsonEventRequest(httpJsonEvent) {
|
|
|
39
40
|
return yield* (0, quidproquo_core_1.askThrowError)(quidproquo_core_1.ErrorTypeEnum.BadRequest, 'Unable to parse incoming json from HTTPEvent.');
|
|
40
41
|
}
|
|
41
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Like `askFromJsonEventRequest`, but the parsed body is run through an app-supplied
|
|
45
|
+
* validator before it is returned - so the `T` is actually checked, not just cast.
|
|
46
|
+
* The validator throws (or returns the typed value); a validation throw becomes an
|
|
47
|
+
* `Invalid` (422) response. Any schema library fits, e.g. zod: `(data) => schema.parse(data)`.
|
|
48
|
+
*/
|
|
49
|
+
function* askFromValidJsonEventRequest(httpJsonEvent, validate) {
|
|
50
|
+
const parsedBody = yield* askFromJsonEventRequest(httpJsonEvent);
|
|
51
|
+
try {
|
|
52
|
+
return validate(parsedBody);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
return yield* (0, quidproquo_core_1.askThrowError)(quidproquo_core_1.ErrorTypeEnum.Invalid, error instanceof Error ? error.message : 'Invalid request body.');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const readUriQueryParamFromEvent = (event, paramName) => {
|
|
59
|
+
const rawValue = event.query[paramName];
|
|
60
|
+
if (!rawValue) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
return Array.isArray(rawValue) ? rawValue[0] : rawValue;
|
|
64
|
+
};
|
|
65
|
+
exports.readUriQueryParamFromEvent = readUriQueryParamFromEvent;
|
|
42
66
|
const toJsonEventResponse = (item, status = 200) => {
|
|
43
67
|
return {
|
|
44
68
|
status,
|
|
@@ -22,7 +22,7 @@ const isAbsoluteUrl = (url) => /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url);
|
|
|
22
22
|
// Join basePath + url like axios' combineURLs (path concatenation), NOT URL
|
|
23
23
|
// resolution. `new URL('/v1/x', 'http://host/api/svc')` drops the base path
|
|
24
24
|
// because a leading-slash url is root-absolute; we want '.../api/svc/v1/x'.
|
|
25
|
-
const combineUrls = (basePath, url) => url ? `${basePath.replace(/\/+$/, '')}/${url.replace(/^\/+/, '')}` : basePath;
|
|
25
|
+
const combineUrls = (basePath, url) => (url ? `${basePath.replace(/\/+$/, '')}/${url.replace(/^\/+/, '')}` : basePath);
|
|
26
26
|
const buildRequestUrl = (payload) => {
|
|
27
27
|
const fullUrl = payload.basePath && !isAbsoluteUrl(payload.url) ? combineUrls(payload.basePath, payload.url) : payload.url;
|
|
28
28
|
const url = new URL(fullUrl);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { QPQConfig, QpqFunctionRuntime } from 'quidproquo-core';
|
|
2
|
-
import { ApiKeyQPQWebServerConfigSetting, CacheQPQWebServerConfigSetting, CertificateQPQWebServerConfigSetting, DefaultRouteOptionsQPQWebServerConfigSetting, DnsQPQWebServerConfigSetting, DomainProxyQPQWebServerConfigSetting, OpenApiQPQWebServerConfigSetting, RouteQPQWebServerConfigSetting, SeoQPQWebServerConfigSetting, ServiceFunctionQPQWebServerConfigSetting, SubdomainRedirectQPQWebServerConfigSetting, WebSocketQPQWebServerConfigSetting } from '../config';
|
|
2
|
+
import { ApiKeyQPQWebServerConfigSetting, CacheQPQWebServerConfigSetting, CertificateQPQWebServerConfigSetting, DefaultRouteOptionsQPQWebServerConfigSetting, DnsQPQWebServerConfigSetting, DomainProxyQPQWebServerConfigSetting, FileUploadSettings, OpenApiQPQWebServerConfigSetting, RouteQPQWebServerConfigSetting, SeoQPQWebServerConfigSetting, ServiceFunctionQPQWebServerConfigSetting, SubdomainRedirectQPQWebServerConfigSetting, WebSocketQPQWebServerConfigSetting } from '../config';
|
|
3
3
|
import { ApiQPQWebServerConfigSetting, WebEntryQPQWebServerConfigSetting } from '../config';
|
|
4
4
|
export declare const getAllRoutes: (qpqConfig: QPQConfig) => RouteQPQWebServerConfigSetting[];
|
|
5
5
|
export declare const getAllRoutesForApi: (apiName: string, qpqConfig: QPQConfig) => RouteQPQWebServerConfigSetting[];
|
|
@@ -40,6 +40,15 @@ export declare const resolveServiceScopedCorsAllowedOrigins: (qpqConfig: QPQConf
|
|
|
40
40
|
* `defineStorageDriveCorsSettings` wins; otherwise the service-scoped default.
|
|
41
41
|
*/
|
|
42
42
|
export declare const getStorageDriveCorsAllowedOrigins: (qpqConfig: QPQConfig, storageDriveName: string) => string[];
|
|
43
|
+
/**
|
|
44
|
+
* Whether any web entry in this service serves its assets from the named storage
|
|
45
|
+
* drive (`storageDrive.sourceStorageDrive`) — i.e. the drive's bucket is a
|
|
46
|
+
* CloudFront origin. Drives with no web entry consumer need no CloudFront access
|
|
47
|
+
* to their bucket at all.
|
|
48
|
+
*/
|
|
49
|
+
export declare const isStorageDriveWebEntryOrigin: (qpqConfig: QPQConfig, storageDriveName: string) => boolean;
|
|
50
|
+
export declare const defaultFileUploadSettings: FileUploadSettings;
|
|
51
|
+
export declare const getFileUploadSettings: (qpqConfig: QPQConfig) => FileUploadSettings;
|
|
43
52
|
export declare const resolveApexDomainNameFromDomainConfig: (qpqConfig: QPQConfig, rootDomain: string, onRootDomain: boolean) => string;
|
|
44
53
|
export declare const constructServiceDomainName: (rootDomain: string, environment: string, service: string, feature?: string) => string;
|
|
45
54
|
export declare const constructEnvironmentDomainName: (environment: string, domain: string) => string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getOwnedWebsocketSettings = exports.getWebsocketSettings = exports.resolveDomainRoot = exports.getDomainRoot = exports.getDefaultRouteSettings = exports.constructEnvironmentDomainName = exports.constructServiceDomainName = exports.resolveApexDomainNameFromDomainConfig = exports.getStorageDriveCorsAllowedOrigins = exports.resolveServiceScopedCorsAllowedOrigins = exports.getServiceDomainName = exports.getBaseDomainName = exports.getEnvironmentDomainName = exports.getCacheConfigByName = exports.getAllOwnedCertifcateConfigs = exports.getAllOwnedCacheConfigs = exports.getDomainProxyConfigs = exports.getWebEntryConfigs = exports.getDnsConfigs = exports.getApiConfigs = exports.getSubdomainRedirects = exports.getWebsocketEntryByApiName = exports.getWebEntry = exports.getDomainName = exports.getAllSrcEntries = exports.getAllWebsocketSrcEntries = exports.getAllOpenApiSpecs = exports.getOwnedServiceFunctions = exports.getAllServiceFunctions = exports.getAllSeo = exports.getAllApiKeyConfigs = exports.getAllRoutesForApi = exports.getAllRoutes = void 0;
|
|
3
|
+
exports.getOwnedWebsocketSettings = exports.getWebsocketSettings = exports.resolveDomainRoot = exports.getDomainRoot = exports.getDefaultRouteSettings = exports.constructEnvironmentDomainName = exports.constructServiceDomainName = exports.resolveApexDomainNameFromDomainConfig = exports.getFileUploadSettings = exports.defaultFileUploadSettings = exports.isStorageDriveWebEntryOrigin = exports.getStorageDriveCorsAllowedOrigins = exports.resolveServiceScopedCorsAllowedOrigins = exports.getServiceDomainName = exports.getBaseDomainName = exports.getEnvironmentDomainName = exports.getCacheConfigByName = exports.getAllOwnedCertifcateConfigs = exports.getAllOwnedCacheConfigs = exports.getDomainProxyConfigs = exports.getWebEntryConfigs = exports.getDnsConfigs = exports.getApiConfigs = exports.getSubdomainRedirects = exports.getWebsocketEntryByApiName = exports.getWebEntry = exports.getDomainName = exports.getAllSrcEntries = exports.getAllWebsocketSrcEntries = exports.getAllOpenApiSpecs = exports.getOwnedServiceFunctions = exports.getAllServiceFunctions = exports.getAllSeo = exports.getAllApiKeyConfigs = exports.getAllRoutesForApi = exports.getAllRoutes = void 0;
|
|
4
4
|
const quidproquo_core_1 = require("quidproquo-core");
|
|
5
5
|
const config_1 = require("../config");
|
|
6
6
|
const getAllRoutes = (qpqConfig) => {
|
|
@@ -175,6 +175,29 @@ const getStorageDriveCorsAllowedOrigins = (qpqConfig, storageDriveName) => {
|
|
|
175
175
|
return (0, exports.resolveServiceScopedCorsAllowedOrigins)(qpqConfig, corsSetting === null || corsSetting === void 0 ? void 0 : corsSetting.allowedOrigins);
|
|
176
176
|
};
|
|
177
177
|
exports.getStorageDriveCorsAllowedOrigins = getStorageDriveCorsAllowedOrigins;
|
|
178
|
+
/**
|
|
179
|
+
* Whether any web entry in this service serves its assets from the named storage
|
|
180
|
+
* drive (`storageDrive.sourceStorageDrive`) — i.e. the drive's bucket is a
|
|
181
|
+
* CloudFront origin. Drives with no web entry consumer need no CloudFront access
|
|
182
|
+
* to their bucket at all.
|
|
183
|
+
*/
|
|
184
|
+
const isStorageDriveWebEntryOrigin = (qpqConfig, storageDriveName) => {
|
|
185
|
+
return (0, exports.getWebEntryConfigs)(qpqConfig).some((webEntry) => webEntry.storageDrive.sourceStorageDrive === storageDriveName);
|
|
186
|
+
};
|
|
187
|
+
exports.isStorageDriveWebEntryOrigin = isStorageDriveWebEntryOrigin;
|
|
188
|
+
// API Gateway caps request payloads at 10MB, so the default per-file ceiling matches it;
|
|
189
|
+
// the other defaults exist to bound parser memory rather than to be hit in practice.
|
|
190
|
+
exports.defaultFileUploadSettings = {
|
|
191
|
+
maxFileSizeBytes: 10 * 1024 * 1024,
|
|
192
|
+
maxFileCount: 10,
|
|
193
|
+
maxFieldCount: 100,
|
|
194
|
+
maxFieldSizeBytes: 1024 * 1024,
|
|
195
|
+
};
|
|
196
|
+
const getFileUploadSettings = (qpqConfig) => {
|
|
197
|
+
const setting = quidproquo_core_1.qpqCoreUtils.getConfigSetting(qpqConfig, config_1.QPQWebServerConfigSettingType.FileUploadSettings);
|
|
198
|
+
return Object.assign(Object.assign({}, exports.defaultFileUploadSettings), Object.fromEntries(Object.entries((setting === null || setting === void 0 ? void 0 : setting.fileUploadSettings) || {}).filter(([, value]) => (0, quidproquo_core_1.isDefined)(value))));
|
|
199
|
+
};
|
|
200
|
+
exports.getFileUploadSettings = getFileUploadSettings;
|
|
178
201
|
const resolveApexDomainNameFromDomainConfig = (qpqConfig, rootDomain, onRootDomain) => {
|
|
179
202
|
const feature = quidproquo_core_1.qpqCoreUtils.getApplicationModuleFeature(qpqConfig);
|
|
180
203
|
const environment = quidproquo_core_1.qpqCoreUtils.getApplicationModuleEnvironment(qpqConfig);
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { actionResult, EventActionType, generateUuid, getProcessCustomImplementation, } from 'quidproquo-core';
|
|
2
2
|
import { askValidateRouteAuth } from '../../stories/askValidateRouteAuth';
|
|
3
|
+
import { FileUploadErrorTypeEnum } from '../../types/HTTPEvent';
|
|
3
4
|
import { getCorsHeaders } from '../../utils/headerUtils';
|
|
5
|
+
const fileUploadErrorHttpStatusMap = {
|
|
6
|
+
[FileUploadErrorTypeEnum.fileTooLarge]: 413,
|
|
7
|
+
[FileUploadErrorTypeEnum.tooManyFiles]: 413,
|
|
8
|
+
[FileUploadErrorTypeEnum.tooManyFields]: 400,
|
|
9
|
+
[FileUploadErrorTypeEnum.disallowedMimeType]: 415,
|
|
10
|
+
[FileUploadErrorTypeEnum.malformed]: 400,
|
|
11
|
+
};
|
|
4
12
|
const getProcessAutoRespond = (qpqConfig) => {
|
|
5
13
|
const validateAuth = getProcessCustomImplementation(qpqConfig, askValidateRouteAuth, 'API Auth Validation', null, () => new Date().toISOString(), generateUuid);
|
|
6
14
|
return async ({ qpqEventRecord, matchResult }, session, actionProcessorList, logger, updateSession, dynamicModuleLoader) => {
|
|
@@ -28,6 +36,18 @@ const getProcessAutoRespond = (qpqConfig) => {
|
|
|
28
36
|
headers: getCorsHeaders(qpqConfig, matchResult.config || {}, qpqEventRecord.headers),
|
|
29
37
|
});
|
|
30
38
|
}
|
|
39
|
+
// Reject invalid multipart uploads before the route story runs (after auth, so a 401 wins)
|
|
40
|
+
if (qpqEventRecord.fileUploadError) {
|
|
41
|
+
return actionResult({
|
|
42
|
+
status: fileUploadErrorHttpStatusMap[qpqEventRecord.fileUploadError.errorType] || 400,
|
|
43
|
+
isBase64Encoded: false,
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
errorType: qpqEventRecord.fileUploadError.errorType,
|
|
46
|
+
errorText: qpqEventRecord.fileUploadError.message,
|
|
47
|
+
}),
|
|
48
|
+
headers: getCorsHeaders(qpqConfig, matchResult.config || {}, qpqEventRecord.headers),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
31
51
|
return actionResult(null);
|
|
32
52
|
};
|
|
33
53
|
};
|
|
@@ -13,7 +13,8 @@ export declare enum QPQWebServerConfigSettingType {
|
|
|
13
13
|
Cache = "@quidproquo-webserver/config/Cache",
|
|
14
14
|
Certificate = "@quidproquo-webserver/config/Certificate",
|
|
15
15
|
DomainProxy = "@quidproquo-webserver/config/DomainProxy",
|
|
16
|
-
StorageDriveCorsSettings = "@quidproquo-webserver/config/StorageDriveCorsSettings"
|
|
16
|
+
StorageDriveCorsSettings = "@quidproquo-webserver/config/StorageDriveCorsSettings",
|
|
17
|
+
FileUploadSettings = "@quidproquo-webserver/config/FileUploadSettings"
|
|
17
18
|
}
|
|
18
19
|
export interface CacheSettings {
|
|
19
20
|
minTTLInSeconds: number;
|
|
@@ -15,4 +15,5 @@ export var QPQWebServerConfigSettingType;
|
|
|
15
15
|
QPQWebServerConfigSettingType["Certificate"] = "@quidproquo-webserver/config/Certificate";
|
|
16
16
|
QPQWebServerConfigSettingType["DomainProxy"] = "@quidproquo-webserver/config/DomainProxy";
|
|
17
17
|
QPQWebServerConfigSettingType["StorageDriveCorsSettings"] = "@quidproquo-webserver/config/StorageDriveCorsSettings";
|
|
18
|
+
QPQWebServerConfigSettingType["FileUploadSettings"] = "@quidproquo-webserver/config/FileUploadSettings";
|
|
18
19
|
})(QPQWebServerConfigSettingType || (QPQWebServerConfigSettingType = {}));
|
|
@@ -3,6 +3,7 @@ export interface QPQConfigAdvancedApiSettings extends QPQConfigAdvancedSettings
|
|
|
3
3
|
subDomain?: string;
|
|
4
4
|
cloudflareApiKeySecretName?: string;
|
|
5
5
|
virtualNetworkName?: string;
|
|
6
|
+
maxConcurrentExecutions?: number;
|
|
6
7
|
}
|
|
7
8
|
export interface ApiQPQWebServerConfigSetting extends QPQConfigSetting {
|
|
8
9
|
apiSubdomain: string;
|
|
@@ -11,5 +12,6 @@ export interface ApiQPQWebServerConfigSetting extends QPQConfigSetting {
|
|
|
11
12
|
deprecated: boolean;
|
|
12
13
|
cloudflareApiKeySecretName?: string;
|
|
13
14
|
virtualNetworkName?: string;
|
|
15
|
+
maxConcurrentExecutions?: number;
|
|
14
16
|
}
|
|
15
17
|
export declare const defineApi: (apiName: string, rootDomain: string, options?: QPQConfigAdvancedApiSettings) => ApiQPQWebServerConfigSetting;
|
|
@@ -9,5 +9,6 @@ export const defineApi = (apiName, rootDomain, options) => {
|
|
|
9
9
|
deprecated: options?.deprecated || false,
|
|
10
10
|
cloudflareApiKeySecretName: options?.cloudflareApiKeySecretName,
|
|
11
11
|
virtualNetworkName: options?.virtualNetworkName,
|
|
12
|
+
maxConcurrentExecutions: options?.maxConcurrentExecutions,
|
|
12
13
|
};
|
|
13
14
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { QPQConfigSetting } from 'quidproquo-core';
|
|
2
|
+
export interface FileUploadSettings {
|
|
3
|
+
/** Maximum size of any single uploaded file. Uploads with a larger file are rejected with a 413. */
|
|
4
|
+
maxFileSizeBytes: number;
|
|
5
|
+
/** Maximum number of files in a single multipart request. */
|
|
6
|
+
maxFileCount: number;
|
|
7
|
+
/** Maximum number of non-file fields in a single multipart request. */
|
|
8
|
+
maxFieldCount: number;
|
|
9
|
+
/** Maximum size of any single non-file field value. Larger values are truncated by the parser. */
|
|
10
|
+
maxFieldSizeBytes: number;
|
|
11
|
+
/**
|
|
12
|
+
* Content types accepted for uploaded files, e.g. `['image/*', 'application/pdf']`
|
|
13
|
+
* (`type/*` wildcards supported). Omit to accept any content type.
|
|
14
|
+
*/
|
|
15
|
+
allowedMimeTypes?: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface FileUploadSettingsQPQWebServerConfigSetting extends QPQConfigSetting {
|
|
18
|
+
fileUploadSettings: Partial<FileUploadSettings>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Service-wide limits for multipart/form-data file uploads. Sensible defaults
|
|
22
|
+
* apply even when this setting is not declared — declare it only to override them.
|
|
23
|
+
*/
|
|
24
|
+
export declare const defineFileUploadSettings: (fileUploadSettings: Partial<FileUploadSettings>) => FileUploadSettingsQPQWebServerConfigSetting;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { QPQWebServerConfigSettingType } from '../QPQConfig';
|
|
2
|
+
/**
|
|
3
|
+
* Service-wide limits for multipart/form-data file uploads. Sensible defaults
|
|
4
|
+
* apply even when this setting is not declared — declare it only to override them.
|
|
5
|
+
*/
|
|
6
|
+
export const defineFileUploadSettings = (fileUploadSettings) => ({
|
|
7
|
+
configSettingType: QPQWebServerConfigSettingType.FileUploadSettings,
|
|
8
|
+
uniqueKey: 'fileUploadSettings',
|
|
9
|
+
fileUploadSettings,
|
|
10
|
+
});
|
|
@@ -8,6 +8,7 @@ export * from './defineAdminSettings';
|
|
|
8
8
|
export * from './defineAuthSystem';
|
|
9
9
|
export * from './dns';
|
|
10
10
|
export * from './domainProxy';
|
|
11
|
+
export * from './fileUploadSettings';
|
|
11
12
|
export * from './migration';
|
|
12
13
|
export * from './openApi';
|
|
13
14
|
export * from './route';
|
|
@@ -8,6 +8,7 @@ export * from './defineAdminSettings';
|
|
|
8
8
|
export * from './defineAuthSystem';
|
|
9
9
|
export * from './dns';
|
|
10
10
|
export * from './domainProxy';
|
|
11
|
+
export * from './fileUploadSettings';
|
|
11
12
|
export * from './migration';
|
|
12
13
|
export * from './openApi';
|
|
13
14
|
export * from './route';
|
|
@@ -2,11 +2,13 @@ import { CrossModuleOwner, QPQConfigAdvancedSettings, QPQConfigSetting, QpqFunct
|
|
|
2
2
|
export interface QPQConfigAdvancedServiceFunctionSettings extends QPQConfigAdvancedSettings {
|
|
3
3
|
functionName?: string;
|
|
4
4
|
virtualNetworkName?: string;
|
|
5
|
+
maxConcurrentExecutions?: number;
|
|
5
6
|
owner?: CrossModuleOwner<'functionName'>;
|
|
6
7
|
}
|
|
7
8
|
export interface ServiceFunctionQPQWebServerConfigSetting extends QPQConfigSetting {
|
|
8
9
|
runtime: QpqFunctionRuntime;
|
|
9
10
|
functionName: string;
|
|
10
11
|
virtualNetworkName?: string;
|
|
12
|
+
maxConcurrentExecutions?: number;
|
|
11
13
|
}
|
|
12
14
|
export declare const defineServiceFunction: (runtime: QpqFunctionRuntime, options?: QPQConfigAdvancedServiceFunctionSettings) => ServiceFunctionQPQWebServerConfigSetting;
|
|
@@ -8,6 +8,7 @@ export const defineServiceFunction = (runtime, options) => {
|
|
|
8
8
|
runtime,
|
|
9
9
|
functionName: functionName,
|
|
10
10
|
virtualNetworkName: options?.virtualNetworkName,
|
|
11
|
+
maxConcurrentExecutions: options?.maxConcurrentExecutions,
|
|
11
12
|
owner: qpqCoreUtils.convertCrossModuleOwnerToGenericResourceNameOverride(options?.owner),
|
|
12
13
|
};
|
|
13
14
|
};
|
|
@@ -8,6 +8,7 @@ export interface QPQConfigAdvancedWebSocketSettings extends QPQConfigAdvancedSet
|
|
|
8
8
|
onRootDomain?: boolean;
|
|
9
9
|
apiName?: string;
|
|
10
10
|
cloudflareApiKeySecretName?: string;
|
|
11
|
+
maxConcurrentExecutions?: number;
|
|
11
12
|
owner?: CrossModuleOwner<'websocketApiName'>;
|
|
12
13
|
}
|
|
13
14
|
export interface WebSocketQPQWebServerConfigSetting extends QPQConfigSetting {
|
|
@@ -18,5 +19,6 @@ export interface WebSocketQPQWebServerConfigSetting extends QPQConfigSetting {
|
|
|
18
19
|
eventProcessors: QpqWebSocketEventProcessors;
|
|
19
20
|
deprecated: boolean;
|
|
20
21
|
cloudflareApiKeySecretName?: string;
|
|
22
|
+
maxConcurrentExecutions?: number;
|
|
21
23
|
}
|
|
22
24
|
export declare const defineWebsocket: (apiSubdomain: string, rootDomain: string, eventProcessors: QpqWebSocketEventProcessors, options?: QPQConfigAdvancedWebSocketSettings) => WebSocketQPQWebServerConfigSetting;
|
|
@@ -12,6 +12,7 @@ export const defineWebsocket = (apiSubdomain, rootDomain, eventProcessors, optio
|
|
|
12
12
|
apiName: options?.apiName || 'api',
|
|
13
13
|
deprecated: options?.deprecated || false,
|
|
14
14
|
cloudflareApiKeySecretName: options?.cloudflareApiKeySecretName,
|
|
15
|
+
maxConcurrentExecutions: options?.maxConcurrentExecutions,
|
|
15
16
|
owner: qpqCoreUtils.convertCrossModuleOwnerToGenericResourceNameOverride(options?.owner),
|
|
16
17
|
};
|
|
17
18
|
};
|
|
@@ -10,32 +10,39 @@ export function isWebSocketAuthenticateMessage(event) {
|
|
|
10
10
|
}
|
|
11
11
|
export function* askProcessOnAuthenticate(connectionId, accessToken) {
|
|
12
12
|
const connection = yield* webSocketConnectionData.askGetById(connectionId);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
userId: decodedAccessToken.userId,
|
|
28
|
-
accessToken,
|
|
29
|
-
});
|
|
13
|
+
// No connection record (e.g. the connect event hasn't been processed yet) —
|
|
14
|
+
// tell the client rather than dropping the request silently, so it can
|
|
15
|
+
// distinguish "not authenticated" from "no reply".
|
|
16
|
+
if (!connection) {
|
|
17
|
+
yield* askSendMessage(connectionId, {
|
|
18
|
+
type: WebSocketQueueServerMessageEventType.Unauthenticated,
|
|
19
|
+
});
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const apiName = yield* askWebsocketReadApiNameOrThrow();
|
|
23
|
+
const userDirectoryName = yield* askConfigGetGlobal(getWebSocketQueueGlobalConfigKeyForUserDirectoryName(apiName));
|
|
24
|
+
if (userDirectoryName) {
|
|
25
|
+
const result = yield* askCatch(askUserDirectorySetAccessToken(userDirectoryName, accessToken));
|
|
26
|
+
if (!result.success) {
|
|
30
27
|
yield* askSendMessage(connectionId, {
|
|
31
|
-
type: WebSocketQueueServerMessageEventType.
|
|
28
|
+
type: WebSocketQueueServerMessageEventType.Unauthenticated,
|
|
32
29
|
});
|
|
33
|
-
|
|
34
|
-
const webSocketQueueClientEventMessageAuthenticate = {
|
|
35
|
-
type: WebSocketQueueClientMessageEventType.Authenticate,
|
|
36
|
-
payload: {},
|
|
37
|
-
};
|
|
38
|
-
yield* askBroadcastUnknownMessage(webSocketQueueClientEventMessageAuthenticate);
|
|
30
|
+
return;
|
|
39
31
|
}
|
|
32
|
+
const decodedAccessToken = result.result;
|
|
33
|
+
yield* webSocketConnectionData.askUpsert({
|
|
34
|
+
...connection,
|
|
35
|
+
userId: decodedAccessToken.userId,
|
|
36
|
+
accessToken,
|
|
37
|
+
});
|
|
38
|
+
yield* askSendMessage(connectionId, {
|
|
39
|
+
type: WebSocketQueueServerMessageEventType.Authenticated,
|
|
40
|
+
});
|
|
41
|
+
// Send a websocket message to the event buss WITHOUT an access token
|
|
42
|
+
const webSocketQueueClientEventMessageAuthenticate = {
|
|
43
|
+
type: WebSocketQueueClientMessageEventType.Authenticate,
|
|
44
|
+
payload: {},
|
|
45
|
+
};
|
|
46
|
+
yield* askBroadcastUnknownMessage(webSocketQueueClientEventMessageAuthenticate);
|
|
40
47
|
}
|
|
41
48
|
}
|
|
@@ -5,6 +5,17 @@ export interface HttpEventHeaders {
|
|
|
5
5
|
export interface HttpEventRouteParams {
|
|
6
6
|
[key: string]: string;
|
|
7
7
|
}
|
|
8
|
+
export declare enum FileUploadErrorTypeEnum {
|
|
9
|
+
fileTooLarge = "fileTooLarge",
|
|
10
|
+
tooManyFiles = "tooManyFiles",
|
|
11
|
+
tooManyFields = "tooManyFields",
|
|
12
|
+
disallowedMimeType = "disallowedMimeType",
|
|
13
|
+
malformed = "malformed"
|
|
14
|
+
}
|
|
15
|
+
export interface HTTPEventFileUploadError {
|
|
16
|
+
errorType: FileUploadErrorTypeEnum;
|
|
17
|
+
message: string;
|
|
18
|
+
}
|
|
8
19
|
export interface HTTPEvent {
|
|
9
20
|
path: string;
|
|
10
21
|
query: {
|
|
@@ -17,6 +28,7 @@ export interface HTTPEvent {
|
|
|
17
28
|
sourceIp: string;
|
|
18
29
|
isBase64Encoded: boolean;
|
|
19
30
|
files?: QPQBinaryData[];
|
|
31
|
+
fileUploadError?: HTTPEventFileUploadError;
|
|
20
32
|
}
|
|
21
33
|
export interface HTTPEventResponse {
|
|
22
34
|
status: number;
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
export
|
|
1
|
+
export var FileUploadErrorTypeEnum;
|
|
2
|
+
(function (FileUploadErrorTypeEnum) {
|
|
3
|
+
FileUploadErrorTypeEnum["fileTooLarge"] = "fileTooLarge";
|
|
4
|
+
FileUploadErrorTypeEnum["tooManyFiles"] = "tooManyFiles";
|
|
5
|
+
FileUploadErrorTypeEnum["tooManyFields"] = "tooManyFields";
|
|
6
|
+
FileUploadErrorTypeEnum["disallowedMimeType"] = "disallowedMimeType";
|
|
7
|
+
FileUploadErrorTypeEnum["malformed"] = "malformed";
|
|
8
|
+
})(FileUploadErrorTypeEnum || (FileUploadErrorTypeEnum = {}));
|
|
2
9
|
// type ParseParamType<S extends string> = S extends `${infer ParamName}:number`
|
|
3
10
|
// ? { name: ParamName; type: number }
|
|
4
11
|
// : S extends `${infer ParamName}:int`
|
|
@@ -4,6 +4,14 @@ import { HTTPEvent, HTTPEventResponse } from '../types/HTTPEvent';
|
|
|
4
4
|
export declare const rawFromJsonEventRequest: (httpJsonEvent: HTTPEvent) => string | undefined;
|
|
5
5
|
export declare const fromJsonEventRequest: <T>(httpJsonEvent: HTTPEvent) => T;
|
|
6
6
|
export declare function askFromJsonEventRequest<T>(httpJsonEvent: HTTPEvent): AskResponse<T>;
|
|
7
|
+
/**
|
|
8
|
+
* Like `askFromJsonEventRequest`, but the parsed body is run through an app-supplied
|
|
9
|
+
* validator before it is returned - so the `T` is actually checked, not just cast.
|
|
10
|
+
* The validator throws (or returns the typed value); a validation throw becomes an
|
|
11
|
+
* `Invalid` (422) response. Any schema library fits, e.g. zod: `(data) => schema.parse(data)`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function askFromValidJsonEventRequest<T>(httpJsonEvent: HTTPEvent, validate: (data: unknown) => T): AskResponse<T>;
|
|
14
|
+
export declare const readUriQueryParamFromEvent: (event: HTTPEvent, paramName: string) => string | undefined;
|
|
7
15
|
export declare const toJsonEventResponse: (item: any, status?: number) => HTTPEventResponse;
|
|
8
16
|
export declare const toHtmlResponse: (html: string, status?: number) => HTTPEventResponse;
|
|
9
17
|
export declare const toTextResponse: (text: string, status?: number) => HTTPEventResponse;
|
|
@@ -33,6 +33,28 @@ export function* askFromJsonEventRequest(httpJsonEvent) {
|
|
|
33
33
|
return yield* askThrowError(ErrorTypeEnum.BadRequest, 'Unable to parse incoming json from HTTPEvent.');
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Like `askFromJsonEventRequest`, but the parsed body is run through an app-supplied
|
|
38
|
+
* validator before it is returned - so the `T` is actually checked, not just cast.
|
|
39
|
+
* The validator throws (or returns the typed value); a validation throw becomes an
|
|
40
|
+
* `Invalid` (422) response. Any schema library fits, e.g. zod: `(data) => schema.parse(data)`.
|
|
41
|
+
*/
|
|
42
|
+
export function* askFromValidJsonEventRequest(httpJsonEvent, validate) {
|
|
43
|
+
const parsedBody = yield* askFromJsonEventRequest(httpJsonEvent);
|
|
44
|
+
try {
|
|
45
|
+
return validate(parsedBody);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return yield* askThrowError(ErrorTypeEnum.Invalid, error instanceof Error ? error.message : 'Invalid request body.');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export const readUriQueryParamFromEvent = (event, paramName) => {
|
|
52
|
+
const rawValue = event.query[paramName];
|
|
53
|
+
if (!rawValue) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return Array.isArray(rawValue) ? rawValue[0] : rawValue;
|
|
57
|
+
};
|
|
36
58
|
export const toJsonEventResponse = (item, status = 200) => {
|
|
37
59
|
return {
|
|
38
60
|
status,
|
|
@@ -10,7 +10,7 @@ const isAbsoluteUrl = (url) => /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url);
|
|
|
10
10
|
// Join basePath + url like axios' combineURLs (path concatenation), NOT URL
|
|
11
11
|
// resolution. `new URL('/v1/x', 'http://host/api/svc')` drops the base path
|
|
12
12
|
// because a leading-slash url is root-absolute; we want '.../api/svc/v1/x'.
|
|
13
|
-
const combineUrls = (basePath, url) => url ? `${basePath.replace(/\/+$/, '')}/${url.replace(/^\/+/, '')}` : basePath;
|
|
13
|
+
const combineUrls = (basePath, url) => (url ? `${basePath.replace(/\/+$/, '')}/${url.replace(/^\/+/, '')}` : basePath);
|
|
14
14
|
const buildRequestUrl = (payload) => {
|
|
15
15
|
const fullUrl = payload.basePath && !isAbsoluteUrl(payload.url) ? combineUrls(payload.basePath, payload.url) : payload.url;
|
|
16
16
|
const url = new URL(fullUrl);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { QPQConfig, QpqFunctionRuntime } from 'quidproquo-core';
|
|
2
|
-
import { ApiKeyQPQWebServerConfigSetting, CacheQPQWebServerConfigSetting, CertificateQPQWebServerConfigSetting, DefaultRouteOptionsQPQWebServerConfigSetting, DnsQPQWebServerConfigSetting, DomainProxyQPQWebServerConfigSetting, OpenApiQPQWebServerConfigSetting, RouteQPQWebServerConfigSetting, SeoQPQWebServerConfigSetting, ServiceFunctionQPQWebServerConfigSetting, SubdomainRedirectQPQWebServerConfigSetting, WebSocketQPQWebServerConfigSetting } from '../config';
|
|
2
|
+
import { ApiKeyQPQWebServerConfigSetting, CacheQPQWebServerConfigSetting, CertificateQPQWebServerConfigSetting, DefaultRouteOptionsQPQWebServerConfigSetting, DnsQPQWebServerConfigSetting, DomainProxyQPQWebServerConfigSetting, FileUploadSettings, OpenApiQPQWebServerConfigSetting, RouteQPQWebServerConfigSetting, SeoQPQWebServerConfigSetting, ServiceFunctionQPQWebServerConfigSetting, SubdomainRedirectQPQWebServerConfigSetting, WebSocketQPQWebServerConfigSetting } from '../config';
|
|
3
3
|
import { ApiQPQWebServerConfigSetting, WebEntryQPQWebServerConfigSetting } from '../config';
|
|
4
4
|
export declare const getAllRoutes: (qpqConfig: QPQConfig) => RouteQPQWebServerConfigSetting[];
|
|
5
5
|
export declare const getAllRoutesForApi: (apiName: string, qpqConfig: QPQConfig) => RouteQPQWebServerConfigSetting[];
|
|
@@ -40,6 +40,15 @@ export declare const resolveServiceScopedCorsAllowedOrigins: (qpqConfig: QPQConf
|
|
|
40
40
|
* `defineStorageDriveCorsSettings` wins; otherwise the service-scoped default.
|
|
41
41
|
*/
|
|
42
42
|
export declare const getStorageDriveCorsAllowedOrigins: (qpqConfig: QPQConfig, storageDriveName: string) => string[];
|
|
43
|
+
/**
|
|
44
|
+
* Whether any web entry in this service serves its assets from the named storage
|
|
45
|
+
* drive (`storageDrive.sourceStorageDrive`) — i.e. the drive's bucket is a
|
|
46
|
+
* CloudFront origin. Drives with no web entry consumer need no CloudFront access
|
|
47
|
+
* to their bucket at all.
|
|
48
|
+
*/
|
|
49
|
+
export declare const isStorageDriveWebEntryOrigin: (qpqConfig: QPQConfig, storageDriveName: string) => boolean;
|
|
50
|
+
export declare const defaultFileUploadSettings: FileUploadSettings;
|
|
51
|
+
export declare const getFileUploadSettings: (qpqConfig: QPQConfig) => FileUploadSettings;
|
|
43
52
|
export declare const resolveApexDomainNameFromDomainConfig: (qpqConfig: QPQConfig, rootDomain: string, onRootDomain: boolean) => string;
|
|
44
53
|
export declare const constructServiceDomainName: (rootDomain: string, environment: string, service: string, feature?: string) => string;
|
|
45
54
|
export declare const constructEnvironmentDomainName: (environment: string, domain: string) => string;
|
|
@@ -147,6 +147,30 @@ export const getStorageDriveCorsAllowedOrigins = (qpqConfig, storageDriveName) =
|
|
|
147
147
|
.find((setting) => setting.storageDriveName === storageDriveName);
|
|
148
148
|
return resolveServiceScopedCorsAllowedOrigins(qpqConfig, corsSetting?.allowedOrigins);
|
|
149
149
|
};
|
|
150
|
+
/**
|
|
151
|
+
* Whether any web entry in this service serves its assets from the named storage
|
|
152
|
+
* drive (`storageDrive.sourceStorageDrive`) — i.e. the drive's bucket is a
|
|
153
|
+
* CloudFront origin. Drives with no web entry consumer need no CloudFront access
|
|
154
|
+
* to their bucket at all.
|
|
155
|
+
*/
|
|
156
|
+
export const isStorageDriveWebEntryOrigin = (qpqConfig, storageDriveName) => {
|
|
157
|
+
return getWebEntryConfigs(qpqConfig).some((webEntry) => webEntry.storageDrive.sourceStorageDrive === storageDriveName);
|
|
158
|
+
};
|
|
159
|
+
// API Gateway caps request payloads at 10MB, so the default per-file ceiling matches it;
|
|
160
|
+
// the other defaults exist to bound parser memory rather than to be hit in practice.
|
|
161
|
+
export const defaultFileUploadSettings = {
|
|
162
|
+
maxFileSizeBytes: 10 * 1024 * 1024,
|
|
163
|
+
maxFileCount: 10,
|
|
164
|
+
maxFieldCount: 100,
|
|
165
|
+
maxFieldSizeBytes: 1024 * 1024,
|
|
166
|
+
};
|
|
167
|
+
export const getFileUploadSettings = (qpqConfig) => {
|
|
168
|
+
const setting = qpqCoreUtils.getConfigSetting(qpqConfig, QPQWebServerConfigSettingType.FileUploadSettings);
|
|
169
|
+
return {
|
|
170
|
+
...defaultFileUploadSettings,
|
|
171
|
+
...Object.fromEntries(Object.entries(setting?.fileUploadSettings || {}).filter(([, value]) => isDefined(value))),
|
|
172
|
+
};
|
|
173
|
+
};
|
|
150
174
|
export const resolveApexDomainNameFromDomainConfig = (qpqConfig, rootDomain, onRootDomain) => {
|
|
151
175
|
const feature = qpqCoreUtils.getApplicationModuleFeature(qpqConfig);
|
|
152
176
|
const environment = qpqCoreUtils.getApplicationModuleEnvironment(qpqConfig);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quidproquo-webserver",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./lib/commonjs/index.js",
|
|
6
6
|
"module": "./lib/esm/index.js",
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
"homepage": "https://github.com/joe-coady/quidproquo#readme",
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@anthropic-ai/sdk": "^0.19.1",
|
|
36
|
-
"quidproquo-core": "0.1.
|
|
36
|
+
"quidproquo-core": "0.1.3"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"quidproquo-tsconfig": "0.1.
|
|
39
|
+
"quidproquo-tsconfig": "0.1.3",
|
|
40
40
|
"typescript": "^5.8.2"
|
|
41
41
|
}
|
|
42
42
|
}
|