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,43 @@
1
+ import { actionResult, MetricActionType, MetricUnit, qpqCoreUtils, } from 'quidproquo-core';
2
+ // Emits the metric as a CloudWatch Embedded Metric Format (EMF) log line: CloudWatch
3
+ // extracts metrics from the function's log stream - no api calls, no extra IAM, no
4
+ // latency on the story. Cost: each unique metricName x dimension-combination is one
5
+ // custom metric (~$0.30/mo) - the standard dimensions are service/environment(/feature),
6
+ // so keep payload dimensions low-cardinality (never per-user / per-request ids).
7
+ const getProcessMetricPut = (qpqConfig) => {
8
+ // The namespace is the deployed app instance - app/environment(/feature) - mirroring how
9
+ // every qpq resource name embeds the deployment identity. Apps sharing an account can't
10
+ // merge same-named metrics, per-developer feature sandboxes (e.g. qpq/docgen/development/jane)
11
+ // never pollute mainline, and dimensions are reserved for slices WITHIN the deployment
12
+ // (service + caller-supplied).
13
+ const application = qpqCoreUtils.getApplicationName(qpqConfig);
14
+ const environment = qpqCoreUtils.getApplicationModuleEnvironment(qpqConfig);
15
+ const feature = qpqCoreUtils.getApplicationModuleFeature(qpqConfig);
16
+ const namespace = `qpq/${application}/${environment}${feature ? `/${feature}` : ''}`;
17
+ const service = qpqCoreUtils.getApplicationModuleName(qpqConfig);
18
+ return async ({ metricName, value, unit, dimensions }) => {
19
+ const allDimensions = {
20
+ service,
21
+ ...dimensions,
22
+ };
23
+ const emf = {
24
+ _aws: {
25
+ Timestamp: Date.now(),
26
+ CloudWatchMetrics: [
27
+ {
28
+ Namespace: namespace,
29
+ Dimensions: [Object.keys(allDimensions)],
30
+ Metrics: [{ Name: metricName, Unit: unit ?? MetricUnit.count }],
31
+ },
32
+ ],
33
+ },
34
+ ...allDimensions,
35
+ [metricName]: value,
36
+ };
37
+ console.log(JSON.stringify(emf));
38
+ return actionResult(void 0);
39
+ };
40
+ };
41
+ export const getMetricPutActionProcessor = async (qpqConfig) => ({
42
+ [MetricActionType.Put]: getProcessMetricPut(qpqConfig),
43
+ });
@@ -0,0 +1,2 @@
1
+ import { ActionProcessorListResolver } from 'quidproquo-core';
2
+ export declare const getMetricActionProcessor: ActionProcessorListResolver;
@@ -0,0 +1,4 @@
1
+ import { getMetricPutActionProcessor } from './getMetricPutActionProcessor';
2
+ export const getMetricActionProcessor = async (qpqConfig, dynamicModuleLoader) => ({
3
+ ...(await getMetricPutActionProcessor(qpqConfig, dynamicModuleLoader)),
4
+ });
@@ -1,5 +1,12 @@
1
- // @ts-expect-error - Special webpack file injected
2
- import { qpqDynamicModuleLoader } from 'quidproquo-dynamic-loader';
1
+ // @ts-expect-error - injected at build time by QpqPlugin (webpack virtual module)
2
+ import { qpqConfig, qpqDynamicModuleLoader } from 'quidproquo-dynamic-loader';
3
+ import { loadFederatedStory } from '../../logic/federatedModuleStore';
3
4
  export const dynamicModuleLoader = async (qpqFunctionRuntime) => {
5
+ // Tier 1: federated. undefined means "nothing published for this runtime" -> fall through.
6
+ const federatedStory = await loadFederatedStory(qpqConfig, qpqFunctionRuntime);
7
+ if (federatedStory !== undefined) {
8
+ return federatedStory;
9
+ }
10
+ // Tier 2: the statically bundled story (or a fail-fast throw for a thin shell).
4
11
  return qpqDynamicModuleLoader(qpqFunctionRuntime);
5
12
  };
