quidproquo-actionprocessor-awslambda 0.1.2 → 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.
Files changed (58) hide show
  1. package/lib/commonjs/awsNamingUtils.d.ts +2 -0
  2. package/lib/commonjs/awsNamingUtils.js +24 -1
  3. package/lib/commonjs/getActionProcessor/core/apiKeyValidation/getApiKeyValidationValidateActionProcessor.js +15 -11
  4. package/lib/commonjs/getActionProcessor/core/event/apiGatwayEvent/api/getEventGetRecordsActionProcessor.js +12 -1
  5. package/lib/commonjs/getActionProcessor/core/event/utils/parseMultipartFormData.d.ts +7 -1
  6. package/lib/commonjs/getActionProcessor/core/event/utils/parseMultipartFormData.js +77 -8
  7. package/lib/commonjs/getActionProcessor/core/index.d.ts +1 -0
  8. package/lib/commonjs/getActionProcessor/core/index.js +3 -1
  9. package/lib/commonjs/getActionProcessor/core/metric/getMetricPutActionProcessor.d.ts +2 -0
  10. package/lib/commonjs/getActionProcessor/core/metric/getMetricPutActionProcessor.js +51 -0
  11. package/lib/commonjs/getActionProcessor/core/metric/index.d.ts +2 -0
  12. package/lib/commonjs/getActionProcessor/core/metric/index.js +17 -0
  13. package/lib/commonjs/lambdas/dynamicModuleLoader/dynamicModuleLoader.js +8 -1
  14. package/lib/commonjs/logic/apiGateway/getApiKeyValue.d.ts +1 -0
  15. package/lib/commonjs/logic/apiGateway/{getApiKeys.js → getApiKeyValue.js} +10 -14
  16. package/lib/commonjs/logic/federatedModuleStore/FederatedModuleLoadError.d.ts +10 -0
  17. package/lib/commonjs/logic/federatedModuleStore/FederatedModuleLoadError.js +26 -0
  18. package/lib/commonjs/logic/federatedModuleStore/FederatedModuleStoreManifest.d.ts +8 -0
  19. package/lib/commonjs/logic/federatedModuleStore/FederatedModuleStoreManifest.js +2 -0
  20. package/lib/commonjs/logic/federatedModuleStore/index.d.ts +4 -0
  21. package/lib/commonjs/logic/federatedModuleStore/index.js +20 -0
  22. package/lib/commonjs/logic/federatedModuleStore/loadFederatedStory.d.ts +3 -0
  23. package/lib/commonjs/logic/federatedModuleStore/loadFederatedStory.js +295 -0
  24. package/lib/commonjs/logic/federatedModuleStore/sharedPackages.d.ts +1 -0
  25. package/lib/commonjs/logic/federatedModuleStore/sharedPackages.js +10 -0
  26. package/lib/commonjs/logic/index.d.ts +1 -0
  27. package/lib/commonjs/logic/index.js +1 -0
  28. package/lib/esm/awsNamingUtils.d.ts +2 -0
  29. package/lib/esm/awsNamingUtils.js +20 -0
  30. package/lib/esm/getActionProcessor/core/apiKeyValidation/getApiKeyValidationValidateActionProcessor.js +16 -12
  31. package/lib/esm/getActionProcessor/core/event/apiGatwayEvent/api/getEventGetRecordsActionProcessor.js +14 -3
  32. package/lib/esm/getActionProcessor/core/event/utils/parseMultipartFormData.d.ts +7 -1
  33. package/lib/esm/getActionProcessor/core/event/utils/parseMultipartFormData.js +75 -7
  34. package/lib/esm/getActionProcessor/core/index.d.ts +1 -0
  35. package/lib/esm/getActionProcessor/core/index.js +3 -0
  36. package/lib/esm/getActionProcessor/core/metric/getMetricPutActionProcessor.d.ts +2 -0
  37. package/lib/esm/getActionProcessor/core/metric/getMetricPutActionProcessor.js +43 -0
  38. package/lib/esm/getActionProcessor/core/metric/index.d.ts +2 -0
  39. package/lib/esm/getActionProcessor/core/metric/index.js +4 -0
  40. package/lib/esm/lambdas/dynamicModuleLoader/dynamicModuleLoader.js +9 -2
  41. package/lib/esm/logic/apiGateway/getApiKeyValue.d.ts +1 -0
  42. package/lib/esm/logic/apiGateway/getApiKeyValue.js +12 -0
  43. package/lib/esm/logic/federatedModuleStore/FederatedModuleLoadError.d.ts +10 -0
  44. package/lib/esm/logic/federatedModuleStore/FederatedModuleLoadError.js +23 -0
  45. package/lib/esm/logic/federatedModuleStore/FederatedModuleStoreManifest.d.ts +8 -0
  46. package/lib/esm/logic/federatedModuleStore/FederatedModuleStoreManifest.js +1 -0
  47. package/lib/esm/logic/federatedModuleStore/index.d.ts +4 -0
  48. package/lib/esm/logic/federatedModuleStore/index.js +4 -0
  49. package/lib/esm/logic/federatedModuleStore/loadFederatedStory.d.ts +3 -0
  50. package/lib/esm/logic/federatedModuleStore/loadFederatedStory.js +246 -0
  51. package/lib/esm/logic/federatedModuleStore/sharedPackages.d.ts +1 -0
  52. package/lib/esm/logic/federatedModuleStore/sharedPackages.js +7 -0
  53. package/lib/esm/logic/index.d.ts +1 -0
  54. package/lib/esm/logic/index.js +1 -0
  55. package/package.json +7 -6
  56. package/lib/commonjs/logic/apiGateway/getApiKeys.d.ts +0 -2
  57. package/lib/esm/logic/apiGateway/getApiKeys.d.ts +0 -2
  58. package/lib/esm/logic/apiGateway/getApiKeys.js +0 -17
