mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(integrations): AWS SES, IAM Identity Center, and enhanced IAM/STS/CloudWatch/DynamoDB (#4245)
* feat(integrations): add AWS SES, IAM Identity Center, and enhanced IAM/STS/CloudWatch/DynamoDB integrations - Add AWS SES v2 integration with 9 operations (send email, templated, bulk, templates, account) - Add AWS IAM Identity Center integration with 12 operations (account assignments, permission sets, users, groups) - Add 3 new IAM tools: list-attached-role-policies, list-attached-user-policies, simulate-principal-policy - Fix DynamoDB duplicate subBlock IDs, add operation-scoped field names, add subblock migrations - Add authMode: AuthMode.ApiKey to DynamoDB block - Fix CloudWatch routes: toError, client.destroy(), withRouteHandler, auth outside try - Fix STS/DynamoDB/IAM routes: nullable Zod schemas, withRouteHandler adoption - Fix Identity Center: list_instances pagination, list_groups instanceArn condition - Add subblock migrations for renamed DynamoDB fields (key, filterExpression, etc.) - Apply withRouteHandler to all new and existing AWS tool routes * docs(ses): add manual intro section to SES docs * fix(dynamodb): add legacy fallbacks in params for subblock migration compatibility Workflows saved with the old shared IDs (key, filterExpression, etc.) that migrate to get-scoped slots via subblock-migrations still work correctly on update/delete/scan/put operations via fallback lookups in tools.config.params. * feat(contact): add contact page, migrate help/demo forms to useMutation (#4242) * feat(contact): add contact page, migrate help/demo forms to useMutation * improvement(contact): address greptile review feedback - Map contact topic to help email type for accurate confirmation emails - Drop Zod schema details from 400 response on public /api/contact - Wire aria-describedby + aria-invalid in LandingField for both forms - Reset helpMutation on modal reopen to match demo-request pattern * improvement(landing): extract shared LandingField component * fix(landing): resolve error-page crash on invalid /models and /integrations routes (#4243) * fix(layout): use plain inline script for PublicEnvScript to set env before chunks eval on error pages * fix(landing): handle runtime env race on error-page renders React skips SSR on unhandled server errors and re-renders on the client (see vercel/next.js#63980, #82456). Root-layout scripts — including the runtime env script that populates window.__ENV — are inserted but not executed on that client re-render, so any client module that reads env at module evaluation crashes the render into a blank "Application error" overlay instead of rendering the styled 404. This replaces the earlier PublicEnvScript tweak with the architectural fix: - auth-client.ts: fall back to window.location.origin when getBaseUrl() throws on the client. Auth endpoints are same-origin, so this is the correct baseURL on the client. Server-side we still throw on genuine misconfig. - loading.tsx under /models/[provider], /models/[provider]/[model], and /integrations/[slug]: establishes a Suspense boundary below the root layout so a page-level notFound() no longer invalidates the layout's SSR output (the fix endorsed by Next.js maintainers in #63980). - layout.tsx: revert disableNextScript — the research showed this doesn't actually fix error-page renders. The real fix is above. * improvement(landing): use emcn Loader in scoped loading.tsx, trim auth-client comment Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * fix(iam): correct MissingContextValues mapping in simulatePrincipalPolicy * fix(aws): add conditionExpression migration fallback for DynamoDB delete, fix SES pageSize min * fix(aws): deep validation fixes across SES, IAM, Identity Center, DynamoDB integrations - IAM: replace non-existent StatementId with SourcePolicyType in simulatePrincipalPolicy - IAM: add .int() constraint to list-users/roles/policies/groups Zod schemas - IAM: remove redundant manual requestId from all 21 IAM route handlers - SES: add .refine() body validation to create-template route - SES: make bulk email destination templateData optional, only include ReplacementEmailContent when present - SES: fix pageSize guard to if (pageSize != null) to correctly forward 0 - SES: add max(100) to list-templates pageSize, revert list-identities to min(0) per SDK - STS: fix logger.error calls to use structured metadata pattern - Identity Center: remove deprecated account.Status fallback, use account.State only - DynamoDB: convert empty interface extends to type aliases, remove redundant error field, fix barrel to absolute imports * regen docs * fix(iam): add .int() constraint to maxSessionDuration in create-role route * fix(ses): forward pageSize=0 correctly in listIdentities util * fix(aws): add gradient background to IdentityCenterIcon, fix listTemplates pageSize guard --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
aee6189d14
commit
2d94b3729d
@@ -4681,6 +4681,17 @@ export function IAMIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IdentityCenterIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M13.694,14.8194562 C14.376,14.1374562 14.376,13.0294562 13.694,12.3474562 C13.353,12.0074562 12.906,11.8374562 12.459,11.8374562 C12.01,11.8374562 11.563,12.0074562 11.222,12.3474562 C10.542,13.0284562 10.542,14.1384562 11.222,14.8194562 C11.905,15.5014562 13.013,15.4994562 13.694,14.8194562 M14.718,15.1374562 L18.703,19.1204562 L17.996,19.8274562 L16.868,18.6994562 L15.793,19.7754562 L15.086,19.0684562 L16.161,17.9924562 L14.011,15.8444562 C13.545,16.1654562 13.003,16.3294562 12.458,16.3294562 C11.755,16.3294562 11.051,16.0624562 10.515,15.5264562 C9.445,14.4554562 9.445,12.7124562 10.515,11.6404562 C11.586,10.5714562 13.329,10.5694562 14.401,11.6404562 C15.351,12.5904562 15.455,14.0674562 14.718,15.1374562 M20,12.1014562 C20,14.1684562 18.505,15.0934562 17.023,15.0934562 L17.023,14.0934562 C17.487,14.0934562 19,13.9494562 19,12.1014562 C19,11.0044562 18.353,10.3894562 16.905,10.1084562 C16.68,10.0654562 16.514,9.87545615 16.501,9.64845615 C16.446,8.74445615 15.987,8.11245615 15.384,8.11245615 C15.084,8.11245615 14.854,8.24245615 14.616,8.54645615 C14.506,8.68845615 14.324,8.75945615 14.147,8.73245615 C13.968,8.70545615 13.818,8.58445615 13.755,8.41445615 C13.577,7.94345615 13.211,7.43345615 12.723,6.97745615 C12.231,6.50945615 10.883,5.50745615 8.972,6.27345615 C7.885,6.70545615 7.034,7.94945615 7.034,9.10745615 C7.034,9.23545615 7.043,9.36245615 7.058,9.48845615 C7.061,9.50945615 7.062,9.53045615 7.062,9.55145615 C7.062,9.79945615 6.882,10.0064562 6.645,10.0464562 C5.886,10.2394562 5,10.7454562 5,12.0554562 L5.005,12.2104562 C5.069,13.3254562 6.252,13.9954562 7.358,13.9984562 L8,13.9984562 L8,14.9984562 L7.357,14.9984562 C5.536,14.9944562 4.095,13.8194562 4.006,12.2644562 C4.003,12.1944562 4,12.1244562 4,12.0554562 C4,10.6944562 4.752,9.64845615 6.035,9.18845615 C6.034,9.16145615 6.034,9.13445615 6.034,9.10745615 C6.034,7.54345615 7.138,5.92545615 8.602,5.34345615 C10.298,4.66545615 12.095,5.00345615 13.409,6.24945615 C13.706,6.52745615 14.076,6.92645615 14.372,7.41345615 C14.673,7.21245615 15.008,7.11245615 15.384,7.11245615 C16.257,7.11245615 17.231,7.77145615 17.458,9.20745615 C19.145,9.63245615 20,10.6054562 20,12.1014562'
|
||||
fill='#FFFFFF'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function STSIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'>
|
||||
@@ -4699,6 +4710,24 @@ export function STSIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function SESIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'>
|
||||
<defs>
|
||||
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id='sesGradient'>
|
||||
<stop stopColor='#BD0816' offset='0%' />
|
||||
<stop stopColor='#FF5252' offset='100%' />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect fill='url(#sesGradient)' width='80' height='80' />
|
||||
<path
|
||||
d='M57,60.999875 C57,59.373846 55.626,57.9998214 54,57.9998214 C52.374,57.9998214 51,59.373846 51,60.999875 C51,62.625904 52.374,63.9999286 54,63.9999286 C55.626,63.9999286 57,62.625904 57,60.999875 L57,60.999875 Z M40,59.9998571 C38.374,59.9998571 37,61.3738817 37,62.9999107 C37,64.6259397 38.374,65.9999643 40,65.9999643 C41.626,65.9999643 43,64.6259397 43,62.9999107 C43,61.3738817 41.626,59.9998571 40,59.9998571 L40,59.9998571 Z M26,57.9998214 C24.374,57.9998214 23,59.373846 23,60.999875 C23,62.625904 24.374,63.9999286 26,63.9999286 C27.626,63.9999286 29,62.625904 29,60.999875 C29,59.373846 27.626,57.9998214 26,57.9998214 L26,57.9998214 Z M28.605,42.9995536 L51.395,42.9995536 L43.739,36.1104305 L40.649,38.7584778 C40.463,38.9194807 40.23,38.9994821 39.999,38.9994821 C39.768,38.9994821 39.535,38.9194807 39.349,38.7584778 L36.26,36.1104305 L28.605,42.9995536 Z M27,28.1732888 L27,41.7545313 L34.729,34.7984071 L27,28.1732888 Z M51.297,26.9992678 L28.703,26.9992678 L39.999,36.6824408 L51.297,26.9992678 Z M53,41.7545313 L53,28.1732888 L45.271,34.7974071 L53,41.7545313 Z M59,60.999875 C59,63.7099234 56.71,65.9999643 54,65.9999643 C51.29,65.9999643 49,63.7099234 49,60.999875 C49,58.6308327 50.75,56.5837961 53,56.1057876 L53,52.9997321 L41,52.9997321 L41,58.1058233 C43.25,58.5838319 45,60.6308684 45,62.9999107 C45,65.7099591 42.71,68 40,68 C37.29,68 35,65.7099591 35,62.9999107 C35,60.6308684 36.75,58.5838319 39,58.1058233 L39,52.9997321 L27,52.9997321 L27,56.1057876 C29.25,56.5837961 31,58.6308327 31,60.999875 C31,63.7099234 28.71,65.9999643 26,65.9999643 C23.29,65.9999643 21,63.7099234 21,60.999875 C21,58.6308327 22.75,56.5837961 25,56.1057876 L25,51.9997143 C25,51.4477044 25.447,50.9996964 26,50.9996964 L39,50.9996964 L39,44.9995893 L26,44.9995893 C25.447,44.9995893 25,44.5515813 25,43.9995714 L25,25.99925 C25,25.4472401 25.447,24.9992321 26,24.9992321 L54,24.9992321 C54.553,24.9992321 55,25.4472401 55,25.99925 L55,43.9995714 C55,44.5515813 54.553,44.9995893 54,44.9995893 L41,44.9995893 L41,50.9996964 L54,50.9996964 C54.553,50.9996964 55,51.4477044 55,51.9997143 L55,56.1057876 C57.25,56.5837961 59,58.6308327 59,60.999875 L59,60.999875 Z M68,39.9995 C68,45.9066055 66.177,51.5597064 62.727,56.3447919 L61.104,55.174771 C64.307,50.7316916 66,45.4845979 66,39.9995 C66,25.664244 54.337,14.0000357 40.001,14.0000357 C25.664,14.0000357 14,25.664244 14,39.9995 C14,45.4845979 15.693,50.7316916 18.896,55.174771 L17.273,56.3447919 C13.823,51.5597064 12,45.9066055 12,39.9995 C12,24.5612243 24.561,12 39.999,12 C55.438,12 68,24.5612243 68,39.9995 L68,39.9995 Z'
|
||||
fill='#FFFFFF'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function SecretsManagerIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'>
|
||||
|
||||
@@ -91,6 +91,7 @@ import {
|
||||
HuggingFaceIcon,
|
||||
HunterIOIcon,
|
||||
IAMIcon,
|
||||
IdentityCenterIcon,
|
||||
ImageIcon,
|
||||
IncidentioIcon,
|
||||
InfisicalIcon,
|
||||
@@ -152,6 +153,7 @@ import {
|
||||
RootlyIcon,
|
||||
S3Icon,
|
||||
SalesforceIcon,
|
||||
SESIcon,
|
||||
SearchIcon,
|
||||
SecretsManagerIcon,
|
||||
SendgridIcon,
|
||||
@@ -294,6 +296,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
huggingface: HuggingFaceIcon,
|
||||
hunter: HunterIOIcon,
|
||||
iam: IAMIcon,
|
||||
identity_center: IdentityCenterIcon,
|
||||
image_generator: ImageIcon,
|
||||
imap: MailServerIcon,
|
||||
incidentio: IncidentioIcon,
|
||||
@@ -370,6 +373,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
sentry: SentryIcon,
|
||||
serper: SerperIcon,
|
||||
servicenow: ServiceNowIcon,
|
||||
ses: SESIcon,
|
||||
sftp: SftpIcon,
|
||||
sharepoint: MicrosoftSharepointIcon,
|
||||
shopify: ShopifyIcon,
|
||||
|
||||
@@ -57,9 +57,12 @@ Run a CloudWatch Log Insights query against one or more log groups
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | array | Query result rows |
|
||||
| `statistics` | object | Query statistics \(bytesScanned, recordsMatched, recordsScanned\) |
|
||||
| `status` | string | Query completion status |
|
||||
| `results` | array | Query result rows \(each row is a key/value map of field name to value\) |
|
||||
| `statistics` | object | Query statistics |
|
||||
| ↳ `bytesScanned` | number | Total bytes of log data scanned |
|
||||
| ↳ `recordsMatched` | number | Number of log records that matched the query |
|
||||
| ↳ `recordsScanned` | number | Total log records scanned |
|
||||
| `status` | string | Query completion status \(Complete, Failed, Cancelled, or Timeout\) |
|
||||
|
||||
### `cloudwatch_describe_log_groups`
|
||||
|
||||
@@ -80,6 +83,11 @@ List available CloudWatch log groups
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `logGroups` | array | List of CloudWatch log groups with metadata |
|
||||
| ↳ `logGroupName` | string | Log group name |
|
||||
| ↳ `arn` | string | Log group ARN |
|
||||
| ↳ `storedBytes` | number | Total stored bytes |
|
||||
| ↳ `retentionInDays` | number | Retention period in days \(if set\) |
|
||||
| ↳ `creationTime` | number | Creation time in epoch milliseconds |
|
||||
|
||||
### `cloudwatch_get_log_events`
|
||||
|
||||
@@ -103,6 +111,9 @@ Retrieve log events from a specific CloudWatch log stream
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `events` | array | Log events with timestamp, message, and ingestion time |
|
||||
| ↳ `timestamp` | number | Event timestamp in epoch milliseconds |
|
||||
| ↳ `message` | string | Log event message |
|
||||
| ↳ `ingestionTime` | number | Ingestion time in epoch milliseconds |
|
||||
|
||||
### `cloudwatch_describe_log_streams`
|
||||
|
||||
@@ -123,7 +134,12 @@ List log streams within a CloudWatch log group
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `logStreams` | array | List of log streams with metadata |
|
||||
| `logStreams` | array | List of log streams with metadata, sorted by last event time \(most recent first\) unless a prefix filter is applied |
|
||||
| ↳ `logStreamName` | string | Log stream name |
|
||||
| ↳ `lastEventTimestamp` | number | Timestamp of the last log event in epoch milliseconds |
|
||||
| ↳ `firstEventTimestamp` | number | Timestamp of the first log event in epoch milliseconds |
|
||||
| ↳ `creationTime` | number | Stream creation time in epoch milliseconds |
|
||||
| ↳ `storedBytes` | number | Total stored bytes |
|
||||
|
||||
### `cloudwatch_list_metrics`
|
||||
|
||||
@@ -146,6 +162,9 @@ List available CloudWatch metrics
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `metrics` | array | List of metrics with namespace, name, and dimensions |
|
||||
| ↳ `namespace` | string | Metric namespace \(e.g., AWS/EC2\) |
|
||||
| ↳ `metricName` | string | Metric name \(e.g., CPUUtilization\) |
|
||||
| ↳ `dimensions` | array | Array of name/value dimension pairs |
|
||||
|
||||
### `cloudwatch_get_metric_statistics`
|
||||
|
||||
@@ -170,8 +189,15 @@ Get statistics for a CloudWatch metric over a time range
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `label` | string | Metric label |
|
||||
| `datapoints` | array | Datapoints with timestamp and statistics values |
|
||||
| `label` | string | Metric label returned by CloudWatch |
|
||||
| `datapoints` | array | Datapoints sorted by timestamp with statistics values |
|
||||
| ↳ `timestamp` | number | Datapoint timestamp in epoch milliseconds |
|
||||
| ↳ `average` | number | Average statistic value |
|
||||
| ↳ `sum` | number | Sum statistic value |
|
||||
| ↳ `minimum` | number | Minimum statistic value |
|
||||
| ↳ `maximum` | number | Maximum statistic value |
|
||||
| ↳ `sampleCount` | number | Sample count statistic value |
|
||||
| ↳ `unit` | string | Unit of the metric |
|
||||
|
||||
### `cloudwatch_put_metric_data`
|
||||
|
||||
@@ -222,5 +248,13 @@ List and filter CloudWatch alarms
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `alarms` | array | List of CloudWatch alarms with state and configuration |
|
||||
| ↳ `alarmName` | string | Alarm name |
|
||||
| ↳ `alarmArn` | string | Alarm ARN |
|
||||
| ↳ `stateValue` | string | Current state \(OK, ALARM, INSUFFICIENT_DATA\) |
|
||||
| ↳ `stateReason` | string | Human-readable reason for the state |
|
||||
| ↳ `metricName` | string | Metric name \(MetricAlarm only\) |
|
||||
| ↳ `namespace` | string | Metric namespace \(MetricAlarm only\) |
|
||||
| ↳ `threshold` | number | Threshold value \(MetricAlarm only\) |
|
||||
| ↳ `stateUpdatedTimestamp` | number | Epoch ms when state last changed |
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Amazon DynamoDB
|
||||
description: Connect to Amazon DynamoDB
|
||||
description: Get, put, query, scan, update, and delete items in Amazon DynamoDB tables
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
@@ -55,7 +55,7 @@ Get an item from a DynamoDB table by primary key
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `tableName` | string | Yes | DynamoDB table name \(e.g., "Users", "Orders"\) |
|
||||
| `key` | object | Yes | Primary key of the item to retrieve \(e.g., \{"pk": "USER#123"\} or \{"pk": "ORDER#456", "sk": "ITEM#789"\}\) |
|
||||
| `key` | json | Yes | Primary key of the item to retrieve \(e.g., \{"pk": "USER#123"\} or \{"pk": "ORDER#456", "sk": "ITEM#789"\}\) |
|
||||
| `consistentRead` | boolean | No | Use strongly consistent read |
|
||||
|
||||
#### Output
|
||||
@@ -63,7 +63,7 @@ Get an item from a DynamoDB table by primary key
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `item` | object | Retrieved item |
|
||||
| `item` | json | Retrieved item |
|
||||
|
||||
### `dynamodb_put`
|
||||
|
||||
@@ -77,14 +77,17 @@ Put an item into a DynamoDB table
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `tableName` | string | Yes | DynamoDB table name \(e.g., "Users", "Orders"\) |
|
||||
| `item` | object | Yes | Item to put into the table \(e.g., \{"pk": "USER#123", "name": "John", "email": "john@example.com"\}\) |
|
||||
| `item` | json | Yes | Item to put into the table \(e.g., \{"pk": "USER#123", "name": "John", "email": "john@example.com"\}\) |
|
||||
| `conditionExpression` | string | No | Condition that must be met for the put to succeed \(e.g., "attribute_not_exists\(pk\)" to prevent overwrites\) |
|
||||
| `expressionAttributeNames` | json | No | Attribute name mappings for reserved words used in conditionExpression \(e.g., \{"#name": "name"\}\) |
|
||||
| `expressionAttributeValues` | json | No | Expression attribute values used in conditionExpression \(e.g., \{":expected": "value"\}\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `item` | object | Created item |
|
||||
| `item` | json | Created item |
|
||||
|
||||
### `dynamodb_query`
|
||||
|
||||
@@ -100,10 +103,12 @@ Query items from a DynamoDB table using key conditions
|
||||
| `tableName` | string | Yes | DynamoDB table name \(e.g., "Users", "Orders"\) |
|
||||
| `keyConditionExpression` | string | Yes | Key condition expression \(e.g., "pk = :pk" or "pk = :pk AND sk BEGINS_WITH :prefix"\) |
|
||||
| `filterExpression` | string | No | Filter expression for results \(e.g., "age > :minAge AND #status = :status"\) |
|
||||
| `expressionAttributeNames` | object | No | Attribute name mappings for reserved words \(e.g., \{"#status": "status"\}\) |
|
||||
| `expressionAttributeValues` | object | No | Expression attribute values \(e.g., \{":pk": "USER#123", ":minAge": 18\}\) |
|
||||
| `expressionAttributeNames` | json | No | Attribute name mappings for reserved words \(e.g., \{"#status": "status"\}\) |
|
||||
| `expressionAttributeValues` | json | No | Expression attribute values \(e.g., \{":pk": "USER#123", ":minAge": 18\}\) |
|
||||
| `indexName` | string | No | Secondary index name to query \(e.g., "GSI1", "email-index"\) |
|
||||
| `limit` | number | No | Maximum number of items to return \(e.g., 10, 50, 100\) |
|
||||
| `exclusiveStartKey` | json | No | Pagination token from a previous query's lastEvaluatedKey to continue fetching results |
|
||||
| `scanIndexForward` | boolean | No | Sort order for the sort key: true for ascending \(default\), false for descending |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -112,6 +117,7 @@ Query items from a DynamoDB table using key conditions
|
||||
| `message` | string | Operation status message |
|
||||
| `items` | array | Array of items returned |
|
||||
| `count` | number | Number of items returned |
|
||||
| `lastEvaluatedKey` | json | Pagination token to pass as exclusiveStartKey to fetch the next page of results |
|
||||
|
||||
### `dynamodb_scan`
|
||||
|
||||
@@ -127,9 +133,10 @@ Scan all items in a DynamoDB table
|
||||
| `tableName` | string | Yes | DynamoDB table name \(e.g., "Users", "Orders"\) |
|
||||
| `filterExpression` | string | No | Filter expression for results \(e.g., "age > :minAge AND #status = :status"\) |
|
||||
| `projectionExpression` | string | No | Attributes to retrieve \(e.g., "pk, sk, #name, email"\) |
|
||||
| `expressionAttributeNames` | object | No | Attribute name mappings for reserved words \(e.g., \{"#name": "name", "#status": "status"\}\) |
|
||||
| `expressionAttributeValues` | object | No | Expression attribute values \(e.g., \{":minAge": 18, ":status": "active"\}\) |
|
||||
| `expressionAttributeNames` | json | No | Attribute name mappings for reserved words \(e.g., \{"#name": "name", "#status": "status"\}\) |
|
||||
| `expressionAttributeValues` | json | No | Expression attribute values \(e.g., \{":minAge": 18, ":status": "active"\}\) |
|
||||
| `limit` | number | No | Maximum number of items to return \(e.g., 10, 50, 100\) |
|
||||
| `exclusiveStartKey` | json | No | Pagination token from a previous scan's lastEvaluatedKey to continue fetching results |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -138,6 +145,7 @@ Scan all items in a DynamoDB table
|
||||
| `message` | string | Operation status message |
|
||||
| `items` | array | Array of items returned |
|
||||
| `count` | number | Number of items returned |
|
||||
| `lastEvaluatedKey` | json | Pagination token to pass as exclusiveStartKey to fetch the next page of results |
|
||||
|
||||
### `dynamodb_update`
|
||||
|
||||
@@ -151,10 +159,10 @@ Update an item in a DynamoDB table
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `tableName` | string | Yes | DynamoDB table name \(e.g., "Users", "Orders"\) |
|
||||
| `key` | object | Yes | Primary key of the item to update \(e.g., \{"pk": "USER#123"\} or \{"pk": "ORDER#456", "sk": "ITEM#789"\}\) |
|
||||
| `key` | json | Yes | Primary key of the item to update \(e.g., \{"pk": "USER#123"\} or \{"pk": "ORDER#456", "sk": "ITEM#789"\}\) |
|
||||
| `updateExpression` | string | Yes | Update expression \(e.g., "SET #name = :name, age = :age" or "SET #count = #count + :inc"\) |
|
||||
| `expressionAttributeNames` | object | No | Attribute name mappings for reserved words \(e.g., \{"#name": "name", "#count": "count"\}\) |
|
||||
| `expressionAttributeValues` | object | No | Expression attribute values \(e.g., \{":name": "John", ":age": 30, ":inc": 1\}\) |
|
||||
| `expressionAttributeNames` | json | No | Attribute name mappings for reserved words \(e.g., \{"#name": "name", "#count": "count"\}\) |
|
||||
| `expressionAttributeValues` | json | No | Expression attribute values \(e.g., \{":name": "John", ":age": 30, ":inc": 1\}\) |
|
||||
| `conditionExpression` | string | No | Condition that must be met for the update to succeed \(e.g., "attribute_exists\(pk\)" or "version = :expectedVersion"\) |
|
||||
|
||||
#### Output
|
||||
@@ -162,7 +170,7 @@ Update an item in a DynamoDB table
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `item` | object | Updated item |
|
||||
| `item` | json | Updated item with all attributes |
|
||||
|
||||
### `dynamodb_delete`
|
||||
|
||||
@@ -176,8 +184,10 @@ Delete an item from a DynamoDB table
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `tableName` | string | Yes | DynamoDB table name \(e.g., "Users", "Orders"\) |
|
||||
| `key` | object | Yes | Primary key of the item to delete \(e.g., \{"pk": "USER#123"\} or \{"pk": "ORDER#456", "sk": "ITEM#789"\}\) |
|
||||
| `key` | json | Yes | Primary key of the item to delete \(e.g., \{"pk": "USER#123"\} or \{"pk": "ORDER#456", "sk": "ITEM#789"\}\) |
|
||||
| `conditionExpression` | string | No | Condition that must be met for the delete to succeed \(e.g., "attribute_exists\(pk\)"\) |
|
||||
| `expressionAttributeNames` | json | No | Attribute name mappings for reserved words used in conditionExpression \(e.g., \{"#status": "status"\}\) |
|
||||
| `expressionAttributeValues` | json | No | Expression attribute values used in conditionExpression \(e.g., \{":status": "active"\}\) |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -204,6 +214,6 @@ Introspect DynamoDB to list tables or get detailed schema information for a spec
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
| `tables` | array | List of table names in the region |
|
||||
| `tableDetails` | object | Detailed schema information for a specific table |
|
||||
| `tableDetails` | json | Detailed schema information for a specific table |
|
||||
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ Get detailed information about an IAM user
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `userName` | string | Yes | The name of the IAM user to retrieve |
|
||||
| `userName` | string | No | The name of the IAM user to retrieve \(defaults to the caller if omitted\) |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -440,4 +440,80 @@ Remove an IAM user from a group
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Operation status message |
|
||||
|
||||
### `iam_list_attached_role_policies`
|
||||
|
||||
List all managed policies attached to an IAM role
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `roleName` | string | Yes | Name of the IAM role |
|
||||
| `pathPrefix` | string | No | Path prefix to filter policies \(e.g., /application/\) |
|
||||
| `maxItems` | number | No | Maximum number of policies to return \(1-1000\) |
|
||||
| `marker` | string | No | Pagination marker from a previous request |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `attachedPolicies` | json | List of attached policies with policyName and policyArn |
|
||||
| `isTruncated` | boolean | Whether there are more results available |
|
||||
| `marker` | string | Pagination marker for the next page of results |
|
||||
| `count` | number | Number of attached policies returned |
|
||||
|
||||
### `iam_list_attached_user_policies`
|
||||
|
||||
List all managed policies attached to an IAM user
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `userName` | string | Yes | Name of the IAM user |
|
||||
| `pathPrefix` | string | No | Path prefix to filter policies \(e.g., /application/\) |
|
||||
| `maxItems` | number | No | Maximum number of policies to return \(1-1000\) |
|
||||
| `marker` | string | No | Pagination marker from a previous request |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `attachedPolicies` | json | List of attached policies with policyName and policyArn |
|
||||
| `isTruncated` | boolean | Whether there are more results available |
|
||||
| `marker` | string | Pagination marker for the next page of results |
|
||||
| `count` | number | Number of attached policies returned |
|
||||
|
||||
### `iam_simulate_principal_policy`
|
||||
|
||||
Simulate whether a user, role, or group is allowed to perform specific AWS actions — useful for pre-flight access checks
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `policySourceArn` | string | Yes | ARN of the user, group, or role to simulate \(e.g., arn:aws:iam::123456789012:user/alice\) |
|
||||
| `actionNames` | string | Yes | Comma-separated list of AWS actions to simulate \(e.g., s3:GetObject,ec2:DescribeInstances\) |
|
||||
| `resourceArns` | string | No | Comma-separated list of resource ARNs to simulate against \(defaults to * if not provided\) |
|
||||
| `maxResults` | number | No | Maximum number of simulation results to return \(1-1000\) |
|
||||
| `marker` | string | No | Pagination marker from a previous request |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `evaluationResults` | json | Simulation results per action: evalActionName, evalResourceName, evalDecision \(allowed/explicitDeny/implicitDeny\), matchedStatements \(sourcePolicyId, sourcePolicyType\), missingContextValues |
|
||||
| `isTruncated` | boolean | Whether there are more results available |
|
||||
| `marker` | string | Pagination marker for the next page of results |
|
||||
| `count` | number | Number of evaluation results returned |
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
---
|
||||
title: AWS Identity Center
|
||||
description: Manage temporary elevated access in AWS IAM Identity Center
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="identity_center"
|
||||
color="linear-gradient(45deg, #BD0816 0%, #FF5252 100%)"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[AWS IAM Identity Center](https://aws.amazon.com/iam/identity-center/) (formerly AWS Single Sign-On) is the recommended service for managing workforce access to multiple AWS accounts and applications. It provides a central place to assign users and groups temporary, permission-scoped access to AWS accounts using permission sets — without creating long-lived IAM credentials.
|
||||
|
||||
With AWS IAM Identity Center, you can:
|
||||
|
||||
- **Provision account assignments**: Grant a user or group access to a specific AWS account with a specific permission set — the core primitive of temporary elevated access
|
||||
- **Revoke access on demand**: Delete account assignments to immediately remove elevated permissions when they are no longer needed
|
||||
- **Look up users by email**: Resolve a federated identity (email address) to an Identity Store user ID for programmatic access provisioning
|
||||
- **List permission sets**: Enumerate the available permission sets (e.g., ReadOnly, PowerUser, AdministratorAccess) defined in your Identity Center instance
|
||||
- **Monitor assignment status**: Poll the provisioning status of create/delete operations, which are asynchronous in AWS
|
||||
- **List accounts in your organization**: Enumerate all AWS accounts in your AWS Organizations structure to populate access request dropdowns
|
||||
- **Manage groups**: List groups and resolve group IDs by display name for group-based access grants
|
||||
|
||||
In Sim, the AWS Identity Center integration is designed to power **TEAM (Temporary Elevated Access Management)** workflows — automated pipelines where users request elevated access, approvers approve or deny it, access is provisioned with a time limit, and auto-revocation removes it when the window expires. This replaces manual console-based access management with auditable, agent-driven workflows that integrate with Slack, email, ticketing systems, and CloudTrail for full traceability.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Provision and revoke temporary access to AWS accounts via IAM Identity Center (SSO). Assign permission sets to users or groups, look up users by email, and list accounts and permission sets for access request workflows.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `identity_center_list_instances`
|
||||
|
||||
List all AWS IAM Identity Center instances in your account
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `maxResults` | number | No | Maximum number of instances to return \(1-100\) |
|
||||
| `nextToken` | string | No | Pagination token from a previous request |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `instances` | json | List of Identity Center instances with instanceArn, identityStoreId, name, status, statusReason |
|
||||
| `nextToken` | string | Pagination token for the next page of results |
|
||||
| `count` | number | Number of instances returned |
|
||||
|
||||
### `identity_center_list_accounts`
|
||||
|
||||
List all AWS accounts in your organization
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `maxResults` | number | No | Maximum number of accounts to return |
|
||||
| `nextToken` | string | No | Pagination token from a previous request |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `accounts` | json | List of AWS accounts with id, arn, name, email, status |
|
||||
| `nextToken` | string | Pagination token for the next page of results |
|
||||
| `count` | number | Number of accounts returned |
|
||||
|
||||
### `identity_center_describe_account`
|
||||
|
||||
Retrieve details about a specific AWS account by its ID
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `accountId` | string | Yes | AWS account ID to describe |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | AWS account ID |
|
||||
| `arn` | string | AWS account ARN |
|
||||
| `name` | string | Account name |
|
||||
| `email` | string | Root email address of the account |
|
||||
| `status` | string | Account status \(ACTIVE, SUSPENDED, etc.\) |
|
||||
| `joinedTimestamp` | string | Date the account joined the organization |
|
||||
|
||||
### `identity_center_list_permission_sets`
|
||||
|
||||
List all permission sets defined in an IAM Identity Center instance
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `instanceArn` | string | Yes | ARN of the Identity Center instance |
|
||||
| `maxResults` | number | No | Maximum number of permission sets to return |
|
||||
| `nextToken` | string | No | Pagination token from a previous request |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `permissionSets` | json | List of permission sets with permissionSetArn, name, description, sessionDuration |
|
||||
| `nextToken` | string | Pagination token for the next page of results |
|
||||
| `count` | number | Number of permission sets returned |
|
||||
|
||||
### `identity_center_get_user`
|
||||
|
||||
Look up a user in the Identity Store by email address
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `identityStoreId` | string | Yes | Identity Store ID \(from the Identity Center instance\) |
|
||||
| `email` | string | Yes | Email address of the user to look up |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `userId` | string | Identity Store user ID \(use as principalId\) |
|
||||
| `userName` | string | Username in the Identity Store |
|
||||
| `displayName` | string | Display name of the user |
|
||||
| `email` | string | Email address of the user |
|
||||
|
||||
### `identity_center_get_group`
|
||||
|
||||
Look up a group in the Identity Store by display name
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `identityStoreId` | string | Yes | Identity Store ID \(from the Identity Center instance\) |
|
||||
| `displayName` | string | Yes | Display name of the group to look up |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `groupId` | string | Identity Store group ID \(use as principalId\) |
|
||||
| `displayName` | string | Display name of the group |
|
||||
| `description` | string | Group description |
|
||||
|
||||
### `identity_center_list_groups`
|
||||
|
||||
List all groups in the Identity Store
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `identityStoreId` | string | Yes | Identity Store ID \(from the Identity Center instance\) |
|
||||
| `maxResults` | number | No | Maximum number of groups to return |
|
||||
| `nextToken` | string | No | Pagination token from a previous request |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `groups` | json | List of groups with groupId, displayName, description |
|
||||
| `nextToken` | string | Pagination token for the next page of results |
|
||||
| `count` | number | Number of groups returned |
|
||||
|
||||
### `identity_center_create_account_assignment`
|
||||
|
||||
Grant a user or group access to an AWS account via a permission set (temporary elevated access)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `instanceArn` | string | Yes | ARN of the Identity Center instance |
|
||||
| `accountId` | string | Yes | AWS account ID to grant access to |
|
||||
| `permissionSetArn` | string | Yes | ARN of the permission set to assign |
|
||||
| `principalType` | string | Yes | Type of principal: USER or GROUP |
|
||||
| `principalId` | string | Yes | Identity Store ID of the user or group |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Status message |
|
||||
| `status` | string | Provisioning status: IN_PROGRESS, FAILED, or SUCCEEDED |
|
||||
| `requestId` | string | Request ID to use with Check Assignment Status |
|
||||
| `accountId` | string | Target AWS account ID |
|
||||
| `permissionSetArn` | string | Permission set ARN |
|
||||
| `principalType` | string | Principal type \(USER or GROUP\) |
|
||||
| `principalId` | string | Principal ID |
|
||||
| `failureReason` | string | Reason for failure if status is FAILED |
|
||||
| `createdDate` | string | Date the request was created |
|
||||
|
||||
### `identity_center_delete_account_assignment`
|
||||
|
||||
Revoke a user or group access to an AWS account by removing a permission set assignment
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `instanceArn` | string | Yes | ARN of the Identity Center instance |
|
||||
| `accountId` | string | Yes | AWS account ID to revoke access from |
|
||||
| `permissionSetArn` | string | Yes | ARN of the permission set to remove |
|
||||
| `principalType` | string | Yes | Type of principal: USER or GROUP |
|
||||
| `principalId` | string | Yes | Identity Store ID of the user or group |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Status message |
|
||||
| `status` | string | Deprovisioning status: IN_PROGRESS, FAILED, or SUCCEEDED |
|
||||
| `requestId` | string | Request ID to use with Check Assignment Status |
|
||||
| `accountId` | string | Target AWS account ID |
|
||||
| `permissionSetArn` | string | Permission set ARN |
|
||||
| `principalType` | string | Principal type \(USER or GROUP\) |
|
||||
| `principalId` | string | Principal ID |
|
||||
| `failureReason` | string | Reason for failure if status is FAILED |
|
||||
| `createdDate` | string | Date the request was created |
|
||||
|
||||
### `identity_center_check_assignment_status`
|
||||
|
||||
Check the provisioning status of an account assignment creation request
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `instanceArn` | string | Yes | ARN of the Identity Center instance |
|
||||
| `requestId` | string | Yes | Request ID returned from Create or Delete Account Assignment |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Human-readable status message |
|
||||
| `status` | string | Current status: IN_PROGRESS, FAILED, or SUCCEEDED |
|
||||
| `requestId` | string | The request ID that was checked |
|
||||
| `accountId` | string | Target AWS account ID |
|
||||
| `permissionSetArn` | string | Permission set ARN |
|
||||
| `principalType` | string | Principal type \(USER or GROUP\) |
|
||||
| `principalId` | string | Principal ID |
|
||||
| `failureReason` | string | Reason for failure if status is FAILED |
|
||||
| `createdDate` | string | Date the request was created |
|
||||
|
||||
### `identity_center_check_assignment_deletion_status`
|
||||
|
||||
Check the deprovisioning status of an account assignment deletion request
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `instanceArn` | string | Yes | ARN of the Identity Center instance |
|
||||
| `requestId` | string | Yes | Request ID returned from Delete Account Assignment |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Human-readable status message |
|
||||
| `status` | string | Current deletion status: IN_PROGRESS, FAILED, or SUCCEEDED |
|
||||
| `requestId` | string | The deletion request ID that was checked |
|
||||
| `accountId` | string | Target AWS account ID |
|
||||
| `permissionSetArn` | string | Permission set ARN |
|
||||
| `principalType` | string | Principal type \(USER or GROUP\) |
|
||||
| `principalId` | string | Principal ID |
|
||||
| `failureReason` | string | Reason for failure if status is FAILED |
|
||||
| `createdDate` | string | Date the request was created |
|
||||
|
||||
### `identity_center_list_account_assignments`
|
||||
|
||||
List all account assignments for a specific user or group across all accounts
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `instanceArn` | string | Yes | ARN of the Identity Center instance |
|
||||
| `principalId` | string | Yes | Identity Store ID of the user or group |
|
||||
| `principalType` | string | Yes | Type of principal: USER or GROUP |
|
||||
| `maxResults` | number | No | Maximum number of assignments to return |
|
||||
| `nextToken` | string | No | Pagination token from a previous request |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `assignments` | json | List of account assignments with accountId, permissionSetArn, principalType, principalId |
|
||||
| `nextToken` | string | Pagination token for the next page of results |
|
||||
| `count` | number | Number of assignments returned |
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"huggingface",
|
||||
"hunter",
|
||||
"iam",
|
||||
"identity_center",
|
||||
"image_generator",
|
||||
"imap",
|
||||
"incidentio",
|
||||
@@ -154,6 +155,7 @@
|
||||
"sentry",
|
||||
"serper",
|
||||
"servicenow",
|
||||
"ses",
|
||||
"sftp",
|
||||
"sharepoint",
|
||||
"shopify",
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
---
|
||||
title: AWS SES
|
||||
description: Send emails and manage templates with AWS Simple Email Service
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="ses"
|
||||
color="linear-gradient(45deg, #BD0816 0%, #FF5252 100%)"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Amazon Simple Email Service (SES)](https://aws.amazon.com/ses/) is a cloud-based email sending service designed for high-volume, transactional, and marketing email delivery. It provides a cost-effective, scalable way to send email without managing your own mail server infrastructure.
|
||||
|
||||
With AWS SES, you can:
|
||||
|
||||
- **Send simple emails**: Deliver one-off emails with plain text or HTML body content to individual recipients
|
||||
- **Send templated emails**: Use pre-defined templates with variable substitution (e.g., `{{name}}`, `{{link}}`) for personalized emails at scale
|
||||
- **Send bulk emails**: Deliver templated emails to large lists of recipients in a single API call, with per-destination data overrides
|
||||
- **Manage email templates**: Create, retrieve, list, and delete reusable email templates for transactional and marketing campaigns
|
||||
- **Monitor account health**: Retrieve your account's sending quota, send rate, and whether sending is currently enabled
|
||||
|
||||
In Sim, the AWS SES integration is designed for workflows that need reliable, programmatic email delivery — from access request notifications and approval alerts to bulk outreach and automated reporting. It pairs naturally with the IAM Identity Center integration for TEAM (Temporary Elevated Access Management) workflows, where email notifications are sent when access is provisioned, approved, or revoked.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate AWS SES v2 into the workflow. Send simple, templated, and bulk emails. Manage email templates and retrieve account sending quota and verified identity information.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `ses_send_email`
|
||||
|
||||
Send an email via AWS SES using simple or HTML content
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `fromAddress` | string | Yes | Verified sender email address |
|
||||
| `toAddresses` | string | Yes | Comma-separated list of recipient email addresses |
|
||||
| `subject` | string | Yes | Email subject line |
|
||||
| `bodyText` | string | No | Plain text email body |
|
||||
| `bodyHtml` | string | No | HTML email body |
|
||||
| `ccAddresses` | string | No | Comma-separated list of CC email addresses |
|
||||
| `bccAddresses` | string | No | Comma-separated list of BCC email addresses |
|
||||
| `replyToAddresses` | string | No | Comma-separated list of reply-to email addresses |
|
||||
| `configurationSetName` | string | No | SES configuration set name for tracking |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `messageId` | string | SES message ID for the sent email |
|
||||
|
||||
### `ses_send_templated_email`
|
||||
|
||||
Send an email using an SES email template with dynamic template data
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `fromAddress` | string | Yes | Verified sender email address |
|
||||
| `toAddresses` | string | Yes | Comma-separated list of recipient email addresses |
|
||||
| `templateName` | string | Yes | Name of the SES email template to use |
|
||||
| `templateData` | string | Yes | JSON string of key-value pairs for template variable substitution |
|
||||
| `ccAddresses` | string | No | Comma-separated list of CC email addresses |
|
||||
| `bccAddresses` | string | No | Comma-separated list of BCC email addresses |
|
||||
| `configurationSetName` | string | No | SES configuration set name for tracking |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `messageId` | string | SES message ID for the sent email |
|
||||
|
||||
### `ses_send_bulk_email`
|
||||
|
||||
Send emails to multiple recipients using an SES template with per-recipient data
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `fromAddress` | string | Yes | Verified sender email address |
|
||||
| `templateName` | string | Yes | Name of the SES email template to use |
|
||||
| `destinations` | string | Yes | JSON array of destination objects with toAddresses \(string\[\]\) and optional templateData \(JSON string\); falls back to defaultTemplateData when omitted |
|
||||
| `defaultTemplateData` | string | No | Default JSON template data used when a destination does not specify its own |
|
||||
| `configurationSetName` | string | No | SES configuration set name for tracking |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `results` | array | Per-destination send results with status and messageId |
|
||||
| `successCount` | number | Number of successfully sent emails |
|
||||
| `failureCount` | number | Number of failed email sends |
|
||||
|
||||
### `ses_list_identities`
|
||||
|
||||
List all verified email identities (email addresses and domains) in your SES account
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `pageSize` | number | No | Maximum number of identities to return \(1-1000\) |
|
||||
| `nextToken` | string | No | Pagination token from a previous list response |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `identities` | array | List of email identities with name, type, sending status, and verification status |
|
||||
| `nextToken` | string | Pagination token for the next page of results |
|
||||
| `count` | number | Number of identities returned |
|
||||
|
||||
### `ses_get_account`
|
||||
|
||||
Get SES account sending quota and status information
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `sendingEnabled` | boolean | Whether email sending is enabled for the account |
|
||||
| `max24HourSend` | number | Maximum emails allowed per 24-hour period |
|
||||
| `maxSendRate` | number | Maximum emails allowed per second |
|
||||
| `sentLast24Hours` | number | Number of emails sent in the last 24 hours |
|
||||
|
||||
### `ses_create_template`
|
||||
|
||||
Create a new SES email template for use with templated email sending
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `templateName` | string | Yes | Unique name for the email template |
|
||||
| `subjectPart` | string | Yes | Subject line template \(supports \{\{variable\}\} substitution\) |
|
||||
| `textPart` | string | No | Plain text version of the template body |
|
||||
| `htmlPart` | string | No | HTML version of the template body |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Confirmation message for the created template |
|
||||
|
||||
### `ses_get_template`
|
||||
|
||||
Retrieve the content and details of an SES email template
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `templateName` | string | Yes | Name of the template to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `templateName` | string | Name of the template |
|
||||
| `subjectPart` | string | Subject line of the template |
|
||||
| `textPart` | string | Plain text body of the template |
|
||||
| `htmlPart` | string | HTML body of the template |
|
||||
|
||||
### `ses_list_templates`
|
||||
|
||||
List all SES email templates in your account
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `pageSize` | number | No | Maximum number of templates to return |
|
||||
| `nextToken` | string | No | Pagination token from a previous list response |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `templates` | array | List of email templates with name and creation timestamp |
|
||||
| `nextToken` | string | Pagination token for the next page of results |
|
||||
| `count` | number | Number of templates returned |
|
||||
|
||||
### `ses_delete_template`
|
||||
|
||||
Delete an existing SES email template
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `region` | string | Yes | AWS region \(e.g., us-east-1\) |
|
||||
| `accessKeyId` | string | Yes | AWS access key ID |
|
||||
| `secretAccessKey` | string | Yes | AWS secret access key |
|
||||
| `templateName` | string | Yes | Name of the template to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Confirmation message for the deleted template |
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ Assume an IAM role and receive temporary security credentials
|
||||
| `roleArn` | string | Yes | ARN of the IAM role to assume |
|
||||
| `roleSessionName` | string | Yes | Identifier for the assumed role session |
|
||||
| `durationSeconds` | number | No | Duration of the session in seconds \(900-43200, default 3600\) |
|
||||
| `policy` | string | No | JSON IAM policy to further restrict session permissions \(max 2048 chars\) |
|
||||
| `externalId` | string | No | External ID for cross-account access |
|
||||
| `serialNumber` | string | No | MFA device serial number or ARN |
|
||||
| `tokenCode` | string | No | MFA token code \(6 digits\) |
|
||||
@@ -61,6 +62,7 @@ Assume an IAM role and receive temporary security credentials
|
||||
| `assumedRoleArn` | string | ARN of the assumed role |
|
||||
| `assumedRoleId` | string | Assumed role ID with session name |
|
||||
| `packedPolicySize` | number | Percentage of allowed policy size used |
|
||||
| `sourceIdentity` | string | Source identity set on the role session, if any |
|
||||
|
||||
### `sts_get_caller_identity`
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ Trigger workflow when a Fireflies meeting transcription is complete
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `meetingId` | string | The ID of the transcribed meeting |
|
||||
| `eventType` | string | The type of event \(Transcription completed\) |
|
||||
| `eventType` | string | The type of event \(e.g. Transcription completed, meeting.transcribed\) |
|
||||
| `clientReferenceId` | string | Custom reference ID if set during upload |
|
||||
| `timestamp` | number | Unix timestamp in milliseconds when the event was fired \(V2 webhooks\) |
|
||||
|
||||
|
||||
@@ -304,7 +304,7 @@ Trigger workflow on any Jira Service Management webhook event
|
||||
| ↳ `id` | string | Changelog ID |
|
||||
| `comment` | object | comment output from the tool |
|
||||
| ↳ `id` | string | Comment ID |
|
||||
| ↳ `body` | string | Comment text/body |
|
||||
| ↳ `body` | json | Comment body in Atlassian Document Format \(ADF\). On Jira Server this may be a plain string. |
|
||||
| ↳ `author` | object | author output from the tool |
|
||||
| ↳ `displayName` | string | Comment author display name |
|
||||
| ↳ `accountId` | string | Comment author account ID |
|
||||
|
||||
@@ -25,6 +25,7 @@ Trigger workflow from Slack events like mentions, messages, and reactions
|
||||
| `signingSecret` | string | Yes | The signing secret from your Slack app to validate request authenticity. |
|
||||
| `botToken` | string | No | The bot token from your Slack app. Required for downloading files attached to messages. |
|
||||
| `includeFiles` | boolean | No | Download and include file attachments from messages. Requires a bot token with files:read scope. |
|
||||
| `setupWizard` | modal | No | Walk through manifest creation, app install, and pasting credentials. |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Loader } from '@/components/emcn'
|
||||
|
||||
export default function IntegrationDetailLoading() {
|
||||
return (
|
||||
<div className='flex min-h-[60vh] items-center justify-center bg-[var(--landing-bg)]'>
|
||||
<Loader animate className='h-6 w-6 text-[var(--landing-text-muted)]' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -91,6 +91,7 @@ import {
|
||||
HuggingFaceIcon,
|
||||
HunterIOIcon,
|
||||
IAMIcon,
|
||||
IdentityCenterIcon,
|
||||
ImageIcon,
|
||||
IncidentioIcon,
|
||||
InfisicalIcon,
|
||||
@@ -152,6 +153,7 @@ import {
|
||||
RootlyIcon,
|
||||
S3Icon,
|
||||
SalesforceIcon,
|
||||
SESIcon,
|
||||
SearchIcon,
|
||||
SecretsManagerIcon,
|
||||
SendgridIcon,
|
||||
@@ -284,6 +286,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
huggingface: HuggingFaceIcon,
|
||||
hunter: HunterIOIcon,
|
||||
iam: IAMIcon,
|
||||
identity_center: IdentityCenterIcon,
|
||||
image_generator: ImageIcon,
|
||||
imap: MailServerIcon,
|
||||
incidentio: IncidentioIcon,
|
||||
@@ -352,6 +355,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
sentry: SentryIcon,
|
||||
serper: SerperIcon,
|
||||
servicenow: ServiceNowIcon,
|
||||
ses: SESIcon,
|
||||
sftp: SftpIcon,
|
||||
sharepoint: MicrosoftSharepointIcon,
|
||||
shopify: ShopifyIcon,
|
||||
|
||||
@@ -484,7 +484,7 @@
|
||||
"type": "dynamodb",
|
||||
"slug": "amazon-dynamodb",
|
||||
"name": "Amazon DynamoDB",
|
||||
"description": "Connect to Amazon DynamoDB",
|
||||
"description": "Get, put, query, scan, update, and delete items in Amazon DynamoDB tables",
|
||||
"longDescription": "Integrate Amazon DynamoDB into workflows. Supports Get, Put, Query, Scan, Update, Delete, and Introspect operations on DynamoDB tables.",
|
||||
"bgColor": "linear-gradient(45deg, #2E27AD 0%, #527FFF 100%)",
|
||||
"iconName": "DynamoDBIcon",
|
||||
@@ -1464,9 +1464,21 @@
|
||||
{
|
||||
"name": "Remove User from Group",
|
||||
"description": "Remove an IAM user from a group"
|
||||
},
|
||||
{
|
||||
"name": "List Attached Role Policies",
|
||||
"description": "List all managed policies attached to an IAM role"
|
||||
},
|
||||
{
|
||||
"name": "List Attached User Policies",
|
||||
"description": "List all managed policies attached to an IAM user"
|
||||
},
|
||||
{
|
||||
"name": "Simulate Principal Policy",
|
||||
"description": "Simulate whether a user, role, or group is allowed to perform specific AWS actions — useful for pre-flight access checks"
|
||||
}
|
||||
],
|
||||
"operationCount": 18,
|
||||
"operationCount": 21,
|
||||
"triggers": [],
|
||||
"triggerCount": 0,
|
||||
"authType": "none",
|
||||
@@ -1474,6 +1486,73 @@
|
||||
"integrationTypes": ["developer-tools", "security"],
|
||||
"tags": ["cloud", "identity"]
|
||||
},
|
||||
{
|
||||
"type": "identity_center",
|
||||
"slug": "aws-identity-center",
|
||||
"name": "AWS Identity Center",
|
||||
"description": "Manage temporary elevated access in AWS IAM Identity Center",
|
||||
"longDescription": "Provision and revoke temporary access to AWS accounts via IAM Identity Center (SSO). Assign permission sets to users or groups, look up users by email, and list accounts and permission sets for access request workflows.",
|
||||
"bgColor": "linear-gradient(45deg, #BD0816 0%, #FF5252 100%)",
|
||||
"iconName": "IdentityCenterIcon",
|
||||
"docsUrl": "https://docs.sim.ai/tools/identity-center",
|
||||
"operations": [
|
||||
{
|
||||
"name": "List Instances",
|
||||
"description": "List all AWS IAM Identity Center instances in your account"
|
||||
},
|
||||
{
|
||||
"name": "List Accounts",
|
||||
"description": "List all AWS accounts in your organization"
|
||||
},
|
||||
{
|
||||
"name": "Describe Account",
|
||||
"description": "Retrieve details about a specific AWS account by its ID"
|
||||
},
|
||||
{
|
||||
"name": "List Permission Sets",
|
||||
"description": "List all permission sets defined in an IAM Identity Center instance"
|
||||
},
|
||||
{
|
||||
"name": "Get User",
|
||||
"description": "Look up a user in the Identity Store by email address"
|
||||
},
|
||||
{
|
||||
"name": "Get Group",
|
||||
"description": "Look up a group in the Identity Store by display name"
|
||||
},
|
||||
{
|
||||
"name": "List Groups",
|
||||
"description": "List all groups in the Identity Store"
|
||||
},
|
||||
{
|
||||
"name": "Create Account Assignment",
|
||||
"description": "Grant a user or group access to an AWS account via a permission set (temporary elevated access)"
|
||||
},
|
||||
{
|
||||
"name": "Delete Account Assignment",
|
||||
"description": "Revoke a user or group access to an AWS account by removing a permission set assignment"
|
||||
},
|
||||
{
|
||||
"name": "Check Assignment Status",
|
||||
"description": "Check the provisioning status of an account assignment creation request"
|
||||
},
|
||||
{
|
||||
"name": "Check Assignment Deletion Status",
|
||||
"description": "Check the deprovisioning status of an account assignment deletion request"
|
||||
},
|
||||
{
|
||||
"name": "List Account Assignments",
|
||||
"description": "List all account assignments for a specific user or group across all accounts"
|
||||
}
|
||||
],
|
||||
"operationCount": 12,
|
||||
"triggers": [],
|
||||
"triggerCount": 0,
|
||||
"authType": "none",
|
||||
"category": "tools",
|
||||
"integrationTypes": ["security", "developer-tools"],
|
||||
"tags": ["cloud", "identity"]
|
||||
},
|
||||
{
|
||||
"type": "secrets_manager",
|
||||
"slug": "aws-secrets-manager",
|
||||
@@ -1513,6 +1592,61 @@
|
||||
"integrationTypes": ["developer-tools", "security"],
|
||||
"tags": ["cloud", "secrets-management"]
|
||||
},
|
||||
{
|
||||
"type": "ses",
|
||||
"slug": "aws-ses",
|
||||
"name": "AWS SES",
|
||||
"description": "Send emails and manage templates with AWS Simple Email Service",
|
||||
"longDescription": "Integrate AWS SES v2 into the workflow. Send simple, templated, and bulk emails. Manage email templates and retrieve account sending quota and verified identity information.",
|
||||
"bgColor": "linear-gradient(45deg, #BD0816 0%, #FF5252 100%)",
|
||||
"iconName": "SESIcon",
|
||||
"docsUrl": "https://docs.sim.ai/tools/ses",
|
||||
"operations": [
|
||||
{
|
||||
"name": "Send Email",
|
||||
"description": "Send an email via AWS SES using simple or HTML content"
|
||||
},
|
||||
{
|
||||
"name": "Send Templated Email",
|
||||
"description": "Send an email using an SES email template with dynamic template data"
|
||||
},
|
||||
{
|
||||
"name": "Send Bulk Email",
|
||||
"description": "Send emails to multiple recipients using an SES template with per-recipient data"
|
||||
},
|
||||
{
|
||||
"name": "List Identities",
|
||||
"description": "List all verified email identities (email addresses and domains) in your SES account"
|
||||
},
|
||||
{
|
||||
"name": "Get Account",
|
||||
"description": "Get SES account sending quota and status information"
|
||||
},
|
||||
{
|
||||
"name": "Create Template",
|
||||
"description": "Create a new SES email template for use with templated email sending"
|
||||
},
|
||||
{
|
||||
"name": "Get Template",
|
||||
"description": "Retrieve the content and details of an SES email template"
|
||||
},
|
||||
{
|
||||
"name": "List Templates",
|
||||
"description": "List all SES email templates in your account"
|
||||
},
|
||||
{
|
||||
"name": "Delete Template",
|
||||
"description": "Delete an existing SES email template"
|
||||
}
|
||||
],
|
||||
"operationCount": 9,
|
||||
"triggers": [],
|
||||
"triggerCount": 0,
|
||||
"authType": "none",
|
||||
"category": "tools",
|
||||
"integrationTypes": ["email", "analytics", "developer-tools"],
|
||||
"tags": ["cloud", "marketing"]
|
||||
},
|
||||
{
|
||||
"type": "sts",
|
||||
"slug": "aws-sts",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Loader } from '@/components/emcn'
|
||||
|
||||
export default function ModelDetailLoading() {
|
||||
return (
|
||||
<div className='flex min-h-[60vh] items-center justify-center bg-[var(--landing-bg)]'>
|
||||
<Loader animate className='h-6 w-6 text-[var(--landing-text-muted)]' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Loader } from '@/components/emcn'
|
||||
|
||||
export default function ModelProviderLoading() {
|
||||
return (
|
||||
<div className='flex min-h-[60vh] items-center justify-center bg-[var(--landing-bg)]'>
|
||||
<Loader animate className='h-6 w-6 text-[var(--landing-text-muted)]' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type StateValue,
|
||||
} from '@aws-sdk/client-cloudwatch'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -41,6 +42,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const validatedData = DescribeAlarmsSchema.parse(body)
|
||||
|
||||
logger.info('Describing CloudWatch alarms')
|
||||
|
||||
const client = new CloudWatchClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
@@ -49,57 +52,67 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
},
|
||||
})
|
||||
|
||||
const command = new DescribeAlarmsCommand({
|
||||
...(validatedData.alarmNamePrefix && { AlarmNamePrefix: validatedData.alarmNamePrefix }),
|
||||
...(validatedData.stateValue && { StateValue: validatedData.stateValue as StateValue }),
|
||||
AlarmTypes: validatedData.alarmType
|
||||
? [validatedData.alarmType as AlarmType]
|
||||
: (['MetricAlarm', 'CompositeAlarm'] as AlarmType[]),
|
||||
...(validatedData.limit !== undefined && { MaxRecords: validatedData.limit }),
|
||||
})
|
||||
try {
|
||||
const command = new DescribeAlarmsCommand({
|
||||
...(validatedData.alarmNamePrefix && { AlarmNamePrefix: validatedData.alarmNamePrefix }),
|
||||
...(validatedData.stateValue && { StateValue: validatedData.stateValue as StateValue }),
|
||||
AlarmTypes: validatedData.alarmType
|
||||
? [validatedData.alarmType as AlarmType]
|
||||
: (['MetricAlarm', 'CompositeAlarm'] as AlarmType[]),
|
||||
...(validatedData.limit !== undefined && { MaxRecords: validatedData.limit }),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
const response = await client.send(command)
|
||||
|
||||
const metricAlarms = (response.MetricAlarms ?? []).map((a) => ({
|
||||
alarmName: a.AlarmName ?? '',
|
||||
alarmArn: a.AlarmArn ?? '',
|
||||
stateValue: a.StateValue ?? 'UNKNOWN',
|
||||
stateReason: a.StateReason ?? '',
|
||||
metricName: a.MetricName,
|
||||
namespace: a.Namespace,
|
||||
comparisonOperator: a.ComparisonOperator,
|
||||
threshold: a.Threshold,
|
||||
evaluationPeriods: a.EvaluationPeriods,
|
||||
stateUpdatedTimestamp: a.StateUpdatedTimestamp?.getTime(),
|
||||
}))
|
||||
const metricAlarms = (response.MetricAlarms ?? []).map((a) => ({
|
||||
alarmName: a.AlarmName ?? '',
|
||||
alarmArn: a.AlarmArn ?? '',
|
||||
stateValue: a.StateValue ?? 'UNKNOWN',
|
||||
stateReason: a.StateReason ?? '',
|
||||
metricName: a.MetricName,
|
||||
namespace: a.Namespace,
|
||||
comparisonOperator: a.ComparisonOperator,
|
||||
threshold: a.Threshold,
|
||||
evaluationPeriods: a.EvaluationPeriods,
|
||||
stateUpdatedTimestamp: a.StateUpdatedTimestamp?.getTime(),
|
||||
}))
|
||||
|
||||
const compositeAlarms = (response.CompositeAlarms ?? []).map((a) => ({
|
||||
alarmName: a.AlarmName ?? '',
|
||||
alarmArn: a.AlarmArn ?? '',
|
||||
stateValue: a.StateValue ?? 'UNKNOWN',
|
||||
stateReason: a.StateReason ?? '',
|
||||
metricName: undefined,
|
||||
namespace: undefined,
|
||||
comparisonOperator: undefined,
|
||||
threshold: undefined,
|
||||
evaluationPeriods: undefined,
|
||||
stateUpdatedTimestamp: a.StateUpdatedTimestamp?.getTime(),
|
||||
}))
|
||||
const compositeAlarms = (response.CompositeAlarms ?? []).map((a) => ({
|
||||
alarmName: a.AlarmName ?? '',
|
||||
alarmArn: a.AlarmArn ?? '',
|
||||
stateValue: a.StateValue ?? 'UNKNOWN',
|
||||
stateReason: a.StateReason ?? '',
|
||||
metricName: undefined,
|
||||
namespace: undefined,
|
||||
comparisonOperator: undefined,
|
||||
threshold: undefined,
|
||||
evaluationPeriods: undefined,
|
||||
stateUpdatedTimestamp: a.StateUpdatedTimestamp?.getTime(),
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { alarms: [...metricAlarms, ...compositeAlarms] },
|
||||
})
|
||||
const alarms = [...metricAlarms, ...compositeAlarms]
|
||||
|
||||
logger.info(`Successfully described ${alarms.length} alarms`)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { alarms },
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: error.errors[0]?.message ?? 'Invalid request' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to describe CloudWatch alarms'
|
||||
logger.error('DescribeAlarms failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
logger.error('DescribeAlarms failed', { error: toError(error).message })
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to describe CloudWatch alarms: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DescribeLogGroupsCommand } from '@aws-sdk/client-cloudwatch-logs'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -29,41 +30,51 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const validatedData = DescribeLogGroupsSchema.parse(body)
|
||||
|
||||
logger.info('Describing CloudWatch log groups')
|
||||
|
||||
const client = createCloudWatchLogsClient({
|
||||
region: validatedData.region,
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
})
|
||||
|
||||
const command = new DescribeLogGroupsCommand({
|
||||
...(validatedData.prefix && { logGroupNamePrefix: validatedData.prefix }),
|
||||
...(validatedData.limit !== undefined && { limit: validatedData.limit }),
|
||||
})
|
||||
try {
|
||||
const command = new DescribeLogGroupsCommand({
|
||||
...(validatedData.prefix && { logGroupNamePrefix: validatedData.prefix }),
|
||||
...(validatedData.limit !== undefined && { limit: validatedData.limit }),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
const response = await client.send(command)
|
||||
|
||||
const logGroups = (response.logGroups ?? []).map((lg) => ({
|
||||
logGroupName: lg.logGroupName ?? '',
|
||||
arn: lg.arn ?? '',
|
||||
storedBytes: lg.storedBytes ?? 0,
|
||||
retentionInDays: lg.retentionInDays,
|
||||
creationTime: lg.creationTime,
|
||||
}))
|
||||
const logGroups = (response.logGroups ?? []).map((lg) => ({
|
||||
logGroupName: lg.logGroupName ?? '',
|
||||
arn: lg.arn ?? '',
|
||||
storedBytes: lg.storedBytes ?? 0,
|
||||
retentionInDays: lg.retentionInDays,
|
||||
creationTime: lg.creationTime,
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { logGroups },
|
||||
})
|
||||
logger.info(`Successfully described ${logGroups.length} log groups`)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { logGroups },
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: error.errors[0]?.message ?? 'Invalid request' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to describe CloudWatch log groups'
|
||||
logger.error('DescribeLogGroups failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
logger.error('DescribeLogGroups failed', { error: toError(error).message })
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to describe CloudWatch log groups: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -29,31 +30,41 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const validatedData = DescribeLogStreamsSchema.parse(body)
|
||||
|
||||
logger.info(`Describing log streams for group: ${validatedData.logGroupName}`)
|
||||
|
||||
const client = createCloudWatchLogsClient({
|
||||
region: validatedData.region,
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
})
|
||||
|
||||
const result = await describeLogStreams(client, validatedData.logGroupName, {
|
||||
prefix: validatedData.prefix,
|
||||
limit: validatedData.limit,
|
||||
})
|
||||
try {
|
||||
const result = await describeLogStreams(client, validatedData.logGroupName, {
|
||||
prefix: validatedData.prefix,
|
||||
limit: validatedData.limit,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { logStreams: result.logStreams },
|
||||
})
|
||||
logger.info(`Successfully described ${result.logStreams.length} log streams`)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { logStreams: result.logStreams },
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: error.errors[0]?.message ?? 'Invalid request' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to describe CloudWatch log streams'
|
||||
logger.error('DescribeLogStreams failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
logger.error('DescribeLogStreams failed', { error: toError(error).message })
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to describe CloudWatch log streams: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -31,37 +32,49 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const validatedData = GetLogEventsSchema.parse(body)
|
||||
|
||||
logger.info(
|
||||
`Getting log events from ${validatedData.logGroupName}/${validatedData.logStreamName}`
|
||||
)
|
||||
|
||||
const client = createCloudWatchLogsClient({
|
||||
region: validatedData.region,
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
})
|
||||
|
||||
const result = await getLogEvents(
|
||||
client,
|
||||
validatedData.logGroupName,
|
||||
validatedData.logStreamName,
|
||||
{
|
||||
startTime: validatedData.startTime,
|
||||
endTime: validatedData.endTime,
|
||||
limit: validatedData.limit,
|
||||
}
|
||||
)
|
||||
try {
|
||||
const result = await getLogEvents(
|
||||
client,
|
||||
validatedData.logGroupName,
|
||||
validatedData.logStreamName,
|
||||
{
|
||||
startTime: validatedData.startTime,
|
||||
endTime: validatedData.endTime,
|
||||
limit: validatedData.limit,
|
||||
}
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { events: result.events },
|
||||
})
|
||||
logger.info(`Successfully retrieved ${result.events.length} log events`)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { events: result.events },
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: error.errors[0]?.message ?? 'Invalid request' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to get CloudWatch log events'
|
||||
logger.error('GetLogEvents failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
logger.error('GetLogEvents failed', { error: toError(error).message })
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to get CloudWatch log events: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CloudWatchClient, GetMetricStatisticsCommand } from '@aws-sdk/client-cloudwatch'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -30,6 +31,10 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const validatedData = GetMetricStatisticsSchema.parse(body)
|
||||
|
||||
logger.info(
|
||||
`Getting metric statistics for ${validatedData.namespace}/${validatedData.metricName}`
|
||||
)
|
||||
|
||||
const client = new CloudWatchClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
@@ -38,67 +43,75 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
},
|
||||
})
|
||||
|
||||
let parsedDimensions: { Name: string; Value: string }[] | undefined
|
||||
if (validatedData.dimensions) {
|
||||
try {
|
||||
const dims = JSON.parse(validatedData.dimensions)
|
||||
if (Array.isArray(dims)) {
|
||||
parsedDimensions = dims.map((d: Record<string, string>) => ({
|
||||
Name: d.name,
|
||||
Value: d.value,
|
||||
}))
|
||||
} else if (typeof dims === 'object') {
|
||||
parsedDimensions = Object.entries(dims).map(([name, value]) => ({
|
||||
Name: name,
|
||||
Value: String(value),
|
||||
}))
|
||||
try {
|
||||
let parsedDimensions: { Name: string; Value: string }[] | undefined
|
||||
if (validatedData.dimensions) {
|
||||
try {
|
||||
const dims = JSON.parse(validatedData.dimensions)
|
||||
if (Array.isArray(dims)) {
|
||||
parsedDimensions = dims.map((d: Record<string, string>) => ({
|
||||
Name: d.name,
|
||||
Value: d.value,
|
||||
}))
|
||||
} else if (typeof dims === 'object') {
|
||||
parsedDimensions = Object.entries(dims).map(([name, value]) => ({
|
||||
Name: name,
|
||||
Value: String(value),
|
||||
}))
|
||||
}
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid dimensions JSON format' }, { status: 400 })
|
||||
}
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid dimensions JSON format' }, { status: 400 })
|
||||
}
|
||||
|
||||
const command = new GetMetricStatisticsCommand({
|
||||
Namespace: validatedData.namespace,
|
||||
MetricName: validatedData.metricName,
|
||||
StartTime: new Date(validatedData.startTime * 1000),
|
||||
EndTime: new Date(validatedData.endTime * 1000),
|
||||
Period: validatedData.period,
|
||||
Statistics: validatedData.statistics,
|
||||
...(parsedDimensions && { Dimensions: parsedDimensions }),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
const datapoints = (response.Datapoints ?? [])
|
||||
.sort((a, b) => (a.Timestamp?.getTime() ?? 0) - (b.Timestamp?.getTime() ?? 0))
|
||||
.map((dp) => ({
|
||||
timestamp: dp.Timestamp ? dp.Timestamp.getTime() : 0,
|
||||
average: dp.Average,
|
||||
sum: dp.Sum,
|
||||
minimum: dp.Minimum,
|
||||
maximum: dp.Maximum,
|
||||
sampleCount: dp.SampleCount,
|
||||
unit: dp.Unit,
|
||||
}))
|
||||
|
||||
logger.info(`Successfully retrieved ${datapoints.length} datapoints`)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
label: response.Label ?? validatedData.metricName,
|
||||
datapoints,
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
|
||||
const command = new GetMetricStatisticsCommand({
|
||||
Namespace: validatedData.namespace,
|
||||
MetricName: validatedData.metricName,
|
||||
StartTime: new Date(validatedData.startTime * 1000),
|
||||
EndTime: new Date(validatedData.endTime * 1000),
|
||||
Period: validatedData.period,
|
||||
Statistics: validatedData.statistics,
|
||||
...(parsedDimensions && { Dimensions: parsedDimensions }),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
const datapoints = (response.Datapoints ?? [])
|
||||
.sort((a, b) => (a.Timestamp?.getTime() ?? 0) - (b.Timestamp?.getTime() ?? 0))
|
||||
.map((dp) => ({
|
||||
timestamp: dp.Timestamp ? dp.Timestamp.getTime() : 0,
|
||||
average: dp.Average,
|
||||
sum: dp.Sum,
|
||||
minimum: dp.Minimum,
|
||||
maximum: dp.Maximum,
|
||||
sampleCount: dp.SampleCount,
|
||||
unit: dp.Unit,
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
label: response.Label ?? validatedData.metricName,
|
||||
datapoints,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: error.errors[0]?.message ?? 'Invalid request' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to get CloudWatch metric statistics'
|
||||
logger.error('GetMetricStatistics failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
logger.error('GetMetricStatistics failed', { error: toError(error).message })
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to get CloudWatch metric statistics: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CloudWatchClient, ListMetricsCommand } from '@aws-sdk/client-cloudwatch'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -30,6 +31,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const validatedData = ListMetricsSchema.parse(body)
|
||||
|
||||
logger.info('Listing CloudWatch metrics')
|
||||
|
||||
const client = new CloudWatchClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
@@ -38,40 +41,47 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
},
|
||||
})
|
||||
|
||||
const limit = validatedData.limit ?? 500
|
||||
try {
|
||||
const limit = validatedData.limit ?? 500
|
||||
|
||||
const command = new ListMetricsCommand({
|
||||
...(validatedData.namespace && { Namespace: validatedData.namespace }),
|
||||
...(validatedData.metricName && { MetricName: validatedData.metricName }),
|
||||
...(validatedData.recentlyActive && { RecentlyActive: 'PT3H' }),
|
||||
...(limit <= 500 && { MaxResults: limit }),
|
||||
})
|
||||
const command = new ListMetricsCommand({
|
||||
...(validatedData.namespace && { Namespace: validatedData.namespace }),
|
||||
...(validatedData.metricName && { MetricName: validatedData.metricName }),
|
||||
...(validatedData.recentlyActive && { RecentlyActive: 'PT3H' }),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
const response = await client.send(command)
|
||||
|
||||
const metrics = (response.Metrics ?? []).slice(0, limit).map((m) => ({
|
||||
namespace: m.Namespace ?? '',
|
||||
metricName: m.MetricName ?? '',
|
||||
dimensions: (m.Dimensions ?? []).map((d) => ({
|
||||
name: d.Name ?? '',
|
||||
value: d.Value ?? '',
|
||||
})),
|
||||
}))
|
||||
const metrics = (response.Metrics ?? []).slice(0, limit).map((m) => ({
|
||||
namespace: m.Namespace ?? '',
|
||||
metricName: m.MetricName ?? '',
|
||||
dimensions: (m.Dimensions ?? []).map((d) => ({
|
||||
name: d.Name ?? '',
|
||||
value: d.Value ?? '',
|
||||
})),
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { metrics },
|
||||
})
|
||||
logger.info(`Successfully listed ${metrics.length} metrics`)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { metrics },
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: error.errors[0]?.message ?? 'Invalid request' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to list CloudWatch metrics'
|
||||
logger.error('ListMetrics failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
logger.error('ListMetrics failed', { error: toError(error).message })
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to list CloudWatch metrics: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type StandardUnit,
|
||||
} from '@aws-sdk/client-cloudwatch'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -78,6 +79,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const validatedData = PutMetricDataSchema.parse(body)
|
||||
|
||||
logger.info(`Publishing metric ${validatedData.namespace}/${validatedData.metricName}`)
|
||||
|
||||
const client = new CloudWatchClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
@@ -86,52 +89,60 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
},
|
||||
})
|
||||
|
||||
const timestamp = new Date()
|
||||
try {
|
||||
const timestamp = new Date()
|
||||
|
||||
const dimensions: { Name: string; Value: string }[] = []
|
||||
if (validatedData.dimensions) {
|
||||
const parsed = JSON.parse(validatedData.dimensions)
|
||||
for (const [name, value] of Object.entries(parsed)) {
|
||||
dimensions.push({ Name: name, Value: String(value) })
|
||||
const dimensions: { Name: string; Value: string }[] = []
|
||||
if (validatedData.dimensions) {
|
||||
const parsed = JSON.parse(validatedData.dimensions)
|
||||
for (const [name, value] of Object.entries(parsed)) {
|
||||
dimensions.push({ Name: name, Value: String(value) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const command = new PutMetricDataCommand({
|
||||
Namespace: validatedData.namespace,
|
||||
MetricData: [
|
||||
{
|
||||
MetricName: validatedData.metricName,
|
||||
Value: validatedData.value,
|
||||
Timestamp: timestamp,
|
||||
...(validatedData.unit && { Unit: validatedData.unit as StandardUnit }),
|
||||
...(dimensions.length > 0 && { Dimensions: dimensions }),
|
||||
},
|
||||
],
|
||||
})
|
||||
const command = new PutMetricDataCommand({
|
||||
Namespace: validatedData.namespace,
|
||||
MetricData: [
|
||||
{
|
||||
MetricName: validatedData.metricName,
|
||||
Value: validatedData.value,
|
||||
Timestamp: timestamp,
|
||||
...(validatedData.unit && { Unit: validatedData.unit as StandardUnit }),
|
||||
...(dimensions.length > 0 && { Dimensions: dimensions }),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await client.send(command)
|
||||
await client.send(command)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
logger.info('Successfully published metric')
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
namespace: validatedData.namespace,
|
||||
metricName: validatedData.metricName,
|
||||
value: validatedData.value,
|
||||
unit: validatedData.unit ?? 'None',
|
||||
timestamp: timestamp.toISOString(),
|
||||
},
|
||||
})
|
||||
output: {
|
||||
success: true,
|
||||
namespace: validatedData.namespace,
|
||||
metricName: validatedData.metricName,
|
||||
value: validatedData.value,
|
||||
unit: validatedData.unit ?? 'None',
|
||||
timestamp: timestamp.toISOString(),
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: error.errors[0]?.message ?? 'Invalid request' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to publish CloudWatch metric'
|
||||
logger.error('PutMetricData failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
logger.error('PutMetricData failed', { error: toError(error).message })
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to publish CloudWatch metric: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { StartQueryCommand } from '@aws-sdk/client-cloudwatch-logs'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -32,47 +33,57 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const validatedData = QueryLogsSchema.parse(body)
|
||||
|
||||
logger.info('Running CloudWatch Log Insights query')
|
||||
|
||||
const client = createCloudWatchLogsClient({
|
||||
region: validatedData.region,
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
})
|
||||
|
||||
const startQueryCommand = new StartQueryCommand({
|
||||
logGroupNames: validatedData.logGroupNames,
|
||||
queryString: validatedData.queryString,
|
||||
startTime: validatedData.startTime,
|
||||
endTime: validatedData.endTime,
|
||||
...(validatedData.limit !== undefined && { limit: validatedData.limit }),
|
||||
})
|
||||
try {
|
||||
const startQueryCommand = new StartQueryCommand({
|
||||
logGroupNames: validatedData.logGroupNames,
|
||||
queryString: validatedData.queryString,
|
||||
startTime: validatedData.startTime,
|
||||
endTime: validatedData.endTime,
|
||||
...(validatedData.limit !== undefined && { limit: validatedData.limit }),
|
||||
})
|
||||
|
||||
const startQueryResponse = await client.send(startQueryCommand)
|
||||
const queryId = startQueryResponse.queryId
|
||||
const startQueryResponse = await client.send(startQueryCommand)
|
||||
const queryId = startQueryResponse.queryId
|
||||
|
||||
if (!queryId) {
|
||||
throw new Error('Failed to start CloudWatch Log Insights query: no queryId returned')
|
||||
if (!queryId) {
|
||||
throw new Error('Failed to start CloudWatch Log Insights query: no queryId returned')
|
||||
}
|
||||
|
||||
const result = await pollQueryResults(client, queryId)
|
||||
|
||||
logger.info(`Query completed with status: ${result.status}`)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
results: result.results,
|
||||
statistics: result.statistics,
|
||||
status: result.status,
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
|
||||
const result = await pollQueryResults(client, queryId)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
results: result.results,
|
||||
statistics: result.statistics,
|
||||
status: result.status,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: error.errors[0]?.message ?? 'Invalid request' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'CloudWatch Log Insights query failed'
|
||||
logger.error('QueryLogs failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
logger.error('QueryLogs failed', { error: toError(error).message })
|
||||
return NextResponse.json(
|
||||
{ error: `CloudWatch Log Insights query failed: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -143,7 +143,7 @@ export async function getLogEvents(
|
||||
}[]
|
||||
}> {
|
||||
const command = new GetLogEventsCommand({
|
||||
logGroupIdentifier: logGroupName,
|
||||
logGroupName,
|
||||
logStreamName,
|
||||
...(options?.startTime !== undefined && { startTime: options.startTime * 1000 }),
|
||||
...(options?.endTime !== undefined && { endTime: options.endTime * 1000 }),
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createDynamoDBClient, deleteItem } from '@/app/api/tools/dynamodb/utils'
|
||||
|
||||
const logger = createLogger('DynamoDBDeleteAPI')
|
||||
|
||||
const DeleteSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
@@ -13,6 +17,8 @@ const DeleteSchema = z.object({
|
||||
message: 'Key is required',
|
||||
}),
|
||||
conditionExpression: z.string().optional(),
|
||||
expressionAttributeNames: z.record(z.string()).optional(),
|
||||
expressionAttributeValues: z.record(z.unknown()).optional(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
@@ -25,30 +31,39 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const validatedData = DeleteSchema.parse(body)
|
||||
|
||||
logger.info(`Deleting item from table '${validatedData.tableName}'`)
|
||||
|
||||
const client = createDynamoDBClient({
|
||||
region: validatedData.region,
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
})
|
||||
|
||||
await deleteItem(
|
||||
client,
|
||||
validatedData.tableName,
|
||||
validatedData.key,
|
||||
validatedData.conditionExpression
|
||||
)
|
||||
try {
|
||||
await deleteItem(client, validatedData.tableName, validatedData.key, {
|
||||
conditionExpression: validatedData.conditionExpression,
|
||||
expressionAttributeNames: validatedData.expressionAttributeNames,
|
||||
expressionAttributeValues: validatedData.expressionAttributeValues,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Item deleted successfully',
|
||||
})
|
||||
logger.info(`Delete completed for table '${validatedData.tableName}'`)
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Item deleted successfully',
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: error.errors[0]?.message ?? 'Invalid request' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'DynamoDB delete failed'
|
||||
const errorMessage = toError(error).message || 'DynamoDB delete failed'
|
||||
logger.error('DynamoDB delete failed:', error)
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createDynamoDBClient, getItem } from '@/app/api/tools/dynamodb/utils'
|
||||
|
||||
const logger = createLogger('DynamoDBGetAPI')
|
||||
|
||||
const GetSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
@@ -31,31 +35,41 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const validatedData = GetSchema.parse(body)
|
||||
|
||||
logger.info(`Getting item from table '${validatedData.tableName}'`)
|
||||
|
||||
const client = createDynamoDBClient({
|
||||
region: validatedData.region,
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
})
|
||||
|
||||
const result = await getItem(
|
||||
client,
|
||||
validatedData.tableName,
|
||||
validatedData.key,
|
||||
validatedData.consistentRead
|
||||
)
|
||||
try {
|
||||
const result = await getItem(
|
||||
client,
|
||||
validatedData.tableName,
|
||||
validatedData.key,
|
||||
validatedData.consistentRead
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
message: result.item ? 'Item retrieved successfully' : 'Item not found',
|
||||
item: result.item,
|
||||
})
|
||||
logger.info(`Get item completed for table '${validatedData.tableName}'`)
|
||||
|
||||
return NextResponse.json({
|
||||
message: result.item ? 'Item retrieved successfully' : 'Item not found',
|
||||
item: result.item,
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: error.errors[0]?.message ?? 'Invalid request' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'DynamoDB get failed'
|
||||
const errorMessage = toError(error).message || 'DynamoDB get failed'
|
||||
logger.error('DynamoDB get failed:', error)
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -16,8 +16,6 @@ const IntrospectSchema = z.object({
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
@@ -27,7 +25,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const params = IntrospectSchema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Introspecting DynamoDB in region ${params.region}`)
|
||||
logger.info(`Introspecting DynamoDB in region ${params.region}`)
|
||||
|
||||
const client = createRawDynamoDBClient({
|
||||
region: params.region,
|
||||
@@ -39,10 +37,10 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const { tables } = await listTables(client)
|
||||
|
||||
if (params.tableName) {
|
||||
logger.info(`[${requestId}] Describing table: ${params.tableName}`)
|
||||
logger.info(`Describing table: ${params.tableName}`)
|
||||
const { tableDetails } = await describeTable(client, params.tableName)
|
||||
|
||||
logger.info(`[${requestId}] Table description completed for '${params.tableName}'`)
|
||||
logger.info(`Table description completed for '${params.tableName}'`)
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Table '${params.tableName}' described successfully.`,
|
||||
@@ -51,7 +49,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
})
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Listed ${tables.length} tables`)
|
||||
logger.info(`Listed ${tables.length} tables`)
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Found ${tables.length} table(s) in region '${params.region}'.`,
|
||||
@@ -62,15 +60,15 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] DynamoDB introspection failed:`, error)
|
||||
const errorMessage = toError(error).message || 'Unknown error occurred'
|
||||
logger.error('DynamoDB introspection failed:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `DynamoDB introspection failed: ${errorMessage}` },
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createDynamoDBClient, putItem } from '@/app/api/tools/dynamodb/utils'
|
||||
|
||||
const logger = createLogger('DynamoDBPutAPI')
|
||||
|
||||
const PutSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
@@ -12,6 +16,9 @@ const PutSchema = z.object({
|
||||
item: z.record(z.unknown()).refine((val) => Object.keys(val).length > 0, {
|
||||
message: 'Item is required',
|
||||
}),
|
||||
conditionExpression: z.string().optional(),
|
||||
expressionAttributeNames: z.record(z.string()).optional(),
|
||||
expressionAttributeValues: z.record(z.unknown()).optional(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
@@ -24,26 +31,40 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const validatedData = PutSchema.parse(body)
|
||||
|
||||
logger.info(`Putting item into table '${validatedData.tableName}'`)
|
||||
|
||||
const client = createDynamoDBClient({
|
||||
region: validatedData.region,
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
})
|
||||
|
||||
await putItem(client, validatedData.tableName, validatedData.item)
|
||||
try {
|
||||
await putItem(client, validatedData.tableName, validatedData.item, {
|
||||
conditionExpression: validatedData.conditionExpression,
|
||||
expressionAttributeNames: validatedData.expressionAttributeNames,
|
||||
expressionAttributeValues: validatedData.expressionAttributeValues,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Item created successfully',
|
||||
item: validatedData.item,
|
||||
})
|
||||
logger.info(`Put item completed for table '${validatedData.tableName}'`)
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Item created successfully',
|
||||
item: validatedData.item,
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: error.errors[0]?.message ?? 'Invalid request' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'DynamoDB put failed'
|
||||
const errorMessage = toError(error).message || 'DynamoDB put failed'
|
||||
logger.error('DynamoDB put failed:', error)
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createDynamoDBClient, queryItems } from '@/app/api/tools/dynamodb/utils'
|
||||
|
||||
const logger = createLogger('DynamoDBQueryAPI')
|
||||
|
||||
const QuerySchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
@@ -15,6 +19,8 @@ const QuerySchema = z.object({
|
||||
expressionAttributeValues: z.record(z.unknown()).optional(),
|
||||
indexName: z.string().optional(),
|
||||
limit: z.number().positive().optional(),
|
||||
exclusiveStartKey: z.record(z.unknown()).optional(),
|
||||
scanIndexForward: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
@@ -27,38 +33,53 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const validatedData = QuerySchema.parse(body)
|
||||
|
||||
logger.info(`Querying table '${validatedData.tableName}'`)
|
||||
|
||||
const client = createDynamoDBClient({
|
||||
region: validatedData.region,
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
})
|
||||
|
||||
const result = await queryItems(
|
||||
client,
|
||||
validatedData.tableName,
|
||||
validatedData.keyConditionExpression,
|
||||
{
|
||||
filterExpression: validatedData.filterExpression,
|
||||
expressionAttributeNames: validatedData.expressionAttributeNames,
|
||||
expressionAttributeValues: validatedData.expressionAttributeValues,
|
||||
indexName: validatedData.indexName,
|
||||
limit: validatedData.limit,
|
||||
}
|
||||
)
|
||||
try {
|
||||
const result = await queryItems(
|
||||
client,
|
||||
validatedData.tableName,
|
||||
validatedData.keyConditionExpression,
|
||||
{
|
||||
filterExpression: validatedData.filterExpression,
|
||||
expressionAttributeNames: validatedData.expressionAttributeNames,
|
||||
expressionAttributeValues: validatedData.expressionAttributeValues,
|
||||
indexName: validatedData.indexName,
|
||||
limit: validatedData.limit,
|
||||
exclusiveStartKey: validatedData.exclusiveStartKey,
|
||||
scanIndexForward: validatedData.scanIndexForward,
|
||||
}
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Query returned ${result.count} items`,
|
||||
items: result.items,
|
||||
count: result.count,
|
||||
})
|
||||
logger.info(
|
||||
`Query completed for table '${validatedData.tableName}', returned ${result.count} items`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Query returned ${result.count} items`,
|
||||
items: result.items,
|
||||
count: result.count,
|
||||
...(result.lastEvaluatedKey && { lastEvaluatedKey: result.lastEvaluatedKey }),
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: error.errors[0]?.message ?? 'Invalid request' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'DynamoDB query failed'
|
||||
const errorMessage = toError(error).message || 'DynamoDB query failed'
|
||||
logger.error('DynamoDB query failed:', error)
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createDynamoDBClient, scanItems } from '@/app/api/tools/dynamodb/utils'
|
||||
|
||||
const logger = createLogger('DynamoDBScanAPI')
|
||||
|
||||
const ScanSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
@@ -14,6 +18,7 @@ const ScanSchema = z.object({
|
||||
expressionAttributeNames: z.record(z.string()).optional(),
|
||||
expressionAttributeValues: z.record(z.unknown()).optional(),
|
||||
limit: z.number().positive().optional(),
|
||||
exclusiveStartKey: z.record(z.unknown()).optional(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
@@ -26,33 +31,47 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const validatedData = ScanSchema.parse(body)
|
||||
|
||||
logger.info(`Scanning table '${validatedData.tableName}'`)
|
||||
|
||||
const client = createDynamoDBClient({
|
||||
region: validatedData.region,
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
})
|
||||
|
||||
const result = await scanItems(client, validatedData.tableName, {
|
||||
filterExpression: validatedData.filterExpression,
|
||||
projectionExpression: validatedData.projectionExpression,
|
||||
expressionAttributeNames: validatedData.expressionAttributeNames,
|
||||
expressionAttributeValues: validatedData.expressionAttributeValues,
|
||||
limit: validatedData.limit,
|
||||
})
|
||||
try {
|
||||
const result = await scanItems(client, validatedData.tableName, {
|
||||
filterExpression: validatedData.filterExpression,
|
||||
projectionExpression: validatedData.projectionExpression,
|
||||
expressionAttributeNames: validatedData.expressionAttributeNames,
|
||||
expressionAttributeValues: validatedData.expressionAttributeValues,
|
||||
limit: validatedData.limit,
|
||||
exclusiveStartKey: validatedData.exclusiveStartKey,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Scan returned ${result.count} items`,
|
||||
items: result.items,
|
||||
count: result.count,
|
||||
})
|
||||
logger.info(
|
||||
`Scan completed for table '${validatedData.tableName}', returned ${result.count} items`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Scan returned ${result.count} items`,
|
||||
items: result.items,
|
||||
count: result.count,
|
||||
...(result.lastEvaluatedKey && { lastEvaluatedKey: result.lastEvaluatedKey }),
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: error.errors[0]?.message ?? 'Invalid request' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'DynamoDB scan failed'
|
||||
const errorMessage = toError(error).message || 'DynamoDB scan failed'
|
||||
logger.error('DynamoDB scan failed:', error)
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createDynamoDBClient, updateItem } from '@/app/api/tools/dynamodb/utils'
|
||||
|
||||
const logger = createLogger('DynamoDBUpdateAPI')
|
||||
|
||||
const UpdateSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
@@ -28,36 +32,46 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const validatedData = UpdateSchema.parse(body)
|
||||
|
||||
logger.info(`Updating item in table '${validatedData.tableName}'`)
|
||||
|
||||
const client = createDynamoDBClient({
|
||||
region: validatedData.region,
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
})
|
||||
|
||||
const result = await updateItem(
|
||||
client,
|
||||
validatedData.tableName,
|
||||
validatedData.key,
|
||||
validatedData.updateExpression,
|
||||
{
|
||||
expressionAttributeNames: validatedData.expressionAttributeNames,
|
||||
expressionAttributeValues: validatedData.expressionAttributeValues,
|
||||
conditionExpression: validatedData.conditionExpression,
|
||||
}
|
||||
)
|
||||
try {
|
||||
const result = await updateItem(
|
||||
client,
|
||||
validatedData.tableName,
|
||||
validatedData.key,
|
||||
validatedData.updateExpression,
|
||||
{
|
||||
expressionAttributeNames: validatedData.expressionAttributeNames,
|
||||
expressionAttributeValues: validatedData.expressionAttributeValues,
|
||||
conditionExpression: validatedData.conditionExpression,
|
||||
}
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Item updated successfully',
|
||||
item: result.attributes,
|
||||
})
|
||||
logger.info(`Update completed for table '${validatedData.tableName}'`)
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Item updated successfully',
|
||||
item: result.attributes,
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: error.errors[0]?.message ?? 'Invalid request' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'DynamoDB update failed'
|
||||
const errorMessage = toError(error).message || 'DynamoDB update failed'
|
||||
logger.error('DynamoDB update failed:', error)
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -51,11 +51,23 @@ export async function getItem(
|
||||
export async function putItem(
|
||||
client: DynamoDBDocumentClient,
|
||||
tableName: string,
|
||||
item: Record<string, unknown>
|
||||
item: Record<string, unknown>,
|
||||
options?: {
|
||||
conditionExpression?: string
|
||||
expressionAttributeNames?: Record<string, string>
|
||||
expressionAttributeValues?: Record<string, unknown>
|
||||
}
|
||||
): Promise<{ success: boolean }> {
|
||||
const command = new PutCommand({
|
||||
TableName: tableName,
|
||||
Item: item,
|
||||
...(options?.conditionExpression && { ConditionExpression: options.conditionExpression }),
|
||||
...(options?.expressionAttributeNames && {
|
||||
ExpressionAttributeNames: options.expressionAttributeNames,
|
||||
}),
|
||||
...(options?.expressionAttributeValues && {
|
||||
ExpressionAttributeValues: options.expressionAttributeValues,
|
||||
}),
|
||||
})
|
||||
|
||||
await client.send(command)
|
||||
@@ -72,8 +84,14 @@ export async function queryItems(
|
||||
expressionAttributeValues?: Record<string, unknown>
|
||||
indexName?: string
|
||||
limit?: number
|
||||
exclusiveStartKey?: Record<string, unknown>
|
||||
scanIndexForward?: boolean
|
||||
}
|
||||
): Promise<{ items: Record<string, unknown>[]; count: number }> {
|
||||
): Promise<{
|
||||
items: Record<string, unknown>[]
|
||||
count: number
|
||||
lastEvaluatedKey?: Record<string, unknown>
|
||||
}> {
|
||||
const command = new QueryCommand({
|
||||
TableName: tableName,
|
||||
KeyConditionExpression: keyConditionExpression,
|
||||
@@ -86,12 +104,15 @@ export async function queryItems(
|
||||
}),
|
||||
...(options?.indexName && { IndexName: options.indexName }),
|
||||
...(options?.limit && { Limit: options.limit }),
|
||||
...(options?.exclusiveStartKey && { ExclusiveStartKey: options.exclusiveStartKey }),
|
||||
...(options?.scanIndexForward !== undefined && { ScanIndexForward: options.scanIndexForward }),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
return {
|
||||
items: (response.Items as Record<string, unknown>[]) || [],
|
||||
count: response.Count || 0,
|
||||
lastEvaluatedKey: response.LastEvaluatedKey as Record<string, unknown> | undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,8 +125,13 @@ export async function scanItems(
|
||||
expressionAttributeNames?: Record<string, string>
|
||||
expressionAttributeValues?: Record<string, unknown>
|
||||
limit?: number
|
||||
exclusiveStartKey?: Record<string, unknown>
|
||||
}
|
||||
): Promise<{ items: Record<string, unknown>[]; count: number }> {
|
||||
): Promise<{
|
||||
items: Record<string, unknown>[]
|
||||
count: number
|
||||
lastEvaluatedKey?: Record<string, unknown>
|
||||
}> {
|
||||
const command = new ScanCommand({
|
||||
TableName: tableName,
|
||||
...(options?.filterExpression && { FilterExpression: options.filterExpression }),
|
||||
@@ -117,12 +143,14 @@ export async function scanItems(
|
||||
ExpressionAttributeValues: options.expressionAttributeValues,
|
||||
}),
|
||||
...(options?.limit && { Limit: options.limit }),
|
||||
...(options?.exclusiveStartKey && { ExclusiveStartKey: options.exclusiveStartKey }),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
return {
|
||||
items: (response.Items as Record<string, unknown>[]) || [],
|
||||
count: response.Count || 0,
|
||||
lastEvaluatedKey: response.LastEvaluatedKey as Record<string, unknown> | undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,12 +189,22 @@ export async function deleteItem(
|
||||
client: DynamoDBDocumentClient,
|
||||
tableName: string,
|
||||
key: Record<string, unknown>,
|
||||
conditionExpression?: string
|
||||
options?: {
|
||||
conditionExpression?: string
|
||||
expressionAttributeNames?: Record<string, string>
|
||||
expressionAttributeValues?: Record<string, unknown>
|
||||
}
|
||||
): Promise<{ success: boolean }> {
|
||||
const command = new DeleteCommand({
|
||||
TableName: tableName,
|
||||
Key: key,
|
||||
...(conditionExpression && { ConditionExpression: conditionExpression }),
|
||||
...(options?.conditionExpression && { ConditionExpression: options.conditionExpression }),
|
||||
...(options?.expressionAttributeNames && {
|
||||
ExpressionAttributeNames: options.expressionAttributeNames,
|
||||
}),
|
||||
...(options?.expressionAttributeValues && {
|
||||
ExpressionAttributeValues: options.expressionAttributeValues,
|
||||
}),
|
||||
})
|
||||
|
||||
await client.send(command)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -17,8 +17,6 @@ const Schema = z.object({
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
@@ -28,7 +26,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Adding user "${params.userName}" to group "${params.groupName}"`)
|
||||
logger.info(`Adding user "${params.userName}" to group "${params.groupName}"`)
|
||||
|
||||
const client = createIAMClient({
|
||||
region: params.region,
|
||||
@@ -38,9 +36,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
|
||||
try {
|
||||
await addUserToGroup(client, params.userName, params.groupName)
|
||||
logger.info(
|
||||
`[${requestId}] Successfully added user "${params.userName}" to group "${params.groupName}"`
|
||||
)
|
||||
logger.info(`Successfully added user "${params.userName}" to group "${params.groupName}"`)
|
||||
return NextResponse.json({
|
||||
message: `User "${params.userName}" added to group "${params.groupName}"`,
|
||||
})
|
||||
@@ -49,16 +45,15 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
logger.warn(`Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Failed to add user to group:`, error)
|
||||
logger.error(`Failed to add user to group:`, error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to add user to group: ${errorMessage}` },
|
||||
{ error: `Failed to add user to group: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -17,8 +17,6 @@ const Schema = z.object({
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
@@ -28,7 +26,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Attaching policy to IAM role "${params.roleName}"`)
|
||||
logger.info(`Attaching policy to IAM role "${params.roleName}"`)
|
||||
|
||||
const client = createIAMClient({
|
||||
region: params.region,
|
||||
@@ -38,7 +36,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
|
||||
try {
|
||||
await attachRolePolicy(client, params.roleName, params.policyArn)
|
||||
logger.info(`[${requestId}] Successfully attached policy to IAM role "${params.roleName}"`)
|
||||
logger.info(`Successfully attached policy to IAM role "${params.roleName}"`)
|
||||
return NextResponse.json({
|
||||
message: `Policy "${params.policyArn}" attached to role "${params.roleName}"`,
|
||||
})
|
||||
@@ -47,16 +45,15 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
logger.warn(`Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Failed to attach role policy:`, error)
|
||||
logger.error(`Failed to attach role policy:`, error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to attach role policy: ${errorMessage}` },
|
||||
{ error: `Failed to attach role policy: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -17,8 +17,6 @@ const Schema = z.object({
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
@@ -28,7 +26,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Attaching policy to IAM user "${params.userName}"`)
|
||||
logger.info(`Attaching policy to IAM user "${params.userName}"`)
|
||||
|
||||
const client = createIAMClient({
|
||||
region: params.region,
|
||||
@@ -38,7 +36,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
|
||||
try {
|
||||
await attachUserPolicy(client, params.userName, params.policyArn)
|
||||
logger.info(`[${requestId}] Successfully attached policy to IAM user "${params.userName}"`)
|
||||
logger.info(`Successfully attached policy to IAM user "${params.userName}"`)
|
||||
return NextResponse.json({
|
||||
message: `Policy "${params.policyArn}" attached to user "${params.userName}"`,
|
||||
})
|
||||
@@ -47,16 +45,15 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
logger.warn(`Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Failed to attach user policy:`, error)
|
||||
logger.error(`Failed to attach user policy:`, error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to attach user policy: ${errorMessage}` },
|
||||
{ error: `Failed to attach user policy: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -12,12 +12,10 @@ const Schema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
userName: z.string().optional(),
|
||||
userName: z.string().optional().nullable(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
@@ -27,7 +25,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Creating IAM access key`)
|
||||
logger.info(`Creating IAM access key`)
|
||||
|
||||
const client = createIAMClient({
|
||||
region: params.region,
|
||||
@@ -37,7 +35,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
|
||||
try {
|
||||
const result = await createAccessKey(client, params.userName)
|
||||
logger.info(`[${requestId}] Successfully created access key for user "${result.userName}"`)
|
||||
logger.info(`Successfully created access key for user "${result.userName}"`)
|
||||
return NextResponse.json({
|
||||
message: `Access key created for user "${result.userName}"`,
|
||||
...result,
|
||||
@@ -47,16 +45,15 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
logger.warn(`Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Failed to create access key:`, error)
|
||||
logger.error(`Failed to create access key:`, error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to create access key: ${errorMessage}` },
|
||||
{ error: `Failed to create access key: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -14,14 +14,12 @@ const Schema = z.object({
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
roleName: z.string().min(1, 'Role name is required'),
|
||||
assumeRolePolicyDocument: z.string().min(1, 'Assume role policy document is required'),
|
||||
description: z.string().optional(),
|
||||
path: z.string().optional(),
|
||||
maxSessionDuration: z.number().min(3600).max(43200).optional(),
|
||||
description: z.string().optional().nullable(),
|
||||
path: z.string().optional().nullable(),
|
||||
maxSessionDuration: z.number().int().min(3600).max(43200).optional().nullable(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
@@ -31,7 +29,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Creating IAM role "${params.roleName}"`)
|
||||
logger.info(`Creating IAM role "${params.roleName}"`)
|
||||
|
||||
const client = createIAMClient({
|
||||
region: params.region,
|
||||
@@ -48,7 +46,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
params.path,
|
||||
params.maxSessionDuration
|
||||
)
|
||||
logger.info(`[${requestId}] Successfully created IAM role "${result.roleName}"`)
|
||||
logger.info(`Successfully created IAM role "${result.roleName}"`)
|
||||
return NextResponse.json({
|
||||
message: `Role "${result.roleName}" created successfully`,
|
||||
...result,
|
||||
@@ -58,16 +56,15 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
logger.warn(`Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Failed to create IAM role:`, error)
|
||||
logger.error(`Failed to create IAM role:`, error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to create IAM role: ${errorMessage}` },
|
||||
{ error: `Failed to create IAM role: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -13,12 +13,10 @@ const Schema = z.object({
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
userName: z.string().min(1, 'User name is required'),
|
||||
path: z.string().optional(),
|
||||
path: z.string().optional().nullable(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
@@ -28,7 +26,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Creating IAM user "${params.userName}"`)
|
||||
logger.info(`Creating IAM user "${params.userName}"`)
|
||||
|
||||
const client = createIAMClient({
|
||||
region: params.region,
|
||||
@@ -38,7 +36,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
|
||||
try {
|
||||
const result = await createUser(client, params.userName, params.path)
|
||||
logger.info(`[${requestId}] Successfully created IAM user "${result.userName}"`)
|
||||
logger.info(`Successfully created IAM user "${result.userName}"`)
|
||||
return NextResponse.json({
|
||||
message: `User "${result.userName}" created successfully`,
|
||||
...result,
|
||||
@@ -48,16 +46,15 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
logger.warn(`Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Failed to create IAM user:`, error)
|
||||
logger.error(`Failed to create IAM user:`, error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to create IAM user: ${errorMessage}` },
|
||||
{ error: `Failed to create IAM user: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -13,12 +13,10 @@ const Schema = z.object({
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
accessKeyIdToDelete: z.string().min(1, 'Access key ID to delete is required'),
|
||||
userName: z.string().optional(),
|
||||
userName: z.string().optional().nullable(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
@@ -28,7 +26,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Deleting IAM access key "${params.accessKeyIdToDelete}"`)
|
||||
logger.info(`Deleting IAM access key "${params.accessKeyIdToDelete}"`)
|
||||
|
||||
const client = createIAMClient({
|
||||
region: params.region,
|
||||
@@ -38,23 +36,22 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
|
||||
try {
|
||||
await deleteAccessKey(client, params.accessKeyIdToDelete, params.userName)
|
||||
logger.info(`[${requestId}] Successfully deleted access key "${params.accessKeyIdToDelete}"`)
|
||||
logger.info(`Successfully deleted access key "${params.accessKeyIdToDelete}"`)
|
||||
return NextResponse.json({ message: `Access key "${params.accessKeyIdToDelete}" deleted` })
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
logger.warn(`Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Failed to delete access key:`, error)
|
||||
logger.error(`Failed to delete access key:`, error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to delete access key: ${errorMessage}` },
|
||||
{ error: `Failed to delete access key: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -16,8 +16,6 @@ const Schema = z.object({
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
@@ -27,7 +25,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Deleting IAM role "${params.roleName}"`)
|
||||
logger.info(`Deleting IAM role "${params.roleName}"`)
|
||||
|
||||
const client = createIAMClient({
|
||||
region: params.region,
|
||||
@@ -37,23 +35,22 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
|
||||
try {
|
||||
await deleteRole(client, params.roleName)
|
||||
logger.info(`[${requestId}] Successfully deleted IAM role "${params.roleName}"`)
|
||||
logger.info(`Successfully deleted IAM role "${params.roleName}"`)
|
||||
return NextResponse.json({ message: `Role "${params.roleName}" deleted successfully` })
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
logger.warn(`Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Failed to delete IAM role:`, error)
|
||||
logger.error(`Failed to delete IAM role:`, error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to delete IAM role: ${errorMessage}` },
|
||||
{ error: `Failed to delete IAM role: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -16,8 +16,6 @@ const Schema = z.object({
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
@@ -27,7 +25,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Deleting IAM user "${params.userName}"`)
|
||||
logger.info(`Deleting IAM user "${params.userName}"`)
|
||||
|
||||
const client = createIAMClient({
|
||||
region: params.region,
|
||||
@@ -37,23 +35,22 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
|
||||
try {
|
||||
await deleteUser(client, params.userName)
|
||||
logger.info(`[${requestId}] Successfully deleted IAM user "${params.userName}"`)
|
||||
logger.info(`Successfully deleted IAM user "${params.userName}"`)
|
||||
return NextResponse.json({ message: `User "${params.userName}" deleted successfully` })
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
logger.warn(`Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Failed to delete IAM user:`, error)
|
||||
logger.error(`Failed to delete IAM user:`, error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to delete IAM user: ${errorMessage}` },
|
||||
{ error: `Failed to delete IAM user: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -17,8 +17,6 @@ const Schema = z.object({
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
@@ -28,7 +26,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Detaching policy from IAM role "${params.roleName}"`)
|
||||
logger.info(`Detaching policy from IAM role "${params.roleName}"`)
|
||||
|
||||
const client = createIAMClient({
|
||||
region: params.region,
|
||||
@@ -38,7 +36,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
|
||||
try {
|
||||
await detachRolePolicy(client, params.roleName, params.policyArn)
|
||||
logger.info(`[${requestId}] Successfully detached policy from IAM role "${params.roleName}"`)
|
||||
logger.info(`Successfully detached policy from IAM role "${params.roleName}"`)
|
||||
return NextResponse.json({
|
||||
message: `Policy "${params.policyArn}" detached from role "${params.roleName}"`,
|
||||
})
|
||||
@@ -47,16 +45,15 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
logger.warn(`Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Failed to detach role policy:`, error)
|
||||
logger.error(`Failed to detach role policy:`, error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to detach role policy: ${errorMessage}` },
|
||||
{ error: `Failed to detach role policy: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -17,8 +17,6 @@ const Schema = z.object({
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
@@ -28,7 +26,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Detaching policy from IAM user "${params.userName}"`)
|
||||
logger.info(`Detaching policy from IAM user "${params.userName}"`)
|
||||
|
||||
const client = createIAMClient({
|
||||
region: params.region,
|
||||
@@ -38,7 +36,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
|
||||
try {
|
||||
await detachUserPolicy(client, params.userName, params.policyArn)
|
||||
logger.info(`[${requestId}] Successfully detached policy from IAM user "${params.userName}"`)
|
||||
logger.info(`Successfully detached policy from IAM user "${params.userName}"`)
|
||||
return NextResponse.json({
|
||||
message: `Policy "${params.policyArn}" detached from user "${params.userName}"`,
|
||||
})
|
||||
@@ -47,16 +45,15 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
logger.warn(`Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Failed to detach user policy:`, error)
|
||||
logger.error(`Failed to detach user policy:`, error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to detach user policy: ${errorMessage}` },
|
||||
{ error: `Failed to detach user policy: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -16,8 +16,6 @@ const Schema = z.object({
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
@@ -27,7 +25,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Getting IAM role "${params.roleName}"`)
|
||||
logger.info(`Getting IAM role "${params.roleName}"`)
|
||||
|
||||
const client = createIAMClient({
|
||||
region: params.region,
|
||||
@@ -37,21 +35,23 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
|
||||
try {
|
||||
const result = await getRole(client, params.roleName)
|
||||
logger.info(`[${requestId}] Successfully retrieved IAM role "${params.roleName}"`)
|
||||
logger.info(`Successfully retrieved IAM role "${params.roleName}"`)
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
logger.warn(`Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Failed to get IAM role:`, error)
|
||||
return NextResponse.json({ error: `Failed to get IAM role: ${errorMessage}` }, { status: 500 })
|
||||
logger.error(`Failed to get IAM role:`, error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to get IAM role: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -12,12 +12,10 @@ const Schema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
userName: z.string().min(1, 'User name is required'),
|
||||
userName: z.string().min(1).optional().nullable(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
@@ -27,7 +25,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Getting IAM user "${params.userName}"`)
|
||||
logger.info(`Getting IAM user "${params.userName}"`)
|
||||
|
||||
const client = createIAMClient({
|
||||
region: params.region,
|
||||
@@ -37,21 +35,23 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
|
||||
try {
|
||||
const result = await getUser(client, params.userName)
|
||||
logger.info(`[${requestId}] Successfully retrieved IAM user "${params.userName}"`)
|
||||
logger.info(`Successfully retrieved IAM user "${params.userName ?? 'caller'}"`)
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
logger.warn(`Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Failed to get IAM user:`, error)
|
||||
return NextResponse.json({ error: `Failed to get IAM user: ${errorMessage}` }, { status: 500 })
|
||||
logger.error(`Failed to get IAM user:`, error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to get IAM user: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createIAMClient, listAttachedRolePolicies } from '../utils'
|
||||
|
||||
const logger = createLogger('IAMListAttachedRolePoliciesAPI')
|
||||
|
||||
const Schema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
roleName: z.string().min(1, 'Role name is required'),
|
||||
pathPrefix: z.string().optional().nullable(),
|
||||
maxItems: z.number().int().min(1).max(1000).optional().nullable(),
|
||||
marker: z.string().optional().nullable(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`Listing policies attached to IAM role "${params.roleName}"`)
|
||||
|
||||
const client = createIAMClient({
|
||||
region: params.region,
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await listAttachedRolePolicies(
|
||||
client,
|
||||
params.roleName,
|
||||
params.pathPrefix,
|
||||
params.maxItems,
|
||||
params.marker
|
||||
)
|
||||
logger.info(`Found ${result.count} policies attached to role "${params.roleName}"`)
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
logger.error(`Failed to list attached role policies:`, error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to list attached role policies: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createIAMClient, listAttachedUserPolicies } from '../utils'
|
||||
|
||||
const logger = createLogger('IAMListAttachedUserPoliciesAPI')
|
||||
|
||||
const Schema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
userName: z.string().min(1, 'User name is required'),
|
||||
pathPrefix: z.string().optional().nullable(),
|
||||
maxItems: z.number().int().min(1).max(1000).optional().nullable(),
|
||||
marker: z.string().optional().nullable(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`Listing policies attached to IAM user "${params.userName}"`)
|
||||
|
||||
const client = createIAMClient({
|
||||
region: params.region,
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await listAttachedUserPolicies(
|
||||
client,
|
||||
params.userName,
|
||||
params.pathPrefix,
|
||||
params.maxItems,
|
||||
params.marker
|
||||
)
|
||||
logger.info(`Found ${result.count} policies attached to user "${params.userName}"`)
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
logger.error(`Failed to list attached user policies:`, error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to list attached user policies: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -12,14 +12,12 @@ const Schema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
pathPrefix: z.string().optional(),
|
||||
maxItems: z.number().min(1).max(1000).optional(),
|
||||
marker: z.string().optional(),
|
||||
pathPrefix: z.string().optional().nullable(),
|
||||
maxItems: z.number().int().min(1).max(1000).optional().nullable(),
|
||||
marker: z.string().optional().nullable(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
@@ -29,7 +27,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Listing IAM groups`)
|
||||
logger.info(`Listing IAM groups`)
|
||||
|
||||
const client = createIAMClient({
|
||||
region: params.region,
|
||||
@@ -39,23 +37,22 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
|
||||
try {
|
||||
const result = await listGroups(client, params.pathPrefix, params.maxItems, params.marker)
|
||||
logger.info(`[${requestId}] Successfully listed ${result.count} IAM groups`)
|
||||
logger.info(`Successfully listed ${result.count} IAM groups`)
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
logger.warn(`Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Failed to list IAM groups:`, error)
|
||||
logger.error(`Failed to list IAM groups:`, error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to list IAM groups: ${errorMessage}` },
|
||||
{ error: `Failed to list IAM groups: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -12,16 +12,14 @@ const Schema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
scope: z.string().optional(),
|
||||
onlyAttached: z.boolean().optional(),
|
||||
pathPrefix: z.string().optional(),
|
||||
maxItems: z.number().min(1).max(1000).optional(),
|
||||
marker: z.string().optional(),
|
||||
scope: z.string().optional().nullable(),
|
||||
onlyAttached: z.boolean().optional().nullable(),
|
||||
pathPrefix: z.string().optional().nullable(),
|
||||
maxItems: z.number().int().min(1).max(1000).optional().nullable(),
|
||||
marker: z.string().optional().nullable(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
@@ -31,7 +29,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Listing IAM policies`)
|
||||
logger.info(`Listing IAM policies`)
|
||||
|
||||
const client = createIAMClient({
|
||||
region: params.region,
|
||||
@@ -48,23 +46,22 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
params.maxItems,
|
||||
params.marker
|
||||
)
|
||||
logger.info(`[${requestId}] Successfully listed ${result.count} IAM policies`)
|
||||
logger.info(`Successfully listed ${result.count} IAM policies`)
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
logger.warn(`Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Failed to list IAM policies:`, error)
|
||||
logger.error(`Failed to list IAM policies:`, error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to list IAM policies: ${errorMessage}` },
|
||||
{ error: `Failed to list IAM policies: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -12,14 +12,12 @@ const Schema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
pathPrefix: z.string().optional(),
|
||||
maxItems: z.number().min(1).max(1000).optional(),
|
||||
marker: z.string().optional(),
|
||||
pathPrefix: z.string().optional().nullable(),
|
||||
maxItems: z.number().int().min(1).max(1000).optional().nullable(),
|
||||
marker: z.string().optional().nullable(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
@@ -29,7 +27,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Listing IAM roles`)
|
||||
logger.info(`Listing IAM roles`)
|
||||
|
||||
const client = createIAMClient({
|
||||
region: params.region,
|
||||
@@ -39,23 +37,22 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
|
||||
try {
|
||||
const result = await listRoles(client, params.pathPrefix, params.maxItems, params.marker)
|
||||
logger.info(`[${requestId}] Successfully listed ${result.count} IAM roles`)
|
||||
logger.info(`Successfully listed ${result.count} IAM roles`)
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
logger.warn(`Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Failed to list IAM roles:`, error)
|
||||
logger.error(`Failed to list IAM roles:`, error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to list IAM roles: ${errorMessage}` },
|
||||
{ error: `Failed to list IAM roles: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -12,14 +12,12 @@ const Schema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
pathPrefix: z.string().optional(),
|
||||
maxItems: z.number().min(1).max(1000).optional(),
|
||||
marker: z.string().optional(),
|
||||
pathPrefix: z.string().optional().nullable(),
|
||||
maxItems: z.number().int().min(1).max(1000).optional().nullable(),
|
||||
marker: z.string().optional().nullable(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
@@ -29,7 +27,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Listing IAM users`)
|
||||
logger.info(`Listing IAM users`)
|
||||
|
||||
const client = createIAMClient({
|
||||
region: params.region,
|
||||
@@ -39,23 +37,22 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
|
||||
try {
|
||||
const result = await listUsers(client, params.pathPrefix, params.maxItems, params.marker)
|
||||
logger.info(`[${requestId}] Successfully listed ${result.count} IAM users`)
|
||||
logger.info(`Successfully listed ${result.count} IAM users`)
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
logger.warn(`Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Failed to list IAM users:`, error)
|
||||
logger.error(`Failed to list IAM users:`, error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to list IAM users: ${errorMessage}` },
|
||||
{ error: `Failed to list IAM users: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -17,8 +17,6 @@ const Schema = z.object({
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
@@ -28,9 +26,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Removing user "${params.userName}" from group "${params.groupName}"`
|
||||
)
|
||||
logger.info(`Removing user "${params.userName}" from group "${params.groupName}"`)
|
||||
|
||||
const client = createIAMClient({
|
||||
region: params.region,
|
||||
@@ -40,9 +36,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
|
||||
try {
|
||||
await removeUserFromGroup(client, params.userName, params.groupName)
|
||||
logger.info(
|
||||
`[${requestId}] Successfully removed user "${params.userName}" from group "${params.groupName}"`
|
||||
)
|
||||
logger.info(`Successfully removed user "${params.userName}" from group "${params.groupName}"`)
|
||||
return NextResponse.json({
|
||||
message: `User "${params.userName}" removed from group "${params.groupName}"`,
|
||||
})
|
||||
@@ -51,16 +45,15 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
logger.warn(`Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Failed to remove user from group:`, error)
|
||||
logger.error(`Failed to remove user from group:`, error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to remove user from group: ${errorMessage}` },
|
||||
{ error: `Failed to remove user from group: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createIAMClient, simulatePrincipalPolicy } from '../utils'
|
||||
|
||||
const logger = createLogger('IAMSimulatePrincipalPolicyAPI')
|
||||
|
||||
const Schema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
policySourceArn: z.string().min(1, 'Policy source ARN is required'),
|
||||
actionNames: z.string().min(1, 'Action names are required'),
|
||||
resourceArns: z.string().optional().nullable(),
|
||||
maxResults: z.number().int().min(1).max(1000).optional().nullable(),
|
||||
marker: z.string().optional().nullable(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(
|
||||
`Simulating principal policy for "${params.policySourceArn}" on actions: ${params.actionNames}`
|
||||
)
|
||||
|
||||
const client = createIAMClient({
|
||||
region: params.region,
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await simulatePrincipalPolicy(
|
||||
client,
|
||||
params.policySourceArn,
|
||||
params.actionNames,
|
||||
params.resourceArns,
|
||||
params.maxResults,
|
||||
params.marker
|
||||
)
|
||||
logger.info(`Simulation complete: ${result.count} results`)
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
logger.error(`Failed to simulate principal policy:`, error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to simulate principal policy: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,4 +1,11 @@
|
||||
import type { Group, Policy, PolicyScopeType, Role, User } from '@aws-sdk/client-iam'
|
||||
import type {
|
||||
AttachedPolicy,
|
||||
Group,
|
||||
Policy,
|
||||
PolicyScopeType,
|
||||
Role,
|
||||
User,
|
||||
} from '@aws-sdk/client-iam'
|
||||
import {
|
||||
AddUserToGroupCommand,
|
||||
AttachRolePolicyCommand,
|
||||
@@ -14,11 +21,14 @@ import {
|
||||
GetRoleCommand,
|
||||
GetUserCommand,
|
||||
IAMClient,
|
||||
ListAttachedRolePoliciesCommand,
|
||||
ListAttachedUserPoliciesCommand,
|
||||
ListGroupsCommand,
|
||||
ListPoliciesCommand,
|
||||
ListRolesCommand,
|
||||
ListUsersCommand,
|
||||
RemoveUserFromGroupCommand,
|
||||
SimulatePrincipalPolicyCommand,
|
||||
} from '@aws-sdk/client-iam'
|
||||
import type { IAMConnectionConfig } from '@/tools/iam/types'
|
||||
|
||||
@@ -62,8 +72,8 @@ export async function listUsers(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUser(client: IAMClient, userName: string) {
|
||||
const command = new GetUserCommand({ UserName: userName })
|
||||
export async function getUser(client: IAMClient, userName?: string | null) {
|
||||
const command = new GetUserCommand(userName ? { UserName: userName } : {})
|
||||
const response = await client.send(command)
|
||||
const user = response.User
|
||||
|
||||
@@ -338,3 +348,106 @@ export async function removeUserFromGroup(client: IAMClient, userName: string, g
|
||||
})
|
||||
await client.send(command)
|
||||
}
|
||||
|
||||
export async function listAttachedRolePolicies(
|
||||
client: IAMClient,
|
||||
roleName: string,
|
||||
pathPrefix?: string | null,
|
||||
maxItems?: number | null,
|
||||
marker?: string | null
|
||||
) {
|
||||
const command = new ListAttachedRolePoliciesCommand({
|
||||
RoleName: roleName,
|
||||
...(pathPrefix ? { PathPrefix: pathPrefix } : {}),
|
||||
...(maxItems ? { MaxItems: maxItems } : {}),
|
||||
...(marker ? { Marker: marker } : {}),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
const attachedPolicies = (response.AttachedPolicies ?? []).map((p: AttachedPolicy) => ({
|
||||
policyName: p.PolicyName ?? '',
|
||||
policyArn: p.PolicyArn ?? '',
|
||||
}))
|
||||
|
||||
return {
|
||||
attachedPolicies,
|
||||
isTruncated: response.IsTruncated ?? false,
|
||||
marker: response.Marker ?? null,
|
||||
count: attachedPolicies.length,
|
||||
}
|
||||
}
|
||||
|
||||
export async function listAttachedUserPolicies(
|
||||
client: IAMClient,
|
||||
userName: string,
|
||||
pathPrefix?: string | null,
|
||||
maxItems?: number | null,
|
||||
marker?: string | null
|
||||
) {
|
||||
const command = new ListAttachedUserPoliciesCommand({
|
||||
UserName: userName,
|
||||
...(pathPrefix ? { PathPrefix: pathPrefix } : {}),
|
||||
...(maxItems ? { MaxItems: maxItems } : {}),
|
||||
...(marker ? { Marker: marker } : {}),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
const attachedPolicies = (response.AttachedPolicies ?? []).map((p: AttachedPolicy) => ({
|
||||
policyName: p.PolicyName ?? '',
|
||||
policyArn: p.PolicyArn ?? '',
|
||||
}))
|
||||
|
||||
return {
|
||||
attachedPolicies,
|
||||
isTruncated: response.IsTruncated ?? false,
|
||||
marker: response.Marker ?? null,
|
||||
count: attachedPolicies.length,
|
||||
}
|
||||
}
|
||||
|
||||
export async function simulatePrincipalPolicy(
|
||||
client: IAMClient,
|
||||
policySourceArn: string,
|
||||
actionNames: string,
|
||||
resourceArns?: string | null,
|
||||
maxResults?: number | null,
|
||||
marker?: string | null
|
||||
) {
|
||||
const actions = actionNames
|
||||
.split(',')
|
||||
.map((a) => a.trim())
|
||||
.filter(Boolean)
|
||||
const resources = resourceArns
|
||||
? resourceArns
|
||||
.split(',')
|
||||
.map((r) => r.trim())
|
||||
.filter(Boolean)
|
||||
: ['*']
|
||||
|
||||
const command = new SimulatePrincipalPolicyCommand({
|
||||
PolicySourceArn: policySourceArn,
|
||||
ActionNames: actions,
|
||||
ResourceArns: resources,
|
||||
...(maxResults ? { MaxItems: maxResults } : {}),
|
||||
...(marker ? { Marker: marker } : {}),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
const evaluationResults = (response.EvaluationResults ?? []).map((r) => ({
|
||||
evalActionName: r.EvalActionName ?? '',
|
||||
evalResourceName: r.EvalResourceName ?? '',
|
||||
evalDecision: r.EvalDecision ?? '',
|
||||
matchedStatements: (r.MatchedStatements ?? []).map((s) => ({
|
||||
sourcePolicyId: s.SourcePolicyId ?? '',
|
||||
sourcePolicyType: s.SourcePolicyType ?? '',
|
||||
})),
|
||||
missingContextValues: (r.MissingContextValues ?? []).map((v) => String(v)),
|
||||
}))
|
||||
|
||||
return {
|
||||
evaluationResults,
|
||||
isTruncated: response.IsTruncated ?? false,
|
||||
marker: response.Marker ?? null,
|
||||
count: evaluationResults.length,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { checkAssignmentDeletionStatus, createSSOAdminClient } from '../utils'
|
||||
|
||||
const logger = createLogger('IdentityCenterCheckAssignmentDeletionStatusAPI')
|
||||
|
||||
const Schema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
instanceArn: z.string().min(1, 'Instance ARN is required'),
|
||||
requestId: z.string().min(1, 'Request ID is required'),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`Checking assignment deletion status for request ${params.requestId}`)
|
||||
|
||||
const client = createSSOAdminClient(params)
|
||||
try {
|
||||
const result = await checkAssignmentDeletionStatus(
|
||||
client,
|
||||
params.instanceArn,
|
||||
params.requestId
|
||||
)
|
||||
logger.info(`Assignment deletion status: ${result.status}`)
|
||||
return NextResponse.json({
|
||||
message: `Assignment deletion status: ${result.status}`,
|
||||
...result,
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
logger.error('Failed to check assignment deletion status:', error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to check assignment deletion status: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { checkAssignmentCreationStatus, createSSOAdminClient } from '../utils'
|
||||
|
||||
const logger = createLogger('IdentityCenterCheckAssignmentStatusAPI')
|
||||
|
||||
const Schema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
instanceArn: z.string().min(1, 'Instance ARN is required'),
|
||||
requestId: z.string().min(1, 'Request ID is required'),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`Checking assignment status for request ${params.requestId}`)
|
||||
|
||||
const client = createSSOAdminClient(params)
|
||||
try {
|
||||
const result = await checkAssignmentCreationStatus(
|
||||
client,
|
||||
params.instanceArn,
|
||||
params.requestId
|
||||
)
|
||||
logger.info(`Assignment status: ${result.status}`)
|
||||
return NextResponse.json({
|
||||
message: `Assignment status: ${result.status}`,
|
||||
...result,
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
logger.error('Failed to check assignment status:', error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to check assignment status: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
CreateAccountAssignmentCommand,
|
||||
type PrincipalType,
|
||||
type TargetType,
|
||||
} from '@aws-sdk/client-sso-admin'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createSSOAdminClient, mapAssignmentStatus } from '../utils'
|
||||
|
||||
const logger = createLogger('IdentityCenterCreateAccountAssignmentAPI')
|
||||
|
||||
const Schema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
instanceArn: z.string().min(1, 'Instance ARN is required'),
|
||||
accountId: z.string().min(1, 'Account ID is required'),
|
||||
permissionSetArn: z.string().min(1, 'Permission set ARN is required'),
|
||||
principalType: z.enum(['USER', 'GROUP']),
|
||||
principalId: z.string().min(1, 'Principal ID is required'),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(
|
||||
`Creating account assignment for ${params.principalType} ${params.principalId} on account ${params.accountId}`
|
||||
)
|
||||
|
||||
const client = createSSOAdminClient(params)
|
||||
try {
|
||||
const command = new CreateAccountAssignmentCommand({
|
||||
InstanceArn: params.instanceArn,
|
||||
TargetId: params.accountId,
|
||||
TargetType: 'AWS_ACCOUNT' as TargetType,
|
||||
PermissionSetArn: params.permissionSetArn,
|
||||
PrincipalType: params.principalType as PrincipalType,
|
||||
PrincipalId: params.principalId,
|
||||
})
|
||||
const response = await client.send(command)
|
||||
const status = response.AccountAssignmentCreationStatus ?? {}
|
||||
const result = mapAssignmentStatus(status)
|
||||
|
||||
logger.info(
|
||||
`Account assignment creation initiated with status ${result.status}, requestId ${result.requestId}`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Account assignment creation ${result.status === 'SUCCEEDED' ? 'succeeded' : 'initiated'}`,
|
||||
...result,
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
logger.error('Failed to create account assignment:', error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to create account assignment: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
DeleteAccountAssignmentCommand,
|
||||
type PrincipalType,
|
||||
type TargetType,
|
||||
} from '@aws-sdk/client-sso-admin'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createSSOAdminClient, mapAssignmentStatus } from '../utils'
|
||||
|
||||
const logger = createLogger('IdentityCenterDeleteAccountAssignmentAPI')
|
||||
|
||||
const Schema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
instanceArn: z.string().min(1, 'Instance ARN is required'),
|
||||
accountId: z.string().min(1, 'Account ID is required'),
|
||||
permissionSetArn: z.string().min(1, 'Permission set ARN is required'),
|
||||
principalType: z.enum(['USER', 'GROUP']),
|
||||
principalId: z.string().min(1, 'Principal ID is required'),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(
|
||||
`Deleting account assignment for ${params.principalType} ${params.principalId} on account ${params.accountId}`
|
||||
)
|
||||
|
||||
const client = createSSOAdminClient(params)
|
||||
try {
|
||||
const command = new DeleteAccountAssignmentCommand({
|
||||
InstanceArn: params.instanceArn,
|
||||
TargetId: params.accountId,
|
||||
TargetType: 'AWS_ACCOUNT' as TargetType,
|
||||
PermissionSetArn: params.permissionSetArn,
|
||||
PrincipalType: params.principalType as PrincipalType,
|
||||
PrincipalId: params.principalId,
|
||||
})
|
||||
const response = await client.send(command)
|
||||
const status = response.AccountAssignmentDeletionStatus ?? {}
|
||||
const result = mapAssignmentStatus(status)
|
||||
|
||||
logger.info(
|
||||
`Account assignment deletion initiated with status ${result.status}, requestId ${result.requestId}`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
message: `Account assignment deletion ${result.status === 'SUCCEEDED' ? 'succeeded' : 'initiated'}`,
|
||||
...result,
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
logger.error('Failed to delete account assignment:', error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to delete account assignment: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createOrganizationsClient, describeAccount } from '../utils'
|
||||
|
||||
const logger = createLogger('IdentityCenterDescribeAccountAPI')
|
||||
|
||||
const Schema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
accountId: z.string().min(12, 'Account ID must be 12 digits').max(12),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`Describing AWS account ${params.accountId}`)
|
||||
|
||||
const client = createOrganizationsClient(params)
|
||||
try {
|
||||
const result = await describeAccount(client, params.accountId)
|
||||
logger.info(`Successfully described account ${result.name}`)
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
logger.error('Failed to describe account:', error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to describe account: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createIdentityStoreClient, getGroupByDisplayName } from '../utils'
|
||||
|
||||
const logger = createLogger('IdentityCenterGetGroupAPI')
|
||||
|
||||
const Schema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
identityStoreId: z.string().min(1, 'Identity Store ID is required'),
|
||||
displayName: z.string().min(1, 'Group display name is required'),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(
|
||||
`Looking up group "${params.displayName}" in identity store ${params.identityStoreId}`
|
||||
)
|
||||
|
||||
const client = createIdentityStoreClient(params)
|
||||
try {
|
||||
const result = await getGroupByDisplayName(client, params.identityStoreId, params.displayName)
|
||||
logger.info(`Successfully found group ${result.groupId}`)
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
logger.error('Failed to get group:', error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to get group: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createIdentityStoreClient, getUserByEmail } from '../utils'
|
||||
|
||||
const logger = createLogger('IdentityCenterGetUserAPI')
|
||||
|
||||
const Schema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
identityStoreId: z.string().min(1, 'Identity Store ID is required'),
|
||||
email: z.string().email('Valid email address is required'),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`Looking up user by email in identity store ${params.identityStoreId}`)
|
||||
|
||||
const client = createIdentityStoreClient(params)
|
||||
try {
|
||||
const result = await getUserByEmail(client, params.identityStoreId, params.email)
|
||||
logger.info(`Successfully found user ${result.userId}`)
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
logger.error('Failed to get user:', error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to get user: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,63 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createSSOAdminClient, listAccountAssignmentsForPrincipal } from '../utils'
|
||||
|
||||
const logger = createLogger('IdentityCenterListAccountAssignmentsAPI')
|
||||
|
||||
const Schema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
instanceArn: z.string().min(1, 'Instance ARN is required'),
|
||||
principalId: z.string().min(1, 'Principal ID is required'),
|
||||
principalType: z.enum(['USER', 'GROUP']),
|
||||
maxResults: z.number().min(1).max(100).optional(),
|
||||
nextToken: z.string().optional(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`Listing account assignments for ${params.principalType} ${params.principalId}`)
|
||||
|
||||
const client = createSSOAdminClient(params)
|
||||
try {
|
||||
const result = await listAccountAssignmentsForPrincipal(
|
||||
client,
|
||||
params.instanceArn,
|
||||
params.principalId,
|
||||
params.principalType,
|
||||
params.maxResults,
|
||||
params.nextToken
|
||||
)
|
||||
logger.info(`Successfully listed ${result.count} account assignments`)
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
logger.error('Failed to list account assignments:', error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to list account assignments: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createOrganizationsClient, listAccounts } from '../utils'
|
||||
|
||||
const logger = createLogger('IdentityCenterListAccountsAPI')
|
||||
|
||||
const Schema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
maxResults: z.number().min(1).max(20).optional(),
|
||||
nextToken: z.string().optional(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info('Listing AWS accounts')
|
||||
|
||||
const client = createOrganizationsClient(params)
|
||||
try {
|
||||
const result = await listAccounts(client, params.maxResults, params.nextToken)
|
||||
logger.info(`Successfully listed ${result.count} accounts`)
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
logger.error('Failed to list AWS accounts:', error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to list AWS accounts: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createIdentityStoreClient, listGroups } from '../utils'
|
||||
|
||||
const logger = createLogger('IdentityCenterListGroupsAPI')
|
||||
|
||||
const Schema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
identityStoreId: z.string().min(1, 'Identity Store ID is required'),
|
||||
maxResults: z.number().min(1).max(100).optional(),
|
||||
nextToken: z.string().optional(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`Listing groups in identity store ${params.identityStoreId}`)
|
||||
|
||||
const client = createIdentityStoreClient(params)
|
||||
try {
|
||||
const result = await listGroups(
|
||||
client,
|
||||
params.identityStoreId,
|
||||
params.maxResults,
|
||||
params.nextToken
|
||||
)
|
||||
logger.info(`Successfully listed ${result.count} groups`)
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
logger.error('Failed to list groups:', error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to list groups: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createSSOAdminClient, listInstances } from '../utils'
|
||||
|
||||
const logger = createLogger('IdentityCenterListInstancesAPI')
|
||||
|
||||
const Schema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
maxResults: z.number().min(1).max(100).optional(),
|
||||
nextToken: z.string().optional(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info('Listing Identity Center instances')
|
||||
|
||||
const client = createSSOAdminClient(params)
|
||||
try {
|
||||
const result = await listInstances(client, params.maxResults, params.nextToken)
|
||||
logger.info(`Successfully listed ${result.count} instances`)
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
logger.error('Failed to list Identity Center instances:', error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to list Identity Center instances: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createSSOAdminClient, listPermissionSets } from '../utils'
|
||||
|
||||
const logger = createLogger('IdentityCenterListPermissionSetsAPI')
|
||||
|
||||
const Schema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
instanceArn: z.string().min(1, 'Instance ARN is required'),
|
||||
maxResults: z.number().min(1).max(100).optional(),
|
||||
nextToken: z.string().optional(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = Schema.parse(body)
|
||||
|
||||
logger.info(`Listing permission sets for instance ${params.instanceArn}`)
|
||||
|
||||
const client = createSSOAdminClient(params)
|
||||
try {
|
||||
const result = await listPermissionSets(
|
||||
client,
|
||||
params.instanceArn,
|
||||
params.maxResults,
|
||||
params.nextToken
|
||||
)
|
||||
logger.info(`Successfully listed ${result.count} permission sets`)
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
logger.error('Failed to list permission sets:', error)
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to list permission sets: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,304 @@
|
||||
import {
|
||||
DescribeGroupCommand,
|
||||
DescribeUserCommand,
|
||||
GetGroupIdCommand,
|
||||
GetUserIdCommand,
|
||||
IdentitystoreClient,
|
||||
ListGroupsCommand,
|
||||
} from '@aws-sdk/client-identitystore'
|
||||
import {
|
||||
DescribeAccountCommand,
|
||||
ListAccountsCommand,
|
||||
OrganizationsClient,
|
||||
} from '@aws-sdk/client-organizations'
|
||||
import {
|
||||
type AccountAssignmentOperationStatus,
|
||||
DescribeAccountAssignmentCreationStatusCommand,
|
||||
DescribeAccountAssignmentDeletionStatusCommand,
|
||||
DescribePermissionSetCommand,
|
||||
ListAccountAssignmentsForPrincipalCommand,
|
||||
ListInstancesCommand,
|
||||
ListPermissionSetsCommand,
|
||||
type PrincipalType,
|
||||
SSOAdminClient,
|
||||
} from '@aws-sdk/client-sso-admin'
|
||||
import type { IdentityCenterConnectionConfig } from '@/tools/identity_center/types'
|
||||
|
||||
export function createSSOAdminClient(config: IdentityCenterConnectionConfig): SSOAdminClient {
|
||||
return new SSOAdminClient({
|
||||
region: config.region,
|
||||
credentials: {
|
||||
accessKeyId: config.accessKeyId,
|
||||
secretAccessKey: config.secretAccessKey,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function createIdentityStoreClient(
|
||||
config: IdentityCenterConnectionConfig
|
||||
): IdentitystoreClient {
|
||||
return new IdentitystoreClient({
|
||||
region: config.region,
|
||||
credentials: {
|
||||
accessKeyId: config.accessKeyId,
|
||||
secretAccessKey: config.secretAccessKey,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function createOrganizationsClient(config: IdentityCenterConnectionConfig) {
|
||||
return new OrganizationsClient({
|
||||
region: 'us-east-1', // Organizations API only available in us-east-1
|
||||
credentials: {
|
||||
accessKeyId: config.accessKeyId,
|
||||
secretAccessKey: config.secretAccessKey,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function listInstances(
|
||||
client: SSOAdminClient,
|
||||
maxResults?: number | null,
|
||||
nextToken?: string | null
|
||||
) {
|
||||
const command = new ListInstancesCommand({
|
||||
...(maxResults ? { MaxResults: maxResults } : {}),
|
||||
...(nextToken ? { NextToken: nextToken } : {}),
|
||||
})
|
||||
const response = await client.send(command)
|
||||
const instances = (response.Instances ?? []).map((instance) => ({
|
||||
instanceArn: instance.InstanceArn ?? '',
|
||||
identityStoreId: instance.IdentityStoreId ?? '',
|
||||
name: instance.Name ?? null,
|
||||
status: instance.Status ?? '',
|
||||
statusReason: instance.StatusReason ?? null,
|
||||
ownerAccountId: instance.OwnerAccountId ?? null,
|
||||
createdDate: instance.CreatedDate?.toISOString() ?? null,
|
||||
}))
|
||||
return { instances, nextToken: response.NextToken ?? null, count: instances.length }
|
||||
}
|
||||
|
||||
export async function listAccounts(
|
||||
client: OrganizationsClient,
|
||||
maxResults?: number | null,
|
||||
nextToken?: string | null
|
||||
) {
|
||||
const command = new ListAccountsCommand({
|
||||
...(maxResults ? { MaxResults: maxResults } : {}),
|
||||
...(nextToken ? { NextToken: nextToken } : {}),
|
||||
})
|
||||
const response = await client.send(command)
|
||||
const accounts = (response.Accounts ?? []).map((account) => ({
|
||||
id: account.Id ?? '',
|
||||
arn: account.Arn ?? '',
|
||||
name: account.Name ?? '',
|
||||
email: account.Email ?? '',
|
||||
status: account.State ?? '',
|
||||
joinedTimestamp: account.JoinedTimestamp?.toISOString() ?? null,
|
||||
}))
|
||||
return { accounts, nextToken: response.NextToken ?? null, count: accounts.length }
|
||||
}
|
||||
|
||||
export async function listPermissionSets(
|
||||
client: SSOAdminClient,
|
||||
instanceArn: string,
|
||||
maxResults?: number | null,
|
||||
nextToken?: string | null
|
||||
) {
|
||||
const listCommand = new ListPermissionSetsCommand({
|
||||
InstanceArn: instanceArn,
|
||||
...(maxResults ? { MaxResults: maxResults } : {}),
|
||||
...(nextToken ? { NextToken: nextToken } : {}),
|
||||
})
|
||||
const listResponse = await client.send(listCommand)
|
||||
const permissionSetArns = listResponse.PermissionSets ?? []
|
||||
|
||||
const permissionSets = await Promise.all(
|
||||
permissionSetArns.map(async (arn) => {
|
||||
const describeCommand = new DescribePermissionSetCommand({
|
||||
InstanceArn: instanceArn,
|
||||
PermissionSetArn: arn,
|
||||
})
|
||||
const describeResponse = await client.send(describeCommand)
|
||||
const ps = describeResponse.PermissionSet
|
||||
return {
|
||||
permissionSetArn: ps?.PermissionSetArn ?? arn,
|
||||
name: ps?.Name ?? '',
|
||||
description: ps?.Description ?? null,
|
||||
sessionDuration: ps?.SessionDuration ?? null,
|
||||
createdDate: ps?.CreatedDate?.toISOString() ?? null,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
permissionSets,
|
||||
nextToken: listResponse.NextToken ?? null,
|
||||
count: permissionSets.length,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserByEmail(
|
||||
ssoClient: IdentitystoreClient,
|
||||
identityStoreId: string,
|
||||
email: string
|
||||
) {
|
||||
const getUserIdCommand = new GetUserIdCommand({
|
||||
IdentityStoreId: identityStoreId,
|
||||
AlternateIdentifier: {
|
||||
UniqueAttribute: {
|
||||
AttributePath: 'emails.value',
|
||||
AttributeValue: email,
|
||||
},
|
||||
},
|
||||
})
|
||||
const getUserIdResponse = await ssoClient.send(getUserIdCommand)
|
||||
const userId = getUserIdResponse.UserId ?? ''
|
||||
|
||||
const describeCommand = new DescribeUserCommand({
|
||||
IdentityStoreId: identityStoreId,
|
||||
UserId: userId,
|
||||
})
|
||||
const describeResponse = await ssoClient.send(describeCommand)
|
||||
|
||||
const primaryEmail =
|
||||
describeResponse.Emails?.find((e) => e.Primary)?.Value ??
|
||||
describeResponse.Emails?.[0]?.Value ??
|
||||
null
|
||||
|
||||
return {
|
||||
userId,
|
||||
userName: describeResponse.UserName ?? '',
|
||||
displayName: describeResponse.DisplayName ?? null,
|
||||
email: primaryEmail,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapAssignmentStatus(status: AccountAssignmentOperationStatus) {
|
||||
return {
|
||||
status: status.Status ?? '',
|
||||
requestId: status.RequestId ?? '',
|
||||
accountId: status.TargetId ?? null,
|
||||
permissionSetArn: status.PermissionSetArn ?? null,
|
||||
principalType: status.PrincipalType ?? null,
|
||||
principalId: status.PrincipalId ?? null,
|
||||
failureReason: status.FailureReason ?? null,
|
||||
createdDate: status.CreatedDate?.toISOString() ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkAssignmentCreationStatus(
|
||||
client: SSOAdminClient,
|
||||
instanceArn: string,
|
||||
requestId: string
|
||||
) {
|
||||
const command = new DescribeAccountAssignmentCreationStatusCommand({
|
||||
InstanceArn: instanceArn,
|
||||
AccountAssignmentCreationRequestId: requestId,
|
||||
})
|
||||
const response = await client.send(command)
|
||||
return mapAssignmentStatus(response.AccountAssignmentCreationStatus ?? {})
|
||||
}
|
||||
|
||||
export async function checkAssignmentDeletionStatus(
|
||||
client: SSOAdminClient,
|
||||
instanceArn: string,
|
||||
requestId: string
|
||||
) {
|
||||
const command = new DescribeAccountAssignmentDeletionStatusCommand({
|
||||
InstanceArn: instanceArn,
|
||||
AccountAssignmentDeletionRequestId: requestId,
|
||||
})
|
||||
const response = await client.send(command)
|
||||
return mapAssignmentStatus(response.AccountAssignmentDeletionStatus ?? {})
|
||||
}
|
||||
|
||||
export async function listGroups(
|
||||
client: IdentitystoreClient,
|
||||
identityStoreId: string,
|
||||
maxResults?: number | null,
|
||||
nextToken?: string | null
|
||||
) {
|
||||
const command = new ListGroupsCommand({
|
||||
IdentityStoreId: identityStoreId,
|
||||
...(maxResults ? { MaxResults: maxResults } : {}),
|
||||
...(nextToken ? { NextToken: nextToken } : {}),
|
||||
})
|
||||
const response = await client.send(command)
|
||||
const groups = (response.Groups ?? []).map((group) => ({
|
||||
groupId: group.GroupId ?? '',
|
||||
displayName: group.DisplayName ?? null,
|
||||
description: group.Description ?? null,
|
||||
externalIds: group.ExternalIds?.map((e) => ({ issuer: e.Issuer ?? '', id: e.Id ?? '' })) ?? [],
|
||||
}))
|
||||
return { groups, nextToken: response.NextToken ?? null, count: groups.length }
|
||||
}
|
||||
|
||||
export async function getGroupByDisplayName(
|
||||
client: IdentitystoreClient,
|
||||
identityStoreId: string,
|
||||
displayName: string
|
||||
) {
|
||||
const getGroupIdCommand = new GetGroupIdCommand({
|
||||
IdentityStoreId: identityStoreId,
|
||||
AlternateIdentifier: {
|
||||
UniqueAttribute: {
|
||||
AttributePath: 'displayName',
|
||||
AttributeValue: displayName,
|
||||
},
|
||||
},
|
||||
})
|
||||
const getGroupIdResponse = await client.send(getGroupIdCommand)
|
||||
const groupId = getGroupIdResponse.GroupId ?? ''
|
||||
|
||||
const describeCommand = new DescribeGroupCommand({
|
||||
IdentityStoreId: identityStoreId,
|
||||
GroupId: groupId,
|
||||
})
|
||||
const describeResponse = await client.send(describeCommand)
|
||||
|
||||
return {
|
||||
groupId,
|
||||
displayName: describeResponse.DisplayName ?? null,
|
||||
description: describeResponse.Description ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
export async function describeAccount(client: OrganizationsClient, accountId: string) {
|
||||
const command = new DescribeAccountCommand({ AccountId: accountId })
|
||||
const response = await client.send(command)
|
||||
const account = response.Account
|
||||
return {
|
||||
id: account?.Id ?? '',
|
||||
arn: account?.Arn ?? '',
|
||||
name: account?.Name ?? '',
|
||||
email: account?.Email ?? '',
|
||||
status: account?.State ?? '',
|
||||
joinedTimestamp: account?.JoinedTimestamp?.toISOString() ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
export async function listAccountAssignmentsForPrincipal(
|
||||
client: SSOAdminClient,
|
||||
instanceArn: string,
|
||||
principalId: string,
|
||||
principalType: string,
|
||||
maxResults?: number | null,
|
||||
nextToken?: string | null
|
||||
) {
|
||||
const command = new ListAccountAssignmentsForPrincipalCommand({
|
||||
InstanceArn: instanceArn,
|
||||
PrincipalId: principalId,
|
||||
PrincipalType: principalType as PrincipalType,
|
||||
...(maxResults ? { MaxResults: maxResults } : {}),
|
||||
...(nextToken ? { NextToken: nextToken } : {}),
|
||||
})
|
||||
const response = await client.send(command)
|
||||
const assignments = (response.AccountAssignments ?? []).map((a) => ({
|
||||
accountId: a.AccountId ?? '',
|
||||
permissionSetArn: a.PermissionSetArn ?? '',
|
||||
principalType: a.PrincipalType ?? '',
|
||||
principalId: a.PrincipalId ?? '',
|
||||
}))
|
||||
return { assignments, nextToken: response.NextToken ?? null, count: assignments.length }
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createSESClient, createTemplate } from '../utils'
|
||||
|
||||
const logger = createLogger('SESCreateTemplateAPI')
|
||||
|
||||
const CreateTemplateSchema = z
|
||||
.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
templateName: z.string().min(1, 'Template name is required'),
|
||||
subjectPart: z.string().min(1, 'Subject is required'),
|
||||
textPart: z.string().nullish(),
|
||||
htmlPart: z.string().nullish(),
|
||||
})
|
||||
.refine((data) => data.textPart || data.htmlPart, {
|
||||
message: 'At least one of textPart or htmlPart is required',
|
||||
path: ['textPart'],
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = CreateTemplateSchema.parse(body)
|
||||
|
||||
logger.info(`Creating SES template '${params.templateName}'`)
|
||||
|
||||
const client = createSESClient({
|
||||
region: params.region,
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await createTemplate(client, {
|
||||
templateName: params.templateName,
|
||||
subjectPart: params.subjectPart,
|
||||
textPart: params.textPart,
|
||||
htmlPart: params.htmlPart,
|
||||
})
|
||||
|
||||
logger.info(`Template '${params.templateName}' created successfully`)
|
||||
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.error('Failed to create template:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to create template: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createSESClient, deleteTemplate } from '../utils'
|
||||
|
||||
const logger = createLogger('SESDeleteTemplateAPI')
|
||||
|
||||
const DeleteTemplateSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
templateName: z.string().min(1, 'Template name is required'),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = DeleteTemplateSchema.parse(body)
|
||||
|
||||
logger.info(`Deleting SES template '${params.templateName}'`)
|
||||
|
||||
const client = createSESClient({
|
||||
region: params.region,
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await deleteTemplate(client, params.templateName)
|
||||
|
||||
logger.info(`Template '${params.templateName}' deleted successfully`)
|
||||
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.error('Failed to delete template:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to delete template: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createSESClient, getAccount } from '../utils'
|
||||
|
||||
const logger = createLogger('SESGetAccountAPI')
|
||||
|
||||
const GetAccountSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = GetAccountSchema.parse(body)
|
||||
|
||||
logger.info('Getting SES account information')
|
||||
|
||||
const client = createSESClient({
|
||||
region: params.region,
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await getAccount(client)
|
||||
|
||||
logger.info('SES account info retrieved successfully')
|
||||
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.error('Failed to get account information:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to get account information: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createSESClient, getTemplate } from '../utils'
|
||||
|
||||
const logger = createLogger('SESGetTemplateAPI')
|
||||
|
||||
const GetTemplateSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
templateName: z.string().min(1, 'Template name is required'),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = GetTemplateSchema.parse(body)
|
||||
|
||||
logger.info(`Getting SES template '${params.templateName}'`)
|
||||
|
||||
const client = createSESClient({
|
||||
region: params.region,
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await getTemplate(client, params.templateName)
|
||||
|
||||
logger.info(`Template '${params.templateName}' retrieved successfully`)
|
||||
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.error('Failed to get template:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to get template: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,65 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createSESClient, listIdentities } from '../utils'
|
||||
|
||||
const logger = createLogger('SESListIdentitiesAPI')
|
||||
|
||||
const ListIdentitiesSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
pageSize: z.number().int().min(0).max(1000).nullish(),
|
||||
nextToken: z.string().nullish(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = ListIdentitiesSchema.parse(body)
|
||||
|
||||
logger.info('Listing SES email identities')
|
||||
|
||||
const client = createSESClient({
|
||||
region: params.region,
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await listIdentities(client, {
|
||||
pageSize: params.pageSize,
|
||||
nextToken: params.nextToken,
|
||||
})
|
||||
|
||||
logger.info(`Listed ${result.count} identities`)
|
||||
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.error('Failed to list identities:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to list identities: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,65 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createSESClient, listTemplates } from '../utils'
|
||||
|
||||
const logger = createLogger('SESListTemplatesAPI')
|
||||
|
||||
const ListTemplatesSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
pageSize: z.number().int().min(1).max(100).nullish(),
|
||||
nextToken: z.string().nullish(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = ListTemplatesSchema.parse(body)
|
||||
|
||||
logger.info('Listing SES email templates')
|
||||
|
||||
const client = createSESClient({
|
||||
region: params.region,
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await listTemplates(client, {
|
||||
pageSize: params.pageSize,
|
||||
nextToken: params.nextToken,
|
||||
})
|
||||
|
||||
logger.info(`Listed ${result.count} templates`)
|
||||
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.error('Failed to list templates:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to list templates: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,91 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createSESClient, sendBulkEmail } from '../utils'
|
||||
|
||||
const logger = createLogger('SESSendBulkEmailAPI')
|
||||
|
||||
const DestinationSchema = z.object({
|
||||
toAddresses: z.array(z.string().email()),
|
||||
templateData: z.string().optional(),
|
||||
})
|
||||
|
||||
const SendBulkEmailSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
fromAddress: z.string().email('Valid sender email address is required'),
|
||||
templateName: z.string().min(1, 'Template name is required'),
|
||||
destinations: z.string().min(1, 'Destinations JSON array is required'),
|
||||
defaultTemplateData: z.string().nullish(),
|
||||
configurationSetName: z.string().nullish(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = SendBulkEmailSchema.parse(body)
|
||||
|
||||
let destinations: Array<{ toAddresses: string[]; templateData?: string }>
|
||||
try {
|
||||
const parsed = JSON.parse(params.destinations)
|
||||
destinations = z.array(DestinationSchema).parse(parsed)
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: 'destinations must be a valid JSON array of destination objects' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Sending bulk email from ${params.fromAddress} to ${destinations.length} destination(s) using template '${params.templateName}'`
|
||||
)
|
||||
|
||||
const client = createSESClient({
|
||||
region: params.region,
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await sendBulkEmail(client, {
|
||||
fromAddress: params.fromAddress,
|
||||
templateName: params.templateName,
|
||||
destinations,
|
||||
defaultTemplateData: params.defaultTemplateData,
|
||||
configurationSetName: params.configurationSetName,
|
||||
})
|
||||
|
||||
logger.info(
|
||||
`Bulk email sent: ${result.successCount} succeeded, ${result.failureCount} failed`
|
||||
)
|
||||
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.error('Failed to send bulk email:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to send bulk email: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,104 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createSESClient, sendEmail } from '../utils'
|
||||
|
||||
const logger = createLogger('SESSendEmailAPI')
|
||||
|
||||
const SendEmailSchema = z
|
||||
.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
fromAddress: z.string().email('Valid sender email address is required'),
|
||||
toAddresses: z.string().min(1, 'At least one recipient address is required'),
|
||||
subject: z.string().min(1, 'Email subject is required'),
|
||||
bodyText: z.string().nullish(),
|
||||
bodyHtml: z.string().nullish(),
|
||||
ccAddresses: z.string().nullish(),
|
||||
bccAddresses: z.string().nullish(),
|
||||
replyToAddresses: z.string().nullish(),
|
||||
configurationSetName: z.string().nullish(),
|
||||
})
|
||||
.refine((data) => data.bodyText || data.bodyHtml, {
|
||||
message: 'At least one of bodyText or bodyHtml is required',
|
||||
path: ['bodyText'],
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = SendEmailSchema.parse(body)
|
||||
|
||||
const toList = params.toAddresses
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
logger.info(`Sending email from ${params.fromAddress} to ${toList.length} recipient(s)`)
|
||||
|
||||
const client = createSESClient({
|
||||
region: params.region,
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await sendEmail(client, {
|
||||
fromAddress: params.fromAddress,
|
||||
toAddresses: toList,
|
||||
subject: params.subject,
|
||||
bodyText: params.bodyText,
|
||||
bodyHtml: params.bodyHtml,
|
||||
ccAddresses: params.ccAddresses
|
||||
? params.ccAddresses
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: null,
|
||||
bccAddresses: params.bccAddresses
|
||||
? params.bccAddresses
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: null,
|
||||
replyToAddresses: params.replyToAddresses
|
||||
? params.replyToAddresses
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: null,
|
||||
configurationSetName: params.configurationSetName,
|
||||
})
|
||||
|
||||
logger.info(`Email sent successfully, messageId: ${result.messageId}`)
|
||||
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.error('Failed to send email:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to send email: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,92 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { createSESClient, sendTemplatedEmail } from '../utils'
|
||||
|
||||
const logger = createLogger('SESSendTemplatedEmailAPI')
|
||||
|
||||
const SendTemplatedEmailSchema = z.object({
|
||||
region: z.string().min(1, 'AWS region is required'),
|
||||
accessKeyId: z.string().min(1, 'AWS access key ID is required'),
|
||||
secretAccessKey: z.string().min(1, 'AWS secret access key is required'),
|
||||
fromAddress: z.string().email('Valid sender email address is required'),
|
||||
toAddresses: z.string().min(1, 'At least one recipient address is required'),
|
||||
templateName: z.string().min(1, 'Template name is required'),
|
||||
templateData: z.string().min(1, 'Template data is required'),
|
||||
ccAddresses: z.string().nullish(),
|
||||
bccAddresses: z.string().nullish(),
|
||||
configurationSetName: z.string().nullish(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const params = SendTemplatedEmailSchema.parse(body)
|
||||
|
||||
const toList = params.toAddresses
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
logger.info(
|
||||
`Sending templated email from ${params.fromAddress} using template '${params.templateName}'`
|
||||
)
|
||||
|
||||
const client = createSESClient({
|
||||
region: params.region,
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await sendTemplatedEmail(client, {
|
||||
fromAddress: params.fromAddress,
|
||||
toAddresses: toList,
|
||||
templateName: params.templateName,
|
||||
templateData: params.templateData,
|
||||
ccAddresses: params.ccAddresses
|
||||
? params.ccAddresses
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: null,
|
||||
bccAddresses: params.bccAddresses
|
||||
? params.bccAddresses
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: null,
|
||||
configurationSetName: params.configurationSetName,
|
||||
})
|
||||
|
||||
logger.info(`Templated email sent successfully, messageId: ${result.messageId}`)
|
||||
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.error('Failed to send templated email:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to send templated email: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,257 @@
|
||||
import {
|
||||
CreateEmailTemplateCommand,
|
||||
DeleteEmailTemplateCommand,
|
||||
GetAccountCommand,
|
||||
GetEmailTemplateCommand,
|
||||
ListEmailIdentitiesCommand,
|
||||
ListEmailTemplatesCommand,
|
||||
SESv2Client,
|
||||
SendBulkEmailCommand,
|
||||
SendEmailCommand,
|
||||
} from '@aws-sdk/client-sesv2'
|
||||
import type { SESConnectionConfig } from '@/tools/ses/types'
|
||||
|
||||
export function createSESClient(config: SESConnectionConfig): SESv2Client {
|
||||
return new SESv2Client({
|
||||
region: config.region,
|
||||
credentials: {
|
||||
accessKeyId: config.accessKeyId,
|
||||
secretAccessKey: config.secretAccessKey,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function sendEmail(
|
||||
client: SESv2Client,
|
||||
params: {
|
||||
fromAddress: string
|
||||
toAddresses: string[]
|
||||
subject: string
|
||||
bodyText?: string | null
|
||||
bodyHtml?: string | null
|
||||
ccAddresses?: string[] | null
|
||||
bccAddresses?: string[] | null
|
||||
replyToAddresses?: string[] | null
|
||||
configurationSetName?: string | null
|
||||
}
|
||||
) {
|
||||
const command = new SendEmailCommand({
|
||||
FromEmailAddress: params.fromAddress,
|
||||
Destination: {
|
||||
ToAddresses: params.toAddresses,
|
||||
...(params.ccAddresses?.length ? { CcAddresses: params.ccAddresses } : {}),
|
||||
...(params.bccAddresses?.length ? { BccAddresses: params.bccAddresses } : {}),
|
||||
},
|
||||
Content: {
|
||||
Simple: {
|
||||
Subject: { Data: params.subject },
|
||||
Body: {
|
||||
...(params.bodyText ? { Text: { Data: params.bodyText } } : {}),
|
||||
...(params.bodyHtml ? { Html: { Data: params.bodyHtml } } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
...(params.replyToAddresses?.length ? { ReplyToAddresses: params.replyToAddresses } : {}),
|
||||
...(params.configurationSetName ? { ConfigurationSetName: params.configurationSetName } : {}),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
return {
|
||||
messageId: response.MessageId ?? '',
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendTemplatedEmail(
|
||||
client: SESv2Client,
|
||||
params: {
|
||||
fromAddress: string
|
||||
toAddresses: string[]
|
||||
templateName: string
|
||||
templateData: string
|
||||
ccAddresses?: string[] | null
|
||||
bccAddresses?: string[] | null
|
||||
configurationSetName?: string | null
|
||||
}
|
||||
) {
|
||||
const command = new SendEmailCommand({
|
||||
FromEmailAddress: params.fromAddress,
|
||||
Destination: {
|
||||
ToAddresses: params.toAddresses,
|
||||
...(params.ccAddresses?.length ? { CcAddresses: params.ccAddresses } : {}),
|
||||
...(params.bccAddresses?.length ? { BccAddresses: params.bccAddresses } : {}),
|
||||
},
|
||||
Content: {
|
||||
Template: {
|
||||
TemplateName: params.templateName,
|
||||
TemplateData: params.templateData,
|
||||
},
|
||||
},
|
||||
...(params.configurationSetName ? { ConfigurationSetName: params.configurationSetName } : {}),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
return {
|
||||
messageId: response.MessageId ?? '',
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendBulkEmail(
|
||||
client: SESv2Client,
|
||||
params: {
|
||||
fromAddress: string
|
||||
templateName: string
|
||||
destinations: Array<{ toAddresses: string[]; templateData?: string }>
|
||||
defaultTemplateData?: string | null
|
||||
configurationSetName?: string | null
|
||||
}
|
||||
) {
|
||||
const command = new SendBulkEmailCommand({
|
||||
FromEmailAddress: params.fromAddress,
|
||||
DefaultContent: {
|
||||
Template: {
|
||||
TemplateName: params.templateName,
|
||||
...(params.defaultTemplateData ? { TemplateData: params.defaultTemplateData } : {}),
|
||||
},
|
||||
},
|
||||
BulkEmailEntries: params.destinations.map((dest) => ({
|
||||
Destination: { ToAddresses: dest.toAddresses },
|
||||
...(dest.templateData
|
||||
? {
|
||||
ReplacementEmailContent: {
|
||||
ReplacementTemplate: {
|
||||
ReplacementTemplateData: dest.templateData,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
})),
|
||||
...(params.configurationSetName ? { ConfigurationSetName: params.configurationSetName } : {}),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
const results = (response.BulkEmailEntryResults ?? []).map((r) => ({
|
||||
messageId: r.MessageId ?? null,
|
||||
status: r.Status ?? 'UNKNOWN',
|
||||
error: r.Error ?? null,
|
||||
}))
|
||||
|
||||
const successCount = results.filter((r) => r.status === 'SUCCESS').length
|
||||
const failureCount = results.length - successCount
|
||||
|
||||
return { results, successCount, failureCount }
|
||||
}
|
||||
|
||||
export async function listIdentities(
|
||||
client: SESv2Client,
|
||||
params: {
|
||||
pageSize?: number | null
|
||||
nextToken?: string | null
|
||||
}
|
||||
) {
|
||||
const command = new ListEmailIdentitiesCommand({
|
||||
...(params.pageSize != null ? { PageSize: params.pageSize } : {}),
|
||||
...(params.nextToken ? { NextToken: params.nextToken } : {}),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
const identities = (response.EmailIdentities ?? []).map((identity) => ({
|
||||
identityName: identity.IdentityName ?? '',
|
||||
identityType: identity.IdentityType ?? '',
|
||||
sendingEnabled: identity.SendingEnabled ?? false,
|
||||
verificationStatus: identity.VerificationStatus ?? '',
|
||||
}))
|
||||
|
||||
return {
|
||||
identities,
|
||||
nextToken: response.NextToken ?? null,
|
||||
count: identities.length,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAccount(client: SESv2Client) {
|
||||
const command = new GetAccountCommand({})
|
||||
const response = await client.send(command)
|
||||
|
||||
return {
|
||||
sendingEnabled: response.SendingEnabled ?? false,
|
||||
max24HourSend: response.SendQuota?.Max24HourSend ?? 0,
|
||||
maxSendRate: response.SendQuota?.MaxSendRate ?? 0,
|
||||
sentLast24Hours: response.SendQuota?.SentLast24Hours ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
export async function createTemplate(
|
||||
client: SESv2Client,
|
||||
params: {
|
||||
templateName: string
|
||||
subjectPart: string
|
||||
textPart?: string | null
|
||||
htmlPart?: string | null
|
||||
}
|
||||
) {
|
||||
const command = new CreateEmailTemplateCommand({
|
||||
TemplateName: params.templateName,
|
||||
TemplateContent: {
|
||||
Subject: params.subjectPart,
|
||||
...(params.textPart ? { Text: params.textPart } : {}),
|
||||
...(params.htmlPart ? { Html: params.htmlPart } : {}),
|
||||
},
|
||||
})
|
||||
|
||||
await client.send(command)
|
||||
|
||||
return {
|
||||
message: `Template '${params.templateName}' created successfully`,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTemplate(client: SESv2Client, templateName: string) {
|
||||
const command = new GetEmailTemplateCommand({ TemplateName: templateName })
|
||||
const response = await client.send(command)
|
||||
|
||||
return {
|
||||
templateName: response.TemplateName ?? '',
|
||||
subjectPart: response.TemplateContent?.Subject ?? '',
|
||||
textPart: response.TemplateContent?.Text ?? null,
|
||||
htmlPart: response.TemplateContent?.Html ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
export async function listTemplates(
|
||||
client: SESv2Client,
|
||||
params: {
|
||||
pageSize?: number | null
|
||||
nextToken?: string | null
|
||||
}
|
||||
) {
|
||||
const command = new ListEmailTemplatesCommand({
|
||||
...(params.pageSize != null ? { PageSize: params.pageSize } : {}),
|
||||
...(params.nextToken ? { NextToken: params.nextToken } : {}),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
const templates = (response.TemplatesMetadata ?? []).map((t) => ({
|
||||
templateName: t.TemplateName ?? '',
|
||||
createdTimestamp: t.CreatedTimestamp?.toISOString() ?? null,
|
||||
}))
|
||||
|
||||
return {
|
||||
templates,
|
||||
nextToken: response.NextToken ?? null,
|
||||
count: templates.length,
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteTemplate(client: SESv2Client, templateName: string) {
|
||||
const command = new DeleteEmailTemplateCommand({ TemplateName: templateName })
|
||||
await client.send(command)
|
||||
|
||||
return {
|
||||
message: `Template '${templateName}' deleted successfully`,
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -15,14 +15,13 @@ const AssumeRoleSchema = z.object({
|
||||
roleArn: z.string().min(1, 'Role ARN is required'),
|
||||
roleSessionName: z.string().min(1, 'Role session name is required'),
|
||||
durationSeconds: z.number().int().min(900).max(43200).nullish(),
|
||||
policy: z.string().max(2048).nullish(),
|
||||
externalId: z.string().nullish(),
|
||||
serialNumber: z.string().nullish(),
|
||||
tokenCode: z.string().nullish(),
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
@@ -32,7 +31,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const params = AssumeRoleSchema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Assuming role ${params.roleArn}`)
|
||||
logger.info(`Assuming role ${params.roleArn}`)
|
||||
|
||||
const client = createSTSClient({
|
||||
region: params.region,
|
||||
@@ -46,12 +45,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
params.roleArn,
|
||||
params.roleSessionName,
|
||||
params.durationSeconds,
|
||||
params.policy,
|
||||
params.externalId,
|
||||
params.serialNumber,
|
||||
params.tokenCode
|
||||
)
|
||||
|
||||
logger.info(`[${requestId}] Role assumed successfully`)
|
||||
logger.info('Role assumed successfully')
|
||||
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
@@ -59,16 +59,18 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Failed to assume role:`, error)
|
||||
logger.error('Failed to assume role', { error: toError(error).message })
|
||||
|
||||
return NextResponse.json({ error: `Failed to assume role: ${errorMessage}` }, { status: 500 })
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to assume role: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -16,8 +16,6 @@ const GetAccessKeyInfoSchema = z.object({
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
@@ -27,7 +25,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const params = GetAccessKeyInfoSchema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Getting access key info for ${params.targetAccessKeyId}`)
|
||||
logger.info(`Getting access key info for ${params.targetAccessKeyId}`)
|
||||
|
||||
const client = createSTSClient({
|
||||
region: params.region,
|
||||
@@ -38,7 +36,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const result = await getAccessKeyInfo(client, params.targetAccessKeyId)
|
||||
|
||||
logger.info(`[${requestId}] Access key info retrieved successfully`)
|
||||
logger.info('Access key info retrieved successfully')
|
||||
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
@@ -46,18 +44,17 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Failed to get access key info:`, error)
|
||||
logger.error('Failed to get access key info', { error: toError(error).message })
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to get access key info: ${errorMessage}` },
|
||||
{ error: `Failed to get access key info: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -15,8 +15,6 @@ const GetCallerIdentitySchema = z.object({
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
@@ -26,7 +24,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const params = GetCallerIdentitySchema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Getting caller identity`)
|
||||
logger.info('Getting caller identity')
|
||||
|
||||
const client = createSTSClient({
|
||||
region: params.region,
|
||||
@@ -37,7 +35,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const result = await getCallerIdentity(client)
|
||||
|
||||
logger.info(`[${requestId}] Caller identity retrieved successfully`)
|
||||
logger.info('Caller identity retrieved successfully')
|
||||
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
@@ -45,18 +43,17 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Failed to get caller identity:`, error)
|
||||
logger.error('Failed to get caller identity', { error: toError(error).message })
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to get caller identity: ${errorMessage}` },
|
||||
{ error: `Failed to get caller identity: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
@@ -18,8 +18,6 @@ const GetSessionTokenSchema = z.object({
|
||||
})
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
@@ -29,7 +27,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
const body = await request.json()
|
||||
const params = GetSessionTokenSchema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Getting session token`)
|
||||
logger.info('Getting session token')
|
||||
|
||||
const client = createSTSClient({
|
||||
region: params.region,
|
||||
@@ -45,7 +43,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
params.tokenCode
|
||||
)
|
||||
|
||||
logger.info(`[${requestId}] Session token retrieved successfully`)
|
||||
logger.info('Session token retrieved successfully')
|
||||
|
||||
return NextResponse.json(result)
|
||||
} finally {
|
||||
@@ -53,18 +51,17 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
logger.warn('Invalid request data', { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
logger.error(`[${requestId}] Failed to get session token:`, error)
|
||||
logger.error('Failed to get session token', { error: toError(error).message })
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `Failed to get session token: ${errorMessage}` },
|
||||
{ error: `Failed to get session token: ${toError(error).message}` },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export async function assumeRole(
|
||||
roleArn: string,
|
||||
roleSessionName: string,
|
||||
durationSeconds?: number | null,
|
||||
policy?: string | null,
|
||||
externalId?: string | null,
|
||||
serialNumber?: string | null,
|
||||
tokenCode?: string | null
|
||||
@@ -30,6 +31,7 @@ export async function assumeRole(
|
||||
RoleArn: roleArn,
|
||||
RoleSessionName: roleSessionName,
|
||||
...(durationSeconds ? { DurationSeconds: durationSeconds } : {}),
|
||||
...(policy ? { Policy: policy } : {}),
|
||||
...(externalId ? { ExternalId: externalId } : {}),
|
||||
...(serialNumber ? { SerialNumber: serialNumber } : {}),
|
||||
...(tokenCode ? { TokenCode: tokenCode } : {}),
|
||||
@@ -45,6 +47,7 @@ export async function assumeRole(
|
||||
assumedRoleArn: response.AssumedRoleUser?.Arn ?? '',
|
||||
assumedRoleId: response.AssumedRoleUser?.AssumedRoleId ?? '',
|
||||
packedPolicySize: response.PackedPolicySize ?? null,
|
||||
sourceIdentity: response.SourceIdentity ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { toError } from '@sim/utils/errors'
|
||||
import { DynamoDBIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import type { DynamoDBIntrospectResponse, DynamoDBResponse } from '@/tools/dynamodb/types'
|
||||
|
||||
export const DynamoDBBlock: BlockConfig<DynamoDBResponse | DynamoDBIntrospectResponse> = {
|
||||
type: 'dynamodb',
|
||||
name: 'Amazon DynamoDB',
|
||||
description: 'Connect to Amazon DynamoDB',
|
||||
description: 'Get, put, query, scan, update, and delete items in Amazon DynamoDB tables',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription:
|
||||
'Integrate Amazon DynamoDB into workflows. Supports Get, Put, Query, Scan, Update, Delete, and Introspect operations on DynamoDB tables.',
|
||||
docsLink: 'https://docs.sim.ai/tools/dynamodb',
|
||||
@@ -67,16 +69,15 @@ export const DynamoDBBlock: BlockConfig<DynamoDBResponse | DynamoDBIntrospectRes
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'tableName',
|
||||
id: 'introspectTableName',
|
||||
title: 'Table Name (Optional)',
|
||||
type: 'short-input',
|
||||
placeholder: 'Leave empty to list all tables',
|
||||
required: false,
|
||||
condition: { field: 'operation', value: 'introspect' },
|
||||
},
|
||||
// Key field for get, update, delete operations
|
||||
{
|
||||
id: 'key',
|
||||
id: 'getKey',
|
||||
title: 'Key (JSON)',
|
||||
type: 'code',
|
||||
placeholder: '{\n "pk": "user#123"\n}',
|
||||
@@ -96,7 +97,7 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'key',
|
||||
id: 'updateKey',
|
||||
title: 'Key (JSON)',
|
||||
type: 'code',
|
||||
placeholder: '{\n "pk": "user#123"\n}',
|
||||
@@ -116,7 +117,7 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'key',
|
||||
id: 'deleteKey',
|
||||
title: 'Key (JSON)',
|
||||
type: 'code',
|
||||
placeholder: '{\n "pk": "user#123"\n}',
|
||||
@@ -135,7 +136,6 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`,
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
// Consistent read for get
|
||||
{
|
||||
id: 'consistentRead',
|
||||
title: 'Consistent Read',
|
||||
@@ -146,8 +146,8 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`,
|
||||
],
|
||||
value: () => 'false',
|
||||
condition: { field: 'operation', value: 'get' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Item for put operation
|
||||
{
|
||||
id: 'item',
|
||||
title: 'Item (JSON)',
|
||||
@@ -167,7 +167,6 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`,
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
// Key condition expression for query
|
||||
{
|
||||
id: 'keyConditionExpression',
|
||||
title: 'Key Condition Expression',
|
||||
@@ -189,7 +188,6 @@ Return ONLY the expression - no explanations.`,
|
||||
placeholder: 'Describe the key condition...',
|
||||
},
|
||||
},
|
||||
// Update expression for update operation
|
||||
{
|
||||
id: 'updateExpression',
|
||||
title: 'Update Expression',
|
||||
@@ -211,9 +209,8 @@ Return ONLY the expression - no explanations.`,
|
||||
placeholder: 'Describe what updates to make...',
|
||||
},
|
||||
},
|
||||
// Filter expression for query and scan
|
||||
{
|
||||
id: 'filterExpression',
|
||||
id: 'queryFilterExpression',
|
||||
title: 'Filter Expression',
|
||||
type: 'short-input',
|
||||
placeholder: 'attribute_exists(email)',
|
||||
@@ -233,7 +230,7 @@ Return ONLY the expression - no explanations.`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'filterExpression',
|
||||
id: 'scanFilterExpression',
|
||||
title: 'Filter Expression',
|
||||
type: 'short-input',
|
||||
placeholder: 'attribute_exists(email)',
|
||||
@@ -252,7 +249,6 @@ Return ONLY the expression - no explanations.`,
|
||||
placeholder: 'Describe how to filter results...',
|
||||
},
|
||||
},
|
||||
// Projection expression for scan
|
||||
{
|
||||
id: 'projectionExpression',
|
||||
title: 'Projection Expression',
|
||||
@@ -260,9 +256,8 @@ Return ONLY the expression - no explanations.`,
|
||||
placeholder: 'pk, #name, email',
|
||||
condition: { field: 'operation', value: 'scan' },
|
||||
},
|
||||
// Expression attribute names for query, scan, update
|
||||
{
|
||||
id: 'expressionAttributeNames',
|
||||
id: 'queryExpressionAttributeNames',
|
||||
title: 'Expression Attribute Names (JSON)',
|
||||
type: 'code',
|
||||
placeholder: '{\n "#name": "name"\n}',
|
||||
@@ -280,7 +275,7 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'expressionAttributeNames',
|
||||
id: 'scanExpressionAttributeNames',
|
||||
title: 'Expression Attribute Names (JSON)',
|
||||
type: 'code',
|
||||
placeholder: '{\n "#name": "name"\n}',
|
||||
@@ -298,7 +293,7 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'expressionAttributeNames',
|
||||
id: 'updateExpressionAttributeNames',
|
||||
title: 'Expression Attribute Names (JSON)',
|
||||
type: 'code',
|
||||
placeholder: '{\n "#name": "name"\n}',
|
||||
@@ -315,9 +310,44 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`,
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
// Expression attribute values for query, scan, update
|
||||
{
|
||||
id: 'expressionAttributeValues',
|
||||
id: 'putExpressionAttributeNames',
|
||||
title: 'Expression Attribute Names (JSON)',
|
||||
type: 'code',
|
||||
placeholder: '{\n "#name": "name"\n}',
|
||||
condition: { field: 'operation', value: 'put' },
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate DynamoDB expression attribute names JSON based on the user's description.
|
||||
Map placeholder names (starting with #) to actual attribute names used in the condition expression.
|
||||
Example: {"#name": "name", "#status": "status"}
|
||||
|
||||
Return ONLY valid JSON - no explanations, no markdown code blocks.`,
|
||||
placeholder: 'Describe the attribute name mappings...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'deleteExpressionAttributeNames',
|
||||
title: 'Expression Attribute Names (JSON)',
|
||||
type: 'code',
|
||||
placeholder: '{\n "#status": "status"\n}',
|
||||
condition: { field: 'operation', value: 'delete' },
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate DynamoDB expression attribute names JSON based on the user's description.
|
||||
Map placeholder names (starting with #) to actual attribute names used in the condition expression.
|
||||
Example: {"#status": "status"}
|
||||
|
||||
Return ONLY valid JSON - no explanations, no markdown code blocks.`,
|
||||
placeholder: 'Describe the attribute name mappings...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'queryExpressionAttributeValues',
|
||||
title: 'Expression Attribute Values (JSON)',
|
||||
type: 'code',
|
||||
placeholder: '{\n ":pk": "user#123",\n ":name": "Jane"\n}',
|
||||
@@ -334,7 +364,7 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'expressionAttributeValues',
|
||||
id: 'scanExpressionAttributeValues',
|
||||
title: 'Expression Attribute Values (JSON)',
|
||||
type: 'code',
|
||||
placeholder: '{\n ":status": "active"\n}',
|
||||
@@ -351,7 +381,7 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'expressionAttributeValues',
|
||||
id: 'updateExpressionAttributeValues',
|
||||
title: 'Expression Attribute Values (JSON)',
|
||||
type: 'code',
|
||||
placeholder: '{\n ":name": "Jane Doe"\n}',
|
||||
@@ -367,36 +397,92 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`,
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
// Index name for query
|
||||
{
|
||||
id: 'putExpressionAttributeValues',
|
||||
title: 'Expression Attribute Values (JSON)',
|
||||
type: 'code',
|
||||
placeholder: '{\n ":expected": "value"\n}',
|
||||
condition: { field: 'operation', value: 'put' },
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate DynamoDB expression attribute values JSON based on the user's description.
|
||||
Map placeholder values (starting with :) to actual values used in the condition expression.
|
||||
Example: {":expectedVersion": 3}
|
||||
|
||||
Return ONLY valid JSON - no explanations, no markdown code blocks.`,
|
||||
placeholder: 'Describe the attribute values...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'deleteExpressionAttributeValues',
|
||||
title: 'Expression Attribute Values (JSON)',
|
||||
type: 'code',
|
||||
placeholder: '{\n ":status": "active"\n}',
|
||||
condition: { field: 'operation', value: 'delete' },
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate DynamoDB expression attribute values JSON based on the user's description.
|
||||
Map placeholder values (starting with :) to actual values used in the condition expression.
|
||||
Example: {":status": "active", ":version": 3}
|
||||
|
||||
Return ONLY valid JSON - no explanations, no markdown code blocks.`,
|
||||
placeholder: 'Describe the attribute values...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'indexName',
|
||||
title: 'Index Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'GSI1',
|
||||
condition: { field: 'operation', value: 'query' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Limit for query and scan
|
||||
{
|
||||
id: 'limit',
|
||||
id: 'queryLimit',
|
||||
title: 'Limit',
|
||||
type: 'short-input',
|
||||
placeholder: '100',
|
||||
condition: { field: 'operation', value: 'query' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'limit',
|
||||
id: 'scanLimit',
|
||||
title: 'Limit',
|
||||
type: 'short-input',
|
||||
placeholder: '100',
|
||||
condition: { field: 'operation', value: 'scan' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Condition expression for update and delete
|
||||
{
|
||||
id: 'conditionExpression',
|
||||
id: 'putConditionExpression',
|
||||
title: 'Condition Expression',
|
||||
type: 'short-input',
|
||||
placeholder: 'attribute_not_exists(pk)',
|
||||
condition: { field: 'operation', value: 'put' },
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a DynamoDB condition expression based on the user's description.
|
||||
Condition expressions prevent the operation if the condition is not met.
|
||||
Examples:
|
||||
- "attribute_not_exists(pk)" - Prevent overwriting an existing item
|
||||
- "#version = :expectedVersion" - Optimistic locking
|
||||
|
||||
Return ONLY the expression - no explanations.`,
|
||||
placeholder: 'Describe the condition that must be true...',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'updateConditionExpression',
|
||||
title: 'Condition Expression',
|
||||
type: 'short-input',
|
||||
placeholder: 'attribute_exists(pk)',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a DynamoDB condition expression based on the user's description.
|
||||
@@ -411,11 +497,12 @@ Return ONLY the expression - no explanations.`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'conditionExpression',
|
||||
id: 'deleteConditionExpression',
|
||||
title: 'Condition Expression',
|
||||
type: 'short-input',
|
||||
placeholder: 'attribute_exists(pk)',
|
||||
condition: { field: 'operation', value: 'delete' },
|
||||
mode: 'advanced',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a DynamoDB condition expression based on the user's description.
|
||||
@@ -428,6 +515,34 @@ Return ONLY the expression - no explanations.`,
|
||||
placeholder: 'Describe the condition that must be true...',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'queryExclusiveStartKey',
|
||||
title: 'Exclusive Start Key (JSON)',
|
||||
type: 'code',
|
||||
placeholder: '{\n "pk": "user#123"\n}',
|
||||
condition: { field: 'operation', value: 'query' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'scanExclusiveStartKey',
|
||||
title: 'Exclusive Start Key (JSON)',
|
||||
type: 'code',
|
||||
placeholder: '{\n "pk": "user#123"\n}',
|
||||
condition: { field: 'operation', value: 'scan' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'scanIndexForward',
|
||||
title: 'Sort Order',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Ascending (default)', id: 'true' },
|
||||
{ label: 'Descending', id: 'false' },
|
||||
],
|
||||
value: () => 'true',
|
||||
condition: { field: 'operation', value: 'query' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
@@ -461,18 +576,6 @@ Return ONLY the expression - no explanations.`,
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const {
|
||||
operation,
|
||||
key,
|
||||
item,
|
||||
expressionAttributeNames,
|
||||
expressionAttributeValues,
|
||||
consistentRead,
|
||||
limit,
|
||||
...rest
|
||||
} = params
|
||||
|
||||
// Parse JSON fields
|
||||
const parseJson = (value: unknown, fieldName: string) => {
|
||||
if (!value) return undefined
|
||||
if (typeof value === 'object') return value
|
||||
@@ -480,56 +583,146 @@ Return ONLY the expression - no explanations.`,
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch (parseError) {
|
||||
const errorMsg =
|
||||
parseError instanceof Error ? parseError.message : 'Unknown JSON error'
|
||||
throw new Error(`Invalid JSON in ${fieldName}: ${errorMsg}`)
|
||||
throw new Error(`Invalid JSON in ${fieldName}: ${toError(parseError).message}`)
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const parsedKey = parseJson(key, 'key')
|
||||
const parsedItem = parseJson(item, 'item')
|
||||
const parsedExpressionAttributeNames = parseJson(
|
||||
expressionAttributeNames,
|
||||
'expressionAttributeNames'
|
||||
)
|
||||
const parsedExpressionAttributeValues = parseJson(
|
||||
expressionAttributeValues,
|
||||
'expressionAttributeValues'
|
||||
)
|
||||
|
||||
// Build connection config
|
||||
const connectionConfig = {
|
||||
region: rest.region,
|
||||
accessKeyId: rest.accessKeyId,
|
||||
secretAccessKey: rest.secretAccessKey,
|
||||
}
|
||||
|
||||
// Build params object
|
||||
const op = params.operation as string
|
||||
const result: Record<string, unknown> = {
|
||||
...connectionConfig,
|
||||
tableName: rest.tableName,
|
||||
region: params.region,
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
}
|
||||
|
||||
if (parsedKey !== undefined) result.key = parsedKey
|
||||
if (parsedItem !== undefined) result.item = parsedItem
|
||||
if (rest.keyConditionExpression) result.keyConditionExpression = rest.keyConditionExpression
|
||||
if (rest.updateExpression) result.updateExpression = rest.updateExpression
|
||||
if (rest.filterExpression) result.filterExpression = rest.filterExpression
|
||||
if (rest.projectionExpression) result.projectionExpression = rest.projectionExpression
|
||||
if (parsedExpressionAttributeNames !== undefined) {
|
||||
result.expressionAttributeNames = parsedExpressionAttributeNames
|
||||
// Table name (introspect uses introspectTableName, all others use tableName).
|
||||
// Legacy blocks stored both operations under 'tableName'; fall back to tableName
|
||||
// for introspect if introspectTableName is not present (migration grace period).
|
||||
if (op === 'introspect') {
|
||||
const tbl = params.introspectTableName || params.tableName
|
||||
if (tbl) result.tableName = tbl
|
||||
} else {
|
||||
result.tableName = params.tableName
|
||||
}
|
||||
if (parsedExpressionAttributeValues !== undefined) {
|
||||
result.expressionAttributeValues = parsedExpressionAttributeValues
|
||||
|
||||
// Operation-specific params — map unique subBlock IDs back to tool param names.
|
||||
// Where a fallback to the legacy migrated ID is present (e.g. params.getKey as
|
||||
// fallback for update/delete), it covers blocks saved before the subblock rename
|
||||
// whose migration entry routed the shared old ID to the query/get slot.
|
||||
if (op === 'get') {
|
||||
const key = parseJson(params.getKey, 'key')
|
||||
if (key !== undefined) result.key = key
|
||||
if (params.consistentRead === 'true' || params.consistentRead === true) {
|
||||
result.consistentRead = true
|
||||
}
|
||||
}
|
||||
if (rest.indexName) result.indexName = rest.indexName
|
||||
if (limit) result.limit = Number.parseInt(String(limit), 10)
|
||||
if (rest.conditionExpression) result.conditionExpression = rest.conditionExpression
|
||||
// Handle consistentRead - dropdown sends 'true'/'false' strings or boolean
|
||||
if (consistentRead === 'true' || consistentRead === true) {
|
||||
result.consistentRead = true
|
||||
|
||||
if (op === 'put') {
|
||||
const item = parseJson(params.item, 'item')
|
||||
if (item !== undefined) result.item = item
|
||||
// conditionExpression: fall back to updateConditionExpression (legacy migration target)
|
||||
const condExpr = params.putConditionExpression || params.updateConditionExpression
|
||||
if (condExpr) result.conditionExpression = condExpr
|
||||
// expressionAttributeNames: fall back to queryExpressionAttributeNames (legacy migration target)
|
||||
const names = parseJson(
|
||||
params.putExpressionAttributeNames || params.queryExpressionAttributeNames,
|
||||
'expressionAttributeNames'
|
||||
)
|
||||
if (names !== undefined) result.expressionAttributeNames = names
|
||||
// expressionAttributeValues: fall back to queryExpressionAttributeValues (legacy migration target)
|
||||
const values = parseJson(
|
||||
params.putExpressionAttributeValues || params.queryExpressionAttributeValues,
|
||||
'expressionAttributeValues'
|
||||
)
|
||||
if (values !== undefined) result.expressionAttributeValues = values
|
||||
}
|
||||
|
||||
if (op === 'query') {
|
||||
if (params.keyConditionExpression)
|
||||
result.keyConditionExpression = params.keyConditionExpression
|
||||
if (params.queryFilterExpression) result.filterExpression = params.queryFilterExpression
|
||||
const names = parseJson(params.queryExpressionAttributeNames, 'expressionAttributeNames')
|
||||
if (names !== undefined) result.expressionAttributeNames = names
|
||||
const values = parseJson(
|
||||
params.queryExpressionAttributeValues,
|
||||
'expressionAttributeValues'
|
||||
)
|
||||
if (values !== undefined) result.expressionAttributeValues = values
|
||||
if (params.indexName) result.indexName = params.indexName
|
||||
if (params.queryLimit) result.limit = Number.parseInt(String(params.queryLimit), 10)
|
||||
const esk = parseJson(params.queryExclusiveStartKey, 'exclusiveStartKey')
|
||||
if (esk !== undefined) result.exclusiveStartKey = esk
|
||||
if (params.scanIndexForward === 'false' || params.scanIndexForward === false) {
|
||||
result.scanIndexForward = false
|
||||
}
|
||||
}
|
||||
|
||||
if (op === 'scan') {
|
||||
// filterExpression: fall back to queryFilterExpression (legacy migration target for 'filterExpression')
|
||||
const filterExpr = params.scanFilterExpression || params.queryFilterExpression
|
||||
if (filterExpr) result.filterExpression = filterExpr
|
||||
if (params.projectionExpression) result.projectionExpression = params.projectionExpression
|
||||
// expressionAttributeNames: fall back to queryExpressionAttributeNames (legacy migration target)
|
||||
const names = parseJson(
|
||||
params.scanExpressionAttributeNames || params.queryExpressionAttributeNames,
|
||||
'expressionAttributeNames'
|
||||
)
|
||||
if (names !== undefined) result.expressionAttributeNames = names
|
||||
// expressionAttributeValues: fall back to queryExpressionAttributeValues (legacy migration target)
|
||||
const values = parseJson(
|
||||
params.scanExpressionAttributeValues || params.queryExpressionAttributeValues,
|
||||
'expressionAttributeValues'
|
||||
)
|
||||
if (values !== undefined) result.expressionAttributeValues = values
|
||||
// limit: fall back to queryLimit (legacy migration target for 'limit')
|
||||
const lim = params.scanLimit || params.queryLimit
|
||||
if (lim) result.limit = Number.parseInt(String(lim), 10)
|
||||
const esk = parseJson(params.scanExclusiveStartKey, 'exclusiveStartKey')
|
||||
if (esk !== undefined) result.exclusiveStartKey = esk
|
||||
}
|
||||
|
||||
if (op === 'update') {
|
||||
// key: fall back to getKey (legacy migration target for shared 'key' subblock)
|
||||
const key = parseJson(params.updateKey || params.getKey, 'key')
|
||||
if (key !== undefined) result.key = key
|
||||
if (params.updateExpression) result.updateExpression = params.updateExpression
|
||||
// expressionAttributeNames: fall back to queryExpressionAttributeNames (legacy migration target)
|
||||
const names = parseJson(
|
||||
params.updateExpressionAttributeNames || params.queryExpressionAttributeNames,
|
||||
'expressionAttributeNames'
|
||||
)
|
||||
if (names !== undefined) result.expressionAttributeNames = names
|
||||
// expressionAttributeValues: fall back to queryExpressionAttributeValues (legacy migration target)
|
||||
const values = parseJson(
|
||||
params.updateExpressionAttributeValues || params.queryExpressionAttributeValues,
|
||||
'expressionAttributeValues'
|
||||
)
|
||||
if (values !== undefined) result.expressionAttributeValues = values
|
||||
if (params.updateConditionExpression)
|
||||
result.conditionExpression = params.updateConditionExpression
|
||||
}
|
||||
|
||||
if (op === 'delete') {
|
||||
// key: fall back to getKey (legacy migration target for shared 'key' subblock)
|
||||
const key = parseJson(params.deleteKey || params.getKey, 'key')
|
||||
if (key !== undefined) result.key = key
|
||||
// conditionExpression: fall back to updateConditionExpression (legacy migration target for shared 'conditionExpression' subblock)
|
||||
const deleteCondExpr =
|
||||
params.deleteConditionExpression || params.updateConditionExpression
|
||||
if (deleteCondExpr) result.conditionExpression = deleteCondExpr
|
||||
// expressionAttributeNames: fall back to queryExpressionAttributeNames (legacy migration target)
|
||||
const names = parseJson(
|
||||
params.deleteExpressionAttributeNames || params.queryExpressionAttributeNames,
|
||||
'expressionAttributeNames'
|
||||
)
|
||||
if (names !== undefined) result.expressionAttributeNames = names
|
||||
// expressionAttributeValues: fall back to queryExpressionAttributeValues (legacy migration target)
|
||||
const values = parseJson(
|
||||
params.deleteExpressionAttributeValues || params.queryExpressionAttributeValues,
|
||||
'expressionAttributeValues'
|
||||
)
|
||||
if (values !== undefined) result.expressionAttributeValues = values
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -542,18 +735,66 @@ Return ONLY the expression - no explanations.`,
|
||||
accessKeyId: { type: 'string', description: 'AWS access key ID' },
|
||||
secretAccessKey: { type: 'string', description: 'AWS secret access key' },
|
||||
tableName: { type: 'string', description: 'DynamoDB table name' },
|
||||
key: { type: 'json', description: 'Primary key for get/update/delete operations' },
|
||||
introspectTableName: {
|
||||
type: 'string',
|
||||
description: 'Optional table name for introspect operation',
|
||||
},
|
||||
getKey: { type: 'json', description: 'Primary key for get operation' },
|
||||
updateKey: { type: 'json', description: 'Primary key for update operation' },
|
||||
deleteKey: { type: 'json', description: 'Primary key for delete operation' },
|
||||
item: { type: 'json', description: 'Item to put into the table' },
|
||||
keyConditionExpression: { type: 'string', description: 'Key condition for query operations' },
|
||||
updateExpression: { type: 'string', description: 'Update expression for update operations' },
|
||||
filterExpression: { type: 'string', description: 'Filter expression for query/scan' },
|
||||
queryFilterExpression: { type: 'string', description: 'Filter expression for query' },
|
||||
scanFilterExpression: { type: 'string', description: 'Filter expression for scan' },
|
||||
projectionExpression: { type: 'string', description: 'Attributes to retrieve in scan' },
|
||||
expressionAttributeNames: { type: 'json', description: 'Attribute name mappings' },
|
||||
expressionAttributeValues: { type: 'json', description: 'Expression attribute values' },
|
||||
queryExpressionAttributeNames: {
|
||||
type: 'json',
|
||||
description: 'Attribute name mappings for query',
|
||||
},
|
||||
scanExpressionAttributeNames: { type: 'json', description: 'Attribute name mappings for scan' },
|
||||
updateExpressionAttributeNames: {
|
||||
type: 'json',
|
||||
description: 'Attribute name mappings for update',
|
||||
},
|
||||
putExpressionAttributeNames: { type: 'json', description: 'Attribute name mappings for put' },
|
||||
deleteExpressionAttributeNames: {
|
||||
type: 'json',
|
||||
description: 'Attribute name mappings for delete',
|
||||
},
|
||||
queryExpressionAttributeValues: {
|
||||
type: 'json',
|
||||
description: 'Expression attribute values for query',
|
||||
},
|
||||
scanExpressionAttributeValues: {
|
||||
type: 'json',
|
||||
description: 'Expression attribute values for scan',
|
||||
},
|
||||
updateExpressionAttributeValues: {
|
||||
type: 'json',
|
||||
description: 'Expression attribute values for update',
|
||||
},
|
||||
putExpressionAttributeValues: {
|
||||
type: 'json',
|
||||
description: 'Expression attribute values for put',
|
||||
},
|
||||
deleteExpressionAttributeValues: {
|
||||
type: 'json',
|
||||
description: 'Expression attribute values for delete',
|
||||
},
|
||||
indexName: { type: 'string', description: 'Secondary index name for query' },
|
||||
limit: { type: 'number', description: 'Maximum items to return' },
|
||||
conditionExpression: { type: 'string', description: 'Condition for update/delete' },
|
||||
queryLimit: { type: 'number', description: 'Maximum items to return for query' },
|
||||
scanLimit: { type: 'number', description: 'Maximum items to return for scan' },
|
||||
putConditionExpression: { type: 'string', description: 'Condition for put operation' },
|
||||
updateConditionExpression: { type: 'string', description: 'Condition for update operation' },
|
||||
deleteConditionExpression: { type: 'string', description: 'Condition for delete operation' },
|
||||
consistentRead: { type: 'string', description: 'Use strongly consistent read' },
|
||||
queryExclusiveStartKey: { type: 'json', description: 'Pagination token for query' },
|
||||
scanExclusiveStartKey: { type: 'json', description: 'Pagination token for scan' },
|
||||
scanIndexForward: {
|
||||
type: 'string',
|
||||
description: 'Sort order for query: true for ascending, false for descending',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
message: {
|
||||
@@ -572,6 +813,11 @@ Return ONLY the expression - no explanations.`,
|
||||
type: 'number',
|
||||
description: 'Number of items returned',
|
||||
},
|
||||
lastEvaluatedKey: {
|
||||
type: 'json',
|
||||
description:
|
||||
'Pagination token from query/scan — pass as exclusiveStartKey to fetch the next page',
|
||||
},
|
||||
tables: {
|
||||
type: 'array',
|
||||
description: 'List of table names from introspect operation',
|
||||
|
||||
@@ -40,6 +40,9 @@ export const IAMBlock: BlockConfig<IAMBaseResponse> = {
|
||||
{ label: 'List Groups', id: 'list_groups' },
|
||||
{ label: 'Add User to Group', id: 'add_user_to_group' },
|
||||
{ label: 'Remove User from Group', id: 'remove_user_from_group' },
|
||||
{ label: 'List Attached Role Policies', id: 'list_attached_role_policies' },
|
||||
{ label: 'List Attached User Policies', id: 'list_attached_user_policies' },
|
||||
{ label: 'Simulate Principal Policy', id: 'simulate_principal_policy' },
|
||||
],
|
||||
value: () => 'list_users',
|
||||
},
|
||||
@@ -83,6 +86,7 @@ export const IAMBlock: BlockConfig<IAMBaseResponse> = {
|
||||
'delete_access_key',
|
||||
'add_user_to_group',
|
||||
'remove_user_from_group',
|
||||
'list_attached_user_policies',
|
||||
],
|
||||
},
|
||||
required: {
|
||||
@@ -95,6 +99,7 @@ export const IAMBlock: BlockConfig<IAMBaseResponse> = {
|
||||
'detach_user_policy',
|
||||
'add_user_to_group',
|
||||
'remove_user_from_group',
|
||||
'list_attached_user_policies',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -111,6 +116,7 @@ export const IAMBlock: BlockConfig<IAMBaseResponse> = {
|
||||
'delete_role',
|
||||
'attach_role_policy',
|
||||
'detach_role_policy',
|
||||
'list_attached_role_policies',
|
||||
],
|
||||
},
|
||||
required: {
|
||||
@@ -121,6 +127,7 @@ export const IAMBlock: BlockConfig<IAMBaseResponse> = {
|
||||
'delete_role',
|
||||
'attach_role_policy',
|
||||
'detach_role_policy',
|
||||
'list_attached_role_policies',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -239,6 +246,37 @@ export const IAMBlock: BlockConfig<IAMBaseResponse> = {
|
||||
required: false,
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'policySourceArn',
|
||||
title: 'Principal ARN',
|
||||
type: 'short-input',
|
||||
placeholder: 'arn:aws:iam::123456789012:user/alice',
|
||||
condition: { field: 'operation', value: 'simulate_principal_policy' },
|
||||
required: { field: 'operation', value: 'simulate_principal_policy' },
|
||||
},
|
||||
{
|
||||
id: 'actionNames',
|
||||
title: 'Actions (comma-separated)',
|
||||
type: 'short-input',
|
||||
placeholder: 's3:GetObject,ec2:DescribeInstances',
|
||||
condition: { field: 'operation', value: 'simulate_principal_policy' },
|
||||
required: { field: 'operation', value: 'simulate_principal_policy' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt:
|
||||
'Generate a comma-separated list of AWS IAM action names to simulate (e.g., s3:GetObject,ec2:DescribeInstances,iam:ListUsers). Return ONLY the comma-separated list - no explanations, no extra text.',
|
||||
placeholder: 'Describe the actions you want to check',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'resourceArns',
|
||||
title: 'Resource ARNs (comma-separated)',
|
||||
type: 'short-input',
|
||||
placeholder: 'arn:aws:s3:::my-bucket/*',
|
||||
condition: { field: 'operation', value: 'simulate_principal_policy' },
|
||||
required: false,
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'pathPrefix',
|
||||
title: 'Path Prefix',
|
||||
@@ -246,7 +284,14 @@ export const IAMBlock: BlockConfig<IAMBaseResponse> = {
|
||||
placeholder: '/division_abc/',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_users', 'list_roles', 'list_policies', 'list_groups'],
|
||||
value: [
|
||||
'list_users',
|
||||
'list_roles',
|
||||
'list_policies',
|
||||
'list_groups',
|
||||
'list_attached_role_policies',
|
||||
'list_attached_user_policies',
|
||||
],
|
||||
},
|
||||
required: false,
|
||||
mode: 'advanced',
|
||||
@@ -258,7 +303,15 @@ export const IAMBlock: BlockConfig<IAMBaseResponse> = {
|
||||
placeholder: '100',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_users', 'list_roles', 'list_policies', 'list_groups'],
|
||||
value: [
|
||||
'list_users',
|
||||
'list_roles',
|
||||
'list_policies',
|
||||
'list_groups',
|
||||
'list_attached_role_policies',
|
||||
'list_attached_user_policies',
|
||||
'simulate_principal_policy',
|
||||
],
|
||||
},
|
||||
required: false,
|
||||
mode: 'advanced',
|
||||
@@ -270,7 +323,15 @@ export const IAMBlock: BlockConfig<IAMBaseResponse> = {
|
||||
placeholder: 'Pagination marker',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_users', 'list_roles', 'list_policies', 'list_groups'],
|
||||
value: [
|
||||
'list_users',
|
||||
'list_roles',
|
||||
'list_policies',
|
||||
'list_groups',
|
||||
'list_attached_role_policies',
|
||||
'list_attached_user_policies',
|
||||
'simulate_principal_policy',
|
||||
],
|
||||
},
|
||||
required: false,
|
||||
mode: 'advanced',
|
||||
@@ -296,6 +357,9 @@ export const IAMBlock: BlockConfig<IAMBaseResponse> = {
|
||||
'iam_list_groups',
|
||||
'iam_add_user_to_group',
|
||||
'iam_remove_user_from_group',
|
||||
'iam_list_attached_role_policies',
|
||||
'iam_list_attached_user_policies',
|
||||
'iam_simulate_principal_policy',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
@@ -336,12 +400,19 @@ export const IAMBlock: BlockConfig<IAMBaseResponse> = {
|
||||
return 'iam_add_user_to_group'
|
||||
case 'remove_user_from_group':
|
||||
return 'iam_remove_user_from_group'
|
||||
case 'list_attached_role_policies':
|
||||
return 'iam_list_attached_role_policies'
|
||||
case 'list_attached_user_policies':
|
||||
return 'iam_list_attached_user_policies'
|
||||
case 'simulate_principal_policy':
|
||||
return 'iam_simulate_principal_policy'
|
||||
default:
|
||||
throw new Error(`Invalid IAM operation: ${params.operation}`)
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { operation, maxItems, maxSessionDuration, onlyAttached, ...rest } = params
|
||||
const { operation, maxItems, maxSessionDuration, onlyAttached, resourceArns, ...rest } =
|
||||
params
|
||||
|
||||
const connectionConfig = {
|
||||
region: rest.region,
|
||||
@@ -416,6 +487,34 @@ export const IAMBlock: BlockConfig<IAMBaseResponse> = {
|
||||
result.userName = rest.userName
|
||||
result.groupName = rest.groupName
|
||||
break
|
||||
case 'list_attached_role_policies':
|
||||
result.roleName = rest.roleName
|
||||
if (rest.pathPrefix) result.pathPrefix = rest.pathPrefix
|
||||
if (maxItems) {
|
||||
const parsed = Number.parseInt(String(maxItems), 10)
|
||||
if (!Number.isNaN(parsed)) result.maxItems = parsed
|
||||
}
|
||||
if (rest.marker) result.marker = rest.marker
|
||||
break
|
||||
case 'list_attached_user_policies':
|
||||
result.userName = rest.userName
|
||||
if (rest.pathPrefix) result.pathPrefix = rest.pathPrefix
|
||||
if (maxItems) {
|
||||
const parsed = Number.parseInt(String(maxItems), 10)
|
||||
if (!Number.isNaN(parsed)) result.maxItems = parsed
|
||||
}
|
||||
if (rest.marker) result.marker = rest.marker
|
||||
break
|
||||
case 'simulate_principal_policy':
|
||||
result.policySourceArn = rest.policySourceArn
|
||||
result.actionNames = rest.actionNames
|
||||
if (resourceArns) result.resourceArns = resourceArns
|
||||
if (maxItems) {
|
||||
const parsed = Number.parseInt(String(maxItems), 10)
|
||||
if (!Number.isNaN(parsed)) result.maxResults = parsed
|
||||
}
|
||||
if (rest.marker) result.marker = rest.marker
|
||||
break
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -441,6 +540,12 @@ export const IAMBlock: BlockConfig<IAMBaseResponse> = {
|
||||
pathPrefix: { type: 'string', description: 'Path prefix filter' },
|
||||
maxItems: { type: 'number', description: 'Maximum number of items to return' },
|
||||
marker: { type: 'string', description: 'Pagination marker' },
|
||||
policySourceArn: { type: 'string', description: 'ARN of the principal to simulate' },
|
||||
actionNames: { type: 'string', description: 'Comma-separated AWS actions to simulate' },
|
||||
resourceArns: {
|
||||
type: 'string',
|
||||
description: 'Comma-separated resource ARNs to simulate against',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
message: {
|
||||
@@ -549,5 +654,14 @@ export const IAMBlock: BlockConfig<IAMBaseResponse> = {
|
||||
type: 'number',
|
||||
description: 'Number of items returned',
|
||||
},
|
||||
attachedPolicies: {
|
||||
type: 'json',
|
||||
description: 'List of attached managed policies with policyName and policyArn',
|
||||
},
|
||||
evaluationResults: {
|
||||
type: 'json',
|
||||
description:
|
||||
'Policy simulation results per action: evalActionName, evalResourceName, evalDecision (allowed/explicitDeny/implicitDeny), matchedStatements (sourcePolicyId, sourcePolicyType), missingContextValues',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,438 @@
|
||||
import { IdentityCenterIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import type { IdentityCenterBaseResponse } from '@/tools/identity_center/types'
|
||||
|
||||
export const IdentityCenterBlock: BlockConfig<IdentityCenterBaseResponse> = {
|
||||
type: 'identity_center',
|
||||
name: 'AWS Identity Center',
|
||||
description: 'Manage temporary elevated access in AWS IAM Identity Center',
|
||||
longDescription:
|
||||
'Provision and revoke temporary access to AWS accounts via IAM Identity Center (SSO). Assign permission sets to users or groups, look up users by email, and list accounts and permission sets for access request workflows.',
|
||||
docsLink: 'https://docs.sim.ai/tools/identity-center',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Security,
|
||||
tags: ['cloud', 'identity'],
|
||||
bgColor: 'linear-gradient(45deg, #BD0816 0%, #FF5252 100%)',
|
||||
icon: IdentityCenterIcon,
|
||||
authMode: AuthMode.ApiKey,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'List Instances', id: 'list_instances' },
|
||||
{ label: 'List Accounts', id: 'list_accounts' },
|
||||
{ label: 'Describe Account', id: 'describe_account' },
|
||||
{ label: 'List Permission Sets', id: 'list_permission_sets' },
|
||||
{ label: 'Get User', id: 'get_user' },
|
||||
{ label: 'Get Group', id: 'get_group' },
|
||||
{ label: 'List Groups', id: 'list_groups' },
|
||||
{ label: 'Create Account Assignment', id: 'create_account_assignment' },
|
||||
{ label: 'Delete Account Assignment', id: 'delete_account_assignment' },
|
||||
{ label: 'Check Assignment Status', id: 'check_assignment_status' },
|
||||
{ label: 'Check Assignment Deletion Status', id: 'check_assignment_deletion_status' },
|
||||
{ label: 'List Account Assignments', id: 'list_account_assignments' },
|
||||
],
|
||||
value: () => 'list_instances',
|
||||
},
|
||||
{
|
||||
id: 'region',
|
||||
title: 'AWS Region',
|
||||
type: 'short-input',
|
||||
placeholder: 'us-east-1',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'accessKeyId',
|
||||
title: 'AWS Access Key ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'AKIA...',
|
||||
password: true,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'secretAccessKey',
|
||||
title: 'AWS Secret Access Key',
|
||||
type: 'short-input',
|
||||
placeholder: 'Your secret access key',
|
||||
password: true,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'instanceArn',
|
||||
title: 'Instance ARN',
|
||||
type: 'short-input',
|
||||
placeholder: 'arn:aws:sso:::instance/ssoins-...',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'list_instances',
|
||||
'list_accounts',
|
||||
'get_user',
|
||||
'get_group',
|
||||
'describe_account',
|
||||
'list_groups',
|
||||
],
|
||||
not: true,
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'list_instances',
|
||||
'list_accounts',
|
||||
'get_user',
|
||||
'get_group',
|
||||
'describe_account',
|
||||
'list_groups',
|
||||
],
|
||||
not: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'identityStoreId',
|
||||
title: 'Identity Store ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'd-1234567890',
|
||||
condition: { field: 'operation', value: ['get_user', 'get_group', 'list_groups'] },
|
||||
required: { field: 'operation', value: ['get_user', 'get_group', 'list_groups'] },
|
||||
},
|
||||
{
|
||||
id: 'email',
|
||||
title: 'Email Address',
|
||||
type: 'short-input',
|
||||
placeholder: 'user@example.com',
|
||||
condition: { field: 'operation', value: 'get_user' },
|
||||
required: { field: 'operation', value: 'get_user' },
|
||||
},
|
||||
{
|
||||
id: 'displayName',
|
||||
title: 'Group Display Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Engineering-Admins',
|
||||
condition: { field: 'operation', value: 'get_group' },
|
||||
required: { field: 'operation', value: 'get_group' },
|
||||
},
|
||||
{
|
||||
id: 'accountId',
|
||||
title: 'AWS Account ID',
|
||||
type: 'short-input',
|
||||
placeholder: '123456789012',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['create_account_assignment', 'delete_account_assignment', 'describe_account'],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['create_account_assignment', 'delete_account_assignment', 'describe_account'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'permissionSetArn',
|
||||
title: 'Permission Set ARN',
|
||||
type: 'short-input',
|
||||
placeholder: 'arn:aws:sso:::permissionSet/ssoins-.../ps-...',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['create_account_assignment', 'delete_account_assignment'],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['create_account_assignment', 'delete_account_assignment'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'principalType',
|
||||
title: 'Principal Type',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'User', id: 'USER' },
|
||||
{ label: 'Group', id: 'GROUP' },
|
||||
],
|
||||
value: () => 'USER',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'create_account_assignment',
|
||||
'delete_account_assignment',
|
||||
'list_account_assignments',
|
||||
],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'create_account_assignment',
|
||||
'delete_account_assignment',
|
||||
'list_account_assignments',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'principalId',
|
||||
title: 'Principal ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Identity Store user or group ID',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'create_account_assignment',
|
||||
'delete_account_assignment',
|
||||
'list_account_assignments',
|
||||
],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'create_account_assignment',
|
||||
'delete_account_assignment',
|
||||
'list_account_assignments',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'requestId',
|
||||
title: 'Request ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Request ID from Create or Delete Assignment',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['check_assignment_status', 'check_assignment_deletion_status'],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['check_assignment_status', 'check_assignment_deletion_status'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'maxResults',
|
||||
title: 'Max Results',
|
||||
type: 'short-input',
|
||||
placeholder: '20',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'list_instances',
|
||||
'list_accounts',
|
||||
'list_permission_sets',
|
||||
'list_account_assignments',
|
||||
'list_groups',
|
||||
],
|
||||
},
|
||||
required: false,
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'nextToken',
|
||||
title: 'Pagination Token',
|
||||
type: 'short-input',
|
||||
placeholder: 'Next page token from previous request',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'list_instances',
|
||||
'list_accounts',
|
||||
'list_permission_sets',
|
||||
'list_account_assignments',
|
||||
'list_groups',
|
||||
],
|
||||
},
|
||||
required: false,
|
||||
mode: 'advanced',
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
'identity_center_list_instances',
|
||||
'identity_center_list_accounts',
|
||||
'identity_center_describe_account',
|
||||
'identity_center_list_permission_sets',
|
||||
'identity_center_get_user',
|
||||
'identity_center_get_group',
|
||||
'identity_center_list_groups',
|
||||
'identity_center_create_account_assignment',
|
||||
'identity_center_delete_account_assignment',
|
||||
'identity_center_check_assignment_status',
|
||||
'identity_center_check_assignment_deletion_status',
|
||||
'identity_center_list_account_assignments',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
switch (params.operation) {
|
||||
case 'list_instances':
|
||||
return 'identity_center_list_instances'
|
||||
case 'list_accounts':
|
||||
return 'identity_center_list_accounts'
|
||||
case 'describe_account':
|
||||
return 'identity_center_describe_account'
|
||||
case 'list_permission_sets':
|
||||
return 'identity_center_list_permission_sets'
|
||||
case 'get_user':
|
||||
return 'identity_center_get_user'
|
||||
case 'get_group':
|
||||
return 'identity_center_get_group'
|
||||
case 'list_groups':
|
||||
return 'identity_center_list_groups'
|
||||
case 'create_account_assignment':
|
||||
return 'identity_center_create_account_assignment'
|
||||
case 'delete_account_assignment':
|
||||
return 'identity_center_delete_account_assignment'
|
||||
case 'check_assignment_status':
|
||||
return 'identity_center_check_assignment_status'
|
||||
case 'check_assignment_deletion_status':
|
||||
return 'identity_center_check_assignment_deletion_status'
|
||||
case 'list_account_assignments':
|
||||
return 'identity_center_list_account_assignments'
|
||||
default:
|
||||
throw new Error(`Invalid Identity Center operation: ${params.operation}`)
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { operation, maxResults, ...rest } = params
|
||||
|
||||
const connectionConfig = {
|
||||
region: rest.region,
|
||||
accessKeyId: rest.accessKeyId,
|
||||
secretAccessKey: rest.secretAccessKey,
|
||||
}
|
||||
|
||||
const result: Record<string, unknown> = { ...connectionConfig }
|
||||
|
||||
switch (operation) {
|
||||
case 'list_instances':
|
||||
if (maxResults) {
|
||||
const parsed = Number.parseInt(String(maxResults), 10)
|
||||
if (!Number.isNaN(parsed)) result.maxResults = parsed
|
||||
}
|
||||
if (rest.nextToken) result.nextToken = rest.nextToken
|
||||
break
|
||||
case 'list_accounts':
|
||||
if (maxResults) {
|
||||
const parsed = Number.parseInt(String(maxResults), 10)
|
||||
if (!Number.isNaN(parsed)) result.maxResults = parsed
|
||||
}
|
||||
if (rest.nextToken) result.nextToken = rest.nextToken
|
||||
break
|
||||
case 'describe_account':
|
||||
result.accountId = rest.accountId
|
||||
break
|
||||
case 'list_permission_sets':
|
||||
result.instanceArn = rest.instanceArn
|
||||
if (maxResults) {
|
||||
const parsed = Number.parseInt(String(maxResults), 10)
|
||||
if (!Number.isNaN(parsed)) result.maxResults = parsed
|
||||
}
|
||||
if (rest.nextToken) result.nextToken = rest.nextToken
|
||||
break
|
||||
case 'get_user':
|
||||
result.identityStoreId = rest.identityStoreId
|
||||
result.email = rest.email
|
||||
break
|
||||
case 'get_group':
|
||||
result.identityStoreId = rest.identityStoreId
|
||||
result.displayName = rest.displayName
|
||||
break
|
||||
case 'list_groups':
|
||||
result.identityStoreId = rest.identityStoreId
|
||||
if (maxResults) {
|
||||
const parsed = Number.parseInt(String(maxResults), 10)
|
||||
if (!Number.isNaN(parsed)) result.maxResults = parsed
|
||||
}
|
||||
if (rest.nextToken) result.nextToken = rest.nextToken
|
||||
break
|
||||
case 'create_account_assignment':
|
||||
case 'delete_account_assignment':
|
||||
result.instanceArn = rest.instanceArn
|
||||
result.accountId = rest.accountId
|
||||
result.permissionSetArn = rest.permissionSetArn
|
||||
result.principalType = rest.principalType
|
||||
result.principalId = rest.principalId
|
||||
break
|
||||
case 'check_assignment_status':
|
||||
case 'check_assignment_deletion_status':
|
||||
result.instanceArn = rest.instanceArn
|
||||
result.requestId = rest.requestId
|
||||
break
|
||||
case 'list_account_assignments':
|
||||
result.instanceArn = rest.instanceArn
|
||||
result.principalId = rest.principalId
|
||||
result.principalType = rest.principalType
|
||||
if (maxResults) {
|
||||
const parsed = Number.parseInt(String(maxResults), 10)
|
||||
if (!Number.isNaN(parsed)) result.maxResults = parsed
|
||||
}
|
||||
if (rest.nextToken) result.nextToken = rest.nextToken
|
||||
break
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Identity Center operation to perform' },
|
||||
region: { type: 'string', description: 'AWS region' },
|
||||
accessKeyId: { type: 'string', description: 'AWS access key ID' },
|
||||
secretAccessKey: { type: 'string', description: 'AWS secret access key' },
|
||||
instanceArn: { type: 'string', description: 'Identity Center instance ARN' },
|
||||
identityStoreId: { type: 'string', description: 'Identity Store ID' },
|
||||
email: { type: 'string', description: 'User email address' },
|
||||
displayName: { type: 'string', description: 'Group display name' },
|
||||
accountId: { type: 'string', description: 'AWS account ID' },
|
||||
permissionSetArn: { type: 'string', description: 'Permission set ARN' },
|
||||
principalType: { type: 'string', description: 'Principal type: USER or GROUP' },
|
||||
principalId: { type: 'string', description: 'Identity Store user or group ID' },
|
||||
requestId: { type: 'string', description: 'Assignment creation/deletion request ID' },
|
||||
maxResults: { type: 'number', description: 'Maximum number of results to return' },
|
||||
nextToken: { type: 'string', description: 'Pagination token from previous request' },
|
||||
},
|
||||
outputs: {
|
||||
message: { type: 'string', description: 'Operation status message' },
|
||||
instances: {
|
||||
type: 'json',
|
||||
description:
|
||||
'List of Identity Center instances (instanceArn, identityStoreId, name, status, statusReason)',
|
||||
},
|
||||
accounts: {
|
||||
type: 'json',
|
||||
description: 'List of AWS accounts (id, arn, name, email, status)',
|
||||
},
|
||||
permissionSets: {
|
||||
type: 'json',
|
||||
description: 'List of permission sets (permissionSetArn, name, description, sessionDuration)',
|
||||
},
|
||||
groups: {
|
||||
type: 'json',
|
||||
description: 'List of Identity Store groups (groupId, displayName, description)',
|
||||
},
|
||||
userId: { type: 'string', description: 'Identity Store user ID (use as principalId)' },
|
||||
userName: { type: 'string', description: 'Username in the Identity Store' },
|
||||
displayName: { type: 'string', description: 'Display name of the user or group' },
|
||||
email: { type: 'string', description: 'Email address of the user' },
|
||||
groupId: { type: 'string', description: 'Identity Store group ID (use as principalId)' },
|
||||
description: { type: 'string', description: 'Group description' },
|
||||
id: { type: 'string', description: 'AWS account ID (from describe_account)' },
|
||||
arn: { type: 'string', description: 'AWS account ARN' },
|
||||
name: { type: 'string', description: 'AWS account name' },
|
||||
status: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Assignment provisioning status (IN_PROGRESS, FAILED, SUCCEEDED) or account status',
|
||||
},
|
||||
requestId: { type: 'string', description: 'Request ID for polling assignment status' },
|
||||
accountId: { type: 'string', description: 'Target AWS account ID' },
|
||||
permissionSetArn: { type: 'string', description: 'Permission set ARN' },
|
||||
principalType: { type: 'string', description: 'Principal type (USER or GROUP)' },
|
||||
principalId: { type: 'string', description: 'Principal ID' },
|
||||
failureReason: { type: 'string', description: 'Failure reason if status is FAILED' },
|
||||
createdDate: { type: 'string', description: 'Date the request was created' },
|
||||
joinedTimestamp: { type: 'string', description: 'Date the account joined the organization' },
|
||||
assignments: {
|
||||
type: 'json',
|
||||
description:
|
||||
'List of account assignments (accountId, permissionSetArn, principalType, principalId)',
|
||||
},
|
||||
nextToken: { type: 'string', description: 'Pagination token for the next page' },
|
||||
count: { type: 'number', description: 'Number of items returned' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
import { SESIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
export const SESBlock: BlockConfig<ToolResponse> = {
|
||||
type: 'ses',
|
||||
name: 'AWS SES',
|
||||
description: 'Send emails and manage templates with AWS Simple Email Service',
|
||||
longDescription:
|
||||
'Integrate AWS SES v2 into the workflow. Send simple, templated, and bulk emails. Manage email templates and retrieve account sending quota and verified identity information.',
|
||||
docsLink: 'https://docs.sim.ai/tools/ses',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Email,
|
||||
tags: ['cloud', 'marketing'],
|
||||
authMode: AuthMode.ApiKey,
|
||||
bgColor: 'linear-gradient(45deg, #BD0816 0%, #FF5252 100%)',
|
||||
icon: SESIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Send Email', id: 'send_email' },
|
||||
{ label: 'Send Templated Email', id: 'send_templated_email' },
|
||||
{ label: 'Send Bulk Email', id: 'send_bulk_email' },
|
||||
{ label: 'List Identities', id: 'list_identities' },
|
||||
{ label: 'Get Account', id: 'get_account' },
|
||||
{ label: 'Create Template', id: 'create_template' },
|
||||
{ label: 'Get Template', id: 'get_template' },
|
||||
{ label: 'List Templates', id: 'list_templates' },
|
||||
{ label: 'Delete Template', id: 'delete_template' },
|
||||
],
|
||||
value: () => 'send_email',
|
||||
},
|
||||
{
|
||||
id: 'region',
|
||||
title: 'AWS Region',
|
||||
type: 'short-input',
|
||||
placeholder: 'us-east-1',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'accessKeyId',
|
||||
title: 'AWS Access Key ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'AKIA...',
|
||||
password: true,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'secretAccessKey',
|
||||
title: 'AWS Secret Access Key',
|
||||
type: 'short-input',
|
||||
placeholder: 'Your secret access key',
|
||||
password: true,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'fromAddress',
|
||||
title: 'From Address',
|
||||
type: 'short-input',
|
||||
placeholder: 'sender@example.com',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['send_email', 'send_templated_email', 'send_bulk_email'],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['send_email', 'send_templated_email', 'send_bulk_email'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'toAddresses',
|
||||
title: 'To Addresses',
|
||||
type: 'short-input',
|
||||
placeholder: 'recipient@example.com, other@example.com',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['send_email', 'send_templated_email'],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['send_email', 'send_templated_email'],
|
||||
},
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt:
|
||||
'Generate a comma-separated list of recipient email addresses. Return ONLY the comma-separated addresses - no explanations, no extra text.',
|
||||
placeholder: 'e.g. alice@example.com, bob@example.com',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'subject',
|
||||
title: 'Subject',
|
||||
type: 'short-input',
|
||||
placeholder: 'Your email subject',
|
||||
condition: { field: 'operation', value: 'send_email' },
|
||||
required: { field: 'operation', value: 'send_email' },
|
||||
},
|
||||
{
|
||||
id: 'bodyHtml',
|
||||
title: 'HTML Body',
|
||||
type: 'long-input',
|
||||
placeholder: '<h1>Hello</h1><p>Your email content here</p>',
|
||||
condition: { field: 'operation', value: 'send_email' },
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'bodyText',
|
||||
title: 'Plain Text Body',
|
||||
type: 'long-input',
|
||||
placeholder: 'Plain text version of your email',
|
||||
condition: { field: 'operation', value: 'send_email' },
|
||||
required: false,
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'templateName',
|
||||
title: 'Template Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'my-email-template',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'send_templated_email',
|
||||
'send_bulk_email',
|
||||
'get_template',
|
||||
'create_template',
|
||||
'delete_template',
|
||||
],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'send_templated_email',
|
||||
'send_bulk_email',
|
||||
'get_template',
|
||||
'create_template',
|
||||
'delete_template',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'templateData',
|
||||
title: 'Template Data (JSON)',
|
||||
type: 'code',
|
||||
language: 'json',
|
||||
placeholder: '{"name": "John", "link": "https://example.com"}',
|
||||
condition: { field: 'operation', value: 'send_templated_email' },
|
||||
required: { field: 'operation', value: 'send_templated_email' },
|
||||
},
|
||||
{
|
||||
id: 'destinations',
|
||||
title: 'Destinations (JSON)',
|
||||
type: 'code',
|
||||
language: 'json',
|
||||
placeholder:
|
||||
'[{"toAddresses": ["user1@example.com"], "templateData": "{\"name\": \"User 1\"}"}, {"toAddresses": ["user2@example.com"]}]',
|
||||
condition: { field: 'operation', value: 'send_bulk_email' },
|
||||
required: { field: 'operation', value: 'send_bulk_email' },
|
||||
},
|
||||
{
|
||||
id: 'subjectPart',
|
||||
title: 'Subject',
|
||||
type: 'short-input',
|
||||
placeholder: 'Hello, {{name}}!',
|
||||
condition: { field: 'operation', value: 'create_template' },
|
||||
required: { field: 'operation', value: 'create_template' },
|
||||
},
|
||||
{
|
||||
id: 'htmlPart',
|
||||
title: 'HTML Body',
|
||||
type: 'long-input',
|
||||
placeholder: '<h1>Hello, {{name}}!</h1>',
|
||||
condition: { field: 'operation', value: 'create_template' },
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'textPart',
|
||||
title: 'Plain Text Body',
|
||||
type: 'long-input',
|
||||
placeholder: 'Hello, {{name}}!',
|
||||
condition: { field: 'operation', value: 'create_template' },
|
||||
required: false,
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'ccAddresses',
|
||||
title: 'CC Addresses',
|
||||
type: 'short-input',
|
||||
placeholder: 'cc@example.com',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['send_email', 'send_templated_email'],
|
||||
},
|
||||
required: false,
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'bccAddresses',
|
||||
title: 'BCC Addresses',
|
||||
type: 'short-input',
|
||||
placeholder: 'bcc@example.com',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['send_email', 'send_templated_email'],
|
||||
},
|
||||
required: false,
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'replyToAddresses',
|
||||
title: 'Reply-To Addresses',
|
||||
type: 'short-input',
|
||||
placeholder: 'replyto@example.com',
|
||||
condition: { field: 'operation', value: 'send_email' },
|
||||
required: false,
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'defaultTemplateData',
|
||||
title: 'Default Template Data (JSON)',
|
||||
type: 'code',
|
||||
language: 'json',
|
||||
placeholder: '{"company": "Acme"}',
|
||||
condition: { field: 'operation', value: 'send_bulk_email' },
|
||||
required: false,
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'configurationSetName',
|
||||
title: 'Configuration Set',
|
||||
type: 'short-input',
|
||||
placeholder: 'my-configuration-set',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['send_email', 'send_templated_email', 'send_bulk_email'],
|
||||
},
|
||||
required: false,
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'pageSize',
|
||||
title: 'Page Size',
|
||||
type: 'short-input',
|
||||
placeholder: '100',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_identities', 'list_templates'],
|
||||
},
|
||||
required: false,
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'nextToken',
|
||||
title: 'Next Token',
|
||||
type: 'short-input',
|
||||
placeholder: 'Pagination token from previous response',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['list_identities', 'list_templates'],
|
||||
},
|
||||
required: false,
|
||||
mode: 'advanced',
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
'ses_send_email',
|
||||
'ses_send_templated_email',
|
||||
'ses_send_bulk_email',
|
||||
'ses_list_identities',
|
||||
'ses_get_account',
|
||||
'ses_create_template',
|
||||
'ses_get_template',
|
||||
'ses_list_templates',
|
||||
'ses_delete_template',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
switch (params.operation) {
|
||||
case 'send_email':
|
||||
return 'ses_send_email'
|
||||
case 'send_templated_email':
|
||||
return 'ses_send_templated_email'
|
||||
case 'send_bulk_email':
|
||||
return 'ses_send_bulk_email'
|
||||
case 'list_identities':
|
||||
return 'ses_list_identities'
|
||||
case 'get_account':
|
||||
return 'ses_get_account'
|
||||
case 'create_template':
|
||||
return 'ses_create_template'
|
||||
case 'get_template':
|
||||
return 'ses_get_template'
|
||||
case 'list_templates':
|
||||
return 'ses_list_templates'
|
||||
case 'delete_template':
|
||||
return 'ses_delete_template'
|
||||
default:
|
||||
throw new Error(`Invalid SES operation: ${params.operation}`)
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { operation, pageSize, ...rest } = params
|
||||
|
||||
const connectionConfig = {
|
||||
region: rest.region,
|
||||
accessKeyId: rest.accessKeyId,
|
||||
secretAccessKey: rest.secretAccessKey,
|
||||
}
|
||||
|
||||
const result: Record<string, unknown> = { ...connectionConfig }
|
||||
|
||||
switch (operation) {
|
||||
case 'send_email':
|
||||
result.fromAddress = rest.fromAddress
|
||||
result.toAddresses = rest.toAddresses
|
||||
result.subject = rest.subject
|
||||
if (rest.bodyHtml) result.bodyHtml = rest.bodyHtml
|
||||
if (rest.bodyText) result.bodyText = rest.bodyText
|
||||
if (rest.ccAddresses) result.ccAddresses = rest.ccAddresses
|
||||
if (rest.bccAddresses) result.bccAddresses = rest.bccAddresses
|
||||
if (rest.replyToAddresses) result.replyToAddresses = rest.replyToAddresses
|
||||
if (rest.configurationSetName) result.configurationSetName = rest.configurationSetName
|
||||
break
|
||||
case 'send_templated_email':
|
||||
result.fromAddress = rest.fromAddress
|
||||
result.toAddresses = rest.toAddresses
|
||||
result.templateName = rest.templateName
|
||||
result.templateData = rest.templateData
|
||||
if (rest.ccAddresses) result.ccAddresses = rest.ccAddresses
|
||||
if (rest.bccAddresses) result.bccAddresses = rest.bccAddresses
|
||||
if (rest.configurationSetName) result.configurationSetName = rest.configurationSetName
|
||||
break
|
||||
case 'send_bulk_email':
|
||||
result.fromAddress = rest.fromAddress
|
||||
result.templateName = rest.templateName
|
||||
result.destinations = rest.destinations
|
||||
if (rest.defaultTemplateData) result.defaultTemplateData = rest.defaultTemplateData
|
||||
if (rest.configurationSetName) result.configurationSetName = rest.configurationSetName
|
||||
break
|
||||
case 'list_identities':
|
||||
if (pageSize != null) {
|
||||
const parsed = Number.parseInt(String(pageSize), 10)
|
||||
if (!Number.isNaN(parsed)) result.pageSize = parsed
|
||||
}
|
||||
if (rest.nextToken) result.nextToken = rest.nextToken
|
||||
break
|
||||
case 'get_account':
|
||||
break
|
||||
case 'create_template':
|
||||
result.templateName = rest.templateName
|
||||
result.subjectPart = rest.subjectPart
|
||||
if (rest.htmlPart) result.htmlPart = rest.htmlPart
|
||||
if (rest.textPart) result.textPart = rest.textPart
|
||||
break
|
||||
case 'get_template':
|
||||
result.templateName = rest.templateName
|
||||
break
|
||||
case 'list_templates':
|
||||
if (pageSize != null) {
|
||||
const parsed = Number.parseInt(String(pageSize), 10)
|
||||
if (!Number.isNaN(parsed)) result.pageSize = parsed
|
||||
}
|
||||
if (rest.nextToken) result.nextToken = rest.nextToken
|
||||
break
|
||||
case 'delete_template':
|
||||
result.templateName = rest.templateName
|
||||
break
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'SES operation to perform' },
|
||||
region: { type: 'string', description: 'AWS region' },
|
||||
accessKeyId: { type: 'string', description: 'AWS access key ID' },
|
||||
secretAccessKey: { type: 'string', description: 'AWS secret access key' },
|
||||
fromAddress: { type: 'string', description: 'Verified sender email address' },
|
||||
toAddresses: {
|
||||
type: 'string',
|
||||
description: 'Comma-separated list of recipient email addresses',
|
||||
},
|
||||
subject: { type: 'string', description: 'Email subject line' },
|
||||
bodyHtml: { type: 'string', description: 'HTML email body' },
|
||||
bodyText: { type: 'string', description: 'Plain text email body' },
|
||||
templateName: { type: 'string', description: 'SES template name' },
|
||||
templateData: { type: 'string', description: 'JSON template variable data' },
|
||||
destinations: {
|
||||
type: 'string',
|
||||
description: 'JSON array of bulk email destinations',
|
||||
},
|
||||
subjectPart: { type: 'string', description: 'Template subject line' },
|
||||
htmlPart: { type: 'string', description: 'HTML body of the template' },
|
||||
textPart: { type: 'string', description: 'Plain text body of the template' },
|
||||
ccAddresses: { type: 'string', description: 'Comma-separated CC email addresses' },
|
||||
bccAddresses: { type: 'string', description: 'Comma-separated BCC email addresses' },
|
||||
replyToAddresses: { type: 'string', description: 'Comma-separated reply-to addresses' },
|
||||
defaultTemplateData: {
|
||||
type: 'string',
|
||||
description: 'Default JSON template data for bulk sends',
|
||||
},
|
||||
configurationSetName: { type: 'string', description: 'SES configuration set name' },
|
||||
pageSize: { type: 'number', description: 'Maximum number of results to return' },
|
||||
nextToken: { type: 'string', description: 'Pagination token from previous response' },
|
||||
},
|
||||
outputs: {
|
||||
messageId: {
|
||||
type: 'string',
|
||||
description: 'SES message ID (send_email, send_templated_email)',
|
||||
condition: { field: 'operation', value: ['send_email', 'send_templated_email'] },
|
||||
},
|
||||
results: {
|
||||
type: 'array',
|
||||
description: 'Per-destination send results (send_bulk_email)',
|
||||
condition: { field: 'operation', value: 'send_bulk_email' },
|
||||
},
|
||||
successCount: {
|
||||
type: 'number',
|
||||
description: 'Number of successfully sent emails (send_bulk_email)',
|
||||
condition: { field: 'operation', value: 'send_bulk_email' },
|
||||
},
|
||||
failureCount: {
|
||||
type: 'number',
|
||||
description: 'Number of failed email sends (send_bulk_email)',
|
||||
condition: { field: 'operation', value: 'send_bulk_email' },
|
||||
},
|
||||
identities: {
|
||||
type: 'array',
|
||||
description: 'List of verified email identities (list_identities)',
|
||||
condition: { field: 'operation', value: 'list_identities' },
|
||||
},
|
||||
nextToken: {
|
||||
type: 'string',
|
||||
description: 'Pagination token for the next page (list_identities, list_templates)',
|
||||
condition: { field: 'operation', value: ['list_identities', 'list_templates'] },
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
description: 'Number of items returned (list_identities, list_templates)',
|
||||
condition: { field: 'operation', value: ['list_identities', 'list_templates'] },
|
||||
},
|
||||
sendingEnabled: {
|
||||
type: 'boolean',
|
||||
description: 'Whether email sending is enabled (get_account)',
|
||||
condition: { field: 'operation', value: 'get_account' },
|
||||
},
|
||||
max24HourSend: {
|
||||
type: 'number',
|
||||
description: 'Maximum emails per 24 hours (get_account)',
|
||||
condition: { field: 'operation', value: 'get_account' },
|
||||
},
|
||||
maxSendRate: {
|
||||
type: 'number',
|
||||
description: 'Maximum emails per second (get_account)',
|
||||
condition: { field: 'operation', value: 'get_account' },
|
||||
},
|
||||
sentLast24Hours: {
|
||||
type: 'number',
|
||||
description: 'Emails sent in the last 24 hours (get_account)',
|
||||
condition: { field: 'operation', value: 'get_account' },
|
||||
},
|
||||
templateName: {
|
||||
type: 'string',
|
||||
description: 'Template name (get_template)',
|
||||
condition: { field: 'operation', value: 'get_template' },
|
||||
},
|
||||
subjectPart: {
|
||||
type: 'string',
|
||||
description: 'Template subject (get_template)',
|
||||
condition: { field: 'operation', value: 'get_template' },
|
||||
},
|
||||
textPart: {
|
||||
type: 'string',
|
||||
description: 'Template plain text body (get_template)',
|
||||
condition: { field: 'operation', value: 'get_template' },
|
||||
},
|
||||
htmlPart: {
|
||||
type: 'string',
|
||||
description: 'Template HTML body (get_template)',
|
||||
condition: { field: 'operation', value: 'get_template' },
|
||||
},
|
||||
templates: {
|
||||
type: 'array',
|
||||
description: 'List of email templates (list_templates)',
|
||||
condition: { field: 'operation', value: 'list_templates' },
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Confirmation message (create_template, delete_template)',
|
||||
condition: { field: 'operation', value: ['create_template', 'delete_template'] },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -77,6 +77,15 @@ export const STSBlock: BlockConfig<STSBaseResponse> = {
|
||||
required: false,
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'policy',
|
||||
title: 'Session Policy (JSON)',
|
||||
type: 'long-input',
|
||||
placeholder: '{"Version":"2012-10-17","Statement":[...]}',
|
||||
condition: { field: 'operation', value: 'assume_role' },
|
||||
required: false,
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'externalId',
|
||||
title: 'External ID',
|
||||
@@ -154,6 +163,7 @@ export const STSBlock: BlockConfig<STSBaseResponse> = {
|
||||
const parsed = Number.parseInt(String(durationSeconds), 10)
|
||||
if (!Number.isNaN(parsed)) result.durationSeconds = parsed
|
||||
}
|
||||
if (rest.policy) result.policy = rest.policy
|
||||
if (rest.externalId) result.externalId = rest.externalId
|
||||
if (rest.serialNumber) result.serialNumber = rest.serialNumber
|
||||
if (rest.tokenCode) result.tokenCode = rest.tokenCode
|
||||
@@ -184,7 +194,8 @@ export const STSBlock: BlockConfig<STSBaseResponse> = {
|
||||
secretAccessKey: { type: 'string', description: 'AWS secret access key' },
|
||||
roleArn: { type: 'string', description: 'ARN of the role to assume' },
|
||||
roleSessionName: { type: 'string', description: 'Session name for the assumed role' },
|
||||
durationSeconds: { type: 'number', description: 'Session duration in seconds' },
|
||||
durationSeconds: { type: 'string', description: 'Session duration in seconds' },
|
||||
policy: { type: 'string', description: 'JSON IAM session policy to restrict permissions' },
|
||||
externalId: { type: 'string', description: 'External ID for cross-account access' },
|
||||
serialNumber: { type: 'string', description: 'MFA device serial number' },
|
||||
tokenCode: { type: 'string', description: 'MFA token code' },
|
||||
@@ -193,43 +204,47 @@ export const STSBlock: BlockConfig<STSBaseResponse> = {
|
||||
outputs: {
|
||||
accessKeyId: {
|
||||
type: 'string',
|
||||
description: 'Temporary access key ID',
|
||||
description: 'Temporary access key ID (assume_role, get_session_token)',
|
||||
},
|
||||
secretAccessKey: {
|
||||
type: 'string',
|
||||
description: 'Temporary secret access key',
|
||||
description: 'Temporary secret access key (assume_role, get_session_token)',
|
||||
},
|
||||
sessionToken: {
|
||||
type: 'string',
|
||||
description: 'Temporary session token',
|
||||
description: 'Temporary session token (assume_role, get_session_token)',
|
||||
},
|
||||
expiration: {
|
||||
type: 'string',
|
||||
description: 'Credential expiration timestamp',
|
||||
description: 'Credential expiration timestamp (assume_role, get_session_token)',
|
||||
},
|
||||
assumedRoleArn: {
|
||||
type: 'string',
|
||||
description: 'ARN of the assumed role',
|
||||
description: 'ARN of the assumed role (assume_role only)',
|
||||
},
|
||||
assumedRoleId: {
|
||||
type: 'string',
|
||||
description: 'Assumed role ID with session name',
|
||||
},
|
||||
account: {
|
||||
type: 'string',
|
||||
description: 'AWS account ID',
|
||||
},
|
||||
arn: {
|
||||
type: 'string',
|
||||
description: 'ARN of the calling entity',
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
description: 'Unique identifier of the calling entity',
|
||||
description: 'Assumed role ID with session name (assume_role only)',
|
||||
},
|
||||
packedPolicySize: {
|
||||
type: 'number',
|
||||
description: 'Percentage of allowed policy size used',
|
||||
description: 'Percentage of allowed policy size used (assume_role only)',
|
||||
},
|
||||
sourceIdentity: {
|
||||
type: 'string',
|
||||
description: 'Source identity set on the role session (assume_role only)',
|
||||
},
|
||||
account: {
|
||||
type: 'string',
|
||||
description: 'AWS account ID (get_caller_identity, get_access_key_info)',
|
||||
},
|
||||
arn: {
|
||||
type: 'string',
|
||||
description: 'ARN of the calling entity (get_caller_identity only)',
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
description: 'Unique identifier of the calling entity (get_caller_identity only)',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ import { HuggingFaceBlock } from '@/blocks/blocks/huggingface'
|
||||
import { HumanInTheLoopBlock } from '@/blocks/blocks/human_in_the_loop'
|
||||
import { HunterBlock } from '@/blocks/blocks/hunter'
|
||||
import { IAMBlock } from '@/blocks/blocks/iam'
|
||||
import { IdentityCenterBlock } from '@/blocks/blocks/identity_center'
|
||||
import { ImageGeneratorBlock } from '@/blocks/blocks/image_generator'
|
||||
import { ImapBlock } from '@/blocks/blocks/imap'
|
||||
import { IncidentioBlock } from '@/blocks/blocks/incidentio'
|
||||
@@ -174,6 +175,7 @@ import { SendGridBlock } from '@/blocks/blocks/sendgrid'
|
||||
import { SentryBlock } from '@/blocks/blocks/sentry'
|
||||
import { SerperBlock } from '@/blocks/blocks/serper'
|
||||
import { ServiceNowBlock } from '@/blocks/blocks/servicenow'
|
||||
import { SESBlock } from '@/blocks/blocks/ses'
|
||||
import { SftpBlock } from '@/blocks/blocks/sftp'
|
||||
import { SharepointBlock } from '@/blocks/blocks/sharepoint'
|
||||
import { ShopifyBlock } from '@/blocks/blocks/shopify'
|
||||
@@ -334,6 +336,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
human_in_the_loop: HumanInTheLoopBlock,
|
||||
hunter: HunterBlock,
|
||||
iam: IAMBlock,
|
||||
identity_center: IdentityCenterBlock,
|
||||
image_generator: ImageGeneratorBlock,
|
||||
imap: ImapBlock,
|
||||
incidentio: IncidentioBlock,
|
||||
@@ -429,6 +432,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
smtp: SmtpBlock,
|
||||
spotify: SpotifyBlock,
|
||||
secrets_manager: SecretsManagerBlock,
|
||||
ses: SESBlock,
|
||||
sqs: SQSBlock,
|
||||
ssh: SSHBlock,
|
||||
sts: STSBlock,
|
||||
|
||||
@@ -4681,6 +4681,24 @@ export function IAMIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function IdentityCenterIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'>
|
||||
<defs>
|
||||
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id='identityCenterGradient'>
|
||||
<stop stopColor='#BD0816' offset='0%' />
|
||||
<stop stopColor='#FF5252' offset='100%' />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect fill='url(#identityCenterGradient)' width='80' height='80' />
|
||||
<path
|
||||
d='M46.694,46.8194562 C47.376,46.1374562 47.376,45.0294562 46.694,44.3474562 C46.353,44.0074562 45.906,43.8374562 45.459,43.8374562 C45.01,43.8374562 44.563,44.0074562 44.222,44.3474562 C43.542,45.0284562 43.542,46.1384562 44.222,46.8194562 C44.905,47.5014562 46.013,47.4994562 46.694,46.8194562 M47.718,47.1374562 L51.703,51.1204562 L50.996,51.8274562 L49.868,50.6994562 L48.793,51.7754562 L48.086,51.0684562 L49.161,49.9924562 L47.011,47.8444562 C46.545,48.1654562 46.003,48.3294562 45.458,48.3294562 C44.755,48.3294562 44.051,48.0624562 43.515,47.5264562 C42.445,46.4554562 42.445,44.7124562 43.515,43.6404562 C44.586,42.5714562 46.329,42.5694562 47.401,43.6404562 C48.351,44.5904562 48.455,46.0674562 47.718,47.1374562 M53,44.1014562 C53,46.1684562 51.505,47.0934562 50.023,47.0934562 L50.023,46.0934562 C50.487,46.0934562 52,45.9494562 52,44.1014562 C52,43.0044562 51.353,42.3894562 49.905,42.1084562 C49.68,42.0654562 49.514,41.8754562 49.501,41.6484562 C49.446,40.7444562 48.987,40.1124562 48.384,40.1124562 C48.084,40.1124562 47.854,40.2424562 47.616,40.5464562 C47.506,40.6884562 47.324,40.7594562 47.147,40.7324562 C46.968,40.7054562 46.818,40.5844562 46.755,40.4144562 C46.577,39.9434562 46.211,39.4334562 45.723,38.9774562 C45.231,38.5094562 43.883,37.5074562 41.972,38.2734562 C40.885,38.7054562 40.034,39.9494562 40.034,41.1074562 C40.034,41.2354562 40.043,41.3624562 40.058,41.4884562 C40.061,41.5094562 40.062,41.5304562 40.062,41.5514562 C40.062,41.7994562 39.882,42.0064562 39.645,42.0464562 C38.886,42.2394562 38,42.7454562 38,44.0554562 L38.005,44.2104562 C38.069,45.3254562 39.252,45.9954562 40.358,45.9984562 L41,45.9984562 L41,46.9984562 L40.357,46.9984562 C38.536,46.9944562 37.095,45.8194562 37.006,44.2644562 C37.003,44.1944562 37,44.1244562 37,44.0554562 C37,42.6944562 37.752,41.6484562 39.035,41.1884562 C39.034,41.1614562 39.034,41.1344562 39.034,41.1074562 C39.034,39.5434562 40.138,37.9254562 41.602,37.3434562 C43.298,36.6654562 45.095,37.0034562 46.409,38.2494562 C46.706,38.5274562 47.076,38.9264562 47.372,39.4134562 C47.673,39.2124562 48.008,39.1124562 48.384,39.1124562 C49.257,39.1124562 50.231,39.7714562 50.458,41.2074562 C52.145,41.6324562 53,42.6054562 53,44.1014562 M27,53 L27,27 L53,27 L53,34 L51,34 L51,29 L29,29 L29,51 L51,51 L51,46 L53,46 L53,53 Z'
|
||||
fill='#FFFFFF'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function STSIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'>
|
||||
@@ -4699,6 +4717,24 @@ export function STSIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function SESIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'>
|
||||
<defs>
|
||||
<linearGradient x1='0%' y1='100%' x2='100%' y2='0%' id='sesGradient'>
|
||||
<stop stopColor='#BD0816' offset='0%' />
|
||||
<stop stopColor='#FF5252' offset='100%' />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect fill='url(#sesGradient)' width='80' height='80' />
|
||||
<path
|
||||
d='M57,60.999875 C57,59.373846 55.626,57.9998214 54,57.9998214 C52.374,57.9998214 51,59.373846 51,60.999875 C51,62.625904 52.374,63.9999286 54,63.9999286 C55.626,63.9999286 57,62.625904 57,60.999875 L57,60.999875 Z M40,59.9998571 C38.374,59.9998571 37,61.3738817 37,62.9999107 C37,64.6259397 38.374,65.9999643 40,65.9999643 C41.626,65.9999643 43,64.6259397 43,62.9999107 C43,61.3738817 41.626,59.9998571 40,59.9998571 L40,59.9998571 Z M26,57.9998214 C24.374,57.9998214 23,59.373846 23,60.999875 C23,62.625904 24.374,63.9999286 26,63.9999286 C27.626,63.9999286 29,62.625904 29,60.999875 C29,59.373846 27.626,57.9998214 26,57.9998214 L26,57.9998214 Z M28.605,42.9995536 L51.395,42.9995536 L43.739,36.1104305 L40.649,38.7584778 C40.463,38.9194807 40.23,38.9994821 39.999,38.9994821 C39.768,38.9994821 39.535,38.9194807 39.349,38.7584778 L36.26,36.1104305 L28.605,42.9995536 Z M27,28.1732888 L27,41.7545313 L34.729,34.7984071 L27,28.1732888 Z M51.297,26.9992678 L28.703,26.9992678 L39.999,36.6824408 L51.297,26.9992678 Z M53,41.7545313 L53,28.1732888 L45.271,34.7974071 L53,41.7545313 Z M59,60.999875 C59,63.7099234 56.71,65.9999643 54,65.9999643 C51.29,65.9999643 49,63.7099234 49,60.999875 C49,58.6308327 50.75,56.5837961 53,56.1057876 L53,52.9997321 L41,52.9997321 L41,58.1058233 C43.25,58.5838319 45,60.6308684 45,62.9999107 C45,65.7099591 42.71,68 40,68 C37.29,68 35,65.7099591 35,62.9999107 C35,60.6308684 36.75,58.5838319 39,58.1058233 L39,52.9997321 L27,52.9997321 L27,56.1057876 C29.25,56.5837961 31,58.6308327 31,60.999875 C31,63.7099234 28.71,65.9999643 26,65.9999643 C23.29,65.9999643 21,63.7099234 21,60.999875 C21,58.6308327 22.75,56.5837961 25,56.1057876 L25,51.9997143 C25,51.4477044 25.447,50.9996964 26,50.9996964 L39,50.9996964 L39,44.9995893 L26,44.9995893 C25.447,44.9995893 25,44.5515813 25,43.9995714 L25,25.99925 C25,25.4472401 25.447,24.9992321 26,24.9992321 L54,24.9992321 C54.553,24.9992321 55,25.4472401 55,25.99925 L55,43.9995714 C55,44.5515813 54.553,44.9995893 54,44.9995893 L41,44.9995893 L41,50.9996964 L54,50.9996964 C54.553,50.9996964 55,51.4477044 55,51.9997143 L55,56.1057876 C57.25,56.5837961 59,58.6308327 59,60.999875 L59,60.999875 Z M68,39.9995 C68,45.9066055 66.177,51.5597064 62.727,56.3447919 L61.104,55.174771 C64.307,50.7316916 66,45.4845979 66,39.9995 C66,25.664244 54.337,14.0000357 40.001,14.0000357 C25.664,14.0000357 14,25.664244 14,39.9995 C14,45.4845979 15.693,50.7316916 18.896,55.174771 L17.273,56.3447919 C13.823,51.5597064 12,45.9066055 12,39.9995 C12,24.5612243 24.561,12 39.999,12 C55.438,12 68,24.5612243 68,39.9995 L68,39.9995 Z'
|
||||
fill='#FFFFFF'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function SecretsManagerIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 80 80' xmlns='http://www.w3.org/2000/svg'>
|
||||
|
||||
@@ -23,6 +23,14 @@ export const SUBBLOCK_ID_MIGRATIONS: Record<string, Record<string, string>> = {
|
||||
knowledge: {
|
||||
knowledgeBaseId: 'knowledgeBaseSelector',
|
||||
},
|
||||
dynamodb: {
|
||||
key: 'getKey',
|
||||
filterExpression: 'queryFilterExpression',
|
||||
expressionAttributeNames: 'queryExpressionAttributeNames',
|
||||
expressionAttributeValues: 'queryExpressionAttributeValues',
|
||||
limit: 'queryLimit',
|
||||
conditionExpression: 'updateConditionExpression',
|
||||
},
|
||||
ashby: {
|
||||
emailType: '_removed_emailType',
|
||||
phoneType: '_removed_phoneType',
|
||||
|
||||
@@ -41,10 +41,14 @@
|
||||
"@aws-sdk/client-cloudwatch-logs": "3.940.0",
|
||||
"@aws-sdk/client-dynamodb": "3.940.0",
|
||||
"@aws-sdk/client-iam": "3.1029.0",
|
||||
"@aws-sdk/client-identitystore": "3.1032.0",
|
||||
"@aws-sdk/client-organizations": "3.1032.0",
|
||||
"@aws-sdk/client-rds-data": "3.940.0",
|
||||
"@aws-sdk/client-s3": "^3.779.0",
|
||||
"@aws-sdk/client-secrets-manager": "3.940.0",
|
||||
"@aws-sdk/client-sesv2": "3.940.0",
|
||||
"@aws-sdk/client-sqs": "3.947.0",
|
||||
"@aws-sdk/client-sso-admin": "3.1032.0",
|
||||
"@aws-sdk/client-sts": "3.1029.0",
|
||||
"@aws-sdk/lib-dynamodb": "3.940.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.779.0",
|
||||
|
||||
@@ -94,6 +94,25 @@ export const describeAlarmsTool: ToolConfig<
|
||||
alarms: {
|
||||
type: 'array',
|
||||
description: 'List of CloudWatch alarms with state and configuration',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
alarmName: { type: 'string', description: 'Alarm name' },
|
||||
alarmArn: { type: 'string', description: 'Alarm ARN' },
|
||||
stateValue: {
|
||||
type: 'string',
|
||||
description: 'Current state (OK, ALARM, INSUFFICIENT_DATA)',
|
||||
},
|
||||
stateReason: { type: 'string', description: 'Human-readable reason for the state' },
|
||||
metricName: { type: 'string', description: 'Metric name (MetricAlarm only)' },
|
||||
namespace: { type: 'string', description: 'Metric namespace (MetricAlarm only)' },
|
||||
threshold: { type: 'number', description: 'Threshold value (MetricAlarm only)' },
|
||||
stateUpdatedTimestamp: {
|
||||
type: 'number',
|
||||
description: 'Epoch ms when state last changed',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -77,6 +77,19 @@ export const describeLogGroupsTool: ToolConfig<
|
||||
},
|
||||
|
||||
outputs: {
|
||||
logGroups: { type: 'array', description: 'List of CloudWatch log groups with metadata' },
|
||||
logGroups: {
|
||||
type: 'array',
|
||||
description: 'List of CloudWatch log groups with metadata',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
logGroupName: { type: 'string', description: 'Log group name' },
|
||||
arn: { type: 'string', description: 'Log group ARN' },
|
||||
storedBytes: { type: 'number', description: 'Total stored bytes' },
|
||||
retentionInDays: { type: 'number', description: 'Retention period in days (if set)' },
|
||||
creationTime: { type: 'number', description: 'Creation time in epoch milliseconds' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -86,7 +86,27 @@ export const describeLogStreamsTool: ToolConfig<
|
||||
outputs: {
|
||||
logStreams: {
|
||||
type: 'array',
|
||||
description: 'List of log streams with metadata',
|
||||
description:
|
||||
'List of log streams with metadata, sorted by last event time (most recent first) unless a prefix filter is applied',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
logStreamName: { type: 'string', description: 'Log stream name' },
|
||||
lastEventTimestamp: {
|
||||
type: 'number',
|
||||
description: 'Timestamp of the last log event in epoch milliseconds',
|
||||
},
|
||||
firstEventTimestamp: {
|
||||
type: 'number',
|
||||
description: 'Timestamp of the first log event in epoch milliseconds',
|
||||
},
|
||||
creationTime: {
|
||||
type: 'number',
|
||||
description: 'Stream creation time in epoch milliseconds',
|
||||
},
|
||||
storedBytes: { type: 'number', description: 'Total stored bytes' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -101,6 +101,14 @@ export const getLogEventsTool: ToolConfig<
|
||||
events: {
|
||||
type: 'array',
|
||||
description: 'Log events with timestamp, message, and ingestion time',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
timestamp: { type: 'number', description: 'Event timestamp in epoch milliseconds' },
|
||||
message: { type: 'string', description: 'Log event message' },
|
||||
ingestionTime: { type: 'number', description: 'Ingestion time in epoch milliseconds' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -113,7 +113,22 @@ export const getMetricStatisticsTool: ToolConfig<
|
||||
},
|
||||
|
||||
outputs: {
|
||||
label: { type: 'string', description: 'Metric label' },
|
||||
datapoints: { type: 'array', description: 'Datapoints with timestamp and statistics values' },
|
||||
label: { type: 'string', description: 'Metric label returned by CloudWatch' },
|
||||
datapoints: {
|
||||
type: 'array',
|
||||
description: 'Datapoints sorted by timestamp with statistics values',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
timestamp: { type: 'number', description: 'Datapoint timestamp in epoch milliseconds' },
|
||||
average: { type: 'number', description: 'Average statistic value' },
|
||||
sum: { type: 'number', description: 'Sum statistic value' },
|
||||
minimum: { type: 'number', description: 'Minimum statistic value' },
|
||||
maximum: { type: 'number', description: 'Maximum statistic value' },
|
||||
sampleCount: { type: 'number', description: 'Sample count statistic value' },
|
||||
unit: { type: 'string', description: 'Unit of the metric' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -91,6 +91,17 @@ export const listMetricsTool: ToolConfig<
|
||||
},
|
||||
|
||||
outputs: {
|
||||
metrics: { type: 'array', description: 'List of metrics with namespace, name, and dimensions' },
|
||||
metrics: {
|
||||
type: 'array',
|
||||
description: 'List of metrics with namespace, name, and dimensions',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
namespace: { type: 'string', description: 'Metric namespace (e.g., AWS/EC2)' },
|
||||
metricName: { type: 'string', description: 'Metric name (e.g., CPUUtilization)' },
|
||||
dimensions: { type: 'array', description: 'Array of name/value dimension pairs' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -97,11 +97,25 @@ export const queryLogsTool: ToolConfig<CloudWatchQueryLogsParams, CloudWatchQuer
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: { type: 'array', description: 'Query result rows' },
|
||||
results: {
|
||||
type: 'array',
|
||||
description: 'Query result rows (each row is a key/value map of field name to value)',
|
||||
},
|
||||
statistics: {
|
||||
type: 'object',
|
||||
description: 'Query statistics (bytesScanned, recordsMatched, recordsScanned)',
|
||||
description: 'Query statistics',
|
||||
properties: {
|
||||
bytesScanned: { type: 'number', description: 'Total bytes of log data scanned' },
|
||||
recordsMatched: {
|
||||
type: 'number',
|
||||
description: 'Number of log records that matched the query',
|
||||
},
|
||||
recordsScanned: { type: 'number', description: 'Total log records scanned' },
|
||||
},
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Query completion status (Complete, Failed, Cancelled, or Timeout)',
|
||||
},
|
||||
status: { type: 'string', description: 'Query completion status' },
|
||||
},
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user