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
@@ -0,0 +1,295 @@
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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.loadFederatedStory = exports.resetFederatedModuleStoreCache = void 0;
46
+ const quidproquo_config_aws_1 = require("quidproquo-config-aws");
47
+ const quidproquoCore = __importStar(require("quidproquo-core"));
48
+ const quidproquo_core_1 = require("quidproquo-core");
49
+ const quidproquoWebserver = __importStar(require("quidproquo-webserver"));
50
+ const fs = __importStar(require("fs"));
51
+ const module_1 = require("module");
52
+ const path = __importStar(require("path"));
53
+ const url_1 = require("url");
54
+ const client_s3_1 = require("@aws-sdk/client-s3");
55
+ const runtime_1 = require("@module-federation/enhanced/runtime");
56
+ const createAwsClient_1 = require("../createAwsClient");
57
+ const FederatedModuleLoadError_1 = require("./FederatedModuleLoadError");
58
+ const sharedPackages_1 = require("./sharedPackages");
59
+ // How long a resolved state is served before the manifest is re-checked for a new
60
+ // version. Overridable per service (defineFederatedModuleStore recheckMs -> this env).
61
+ // Lower = faster pickup of new publishes at the cost of one small S3 GET per container
62
+ // per interval. The re-check runs in the BACKGROUND so it never blocks a request.
63
+ const DEFAULT_RECHECK_MS = 60000;
64
+ const getRecheckMs = () => {
65
+ const raw = process.env.federatedCodeStoreRecheckMs;
66
+ const parsed = raw ? parseInt(raw, 10) : NaN;
67
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_RECHECK_MS;
68
+ };
69
+ let resolvedState;
70
+ let recheckAfter = 0;
71
+ let inflightResolve;
72
+ let hostInitialised = false;
73
+ // Test seam - forces the next call to re-resolve the store from env.
74
+ const resetFederatedModuleStoreCache = () => {
75
+ resolvedState = undefined;
76
+ recheckAfter = 0;
77
+ inflightResolve = undefined;
78
+ };
79
+ exports.resetFederatedModuleStoreCache = resetFederatedModuleStoreCache;
80
+ // ── Transport ────────────────────────────────────────────────────────────────
81
+ // Returns a function that reads a path RELATIVE to the store (e.g. 'manifest.json'
82
+ // or '<hash>/remoteEntry.js'). Two backends: s3:// for real deploys, file:// for
83
+ // local/dev and tests. The store URL already includes the per-service prefix, so
84
+ // callers pass only the in-service relative path.
85
+ const getFileFetcher = (storeUrl, qpqConfig) => {
86
+ if (storeUrl.startsWith('s3://')) {
87
+ const url = new URL(storeUrl);
88
+ const bucketName = url.host;
89
+ const keyPrefix = url.pathname.replace(/^\/|\/$/g, '');
90
+ const region = quidproquo_config_aws_1.qpqConfigAwsUtils.getApplicationModuleDeployRegion(qpqConfig);
91
+ // Pin the owner so a same-name bucket in another account can't be squatted to
92
+ // serve code we'd then require() with the service role's credentials.
93
+ const expectedBucketOwner = quidproquo_config_aws_1.qpqConfigAwsUtils.getApplicationModuleDeployAccountId(qpqConfig);
94
+ const s3Client = (0, createAwsClient_1.createAwsClient)(client_s3_1.S3Client, { region });
95
+ return (relativePath) => __awaiter(void 0, void 0, void 0, function* () {
96
+ var _a;
97
+ const response = yield s3Client.send(new client_s3_1.GetObjectCommand({
98
+ Bucket: bucketName,
99
+ Key: keyPrefix ? `${keyPrefix}/${relativePath}` : relativePath,
100
+ ExpectedBucketOwner: expectedBucketOwner,
101
+ }));
102
+ const bytes = yield ((_a = response.Body) === null || _a === void 0 ? void 0 : _a.transformToByteArray());
103
+ if (!bytes) {
104
+ throw new FederatedModuleLoadError_1.FederatedModuleLoadError(FederatedModuleLoadError_1.FederatedModuleLoadErrorCode.containerLoadFailed, `empty response reading [${relativePath}]`);
105
+ }
106
+ return Buffer.from(bytes);
107
+ });
108
+ }
109
+ if (storeUrl.startsWith('file://')) {
110
+ const baseDirectory = (0, url_1.fileURLToPath)(storeUrl.endsWith('/') ? storeUrl : `${storeUrl}/`);
111
+ return (relativePath) => __awaiter(void 0, void 0, void 0, function* () { return fs.promises.readFile(path.join(baseDirectory, relativePath)); });
112
+ }
113
+ throw new FederatedModuleLoadError_1.FederatedModuleLoadError(FederatedModuleLoadError_1.FederatedModuleLoadErrorCode.unsupportedStoreUrl, `unsupported federated code store url [${storeUrl}]`);
114
+ };
115
+ // "Nothing published yet" is a legitimate not-found: the manifest key is absent
116
+ // (NoSuchKey/NotFound) or the store bucket isn't created yet, which happens normally
117
+ // when a lambda cold-starts between deploy phases (NoSuchBucket). We do NOT include
118
+ // AccessDenied: a permission failure is a real misconfiguration and must surface (as a
119
+ // warning, and be retried) rather than masquerade as 'not published'.
120
+ const isManifestNotFoundError = (error) => {
121
+ const name = (error === null || error === void 0 ? void 0 : error.name) || '';
122
+ const code = (error === null || error === void 0 ? void 0 : error.code) || '';
123
+ return ['NoSuchKey', 'NotFound', 'NoSuchBucket'].includes(name) || code === 'ENOENT';
124
+ };
125
+ // ── Module-federation shared scope ───────────────────────────────────────────
126
+ // The host provides the framework packages to the container's share scope so remote
127
+ // story code resolves the SAME module instances as the bundled runtime. The package
128
+ // list is shared with the remote build so the two can't drift (see sharedPackages).
129
+ const SHARED_LIBS = {
130
+ 'quidproquo-core': quidproquoCore,
131
+ 'quidproquo-webserver': quidproquoWebserver,
132
+ };
133
+ const getHostSharedModules = () => Object.fromEntries(sharedPackages_1.FEDERATED_SHARED_PACKAGE_NAMES.map((name) => [
134
+ name,
135
+ {
136
+ version: '0.0.0',
137
+ lib: () => SHARED_LIBS[name],
138
+ shareConfig: { singleton: true, requiredVersion: false },
139
+ },
140
+ ]));
141
+ const ensureHostInitialised = () => {
142
+ if (hostInitialised) {
143
+ return;
144
+ }
145
+ (0, runtime_1.init)({
146
+ name: 'qpq_federated_host',
147
+ remotes: [],
148
+ shared: getHostSharedModules(),
149
+ });
150
+ hostInitialised = true;
151
+ };
152
+ // ── Version loading ──────────────────────────────────────────────────────────
153
+ // Downloads a manifest's files (atomically) and registers the container with the MF
154
+ // runtime under a HASH-UNIQUE remote name, so loadRemote can't serve a previous
155
+ // version's module from its cache when the hash changes. Returns the runtime name.
156
+ // This is the step that actually puts a new version's code into effect.
157
+ const loadStoreVersion = (getFile, manifest) => __awaiter(void 0, void 0, void 0, function* () {
158
+ var _a, _b;
159
+ const cacheRoot = process.env.federatedCodeStoreCacheDir || '/tmp/qpq-federated-code';
160
+ const versionDirectory = path.join(cacheRoot, manifest.containerName, manifest.hash);
161
+ yield fs.promises.mkdir(versionDirectory, { recursive: true });
162
+ yield Promise.all(manifest.files.map((file) => __awaiter(void 0, void 0, void 0, function* () {
163
+ const localPath = path.join(versionDirectory, file);
164
+ // Files are immutable under their hash dir AND only ever appear via the atomic
165
+ // rename below, so an existing file is always complete and valid - this is what
166
+ // lets a re-check of an unchanged version skip re-downloading.
167
+ if (fs.existsSync(localPath)) {
168
+ return;
169
+ }
170
+ const data = yield getFile(`${manifest.hash}/${file}`);
171
+ yield fs.promises.mkdir(path.dirname(localPath), { recursive: true });
172
+ // Write to a temp name then rename: rename is atomic on the same filesystem, so a
173
+ // crash/timeout mid-write never leaves a truncated file that a later invocation
174
+ // would trust via the existsSync check above.
175
+ const tempPath = `${localPath}.${process.pid}.tmp`;
176
+ yield fs.promises.writeFile(tempPath, data);
177
+ yield fs.promises.rename(tempPath, localPath);
178
+ })));
179
+ const entryPath = path.join(versionDirectory, manifest.entry);
180
+ const containerRequire = (0, module_1.createRequire)(entryPath);
181
+ const containerModule = containerRequire(entryPath);
182
+ const container = (_b = (_a = containerModule[manifest.containerName]) !== null && _a !== void 0 ? _a : containerModule.default) !== null && _b !== void 0 ? _b : containerModule;
183
+ if (typeof (container === null || container === void 0 ? void 0 : container.get) !== 'function' || typeof (container === null || container === void 0 ? void 0 : container.init) !== 'function') {
184
+ throw new FederatedModuleLoadError_1.FederatedModuleLoadError(FederatedModuleLoadError_1.FederatedModuleLoadErrorCode.containerLoadFailed, `[${manifest.entry}] does not export a module federation container named [${manifest.containerName}]`);
185
+ }
186
+ // entryGlobalName is the container's real (build-time) library name; the registered
187
+ // remote name is hash-unique so each version is a distinct loadRemote key.
188
+ globalThis[manifest.containerName] = container;
189
+ ensureHostInitialised();
190
+ const runtimeName = `${manifest.containerName}__${manifest.hash}`;
191
+ (0, runtime_1.registerRemotes)([
192
+ {
193
+ name: runtimeName,
194
+ entry: entryPath,
195
+ type: 'global',
196
+ entryGlobalName: manifest.containerName,
197
+ },
198
+ ]);
199
+ return runtimeName;
200
+ });
201
+ // ── Probe & resolve (the caching state machine) ──────────────────────────────
202
+ // Reads the manifest and decides the current state. If the hash is unchanged from what
203
+ // we already loaded, keeps the existing container (no re-sync). Not-found -> unavailable.
204
+ const probeStore = (qpqConfig, storeUrl, current) => __awaiter(void 0, void 0, void 0, function* () {
205
+ const getFile = getFileFetcher(storeUrl, qpqConfig);
206
+ let manifest;
207
+ try {
208
+ manifest = JSON.parse((yield getFile('manifest.json')).toString('utf8'));
209
+ }
210
+ catch (error) {
211
+ if (isManifestNotFoundError(error)) {
212
+ console.log(`federated code store [${storeUrl}] has no manifest - using bundled modules`);
213
+ return { available: false };
214
+ }
215
+ throw error;
216
+ }
217
+ if (!(manifest === null || manifest === void 0 ? void 0 : manifest.containerName) ||
218
+ !(manifest === null || manifest === void 0 ? void 0 : manifest.hash) ||
219
+ !(manifest === null || manifest === void 0 ? void 0 : manifest.entry) ||
220
+ !Array.isArray(manifest === null || manifest === void 0 ? void 0 : manifest.files) ||
221
+ typeof (manifest === null || manifest === void 0 ? void 0 : manifest.exposes) !== 'object' ||
222
+ manifest.exposes === null) {
223
+ throw new FederatedModuleLoadError_1.FederatedModuleLoadError(FederatedModuleLoadError_1.FederatedModuleLoadErrorCode.manifestInvalid, `invalid manifest in federated code store [${storeUrl}]`);
224
+ }
225
+ if ((current === null || current === void 0 ? void 0 : current.available) && current.manifest.hash === manifest.hash) {
226
+ return current;
227
+ }
228
+ const runtimeName = yield loadStoreVersion(getFile, manifest);
229
+ return { available: true, manifest, runtimeName };
230
+ });
231
+ // Resolves the store, re-checking the manifest after the recheck interval. The re-check
232
+ // runs in the background: a stale-but-usable state is served immediately so a request
233
+ // never blocks on S3; only the very first resolve (no state yet) awaits.
234
+ const resolveStore = (qpqConfig, storeUrl) => {
235
+ if (resolvedState && Date.now() < recheckAfter) {
236
+ return Promise.resolve(resolvedState);
237
+ }
238
+ if (!inflightResolve) {
239
+ inflightResolve = probeStore(qpqConfig, storeUrl, resolvedState)
240
+ .then((state) => {
241
+ resolvedState = state;
242
+ recheckAfter = Date.now() + getRecheckMs();
243
+ return state;
244
+ })
245
+ .catch((error) => {
246
+ // Back off and keep serving whatever we had (or unavailable). Surfaced as a
247
+ // warning so real errors (e.g. AccessDenied) are visible, not silent.
248
+ console.warn('federated code store check failed - keeping current modules', error);
249
+ recheckAfter = Date.now() + getRecheckMs();
250
+ resolvedState = resolvedState !== null && resolvedState !== void 0 ? resolvedState : { available: false };
251
+ return resolvedState;
252
+ })
253
+ .finally(() => {
254
+ inflightResolve = undefined;
255
+ });
256
+ }
257
+ // Serve the stale state instantly while the refresh happens; only block if we have
258
+ // nothing yet.
259
+ return resolvedState ? Promise.resolve(resolvedState) : inflightResolve;
260
+ };
261
+ // ── Public entry ─────────────────────────────────────────────────────────────
262
+ // Called by dynamicModuleLoader for every story load. Returns the story function, or
263
+ // undefined to signal "use the bundled module" (no store, nothing published, runtime
264
+ // not exposed, or any error - availability always wins over federation).
265
+ const loadFederatedStory = (qpqConfig, qpqFunctionRuntime) => __awaiter(void 0, void 0, void 0, function* () {
266
+ const storeUrl = process.env.federatedCodeStoreUrl;
267
+ if (!storeUrl) {
268
+ return undefined;
269
+ }
270
+ try {
271
+ const storeState = yield resolveStore(qpqConfig, storeUrl);
272
+ if (!storeState.available) {
273
+ return undefined;
274
+ }
275
+ const federatedKey = (0, quidproquo_core_1.getFederatedKeyFromQpqFunctionRuntime)(qpqFunctionRuntime);
276
+ const exposePath = storeState.manifest.exposes[federatedKey];
277
+ if (!exposePath) {
278
+ console.warn(`federated code store has no module for [${federatedKey}] - using the bundled module`);
279
+ return undefined;
280
+ }
281
+ const remoteModule = yield (0, runtime_1.loadRemote)(`${storeState.runtimeName}/${exposePath}`);
282
+ const storyName = quidproquo_core_1.qpqCoreUtils.getStoryNameFromQpqFunctionRuntime(qpqFunctionRuntime);
283
+ const story = remoteModule === null || remoteModule === void 0 ? void 0 : remoteModule[storyName];
284
+ if (!story) {
285
+ throw new FederatedModuleLoadError_1.FederatedModuleLoadError(FederatedModuleLoadError_1.FederatedModuleLoadErrorCode.storyNotExported, `story [${storyName}] is not exported from federated module [${exposePath}]`);
286
+ }
287
+ return story;
288
+ }
289
+ catch (error) {
290
+ // Availability first: a broken store must never take the service down
291
+ console.warn('federated module load failed - using the bundled module', error);
292
+ return undefined;
293
+ }
294
+ });
295
+ exports.loadFederatedStory = loadFederatedStory;
@@ -0,0 +1 @@
1
+ export declare const FEDERATED_SHARED_PACKAGE_NAMES: readonly ["quidproquo-core", "quidproquo-webserver"];
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FEDERATED_SHARED_PACKAGE_NAMES = void 0;
4
+ // The framework packages the lambda host provides to the module-federation share
5
+ // scope, so a federated remote resolves the SAME module instances as the host
6
+ // (instanceof checks and module-level state stay coherent). Single source of truth
7
+ // for BOTH the host runtime (loadFederatedStory getHostSharedModules) and the
8
+ // remote build (quidproquo-deploy-webpack getWebpackConfigForQpqRemote) — the two
9
+ // lists must never drift, or the remote would silently bundle its own copy.
10
+ exports.FEDERATED_SHARED_PACKAGE_NAMES = ['quidproquo-core', 'quidproquo-webserver'];
@@ -1 +1,2 @@
1
1
  export * from './cognito';