@@ -17,7 +17,9 @@ export declare const getCFExportNameApiKeyIdFromConfig: (apiKeyName: string, qpq
17
17
  export declare const getCFExportNameSnsTopicArnFromConfig: (eventBusName: string, qpqConfig: QPQConfig, applicationOverride?: string) => string;
18
18
  export declare const getCFExportNameDistributionIdArnFromConfig: (webEntryName: string, qpqConfig: QPQConfig, serviceOverride?: string, applicationOverride?: string) => string;
19
19
  export declare const getCFExportNameWebsocketApiIdFromConfig: (websocketApiName: string, qpqConfig: QPQConfig) => string;
20
+ export declare const getWebsocketApiIdSsmParameterName: (websocketApiName: string, qpqConfig: QPQConfig) => string;
20
21
  export declare const getEventBusSnsTopicArn: (eventBusName: string, qpqConfig: QPQConfig, module: string, environment: string, application: string, feature?: string) => string;
22
+ export declare const getAccountStackName: () => string;
21
23
  export declare const getBaseStackName: (qpqConfig: QPQConfig) => string;
22
24
  export declare const getInfStackName: (qpqConfig: QPQConfig) => string;
23
25
  export declare const getWebStackName: (qpqConfig: QPQConfig) => string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getDomainStackName = exports.getBootstrapStackName = exports.getApiStackName = exports.getWebStackName = exports.getInfStackName = exports.getBaseStackName = exports.getEventBusSnsTopicArn = exports.getCFExportNameWebsocketApiIdFromConfig = exports.getCFExportNameDistributionIdArnFromConfig = exports.getCFExportNameSnsTopicArnFromConfig = exports.getCFExportNameApiKeyIdFromConfig = exports.getCFExportNameUserPoolClientIdFromConfig = exports.getCFExportNameCachePolicyIdFromConfig = exports.getCFExportNameUserPoolIdFromConfig = exports.getKvsDynamoTableNameFromConfig = exports.getQpqRuntimeResourceNameFromConfig = exports.getQpqRuntimeResourceName = exports.getConfigRuntimeResourceNameFromConfigWithServiceOverride = exports.getConfigRuntimeBootstrapResourceNameFromConfig = exports.resolveConfigRuntimeResourceNameFromConfig = exports.getConfigRuntimeResourceNameFromConfig = exports.getConfigRuntimeBootstrapResourceName = exports.getConfigRuntimeResourceName = exports.getGlobalQpqRuntimeResourceName = exports.getGlobalConfigRuntimeResourceName = void 0;
3
+ exports.getDomainStackName = exports.getBootstrapStackName = exports.getApiStackName = exports.getWebStackName = exports.getInfStackName = exports.getBaseStackName = exports.getAccountStackName = exports.getEventBusSnsTopicArn = exports.getWebsocketApiIdSsmParameterName = exports.getCFExportNameWebsocketApiIdFromConfig = exports.getCFExportNameDistributionIdArnFromConfig = exports.getCFExportNameSnsTopicArnFromConfig = exports.getCFExportNameApiKeyIdFromConfig = exports.getCFExportNameUserPoolClientIdFromConfig = exports.getCFExportNameCachePolicyIdFromConfig = exports.getCFExportNameUserPoolIdFromConfig = exports.getKvsDynamoTableNameFromConfig = exports.getQpqRuntimeResourceNameFromConfig = exports.getQpqRuntimeResourceName = exports.getConfigRuntimeResourceNameFromConfigWithServiceOverride = exports.getConfigRuntimeBootstrapResourceNameFromConfig = exports.resolveConfigRuntimeResourceNameFromConfig = exports.getConfigRuntimeResourceNameFromConfig = exports.getConfigRuntimeBootstrapResourceName = exports.getConfigRuntimeResourceName = exports.getGlobalQpqRuntimeResourceName = exports.getGlobalConfigRuntimeResourceName = void 0;
4
4
  const quidproquo_config_aws_1 = require("quidproquo-config-aws");
5
5
  const quidproquo_core_1 = require("quidproquo-core");
6
6
  const quidproquo_webserver_1 = require("quidproquo-webserver");
@@ -156,6 +156,21 @@ const getCFExportNameWebsocketApiIdFromConfig = (websocketApiName, qpqConfig) =>
156
156
  return (0, exports.getQpqRuntimeResourceName)(resourceName, application, service, environment, feature, 'websocket-api-id-export');
157
157
  };