@@ -0,0 +1 @@
1
+ export declare const getApiKeyValue: (region: string, apiKeyId: string) => Promise<string | undefined>;
@@ -0,0 +1,12 @@
1
+ import { APIGatewayClient, GetApiKeyCommand } from '@aws-sdk/client-api-gateway';
2
+ import { createAwsClient } from '../createAwsClient';
3
+ // Fetch a single api key's value by id - keys are read individually (not listed) so the
4
+ // IAM grant can be scoped per-key with tag conditions rather than the /apikeys collection.
5
+ export const getApiKeyValue = async (region, apiKeyId) => {
6
+ const apiGatewayClient = createAwsClient(APIGatewayClient, { region });
7
+ const res = await apiGatewayClient.send(new GetApiKeyCommand({
8
+ apiKey: apiKeyId,
9
+ includeValue: true,
10
+ }));
11
+ return res.value;
12
+ };
@@ -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,23 @@
1
+ // Discriminable failures raised inside loadFederatedStory. All of them are ultimately
2
+ // caught there and turned into a fall-back-to-bundled (with a warning) - they exist so
3
+ // logs/backoff can tell WHY a store didn't load, not to crash the request.
4
+ export var FederatedModuleLoadErrorCode;
5
+ (function (FederatedModuleLoadErrorCode) {
6
+ // Store URL wasn't s3:// or file:// (a config/env mistake).
7
+ FederatedModuleLoadErrorCode["unsupportedStoreUrl"] = "unsupportedStoreUrl";
8
+ // manifest.json parsed but is missing required fields (bad/partial publish).
9
+ FederatedModuleLoadErrorCode["manifestInvalid"] = "manifestInvalid";
10
+ // A version's files downloaded but the entry didn't expose a valid MF container.
11
+ FederatedModuleLoadErrorCode["containerLoadFailed"] = "containerLoadFailed";
12
+ // The manifest exposed the module, but it didn't export the requested story name
13
+ // (e.g. a story was renamed in the remote but the manifest still lists it).
14
+ FederatedModuleLoadErrorCode["storyNotExported"] = "storyNotExported";
15
+ })(FederatedModuleLoadErrorCode || (FederatedModuleLoadErrorCode = {}));
16
+ export class FederatedModuleLoadError extends Error {
17
+ code;
18
+ constructor(code, message) {
19
+ super(message);
20
+ this.code = code;
21
+ this.name = 'FederatedModuleLoadError';
22
+ }
23
+ }
@@ -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,4 @@
1
+ export * from './FederatedModuleLoadError';
2
+ export * from './FederatedModuleStoreManifest';
3
+ export * from './loadFederatedStory';
4
+ export * from './sharedPackages';
@@ -0,0 +1,4 @@
1
+ export * from './FederatedModuleLoadError';
2
+ export * from './FederatedModuleStoreManifest';
3
+ export * from './loadFederatedStory';
4
+ export * from './sharedPackages';
@@ -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>;
@@ -0,0 +1,246 @@
1
+ import { qpqConfigAwsUtils } from 'quidproquo-config-aws';
2
+ import * as quidproquoCore from 'quidproquo-core';
3
+ import { getFederatedKeyFromQpqFunctionRuntime, qpqCoreUtils } from 'quidproquo-core';
4
+ import * as quidproquoWebserver from 'quidproquo-webserver';
5
+ import * as fs from 'fs';
6
+ import { createRequire } from 'module';
7
+ import * as path from 'path';
8
+ import { fileURLToPath } from 'url';
9
+ import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
10
+ import { init, loadRemote, registerRemotes } from '@module-federation/enhanced/runtime';
11
+ import { createAwsClient } from '../createAwsClient';
12
+ import { FederatedModuleLoadError, FederatedModuleLoadErrorCode } from './FederatedModuleLoadError';
13
+ import { FEDERATED_SHARED_PACKAGE_NAMES } from './sharedPackages';
14
+ // How long a resolved state is served before the manifest is re-checked for a new
15
+ // version. Overridable per service (defineFederatedModuleStore recheckMs -> this env).
16
+ // Lower = faster pickup of new publishes at the cost of one small S3 GET per container
17
+ // per interval. The re-check runs in the BACKGROUND so it never blocks a request.
18
+ const DEFAULT_RECHECK_MS = 60_000;
19
+ const getRecheckMs = () => {
20
+ const raw = process.env.federatedCodeStoreRecheckMs;
21
+ const parsed = raw ? parseInt(raw, 10) : NaN;
22
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_RECHECK_MS;
23
+ };
24
+ let resolvedState;
25
+ let recheckAfter = 0;
26
+ let inflightResolve;
27
+ let hostInitialised = false;
28
+ // Test seam - forces the next call to re-resolve the store from env.
29
+ export const resetFederatedModuleStoreCache = () => {
30
+ resolvedState = undefined;
31
+ recheckAfter = 0;
32
+ inflightResolve = undefined;
33
+ };
34
+ // ── Transport ────────────────────────────────────────────────────────────────
35
+ // Returns a function that reads a path RELATIVE to the store (e.g. 'manifest.json'
36
+ // or '<hash>/remoteEntry.js'). Two backends: s3:// for real deploys, file:// for
37
+ // local/dev and tests. The store URL already includes the per-service prefix, so
38
+ // callers pass only the in-service relative path.
39
+ const getFileFetcher = (storeUrl, qpqConfig) => {
40
+ if (storeUrl.startsWith('s3://')) {
41
+ const url = new URL(storeUrl);
42
+ const bucketName = url.host;
43
+ const keyPrefix = url.pathname.replace(/^\/|\/$/g, '');
44
+ const region = qpqConfigAwsUtils.getApplicationModuleDeployRegion(qpqConfig);
45
+ // Pin the owner so a same-name bucket in another account can't be squatted to
46
+ // serve code we'd then require() with the service role's credentials.
47
+ const expectedBucketOwner = qpqConfigAwsUtils.getApplicationModuleDeployAccountId(qpqConfig);
48
+ const s3Client = createAwsClient(S3Client, { region });
49
+ return async (relativePath) => {
50
+ const response = await s3Client.send(new GetObjectCommand({
51
+ Bucket: bucketName,
52
+ Key: keyPrefix ? `${keyPrefix}/${relativePath}` : relativePath,
53
+ ExpectedBucketOwner: expectedBucketOwner,
54
+ }));
55
+ const bytes = await response.Body?.transformToByteArray();
56
+ if (!bytes) {
57
+ throw new FederatedModuleLoadError(FederatedModuleLoadErrorCode.containerLoadFailed, `empty response reading [${relativePath}]`);
58
+ }
59
+ return Buffer.from(bytes);
60
+ };
61
+ }
62
+ if (storeUrl.startsWith('file://')) {
63
+ const baseDirectory = fileURLToPath(storeUrl.endsWith('/') ? storeUrl : `${storeUrl}/`);
64
+ return async (relativePath) => fs.promises.readFile(path.join(baseDirectory, relativePath));
65
+ }
66
+ throw new FederatedModuleLoadError(FederatedModuleLoadErrorCode.unsupportedStoreUrl, `unsupported federated code store url [${storeUrl}]`);
67
+ };
68
+ // "Nothing published yet" is a legitimate not-found: the manifest key is absent
69
+ // (NoSuchKey/NotFound) or the store bucket isn't created yet, which happens normally
70
+ // when a lambda cold-starts between deploy phases (NoSuchBucket). We do NOT include
71
+ // AccessDenied: a permission failure is a real misconfiguration and must surface (as a
72
+ // warning, and be retried) rather than masquerade as 'not published'.
73
+ const isManifestNotFoundError = (error) => {
74
+ const name = error?.name || '';
75
+ const code = error?.code || '';
76
+ return ['NoSuchKey', 'NotFound', 'NoSuchBucket'].includes(name) || code === 'ENOENT';
77
+ };
78
+ // ── Module-federation shared scope ───────────────────────────────────────────
79
+ // The host provides the framework packages to the container's share scope so remote
80
+ // story code resolves the SAME module instances as the bundled runtime. The package
81
+ // list is shared with the remote build so the two can't drift (see sharedPackages).
82
+ const SHARED_LIBS = {
83
+ 'quidproquo-core': quidproquoCore,
84
+ 'quidproquo-webserver': quidproquoWebserver,
85
+ };
86
+ const getHostSharedModules = () => Object.fromEntries(FEDERATED_SHARED_PACKAGE_NAMES.map((name) => [
87
+ name,
88
+ {
89
+ version: '0.0.0',
90
+ lib: () => SHARED_LIBS[name],
91
+ shareConfig: { singleton: true, requiredVersion: false },
92
+ },
93
+ ]));
94
+ const ensureHostInitialised = () => {
95
+ if (hostInitialised) {
96
+ return;
97
+ }
98
+ init({
99
+ name: 'qpq_federated_host',
100
+ remotes: [],
101
+ shared: getHostSharedModules(),
102
+ });
103
+ hostInitialised = true;
104
+ };
105
+ // ── Version loading ──────────────────────────────────────────────────────────
106
+ // Downloads a manifest's files (atomically) and registers the container with the MF
107
+ // runtime under a HASH-UNIQUE remote name, so loadRemote can't serve a previous
108
+ // version's module from its cache when the hash changes. Returns the runtime name.
109
+ // This is the step that actually puts a new version's code into effect.
110
+ const loadStoreVersion = async (getFile, manifest) => {
111
+ const cacheRoot = process.env.federatedCodeStoreCacheDir || '/tmp/qpq-federated-code';
112
+ const versionDirectory = path.join(cacheRoot, manifest.containerName, manifest.hash);
113
+ await fs.promises.mkdir(versionDirectory, { recursive: true });
114
+ await Promise.all(manifest.files.map(async (file) => {
115
+ const localPath = path.join(versionDirectory, file);
116
+ // Files are immutable under their hash dir AND only ever appear via the atomic
117
+ // rename below, so an existing file is always complete and valid - this is what
118
+ // lets a re-check of an unchanged version skip re-downloading.
119
+ if (fs.existsSync(localPath)) {
120
+ return;
121
+ }
122
+ const data = await getFile(`${manifest.hash}/${file}`);
123
+ await fs.promises.mkdir(path.dirname(localPath), { recursive: true });
124
+ // Write to a temp name then rename: rename is atomic on the same filesystem, so a
125
+ // crash/timeout mid-write never leaves a truncated file that a later invocation
126
+ // would trust via the existsSync check above.
127
+ const tempPath = `${localPath}.${process.pid}.tmp`;
128
+ await fs.promises.writeFile(tempPath, data);
129
+ await fs.promises.rename(tempPath, localPath);
130
+ }));
131
+ const entryPath = path.join(versionDirectory, manifest.entry);
132
+ const containerRequire = createRequire(entryPath);
133
+ const containerModule = containerRequire(entryPath);
134
+ const container = containerModule[manifest.containerName] ?? containerModule.default ?? containerModule;
135
+ if (typeof container?.get !== 'function' || typeof container?.init !== 'function') {
136
+ throw new FederatedModuleLoadError(FederatedModuleLoadErrorCode.containerLoadFailed, `[${manifest.entry}] does not export a module federation container named [${manifest.containerName}]`);
137
+ }
138
+ // entryGlobalName is the container's real (build-time) library name; the registered
139
+ // remote name is hash-unique so each version is a distinct loadRemote key.
140
+ globalThis[manifest.containerName] = container;
141
+ ensureHostInitialised();
142
+ const runtimeName = `${manifest.containerName}__${manifest.hash}`;
143
+ registerRemotes([
144
+ {
145
+ name: runtimeName,
146
+ entry: entryPath,
147
+ type: 'global',
148
+ entryGlobalName: manifest.containerName,
149
+ },
150
+ ]);
151
+ return runtimeName;
152
+ };
153
+ // ── Probe & resolve (the caching state machine) ──────────────────────────────
154
+ // Reads the manifest and decides the current state. If the hash is unchanged from what
155
+ // we already loaded, keeps the existing container (no re-sync). Not-found -> unavailable.
156
+ const probeStore = async (qpqConfig, storeUrl, current) => {
157
+ const getFile = getFileFetcher(storeUrl, qpqConfig);
158
+ let manifest;
159
+ try {
160
+ manifest = JSON.parse((await getFile('manifest.json')).toString('utf8'));
161
+ }
162
+ catch (error) {
163
+ if (isManifestNotFoundError(error)) {
164
+ console.log(`federated code store [${storeUrl}] has no manifest - using bundled modules`);
165
+ return { available: false };
166
+ }
167
+ throw error;
168
+ }
169
+ if (!manifest?.containerName ||
170
+ !manifest?.hash ||
171
+ !manifest?.entry ||
172
+ !Array.isArray(manifest?.files) ||
173
+ typeof manifest?.exposes !== 'object' ||
174
+ manifest.exposes === null) {
175
+ throw new FederatedModuleLoadError(FederatedModuleLoadErrorCode.manifestInvalid, `invalid manifest in federated code store [${storeUrl}]`);
176
+ }
177
+ if (current?.available && current.manifest.hash === manifest.hash) {
178
+ return current;
179
+ }
180
+ const runtimeName = await loadStoreVersion(getFile, manifest);
181
+ return { available: true, manifest, runtimeName };
182
+ };
183
+ // Resolves the store, re-checking the manifest after the recheck interval. The re-check
184
+ // runs in the background: a stale-but-usable state is served immediately so a request
185
+ // never blocks on S3; only the very first resolve (no state yet) awaits.
186
+ const resolveStore = (qpqConfig, storeUrl) => {
187
+ if (resolvedState && Date.now() < recheckAfter) {
188
+ return Promise.resolve(resolvedState);
189
+ }
190
+ if (!inflightResolve) {
191
+ inflightResolve = probeStore(qpqConfig, storeUrl, resolvedState)
192
+ .then((state) => {
193
+ resolvedState = state;
194
+ recheckAfter = Date.now() + getRecheckMs();
195
+ return state;
196
+ })
197
+ .catch((error) => {
198
+ // Back off and keep serving whatever we had (or unavailable). Surfaced as a
199
+ // warning so real errors (e.g. AccessDenied) are visible, not silent.
200
+ console.warn('federated code store check failed - keeping current modules', error);
201
+ recheckAfter = Date.now() + getRecheckMs();
202
+ resolvedState = resolvedState ?? { available: false };
203
+ return resolvedState;
204
+ })
205
+ .finally(() => {
206
+ inflightResolve = undefined;
207
+ });
208
+ }
209
+ // Serve the stale state instantly while the refresh happens; only block if we have
210
+ // nothing yet.
211
+ return resolvedState ? Promise.resolve(resolvedState) : inflightResolve;
212
+ };
213
+ // ── Public entry ─────────────────────────────────────────────────────────────
214
+ // Called by dynamicModuleLoader for every story load. Returns the story function, or
215
+ // undefined to signal "use the bundled module" (no store, nothing published, runtime
216
+ // not exposed, or any error - availability always wins over federation).
217
+ export const loadFederatedStory = async (qpqConfig, qpqFunctionRuntime) => {
218
+ const storeUrl = process.env.federatedCodeStoreUrl;
219
+ if (!storeUrl) {
220
+ return undefined;
221
+ }
222
+ try {
223
+ const storeState = await resolveStore(qpqConfig, storeUrl);
224
+ if (!storeState.available) {
225
+ return undefined;
226
+ }
227
+ const federatedKey = getFederatedKeyFromQpqFunctionRuntime(qpqFunctionRuntime);
228
+ const exposePath = storeState.manifest.exposes[federatedKey];
229
+ if (!exposePath) {
230
+ console.warn(`federated code store has no module for [${federatedKey}] - using the bundled module`);
231
+ return undefined;
232
+ }
233
+ const remoteModule = await loadRemote(`${storeState.runtimeName}/${exposePath}`);
234
+ const storyName = qpqCoreUtils.getStoryNameFromQpqFunctionRuntime(qpqFunctionRuntime);
235
+ const story = remoteModule?.[storyName];
236
+ if (!story) {
237
+ throw new FederatedModuleLoadError(FederatedModuleLoadErrorCode.storyNotExported, `story [${storyName}] is not exported from federated module [${exposePath}]`);
238
+ }
239
+ return story;
240
+ }
241
+ catch (error) {
242
+ // Availability first: a broken store must never take the service down
243
+ console.warn('federated module load failed - using the bundled module', error);
244
+ return undefined;
245
+ }
246
+ };
@@ -0,0 +1 @@
1
+ export declare const FEDERATED_SHARED_PACKAGE_NAMES: readonly ["quidproquo-core", "quidproquo-webserver"];
@@ -0,0 +1,7 @@
1
+ // The framework packages the lambda host provides to the module-federation share
2
+ // scope, so a federated remote resolves the SAME module instances as the host
3
+ // (instanceof checks and module-level state stay coherent). Single source of truth
4
+ // for BOTH the host runtime (loadFederatedStory getHostSharedModules) and the
5
+ // remote build (quidproquo-deploy-webpack getWebpackConfigForQpqRemote) — the two
6
+ // lists must never drift, or the remote would silently bundle its own copy.
7
+ export const FEDERATED_SHARED_PACKAGE_NAMES = ['quidproquo-core', 'quidproquo-webserver'];
@@ -1 +1,2 @@
1
1
  export * from './cognito';
