quidproquo-actionprocessor-awslambda 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) 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/ai/getAiPromptActionProcessor.js +11 -4
  4. package/lib/commonjs/getActionProcessor/core/ai/getAiPromptStreamActionProcessor.js +11 -4
  5. package/lib/commonjs/getActionProcessor/core/ai/logic/aiStreamPartMappers.js +8 -8
  6. package/lib/commonjs/getActionProcessor/core/ai/logic/createDriveFileResolver.d.ts +3 -0
  7. package/lib/commonjs/getActionProcessor/core/ai/logic/createDriveFileResolver.js +27 -0
  8. package/lib/commonjs/getActionProcessor/core/ai/logic/index.d.ts +1 -0
  9. package/lib/commonjs/getActionProcessor/core/ai/logic/index.js +1 -0
  10. package/lib/commonjs/getActionProcessor/core/ai/logic/toSdkMessages.d.ts +3 -2
  11. package/lib/commonjs/getActionProcessor/core/ai/logic/toSdkMessages.js +41 -24
  12. package/lib/commonjs/getActionProcessor/core/apiKeyValidation/getApiKeyValidationValidateActionProcessor.js +15 -11
  13. package/lib/commonjs/getActionProcessor/core/event/apiGatwayEvent/api/getEventGetRecordsActionProcessor.js +12 -1
  14. package/lib/commonjs/getActionProcessor/core/event/utils/parseMultipartFormData.d.ts +7 -1
  15. package/lib/commonjs/getActionProcessor/core/event/utils/parseMultipartFormData.js +77 -8
  16. package/lib/commonjs/getActionProcessor/core/eventBus/getEventBusSendMessagesActionProcessor.js +1 -1
  17. package/lib/commonjs/getActionProcessor/core/file/getFileStreamOpenActionProcessor.js +1 -3
  18. package/lib/commonjs/getActionProcessor/core/index.d.ts +1 -0
  19. package/lib/commonjs/getActionProcessor/core/index.js +3 -1
  20. package/lib/commonjs/getActionProcessor/core/keyValueStore/getKeyValueStoreUpsertActionProcessor.js +2 -0
  21. package/lib/commonjs/getActionProcessor/core/metric/getMetricPutActionProcessor.d.ts +2 -0
  22. package/lib/commonjs/getActionProcessor/core/metric/getMetricPutActionProcessor.js +51 -0
  23. package/lib/commonjs/getActionProcessor/core/metric/index.d.ts +2 -0
  24. package/lib/commonjs/getActionProcessor/core/metric/index.js +17 -0
  25. package/lib/commonjs/lambdaHandlers/helpers/logger.js +1 -3
  26. package/lib/commonjs/lambdas/dynamicModuleLoader/dynamicModuleLoader.js +8 -1
  27. package/lib/commonjs/logic/apiGateway/getApiKeyValue.d.ts +1 -0
  28. package/lib/commonjs/logic/apiGateway/{getApiKeys.js → getApiKeyValue.js} +10 -14
  29. package/lib/commonjs/logic/cognito/utils/transformCognitoResponse.js +1 -1
  30. package/lib/commonjs/logic/dynamo/putItem.d.ts +1 -0
  31. package/lib/commonjs/logic/dynamo/putItem.js +6 -4
  32. package/lib/commonjs/logic/dynamo/qpqDynamoOrm/buildDynamoUpdate.js +1 -5
  33. package/lib/commonjs/logic/federatedModuleStore/FederatedModuleLoadError.d.ts +10 -0
  34. package/lib/commonjs/logic/federatedModuleStore/FederatedModuleLoadError.js +26 -0
  35. package/lib/commonjs/logic/federatedModuleStore/FederatedModuleStoreManifest.d.ts +8 -0
  36. package/lib/commonjs/logic/federatedModuleStore/FederatedModuleStoreManifest.js +2 -0
  37. package/lib/commonjs/logic/federatedModuleStore/index.d.ts +4 -0
  38. package/lib/commonjs/logic/federatedModuleStore/index.js +20 -0
  39. package/lib/commonjs/logic/federatedModuleStore/loadFederatedStory.d.ts +3 -0
  40. package/lib/commonjs/logic/federatedModuleStore/loadFederatedStory.js +295 -0
  41. package/lib/commonjs/logic/federatedModuleStore/sharedPackages.d.ts +1 -0
  42. package/lib/commonjs/logic/federatedModuleStore/sharedPackages.js +10 -0
  43. package/lib/commonjs/logic/index.d.ts +1 -0
  44. package/lib/commonjs/logic/index.js +1 -0
  45. package/lib/commonjs/logic/s3/generatePresignedUploadUrl.js +4 -4
  46. package/lib/esm/awsNamingUtils.d.ts +2 -0
  47. package/lib/esm/awsNamingUtils.js +20 -0
  48. package/lib/esm/getActionProcessor/core/ai/getAiPromptActionProcessor.js +11 -4
  49. package/lib/esm/getActionProcessor/core/ai/getAiPromptStreamActionProcessor.js +11 -4
  50. package/lib/esm/getActionProcessor/core/ai/logic/aiStreamPartMappers.js +8 -8
  51. package/lib/esm/getActionProcessor/core/ai/logic/createDriveFileResolver.d.ts +3 -0
  52. package/lib/esm/getActionProcessor/core/ai/logic/createDriveFileResolver.js +14 -0
  53. package/lib/esm/getActionProcessor/core/ai/logic/index.d.ts +1 -0
  54. package/lib/esm/getActionProcessor/core/ai/logic/index.js +1 -0
  55. package/lib/esm/getActionProcessor/core/ai/logic/toSdkMessages.d.ts +3 -2
  56. package/lib/esm/getActionProcessor/core/ai/logic/toSdkMessages.js +27 -20
  57. package/lib/esm/getActionProcessor/core/apiKeyValidation/getApiKeyValidationValidateActionProcessor.js +16 -12
  58. package/lib/esm/getActionProcessor/core/event/apiGatwayEvent/api/getEventGetRecordsActionProcessor.js +14 -3
  59. package/lib/esm/getActionProcessor/core/event/utils/parseMultipartFormData.d.ts +7 -1
  60. package/lib/esm/getActionProcessor/core/event/utils/parseMultipartFormData.js +75 -7
  61. package/lib/esm/getActionProcessor/core/eventBus/getEventBusSendMessagesActionProcessor.js +1 -1
  62. package/lib/esm/getActionProcessor/core/file/getFileDeleteActionProcessor.js +1 -1
  63. package/lib/esm/getActionProcessor/core/file/getFileGenerateTemporarySecureUrlActionProcessor.js +1 -1
  64. package/lib/esm/getActionProcessor/core/file/getFileGenerateTemporaryUploadSecureUrlActionProcessor.js +1 -1
  65. package/lib/esm/getActionProcessor/core/file/getFileStreamOpenActionProcessor.js +1 -3
  66. package/lib/esm/getActionProcessor/core/index.d.ts +1 -0
  67. package/lib/esm/getActionProcessor/core/index.js +3 -0
  68. package/lib/esm/getActionProcessor/core/keyValueStore/getKeyValueStoreDeleteActionProcessor.js +1 -1
  69. package/lib/esm/getActionProcessor/core/keyValueStore/getKeyValueStoreGetActionProcessor.js +1 -1
  70. package/lib/esm/getActionProcessor/core/keyValueStore/getKeyValueStoreGetAllActionProcessor.js +1 -1
  71. package/lib/esm/getActionProcessor/core/keyValueStore/getKeyValueStoreQueryActionProcessor.js +1 -1
  72. package/lib/esm/getActionProcessor/core/keyValueStore/getKeyValueStoreScanActionProcessor.js +1 -1
  73. package/lib/esm/getActionProcessor/core/keyValueStore/getKeyValueStoreUpdateActionProcessor.js +1 -1
  74. package/lib/esm/getActionProcessor/core/keyValueStore/getKeyValueStoreUpsertActionProcessor.js +2 -0
  75. package/lib/esm/getActionProcessor/core/metric/getMetricPutActionProcessor.d.ts +2 -0
  76. package/lib/esm/getActionProcessor/core/metric/getMetricPutActionProcessor.js +43 -0
  77. package/lib/esm/getActionProcessor/core/metric/index.d.ts +2 -0
  78. package/lib/esm/getActionProcessor/core/metric/index.js +4 -0
  79. package/lib/esm/getActionProcessor/core/queue/getQueueSendMessageActionProcessor.js +1 -1
  80. package/lib/esm/lambdaHandlers/helpers/logger.js +1 -3
  81. package/lib/esm/lambdas/dynamicModuleLoader/dynamicModuleLoader.js +9 -2
  82. package/lib/esm/logic/apiGateway/getApiKeyValue.d.ts +1 -0
  83. package/lib/esm/logic/apiGateway/getApiKeyValue.js +12 -0
  84. package/lib/esm/logic/cognito/utils/transformCognitoResponse.js +2 -2
  85. package/lib/esm/logic/dynamo/putItem.d.ts +1 -0
  86. package/lib/esm/logic/dynamo/putItem.js +6 -0
  87. package/lib/esm/logic/dynamo/qpqDynamoOrm/buildDynamoUpdate.js +1 -5
  88. package/lib/esm/logic/federatedModuleStore/FederatedModuleLoadError.d.ts +10 -0
  89. package/lib/esm/logic/federatedModuleStore/FederatedModuleLoadError.js +23 -0
  90. package/lib/esm/logic/federatedModuleStore/FederatedModuleStoreManifest.d.ts +8 -0
  91. package/lib/esm/logic/federatedModuleStore/FederatedModuleStoreManifest.js +1 -0
  92. package/lib/esm/logic/federatedModuleStore/index.d.ts +4 -0
  93. package/lib/esm/logic/federatedModuleStore/index.js +4 -0
  94. package/lib/esm/logic/federatedModuleStore/loadFederatedStory.d.ts +3 -0
  95. package/lib/esm/logic/federatedModuleStore/loadFederatedStory.js +246 -0
  96. package/lib/esm/logic/federatedModuleStore/sharedPackages.d.ts +1 -0
  97. package/lib/esm/logic/federatedModuleStore/sharedPackages.js +7 -0
  98. package/lib/esm/logic/index.d.ts +1 -0
  99. package/lib/esm/logic/index.js +1 -0
  100. package/lib/esm/logic/s3/generatePresignedUploadUrl.js +5 -5
  101. package/package.json +8 -7
  102. package/lib/commonjs/logic/apiGateway/getApiKeys.d.ts +0 -2
  103. package/lib/esm/logic/apiGateway/getApiKeys.d.ts +0 -2
  104. package/lib/esm/logic/apiGateway/getApiKeys.js +0 -17
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.getMetricPutActionProcessor = void 0;
13
+ const quidproquo_core_1 = require("quidproquo-core");
14
+ // Emits the metric as a CloudWatch Embedded Metric Format (EMF) log line: CloudWatch
15
+ // extracts metrics from the function's log stream - no api calls, no extra IAM, no
16
+ // latency on the story. Cost: each unique metricName x dimension-combination is one
17
+ // custom metric (~$0.30/mo) - the standard dimensions are service/environment(/feature),
18
+ // so keep payload dimensions low-cardinality (never per-user / per-request ids).
19
+ const getProcessMetricPut = (qpqConfig) => {
20
+ // The namespace is the deployed app instance - app/environment(/feature) - mirroring how
21
+ // every qpq resource name embeds the deployment identity. Apps sharing an account can't
22
+ // merge same-named metrics, per-developer feature sandboxes (e.g. qpq/docgen/development/jane)
23
+ // never pollute mainline, and dimensions are reserved for slices WITHIN the deployment
24
+ // (service + caller-supplied).
25
+ const application = quidproquo_core_1.qpqCoreUtils.getApplicationName(qpqConfig);
26
+ const environment = quidproquo_core_1.qpqCoreUtils.getApplicationModuleEnvironment(qpqConfig);
27
+ const feature = quidproquo_core_1.qpqCoreUtils.getApplicationModuleFeature(qpqConfig);
28
+ const namespace = `qpq/${application}/${environment}${feature ? `/${feature}` : ''}`;
29
+ const service = quidproquo_core_1.qpqCoreUtils.getApplicationModuleName(qpqConfig);
30
+ return (_a) => __awaiter(void 0, [_a], void 0, function* ({ metricName, value, unit, dimensions }) {
31
+ const allDimensions = Object.assign({ service }, dimensions);
32
+ const emf = Object.assign(Object.assign({ _aws: {
33
+ Timestamp: Date.now(),
34
+ CloudWatchMetrics: [
35
+ {
36
+ Namespace: namespace,
37
+ Dimensions: [Object.keys(allDimensions)],
38
+ Metrics: [{ Name: metricName, Unit: unit !== null && unit !== void 0 ? unit : quidproquo_core_1.MetricUnit.count }],
39
+ },
40
+ ],
41
+ } }, allDimensions), { [metricName]: value });
42
+ console.log(JSON.stringify(emf));
43
+ return (0, quidproquo_core_1.actionResult)(void 0);
44
+ });
45
+ };
46
+ const getMetricPutActionProcessor = (qpqConfig) => __awaiter(void 0, void 0, void 0, function* () {
47
+ return ({
48
+ [quidproquo_core_1.MetricActionType.Put]: getProcessMetricPut(qpqConfig),
49
+ });
50
+ });
51
+ exports.getMetricPutActionProcessor = getMetricPutActionProcessor;
@@ -0,0 +1,2 @@
1
+ import { ActionProcessorListResolver } from 'quidproquo-core';
2
+ export declare const getMetricActionProcessor: ActionProcessorListResolver;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.getMetricActionProcessor = void 0;
13
+ const getMetricPutActionProcessor_1 = require("./getMetricPutActionProcessor");
14
+ const getMetricActionProcessor = (qpqConfig, dynamicModuleLoader) => __awaiter(void 0, void 0, void 0, function* () {
15
+ return (Object.assign({}, (yield (0, getMetricPutActionProcessor_1.getMetricPutActionProcessor)(qpqConfig, dynamicModuleLoader))));
16
+ });
17
+ exports.getMetricActionProcessor = getMetricActionProcessor;
@@ -94,9 +94,7 @@ const createBufferedLogger = (bucketName, regionForBucket, ship) => {
94
94
  let disabledLogCorrelations = [];
95
95
  return {
96
96
  enableLogs: (enable, reason, correlation) => __awaiter(void 0, void 0, void 0, function* () {
97
- disabledLogCorrelations = enable
98
- ? disabledLogCorrelations.filter((dlc) => dlc !== correlation)
99
- : [...disabledLogCorrelations, correlation];
97
+ disabledLogCorrelations = enable ? disabledLogCorrelations.filter((dlc) => dlc !== correlation) : [...disabledLogCorrelations, correlation];
100
98
  }),
101
99
  log: (result) => {
102
100
  // Defer all work (including JSON.stringify) to a microtask so the caller is not blocked.
@@ -10,9 +10,16 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.dynamicModuleLoader = void 0;
13
- // @ts-expect-error - Special webpack file injected
13
+ // @ts-expect-error - injected at build time by QpqPlugin (webpack virtual module)
14
14
  const quidproquo_dynamic_loader_1 = require("quidproquo-dynamic-loader");
15
+ const federatedModuleStore_1 = require("../../logic/federatedModuleStore");
15
16
  const dynamicModuleLoader = (qpqFunctionRuntime) => __awaiter(void 0, void 0, void 0, function* () {
17
+ // Tier 1: federated. undefined means "nothing published for this runtime" -> fall through.
18
+ const federatedStory = yield (0, federatedModuleStore_1.loadFederatedStory)(quidproquo_dynamic_loader_1.qpqConfig, qpqFunctionRuntime);
19
+ if (federatedStory !== undefined) {
20
+ return federatedStory;
21
+ }
22
+ // Tier 2: the statically bundled story (or a fail-fast throw for a thin shell).
16
23
  return (0, quidproquo_dynamic_loader_1.qpqDynamicModuleLoader)(qpqFunctionRuntime);
17
24
  });
18
25
  exports.dynamicModuleLoader = dynamicModuleLoader;
@@ -0,0 +1 @@
1
+ export declare const getApiKeyValue: (region: string, apiKeyId: string) => Promise<string | undefined>;
@@ -9,21 +9,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.getApiKeys = void 0;
12
+ exports.getApiKeyValue = void 0;
13
13
  const client_api_gateway_1 = require("@aws-sdk/client-api-gateway");
14
14
  const createAwsClient_1 = require("../createAwsClient");
15
- const getApiKeys = (region, ...keyNames) => __awaiter(void 0, void 0, void 0, function* () {
16
- var _a;
15
+ // Fetch a single api key's value by id - keys are read individually (not listed) so the
16
+ // IAM grant can be scoped per-key with tag conditions rather than the /apikeys collection.
17
+ const getApiKeyValue = (region, apiKeyId) => __awaiter(void 0, void 0, void 0, function* () {
17
18
  const apiGatewayClient = (0, createAwsClient_1.createAwsClient)(client_api_gateway_1.APIGatewayClient, { region });
18
- const input = {
19
- includeValues: true,
20
- limit: 500,
21
- };
22
- const res = yield apiGatewayClient.send(new client_api_gateway_1.GetApiKeysCommand(input));
23
- return (((_a = res.items) === null || _a === void 0 ? void 0 : _a.filter((i) => keyNames.indexOf(i.name) >= 0).map((i) => ({
24
- name: i.name,
25
- value: i.value,
26
- description: i.description,
27
- }))) || []);
19
+ const res = yield apiGatewayClient.send(new client_api_gateway_1.GetApiKeyCommand({
20
+ apiKey: apiKeyId,
21
+ includeValue: true,
22
+ }));
23
+ return res.value;
28
24
  });
29
- exports.getApiKeys = getApiKeys;
25
+ exports.getApiKeyValue = getApiKeyValue;
@@ -7,7 +7,7 @@ const cognitoAuthenticationResultTypeToQpqAuthenticationInfo = (authResult, issu
7
7
  // Parse the issueDateTime and add the expiresIn to get the expiration date
8
8
  let issueDate = new Date(issueDateTime);
9
9
  issueDate.setSeconds(issueDate.getSeconds() + (authResult.ExpiresIn || 0));
10
- const expiresAt = issueDate.toISOString();
10
+ const expiresAt = (0, quidproquo_core_1.getQpqIsoDateTimeFromDate)(issueDate);
11
11
  return {
12
12
  accessToken: authResult.AccessToken,
13
13
  idToken: authResult.IdToken,
@@ -1,5 +1,6 @@
1
1
  import { KvsKey } from 'quidproquo-core';
2
2
  export interface PutItemOptions {
3
3
  expires?: number;
4
+ ifNotExistsAttribute?: string;
4
5
  }
5
6
  export declare function putItem<Item, T extends object = any>(tableName: string, item: Item, attributes: KvsKey[], options: PutItemOptions, region: string): Promise<void>;
@@ -39,9 +39,11 @@ const convertObjectToDynamoItem = (obj) => {
39
39
  function putItem(tableName, item, attributes, options, region) {
40
40
  return __awaiter(this, void 0, void 0, function* () {
41
41
  const dynamoDBClient = (0, createAwsClient_1.createAwsClient)(client_dynamodb_1.DynamoDBClient, { region });
42
- yield dynamoDBClient.send(new client_dynamodb_1.PutItemCommand({
43
- TableName: tableName,
44
- Item: convertObjectToDynamoItem(item),
45
- }));
42
+ yield dynamoDBClient.send(new client_dynamodb_1.PutItemCommand(Object.assign({ TableName: tableName, Item: convertObjectToDynamoItem(item) }, (options.ifNotExistsAttribute
43
+ ? {
44
+ ConditionExpression: 'attribute_not_exists(#ineAttr)',
45
+ ExpressionAttributeNames: { '#ineAttr': options.ifNotExistsAttribute },
46
+ }
47
+ : {}))));
46
48
  });
47
49
  }
@@ -120,11 +120,7 @@ const buildDynamoUpdateExpressionPart = (update, updateIndex) => {
120
120
  }
121
121
  };
122
122
  // Types that generate SET expressions
123
- const SET_ACTION_TYPES = [
124
- quidproquo_core_1.KvsUpdateActionType.Set,
125
- quidproquo_core_1.KvsUpdateActionType.SetIfNotExists,
126
- quidproquo_core_1.KvsUpdateActionType.Increment
127
- ];
123
+ const SET_ACTION_TYPES = [quidproquo_core_1.KvsUpdateActionType.Set, quidproquo_core_1.KvsUpdateActionType.SetIfNotExists, quidproquo_core_1.KvsUpdateActionType.Increment];
128
124
  const buildDynamoUpdateExpressionForClause = (clause, actionTypes, kvsUpdate) => {
129
125
  const actions = kvsUpdate.filter((update) => actionTypes.includes(update.action));
130
126
  if (actions.length === 0) {
@@ -0,0 +1,10 @@
1
+ export declare enum FederatedModuleLoadErrorCode {
2
+ unsupportedStoreUrl = "unsupportedStoreUrl",
3
+ manifestInvalid = "manifestInvalid",
4
+ containerLoadFailed = "containerLoadFailed",
5
+ storyNotExported = "storyNotExported"
6
+ }
7
+ export declare class FederatedModuleLoadError extends Error {
8
+ readonly code: FederatedModuleLoadErrorCode;
9
+ constructor(code: FederatedModuleLoadErrorCode, message: string);
10
+ }
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FederatedModuleLoadError = exports.FederatedModuleLoadErrorCode = void 0;
4
+ // Discriminable failures raised inside loadFederatedStory. All of them are ultimately
5
+ // caught there and turned into a fall-back-to-bundled (with a warning) - they exist so
6
+ // logs/backoff can tell WHY a store didn't load, not to crash the request.
7
+ var FederatedModuleLoadErrorCode;
8
+ (function (FederatedModuleLoadErrorCode) {
9
+ // Store URL wasn't s3:// or file:// (a config/env mistake).
10
+ FederatedModuleLoadErrorCode["unsupportedStoreUrl"] = "unsupportedStoreUrl";
11
+ // manifest.json parsed but is missing required fields (bad/partial publish).
12
+ FederatedModuleLoadErrorCode["manifestInvalid"] = "manifestInvalid";
13
+ // A version's files downloaded but the entry didn't expose a valid MF container.
14
+ FederatedModuleLoadErrorCode["containerLoadFailed"] = "containerLoadFailed";
15
+ // The manifest exposed the module, but it didn't export the requested story name
16
+ // (e.g. a story was renamed in the remote but the manifest still lists it).
17
+ FederatedModuleLoadErrorCode["storyNotExported"] = "storyNotExported";
18
+ })(FederatedModuleLoadErrorCode || (exports.FederatedModuleLoadErrorCode = FederatedModuleLoadErrorCode = {}));
19
+ class FederatedModuleLoadError extends Error {
20
+ constructor(code, message) {
21
+ super(message);
22
+ this.code = code;
23
+ this.name = 'FederatedModuleLoadError';
24
+ }
25
+ }
26
+ exports.FederatedModuleLoadError = FederatedModuleLoadError;
@@ -0,0 +1,8 @@
1
+ export interface FederatedModuleStoreManifest {
2
+ containerName: string;
3
+ service: string;
4
+ hash: string;
5
+ entry: string;
6
+ files: string[];
7
+ exposes: Record<string, string>;
8
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,4 @@
1
+ export * from './FederatedModuleLoadError';
2
+ export * from './FederatedModuleStoreManifest';
3
+ export * from './loadFederatedStory';
4
+ export * from './sharedPackages';
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./FederatedModuleLoadError"), exports);
18
+ __exportStar(require("./FederatedModuleStoreManifest"), exports);
19
+ __exportStar(require("./loadFederatedStory"), exports);
20
+ __exportStar(require("./sharedPackages"), exports);
@@ -0,0 +1,3 @@
1
+ import { QPQConfig, QpqFunctionRuntime } from 'quidproquo-core';
2
+ export declare const resetFederatedModuleStoreCache: () => void;
3
+ export declare const loadFederatedStory: <T = any>(qpqConfig: QPQConfig, qpqFunctionRuntime: QpqFunctionRuntime) => Promise<T | undefined>;
@@ -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);
@@ -15,9 +15,9 @@ const s3_request_presigner_1 = require("@aws-sdk/s3-request-presigner");
15
15
  // import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
16
16
  const createAwsClient_1 = require("../createAwsClient");
17
17
  // export const generatePresignedUploadFormUrl = async (
18
- // bucketName: string,
19
- // objectKey: string,
20
- // region: string,
18
+ // bucketName: string,
19
+ // objectKey: string,
20
+ // region: string,
21
21
  // expirationMs: number,
22
22
  // contentType: string | undefined,
23
23
  // maxSizeBytes?: number | undefined
@@ -56,7 +56,7 @@ const generatePresignedUploadUrl = (bucketName, objectKey, region, expirationMs,
56
56
  Bucket: bucketName,
57
57
  Key: objectKey,
58
58
  ContentType: contentType,
59
- Metadata: Object.assign(Object.assign({}, (correlationId && { 'correlation-id': correlationId })), { 'upload-timestamp': Date.now().toString() })
59
+ Metadata: Object.assign(Object.assign({}, (correlationId && { 'correlation-id': correlationId })), { 'upload-timestamp': Date.now().toString() }),
60
60
  });
61
61
  const url = yield (0, s3_request_presigner_1.getSignedUrl)(s3Client, putObjectCommand, {
62
62
  expiresIn: expirationMs / 1000,
@@ -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;