158
158
  exports.getCFExportNameWebsocketApiIdFromConfig = getCFExportNameWebsocketApiIdFromConfig;
159
+ // SSM parameter holding a websocket api's AWS-generated id, written by the owning service's
160
+ // inf stack. Referencing services resolve it at deploy time to build exact execute-api
161
+ // grants (deploy the owning inf stack first - same owner-publishes/others-read pattern as
162
+ // the domain certificate arn parameter).
163
+ const getWebsocketApiIdSsmParameterName = (websocketApiName, qpqConfig) => {
164
+ var _a, _b, _c, _d, _e;
165
+ const websocketApiConfig = quidproquo_webserver_1.qpqWebServerUtils.getWebsocketEntryByApiName(websocketApiName, qpqConfig);
166
+ const application = ((_a = websocketApiConfig.owner) === null || _a === void 0 ? void 0 : _a.application) || quidproquo_core_1.qpqCoreUtils.getApplicationName(qpqConfig);
167
+ const service = ((_b = websocketApiConfig.owner) === null || _b === void 0 ? void 0 : _b.module) || quidproquo_core_1.qpqCoreUtils.getApplicationModuleName(qpqConfig);
168
+ const environment = ((_c = websocketApiConfig.owner) === null || _c === void 0 ? void 0 : _c.environment) || quidproquo_core_1.qpqCoreUtils.getApplicationModuleEnvironment(qpqConfig);
169
+ const feature = ((_d = websocketApiConfig.owner) === null || _d === void 0 ? void 0 : _d.feature) || quidproquo_core_1.qpqCoreUtils.getApplicationModuleFeature(qpqConfig);
170
+ const resourceName = ((_e = websocketApiConfig.owner) === null || _e === void 0 ? void 0 : _e.resourceNameOverride) || websocketApiName;
171
+ return `/qpq/websocket/api-id/${(0, exports.getConfigRuntimeResourceName)(resourceName, application, service, environment, feature)}`;
172
+ };
173
+ exports.getWebsocketApiIdSsmParameterName = getWebsocketApiIdSsmParameterName;
159
174
  const getEventBusSnsTopicArn = (eventBusName, qpqConfig, module, environment, application, feature) => {
160
175
  const topicName = (0, exports.getConfigRuntimeResourceName)(eventBusName, application, module, environment, feature);
161
176
  const accountInfo = (0, quidproquo_config_aws_1.getAwsServiceAccountInfoByDeploymentInfo)(qpqConfig, module, environment, feature, application);
@@ -164,6 +179,14 @@ const getEventBusSnsTopicArn = (eventBusName, qpqConfig, module, environment, ap
164
179
  return `arn:aws:sns:${region}:${awsAccountId}:${topicName}`;
165
180
  };
166
181
  exports.getEventBusSnsTopicArn = getEventBusSnsTopicArn;
182
+ // The account stack owns account-level resources (cloud trail, budgets, security services)
183
+ // so they never share an app's lifecycle. The name is deliberately static: stack names are
184
+ // already namespaced per account+region, and the resources it holds meter/audit the whole
185
+ // account - an app/environment segment would only fake a separation that doesn't exist.
186
+ // Everything in one account converges on this one stack, so exactly ONE repo/config must
187
+ // own it, and actor (feature) deploys must not deploy it.
188
+ const getAccountStackName = () => 'qpq-account';
189
+ exports.getAccountStackName = getAccountStackName;
167
190
  const getBaseStackName = (qpqConfig) => {
168
191
  const appName = quidproquo_core_1.qpqCoreUtils.getApplicationName(qpqConfig);
169
192
  const moduleName = quidproquo_core_1.qpqCoreUtils.getApplicationModuleName(qpqConfig);
@@ -15,7 +15,8 @@ const quidproquo_core_1 = require("quidproquo-core");
15
15
  const quidproquo_webserver_1 = require("quidproquo-webserver");
16
16
  const crypto_1 = require("crypto");
17
17
  const awsNamingUtils_1 = require("../../../awsNamingUtils");
18
- const getApiKeys_1 = require("../../../logic/apiGateway/getApiKeys");
18
+ const getApiKeyValue_1 = require("../../../logic/apiGateway/getApiKeyValue");
19
+ const getExportedValue_1 = require("../../../logic/cloudformation/getExportedValue");
19
20
  // Constant-time string equality. `timingSafeEqual` throws on length mismatch,
20
21
  // so guard the length check — the length itself isn't secret for API keys.
21
22
  const safeEqual = (a, b) => {
@@ -28,17 +29,20 @@ const safeEqual = (a, b) => {
28
29
  };
29
30
  const getProcessApiKeyValidationValidate = (qpqConfig) => {
30
31
  const region = quidproquo_config_aws_1.qpqConfigAwsUtils.getApplicationModuleDeployRegion(qpqConfig);
31
- const application = quidproquo_core_1.qpqCoreUtils.getApplicationName(qpqConfig);
32
- const service = quidproquo_core_1.qpqCoreUtils.getApplicationModuleName(qpqConfig);
33
- const environment = quidproquo_core_1.qpqCoreUtils.getApplicationModuleEnvironment(qpqConfig);
34
- const feature = quidproquo_core_1.qpqCoreUtils.getApplicationModuleFeature(qpqConfig);
35
32
  return (_a) => __awaiter(void 0, [_a], void 0, function* ({ apiKeyValue, apiKeyReferences }) {
36
- const realApiKeys = yield (0, getApiKeys_1.getApiKeys)(region, ...apiKeyReferences.map((apiKey) => {
37
- const apiKeyApplication = apiKey.applicationName || application;
38
- const apiKeyService = apiKey.serviceName || service;
39
- return (0, awsNamingUtils_1.getConfigRuntimeResourceName)(apiKey.name, apiKeyApplication, apiKeyService, environment, feature);
40
- }));
41
- const matched = realApiKeys.some((apiKey) => safeEqual(apiKey.value, apiKeyValue));
33
+ // Fetch each referenced key individually (id via CFN export, then GetApiKey) rather than
34
+ // listing every key in the account - keeps the IAM grant per-key instead of /apikeys-wide.
35
+ const realApiKeyValues = yield Promise.all(apiKeyReferences.map((apiKeyReference) => __awaiter(void 0, void 0, void 0, function* () {
36
+ try {
37
+ const apiKeyId = yield (0, getExportedValue_1.getExportedValue)((0, awsNamingUtils_1.getCFExportNameApiKeyIdFromConfig)(apiKeyReference.name, qpqConfig, apiKeyReference.serviceName, apiKeyReference.applicationName), region);
38
+ return yield (0, getApiKeyValue_1.getApiKeyValue)(region, apiKeyId);
39
+ }
40
+ catch (_a) {
41
+ // An unresolvable reference behaves like the old list-and-filter miss - no match
42
+ return undefined;
43
+ }
44
+ })));
45
+ const matched = realApiKeyValues.some((realApiKeyValue) => safeEqual(realApiKeyValue, apiKeyValue));
42
46
  return (0, quidproquo_core_1.actionResult)(matched);
43
47
  });
44
48
  };
@@ -15,6 +15,7 @@ const quidproquo_webserver_1 = require("quidproquo-webserver");
15
15
  const parseMultipartFormData_1 = require("../../utils/parseMultipartFormData");
16
16
  const getProcessGetRecords = (qpqConfig) => {
17
17
  const serviceName = quidproquo_core_1.qpqCoreUtils.getApplicationModuleName(qpqConfig);
18
+ const fileUploadSettings = quidproquo_webserver_1.qpqWebServerUtils.getFileUploadSettings(qpqConfig);
18
19
  return (_a) => __awaiter(void 0, [_a], void 0, function* ({ eventParams: [apiGatewayEvent, context] }) {
19
20
  // Initialize `path` by removing the service name prefix from `apiGatewayEvent.path`.
20
21
  // This adjustment is necessary because the API gateway routes requests to services based on
@@ -35,7 +36,17 @@ const getProcessGetRecords = (qpqConfig) => {
35
36
  };
36
37
  // Transform the body if its a multipart/form-data
37
38
  if ((quidproquo_webserver_1.qpqWebServerUtils.getHeaderValue('Content-Type', apiGatewayEvent.headers) || '').startsWith('multipart/form-data') && apiGatewayEvent.body) {
38
- internalEventRecord.files = yield (0, parseMultipartFormData_1.parseMultipartFormData)(apiGatewayEvent);
39
+ try {
40
+ internalEventRecord.files = yield (0, parseMultipartFormData_1.parseMultipartFormData)(apiGatewayEvent, fileUploadSettings);
41
+ }
42
+ catch (error) {
43
+ // Stamp the failure on the record so the auto-respond step returns the matching
44
+ // 4xx before the route story runs - throwing here would fail the whole event as a 5xx
45
+ internalEventRecord.fileUploadError =
46
+ error instanceof parseMultipartFormData_1.FileUploadValidationError
47
+ ? { errorType: error.errorType, message: error.message }
48
+ : { errorType: quidproquo_webserver_1.FileUploadErrorTypeEnum.malformed, message: 'Unable to parse multipart/form-data body' };
49
+ }
39
50
  }
40
51
  return (0, quidproquo_core_1.actionResult)([internalEventRecord]);
41
52
  });
@@ -1,3 +1,9 @@
1
1
  import { QPQBinaryData } from 'quidproquo-core';
2
+ import { FileUploadErrorTypeEnum, FileUploadSettings } from 'quidproquo-webserver';
2
3
  import { APIGatewayEvent } from 'aws-lambda';
3
- export declare const parseMultipartFormData: (event: APIGatewayEvent) => Promise<QPQBinaryData[]>;
4
+ export declare class FileUploadValidationError extends Error {
5
+ readonly errorType: FileUploadErrorTypeEnum;
6
+ constructor(errorType: FileUploadErrorTypeEnum, message: string);
7
+ }
8
+ export declare const sanitizeFilename: (filename?: string) => string;
9
+ export declare const parseMultipartFormData: (event: APIGatewayEvent, fileUploadSettings: FileUploadSettings) => Promise<QPQBinaryData[]>;
@@ -3,42 +3,111 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.parseMultipartFormData = void 0;
6
+ exports.parseMultipartFormData = exports.sanitizeFilename = exports.FileUploadValidationError = void 0;
7
+ const quidproquo_webserver_1 = require("quidproquo-webserver");
7
8
  const busboy_1 = __importDefault(require("busboy"));
8
- const parseMultipartFormData = (event) => new Promise((resolve, reject) => {
9
+ class FileUploadValidationError extends Error {
10
+ constructor(errorType, message) {
11
+ super(message);
12
+ this.errorType = errorType;
13
+ this.name = 'FileUploadValidationError';
14
+ }
15
+ }
16
+ exports.FileUploadValidationError = FileUploadValidationError;
17
+ // Reduce a client-supplied filename to a bare, path-traversal-safe name.
18
+ const sanitizeFilename = (filename) => {
19
+ const baseName = (filename || '')
20
+ .split(/[/\\]/)
21
+ .filter((segment) => segment.length > 0)
22
+ .pop();
23
+ // eslint-disable-next-line no-control-regex
24
+ const cleaned = (baseName || '').replace(/[\x00-\x1f]/g, '');
25
+ if (!cleaned || cleaned === '.' || cleaned === '..') {
26
+ return 'unnamed';
27
+ }
28
+ return cleaned;
29
+ };
30
+ exports.sanitizeFilename = sanitizeFilename;
31
+ const isMimeTypeAllowed = (mimeType, allowedMimeTypes) => {
32
+ if (!allowedMimeTypes) {
33
+ return true;
34
+ }
35
+ const normalizedMimeType = (mimeType || '').toLowerCase();
36
+ return allowedMimeTypes.some((allowed) => {
37
+ const normalizedAllowed = allowed.toLowerCase();
38
+ return normalizedAllowed.endsWith('/*')
39
+ ? normalizedMimeType.startsWith(normalizedAllowed.slice(0, -1))
40
+ : normalizedMimeType === normalizedAllowed;
41
+ });
42
+ };
43
+ const parseMultipartFormData = (event, fileUploadSettings) => new Promise((resolve, reject) => {
9
44
  const busboy = (0, busboy_1.default)({
10
45
  headers: {
11
46
  'content-type': event.headers['content-type'] || event.headers['Content-Type'],
12
47
  },
48
+ limits: {
49
+ fileSize: fileUploadSettings.maxFileSizeBytes,
50
+ files: fileUploadSettings.maxFileCount,
51
+ fields: fileUploadSettings.maxFieldCount,
52
+ fieldSize: fileUploadSettings.maxFieldSizeBytes,
53
+ },
13
54
  });
14
55
  const result = {
15
56
  files: [],
16
57
  fields: {},
17
58
  };
59
+ let settled = false;
60
+ const rejectOnce = (error) => {
61
+ if (!settled) {
62
+ settled = true;
63
+ reject(error);
64
+ }
65
+ };
18
66
  busboy.on('file', (name, file, info) => {
19
- let fileBuffer = Buffer.alloc(0);
67
+ const filename = (0, exports.sanitizeFilename)(info.filename);
68
+ if (!isMimeTypeAllowed(info.mimeType, fileUploadSettings.allowedMimeTypes)) {
69
+ rejectOnce(new FileUploadValidationError(quidproquo_webserver_1.FileUploadErrorTypeEnum.disallowedMimeType, `File "${filename}" has disallowed content type "${info.mimeType}"`));
70
+ file.resume();
71
+ return;
72
+ }
73
+ const chunks = [];
20
74
  file.on('data', (data) => {
21
- fileBuffer = Buffer.concat([fileBuffer, data]); // Concatenate the new data to the existing buffer
75
+ chunks.push(data);
76
+ });
77
+ // Emitted when the stream is truncated at limits.fileSize - a silently truncated upload must fail, not succeed
78
+ file.on('limit', () => {
79
+ rejectOnce(new FileUploadValidationError(quidproquo_webserver_1.FileUploadErrorTypeEnum.fileTooLarge, `File "${filename}" exceeds the maximum file size of ${fileUploadSettings.maxFileSizeBytes} bytes`));
22
80
  });
23
81
  file.on('end', () => {
24
- if (fileBuffer.length > 0) {
82
+ const content = Buffer.concat(chunks);
83
+ if (!settled && content.length > 0) {
25
84
  result.files.push({
26
- filename: info.filename,
85
+ filename,
27
86
  contentType: info.mimeType,
28
87
  encoding: info.encoding,
29
88
  name: name,
30
- content: fileBuffer,
89
+ content,
31
90
  });
32
91
  }
33
92
  });
34
93
  });
94
+ busboy.on('filesLimit', () => {
95
+ rejectOnce(new FileUploadValidationError(quidproquo_webserver_1.FileUploadErrorTypeEnum.tooManyFiles, `Upload exceeds the maximum of ${fileUploadSettings.maxFileCount} files`));
96
+ });
97
+ busboy.on('fieldsLimit', () => {
98
+ rejectOnce(new FileUploadValidationError(quidproquo_webserver_1.FileUploadErrorTypeEnum.tooManyFields, `Upload exceeds the maximum of ${fileUploadSettings.maxFieldCount} fields`));
99
+ });
35
100
  busboy.on('field', (name, value) => {
36
101
  result.fields[name] = value;
37
102
  });
38
103
  busboy.on('error', (error) => {
39
- reject(error);
104
+ rejectOnce(error);
40
105
  });
41
106
  busboy.on('finish', () => {
107
+ if (settled) {
108
+ return;
109
+ }
110
+ settled = true;
42
111
  const qpqBinaryFiles = result.files.map((f) => ({
43
112
  filename: f.filename,
44
113
  mimetype: f.contentType,
@@ -6,6 +6,7 @@ export * from './eventBus';
6
6
  export * from './file';
7
7
  export * from './graphDatabase';
8
8
  export * from './keyValueStore';
9
+ export * from './metric';
9
10
  export * from './queue';
10
11
  export * from './system';
11
12
  export * from './userDirectory';
@@ -32,6 +32,7 @@ __exportStar(require("./eventBus"), exports);
32
32
  __exportStar(require("./file"), exports);
33
33
  __exportStar(require("./graphDatabase"), exports);
34
34
  __exportStar(require("./keyValueStore"), exports);
35
+ __exportStar(require("./metric"), exports);
35
36
  __exportStar(require("./queue"), exports);
36
37
  __exportStar(require("./system"), exports);
37
38
  __exportStar(require("./userDirectory"), exports);
@@ -44,10 +45,11 @@ const eventBus_1 = require("./eventBus");
44
45
  const file_1 = require("./file");
45
46
  const graphDatabase_1 = require("./graphDatabase");
46
47
  const keyValueStore_1 = require("./keyValueStore");
48
+ const metric_1 = require("./metric");
47
49
  const queue_1 = require("./queue");
48
50
  const system_1 = require("./system");
49
51
  const userDirectory_1 = require("./userDirectory");
50
52
  const getCoreActionProcessor = (qpqConfig, dynamicModuleLoader) => __awaiter(void 0, void 0, void 0, function* () {
51
- return (Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (yield (0, ai_1.getAiActionProcessor)(qpqConfig, dynamicModuleLoader))), (yield (0, apiKeyValidation_1.getApiKeyValidationActionProcessor)(qpqConfig, dynamicModuleLoader))), (yield (0, file_1.getFileActionProcessor)(qpqConfig, dynamicModuleLoader))), (yield (0, keyValueStore_1.getKeyValueStoreActionProcessor)(qpqConfig, dynamicModuleLoader))), (yield (0, queue_1.getQueueActionProcessor)(qpqConfig, dynamicModuleLoader))), (yield (0, userDirectory_1.getUserDirectoryActionProcessor)(qpqConfig, dynamicModuleLoader))), (yield (0, eventBus_1.getEventBusActionProcessor)(qpqConfig, dynamicModuleLoader))), (yield (0, system_1.getSystemActionProcessor)(qpqConfig, dynamicModuleLoader))), (yield (0, config_1.getConfigActionProcessor)(qpqConfig, dynamicModuleLoader))), (yield (0, event_1.getApiGatewayApiEventEventProcessor)(qpqConfig, dynamicModuleLoader))), (yield (0, graphDatabase_1.getGraphDatabaseActionProcessor)(qpqConfig, dynamicModuleLoader))), (yield (0, quidproquo_webserver_1.getRouteAuthValidationActionProcessor)(qpqConfig, dynamicModuleLoader))));
53
+ return (Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (yield (0, ai_1.getAiActionProcessor)(qpqConfig, dynamicModuleLoader))), (yield (0, apiKeyValidation_1.getApiKeyValidationActionProcessor)(qpqConfig, dynamicModuleLoader))), (yield (0, file_1.getFileActionProcessor)(qpqConfig, dynamicModuleLoader))), (yield (0, keyValueStore_1.getKeyValueStoreActionProcessor)(qpqConfig, dynamicModuleLoader))), (yield (0, metric_1.getMetricActionProcessor)(qpqConfig, dynamicModuleLoader))), (yield (0, queue_1.getQueueActionProcessor)(qpqConfig, dynamicModuleLoader))), (yield (0, userDirectory_1.getUserDirectoryActionProcessor)(qpqConfig, dynamicModuleLoader))), (yield (0, eventBus_1.getEventBusActionProcessor)(qpqConfig, dynamicModuleLoader))), (yield (0, system_1.getSystemActionProcessor)(qpqConfig, dynamicModuleLoader))), (yield (0, config_1.getConfigActionProcessor)(qpqConfig, dynamicModuleLoader))), (yield (0, event_1.getApiGatewayApiEventEventProcessor)(qpqConfig, dynamicModuleLoader))), (yield (0, graphDatabase_1.getGraphDatabaseActionProcessor)(qpqConfig, dynamicModuleLoader))), (yield (0, quidproquo_webserver_1.getRouteAuthValidationActionProcessor)(qpqConfig, dynamicModuleLoader))));
52
54
  });
