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
@@ -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,21 +1,28 @@
1
1
  import { actionResult, actionResultError, AiActionType, ErrorTypeEnum, } from 'quidproquo-core';
2
2
  import { generateText, stepCountIs } from 'ai';
3
- import { prepareAiPromptCall, toSdkMessages } from './logic';
3
+ import { createDriveFileResolver, prepareAiPromptCall, toSdkMessages } from './logic';
4
4
  const getProcessAiPrompt = (qpqConfig) => {
5
5
  return async (payload, session, actionProcessorList, logger, updateSession, dynamicModuleLoader, streamRegistry) => {
6
6
  const prepared = prepareAiPromptCall(qpqConfig, payload, session, actionProcessorList, logger, dynamicModuleLoader, streamRegistry);
7
7
  if ('error' in prepared) {
8
8
  return actionResultError(prepared.error.type, prepared.error.message);
9
9
  }
10
- const promptOrMessages = payload.messages
11
- ? { messages: toSdkMessages(payload.messages) }
12
- : { prompt: payload.prompt };
13
10
  try {
11
+ const promptOrMessages = payload.messages
12
+ ? {
13
+ messages: await toSdkMessages(payload.messages, createDriveFileResolver(qpqConfig, session, actionProcessorList, logger, dynamicModuleLoader, streamRegistry)),
14
+ }
15
+ : { prompt: payload.prompt };
16
+ // Extended thinking — the model reasons before answering, within the token budget.
17
+ const providerOptions = payload.reasoning
18
+ ? { bedrock: { reasoningConfig: { type: 'enabled', budgetTokens: payload.reasoning.budgetTokens ?? 4096 } } }
19
+ : undefined;
14
20
  const result = await generateText({
15
21
  model: prepared.model,
16
22
  system: payload.system,
17
23
  ...promptOrMessages,
18
24
  tools: prepared.tools,
25
+ providerOptions,
19
26
  stopWhen: stepCountIs(10),
20
27
  });
21
28
  return actionResult({ text: result.text });
@@ -1,22 +1,29 @@
1
1
  import { actionResult, actionResultError, AiActionType, ErrorTypeEnum, } from 'quidproquo-core';
2
2
  import { stepCountIs, streamText } from 'ai';
3
3
  import { randomGuid } from '../../../awsLambdaUtils';
4
- import { mapAiStreamPart, prepareAiPromptCall, toSdkMessages } from './logic';
4
+ import { createDriveFileResolver, mapAiStreamPart, prepareAiPromptCall, toSdkMessages } from './logic';
5
5
  const getProcessAiPromptStream = (qpqConfig) => {
6
6
  return async (payload, session, actionProcessorList, logger, updateSession, dynamicModuleLoader, streamRegistry) => {
7
7
  const prepared = prepareAiPromptCall(qpqConfig, payload, session, actionProcessorList, logger, dynamicModuleLoader, streamRegistry);
8
8
  if ('error' in prepared) {
9
9
  return actionResultError(prepared.error.type, prepared.error.message);
10
10
  }
11
- const promptOrMessages = payload.messages
12
- ? { messages: toSdkMessages(payload.messages) }
13
- : { prompt: payload.prompt };
14
11
  try {
12
+ const promptOrMessages = payload.messages
13
+ ? {
14
+ messages: await toSdkMessages(payload.messages, createDriveFileResolver(qpqConfig, session, actionProcessorList, logger, dynamicModuleLoader, streamRegistry)),
15
+ }
16
+ : { prompt: payload.prompt };
17
+ // Extended thinking — thinking progress streams out as Reasoning* parts.
18
+ const providerOptions = payload.reasoning
19
+ ? { bedrock: { reasoningConfig: { type: 'enabled', budgetTokens: payload.reasoning.budgetTokens ?? 4096 } } }
20
+ : undefined;
15
21
  const { fullStream } = streamText({
16
22
  model: prepared.model,
17
23
  system: payload.system,
18
24
  ...promptOrMessages,
19
25
  tools: prepared.tools,
26
+ providerOptions,
20
27
  stopWhen: stepCountIs(10),
21
28
  // streamText swallows errors by default to keep the server alive — surface them to CloudWatch.
22
29
  onError: ({ error }) => {
@@ -25,17 +25,17 @@ import { mapAiStreamToolInputStart } from './mapAiStreamToolInputStart';
25
25
  import { mapAiStreamToolOutputDenied } from './mapAiStreamToolOutputDenied';
26
26
  import { mapAiStreamToolResult } from './mapAiStreamToolResult';
27
27
  export const aiStreamPartMappers = {
28
- 'start': mapAiStreamStart,
29
- 'finish': mapAiStreamFinish,
28
+ start: mapAiStreamStart,
29
+ finish: mapAiStreamFinish,
30
30
  'start-step': mapAiStreamStartStep,
31
31
  'finish-step': mapAiStreamFinishStep,
32
- 'abort': mapAiStreamAbort,
33
- 'error': mapAiStreamError,
34
- 'raw': mapAiStreamRaw,
35
- 'source': mapAiStreamSourcePart,
36
- 'file': mapAiStreamFilePart,
32
+ abort: mapAiStreamAbort,
33
+ error: mapAiStreamError,
34
+ raw: mapAiStreamRaw,
35
+ source: mapAiStreamSourcePart,
36
+ file: mapAiStreamFilePart,
37
37
  'reasoning-file': mapAiStreamReasoningFilePart,
38
- 'custom': mapAiStreamCustom,
38
+ custom: mapAiStreamCustom,
39
39
  'text-start': mapAiStreamTextStart,
40
40
  'text-end': mapAiStreamTextEnd,
41
41
  'text-delta': mapAiStreamTextDelta,
@@ -0,0 +1,3 @@
1
+ import { ActionProcessorList, DynamicModuleLoader, QPQConfig, QpqLogger, StorySession, StreamRegistry } from 'quidproquo-core';
2
+ import { AiDriveFileResolver } from './toSdkMessages';
3
+ export declare const createDriveFileResolver: (qpqConfig: QPQConfig, session: StorySession, actionProcessorList: ActionProcessorList, logger: QpqLogger, dynamicModuleLoader: DynamicModuleLoader, streamRegistry?: StreamRegistry) => AiDriveFileResolver;
@@ -0,0 +1,14 @@
1
+ import { askFileReadBinaryContents, createImplementationRuntime, } from 'quidproquo-core';
2
+ import { randomGuid } from '../../../../awsLambdaUtils';
3
+ // Reads drive-referenced files through the active file action processor, so the
4
+ // same message works against S3 in production and local disk under the dev server.
5
+ export const createDriveFileResolver = (qpqConfig, session, actionProcessorList, logger, dynamicModuleLoader, streamRegistry) => {
6
+ return async (drive, filepath) => {
7
+ const resolveStory = createImplementationRuntime(qpqConfig, [`AI File: ${drive}/${filepath}`], () => new Date().toISOString(), randomGuid, session, actionProcessorList, logger, dynamicModuleLoader, streamRegistry);
8
+ const storyResult = await resolveStory(askFileReadBinaryContents, [drive, filepath]);
9
+ if (storyResult.error) {
10
+ throw new Error(storyResult.error.errorText);
11
+ }
12
+ return storyResult.result;
13
+ };
14
+ };
@@ -1,4 +1,5 @@
1
1
  export * from './aiStreamPartMappers';
2
+ export * from './createDriveFileResolver';
2
3
  export * from './mapAiStreamAbort';
3
4
  export * from './mapAiStreamCustom';
4
5
  export * from './mapAiStreamError';
@@ -1,4 +1,5 @@
1
1
  export * from './aiStreamPartMappers';
2
+ export * from './createDriveFileResolver';
2
3
  export * from './mapAiStreamAbort';
3
4
  export * from './mapAiStreamCustom';
4
5
  export * from './mapAiStreamError';
@@ -1,3 +1,4 @@
1
- import { AiMessage } from 'quidproquo-core';
1
+ import { AiMessage, QPQBinaryData } from 'quidproquo-core';
2
2
  import type { ModelMessage } from 'ai';
3
- export declare const toSdkMessages: (messages: AiMessage[]) => ModelMessage[];
3
+ export type AiDriveFileResolver = (drive: string, filepath: string) => Promise<QPQBinaryData>;
4
+ export declare const toSdkMessages: (messages: AiMessage[], resolveDriveFile: AiDriveFileResolver) => Promise<ModelMessage[]>;
@@ -1,25 +1,32 @@
1
- const mapUserPart = (part) => {
2
- if (part.type === 'text') {
3
- return { type: 'text', text: part.text };
1
+ const mapFilePart = async (part, resolveDriveFile) => {
2
+ if ('url' in part) {
3
+ return {
4
+ type: 'file',
5
+ data: new URL(part.url),
6
+ mediaType: part.mediaType,
7
+ filename: part.filename,
8
+ };
4
9
  }
10
+ const binary = await resolveDriveFile(part.drive, part.filepath);
5
11
  return {
6
12
  type: 'file',
7
- data: new URL(part.url),
13
+ data: binary.base64Data,
8
14
  mediaType: part.mediaType,
9
- filename: part.filename,
15
+ filename: part.filename ?? binary.filename,
10
16
  };
11
17
  };
12
- const mapAssistantPart = (part) => {
18
+ const mapUserPart = async (part, resolveDriveFile) => {
19
+ if (part.type === 'text') {
20
+ return { type: 'text', text: part.text };
21
+ }
22
+ return mapFilePart(part, resolveDriveFile);
23
+ };
24
+ const mapAssistantPart = async (part, resolveDriveFile) => {
13
25
  switch (part.type) {
14
26
  case 'text':
15
27
  return { type: 'text', text: part.text };
16
28
  case 'file':
17
- return {
18
- type: 'file',
19
- data: new URL(part.url),
20
- mediaType: part.mediaType,
21
- filename: part.filename,
22
- };
29
+ return mapFilePart(part, resolveDriveFile);
23
30
  case 'tool-call':
24
31
  return {
25
32
  type: 'tool-call',
@@ -50,29 +57,29 @@ const mapToolResult = (part) => {
50
57
  output,
51
58
  };
52
59
  };
53
- const mapUserMessage = (message) => {
60
+ const mapUserMessage = async (message, resolveDriveFile) => {
54
61
  if (typeof message.content === 'string') {
55
62
  return { role: 'user', content: message.content };
56
63
  }
57
- return { role: 'user', content: message.content.map(mapUserPart) };
64
+ return { role: 'user', content: await Promise.all(message.content.map((part) => mapUserPart(part, resolveDriveFile))) };
58
65
  };
59
- const mapAssistantMessage = (message) => {
66
+ const mapAssistantMessage = async (message, resolveDriveFile) => {
60
67
  if (typeof message.content === 'string') {
61
68
  return { role: 'assistant', content: message.content };
62
69
  }
63
- return { role: 'assistant', content: message.content.map(mapAssistantPart) };
70
+ return { role: 'assistant', content: await Promise.all(message.content.map((part) => mapAssistantPart(part, resolveDriveFile))) };
64
71
  };
65
72
  const mapToolMessage = (message) => ({
66
73
  role: 'tool',
67
74
  content: message.content.map(mapToolResult),
68
75
  });
69
- export const toSdkMessages = (messages) => messages.map((message) => {
76
+ export const toSdkMessages = (messages, resolveDriveFile) => Promise.all(messages.map((message) => {
70
77
  switch (message.role) {
71
78
  case 'user':
72
- return mapUserMessage(message);
79
+ return mapUserMessage(message, resolveDriveFile);
73
80
  case 'assistant':
74
- return mapAssistantMessage(message);
81
+ return mapAssistantMessage(message, resolveDriveFile);
75
82
  case 'tool':
76
83
  return mapToolMessage(message);
77
84
  }
78
- });
85
+ }));
@@ -1,9 +1,10 @@
1
1
  import { qpqConfigAwsUtils } from 'quidproquo-config-aws';
2
- import { actionResult, qpqCoreUtils, } from 'quidproquo-core';
2
+ import { actionResult } from 'quidproquo-core';
3
3
  import { ApiKeyValidationActionType } from 'quidproquo-webserver';
4
4
  import { timingSafeEqual } from 'crypto';
5
- import { getConfigRuntimeResourceName } from '../../../awsNamingUtils';
6
- import { getApiKeys } from '../../../logic/apiGateway/getApiKeys';
5
+ import { getCFExportNameApiKeyIdFromConfig } from '../../../awsNamingUtils';
6
+ import { getApiKeyValue } from '../../../logic/apiGateway/getApiKeyValue';
7
+ import { getExportedValue } from '../../../logic/cloudformation/getExportedValue';
7
8
  // Constant-time string equality. `timingSafeEqual` throws on length mismatch,
8
9
  // so guard the length check — the length itself isn't secret for API keys.
9
10
  const safeEqual = (a, b) => {
@@ -16,17 +17,20 @@ const safeEqual = (a, b) => {
16
17
  };
17
18
  const getProcessApiKeyValidationValidate = (qpqConfig) => {
18
19
  const region = qpqConfigAwsUtils.getApplicationModuleDeployRegion(qpqConfig);
19
- const application = qpqCoreUtils.getApplicationName(qpqConfig);
20
- const service = qpqCoreUtils.getApplicationModuleName(qpqConfig);
21
- const environment = qpqCoreUtils.getApplicationModuleEnvironment(qpqConfig);
22
- const feature = qpqCoreUtils.getApplicationModuleFeature(qpqConfig);
23
20
  return async ({ apiKeyValue, apiKeyReferences }) => {
24
- const realApiKeys = await getApiKeys(region, ...apiKeyReferences.map((apiKey) => {
25
- const apiKeyApplication = apiKey.applicationName || application;
26
- const apiKeyService = apiKey.serviceName || service;
27
- return getConfigRuntimeResourceName(apiKey.name, apiKeyApplication, apiKeyService, environment, feature);
21
+ // Fetch each referenced key individually (id via CFN export, then GetApiKey) rather than
22
+ // listing every key in the account - keeps the IAM grant per-key instead of /apikeys-wide.
23
+ const realApiKeyValues = await Promise.all(apiKeyReferences.map(async (apiKeyReference) => {
24
+ try {
25
+ const apiKeyId = await getExportedValue(getCFExportNameApiKeyIdFromConfig(apiKeyReference.name, qpqConfig, apiKeyReference.serviceName, apiKeyReference.applicationName), region);
26
+ return await getApiKeyValue(region, apiKeyId);
27
+ }
28
+ catch {
29
+ // An unresolvable reference behaves like the old list-and-filter miss - no match
30
+ return undefined;
31
+ }
28
32
  }));
29
- const matched = realApiKeys.some((apiKey) => safeEqual(apiKey.value, apiKeyValue));
33
+ const matched = realApiKeyValues.some((realApiKeyValue) => safeEqual(realApiKeyValue, apiKeyValue));
30
34
  return actionResult(matched);
31
35
  };
32
36
  };
@@ -1,8 +1,9 @@
1
1
  import { actionResult, EventActionType, qpqCoreUtils, } from 'quidproquo-core';
2
- import { qpqWebServerUtils } from 'quidproquo-webserver';
3
- import { parseMultipartFormData } from '../../utils/parseMultipartFormData';
2
+ import { FileUploadErrorTypeEnum, qpqWebServerUtils } from 'quidproquo-webserver';
3
+ import { FileUploadValidationError, parseMultipartFormData } from '../../utils/parseMultipartFormData';
4
4
  const getProcessGetRecords = (qpqConfig) => {
5
5
  const serviceName = qpqCoreUtils.getApplicationModuleName(qpqConfig);
6
+ const fileUploadSettings = qpqWebServerUtils.getFileUploadSettings(qpqConfig);
6
7
  return async ({ eventParams: [apiGatewayEvent, context] }) => {
7
8
  // Initialize `path` by removing the service name prefix from `apiGatewayEvent.path`.
8
9
  // This adjustment is necessary because the API gateway routes requests to services based on
@@ -26,7 +27,17 @@ const getProcessGetRecords = (qpqConfig) => {
26
27
  };
27
28
  // Transform the body if its a multipart/form-data
28
29
  if ((qpqWebServerUtils.getHeaderValue('Content-Type', apiGatewayEvent.headers) || '').startsWith('multipart/form-data') && apiGatewayEvent.body) {
29
- internalEventRecord.files = await parseMultipartFormData(apiGatewayEvent);
30
+ try {
31
+ internalEventRecord.files = await parseMultipartFormData(apiGatewayEvent, fileUploadSettings);
32
+ }
33
+ catch (error) {
34
+ // Stamp the failure on the record so the auto-respond step returns the matching
35
+ // 4xx before the route story runs - throwing here would fail the whole event as a 5xx
36
+ internalEventRecord.fileUploadError =
37
+ error instanceof FileUploadValidationError
38
+ ? { errorType: error.errorType, message: error.message }
39
+ : { errorType: FileUploadErrorTypeEnum.malformed, message: 'Unable to parse multipart/form-data body' };
40
+ }
30
41
  }
31
42
  return actionResult([internalEventRecord]);
32
43
  };
@@ -1,3 +1,9 @@
1
1
  import { QPQBinaryData } from 'quidproquo-core';
2
+ import { FileUploadErrorTypeEnum, FileUploadSettings } from 'quidproquo-webserver';
2
3
  import { APIGatewayEvent } from 'aws-lambda';
3
- export declare const parseMultipartFormData: (event: APIGatewayEvent) => Promise<QPQBinaryData[]>;
4
+ export declare class FileUploadValidationError extends Error {
5
+ readonly errorType: FileUploadErrorTypeEnum;
6
+ constructor(errorType: FileUploadErrorTypeEnum, message: string);
7
+ }
8
+ export declare const sanitizeFilename: (filename?: string) => string;
9
+ export declare const parseMultipartFormData: (event: APIGatewayEvent, fileUploadSettings: FileUploadSettings) => Promise<QPQBinaryData[]>;
@@ -1,38 +1,106 @@
1
+ import { FileUploadErrorTypeEnum } from 'quidproquo-webserver';
1
2
  import Busboy from 'busboy';
2
- export const parseMultipartFormData = (event) => new Promise((resolve, reject) => {
3
+ export class FileUploadValidationError extends Error {
4
+ errorType;
5
+ constructor(errorType, message) {
6
+ super(message);
7
+ this.errorType = errorType;
8
+ this.name = 'FileUploadValidationError';
9
+ }
10
+ }
11
+ // Reduce a client-supplied filename to a bare, path-traversal-safe name.
12
+ export const sanitizeFilename = (filename) => {
13
+ const baseName = (filename || '')
14
+ .split(/[/\\]/)
15
+ .filter((segment) => segment.length > 0)
16
+ .pop();
17
+ // eslint-disable-next-line no-control-regex
18
+ const cleaned = (baseName || '').replace(/[\x00-\x1f]/g, '');
19
+ if (!cleaned || cleaned === '.' || cleaned === '..') {
20
+ return 'unnamed';
21
+ }
22
+ return cleaned;
23
+ };
24
+ const isMimeTypeAllowed = (mimeType, allowedMimeTypes) => {
25
+ if (!allowedMimeTypes) {
26
+ return true;
27
+ }
28
+ const normalizedMimeType = (mimeType || '').toLowerCase();
29
+ return allowedMimeTypes.some((allowed) => {
30
+ const normalizedAllowed = allowed.toLowerCase();
31
+ return normalizedAllowed.endsWith('/*')
32
+ ? normalizedMimeType.startsWith(normalizedAllowed.slice(0, -1))
33
+ : normalizedMimeType === normalizedAllowed;
34
+ });
35
+ };
36
+ export const parseMultipartFormData = (event, fileUploadSettings) => new Promise((resolve, reject) => {
3
37
  const busboy = Busboy({
4
38
  headers: {
5
39
  'content-type': event.headers['content-type'] || event.headers['Content-Type'],
6
40
  },
41
+ limits: {
42
+ fileSize: fileUploadSettings.maxFileSizeBytes,
43
+ files: fileUploadSettings.maxFileCount,
44
+ fields: fileUploadSettings.maxFieldCount,
45
+ fieldSize: fileUploadSettings.maxFieldSizeBytes,
46
+ },
7
47
  });
8
48
  const result = {
9
49
  files: [],
10
50
  fields: {},
11
51
  };
52
+ let settled = false;
53
+ const rejectOnce = (error) => {
54
+ if (!settled) {
55
+ settled = true;
56
+ reject(error);
57
+ }
58
+ };
12
59
  busboy.on('file', (name, file, info) => {
13
- let fileBuffer = Buffer.alloc(0);
60
+ const filename = sanitizeFilename(info.filename);
61
+ if (!isMimeTypeAllowed(info.mimeType, fileUploadSettings.allowedMimeTypes)) {
62
+ rejectOnce(new FileUploadValidationError(FileUploadErrorTypeEnum.disallowedMimeType, `File "${filename}" has disallowed content type "${info.mimeType}"`));
63
+ file.resume();
64
+ return;
65
+ }
66
+ const chunks = [];
14
67
  file.on('data', (data) => {
15
- fileBuffer = Buffer.concat([fileBuffer, data]); // Concatenate the new data to the existing buffer
68
+ chunks.push(data);
69
+ });
70
+ // Emitted when the stream is truncated at limits.fileSize - a silently truncated upload must fail, not succeed
71
+ file.on('limit', () => {
72
+ rejectOnce(new FileUploadValidationError(FileUploadErrorTypeEnum.fileTooLarge, `File "${filename}" exceeds the maximum file size of ${fileUploadSettings.maxFileSizeBytes} bytes`));
16
73
  });
17
74
  file.on('end', () => {
18
- if (fileBuffer.length > 0) {
75
+ const content = Buffer.concat(chunks);
76
+ if (!settled && content.length > 0) {
19
77
  result.files.push({
20
- filename: info.filename,
78
+ filename,
21
79
  contentType: info.mimeType,
22
80
  encoding: info.encoding,
23
81
  name: name,
24
- content: fileBuffer,
82
+ content,
25
83
  });
26
84
  }
27
85
  });
28
86
  });
87
+ busboy.on('filesLimit', () => {
88
+ rejectOnce(new FileUploadValidationError(FileUploadErrorTypeEnum.tooManyFiles, `Upload exceeds the maximum of ${fileUploadSettings.maxFileCount} files`));
89
+ });
90
+ busboy.on('fieldsLimit', () => {
91
+ rejectOnce(new FileUploadValidationError(FileUploadErrorTypeEnum.tooManyFields, `Upload exceeds the maximum of ${fileUploadSettings.maxFieldCount} fields`));
92
+ });
29
93
  busboy.on('field', (name, value) => {
30
94
  result.fields[name] = value;
31
95
  });
32
96
  busboy.on('error', (error) => {
33
- reject(error);
97
+ rejectOnce(error);
34
98
  });
35
99
  busboy.on('finish', () => {
100
+ if (settled) {
101
+ return;
102
+ }
103
+ settled = true;
36
104
  const qpqBinaryFiles = result.files.map((f) => ({
37
105
  filename: f.filename,
38
106
  mimetype: f.contentType,
@@ -3,7 +3,7 @@ import { actionResult, actionResultError, actionResultErrorFromCaughtError, Erro
3
3
  import { getEventBusSnsTopicArn } from '../../../awsNamingUtils';
4
4
  import { publishMessage } from '../../../logic/sns/publishMessage';
5
5
  const getProcessEventBusSendMessage = (qpqConfig) => {
6
- return async ({ eventBusName, eventBusMessages, }, session) => {
6
+ return async ({ eventBusName, eventBusMessages }, session) => {
7
7
  const region = qpqConfigAwsUtils.getApplicationModuleDeployRegion(qpqConfig);
8
8
  const eventBusConfig = qpqCoreUtils.getEventBusConfigByName(eventBusName, qpqConfig);
9
9
  if (!eventBusConfig) {
@@ -1,5 +1,5 @@
1
1
  import { qpqConfigAwsUtils } from 'quidproquo-config-aws';
2
- import { actionResult, actionResultError, actionResultErrorFromCaughtError, FileActionType, FileDeleteErrorTypeEnum } from 'quidproquo-core';
2
+ import { actionResult, actionResultError, actionResultErrorFromCaughtError, FileActionType, FileDeleteErrorTypeEnum, } from 'quidproquo-core';
3
3
  import { deleteFiles } from '../../../logic/s3/s3Utils';
4
4
  import { resolveStorageDriveBucketName } from './utils';
5
5
  const getProcessFileDelete = (qpqConfig) => {
@@ -1,6 +1,6 @@
1
1
  import { qpqConfigAwsUtils } from 'quidproquo-config-aws';
2
2
  import { actionResultError, ErrorTypeEnum } from 'quidproquo-core';
3
- import { actionResult, FileActionType, FileGenerateTemporarySecureUrlErrorTypeEnum } from 'quidproquo-core';
3
+ import { actionResult, FileActionType, FileGenerateTemporarySecureUrlErrorTypeEnum, } from 'quidproquo-core';
4
4
  import { generatePresignedUrl } from '../../../logic/s3/generatePresignedUrl';
5
5
  import { resolveStorageDriveBucketName } from './utils';
6
6
  // SigV4 presigned URLs cannot expire more than 7 days in the future
@@ -1,6 +1,6 @@
1
1
  import { qpqConfigAwsUtils } from 'quidproquo-config-aws';
2
2
  import { actionResultError, ErrorTypeEnum } from 'quidproquo-core';
3
- import { actionResult, FileActionType, FileGenerateTemporaryUploadSecureUrlErrorTypeEnum } from 'quidproquo-core';
3
+ import { actionResult, FileActionType, FileGenerateTemporaryUploadSecureUrlErrorTypeEnum, } from 'quidproquo-core';
4
4
  import { generatePresignedUploadUrl } from '../../../logic/s3/generatePresignedUploadUrl';
5
5
  import { resolveStorageDriveBucketName } from './utils';
6
6
  // SigV4 presigned URLs cannot expire more than 7 days in the future
@@ -34,9 +34,7 @@ const getProcessFileStreamOpen = (qpqConfig) => {
34
34
  const streamId = `s3-${randomGuid()}`;
35
35
  const isText = encoding === 'text';
36
36
  const size = chunkSize ?? 65536;
37
- const transform = isText
38
- ? (buf) => buf.toString('utf8')
39
- : (buf) => buf.toString('base64');
37
+ const transform = isText ? (buf) => buf.toString('utf8') : (buf) => buf.toString('base64');
40
38
  const iterator = chunkedReadableIterator(response.Body, size, transform);
41
39
  streamRegistry.register(streamId, iterator);
42
40
  return actionResult({ id: streamId, encoding });
@@ -6,6 +6,7 @@ export * from './eventBus';
6
6
  export * from './file';
7
7
  export * from './graphDatabase';
8
8
  export * from './keyValueStore';
9
+ export * from './metric';
9
10
  export * from './queue';
10
11
  export * from './system';
11
12
  export * from './userDirectory';
@@ -6,6 +6,7 @@ export * from './eventBus';
6
6
  export * from './file';
7
7
  export * from './graphDatabase';
8
8
  export * from './keyValueStore';
9
+ export * from './metric';
9
10
  export * from './queue';
10
11
  export * from './system';
11
12
  export * from './userDirectory';
@@ -18,6 +19,7 @@ import { getEventBusActionProcessor } from './eventBus';
18
19
  import { getFileActionProcessor } from './file';
19
20
  import { getGraphDatabaseActionProcessor } from './graphDatabase';
20
21
  import { getKeyValueStoreActionProcessor } from './keyValueStore';
22
+ import { getMetricActionProcessor } from './metric';
21
23
  import { getQueueActionProcessor } from './queue';
22
24
  import { getSystemActionProcessor } from './system';
23
25
  import { getUserDirectoryActionProcessor } from './userDirectory';
@@ -26,6 +28,7 @@ export const getCoreActionProcessor = async (qpqConfig, dynamicModuleLoader) =>
26
28
  ...(await getApiKeyValidationActionProcessor(qpqConfig, dynamicModuleLoader)),
27
29
  ...(await getFileActionProcessor(qpqConfig, dynamicModuleLoader)),
28
30
  ...(await getKeyValueStoreActionProcessor(qpqConfig, dynamicModuleLoader)),
31
+ ...(await getMetricActionProcessor(qpqConfig, dynamicModuleLoader)),
29
32
  ...(await getQueueActionProcessor(qpqConfig, dynamicModuleLoader)),
30
33
  ...(await getUserDirectoryActionProcessor(qpqConfig, dynamicModuleLoader)),
31
34
  ...(await getEventBusActionProcessor(qpqConfig, dynamicModuleLoader)),
@@ -1,6 +1,6 @@
1
1
  import { qpqConfigAwsUtils } from 'quidproquo-config-aws';
2
2
  import { qpqCoreUtils } from 'quidproquo-core';
3
- import { actionResult, actionResultError, actionResultErrorFromCaughtError, KeyValueStoreActionType, KeyValueStoreDeleteErrorTypeEnum } from 'quidproquo-core';
3
+ import { actionResult, actionResultError, actionResultErrorFromCaughtError, KeyValueStoreActionType, KeyValueStoreDeleteErrorTypeEnum, } from 'quidproquo-core';
4
4
  import { getKvsDynamoTableNameFromConfig } from '../../../awsNamingUtils';
5
5
  import { deleteItem } from '../../../logic/dynamo';
6
6
  const getProcessKeyValueStoreDelete = (qpqConfig) => {
@@ -1,5 +1,5 @@
1
1
  import { qpqConfigAwsUtils } from 'quidproquo-config-aws';
2
- import { actionResult, actionResultError, actionResultErrorFromCaughtError, KeyValueStoreActionType, KeyValueStoreGetErrorTypeEnum } from 'quidproquo-core';
2
+ import { actionResult, actionResultError, actionResultErrorFromCaughtError, KeyValueStoreActionType, KeyValueStoreGetErrorTypeEnum, } from 'quidproquo-core';
3
3
  import { getKvsDynamoTableNameFromConfig } from '../../../awsNamingUtils';
4
4
  import { getItem } from '../../../logic/dynamo';
5
5
  const getProcessKeyValueStoreGet = (qpqConfig) => {
@@ -1,5 +1,5 @@
1
1
  import { qpqConfigAwsUtils } from 'quidproquo-config-aws';
2
- import { actionResult, actionResultError, actionResultErrorFromCaughtError, KeyValueStoreActionType, KeyValueStoreGetAllErrorTypeEnum } from 'quidproquo-core';
2
+ import { actionResult, actionResultError, actionResultErrorFromCaughtError, KeyValueStoreActionType, KeyValueStoreGetAllErrorTypeEnum, } from 'quidproquo-core';
3
3
  import { getKvsDynamoTableNameFromConfig } from '../../../awsNamingUtils';
4
4
  import { getAllItems } from '../../../logic/dynamo';
5
5
  const getProcessKeyValueStoreGetAll = (qpqConfig) => {
@@ -1,5 +1,5 @@
1
1
  import { qpqConfigAwsUtils } from 'quidproquo-config-aws';
2
- import { actionResultError, actionResultErrorFromCaughtError, ErrorTypeEnum, qpqCoreUtils } from 'quidproquo-core';
2
+ import { actionResultError, actionResultErrorFromCaughtError, ErrorTypeEnum, qpqCoreUtils, } from 'quidproquo-core';
3
3
  import { actionResult, KeyValueStoreActionType, KeyValueStoreQueryErrorTypeEnum } from 'quidproquo-core';
4
4
  import { getKvsDynamoTableNameFromConfig } from '../../../awsNamingUtils';
5
5
  import { query } from '../../../logic/dynamo';
@@ -1,5 +1,5 @@
1
1
  import { qpqConfigAwsUtils } from 'quidproquo-config-aws';
2
- import { actionResultError, actionResultErrorFromCaughtError } from 'quidproquo-core';
2
+ import { actionResultError, actionResultErrorFromCaughtError, } from 'quidproquo-core';
3
3
  import { actionResult, KeyValueStoreActionType, KeyValueStoreScanErrorTypeEnum } from 'quidproquo-core';
4
4
  import { getKvsDynamoTableNameFromConfig } from '../../../awsNamingUtils';
5
5
  import { scan } from '../../../logic/dynamo/scan';
@@ -1,6 +1,6 @@
1
1
  import { qpqConfigAwsUtils } from 'quidproquo-config-aws';
2
2
  import { qpqCoreUtils } from 'quidproquo-core';
3
- import { actionResult, actionResultError, actionResultErrorFromCaughtError, KeyValueStoreActionType, KeyValueStoreUpdateErrorTypeEnum } from 'quidproquo-core';
3
+ import { actionResult, actionResultError, actionResultErrorFromCaughtError, KeyValueStoreActionType, KeyValueStoreUpdateErrorTypeEnum, } from 'quidproquo-core';
4
4
  import { getKvsDynamoTableNameFromConfig } from '../../../awsNamingUtils';
5
5
  import { updateItem } from '../../../logic/dynamo';
6
6
  const getProcessKeyValueStoreUpdate = (qpqConfig) => {
@@ -20,6 +20,7 @@ const getProcessKeyValueStoreUpsert = (qpqConfig) => {
20
20
  try {
21
21
  await putItem(dynamoTableName, item, keys, {
22
22
  expires: options?.ttlInSeconds,
23
+ ifNotExistsAttribute: options?.ifNotExists ? storeConfig.partitionKey.key : undefined,
23
24
  }, region);
24
25
  return actionResult(void 0);
25
26
  }
@@ -27,6 +28,7 @@ const getProcessKeyValueStoreUpsert = (qpqConfig) => {
27
28
  return actionResultErrorFromCaughtError(error, {
28
29
  InternalServerError: () => actionResultError(KeyValueStoreUpsertErrorTypeEnum.ServiceUnavailable, 'KVS Service Unavailable'),
29
30
  ResourceNotFoundException: () => actionResultError(KeyValueStoreUpsertErrorTypeEnum.ResourceNotFound, 'KVS Resource Not Found'),
31
+ ConditionalCheckFailedException: () => actionResultError(KeyValueStoreUpsertErrorTypeEnum.Conflict, 'KVS item already exists'),
30
32
  });
31
33
  }
32
34
  };
@@ -0,0 +1,2 @@
1
+ import { ActionProcessorListResolver } from 'quidproquo-core';
2
+ export declare const getMetricPutActionProcessor: ActionProcessorListResolver;