Merge branch 'main' into dependabot/npm_and_yarn/types/node-24.13.0
This commit is contained in:
commit
a8eb3cdd39
4 changed files with 119 additions and 11 deletions
|
|
@ -66,13 +66,17 @@ EventBridge (daily 02:00 UTC)
|
|||
| Conversation Log | DynamoDB `seahaven-conversations` (90-day TTL) |
|
||||
| Unanswered Questions | DynamoDB `seahaven-unanswered-questions` (180-day TTL) |
|
||||
| Payment Data | DynamoDB `PaymentsDashboard` (via payments-dashboard) |
|
||||
| PO Data Source | DynamoDB `purchase-orders` (via po-ingest) |
|
||||
| PO Data Source | DynamoDB `purchase-orders` (read-only; owned by procurement-ingest / po-ingest) |
|
||||
| Work Order Data Source | DynamoDB `WorkOrders` + `WorkOrderComments` (via workorder-ingest) |
|
||||
| Site Assignments | DynamoDB `verified-sites` (auto-populated via po-ingest Streams pipeline) |
|
||||
| VPC | `seahaven-vpc` (`vpc-0d3d4b67bd0cf8a68`) — QBO Lambdas + Socket Mode |
|
||||
| Static Outbound IP | `52.202.83.13` (NAT Gateway for Intuit IP allowlist) |
|
||||
| QBO OAuth URLs | `bot.seahaven.com/qbo/connect`, `/qbo/callback`, `/qbo/disconnect`, `/qbo/launch` |
|
||||
|
||||
### Shared Resources
|
||||
|
||||
This bot reads the `purchase-orders` DynamoDB table **read-only** (via the `po-sync` and `wo-po-lookup` Lambdas, both granted `grantReadData`). The table is owned by the `procurement-ingest` repo (`po-ingest` stack), which is the sole authoritative writer. Any change to the `purchase-orders` schema must be coordinated with `procurement-ingest` (owner) and `payments-dashboard` (the other read-only consumer).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 22+
|
||||
|
|
|
|||
|
|
@ -44,6 +44,17 @@ function getPageTitle(page: Record<string, any>): string {
|
|||
return page.id as string;
|
||||
}
|
||||
|
||||
// S3 user-metadata values travel as HTTP headers, which must be US-ASCII with no
|
||||
// control characters. Notion titles routinely contain em dashes and other
|
||||
// non-ASCII characters (ERR_INVALID_CHAR otherwise). Strip to safe printable
|
||||
// ASCII and cap at the 256-char metadata limit.
|
||||
function sanitizeMetadataValue(value: string): string {
|
||||
return value
|
||||
.replace(/[^\x20-\x7E]/g, '') // drop non-printable / non-ASCII
|
||||
.trim()
|
||||
.slice(0, 256);
|
||||
}
|
||||
|
||||
async function clearOldFiles(bucket: string): Promise<void> {
|
||||
let continuationToken: string | undefined;
|
||||
const toDelete: { Key: string }[] = [];
|
||||
|
|
@ -128,7 +139,7 @@ export const handler = async (): Promise<void> => {
|
|||
ContentType: 'text/markdown',
|
||||
Metadata: {
|
||||
'notion-page-id': pageId,
|
||||
'notion-title': title.slice(0, 256), // S3 metadata value limit
|
||||
'notion-title': sanitizeMetadataValue(title),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
@ -144,13 +155,27 @@ export const handler = async (): Promise<void> => {
|
|||
console.log(`Upload complete. ${synced} synced, ${failed} failed.`);
|
||||
|
||||
console.log('Triggering Bedrock KB ingestion job...');
|
||||
const ingestionRes = await bedrockAgent.send(
|
||||
new StartIngestionJobCommand({
|
||||
knowledgeBaseId: kbId,
|
||||
dataSourceId: dsId,
|
||||
}),
|
||||
);
|
||||
console.log(
|
||||
`Ingestion job started: ${ingestionRes.ingestionJob?.ingestionJobId}`,
|
||||
);
|
||||
try {
|
||||
const ingestionRes = await bedrockAgent.send(
|
||||
new StartIngestionJobCommand({
|
||||
knowledgeBaseId: kbId,
|
||||
dataSourceId: dsId,
|
||||
}),
|
||||
);
|
||||
console.log(
|
||||
`Ingestion job started: ${ingestionRes.ingestionJob?.ingestionJobId}`,
|
||||
);
|
||||
} catch (err) {
|
||||
// A ConflictException means an ingestion job is already running for this data
|
||||
// source (e.g. an overlapping run). The freshly uploaded files will be picked
|
||||
// up by that in-flight job, so this is benign — log and exit cleanly rather
|
||||
// than failing the whole invocation.
|
||||
if (err instanceof Error && err.name === 'ConflictException') {
|
||||
console.log(
|
||||
'Ingestion job already in progress; uploaded files will be picked up by the running job. Skipping.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import * as cdk from 'aws-cdk-lib';
|
||||
import { Construct } from 'constructs';
|
||||
import * as s3 from 'aws-cdk-lib/aws-s3';
|
||||
import * as oss from 'aws-cdk-lib/aws-opensearchserverless';
|
||||
import { bedrock } from '@cdklabs/generative-ai-cdk-constructs';
|
||||
|
||||
export interface KnowledgeBaseProps {
|
||||
|
|
@ -37,6 +38,57 @@ export class KnowledgeBaseConstruct extends Construct {
|
|||
instruction: 'Use this knowledge base to answer questions about Sea Haven Industries company policies, SOPs, SA8000 social accountability compliance requirements, approved vendor lists, the employee handbook, work order status and history, and purchase order details.',
|
||||
});
|
||||
|
||||
// Lock the auto-created AOSS network policy to private (INFRA-92).
|
||||
// @cdklabs/generative-ai-cdk-constructs hardcodes AllowFromPublic: true on the
|
||||
// VectorCollection's network policy and exposes no prop to change it, so we reach
|
||||
// the underlying CfnSecurityPolicy via the construct tree and override its Policy.
|
||||
// SourceServices: ['bedrock.amazonaws.com'] is REQUIRED — it is what keeps
|
||||
// Bedrock-managed retrieval working once public access is removed. A SourceVPCEs-only
|
||||
// policy returns 401 for Bedrock retrieval. Dashboard rule kept for console access
|
||||
// (AWS services cannot reach Dashboards regardless).
|
||||
const vectorCollection = this.knowledgeBase.vectorStore as Construct;
|
||||
const networkPolicy = vectorCollection.node.findChild('NetworkPolicy');
|
||||
if (!(networkPolicy instanceof oss.CfnSecurityPolicy)) {
|
||||
throw new Error(
|
||||
"Expected child 'NetworkPolicy' of the AOSS VectorCollection to be a CfnSecurityPolicy. " +
|
||||
'The @cdklabs/generative-ai-cdk-constructs internals may have changed — review knowledge-base.ts (INFRA-92).',
|
||||
);
|
||||
}
|
||||
const collectionName = (this.knowledgeBase.vectorStore as { collectionName?: string }).collectionName;
|
||||
if (!collectionName) {
|
||||
throw new Error(
|
||||
'Could not resolve the AOSS collection name from vectorStore — check the @cdklabs construct API (INFRA-92).',
|
||||
);
|
||||
}
|
||||
// Policy is typed as a JSON string on the L1 CfnSecurityPolicy, so it must be stringified.
|
||||
networkPolicy.addPropertyOverride(
|
||||
'Policy',
|
||||
JSON.stringify([
|
||||
{
|
||||
Rules: [
|
||||
{
|
||||
ResourceType: 'collection',
|
||||
Resource: [`collection/${collectionName}`],
|
||||
},
|
||||
],
|
||||
AllowFromPublic: false,
|
||||
SourceServices: ['bedrock.amazonaws.com'],
|
||||
},
|
||||
{
|
||||
Rules: [
|
||||
{
|
||||
ResourceType: 'dashboard',
|
||||
Resource: [`collection/${collectionName}`],
|
||||
},
|
||||
],
|
||||
// INFRA-92: dashboard endpoint intentionally left public for console access.
|
||||
// AWS services (incl. Bedrock) cannot reach Dashboards regardless, so this does
|
||||
// not affect the data plane. Remove this rule to fully lock down console access.
|
||||
AllowFromPublic: true,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
// S3 data source — chunking configured via ChunkingStrategy.fixedSize()
|
||||
this.dataSource = new bedrock.S3DataSource(this, 'S3DataSource', {
|
||||
bucket: this.docsBucket,
|
||||
|
|
|
|||
|
|
@ -138,6 +138,33 @@ export class SlackHandlerConstruct extends Construct {
|
|||
});
|
||||
}
|
||||
|
||||
// ── Access logging + throttling (audit M-18) ──────────────────────────────
|
||||
const defaultStage = this.api.defaultStage!.node.defaultChild as apigatewayv2.CfnStage;
|
||||
defaultStage.addPropertyOverride('DefaultRouteSettings', {
|
||||
ThrottlingBurstLimit: 50,
|
||||
ThrottlingRateLimit: 100,
|
||||
});
|
||||
|
||||
const apiAccessLogGroup = new logs.LogGroup(this, 'ApiAccessLogGroup', {
|
||||
logGroupName: '/aws/apigateway/seahaven-slack-webhook',
|
||||
retention: logs.RetentionDays.THREE_MONTHS,
|
||||
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
||||
});
|
||||
defaultStage.addPropertyOverride('AccessLogSettings', {
|
||||
DestinationArn: apiAccessLogGroup.logGroupArn,
|
||||
Format: JSON.stringify({
|
||||
requestId: '$context.requestId',
|
||||
ip: '$context.identity.sourceIp',
|
||||
requestTime: '$context.requestTime',
|
||||
method: '$context.httpMethod',
|
||||
routeKey: '$context.routeKey',
|
||||
status: '$context.status',
|
||||
protocol: '$context.protocol',
|
||||
responseLength: '$context.responseLength',
|
||||
integrationError: '$context.integrationErrorMessage',
|
||||
}),
|
||||
});
|
||||
|
||||
// ── Custom domain: bot.seahaven.com ───────────────────────────────────────
|
||||
const certificate = acm.Certificate.fromCertificateArn(
|
||||
this, 'WildcardCert', props.wildcardCertArn,
|
||||
|
|
|
|||
Reference in a new issue