53
55
  exports.getCoreActionProcessor = getCoreActionProcessor;
@@ -0,0 +1,2 @@
1
+ import { ActionProcessorListResolver } from 'quidproquo-core';
2
+ export declare const getMetricPutActionProcessor: ActionProcessorListResolver;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.getMetricPutActionProcessor = void 0;
13
+ const quidproquo_core_1 = require("quidproquo-core");
14
+ // Emits the metric as a CloudWatch Embedded Metric Format (EMF) log line: CloudWatch
15
+ // extracts metrics from the function's log stream - no api calls, no extra IAM, no
16
+ // latency on the story. Cost: each unique metricName x dimension-combination is one
17
+ // custom metric (~$0.30/mo) - the standard dimensions are service/environment(/feature),
18
+ // so keep payload dimensions low-cardinality (never per-user / per-request ids).
19
+ const getProcessMetricPut = (qpqConfig) => {
20
+ // The namespace is the deployed app instance - app/environment(/feature) - mirroring how
21
+ // every qpq resource name embeds the deployment identity. Apps sharing an account can't
22
+ // merge same-named metrics, per-developer feature sandboxes (e.g. qpq/docgen/development/jane)
23
+ // never pollute mainline, and dimensions are reserved for slices WITHIN the deployment
24
+ // (service + caller-supplied).
25
+ const application = quidproquo_core_1.qpqCoreUtils.getApplicationName(qpqConfig);
26
+ const environment = quidproquo_core_1.qpqCoreUtils.getApplicationModuleEnvironment(qpqConfig);
27
+ const feature = quidproquo_core_1.qpqCoreUtils.getApplicationModuleFeature(qpqConfig);
28
+ const namespace = `qpq/${application}/${environment}${feature ? `/${feature}` : ''}`;
29
+ const service = quidproquo_core_1.qpqCoreUtils.getApplicationModuleName(qpqConfig);
30
+ return (_a) => __awaiter(void 0, [_a], void 0, function* ({ metricName, value, unit, dimensions }) {
31
+ const allDimensions = Object.assign({ service }, dimensions);
32
+ const emf = Object.assign(Object.assign({ _aws: {
33
+ Timestamp: Date.now(),
34
+ CloudWatchMetrics: [
35
+ {
36
+ Namespace: namespace,
37
+ Dimensions: [Object.keys(allDimensions)],
38
+ Metrics: [{ Name: metricName, Unit: unit !== null && unit !== void 0 ? unit : quidproquo_core_1.MetricUnit.count }],
39
+ },
40
+ ],
41
+ } }, allDimensions), { [metricName]: value });
42
+ console.log(JSON.stringify(emf));
43
+ return (0, quidproquo_core_1.actionResult)(void 0);
44
+ });
45
+ };
46
+ const getMetricPutActionProcessor = (qpqConfig) => __awaiter(void 0, void 0, void 0, function* () {
47
+ return ({
48
+ [quidproquo_core_1.MetricActionType.Put]: getProcessMetricPut(qpqConfig),
49
+ });
50
+ });
51
+ exports.getMetricPutActionProcessor = getMetricPutActionProcessor;
@@ -0,0 +1,2 @@
1
+ import { ActionProcessorListResolver } from 'quidproquo-core';
2
+ export declare const getMetricActionProcessor: ActionProcessorListResolver;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.getMetricActionProcessor = void 0;
13
+ const getMetricPutActionProcessor_1 = require("./getMetricPutActionProcessor");
14
+ const getMetricActionProcessor = (qpqConfig, dynamicModuleLoader) => __awaiter(void 0, void 0, void 0, function* () {
15
+ return (Object.assign({}, (yield (0, getMetricPutActionProcessor_1.getMetricPutActionProcessor)(qpqConfig, dynamicModuleLoader))));
16
+ });
17
+ exports.getMetricActionProcessor = getMetricActionProcessor;
@@ -10,9 +10,16 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.dynamicModuleLoader = void 0;
13
- // @ts-expect-error - Special webpack file injected
13
+ // @ts-expect-error - injected at build time by QpqPlugin (webpack virtual module)
14
14
  const quidproquo_dynamic_loader_1 = require("quidproquo-dynamic-loader");