2
+ export * from './federatedModuleStore';
@@ -15,3 +15,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./cognito"), exports);
18
+ __exportStar(require("./federatedModuleStore"), exports);
@@ -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;
@@ -131,6 +131,19 @@ export const getCFExportNameWebsocketApiIdFromConfig = (websocketApiName, qpqCon
131
131
  const resourceName = websocketApiConfig.owner?.resourceNameOverride || websocketApiName;
132
132
  return getQpqRuntimeResourceName(resourceName, application, service, environment, feature, 'websocket-api-id-export');
133
133
  };
134
+ // SSM parameter holding a websocket api's AWS-generated id, written by the owning service's
135
+ // inf stack. Referencing services resolve it at deploy time to build exact execute-api
136
+ // grants (deploy the owning inf stack first - same owner-publishes/others-read pattern as
137
+ // the domain certificate arn parameter).
138
+ export const getWebsocketApiIdSsmParameterName = (websocketApiName, qpqConfig) => {
139
+ const websocketApiConfig = qpqWebServerUtils.getWebsocketEntryByApiName(websocketApiName, qpqConfig);
140
+ const application = websocketApiConfig.owner?.application || qpqCoreUtils.getApplicationName(qpqConfig);
141
+ const service = websocketApiConfig.owner?.module || qpqCoreUtils.getApplicationModuleName(qpqConfig);
142
+ const environment = websocketApiConfig.owner?.environment || qpqCoreUtils.getApplicationModuleEnvironment(qpqConfig);
143
+ const feature = websocketApiConfig.owner?.feature || qpqCoreUtils.getApplicationModuleFeature(qpqConfig);
144
+ const resourceName = websocketApiConfig.owner?.resourceNameOverride || websocketApiName;
145
+ return `/qpq/websocket/api-id/${getConfigRuntimeResourceName(resourceName, application, service, environment, feature)}`;
146
+ };
134
147
  export const getEventBusSnsTopicArn = (eventBusName, qpqConfig, module, environment, application, feature) => {
135
148
  const topicName = getConfigRuntimeResourceName(eventBusName, application, module, environment, feature);
136
149
  const accountInfo = getAwsServiceAccountInfoByDeploymentInfo(qpqConfig, module, environment, feature, application);
@@ -138,6 +151,13 @@ export const getEventBusSnsTopicArn = (eventBusName, qpqConfig, module, environm
138
151
  const region = accountInfo.awsRegion;
139
152
  return `arn:aws:sns:${region}:${awsAccountId}:${topicName}`;
140
153
  };
154
+ // The account stack owns account-level resources (cloud trail, budgets, security services)
155
+ // so they never share an app's lifecycle. The name is deliberately static: stack names are
156
+ // already namespaced per account+region, and the resources it holds meter/audit the whole
157
+ // account - an app/environment segment would only fake a separation that doesn't exist.
158
+ // Everything in one account converges on this one stack, so exactly ONE repo/config must
159
+ // own it, and actor (feature) deploys must not deploy it.
160
+ export const getAccountStackName = () => 'qpq-account';
141
161
  export const getBaseStackName = (qpqConfig) => {
142
162
  const appName = qpqCoreUtils.getApplicationName(qpqConfig);
143
163
  const moduleName = qpqCoreUtils.getApplicationModuleName(qpqConfig);
@@ -1,9 +1,10 @@
1
1
  import { qpqConfigAwsUtils } from 'quidproquo-config-aws';
2
- import { actionResult, qpqCoreUtils } from 'quidproquo-core';
2
+ import { actionResult } from 'quidproquo-core';
3
3
  import { ApiKeyValidationActionType } from 'quidproquo-webserver';
4
4
  import { timingSafeEqual } from 'crypto';
5
- import { getConfigRuntimeResourceName } from '../../../awsNamingUtils';
6
- import { getApiKeys } from '../../../logic/apiGateway/getApiKeys';
5
+ import { getCFExportNameApiKeyIdFromConfig } from '../../../awsNamingUtils';
6
+ import { getApiKeyValue } from '../../../logic/apiGateway/getApiKeyValue';
7
+ import { getExportedValue } from '../../../logic/cloudformation/getExportedValue';
7
8
  // Constant-time string equality. `timingSafeEqual` throws on length mismatch,
8
9
  // so guard the length check — the length itself isn't secret for API keys.
9
10
  const safeEqual = (a, b) => {
@@ -16,17 +17,20 @@ const safeEqual = (a, b) => {
16
17
  };
17
18
  const getProcessApiKeyValidationValidate = (qpqConfig) => {
18
19
  const region = qpqConfigAwsUtils.getApplicationModuleDeployRegion(qpqConfig);
19
- const application = qpqCoreUtils.getApplicationName(qpqConfig);
20
- const service = qpqCoreUtils.getApplicationModuleName(qpqConfig);
21
- const environment = qpqCoreUtils.getApplicationModuleEnvironment(qpqConfig);
22
- const feature = qpqCoreUtils.getApplicationModuleFeature(qpqConfig);
23
20
  return async ({ apiKeyValue, apiKeyReferences }) => {
24
- const realApiKeys = await getApiKeys(region, ...apiKeyReferences.map((apiKey) => {
25
- const apiKeyApplication = apiKey.applicationName || application;
26
- const apiKeyService = apiKey.serviceName || service;
27
- return getConfigRuntimeResourceName(apiKey.name, apiKeyApplication, apiKeyService, environment, feature);
21
+ // Fetch each referenced key individually (id via CFN export, then GetApiKey) rather than
22
+ // listing every key in the account - keeps the IAM grant per-key instead of /apikeys-wide.
23
+ const realApiKeyValues = await Promise.all(apiKeyReferences.map(async (apiKeyReference) => {
24
+ try {
25
+ const apiKeyId = await getExportedValue(getCFExportNameApiKeyIdFromConfig(apiKeyReference.name, qpqConfig, apiKeyReference.serviceName, apiKeyReference.applicationName), region);
26
+ return await getApiKeyValue(region, apiKeyId);
27
+ }
28
+ catch {
29
+ // An unresolvable reference behaves like the old list-and-filter miss - no match
30
+ return undefined;
31
+ }
28
32
  }));
29
- const matched = realApiKeys.some((apiKey) => safeEqual(apiKey.value, apiKeyValue));
33
+ const matched = realApiKeyValues.some((realApiKeyValue) => safeEqual(realApiKeyValue, apiKeyValue));
30
34
  return actionResult(matched);
31
35
  };
32
36
  };
@@ -1,8 +1,9 @@
1
1
  import { actionResult, EventActionType, qpqCoreUtils, } from 'quidproquo-core';
2
- import { qpqWebServerUtils } from 'quidproquo-webserver';
3
- import { parseMultipartFormData } from '../../utils/parseMultipartFormData';
2
+ import { FileUploadErrorTypeEnum, qpqWebServerUtils } from 'quidproquo-webserver';
3
+ import { FileUploadValidationError, parseMultipartFormData } from '../../utils/parseMultipartFormData';
4
4
  const getProcessGetRecords = (qpqConfig) => {
5
5
  const serviceName = qpqCoreUtils.getApplicationModuleName(qpqConfig);
6
+ const fileUploadSettings = qpqWebServerUtils.getFileUploadSettings(qpqConfig);
6
7
  return async ({ eventParams: [apiGatewayEvent, context] }) => {
7
8
  // Initialize `path` by removing the service name prefix from `apiGatewayEvent.path`.
8
9
  // This adjustment is necessary because the API gateway routes requests to services based on
@@ -26,7 +27,17 @@ const getProcessGetRecords = (qpqConfig) => {
26
27
  };
27
28
  // Transform the body if its a multipart/form-data
28
29
  if ((qpqWebServerUtils.getHeaderValue('Content-Type', apiGatewayEvent.headers) || '').startsWith('multipart/form-data') && apiGatewayEvent.body) {
29
- internalEventRecord.files = await parseMultipartFormData(apiGatewayEvent);
30
+ try {
31
+ internalEventRecord.files = await parseMultipartFormData(apiGatewayEvent, fileUploadSettings);
32
+ }
33
+ catch (error) {
34
+ // Stamp the failure on the record so the auto-respond step returns the matching
35
+ // 4xx before the route story runs - throwing here would fail the whole event as a 5xx
36
+ internalEventRecord.fileUploadError =
37
+ error instanceof FileUploadValidationError
38
+ ? { errorType: error.errorType, message: error.message }
39
+ : { errorType: FileUploadErrorTypeEnum.malformed, message: 'Unable to parse multipart/form-data body' };
40
+ }
30
41
  }
31
42
  return actionResult([internalEventRecord]);
32
43
  };
@@ -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[]>;
@@ -1,38 +1,106 @@
1
+ import { FileUploadErrorTypeEnum } from 'quidproquo-webserver';
1
2
  import Busboy from 'busboy';
2
- export const parseMultipartFormData = (event) => new Promise((resolve, reject) => {
3
+ export class FileUploadValidationError extends Error {
4
+ errorType;
5
+ constructor(errorType, message) {
6
+ super(message);
7
+ this.errorType = errorType;
8
+ this.name = 'FileUploadValidationError';
9
+ }
10
+ }
11
+ // Reduce a client-supplied filename to a bare, path-traversal-safe name.
12
+ export const sanitizeFilename = (filename) => {
13
+ const baseName = (filename || '')
14
+ .split(/[/\\]/)
15
+ .filter((segment) => segment.length > 0)
16
+ .pop();
17
+ // eslint-disable-next-line no-control-regex
18
+ const cleaned = (baseName || '').replace(/[\x00-\x1f]/g, '');
19
+ if (!cleaned || cleaned === '.' || cleaned === '..') {
20
+ return 'unnamed';
21
+ }
22
+ return cleaned;
23
+ };
24
+ const isMimeTypeAllowed = (mimeType, allowedMimeTypes) => {
25
+ if (!allowedMimeTypes) {
26
+ return true;
27
+ }
28
+ const normalizedMimeType = (mimeType || '').toLowerCase();
29
+ return allowedMimeTypes.some((allowed) => {
30
+ const normalizedAllowed = allowed.toLowerCase();
31
+ return normalizedAllowed.endsWith('/*')
32
+ ? normalizedMimeType.startsWith(normalizedAllowed.slice(0, -1))
33
+ : normalizedMimeType === normalizedAllowed;
34
+ });
35
+ };
36
+ export const parseMultipartFormData = (event, fileUploadSettings) => new Promise((resolve, reject) => {
3
37
  const busboy = Busboy({
4
38
  headers: {
5
39
  'content-type': event.headers['content-type'] || event.headers['Content-Type'],
6
40
  },
41
+ limits: {
42
+ fileSize: fileUploadSettings.maxFileSizeBytes,
43
+ files: fileUploadSettings.maxFileCount,
44
+ fields: fileUploadSettings.maxFieldCount,
45
+ fieldSize: fileUploadSettings.maxFieldSizeBytes,
46
+ },
7
47
  });
8
48
  const result = {
9
49
  files: [],
10
50
  fields: {},
11
51
  };
52
+ let settled = false;
53
+ const rejectOnce = (error) => {
54
+ if (!settled) {
55
+ settled = true;
56
+ reject(error);
57
+ }
58
+ };
12
59
  busboy.on('file', (name, file, info) => {
13
- let fileBuffer = Buffer.alloc(0);
60
+ const filename = sanitizeFilename(info.filename);
61
+ if (!isMimeTypeAllowed(info.mimeType, fileUploadSettings.allowedMimeTypes)) {
62
+ rejectOnce(new FileUploadValidationError(FileUploadErrorTypeEnum.disallowedMimeType, `File "${filename}" has disallowed content type "${info.mimeType}"`));
63
+ file.resume();
64
+ return;
65
+ }
66
+ const chunks = [];
14
67
  file.on('data', (data) => {
15
- fileBuffer = Buffer.concat([fileBuffer, data]); // Concatenate the new data to the existing buffer
68
+ chunks.push(data);
69
+ });
70
+ // Emitted when the stream is truncated at limits.fileSize - a silently truncated upload must fail, not succeed
71
+ file.on('limit', () => {
72
+ rejectOnce(new FileUploadValidationError(FileUploadErrorTypeEnum.fileTooLarge, `File "${filename}" exceeds the maximum file size of ${fileUploadSettings.maxFileSizeBytes} bytes`));
16
73
  });
17
74
  file.on('end', () => {
18
- if (fileBuffer.length > 0) {
75
+ const content = Buffer.concat(chunks);
76
+ if (!settled && content.length > 0) {
19
77
  result.files.push({
20
- filename: info.filename,
78
+ filename,
21
79
  contentType: info.mimeType,
22
80
  encoding: info.encoding,
23
81
  name: name,
24
- content: fileBuffer,
82
+ content,
25
83
  });
26
84
  }
27
85
  });
28
86
  });
87
+ busboy.on('filesLimit', () => {
88
+ rejectOnce(new FileUploadValidationError(FileUploadErrorTypeEnum.tooManyFiles, `Upload exceeds the maximum of ${fileUploadSettings.maxFileCount} files`));
89
+ });
90
+ busboy.on('fieldsLimit', () => {
91
+ rejectOnce(new FileUploadValidationError(FileUploadErrorTypeEnum.tooManyFields, `Upload exceeds the maximum of ${fileUploadSettings.maxFieldCount} fields`));
92
+ });
29
93
  busboy.on('field', (name, value) => {
30
94
  result.fields[name] = value;
31
95
  });
32
96
  busboy.on('error', (error) => {
33
- reject(error);
97
+ rejectOnce(error);
34
98
  });
35
99
  busboy.on('finish', () => {
100
+ if (settled) {
101
+ return;
102
+ }
103
+ settled = true;
36
104
  const qpqBinaryFiles = result.files.map((f) => ({
37
105
  filename: f.filename,
38
106
  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';
@@ -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';
@@ -18,6 +19,7 @@ import { getEventBusActionProcessor } from './eventBus';
18
19
  import { getFileActionProcessor } from './file';
19
20
  import { getGraphDatabaseActionProcessor } from './graphDatabase';
20
21
  import { getKeyValueStoreActionProcessor } from './keyValueStore';
22
+ import { getMetricActionProcessor } from './metric';
21
23
  import { getQueueActionProcessor } from './queue';
22
24
  import { getSystemActionProcessor } from './system';
23
25
  import { getUserDirectoryActionProcessor } from './userDirectory';
@@ -26,6 +28,7 @@ export const getCoreActionProcessor = async (qpqConfig, dynamicModuleLoader) =>
26
28
  ...(await getApiKeyValidationActionProcessor(qpqConfig, dynamicModuleLoader)),
27
29
  ...(await getFileActionProcessor(qpqConfig, dynamicModuleLoader)),
28
30
  ...(await getKeyValueStoreActionProcessor(qpqConfig, dynamicModuleLoader)),
31
+ ...(await getMetricActionProcessor(qpqConfig, dynamicModuleLoader)),
29
32
  ...(await getQueueActionProcessor(qpqConfig, dynamicModuleLoader)),
30
33
  ...(await getUserDirectoryActionProcessor(qpqConfig, dynamicModuleLoader)),
31
34
  ...(await getEventBusActionProcessor(qpqConfig, dynamicModuleLoader)),
@@ -0,0 +1,2 @@
1
+ import { ActionProcessorListResolver } from 'quidproquo-core';
2
+ export declare const getMetricPutActionProcessor: ActionProcessorListResolver;