2
+ export * from './federatedModuleStore';
@@ -1 +1,2 @@
1
1
  export * from './cognito';
2
+ export * from './federatedModuleStore';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quidproquo-actionprocessor-awslambda",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "",
5
5
  "main": "./lib/commonjs/index.js",
6
6
  "module": "./lib/esm/index.js",
@@ -35,6 +35,7 @@
35
35
  "dependencies": {
36
36
  "@ai-sdk/amazon-bedrock": "^5.0.11",
37
37
  "@aws-sdk/client-acm": "^3.744.0",
38
+ "@module-federation/enhanced": "0.18.2",
38
39
  "@aws-sdk/client-api-gateway": "^3.744.0",
39
40
  "@aws-sdk/client-apigatewaymanagementapi": "^3.744.0",
40
41
  "@aws-sdk/client-cloudformation": "^3.744.0",
@@ -59,10 +60,10 @@
59
60
  "lodash": "^4.17.21",
60
61
  "node-cache": "^5.1.2",
61
62
  "node-match-path": "^0.6.3",
62
- "quidproquo-actionprocessor-js": "0.1.2",
63
- "quidproquo-config-aws": "0.1.2",
64
- "quidproquo-core": "0.1.2",
65
- "quidproquo-webserver": "0.1.2"
63
+ "quidproquo-actionprocessor-js": "0.1.3",
64
+ "quidproquo-config-aws": "0.1.3",
65
+ "quidproquo-core": "0.1.3",
66
+ "quidproquo-webserver": "0.1.3"
66
67
  },