15
+ const federatedModuleStore_1 = require("../../logic/federatedModuleStore");
15
16
  const dynamicModuleLoader = (qpqFunctionRuntime) => __awaiter(void 0, void 0, void 0, function* () {
17
+ // Tier 1: federated. undefined means "nothing published for this runtime" -> fall through.
18
+ const federatedStory = yield (0, federatedModuleStore_1.loadFederatedStory)(quidproquo_dynamic_loader_1.qpqConfig, qpqFunctionRuntime);
19
+ if (federatedStory !== undefined) {
20
+ return federatedStory;
21
+ }
22
+ // Tier 2: the statically bundled story (or a fail-fast throw for a thin shell).
16
23
  return (0, quidproquo_dynamic_loader_1.qpqDynamicModuleLoader)(qpqFunctionRuntime);
17
24
  });
18
25
  exports.dynamicModuleLoader = dynamicModuleLoader;
@@ -0,0 +1 @@
1
+ export declare const getApiKeyValue: (region: string, apiKeyId: string) => Promise<string | undefined>;
@@ -9,21 +9,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.getApiKeys = void 0;
12
+ exports.getApiKeyValue = void 0;
13
13
  const client_api_gateway_1 = require("@aws-sdk/client-api-gateway");
14
14
  const createAwsClient_1 = require("../createAwsClient");
