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.
- package/lib/commonjs/awsNamingUtils.d.ts +2 -0
- package/lib/commonjs/awsNamingUtils.js +24 -1
- package/lib/commonjs/getActionProcessor/core/apiKeyValidation/getApiKeyValidationValidateActionProcessor.js +15 -11
- package/lib/commonjs/getActionProcessor/core/event/apiGatwayEvent/api/getEventGetRecordsActionProcessor.js +12 -1
- package/lib/commonjs/getActionProcessor/core/event/utils/parseMultipartFormData.d.ts +7 -1
- package/lib/commonjs/getActionProcessor/core/event/utils/parseMultipartFormData.js +77 -8
- package/lib/commonjs/getActionProcessor/core/index.d.ts +1 -0
- package/lib/commonjs/getActionProcessor/core/index.js +3 -1
- package/lib/commonjs/getActionProcessor/core/metric/getMetricPutActionProcessor.d.ts +2 -0
- package/lib/commonjs/getActionProcessor/core/metric/getMetricPutActionProcessor.js +51 -0
- package/lib/commonjs/getActionProcessor/core/metric/index.d.ts +2 -0
- package/lib/commonjs/getActionProcessor/core/metric/index.js +17 -0
- package/lib/commonjs/lambdas/dynamicModuleLoader/dynamicModuleLoader.js +8 -1
- package/lib/commonjs/logic/apiGateway/getApiKeyValue.d.ts +1 -0
- package/lib/commonjs/logic/apiGateway/{getApiKeys.js → getApiKeyValue.js} +10 -14
- package/lib/commonjs/logic/federatedModuleStore/FederatedModuleLoadError.d.ts +10 -0
- package/lib/commonjs/logic/federatedModuleStore/FederatedModuleLoadError.js +26 -0
- package/lib/commonjs/logic/federatedModuleStore/FederatedModuleStoreManifest.d.ts +8 -0
- package/lib/commonjs/logic/federatedModuleStore/FederatedModuleStoreManifest.js +2 -0
- package/lib/commonjs/logic/federatedModuleStore/index.d.ts +4 -0
- package/lib/commonjs/logic/federatedModuleStore/index.js +20 -0
- package/lib/commonjs/logic/federatedModuleStore/loadFederatedStory.d.ts +3 -0
- package/lib/commonjs/logic/federatedModuleStore/loadFederatedStory.js +295 -0
- package/lib/commonjs/logic/federatedModuleStore/sharedPackages.d.ts +1 -0
- package/lib/commonjs/logic/federatedModuleStore/sharedPackages.js +10 -0
- package/lib/commonjs/logic/index.d.ts +1 -0
- package/lib/commonjs/logic/index.js +1 -0
- package/lib/esm/awsNamingUtils.d.ts +2 -0
- package/lib/esm/awsNamingUtils.js +20 -0
- package/lib/esm/getActionProcessor/core/apiKeyValidation/getApiKeyValidationValidateActionProcessor.js +16 -12
- package/lib/esm/getActionProcessor/core/event/apiGatwayEvent/api/getEventGetRecordsActionProcessor.js +14 -3
- package/lib/esm/getActionProcessor/core/event/utils/parseMultipartFormData.d.ts +7 -1
- package/lib/esm/getActionProcessor/core/event/utils/parseMultipartFormData.js +75 -7
- package/lib/esm/getActionProcessor/core/index.d.ts +1 -0
- package/lib/esm/getActionProcessor/core/index.js +3 -0
- package/lib/esm/getActionProcessor/core/metric/getMetricPutActionProcessor.d.ts +2 -0
- package/lib/esm/getActionProcessor/core/metric/getMetricPutActionProcessor.js +43 -0
- package/lib/esm/getActionProcessor/core/metric/index.d.ts +2 -0
- package/lib/esm/getActionProcessor/core/metric/index.js +4 -0
- package/lib/esm/lambdas/dynamicModuleLoader/dynamicModuleLoader.js +9 -2
- package/lib/esm/logic/apiGateway/getApiKeyValue.d.ts +1 -0
- package/lib/esm/logic/apiGateway/getApiKeyValue.js +12 -0
- package/lib/esm/logic/federatedModuleStore/FederatedModuleLoadError.d.ts +10 -0
- package/lib/esm/logic/federatedModuleStore/FederatedModuleLoadError.js +23 -0
- package/lib/esm/logic/federatedModuleStore/FederatedModuleStoreManifest.d.ts +8 -0
- package/lib/esm/logic/federatedModuleStore/FederatedModuleStoreManifest.js +1 -0
- package/lib/esm/logic/federatedModuleStore/index.d.ts +4 -0
- package/lib/esm/logic/federatedModuleStore/index.js +4 -0
- package/lib/esm/logic/federatedModuleStore/loadFederatedStory.d.ts +3 -0
- package/lib/esm/logic/federatedModuleStore/loadFederatedStory.js +246 -0
- package/lib/esm/logic/federatedModuleStore/sharedPackages.d.ts +1 -0
- package/lib/esm/logic/federatedModuleStore/sharedPackages.js +7 -0
- package/lib/esm/logic/index.d.ts +1 -0
- package/lib/esm/logic/index.js +1 -0
- package/package.json +7 -6
- package/lib/commonjs/logic/apiGateway/getApiKeys.d.ts +0 -2
- package/lib/esm/logic/apiGateway/getApiKeys.d.ts +0 -2
- 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'];
|
|
@@ -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
|
|
2
|
+
import { actionResult } from 'quidproquo-core';
|
|
3
3
|
import { ApiKeyValidationActionType } from 'quidproquo-webserver';
|
|
4
4
|
import { timingSafeEqual } from 'crypto';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
+
const content = Buffer.concat(chunks);
|
|
76
|
+
if (!settled && content.length > 0) {
|
|
19
77
|
result.files.push({
|
|
20
|
-
filename
|
|
78
|
+
filename,
|
|
21
79
|
contentType: info.mimeType,
|
|
22
80
|
encoding: info.encoding,
|
|
23
81
|
name: name,
|
|
24
|
-
content
|
|
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
|
-
|
|
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';
|
|
@@ -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)),
|