67
68
  "devDependencies": {
68
69
  "@types/busboy": "^1.5.0",
@@ -70,7 +71,7 @@
70
71
  "@types/lodash": "^4.14.194",
71
72
  "@types/node": "^22.13.13",
72
73
  "esbuild": "^0.25.8",
73
- "quidproquo-tsconfig": "0.1.2",
74
+ "quidproquo-tsconfig": "0.1.3",
74
75
  "typescript": "^5.8.2"
75
76
  }
76
77
  }
@@ -1,2 +0,0 @@
1
- import { ApiKey } from 'quidproquo-webserver';
2
- export declare const getApiKeys: (region: string, ...keyNames: string[]) => Promise<ApiKey[]>;
@@ -1,2 +0,0 @@
1
- import { ApiKey } from 'quidproquo-webserver';
2
- export declare const getApiKeys: (region: string, ...keyNames: string[]) => Promise<ApiKey[]>;
@@ -1,17 +0,0 @@
1
- import { APIGatewayClient, GetApiKeysCommand } from '@aws-sdk/client-api-gateway';
2
- import { createAwsClient } from '../createAwsClient';
3
- export const getApiKeys = async (region, ...keyNames) => {
4
- const apiGatewayClient = createAwsClient(APIGatewayClient, { region });
5
- const input = {
6
- includeValues: true,
7
- limit: 500,
8
- };
9
- const res = await apiGatewayClient.send(new GetApiKeysCommand(input));
10
- return (res.items
11
- ?.filter((i) => keyNames.indexOf(i.name) >= 0)
12
- .map((i) => ({
13
- name: i.name,
14
- value: i.value,
15
- description: i.description,
16
- })) || []);
17
- };