15
- const getApiKeys = (region, ...keyNames) => __awaiter(void 0, void 0, void 0, function* () {
16
- var _a;
15
+ // Fetch a single api key's value by id - keys are read individually (not listed) so the
16
+ // IAM grant can be scoped per-key with tag conditions rather than the /apikeys collection.
17
+ const getApiKeyValue = (region, apiKeyId) => __awaiter(void 0, void 0, void 0, function* () {
17
18
  const apiGatewayClient = (0, createAwsClient_1.createAwsClient)(client_api_gateway_1.APIGatewayClient, { region });
18
- const input = {
19
- includeValues: true,
20
- limit: 500,
21
- };
22
- const res = yield apiGatewayClient.send(new client_api_gateway_1.GetApiKeysCommand(input));
23
- return (((_a = res.items) === null || _a === void 0 ? void 0 : _a.filter((i) => keyNames.indexOf(i.name) >= 0).map((i) => ({
24
- name: i.name,
25
- value: i.value,
26
- description: i.description,
27
- }))) || []);
19
+ const res = yield apiGatewayClient.send(new client_api_gateway_1.GetApiKeyCommand({
20
+ apiKey: apiKeyId,
21
+ includeValue: true,
22
+ }));
23
+ return res.value;
28
24
  });
29
- exports.getApiKeys = getApiKeys;
25
+ exports.getApiKeyValue = getApiKeyValue;
@@ -0,0 +1,10 @@
1
+ export declare enum FederatedModuleLoadErrorCode {
2
+ unsupportedStoreUrl = "unsupportedStoreUrl",
3
+ manifestInvalid = "manifestInvalid",
4
+ containerLoadFailed = "containerLoadFailed",
5
+ storyNotExported = "storyNotExported"
6
+ }
7
+ export declare class FederatedModuleLoadError extends Error {
8
+ readonly code: FederatedModuleLoadErrorCode;
9
+ constructor(code: FederatedModuleLoadErrorCode, message: string);
10
+ }
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FederatedModuleLoadError = exports.FederatedModuleLoadErrorCode = void 0;
4
+ // Discriminable failures raised inside loadFederatedStory. All of them are ultimately
5
+ // caught there and turned into a fall-back-to-bundled (with a warning) - they exist so
6
+ // logs/backoff can tell WHY a store didn't load, not to crash the request.
7
+ var FederatedModuleLoadErrorCode;
8
+ (function (FederatedModuleLoadErrorCode) {
9
+ // Store URL wasn't s3:// or file:// (a config/env mistake).
10
+ FederatedModuleLoadErrorCode["unsupportedStoreUrl"] = "unsupportedStoreUrl";
11
+ // manifest.json parsed but is missing required fields (bad/partial publish).
12
+ FederatedModuleLoadErrorCode["manifestInvalid"] = "manifestInvalid";
13
+ // A version's files downloaded but the entry didn't expose a valid MF container.
14
+ FederatedModuleLoadErrorCode["containerLoadFailed"] = "containerLoadFailed";
15
+ // The manifest exposed the module, but it didn't export the requested story name
16
+ // (e.g. a story was renamed in the remote but the manifest still lists it).
17
+ FederatedModuleLoadErrorCode["storyNotExported"] = "storyNotExported";
18
+ })(FederatedModuleLoadErrorCode || (exports.FederatedModuleLoadErrorCode = FederatedModuleLoadErrorCode = {}));
19
+ class FederatedModuleLoadError extends Error {
20
+ constructor(code, message) {
21
+ super(message);
22
+ this.code = code;
23
+ this.name = 'FederatedModuleLoadError';
24
+ }
25
+ }
26
+ exports.FederatedModuleLoadError = FederatedModuleLoadError;
@@ -0,0 +1,8 @@
1
+ export interface FederatedModuleStoreManifest {
2
+ containerName: string;
3
+ service: string;
4
+ hash: string;
5
+ entry: string;
6
+ files: string[];
7
+ exposes: Record<string, string>;
8
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,4 @@
1
+ export * from './FederatedModuleLoadError';
2
+ export * from './FederatedModuleStoreManifest';
3
+ export * from './loadFederatedStory';
4
+ export * from './sharedPackages';
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./FederatedModuleLoadError"), exports);
18
+ __exportStar(require("./FederatedModuleStoreManifest"), exports);
19
+ __exportStar(require("./loadFederatedStory"), exports);
20
+ __exportStar(require("./sharedPackages"), exports);
@@ -0,0 +1,3 @@
1
+ import { QPQConfig, QpqFunctionRuntime } from 'quidproquo-core';
2
+ export declare const resetFederatedModuleStoreCache: () => void;
3
+ export declare const loadFederatedStory: <T = any>(qpqConfig: QPQConfig, qpqFunctionRuntime: QpqFunctionRuntime) => Promise<T | undefined>;