Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b166d86d4 | |||
| f956d977ca | |||
| 9f38313e59 |
@@ -62,21 +62,14 @@ cd packages/api && bun run scripts/service-matrix.ts
|
|||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| Compute | Compute | Yes (list, inspect, create, delete) | Yes (list, inspect, create, delete) | No |
|
| Compute | Compute | Yes (list, inspect, create, delete) | Yes (list, inspect, create, delete) | No |
|
||||||
| Compute | EKS / AKS / GKE | Yes (list, inspect) | Yes (list, inspect) | Yes (list, create, inspect, delete) |
|
| Compute | EKS / AKS / GKE | Yes (list, inspect) | Yes (list, inspect) | Yes (list, create, inspect, delete) |
|
||||||
| Compute | Serverless | Yes (list, create, update, inspect, delete) | Runtime gap | Yes (list, create, inspect, delete) |
|
| Compute | Serverless | Yes (list, create, inspect, delete) | Runtime gap | Yes (list, create, inspect, delete) |
|
||||||
| Compute | Containers / Cloud Run | No | No | Yes (list, create, delete, inspect) |
|
| Compute | Containers / Cloud Run | No | No | Yes (list, create, delete, inspect) |
|
||||||
| Compute | ECS | Yes (list, create, delete, inspect) | No | No |
|
| Compute | CodeBuild | Yes (list, create, delete, inspect) | No | No |
|
||||||
| Compute | Fargate | Yes (list, create, delete, inspect) | No | No |
|
|
||||||
| Compute | Lightsail | Yes (list, create, inspect, update, delete) | No | No |
|
|
||||||
| Developer Tools | CodeBuild | Yes (list, create, delete, inspect) | No | No |
|
|
||||||
| Developer Tools | CodePipeline | Yes (list, create, delete, inspect) | No | No |
|
|
||||||
| Developer Tools | CodeDeploy | Yes (list, create, delete, inspect) | No | No |
|
|
||||||
| Storage | Storage | Yes (list, create, delete, inspect) | Yes (list, create, delete, inspect) | Yes (list, create, delete, inspect) |
|
| Storage | Storage | Yes (list, create, delete, inspect) | Yes (list, create, delete, inspect) | Yes (list, create, delete, inspect) |
|
||||||
| Storage | ECR / Elastic Container Registry | Yes (list, create, delete, inspect) | No | No |
|
|
||||||
| Databases | Database | Yes (list, create, update, delete, inspect) | Yes (list, create, delete, inspect) | Yes (list, create, inspect, delete) |
|
| Databases | Database | Yes (list, create, update, delete, inspect) | Yes (list, create, delete, inspect) | Yes (list, create, inspect, delete) |
|
||||||
| Databases | DynamoDB / Cosmos DB NoSQL / NoSQL | Yes (list, create, delete, inspect) | Yes (list, create, delete, inspect) | No |
|
| Databases | DynamoDB / Cosmos DB NoSQL / NoSQL | Yes (list, create, delete, inspect) | Yes (list, create, delete, inspect) | No |
|
||||||
| Databases | ElastiCache | Yes (list, create, delete, inspect) | No | No |
|
|
||||||
| Networking | Networking | Yes (list) | No | No |
|
| Networking | Networking | Yes (list) | No | No |
|
||||||
| Networking | ELB / Load Balancing | Yes (list, create, update, delete, inspect) | No | No |
|
| Networking | ELB / Load Balancing | Yes (list, create, delete, inspect) | No | No |
|
||||||
| Networking | ELB Target Groups / Target Groups | Yes (list, create, delete, inspect) | No | No |
|
| Networking | ELB Target Groups / Target Groups | Yes (list, create, delete, inspect) | No | No |
|
||||||
| Integration | SQS / Messaging / Pub/Sub | Yes (list, create, inspect, delete) | Yes (list, create, delete, inspect) | Yes (list, create, inspect, delete) |
|
| Integration | SQS / Messaging / Pub/Sub | Yes (list, create, inspect, delete) | Yes (list, create, delete, inspect) | Yes (list, create, inspect, delete) |
|
||||||
| Integration | API Gateway | Yes (list, create, delete, inspect) | No | No |
|
| Integration | API Gateway | Yes (list, create, delete, inspect) | No | No |
|
||||||
@@ -84,14 +77,11 @@ cd packages/api && bun run scripts/service-matrix.ts
|
|||||||
| Integration | EventBridge / Events | Yes (list, create, delete, inspect) | No | No |
|
| Integration | EventBridge / Events | Yes (list, create, delete, inspect) | No | No |
|
||||||
| Integration | SES Mailbox / Email | Yes (list, inspect) | No | No |
|
| Integration | SES Mailbox / Email | Yes (list, inspect) | No | No |
|
||||||
| Integration | Cloud Scheduler | No | No | Yes (list, create, delete, inspect) |
|
| Integration | Cloud Scheduler | No | No | Yes (list, create, delete, inspect) |
|
||||||
| Integration | Step Functions / Workflows | Yes (list, create, update, delete, inspect) | No | No |
|
| Integration | CodePipeline | Yes (list, create, delete, inspect) | No | No |
|
||||||
| Analytics | MSK / Managed Streaming for Kafka | Yes (legacy page) | No | No |
|
| Integration | Step Functions / Workflows | Yes (list, create, delete, inspect) | No | No |
|
||||||
| Analytics | MSK Connect / Managed Streaming for Kafka Connect | Yes (legacy page) | No | No |
|
| Provisioning | CloudFormation / Infrastructure as Code | Yes (list, create, delete, inspect) | No | No |
|
||||||
| Analytics | Kinesis / Kinesis Data Streams | Yes (list, create, inspect, delete) | No | No |
|
|
||||||
| Provisioning | CloudFormation / Infrastructure as Code | Yes (list, create, update, delete, inspect) | No | No |
|
|
||||||
| Provisioning | AppConfig / Configuration | Yes (list, create, delete, inspect) | No | No |
|
| Provisioning | AppConfig / Configuration | Yes (list, create, delete, inspect) | No | No |
|
||||||
| Security | Identity | Yes (list, create, delete, inspect) | No | No |
|
| Security | Identity | Yes (list, create, delete, inspect) | No | No |
|
||||||
| Security | Cognito / User Directories | Yes (list, create, delete, inspect) | No | No |
|
|
||||||
| Security | Secrets Manager / Key Vault / Secret Manager | Yes (list, create, inspect, delete) | Yes (list, create, delete, inspect) | Yes (list, create, inspect, delete) |
|
| Security | Secrets Manager / Key Vault / Secret Manager | Yes (list, create, inspect, delete) | Yes (list, create, delete, inspect) | Yes (list, create, inspect, delete) |
|
||||||
| Security | KMS / Key Management | Yes (list, create, delete, inspect) | No | No |
|
| Security | KMS / Key Management | Yes (list, create, delete, inspect) | No | No |
|
||||||
| Security | Parameter Store | Yes (list, create, delete, inspect) | No | No |
|
| Security | Parameter Store | Yes (list, create, delete, inspect) | No | No |
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
"@aws-sdk/client-codebuild": "^3.1076.0",
|
"@aws-sdk/client-codebuild": "^3.1076.0",
|
||||||
"@aws-sdk/client-codedeploy": "^3.1144.0",
|
"@aws-sdk/client-codedeploy": "^3.1144.0",
|
||||||
"@aws-sdk/client-codepipeline": "^3.1076.0",
|
"@aws-sdk/client-codepipeline": "^3.1076.0",
|
||||||
"@aws-sdk/client-cognito-identity-provider": "^3.1146.0",
|
|
||||||
"@aws-sdk/client-dynamodb": "^3.1088.0",
|
"@aws-sdk/client-dynamodb": "^3.1088.0",
|
||||||
"@aws-sdk/client-ec2": "^3.1076.0",
|
"@aws-sdk/client-ec2": "^3.1076.0",
|
||||||
"@aws-sdk/client-ecr": "^3.1076.0",
|
"@aws-sdk/client-ecr": "^3.1076.0",
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
(() => {
|
(() => {
|
||||||
|
// Shared with the console (useTheme.ts, index.html) so one choice applies everywhere.
|
||||||
|
const THEME_KEY = 'floci-theme';
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
const buttons = document.querySelectorAll('.tg button');
|
const buttons = document.querySelectorAll('.tg button');
|
||||||
const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
@@ -6,7 +8,7 @@
|
|||||||
function setTheme(theme) {
|
function setTheme(theme) {
|
||||||
root.setAttribute('data-theme', theme);
|
root.setAttribute('data-theme', theme);
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('thecloud-theme', theme);
|
localStorage.setItem(THEME_KEY, theme);
|
||||||
} catch (ignored) {
|
} catch (ignored) {
|
||||||
// Theme persistence is optional when browser storage is unavailable.
|
// Theme persistence is optional when browser storage is unavailable.
|
||||||
}
|
}
|
||||||
@@ -15,7 +17,7 @@
|
|||||||
|
|
||||||
let initialTheme = matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
let initialTheme = matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||||
try {
|
try {
|
||||||
const savedTheme = localStorage.getItem('thecloud-theme');
|
const savedTheme = localStorage.getItem(THEME_KEY) ?? localStorage.getItem('thecloud-theme');
|
||||||
if (savedTheme === 'dark' || savedTheme === 'light' || savedTheme === 'cyber') {
|
if (savedTheme === 'dark' || savedTheme === 'light' || savedTheme === 'cyber') {
|
||||||
initialTheme = savedTheme;
|
initialTheme = savedTheme;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,257 +0,0 @@
|
|||||||
import {describe, expect, test} from 'bun:test'
|
|
||||||
import {
|
|
||||||
AdminCreateUserCommand,
|
|
||||||
AdminDeleteUserCommand,
|
|
||||||
AdminGetUserCommand,
|
|
||||||
AdminSetUserPasswordCommand,
|
|
||||||
CreateUserPoolClientCommand,
|
|
||||||
CreateUserPoolCommand,
|
|
||||||
DeleteUserPoolCommand,
|
|
||||||
DescribeUserPoolCommand,
|
|
||||||
ListUserPoolClientsCommand,
|
|
||||||
ListUserPoolsCommand,
|
|
||||||
ListUsersCommand,
|
|
||||||
type CognitoIdentityProviderClient,
|
|
||||||
} from '@aws-sdk/client-cognito-identity-provider'
|
|
||||||
import {AwsUserPoolsAdapter} from './AwsUserPoolsAdapter'
|
|
||||||
import {ValidationError} from '../cloud-spi/errors'
|
|
||||||
|
|
||||||
type SendResult = Record<string, unknown>
|
|
||||||
|
|
||||||
function stubCognito(handler: (command: object) => SendResult | Promise<SendResult>) {
|
|
||||||
const sent: object[] = []
|
|
||||||
const client = {
|
|
||||||
async send(command: object) {
|
|
||||||
sent.push(command)
|
|
||||||
return handler(command)
|
|
||||||
},
|
|
||||||
} as unknown as CognitoIdentityProviderClient
|
|
||||||
return {client, sent}
|
|
||||||
}
|
|
||||||
|
|
||||||
function awsError(name: string): Error {
|
|
||||||
return Object.assign(new Error(name), {name, $metadata: {httpStatusCode: 400}})
|
|
||||||
}
|
|
||||||
|
|
||||||
const POOL_ID = 'us-east-1_8e65d229a'
|
|
||||||
const CREATED = new Date('2026-10-23T16:01:22.000Z')
|
|
||||||
|
|
||||||
const pool = {
|
|
||||||
Id: POOL_ID,
|
|
||||||
Name: 'orders-users',
|
|
||||||
Arn: `arn:aws:cognito-idp:us-east-1:000000000000:userpool/${POOL_ID}`,
|
|
||||||
Status: 'Enabled',
|
|
||||||
CreationDate: CREATED,
|
|
||||||
UsernameAttributes: ['email'],
|
|
||||||
EstimatedNumberOfUsers: 1,
|
|
||||||
MfaConfiguration: 'OFF',
|
|
||||||
DeletionProtection: 'INACTIVE',
|
|
||||||
Policies: {PasswordPolicy: {MinimumLength: 8, RequireUppercase: true, RequireLowercase: true, RequireNumbers: true, RequireSymbols: true}},
|
|
||||||
}
|
|
||||||
|
|
||||||
const alice = {
|
|
||||||
Username: 'aa36ecc1-ed8a-43b5-84e9-988398c17436',
|
|
||||||
Attributes: [
|
|
||||||
{Name: 'sub', Value: 'aa36ecc1-ed8a-43b5-84e9-988398c17436'},
|
|
||||||
{Name: 'email', Value: 'alice@example.com'},
|
|
||||||
{Name: 'email_verified', Value: 'true'},
|
|
||||||
],
|
|
||||||
UserCreateDate: CREATED,
|
|
||||||
UserLastModifiedDate: CREATED,
|
|
||||||
Enabled: true,
|
|
||||||
UserStatus: 'CONFIRMED',
|
|
||||||
}
|
|
||||||
|
|
||||||
function runtimeStub() {
|
|
||||||
return stubCognito((command) => {
|
|
||||||
if (command instanceof CreateUserPoolCommand) return {UserPool: pool}
|
|
||||||
if (command instanceof DescribeUserPoolCommand) return {UserPool: pool}
|
|
||||||
if (command instanceof ListUserPoolClientsCommand) return {UserPoolClients: [{ClientId: 'a8e5c7d38d3b4a1a97c9d822de', ClientName: 'orders-users-client', UserPoolId: POOL_ID}]}
|
|
||||||
if (command instanceof AdminCreateUserCommand) return {User: {...alice, UserStatus: 'FORCE_CHANGE_PASSWORD'}}
|
|
||||||
if (command instanceof AdminGetUserCommand) return {...alice, UserAttributes: alice.Attributes}
|
|
||||||
return {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('AwsUserPoolsAdapter', () => {
|
|
||||||
test('lists every page of user pools and filters by name or id', async () => {
|
|
||||||
const {client, sent} = stubCognito((command) => {
|
|
||||||
if (!(command instanceof ListUserPoolsCommand)) return {}
|
|
||||||
return command.input.NextToken
|
|
||||||
? {UserPools: [{Id: 'us-east-1_second', Name: 'billing', Status: 'Enabled', CreationDate: CREATED}]}
|
|
||||||
: {UserPools: [{Id: POOL_ID, Name: 'orders-users', Status: 'Enabled', CreationDate: CREATED}], NextToken: 'page-2'}
|
|
||||||
})
|
|
||||||
const adapter = new AwsUserPoolsAdapter(client)
|
|
||||||
|
|
||||||
expect((await adapter.list()).map((resource) => resource.id)).toEqual([POOL_ID, 'us-east-1_second'])
|
|
||||||
expect(sent.filter((command) => command instanceof ListUserPoolsCommand)).toHaveLength(2)
|
|
||||||
expect((await adapter.list({search: 'BILL'})).map((resource) => resource.name)).toEqual(['billing'])
|
|
||||||
expect((await adapter.list())[0]).toMatchObject({type: 'user-pool', region: 'us-east-1', status: 'Enabled', createdAt: CREATED.toISOString()})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('lists pools with the details the inspector shows', async () => {
|
|
||||||
const {client} = stubCognito((command) => {
|
|
||||||
if (command instanceof ListUserPoolsCommand) return {UserPools: [{Id: POOL_ID, Name: 'orders-users', Status: 'Enabled', CreationDate: CREATED}]}
|
|
||||||
if (command instanceof DescribeUserPoolCommand) return {UserPool: pool}
|
|
||||||
if (command instanceof ListUserPoolClientsCommand) return {UserPoolClients: [{ClientId: 'a8e5c7d38d3b4a1a97c9d822de', ClientName: 'orders-users-client'}]}
|
|
||||||
return {}
|
|
||||||
})
|
|
||||||
|
|
||||||
const [listed] = await new AwsUserPoolsAdapter(client).list()
|
|
||||||
expect(listed.metadata).toMatchObject({signInWith: 'email', appClients: [{clientId: 'a8e5c7d38d3b4a1a97c9d822de', clientName: 'orders-users-client'}]})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('describes a pool with its sign-in method, password policy and app clients', async () => {
|
|
||||||
const resource = await new AwsUserPoolsAdapter(runtimeStub().client).get(POOL_ID)
|
|
||||||
|
|
||||||
expect(resource?.metadata).toEqual({
|
|
||||||
arn: pool.Arn,
|
|
||||||
signInWith: 'email',
|
|
||||||
passwordPolicy: '8+ characters with uppercase, lowercase, number, symbol',
|
|
||||||
estimatedNumberOfUsers: 1,
|
|
||||||
mfaConfiguration: 'OFF',
|
|
||||||
deletionProtection: 'INACTIVE',
|
|
||||||
appClients: [{clientId: 'a8e5c7d38d3b4a1a97c9d822de', clientName: 'orders-users-client'}],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('summarises a relaxed password policy without character rules', async () => {
|
|
||||||
const {client} = stubCognito((command) => {
|
|
||||||
if (command instanceof DescribeUserPoolCommand) return {UserPool: {...pool, Policies: {PasswordPolicy: {MinimumLength: 8, RequireUppercase: false, RequireLowercase: false, RequireNumbers: false, RequireSymbols: false}}}}
|
|
||||||
return {}
|
|
||||||
})
|
|
||||||
|
|
||||||
expect((await new AwsUserPoolsAdapter(client).get(POOL_ID))?.metadata.passwordPolicy).toBe('8+ characters')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns null for a pool the runtime does not know', async () => {
|
|
||||||
const {client} = stubCognito(() => { throw awsError('ResourceNotFoundException') })
|
|
||||||
|
|
||||||
expect(await new AwsUserPoolsAdapter(client).get('us-east-1_missing')).toBeNull()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('creates an email pool with a public app client by default', async () => {
|
|
||||||
const {client, sent} = runtimeStub()
|
|
||||||
await new AwsUserPoolsAdapter(client).create({values: {name: 'orders-users'}})
|
|
||||||
|
|
||||||
const createPool = sent.find((command) => command instanceof CreateUserPoolCommand) as CreateUserPoolCommand
|
|
||||||
expect(createPool.input).toEqual({PoolName: 'orders-users', UsernameAttributes: ['email'], AutoVerifiedAttributes: ['email']})
|
|
||||||
const createClient = sent.find((command) => command instanceof CreateUserPoolClientCommand) as CreateUserPoolClientCommand
|
|
||||||
expect(createClient.input).toEqual({
|
|
||||||
UserPoolId: POOL_ID,
|
|
||||||
ClientName: 'orders-users-client',
|
|
||||||
GenerateSecret: false,
|
|
||||||
ExplicitAuthFlows: ['ALLOW_USER_PASSWORD_AUTH', 'ALLOW_USER_SRP_AUTH', 'ALLOW_REFRESH_TOKEN_AUTH'],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('creates a username pool with a relaxed password policy and no app client', async () => {
|
|
||||||
const {client, sent} = runtimeStub()
|
|
||||||
await new AwsUserPoolsAdapter(client).create({values: {name: 'testers', signInWith: 'username', passwordPolicy: 'relaxed', createAppClient: 'no'}})
|
|
||||||
|
|
||||||
const createPool = sent.find((command) => command instanceof CreateUserPoolCommand) as CreateUserPoolCommand
|
|
||||||
expect(createPool.input).toEqual({
|
|
||||||
PoolName: 'testers',
|
|
||||||
Policies: {PasswordPolicy: {MinimumLength: 8, RequireUppercase: false, RequireLowercase: false, RequireNumbers: false, RequireSymbols: false}},
|
|
||||||
})
|
|
||||||
expect(sent.some((command) => command instanceof CreateUserPoolClientCommand)).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('removes the new pool when its app client cannot be created', async () => {
|
|
||||||
const {client, sent} = stubCognito((command) => {
|
|
||||||
if (command instanceof CreateUserPoolCommand) return {UserPool: pool}
|
|
||||||
if (command instanceof CreateUserPoolClientCommand) throw awsError('InvalidParameterException')
|
|
||||||
return {}
|
|
||||||
})
|
|
||||||
|
|
||||||
await expect(new AwsUserPoolsAdapter(client).create({values: {name: 'orders-users'}})).rejects.toThrow('InvalidParameterException')
|
|
||||||
expect((sent.at(-1) as DeleteUserPoolCommand).input).toEqual({UserPoolId: POOL_ID})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('rejects invalid create input before calling the runtime', async () => {
|
|
||||||
const {client, sent} = runtimeStub()
|
|
||||||
const adapter = new AwsUserPoolsAdapter(client)
|
|
||||||
|
|
||||||
await expect(adapter.create({values: {}})).rejects.toBeInstanceOf(ValidationError)
|
|
||||||
await expect(adapter.create({values: {name: 'bad/name'}})).rejects.toBeInstanceOf(ValidationError)
|
|
||||||
await expect(adapter.create({values: {name: 'ok', signInWith: 'phone'}})).rejects.toBeInstanceOf(ValidationError)
|
|
||||||
expect(sent).toHaveLength(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('deletes a pool by id', async () => {
|
|
||||||
const {client, sent} = runtimeStub()
|
|
||||||
await new AwsUserPoolsAdapter(client).delete(POOL_ID)
|
|
||||||
|
|
||||||
expect(sent[0]).toBeInstanceOf(DeleteUserPoolCommand)
|
|
||||||
expect((sent[0] as DeleteUserPoolCommand).input).toEqual({UserPoolId: POOL_ID})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('AwsUserPoolsAdapter users', () => {
|
|
||||||
test('lists users as items keyed by username with their email and status', async () => {
|
|
||||||
const {client, sent} = stubCognito(() => ({Users: [alice], PaginationToken: 'next-page'}))
|
|
||||||
const page = await new AwsUserPoolsAdapter(client).items.listItems(POOL_ID, {limit: 500, cursor: 'this-page'})
|
|
||||||
|
|
||||||
expect((sent[0] as ListUsersCommand).input).toEqual({UserPoolId: POOL_ID, Limit: 60, PaginationToken: 'this-page'})
|
|
||||||
expect(page.nextCursor).toBe('next-page')
|
|
||||||
expect(page.items).toEqual([{
|
|
||||||
id: alice.Username,
|
|
||||||
collectionId: null,
|
|
||||||
timestamp: CREATED.toISOString(),
|
|
||||||
body: {
|
|
||||||
username: alice.Username,
|
|
||||||
email: 'alice@example.com',
|
|
||||||
emailVerified: true,
|
|
||||||
status: 'CONFIRMED',
|
|
||||||
enabled: true,
|
|
||||||
lastModifiedAt: CREATED.toISOString(),
|
|
||||||
attributes: {sub: alice.Username, email: 'alice@example.com', email_verified: 'true'},
|
|
||||||
},
|
|
||||||
metadata: {},
|
|
||||||
}])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('creates a user whose password is permanent so it can sign in at once', async () => {
|
|
||||||
const {client, sent} = runtimeStub()
|
|
||||||
const user = await new AwsUserPoolsAdapter(client).items.putItem!(POOL_ID, {email: 'alice@example.com', password: ' Passw0rd! '})
|
|
||||||
|
|
||||||
expect((sent[0] as AdminCreateUserCommand).input).toEqual({
|
|
||||||
UserPoolId: POOL_ID,
|
|
||||||
Username: 'alice@example.com',
|
|
||||||
UserAttributes: [{Name: 'email', Value: 'alice@example.com'}, {Name: 'email_verified', Value: 'true'}],
|
|
||||||
TemporaryPassword: ' Passw0rd! ',
|
|
||||||
MessageAction: 'SUPPRESS',
|
|
||||||
})
|
|
||||||
expect((sent[1] as AdminSetUserPasswordCommand).input).toEqual({UserPoolId: POOL_ID, Username: alice.Username, Password: ' Passw0rd! ', Permanent: true})
|
|
||||||
expect(user.body.status).toBe('CONFIRMED')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('leaves the password temporary when asked', async () => {
|
|
||||||
const {client, sent} = runtimeStub()
|
|
||||||
await new AwsUserPoolsAdapter(client).items.putItem!(POOL_ID, {username: 'bob', password: 'Passw0rd!', permanent: false})
|
|
||||||
|
|
||||||
expect((sent[0] as AdminCreateUserCommand).input.Username).toBe('bob')
|
|
||||||
expect((sent[0] as AdminCreateUserCommand).input.UserAttributes).toEqual([])
|
|
||||||
expect(sent.some((command) => command instanceof AdminSetUserPasswordCommand)).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('rejects a user without a name, password or valid email', async () => {
|
|
||||||
const {client, sent} = runtimeStub()
|
|
||||||
const users = new AwsUserPoolsAdapter(client).items
|
|
||||||
|
|
||||||
await expect(users.putItem!(POOL_ID, {password: 'Passw0rd!'})).rejects.toThrow('Provide a username or an email address')
|
|
||||||
await expect(users.putItem!(POOL_ID, {email: 'alice@example.com'})).rejects.toThrow('password is required')
|
|
||||||
await expect(users.putItem!(POOL_ID, {email: 'not-an-email', password: 'Passw0rd!'})).rejects.toThrow('email must be a valid email address')
|
|
||||||
await expect(users.putItem!(POOL_ID, {username: 'bob', password: 'Passw0rd!', permanent: 'yes'})).rejects.toThrow('permanent must be true or false')
|
|
||||||
expect(sent).toHaveLength(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('returns null for a missing user and deletes by username', async () => {
|
|
||||||
const missing = stubCognito(() => { throw awsError('UserNotFoundException') })
|
|
||||||
expect(await new AwsUserPoolsAdapter(missing.client).items.getItem!(POOL_ID, 'nobody')).toBeNull()
|
|
||||||
|
|
||||||
const {client, sent} = runtimeStub()
|
|
||||||
await new AwsUserPoolsAdapter(client).items.deleteItem!(POOL_ID, alice.Username)
|
|
||||||
expect((sent[0] as AdminDeleteUserCommand).input).toEqual({UserPoolId: POOL_ID, Username: alice.Username})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,332 +0,0 @@
|
|||||||
import {
|
|
||||||
AdminCreateUserCommand,
|
|
||||||
AdminDeleteUserCommand,
|
|
||||||
AdminGetUserCommand,
|
|
||||||
AdminSetUserPasswordCommand,
|
|
||||||
CreateUserPoolClientCommand,
|
|
||||||
CreateUserPoolCommand,
|
|
||||||
DeleteUserPoolCommand,
|
|
||||||
DescribeUserPoolCommand,
|
|
||||||
ListUserPoolClientsCommand,
|
|
||||||
ListUserPoolsCommand,
|
|
||||||
ListUsersCommand,
|
|
||||||
type AttributeType,
|
|
||||||
type CognitoIdentityProviderClient,
|
|
||||||
type PasswordPolicyType,
|
|
||||||
type UserPoolClientDescription,
|
|
||||||
type UserPoolDescriptionType,
|
|
||||||
type UserPoolType,
|
|
||||||
type UserType,
|
|
||||||
} from '@aws-sdk/client-cognito-identity-provider'
|
|
||||||
import type {ChildItem, CollectionPage, ItemStoreAdapter, PageQuery} from '../cloud-spi/childCollections'
|
|
||||||
import {RuntimeError, ValidationError} from '../cloud-spi/errors'
|
|
||||||
import {
|
|
||||||
USER_POOL_NAME_PATTERN,
|
|
||||||
USER_POOL_PASSWORD_POLICIES,
|
|
||||||
USER_POOL_SIGN_IN,
|
|
||||||
awsUserPoolsSchema,
|
|
||||||
} from '../cloud-spi/userPoolsSchema'
|
|
||||||
import type {
|
|
||||||
CloudResource,
|
|
||||||
CloudServiceAdapter,
|
|
||||||
CreateResourceInput,
|
|
||||||
ResourceQuery,
|
|
||||||
ServiceSchema,
|
|
||||||
} from '../cloud-spi/types'
|
|
||||||
|
|
||||||
/** Cognito caps every list page at 60. */
|
|
||||||
const COGNITO_PAGE_LIMIT = 60
|
|
||||||
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Talks to Cognito user pools — verified against Floci core (community, `latest`).
|
|
||||||
*
|
|
||||||
* Notes from probing the runtime:
|
|
||||||
* - A missing pool is `ResourceNotFoundException` and a missing user is
|
|
||||||
* `UserNotFoundException`, both with HTTP **400**; `get` matches on the name.
|
|
||||||
* - With email sign-in the stored `Username` is the generated `sub`, not the
|
|
||||||
* address, so users are keyed by `Username` and the email is read from the
|
|
||||||
* attributes. Admin calls accept either form.
|
|
||||||
* - An omitted password policy defaults to requiring upper, lower, number and
|
|
||||||
* symbol, matching AWS.
|
|
||||||
* - `InitiateAuth` with `USER_PASSWORD_AUTH` issues real tokens once a user's
|
|
||||||
* password is permanent, so users created here can sign in immediately.
|
|
||||||
*
|
|
||||||
* Users are the pool's child items. The app client is created with the pool, as
|
|
||||||
* the console's create flow does, and its id is surfaced in the pool metadata.
|
|
||||||
*/
|
|
||||||
export class AwsUserPoolsAdapter implements CloudServiceAdapter {
|
|
||||||
readonly cloud = 'aws' as const
|
|
||||||
readonly service = 'userpools' as const
|
|
||||||
readonly items: ItemStoreAdapter
|
|
||||||
|
|
||||||
constructor(private readonly cognito: CognitoIdentityProviderClient) {
|
|
||||||
this.items = new UserStore(cognito)
|
|
||||||
}
|
|
||||||
|
|
||||||
schema(): ServiceSchema {
|
|
||||||
return awsUserPoolsSchema()
|
|
||||||
}
|
|
||||||
|
|
||||||
async list(query: ResourceQuery = {}): Promise<CloudResource[]> {
|
|
||||||
const pools: UserPoolDescriptionType[] = []
|
|
||||||
let nextToken: string | undefined
|
|
||||||
|
|
||||||
do {
|
|
||||||
const res = await this.cognito.send(new ListUserPoolsCommand({MaxResults: COGNITO_PAGE_LIMIT, ...(nextToken ? {NextToken: nextToken} : {})}))
|
|
||||||
pools.push(...(res.UserPools ?? []))
|
|
||||||
nextToken = res.NextToken || undefined
|
|
||||||
} while (nextToken)
|
|
||||||
|
|
||||||
// ListUserPools omits the sign-in method, password policy and app clients the
|
|
||||||
// inspector shows, so each match is described. Pools are few on a local runtime.
|
|
||||||
const matches = filterBySearch(pools.flatMap(toSummaryResource), query.search)
|
|
||||||
return Promise.all(matches.map(async (summary) => (await this.get(summary.id)) ?? summary))
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(id: string): Promise<CloudResource | null> {
|
|
||||||
try {
|
|
||||||
const [pool, clients] = await Promise.all([
|
|
||||||
this.cognito.send(new DescribeUserPoolCommand({UserPoolId: id})),
|
|
||||||
this.listClients(id),
|
|
||||||
])
|
|
||||||
return pool.UserPool ? toResource(pool.UserPool, clients) : null
|
|
||||||
} catch (error) {
|
|
||||||
if (isMissing(error)) return null
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(input: CreateResourceInput): Promise<CloudResource> {
|
|
||||||
const name = requiredString(input.values.name, 'name')
|
|
||||||
if (!new RegExp(USER_POOL_NAME_PATTERN).test(name)) {
|
|
||||||
throw new ValidationError('name must be up to 128 letters, digits, spaces or + = , . @ _ -')
|
|
||||||
}
|
|
||||||
const signInWith = optionalOneOf(input.values.signInWith, USER_POOL_SIGN_IN, 'signInWith') ?? 'email'
|
|
||||||
const passwordPolicy = optionalOneOf(input.values.passwordPolicy, USER_POOL_PASSWORD_POLICIES, 'passwordPolicy') ?? 'default'
|
|
||||||
const createAppClient = optionalOneOf(input.values.createAppClient, ['yes', 'no'] as const, 'createAppClient') ?? 'yes'
|
|
||||||
|
|
||||||
const created = await this.cognito.send(new CreateUserPoolCommand({
|
|
||||||
PoolName: name,
|
|
||||||
...(signInWith === 'email' ? {UsernameAttributes: ['email'], AutoVerifiedAttributes: ['email']} : {}),
|
|
||||||
...(passwordPolicy === 'relaxed'
|
|
||||||
? {Policies: {PasswordPolicy: {MinimumLength: 8, RequireUppercase: false, RequireLowercase: false, RequireNumbers: false, RequireSymbols: false}}}
|
|
||||||
: {}),
|
|
||||||
}))
|
|
||||||
const id = created.UserPool?.Id
|
|
||||||
if (!id) throw new RuntimeError(`Cognito did not return the created user pool ${name}`)
|
|
||||||
|
|
||||||
if (createAppClient === 'yes') {
|
|
||||||
try {
|
|
||||||
await this.cognito.send(new CreateUserPoolClientCommand({
|
|
||||||
UserPoolId: id,
|
|
||||||
ClientName: `${name}-client`,
|
|
||||||
GenerateSecret: false,
|
|
||||||
ExplicitAuthFlows: ['ALLOW_USER_PASSWORD_AUTH', 'ALLOW_USER_SRP_AUTH', 'ALLOW_REFRESH_TOKEN_AUTH'],
|
|
||||||
}))
|
|
||||||
} catch (error) {
|
|
||||||
// Without its client the pool is not what was asked for; do not leave it half-made.
|
|
||||||
await this.cognito.send(new DeleteUserPoolCommand({UserPoolId: id})).catch(() => undefined)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pool = await this.get(id)
|
|
||||||
if (!pool) throw new RuntimeError(`Cognito did not return the created user pool ${id}`)
|
|
||||||
return pool
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(id: string): Promise<void> {
|
|
||||||
await this.cognito.send(new DeleteUserPoolCommand({UserPoolId: id}))
|
|
||||||
}
|
|
||||||
|
|
||||||
private async listClients(poolId: string): Promise<UserPoolClientDescription[]> {
|
|
||||||
const clients: UserPoolClientDescription[] = []
|
|
||||||
let nextToken: string | undefined
|
|
||||||
|
|
||||||
do {
|
|
||||||
const res = await this.cognito.send(new ListUserPoolClientsCommand({UserPoolId: poolId, MaxResults: COGNITO_PAGE_LIMIT, ...(nextToken ? {NextToken: nextToken} : {})}))
|
|
||||||
clients.push(...(res.UserPoolClients ?? []))
|
|
||||||
nextToken = res.NextToken || undefined
|
|
||||||
} while (nextToken)
|
|
||||||
|
|
||||||
return clients
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UserStore implements ItemStoreAdapter {
|
|
||||||
constructor(private readonly cognito: CognitoIdentityProviderClient) {}
|
|
||||||
|
|
||||||
async listItems(poolId: string, page: PageQuery = {}): Promise<CollectionPage<ChildItem>> {
|
|
||||||
const res = await this.cognito.send(new ListUsersCommand({
|
|
||||||
UserPoolId: poolId,
|
|
||||||
Limit: Math.min(page.limit ?? COGNITO_PAGE_LIMIT, COGNITO_PAGE_LIMIT),
|
|
||||||
...(page.cursor ? {PaginationToken: page.cursor} : {}),
|
|
||||||
}))
|
|
||||||
return {items: (res.Users ?? []).flatMap((user) => toUserItem(user.Username, user.Attributes, user)), nextCursor: res.PaginationToken || null}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getItem(poolId: string, username: string): Promise<ChildItem | null> {
|
|
||||||
try {
|
|
||||||
const user = await this.cognito.send(new AdminGetUserCommand({UserPoolId: poolId, Username: username}))
|
|
||||||
return toUserItem(user.Username, user.UserAttributes, user)[0] ?? null
|
|
||||||
} catch (error) {
|
|
||||||
if (isMissing(error)) return null
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a user that can sign in straight away: the password is set as
|
|
||||||
* permanent unless `permanent` is false, in which case Cognito will demand a
|
|
||||||
* new one at first sign-in (`FORCE_CHANGE_PASSWORD`).
|
|
||||||
*/
|
|
||||||
async putItem(poolId: string, body: Record<string, unknown>): Promise<ChildItem> {
|
|
||||||
const email = optionalString(body.email, 'email')
|
|
||||||
if (email && !EMAIL_PATTERN.test(email)) throw new ValidationError('email must be a valid email address')
|
|
||||||
const username = optionalString(body.username, 'username') ?? email
|
|
||||||
if (!username) throw new ValidationError('Provide a username or an email address')
|
|
||||||
// Not trimmed: leading or trailing spaces are part of a password.
|
|
||||||
const password = body.password
|
|
||||||
if (typeof password !== 'string' || !password) throw new ValidationError('password is required')
|
|
||||||
if (body.permanent !== undefined && typeof body.permanent !== 'boolean') throw new ValidationError('permanent must be true or false')
|
|
||||||
|
|
||||||
const created = await this.cognito.send(new AdminCreateUserCommand({
|
|
||||||
UserPoolId: poolId,
|
|
||||||
Username: username,
|
|
||||||
UserAttributes: email ? [{Name: 'email', Value: email}, {Name: 'email_verified', Value: 'true'}] : [],
|
|
||||||
TemporaryPassword: password,
|
|
||||||
MessageAction: 'SUPPRESS',
|
|
||||||
}))
|
|
||||||
const storedUsername = created.User?.Username
|
|
||||||
if (!storedUsername) throw new RuntimeError(`Cognito did not return the user created in ${poolId}`)
|
|
||||||
|
|
||||||
if (body.permanent !== false) {
|
|
||||||
await this.cognito.send(new AdminSetUserPasswordCommand({UserPoolId: poolId, Username: storedUsername, Password: password, Permanent: true}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await this.getItem(poolId, storedUsername)
|
|
||||||
if (!user) throw new RuntimeError(`Cognito did not return the user ${storedUsername} in ${poolId}`)
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteItem(poolId: string, username: string): Promise<void> {
|
|
||||||
await this.cognito.send(new AdminDeleteUserCommand({UserPoolId: poolId, Username: username}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toSummaryResource(pool: UserPoolDescriptionType): CloudResource[] {
|
|
||||||
if (!pool.Id) return []
|
|
||||||
return [{
|
|
||||||
id: pool.Id,
|
|
||||||
name: pool.Name ?? pool.Id,
|
|
||||||
cloud: 'aws',
|
|
||||||
service: 'userpools',
|
|
||||||
type: 'user-pool',
|
|
||||||
region: regionFromPoolId(pool.Id),
|
|
||||||
createdAt: pool.CreationDate ? pool.CreationDate.toISOString() : null,
|
|
||||||
status: pool.Status ?? null,
|
|
||||||
metadata: {},
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
function toResource(pool: UserPoolType, clients: UserPoolClientDescription[]): CloudResource {
|
|
||||||
const id = pool.Id ?? ''
|
|
||||||
const policy = pool.Policies?.PasswordPolicy
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
name: pool.Name ?? id,
|
|
||||||
cloud: 'aws',
|
|
||||||
service: 'userpools',
|
|
||||||
type: 'user-pool',
|
|
||||||
region: regionFromPoolId(id),
|
|
||||||
createdAt: pool.CreationDate ? pool.CreationDate.toISOString() : null,
|
|
||||||
status: pool.Status ?? null,
|
|
||||||
metadata: {
|
|
||||||
arn: pool.Arn,
|
|
||||||
signInWith: pool.UsernameAttributes?.includes('email') ? 'email' : 'username',
|
|
||||||
passwordPolicy: policy ? describePasswordPolicy(policy) : null,
|
|
||||||
estimatedNumberOfUsers: pool.EstimatedNumberOfUsers ?? null,
|
|
||||||
mfaConfiguration: pool.MfaConfiguration ?? null,
|
|
||||||
deletionProtection: pool.DeletionProtection ?? null,
|
|
||||||
appClients: clients.map((client) => ({clientId: client.ClientId ?? '', clientName: client.ClientName ?? ''})),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toUserItem(
|
|
||||||
username: string | undefined,
|
|
||||||
attributes: AttributeType[] | undefined,
|
|
||||||
user: Pick<UserType, 'Enabled' | 'UserStatus' | 'UserCreateDate' | 'UserLastModifiedDate'>,
|
|
||||||
): ChildItem[] {
|
|
||||||
if (!username) return []
|
|
||||||
const attributeMap = Object.fromEntries((attributes ?? []).flatMap((attribute) => attribute.Name ? [[attribute.Name, attribute.Value ?? '']] : []))
|
|
||||||
return [{
|
|
||||||
id: username,
|
|
||||||
collectionId: null,
|
|
||||||
timestamp: user.UserCreateDate ? user.UserCreateDate.toISOString() : null,
|
|
||||||
body: {
|
|
||||||
username,
|
|
||||||
email: attributeMap.email ?? null,
|
|
||||||
emailVerified: attributeMap.email_verified === 'true',
|
|
||||||
status: user.UserStatus ?? null,
|
|
||||||
enabled: user.Enabled ?? true,
|
|
||||||
lastModifiedAt: user.UserLastModifiedDate ? user.UserLastModifiedDate.toISOString() : null,
|
|
||||||
attributes: attributeMap,
|
|
||||||
},
|
|
||||||
metadata: {},
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
/** One readable line for the inspector, e.g. "8+ characters with uppercase, number". */
|
|
||||||
function describePasswordPolicy(policy: PasswordPolicyType): string {
|
|
||||||
const required = [
|
|
||||||
policy.RequireUppercase && 'uppercase',
|
|
||||||
policy.RequireLowercase && 'lowercase',
|
|
||||||
policy.RequireNumbers && 'number',
|
|
||||||
policy.RequireSymbols && 'symbol',
|
|
||||||
].filter((rule): rule is string => Boolean(rule))
|
|
||||||
const length = `${policy.MinimumLength ?? 8}+ characters`
|
|
||||||
return required.length ? `${length} with ${required.join(', ')}` : length
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Pool ids are `<region>_<suffix>`. */
|
|
||||||
function regionFromPoolId(id: string): string | null {
|
|
||||||
const separator = id.indexOf('_')
|
|
||||||
return separator > 0 ? id.slice(0, separator) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterBySearch(resources: CloudResource[], search?: string): CloudResource[] {
|
|
||||||
const normalized = search?.trim().toLowerCase()
|
|
||||||
if (!normalized) return resources
|
|
||||||
|
|
||||||
return resources.filter(
|
|
||||||
(resource) =>
|
|
||||||
resource.name.toLowerCase().includes(normalized) || resource.id.toLowerCase().includes(normalized),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isMissing(error: unknown): boolean {
|
|
||||||
if (typeof error !== 'object' || error === null) return false
|
|
||||||
const name = (error as {name?: string}).name
|
|
||||||
return name === 'ResourceNotFoundException' || name === 'UserNotFoundException'
|
|
||||||
}
|
|
||||||
|
|
||||||
function requiredString(value: unknown, field: string): string {
|
|
||||||
const trimmed = optionalString(value, field)
|
|
||||||
if (!trimmed) throw new ValidationError(`${field} is required`)
|
|
||||||
return trimmed
|
|
||||||
}
|
|
||||||
|
|
||||||
function optionalString(value: unknown, field: string): string | undefined {
|
|
||||||
if (value === undefined || value === null || value === '') return undefined
|
|
||||||
if (typeof value !== 'string') throw new ValidationError(`${field} must be a string`)
|
|
||||||
return value.trim() || undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
function optionalOneOf<T extends string>(value: unknown, allowed: readonly T[], field: string): T | undefined {
|
|
||||||
const text = optionalString(value, field)
|
|
||||||
if (text === undefined) return undefined
|
|
||||||
if (!(allowed as readonly string[]).includes(text)) throw new ValidationError(`${field} must be one of ${allowed.join(', ')}`)
|
|
||||||
return text as T
|
|
||||||
}
|
|
||||||
@@ -127,22 +127,3 @@ describe('logs errors carry HTTP 400 and must not fall through to it', () => {
|
|||||||
expect(mapped?.status).toBe(409)
|
expect(mapped?.status).toBe(409)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* Cognito reports every failure with HTTP 400, so these names must be mapped
|
|
||||||
* explicitly or a missing user would read as a validation error.
|
|
||||||
*/
|
|
||||||
describe('cognito errors carry HTTP 400 and keep their meaning', () => {
|
|
||||||
const cases: Array<[string, CloudErrorStatus]> = [
|
|
||||||
['UserNotFoundException', 404],
|
|
||||||
['UsernameExistsException', 409],
|
|
||||||
['AliasExistsException', 409],
|
|
||||||
['InvalidPasswordException', 400],
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const [name, status] of cases) {
|
|
||||||
test(`${name} maps to ${status}`, () => {
|
|
||||||
expect(mapAwsSdkError(sdkError(name, {$metadata: {httpStatusCode: 400}}))?.status).toBe(status)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -58,8 +58,6 @@ const CONFLICT_NAMES = new Set([
|
|||||||
'ConcurrentModificationException',
|
'ConcurrentModificationException',
|
||||||
'IncorrectState',
|
'IncorrectState',
|
||||||
'DeleteConflictException',
|
'DeleteConflictException',
|
||||||
'UsernameExistsException',
|
|
||||||
'AliasExistsException',
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const ACCESS_DENIED_NAMES = new Set([
|
const ACCESS_DENIED_NAMES = new Set([
|
||||||
@@ -92,7 +90,6 @@ const VALIDATION_NAMES = new Set([
|
|||||||
'InvalidBucketName',
|
'InvalidBucketName',
|
||||||
'InvalidArgumentException',
|
'InvalidArgumentException',
|
||||||
'InvalidParameterException',
|
'InvalidParameterException',
|
||||||
'InvalidPasswordException',
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const NOT_FOUND_NAMES = new Set([
|
const NOT_FOUND_NAMES = new Set([
|
||||||
@@ -122,7 +119,6 @@ const NOT_FOUND_NAMES = new Set([
|
|||||||
'InvalidSubnetID.NotFound',
|
'InvalidSubnetID.NotFound',
|
||||||
'InvalidGroupId.NotFound',
|
'InvalidGroupId.NotFound',
|
||||||
'InvalidAMIID.NotFound',
|
'InvalidAMIID.NotFound',
|
||||||
'UserNotFoundException',
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const NOT_IMPLEMENTED_NAMES = new Set([
|
const NOT_IMPLEMENTED_NAMES = new Set([
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import { SESv2Client } from "@aws-sdk/client-sesv2";
|
|||||||
import { CloudWatchClient } from "@aws-sdk/client-cloudwatch";
|
import { CloudWatchClient } from "@aws-sdk/client-cloudwatch";
|
||||||
import { ElastiCacheClient } from "@aws-sdk/client-elasticache";
|
import { ElastiCacheClient } from "@aws-sdk/client-elasticache";
|
||||||
import { LightsailClient } from "@aws-sdk/client-lightsail";
|
import { LightsailClient } from "@aws-sdk/client-lightsail";
|
||||||
import { CognitoIdentityProviderClient } from "@aws-sdk/client-cognito-identity-provider";
|
|
||||||
|
|
||||||
const endpoint = process.env.FLOCI_ENDPOINT;
|
const endpoint = process.env.FLOCI_ENDPOINT;
|
||||||
const region = process.env.AWS_REGION || "us-east-1";
|
const region = process.env.AWS_REGION || "us-east-1";
|
||||||
@@ -92,7 +91,6 @@ export type AwsClients = {
|
|||||||
cloudwatch: CloudWatchClient;
|
cloudwatch: CloudWatchClient;
|
||||||
elasticache: ElastiCacheClient;
|
elasticache: ElastiCacheClient;
|
||||||
lightsail: LightsailClient;
|
lightsail: LightsailClient;
|
||||||
cognito: CognitoIdentityProviderClient;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AwsClientName = keyof AwsClients;
|
export type AwsClientName = keyof AwsClients;
|
||||||
@@ -138,7 +136,6 @@ function buildClients(accountId: string): AwsClients {
|
|||||||
cloudwatch: new CloudWatchClient(base),
|
cloudwatch: new CloudWatchClient(base),
|
||||||
elasticache: new ElastiCacheClient(base),
|
elasticache: new ElastiCacheClient(base),
|
||||||
lightsail: new LightsailClient(base),
|
lightsail: new LightsailClient(base),
|
||||||
cognito: new CognitoIdentityProviderClient(base),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -239,13 +239,6 @@ export const SERVICE_CATALOG = {
|
|||||||
// AWS retains its dedicated page while Azure and GCP use Cloud Explorer.
|
// AWS retains its dedicated page while Azure and GCP use Cloud Explorer.
|
||||||
routeByCloud: {aws: '/secretsmanager'},
|
routeByCloud: {aws: '/secretsmanager'},
|
||||||
},
|
},
|
||||||
userpools: {
|
|
||||||
displayName: 'User Directories',
|
|
||||||
displayNameByCloud: {aws: 'Cognito'},
|
|
||||||
iconKey: 'userpools',
|
|
||||||
group: 'Security',
|
|
||||||
order: 7,
|
|
||||||
},
|
|
||||||
iac: {
|
iac: {
|
||||||
displayName: 'Infrastructure as Code',
|
displayName: 'Infrastructure as Code',
|
||||||
displayNameByCloud: {aws: 'CloudFormation'},
|
displayNameByCloud: {aws: 'CloudFormation'},
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ export type KnownResourceType =
|
|||||||
| 'instance' | 'image' | 'vpc' | 'lambda' | 'azure-function' | 'gcp-function'
|
| 'instance' | 'image' | 'vpc' | 'lambda' | 'azure-function' | 'gcp-function'
|
||||||
| 'secret' | 'iam-user' | 'servicebus-namespace' | 'queue' | 'fifo-queue'
|
| 'secret' | 'iam-user' | 'servicebus-namespace' | 'queue' | 'fifo-queue'
|
||||||
| 'topic' | 'event-bus' | 'rest-api' | 'stack' | 'email' | 'sql-server'
|
| 'topic' | 'event-bus' | 'rest-api' | 'stack' | 'email' | 'sql-server'
|
||||||
| 'postgres-flexible-server' | 'load-balancer' | 'state-machine' | 'scheduler-job' | 'key' | 'parameter' | 'cloud-run-service' | 'log-group' | 'user-pool'
|
| 'postgres-flexible-server' | 'load-balancer' | 'state-machine' | 'scheduler-job' | 'key' | 'parameter' | 'cloud-run-service' | 'log-group'
|
||||||
|
|
||||||
export interface CloudResource {
|
export interface CloudResource {
|
||||||
id: string
|
id: string
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
import type {CloudProvider, FieldSchema, ServiceSchema, TableColumnSchema} from './types'
|
|
||||||
|
|
||||||
/** Cognito's own limit and character set for user pool names. */
|
|
||||||
export const USER_POOL_NAME_PATTERN = '^[\\w\\s+=,.@-]{1,128}$'
|
|
||||||
export const USER_POOL_SIGN_IN = ['email', 'username'] as const
|
|
||||||
export const USER_POOL_PASSWORD_POLICIES = ['default', 'relaxed'] as const
|
|
||||||
|
|
||||||
const userPoolColumns: TableColumnSchema[] = [
|
|
||||||
{name: 'name', label: 'Name'},
|
|
||||||
{name: 'id', label: 'Pool ID', format: 'code'},
|
|
||||||
{name: 'status', label: 'Status', format: 'badge'},
|
|
||||||
{name: 'createdAt', label: 'Created', format: 'datetime'},
|
|
||||||
]
|
|
||||||
|
|
||||||
const userPoolFilters: FieldSchema[] = [{name: 'search', label: 'Search', type: 'text', required: false}]
|
|
||||||
|
|
||||||
export function awsUserPoolsSchema(): ServiceSchema {
|
|
||||||
return {
|
|
||||||
cloud: 'aws',
|
|
||||||
service: 'userpools',
|
|
||||||
displayName: 'Amazon Cognito user pools',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
label: 'Pool name',
|
|
||||||
type: 'text',
|
|
||||||
required: true,
|
|
||||||
validation: {pattern: USER_POOL_NAME_PATTERN, message: 'Use up to 128 letters, digits, spaces or + = , . @ _ -'},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'signInWith',
|
|
||||||
label: 'Sign in with',
|
|
||||||
type: 'select',
|
|
||||||
required: false,
|
|
||||||
defaultValue: 'email',
|
|
||||||
description: 'Email makes the address the sign-in name; username keeps a separate user name.',
|
|
||||||
options: [{label: 'Email address', value: 'email'}, {label: 'Username', value: 'username'}],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'passwordPolicy',
|
|
||||||
label: 'Password policy',
|
|
||||||
type: 'select',
|
|
||||||
required: false,
|
|
||||||
defaultValue: 'default',
|
|
||||||
description: 'Relaxed only requires 8 characters, which keeps local test users quick to create.',
|
|
||||||
options: [
|
|
||||||
{label: 'AWS default (upper, lower, number, symbol)', value: 'default'},
|
|
||||||
{label: 'Relaxed (8+ characters)', value: 'relaxed'},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'createAppClient',
|
|
||||||
label: 'App client',
|
|
||||||
type: 'select',
|
|
||||||
required: false,
|
|
||||||
defaultValue: 'yes',
|
|
||||||
description: 'A public client (no secret) with username/password and refresh-token sign-in, named <pool>-client.',
|
|
||||||
options: [{label: 'Create an app client', value: 'yes'}, {label: 'No app client', value: 'no'}],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
actions: ['list', 'create', 'delete', 'inspect'],
|
|
||||||
capabilities: {
|
|
||||||
resourceActions: [
|
|
||||||
{name: 'list', label: 'List user pools', enabled: true, status: 'available', runtimeRequired: true},
|
|
||||||
{name: 'create', label: 'Create user pool', enabled: true, status: 'available', runtimeRequired: true},
|
|
||||||
{name: 'delete', label: 'Delete user pool', enabled: true, status: 'available', runtimeRequired: true},
|
|
||||||
{name: 'inspect', label: 'Inspect user pool', enabled: true, status: 'available', runtimeRequired: true},
|
|
||||||
],
|
|
||||||
itemActions: [
|
|
||||||
{name: 'list', label: 'List users', enabled: true, status: 'available', runtimeRequired: true},
|
|
||||||
{name: 'get', label: 'Get user', enabled: true, status: 'available', runtimeRequired: true},
|
|
||||||
{name: 'put', label: 'Create user', enabled: true, status: 'available', runtimeRequired: true},
|
|
||||||
{name: 'delete', label: 'Delete user', enabled: true, status: 'available', runtimeRequired: true},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
filters: userPoolFilters,
|
|
||||||
columns: userPoolColumns,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function userPoolsSchemaFor(cloud: CloudProvider): ServiceSchema | null {
|
|
||||||
return cloud === 'aws' ? awsUserPoolsSchema() : null
|
|
||||||
}
|
|
||||||
@@ -47,7 +47,6 @@ import {AwsFargateAdapter} from './adapter-aws/AwsFargateAdapter'
|
|||||||
import {AwsKinesisAdapter} from './adapter-aws/AwsKinesisAdapter'
|
import {AwsKinesisAdapter} from './adapter-aws/AwsKinesisAdapter'
|
||||||
import {AwsElastiCacheAdapter} from './adapter-aws/AwsElastiCacheAdapter'
|
import {AwsElastiCacheAdapter} from './adapter-aws/AwsElastiCacheAdapter'
|
||||||
import {AwsLightsailAdapter} from './adapter-aws/AwsLightsailAdapter'
|
import {AwsLightsailAdapter} from './adapter-aws/AwsLightsailAdapter'
|
||||||
import {AwsUserPoolsAdapter} from './adapter-aws/AwsUserPoolsAdapter'
|
|
||||||
import {awsClientsForAccount, resolveAccountId} from './aws'
|
import {awsClientsForAccount, resolveAccountId} from './aws'
|
||||||
import {createEc2Service} from './services/ec2'
|
import {createEc2Service} from './services/ec2'
|
||||||
import {createEksService} from './services/eks'
|
import {createEksService} from './services/eks'
|
||||||
@@ -97,7 +96,6 @@ export function createCloudAdapterRegistry(accountId?: string | null): CloudAdap
|
|||||||
new AwsKinesisAdapter(clients.kinesis),
|
new AwsKinesisAdapter(clients.kinesis),
|
||||||
new AwsElastiCacheAdapter(clients.elasticache),
|
new AwsElastiCacheAdapter(clients.elasticache),
|
||||||
new AwsLightsailAdapter(clients.lightsail),
|
new AwsLightsailAdapter(clients.lightsail),
|
||||||
new AwsUserPoolsAdapter(clients.cognito),
|
|
||||||
new AzureStorageAdapter(),
|
new AzureStorageAdapter(),
|
||||||
new AzureServiceBusAdapter(),
|
new AzureServiceBusAdapter(),
|
||||||
new AzureDatabaseAdapter(),
|
new AzureDatabaseAdapter(),
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ const AWS_CONSOLE_SERVICES: Record<string, CloudServiceType[]> = {
|
|||||||
codebuild: ['codebuild'],
|
codebuild: ['codebuild'],
|
||||||
codedeploy: ['codedeploy'],
|
codedeploy: ['codedeploy'],
|
||||||
codepipeline: ['codepipeline'],
|
codepipeline: ['codepipeline'],
|
||||||
'cognito-idp': ['userpools'],
|
|
||||||
dynamodb: ['nosql'],
|
dynamodb: ['nosql'],
|
||||||
ec2: ['compute', 'networking'],
|
ec2: ['compute', 'networking'],
|
||||||
ecr: ['ecr'],
|
ecr: ['ecr'],
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
import {QueryClient, QueryClientProvider} from '@tanstack/react-query'
|
|
||||||
import {render, screen} from '@testing-library/react'
|
|
||||||
import userEvent from '@testing-library/user-event'
|
|
||||||
import {beforeEach, expect, test, vi} from 'vitest'
|
|
||||||
import {CognitoUsersPanel} from './CognitoUsersPanel'
|
|
||||||
import type {CloudResource} from '@/types/resource'
|
|
||||||
|
|
||||||
const api = vi.hoisted(() => ({
|
|
||||||
deleteFlatItem: vi.fn(),
|
|
||||||
getCloudResource: vi.fn(),
|
|
||||||
listFlatItems: vi.fn(),
|
|
||||||
putFlatItem: vi.fn(),
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/api/cloudProxyClient', () => api)
|
|
||||||
vi.mock('@/lib/accountStore', () => ({useAccountId: () => '000000000000'}))
|
|
||||||
|
|
||||||
const summary: CloudResource = {
|
|
||||||
id: 'us-east-1_8e65d229a', name: 'orders-users', cloud: 'aws', service: 'userpools', type: 'user-pool',
|
|
||||||
region: 'us-east-1', createdAt: null, metadata: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
function pool(signInWith: 'email' | 'username'): CloudResource {
|
|
||||||
return {...summary, metadata: {signInWith, appClients: [{clientId: 'a8e5c7d38d3b4a1a97c9d822de', clientName: 'orders-users-client'}]}}
|
|
||||||
}
|
|
||||||
|
|
||||||
const alice = {
|
|
||||||
id: 'aa36ecc1-ed8a-43b5-84e9-988398c17436', collectionId: null, timestamp: '2026-10-23T16:01:22.000Z', metadata: {},
|
|
||||||
body: {username: 'aa36ecc1-ed8a-43b5-84e9-988398c17436', email: 'alice@example.com', status: 'CONFIRMED', enabled: true},
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPanel() {
|
|
||||||
const client = new QueryClient({defaultOptions: {queries: {retry: false}}})
|
|
||||||
return render(<QueryClientProvider client={client}><CognitoUsersPanel resource={summary} runtimeReachable/></QueryClientProvider>)
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks()
|
|
||||||
api.listFlatItems.mockResolvedValue({items: [alice], nextCursor: null})
|
|
||||||
api.putFlatItem.mockResolvedValue(alice)
|
|
||||||
api.deleteFlatItem.mockResolvedValue(undefined)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('shows the app client id and the pool users', async () => {
|
|
||||||
api.getCloudResource.mockResolvedValue(pool('email'))
|
|
||||||
renderPanel()
|
|
||||||
|
|
||||||
expect(await screen.findByText('a8e5c7d38d3b4a1a97c9d822de')).toBeInTheDocument()
|
|
||||||
expect(screen.getByRole('button', {name: 'Copy Client ID'})).toBeInTheDocument()
|
|
||||||
expect(await screen.findByText('alice@example.com')).toBeInTheDocument()
|
|
||||||
expect(screen.getByText('CONFIRMED')).toBeInTheDocument()
|
|
||||||
expect(api.getCloudResource).toHaveBeenCalledWith('aws', 'userpools', summary.id, expect.anything())
|
|
||||||
})
|
|
||||||
|
|
||||||
test('creates an email user who can sign in immediately', async () => {
|
|
||||||
const user = userEvent.setup()
|
|
||||||
api.getCloudResource.mockResolvedValue(pool('email'))
|
|
||||||
renderPanel()
|
|
||||||
|
|
||||||
await screen.findByText('a8e5c7d38d3b4a1a97c9d822de')
|
|
||||||
await user.type(screen.getByLabelText('Email'), 'bob@example.com')
|
|
||||||
await user.type(screen.getByLabelText('Password'), 'Passw0rd!')
|
|
||||||
await user.click(screen.getByRole('button', {name: 'Create user'}))
|
|
||||||
|
|
||||||
expect(api.putFlatItem).toHaveBeenCalledWith('aws', 'userpools', summary.id, {email: 'bob@example.com', password: 'Passw0rd!', permanent: true})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('creates a username user with a temporary password when asked', async () => {
|
|
||||||
const user = userEvent.setup()
|
|
||||||
api.getCloudResource.mockResolvedValue(pool('username'))
|
|
||||||
renderPanel()
|
|
||||||
|
|
||||||
await user.type(await screen.findByLabelText('Username'), 'bob')
|
|
||||||
await user.type(screen.getByLabelText('Password'), 'Passw0rd!')
|
|
||||||
await user.click(screen.getByLabelText(/User can sign in now/))
|
|
||||||
await user.click(screen.getByRole('button', {name: 'Create user'}))
|
|
||||||
|
|
||||||
expect(api.putFlatItem).toHaveBeenCalledWith('aws', 'userpools', summary.id, {username: 'bob', password: 'Passw0rd!', permanent: false})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('deletes a user by its stored username', async () => {
|
|
||||||
const user = userEvent.setup()
|
|
||||||
api.getCloudResource.mockResolvedValue(pool('email'))
|
|
||||||
renderPanel()
|
|
||||||
|
|
||||||
await user.click(await screen.findByRole('button', {name: 'Delete user alice@example.com'}))
|
|
||||||
|
|
||||||
expect(api.deleteFlatItem).toHaveBeenCalledWith('aws', 'userpools', summary.id, alice.id)
|
|
||||||
})
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
import {RefreshCw, Trash2} from 'lucide-react'
|
|
||||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
|
||||||
import {useEffect, useState} from 'react'
|
|
||||||
import {deleteFlatItem, getCloudResource, listFlatItems, putFlatItem} from '@/api/cloudProxyClient'
|
|
||||||
import {CopyValueButton} from '@/components/CopyValueButton'
|
|
||||||
import {useAccountId} from '@/lib/accountStore'
|
|
||||||
import {formatDateTime} from '@/lib/format'
|
|
||||||
import type {CloudResource} from '@/types/resource'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
resource?: CloudResource
|
|
||||||
runtimeReachable: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type AppClient = {clientId: string; clientName: string}
|
|
||||||
|
|
||||||
export function CognitoUsersPanel({resource, runtimeReachable}: Props) {
|
|
||||||
const accountId = useAccountId()
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
const [login, setLogin] = useState('')
|
|
||||||
const [password, setPassword] = useState('')
|
|
||||||
const [permanent, setPermanent] = useState(true)
|
|
||||||
const usersKey = ['cognito-users', accountId, resource?.id]
|
|
||||||
|
|
||||||
// The table row is a list summary; sign-in method and app clients come from the full pool.
|
|
||||||
const poolQuery = useQuery({
|
|
||||||
queryKey: ['cognito-pool', accountId, resource?.id],
|
|
||||||
queryFn: ({signal}) => getCloudResource('aws', 'userpools', resource?.id ?? '', signal),
|
|
||||||
enabled: Boolean(resource) && runtimeReachable,
|
|
||||||
})
|
|
||||||
const usersQuery = useQuery({
|
|
||||||
queryKey: usersKey,
|
|
||||||
queryFn: ({signal}) => listFlatItems('aws', 'userpools', resource?.id ?? '', undefined, signal),
|
|
||||||
enabled: Boolean(resource) && runtimeReachable,
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLogin('')
|
|
||||||
setPassword('')
|
|
||||||
setPermanent(true)
|
|
||||||
}, [resource?.id])
|
|
||||||
|
|
||||||
const signInWithEmail = poolQuery.data?.metadata.signInWith !== 'username'
|
|
||||||
const createMutation = useMutation({
|
|
||||||
mutationFn: () => putFlatItem('aws', 'userpools', resource?.id ?? '', {
|
|
||||||
...(signInWithEmail ? {email: login.trim()} : {username: login.trim()}),
|
|
||||||
password,
|
|
||||||
permanent,
|
|
||||||
}),
|
|
||||||
onSuccess: () => {
|
|
||||||
setLogin('')
|
|
||||||
setPassword('')
|
|
||||||
return queryClient.invalidateQueries({queryKey: usersKey})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const deleteMutation = useMutation({
|
|
||||||
mutationFn: (username: string) => deleteFlatItem('aws', 'userpools', resource?.id ?? '', username),
|
|
||||||
onSuccess: () => queryClient.invalidateQueries({queryKey: usersKey}),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!resource) {
|
|
||||||
return <section className="table-panel"><div className="empty compact"><h3>Select a user pool</h3><p>Choose a user pool to manage its app clients and users.</p></div></section>
|
|
||||||
}
|
|
||||||
|
|
||||||
const users = usersQuery.data?.items ?? []
|
|
||||||
const appClients = appClientsOf(poolQuery.data)
|
|
||||||
const pending = createMutation.isPending || deleteMutation.isPending
|
|
||||||
const error = poolQuery.error ?? usersQuery.error ?? createMutation.error ?? deleteMutation.error
|
|
||||||
const loginLabel = signInWithEmail ? 'Email' : 'Username'
|
|
||||||
|
|
||||||
return <section className="table-panel" style={{marginTop: 16}} aria-label={`Users in ${resource.name}`}>
|
|
||||||
<div className="input-row resource-table-bar">
|
|
||||||
<div><p className="eyebrow">App clients</p><span className="muted">Use a client ID with the pool ID to sign users in from your app</span></div>
|
|
||||||
</div>
|
|
||||||
{poolQuery.isSuccess && appClients.length === 0 && <p className="muted padded">This pool has no app clients.</p>}
|
|
||||||
{appClients.length > 0 && <div className="cognito-table"><table className="table"><thead><tr><th>Name</th><th>Client ID</th></tr></thead><tbody>{appClients.map((client) => (
|
|
||||||
<tr key={client.clientId}><td>{client.clientName || '—'}</td><td><span className="copy-value mono">{client.clientId}<CopyValueButton value={client.clientId} label="Client ID"/></span></td></tr>
|
|
||||||
))}</tbody></table></div>}
|
|
||||||
|
|
||||||
<div className="input-row resource-table-bar">
|
|
||||||
<div><p className="eyebrow">Users</p><span className="muted">{users.length} users in {resource.name}</span></div>
|
|
||||||
<button className="button" type="button" disabled={!runtimeReachable || usersQuery.isFetching} onClick={() => { void Promise.all([usersQuery.refetch(), poolQuery.refetch()]) }}><RefreshCw size={14}/>{usersQuery.isFetching ? 'Loading' : 'Refresh'}</button>
|
|
||||||
</div>
|
|
||||||
<form className="dynamic-form" onSubmit={(event) => { event.preventDefault(); if (login.trim() && password) createMutation.mutate() }}>
|
|
||||||
<label className="dynamic-field"><span>{loginLabel}</span><input className="input" type={signInWithEmail ? 'email' : 'text'} value={login} onChange={(event) => setLogin(event.target.value)} autoComplete="off" disabled={pending} required/></label>
|
|
||||||
<label className="dynamic-field"><span>Password</span><input className="input" type="password" value={password} onChange={(event) => setPassword(event.target.value)} autoComplete="new-password" disabled={pending} required/></label>
|
|
||||||
<label className="dynamic-field dynamic-field--span"><span><input type="checkbox" checked={permanent} onChange={(event) => setPermanent(event.target.checked)} disabled={pending}/> User can sign in now</span><small className="muted">When unchecked the password is temporary and must be changed at first sign-in.</small></label>
|
|
||||||
<div style={{alignSelf: 'end'}}><button className="button primary" type="submit" disabled={!runtimeReachable || !login.trim() || !password || pending}>{createMutation.isPending ? 'Creating' : 'Create user'}</button></div>
|
|
||||||
</form>
|
|
||||||
{error instanceof Error && <p className="error-text compact-text" role="alert">{error.message}</p>}
|
|
||||||
{usersQuery.isLoading && <div className="muted padded">Loading users</div>}
|
|
||||||
{usersQuery.isSuccess && users.length === 0 && <div className="empty compact"><h3>No users</h3><p>Create a user to sign in to apps that use this pool.</p></div>}
|
|
||||||
{users.length > 0 && <div className="cognito-table"><table className="table"><thead><tr><th>{loginLabel}</th><th>Status</th><th>Enabled</th><th>Created</th><th/></tr></thead><tbody>{users.map((user) => {
|
|
||||||
const name = stringValue(user.body.email) || stringValue(user.body.username)
|
|
||||||
return <tr key={user.id}>
|
|
||||||
<td><span className="copy-value">{name}<CopyValueButton value={name} label={loginLabel}/></span></td>
|
|
||||||
<td><code>{stringValue(user.body.status) || '—'}</code></td>
|
|
||||||
<td>{user.body.enabled === false ? 'No' : 'Yes'}</td>
|
|
||||||
<td>{formatDateTime(user.timestamp) ?? '—'}</td>
|
|
||||||
<td><button className="icon-btn danger" type="button" title="Delete user" aria-label={`Delete user ${name}`} disabled={!runtimeReachable || pending} onClick={() => deleteMutation.mutate(user.id)}><Trash2 size={14}/></button></td>
|
|
||||||
</tr>
|
|
||||||
})}</tbody></table></div>}
|
|
||||||
</section>
|
|
||||||
}
|
|
||||||
|
|
||||||
function appClientsOf(pool?: CloudResource): AppClient[] {
|
|
||||||
const clients = pool?.metadata.appClients
|
|
||||||
if (!Array.isArray(clients)) return []
|
|
||||||
return clients.filter((client): client is AppClient => typeof client?.clientId === 'string' && client.clientId !== '')
|
|
||||||
}
|
|
||||||
|
|
||||||
function stringValue(value: unknown): string {
|
|
||||||
return typeof value === 'string' ? value : ''
|
|
||||||
}
|
|
||||||
@@ -63,7 +63,6 @@ import { EcrImageBrowser } from "@/components/EcrImageBrowser";
|
|||||||
import { LoadBalancerPanel } from "@/components/LoadBalancerPanel";
|
import { LoadBalancerPanel } from "@/components/LoadBalancerPanel";
|
||||||
import { TargetGroupPanel } from "@/components/TargetGroupPanel";
|
import { TargetGroupPanel } from "@/components/TargetGroupPanel";
|
||||||
import { LoadBalancerListenerPanel } from "@/components/LoadBalancerListenerPanel";
|
import { LoadBalancerListenerPanel } from "@/components/LoadBalancerListenerPanel";
|
||||||
import { CognitoUsersPanel } from "@/components/CognitoUsersPanel";
|
|
||||||
import { WorkflowExecutionDebugger } from "@/components/WorkflowExecutionDebugger";
|
import { WorkflowExecutionDebugger } from "@/components/WorkflowExecutionDebugger";
|
||||||
import { CodeBuildPanel } from "@/components/CodeBuildPanel";
|
import { CodeBuildPanel } from "@/components/CodeBuildPanel";
|
||||||
import { CodePipelinePanel } from "@/components/CodePipelinePanel";
|
import { CodePipelinePanel } from "@/components/CodePipelinePanel";
|
||||||
@@ -970,9 +969,6 @@ export function DynamicResourceView({
|
|||||||
{service === "loadbalancing" && cloud === "aws" && (
|
{service === "loadbalancing" && cloud === "aws" && (
|
||||||
<LoadBalancerListenerPanel resource={activeSelected} runtimeReachable={canUseRuntime} />
|
<LoadBalancerListenerPanel resource={activeSelected} runtimeReachable={canUseRuntime} />
|
||||||
)}
|
)}
|
||||||
{service === "userpools" && cloud === "aws" && (
|
|
||||||
<CognitoUsersPanel resource={activeSelected} runtimeReachable={canUseRuntime} />
|
|
||||||
)}
|
|
||||||
{service === "compute" && (
|
{service === "compute" && (
|
||||||
<ComputePanel
|
<ComputePanel
|
||||||
cloud={cloud}
|
cloud={cloud}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import {
|
|||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
SlidersHorizontal,
|
SlidersHorizontal,
|
||||||
Table2,
|
Table2,
|
||||||
UsersRound,
|
|
||||||
Workflow,
|
Workflow,
|
||||||
Waypoints,
|
Waypoints,
|
||||||
Zap,
|
Zap,
|
||||||
@@ -30,7 +29,6 @@ import appConfigIcon from '@/assets/aws-services/appconfig.svg'
|
|||||||
import codeBuildIcon from '../../../../aws-icons-svg/Developer-Tools/CodeBuild.svg'
|
import codeBuildIcon from '../../../../aws-icons-svg/Developer-Tools/CodeBuild.svg'
|
||||||
import codePipelineIcon from '../../../../aws-icons-svg/Developer-Tools/CodePipeline.svg'
|
import codePipelineIcon from '../../../../aws-icons-svg/Developer-Tools/CodePipeline.svg'
|
||||||
import codeDeployIcon from '../../../../aws-icons-svg/Developer-Tools/CodeDeploy.svg'
|
import codeDeployIcon from '../../../../aws-icons-svg/Developer-Tools/CodeDeploy.svg'
|
||||||
import cognitoIcon from '../../../../aws-icons-svg/Security-Identity-Compliance/Cognito.svg'
|
|
||||||
import cloudFormationIcon from '@/assets/aws-services/cloudformation.svg'
|
import cloudFormationIcon from '@/assets/aws-services/cloudformation.svg'
|
||||||
import cloudWatchLogsIcon from '@/assets/aws-services/cloudwatch-logs.svg'
|
import cloudWatchLogsIcon from '@/assets/aws-services/cloudwatch-logs.svg'
|
||||||
import dynamoDbIcon from '@/assets/aws-services/dynamodb.svg'
|
import dynamoDbIcon from '@/assets/aws-services/dynamodb.svg'
|
||||||
@@ -86,7 +84,6 @@ const SERVICE_ICONS: Record<string, LucideIcon> = {
|
|||||||
logs: ScrollText,
|
logs: ScrollText,
|
||||||
iam: ShieldCheck,
|
iam: ShieldCheck,
|
||||||
kms: Lock,
|
kms: Lock,
|
||||||
userpools: UsersRound,
|
|
||||||
loadbalancing: Scale,
|
loadbalancing: Scale,
|
||||||
parameters: SlidersHorizontal,
|
parameters: SlidersHorizontal,
|
||||||
configuration: SlidersHorizontal,
|
configuration: SlidersHorizontal,
|
||||||
@@ -130,7 +127,6 @@ const AWS_SERVICE_ICON_ASSETS: Record<string, string> = {
|
|||||||
secrets: secretsManagerIcon,
|
secrets: secretsManagerIcon,
|
||||||
serverless: lambdaIcon,
|
serverless: lambdaIcon,
|
||||||
storage: s3Icon,
|
storage: s3Icon,
|
||||||
userpools: cognitoIcon,
|
|
||||||
workflows: stepFunctionsIcon,
|
workflows: stepFunctionsIcon,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -779,6 +779,47 @@ pre,
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.topbar {
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar .search {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar .search input {
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar .kbd,
|
||||||
|
.topbar .account-meta,
|
||||||
|
.topbar .connection-target {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar .connection-state {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar .connection {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar .account-trigger {
|
||||||
|
gap: 4px;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.topbar-status:empty + .account-switcher {
|
.topbar-status:empty + .account-switcher {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
@@ -4261,11 +4302,6 @@ pre,
|
|||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cognito-table {
|
|
||||||
overflow-x: auto;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.diagnostics {
|
.diagnostics {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
|
|||||||
@@ -2,9 +2,17 @@ import {useEffect, useState} from 'react'
|
|||||||
|
|
||||||
export type Theme = 'dark' | 'light' | 'cyber'
|
export type Theme = 'dark' | 'light' | 'cyber'
|
||||||
|
|
||||||
|
/** Shared with the login page (public/login.js) and index.html. */
|
||||||
|
const THEME_KEY = 'floci-theme'
|
||||||
|
|
||||||
function getStored(): Theme {
|
function getStored(): Theme {
|
||||||
const v = localStorage.getItem('floci-theme')
|
try {
|
||||||
return v === 'light' || v === 'cyber' ? v : 'dark'
|
const v = localStorage.getItem(THEME_KEY)
|
||||||
|
return v === 'light' || v === 'cyber' ? v : 'dark'
|
||||||
|
} catch {
|
||||||
|
// localStorage unavailable (private mode) — fall back to the default theme.
|
||||||
|
return 'dark'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTheme() {
|
export function useTheme() {
|
||||||
@@ -12,7 +20,11 @@ export function useTheme() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.setAttribute('data-theme', theme)
|
document.documentElement.setAttribute('data-theme', theme)
|
||||||
localStorage.setItem('floci-theme', theme)
|
try {
|
||||||
|
localStorage.setItem(THEME_KEY, theme)
|
||||||
|
} catch {
|
||||||
|
// Persisting the choice is optional when storage is unavailable.
|
||||||
|
}
|
||||||
}, [theme])
|
}, [theme])
|
||||||
|
|
||||||
return {theme, setTheme}
|
return {theme, setTheme}
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ test('summarises the runtime and lists failing console services first', async ()
|
|||||||
expect(screen.getByText('1 not running')).toBeInTheDocument()
|
expect(screen.getByText('1 not running')).toBeInTheDocument()
|
||||||
expect(screen.getByText('2 / 3')).toBeInTheDocument()
|
expect(screen.getByText('2 / 3')).toBeInTheDocument()
|
||||||
|
|
||||||
const rows = within(screen.getByRole('region', {name: 'Console services'})).getAllByRole('row')
|
const consoleServices = screen.getByRole('region', {name: 'Console services'})
|
||||||
|
const rows = within(consoleServices).getAllByRole('row')
|
||||||
|
expect(rows).toHaveLength(2)
|
||||||
expect(rows[1]).toHaveTextContent('messaging')
|
expect(rows[1]).toHaveTextContent('messaging')
|
||||||
expect(rows[1]).toHaveTextContent('Service is not running (RUNTIME_UNAVAILABLE)')
|
expect(rows[1]).toHaveTextContent('Service is not running (RUNTIME_UNAVAILABLE)')
|
||||||
|
|
||||||
@@ -59,6 +61,25 @@ test('summarises the runtime and lists failing console services first', async ()
|
|||||||
expect(screen.getByText('OIDC single sign-on')).toBeInTheDocument()
|
expect(screen.getByText('OIDC single sign-on')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('keeps responding console services behind a toggle', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
api.getCloudDiagnostics.mockResolvedValue({
|
||||||
|
...healthy,
|
||||||
|
consoleServices: healthy.consoleServices.map((service) => ({...service, runtime: 'reachable', error: null, errorCode: null})),
|
||||||
|
} satisfies RuntimeDiagnostics)
|
||||||
|
renderPage()
|
||||||
|
const consoleServices = await screen.findByRole('region', {name: 'Console services'})
|
||||||
|
|
||||||
|
expect(within(consoleServices).getByText('All 2 responding')).toBeInTheDocument()
|
||||||
|
expect(within(consoleServices).queryByRole('table')).not.toBeInTheDocument()
|
||||||
|
|
||||||
|
await user.click(within(consoleServices).getByRole('button', {name: 'Show 2 responding'}))
|
||||||
|
expect(within(consoleServices).getAllByRole('row')).toHaveLength(3)
|
||||||
|
|
||||||
|
await user.click(within(consoleServices).getByRole('button', {name: 'Hide responding'}))
|
||||||
|
expect(within(consoleServices).queryByRole('table')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
test('filters emulated services by console coverage and state', async () => {
|
test('filters emulated services by console coverage and state', async () => {
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
api.getCloudDiagnostics.mockResolvedValue(healthy)
|
api.getCloudDiagnostics.mockResolvedValue(healthy)
|
||||||
|
|||||||
@@ -88,17 +88,25 @@ function DiagnosticsReport({report}: {report: RuntimeDiagnostics}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ConsoleServicesTable({services}: {services: CloudServiceStatus[]}) {
|
function ConsoleServicesTable({services}: {services: CloudServiceStatus[]}) {
|
||||||
|
const [showResponding, setShowResponding] = useState(false)
|
||||||
const failing = services.filter((service) => service.runtime !== 'reachable')
|
const failing = services.filter((service) => service.runtime !== 'reachable')
|
||||||
const ordered = [...failing, ...services.filter((service) => service.runtime === 'reachable')]
|
const responding = services.filter((service) => service.runtime === 'reachable')
|
||||||
|
const ordered = showResponding ? [...failing, ...responding] : failing
|
||||||
return (
|
return (
|
||||||
<section className="table-panel" aria-labelledby="diagnostics-console-services">
|
<section className="table-panel" aria-labelledby="diagnostics-console-services">
|
||||||
<header className="diagnostics-section-header">
|
<header className="diagnostics-section-header">
|
||||||
<h2 id="diagnostics-console-services">Console services</h2>
|
<h2 id="diagnostics-console-services">Console services</h2>
|
||||||
<span className="muted">{failing.length ? `${failing.length} of ${services.length} failing` : `${services.length} responding`}</span>
|
<div className="diagnostics-filters">
|
||||||
|
<span className="muted">{failing.length ? `${failing.length} of ${services.length} failing` : `All ${services.length} responding`}</span>
|
||||||
|
{responding.length > 0 && (
|
||||||
|
<button className="button compact" type="button" aria-expanded={showResponding} onClick={() => setShowResponding((value) => !value)}>
|
||||||
|
{showResponding ? 'Hide responding' : `Show ${responding.length} responding`}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{services.length === 0
|
{services.length === 0 && <p className="muted padded">No console services are registered for this cloud.</p>}
|
||||||
? <p className="muted padded">No console services are registered for this cloud.</p>
|
{ordered.length > 0 && <table className="table">
|
||||||
: <table className="table">
|
|
||||||
<thead><tr><th>Service</th><th>Status</th><th>Latency</th><th>Detail</th></tr></thead>
|
<thead><tr><th>Service</th><th>Status</th><th>Latency</th><th>Detail</th></tr></thead>
|
||||||
<tbody>{ordered.map((service) => (
|
<tbody>{ordered.map((service) => (
|
||||||
<tr key={service.service}>
|
<tr key={service.service}>
|
||||||
|
|||||||
Generated
+4
-220
@@ -34,9 +34,6 @@ importers:
|
|||||||
'@aws-sdk/client-codepipeline':
|
'@aws-sdk/client-codepipeline':
|
||||||
specifier: ^3.1076.0
|
specifier: ^3.1076.0
|
||||||
version: 3.1144.0
|
version: 3.1144.0
|
||||||
'@aws-sdk/client-cognito-identity-provider':
|
|
||||||
specifier: ^3.1146.0
|
|
||||||
version: 3.1156.0
|
|
||||||
'@aws-sdk/client-dynamodb':
|
'@aws-sdk/client-dynamodb':
|
||||||
specifier: ^3.1088.0
|
specifier: ^3.1088.0
|
||||||
version: 3.1112.0
|
version: 3.1112.0
|
||||||
@@ -321,10 +318,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-IhmU0tlv959L/1DjetFai1JcYfm1TrR+ZshokjrYe1zYk5Vt51nv07upMNywYH9BcArEPAv6pHUZXYFQITVEIA==}
|
resolution: {integrity: sha512-IhmU0tlv959L/1DjetFai1JcYfm1TrR+ZshokjrYe1zYk5Vt51nv07upMNywYH9BcArEPAv6pHUZXYFQITVEIA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/client-cognito-identity-provider@3.1156.0':
|
|
||||||
resolution: {integrity: sha512-R+fVyawDRELzLTMKYIrzGtqku6OULvRnk1SQDR1QQhyVnvZXmJDNuKlcvChl0pEFllpO69Rce9TiZwzMAe/2oQ==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@aws-sdk/client-dynamodb@3.1112.0':
|
'@aws-sdk/client-dynamodb@3.1112.0':
|
||||||
resolution: {integrity: sha512-Dborj6oKxgV9htN0jpdqGA9mTVOAPtYrrNI9ro67JNojtqIp1qHkdTkz1RkVi5Um2meV0qKVvhxLXRsI/S9RYQ==}
|
resolution: {integrity: sha512-Dborj6oKxgV9htN0jpdqGA9mTVOAPtYrrNI9ro67JNojtqIp1qHkdTkz1RkVi5Um2meV0qKVvhxLXRsI/S9RYQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -448,10 +441,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-LbY9aGsEiznDWmUc30Nwv3aIX/+dbwTx8KfS0yOC3NPYMO+O91e6jkT1azf34FwjOndq8/Q+RcVVZz5xnerwdg==}
|
resolution: {integrity: sha512-LbY9aGsEiznDWmUc30Nwv3aIX/+dbwTx8KfS0yOC3NPYMO+O91e6jkT1azf34FwjOndq8/Q+RcVVZz5xnerwdg==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/core@3.978.5':
|
|
||||||
resolution: {integrity: sha512-ruI0AWqcrfL20XDS6SwZIJFPOBZ6N0ADa9nLzWlBhxk1aoUjbUX3JkTR9vEmk7o35XpAHi68ahZ4T6RE2U9XyA==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-env@3.972.54':
|
'@aws-sdk/credential-provider-env@3.972.54':
|
||||||
resolution: {integrity: sha512-F4WQCG8GULIt+XrMHsqUM9dZc0eTwZM3HUWByOjIKOwBqJSzUxX8CFAtbgMvWfCua52FS1oi5FXarH3+khFq8A==}
|
resolution: {integrity: sha512-F4WQCG8GULIt+XrMHsqUM9dZc0eTwZM3HUWByOjIKOwBqJSzUxX8CFAtbgMvWfCua52FS1oi5FXarH3+khFq8A==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -476,10 +465,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-xTKO/FWJPozTIXbozVnVGoNBhaGba8TBcx+KyUjRVeOlXE+dUc7GTR1cLvu0uTdIdmemzaFbqqCshXeZA1fZew==}
|
resolution: {integrity: sha512-xTKO/FWJPozTIXbozVnVGoNBhaGba8TBcx+KyUjRVeOlXE+dUc7GTR1cLvu0uTdIdmemzaFbqqCshXeZA1fZew==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-env@3.972.76':
|
|
||||||
resolution: {integrity: sha512-YxmL1Ar7bV9ZkJvtv+lj+MF6le76NnZnbmgySyZuxpZaCv0/1lZ3KxKy6RIj1h9ui2hWnVjZxbmTMeO0y/NrQA==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-http@3.972.56':
|
'@aws-sdk/credential-provider-http@3.972.56':
|
||||||
resolution: {integrity: sha512-PiwHgEK2srdfi/lFveyQ+3w/qikyh6MKvuZAdlMC+dwQi4K5iVBr32oh7cugnYw+jA8iGtnQMhCGvLm/mqplmw==}
|
resolution: {integrity: sha512-PiwHgEK2srdfi/lFveyQ+3w/qikyh6MKvuZAdlMC+dwQi4K5iVBr32oh7cugnYw+jA8iGtnQMhCGvLm/mqplmw==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -504,10 +489,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-u91E/hT8f4d1xy0Jl7VG4nVKJ3lxbrZkoBTeSVoJdWBiSEUMwMS/9+e0H/aJVQV//Lt5wuzP+E69v4aRSsNTmw==}
|
resolution: {integrity: sha512-u91E/hT8f4d1xy0Jl7VG4nVKJ3lxbrZkoBTeSVoJdWBiSEUMwMS/9+e0H/aJVQV//Lt5wuzP+E69v4aRSsNTmw==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-http@3.972.78':
|
|
||||||
resolution: {integrity: sha512-3VHknDRngoebsaqyT1iR75b5cBT2zfEtODK6yYmYltljkK1HEAQLs2FEd1IpWqAeOYdMGaG9wikngWLTufY7RA==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-ini@3.972.61':
|
'@aws-sdk/credential-provider-ini@3.972.61':
|
||||||
resolution: {integrity: sha512-9CZWMjhBlgfKlqJk40R7kvMOLEIoOr3HJ1J/ELjAH9H5LzR4bCxLujPVM/1PKAEjzSZKZCxWOalm7JUGZbhQqw==}
|
resolution: {integrity: sha512-9CZWMjhBlgfKlqJk40R7kvMOLEIoOr3HJ1J/ELjAH9H5LzR4bCxLujPVM/1PKAEjzSZKZCxWOalm7JUGZbhQqw==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -528,10 +509,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-ged4KXdBkvIC81bLvNHHuQKdKak/VXhQTR1NWYTTqW0474nlmsxy9O/vlgTIohDDWH3xpBdtVMZRyjb+DnocDA==}
|
resolution: {integrity: sha512-ged4KXdBkvIC81bLvNHHuQKdKak/VXhQTR1NWYTTqW0474nlmsxy9O/vlgTIohDDWH3xpBdtVMZRyjb+DnocDA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-ini@3.973.21':
|
|
||||||
resolution: {integrity: sha512-aLx9hbCX6mGAyv4ZZd8UagJ4RASHVJw6BFDSIyuKmLkqNL6H47EjBPK6qaJ4B5TDV6mc+vMa0SJFYyo4mkvVQg==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-ini@3.973.7':
|
'@aws-sdk/credential-provider-ini@3.973.7':
|
||||||
resolution: {integrity: sha512-2CefB8cCxDu52P24B8Ay93/cTT199bcSvNHQ8e2f4BjSCF83yErBnTIZEBo0VeIgCfmw+PJKFUXnlQWxm2dkug==}
|
resolution: {integrity: sha512-2CefB8cCxDu52P24B8Ay93/cTT199bcSvNHQ8e2f4BjSCF83yErBnTIZEBo0VeIgCfmw+PJKFUXnlQWxm2dkug==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -560,10 +537,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-L+Z85anONJd8MaiuraO4wRxATCdEejBZ3K3eymzWI5JPXa9sOS9CkIm72PBKqXKX+Z9p9NGMX5AIMXm0LEflgw==}
|
resolution: {integrity: sha512-L+Z85anONJd8MaiuraO4wRxATCdEejBZ3K3eymzWI5JPXa9sOS9CkIm72PBKqXKX+Z9p9NGMX5AIMXm0LEflgw==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-login@3.972.83':
|
|
||||||
resolution: {integrity: sha512-9bJU6FjRacF4FK/QHf8Dyxhur7B1grRj8C1Yh4IkpoFW2exQflPdCv0VrbemLW9ZctLNle+AE7TETs9KNvuLYg==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-node@3.972.63':
|
'@aws-sdk/credential-provider-node@3.972.63':
|
||||||
resolution: {integrity: sha512-YmgWtTPZDStyT74ApSHpApD3r7W9znsc+WEZjW0vceom+NAxRx9/F3TyukOKix8kJkPaa49aIREQJdpGeLDiEw==}
|
resolution: {integrity: sha512-YmgWtTPZDStyT74ApSHpApD3r7W9znsc+WEZjW0vceom+NAxRx9/F3TyukOKix8kJkPaa49aIREQJdpGeLDiEw==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -592,10 +565,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-oHt854odINVwzwsh+c5x69j0ajm4DbqqqVJ+O1ECsCIZeMDAbzFpXItaqP7UZstJj/ATdTk/KFSH0LaNAgV+kA==}
|
resolution: {integrity: sha512-oHt854odINVwzwsh+c5x69j0ajm4DbqqqVJ+O1ECsCIZeMDAbzFpXItaqP7UZstJj/ATdTk/KFSH0LaNAgV+kA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-node@3.972.88':
|
|
||||||
resolution: {integrity: sha512-AG0KID5qhQPzrFkh5b0Cws1Amytvzo0L0Xj6hMgJXEwD6ZXAqninDhSNAWJiN5zU4YVVZnuidnqwPwYJ04IamA==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-process@3.972.54':
|
'@aws-sdk/credential-provider-process@3.972.54':
|
||||||
resolution: {integrity: sha512-UNmUjtTnp3wH3YTZcctN7yK17S2AdaJpiNIdIf0l70hHOdGuDfdMxk62P5rt64GwGm6qkFOYG0js/cU6cdZDdg==}
|
resolution: {integrity: sha512-UNmUjtTnp3wH3YTZcctN7yK17S2AdaJpiNIdIf0l70hHOdGuDfdMxk62P5rt64GwGm6qkFOYG0js/cU6cdZDdg==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -620,10 +589,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-rLIp2xbMjX/k9/od7APpqq1ZgXXnV0pOL1Th3ZsL8Wu0TRtBsDTVS8iPqcfRFcHakFxPvR04OSTv2ka2qOb/2A==}
|
resolution: {integrity: sha512-rLIp2xbMjX/k9/od7APpqq1ZgXXnV0pOL1Th3ZsL8Wu0TRtBsDTVS8iPqcfRFcHakFxPvR04OSTv2ka2qOb/2A==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-process@3.972.76':
|
|
||||||
resolution: {integrity: sha512-pdhKFTXv1zPYhh92MTlp0jVbLWvUEV9v+rziuEkoskGGQ2iJskxHTO38nA8UAbT/N4IM3n8OZJ9rIY5IBO15/A==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-sso@3.972.60':
|
'@aws-sdk/credential-provider-sso@3.972.60':
|
||||||
resolution: {integrity: sha512-zH8SvJkTRw1Kb7GARjGPjzkQPfL3jDi/OxK32I5SQq7bgwgFHJZaoDEwkEvFlfNfpByM6n4Rs20Y1mLyOdb6ag==}
|
resolution: {integrity: sha512-zH8SvJkTRw1Kb7GARjGPjzkQPfL3jDi/OxK32I5SQq7bgwgFHJZaoDEwkEvFlfNfpByM6n4Rs20Y1mLyOdb6ag==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -644,10 +609,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-IGihaJfFZYacJJr/odqILCoK7W/mvrZ7cuK7ECn3sAu4vLC6u0V8bS7mCGbdugJ8Aum2tnvqmx0F2MRFp2rn9g==}
|
resolution: {integrity: sha512-IGihaJfFZYacJJr/odqILCoK7W/mvrZ7cuK7ECn3sAu4vLC6u0V8bS7mCGbdugJ8Aum2tnvqmx0F2MRFp2rn9g==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-sso@3.973.20':
|
|
||||||
resolution: {integrity: sha512-DlxrAB3MnjNLx/da7b89W+3DyJQYso/q7Ns15MsST2IV2nkAYQOYTUCaffNSjgrpMP99Dm2mV2jxzcbyF94ZfA==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-sso@3.973.6':
|
'@aws-sdk/credential-provider-sso@3.973.6':
|
||||||
resolution: {integrity: sha512-DobZggy3K49xdCpjeyMou0FQhkoYbluVGNydL6D+lcxF8GoAsttFX0xnH5GmiQ89We5dB6TRpW+CD/VowBH6HQ==}
|
resolution: {integrity: sha512-DobZggy3K49xdCpjeyMou0FQhkoYbluVGNydL6D+lcxF8GoAsttFX0xnH5GmiQ89We5dB6TRpW+CD/VowBH6HQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -676,10 +637,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-/y9WvNtlcPBGLR0qc1a+9J/xtYZfVczvLUOuXaVWylzttH7ewsxwHtjmiJSolNrVSDorIxHGHMU61CbonRkmwA==}
|
resolution: {integrity: sha512-/y9WvNtlcPBGLR0qc1a+9J/xtYZfVczvLUOuXaVWylzttH7ewsxwHtjmiJSolNrVSDorIxHGHMU61CbonRkmwA==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-web-identity@3.972.82':
|
|
||||||
resolution: {integrity: sha512-LUo46IqPTJ4xKBsrLuoaxiv/D2GTBCcFDMD1y3jKIqPR/p7Rw14RJFZ0iRWftqZtyLPQX9JMGnSi3mBu4y/pNA==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@aws-sdk/dynamodb-codec@3.973.43':
|
'@aws-sdk/dynamodb-codec@3.973.43':
|
||||||
resolution: {integrity: sha512-5nw01fhFJEKM68n0R65S1DijiSAQWZVbvH7IxTIJpFFbggg816jBVYvhK7W+XISjvtdg0rMkF/gQmuLGH713mg==}
|
resolution: {integrity: sha512-5nw01fhFJEKM68n0R65S1DijiSAQWZVbvH7IxTIJpFFbggg816jBVYvhK7W+XISjvtdg0rMkF/gQmuLGH713mg==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -740,10 +697,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-oRxtBcka/JGHGs9l9p9IVajGoTP8vTPmoAzdHGy4Qcy9P5vPnDf6nhIeM/COQNY9k/OahImTRaLkHftoXvfcmQ==}
|
resolution: {integrity: sha512-oRxtBcka/JGHGs9l9p9IVajGoTP8vTPmoAzdHGy4Qcy9P5vPnDf6nhIeM/COQNY9k/OahImTRaLkHftoXvfcmQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/nested-clients@3.997.50':
|
|
||||||
resolution: {integrity: sha512-nKb8qu4QsnpZTq/6JIXc72mzuzfh7BK4sUQkpQznPec2z4W7Ob2xYkcM4i5e1yGTBnbJVeFv/0sf3jcz85hclA==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@aws-sdk/signature-v4-multi-region@3.996.38':
|
'@aws-sdk/signature-v4-multi-region@3.996.38':
|
||||||
resolution: {integrity: sha512-C379Sk+MiFZCfWZphKlMyLHKxV22OjoGM5KJjj5IJNJcOCWL4IGIpnEGzv1FQiRwhYXfq55SJMfxlqPE08JJ9g==}
|
resolution: {integrity: sha512-C379Sk+MiFZCfWZphKlMyLHKxV22OjoGM5KJjj5IJNJcOCWL4IGIpnEGzv1FQiRwhYXfq55SJMfxlqPE08JJ9g==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -756,10 +709,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-Zk08macMvQTHzQJCLJVkOlviVoqwYMrpXv4lmLN7b7sAbiMoOK7Go0NYdR5UeF+MW8LIbRmwrNy9u/5VvX1U5g==}
|
resolution: {integrity: sha512-Zk08macMvQTHzQJCLJVkOlviVoqwYMrpXv4lmLN7b7sAbiMoOK7Go0NYdR5UeF+MW8LIbRmwrNy9u/5VvX1U5g==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/signature-v4-multi-region@3.996.49':
|
|
||||||
resolution: {integrity: sha512-nvuAZNRrRRVCiARRKsYlQ0VnXiEQiq7vXPH+UA+LSo+kN3Djl9eo/V2r0udkw85PnsNshmv2Em1clld5M3j67w==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@aws-sdk/token-providers@3.1080.0':
|
'@aws-sdk/token-providers@3.1080.0':
|
||||||
resolution: {integrity: sha512-8PufAQvncWXvdZUvODbuyXa8l3aszefEzwSMBUcgheNbZOmJMcNn388Ebt/piVrUHyxKN1MlxnR2OonzTyZaGw==}
|
resolution: {integrity: sha512-8PufAQvncWXvdZUvODbuyXa8l3aszefEzwSMBUcgheNbZOmJMcNn388Ebt/piVrUHyxKN1MlxnR2OonzTyZaGw==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -784,10 +733,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-GpyAr0DD63YOEmYFM6Df+gJuIgC92MMTiBK4FTKfxii5MJ9ge20epR7LyroulscYlG89J+ZB2ivFDPjvfQhzdw==}
|
resolution: {integrity: sha512-GpyAr0DD63YOEmYFM6Df+gJuIgC92MMTiBK4FTKfxii5MJ9ge20epR7LyroulscYlG89J+ZB2ivFDPjvfQhzdw==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/token-providers@3.1155.0':
|
|
||||||
resolution: {integrity: sha512-iIcQnYLhWuq9qOLQbIWFrxF3PF0svVeZvHnUS4DUnO4zUFmFHIyGyjqrntXG+e/5PUgfavV0pYfDIKHxTzN2LA==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@aws-sdk/types@3.973.15':
|
'@aws-sdk/types@3.973.15':
|
||||||
resolution: {integrity: sha512-IULn8uBV/SMtmOIANsm4WHXIOtVPBWfOWs3WGL0j/sI+KhaYehvOw0ET+9urnn8MBpiijuU/0JOpuwKOE451PQ==}
|
resolution: {integrity: sha512-IULn8uBV/SMtmOIANsm4WHXIOtVPBWfOWs3WGL0j/sI+KhaYehvOw0ET+9urnn8MBpiijuU/0JOpuwKOE451PQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -808,10 +753,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-v/clNZzZnDxGyvpHMOGpJKVXFAExJzUNAAjaWGdcx8QAcXLGwTaOkw33p5SHAi0YAioK32xB3hWwOekRVfmfKg==}
|
resolution: {integrity: sha512-v/clNZzZnDxGyvpHMOGpJKVXFAExJzUNAAjaWGdcx8QAcXLGwTaOkw33p5SHAi0YAioK32xB3hWwOekRVfmfKg==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@aws-sdk/types@3.975.0':
|
|
||||||
resolution: {integrity: sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw==}
|
|
||||||
engines: {node: '>=20.0.0'}
|
|
||||||
|
|
||||||
'@aws-sdk/util-dynamodb@3.996.9':
|
'@aws-sdk/util-dynamodb@3.996.9':
|
||||||
resolution: {integrity: sha512-16x2tRvl7OYpZ0W/DdFJieFriD13+RvuRBDbe5sj/tCEfK86HSGd7I2s5j0ivz8p6KWGkS+5wKRO9OliJkjUOQ==}
|
resolution: {integrity: sha512-16x2tRvl7OYpZ0W/DdFJieFriD13+RvuRBDbe5sj/tCEfK86HSGd7I2s5j0ivz8p6KWGkS+5wKRO9OliJkjUOQ==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
@@ -1279,10 +1220,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-zRMhfkByhT2snNdr1si24vJitU6Cr9ix2MikUfWmkAgp4jrNP0GcKSP5YvwQ+TlI8AZXER5QOGJn3JsVtSD9/A==}
|
resolution: {integrity: sha512-zRMhfkByhT2snNdr1si24vJitU6Cr9ix2MikUfWmkAgp4jrNP0GcKSP5YvwQ+TlI8AZXER5QOGJn3JsVtSD9/A==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
'@smithy/core@3.36.2':
|
|
||||||
resolution: {integrity: sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ==}
|
|
||||||
engines: {node: '>=18.0.0'}
|
|
||||||
|
|
||||||
'@smithy/credential-provider-imds@4.4.14':
|
'@smithy/credential-provider-imds@4.4.14':
|
||||||
resolution: {integrity: sha512-QgbuahIb2qxQeZQvNK0sw3aF3JH5zwH8j2lLp5DUasVXexGGMWULAR+7z0omPXFolCP/m5wN9M5lm9EGdSviTQ==}
|
resolution: {integrity: sha512-QgbuahIb2qxQeZQvNK0sw3aF3JH5zwH8j2lLp5DUasVXexGGMWULAR+7z0omPXFolCP/m5wN9M5lm9EGdSviTQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@@ -1363,10 +1300,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow==}
|
resolution: {integrity: sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
'@smithy/signature-v4@5.7.4':
|
|
||||||
resolution: {integrity: sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ==}
|
|
||||||
engines: {node: '>=18.0.0'}
|
|
||||||
|
|
||||||
'@smithy/types@4.15.1':
|
'@smithy/types@4.15.1':
|
||||||
resolution: {integrity: sha512-x3L0XSACF6UYzKpa9biqiRMgvH5+wnFFew9Tm/grFYqgaupPwx/+ojDPpPJM8dZON3S9tjz5U+PQYsCBd1Mw5Q==}
|
resolution: {integrity: sha512-x3L0XSACF6UYzKpa9biqiRMgvH5+wnFFew9Tm/grFYqgaupPwx/+ojDPpPJM8dZON3S9tjz5U+PQYsCBd1Mw5Q==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@@ -2865,10 +2798,10 @@ snapshots:
|
|||||||
|
|
||||||
'@aws-sdk/checksums@3.1000.13':
|
'@aws-sdk/checksums@3.1000.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.978.1
|
'@aws-sdk/core': 3.978.0
|
||||||
'@aws-sdk/types': 3.974.6
|
'@aws-sdk/types': 3.974.5
|
||||||
'@smithy/core': 3.35.0
|
'@smithy/core': 3.33.3
|
||||||
'@smithy/types': 4.19.0
|
'@smithy/types': 4.17.2
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/client-api-gateway@3.1096.0':
|
'@aws-sdk/client-api-gateway@3.1096.0':
|
||||||
@@ -2961,17 +2894,6 @@ snapshots:
|
|||||||
'@smithy/types': 4.19.0
|
'@smithy/types': 4.19.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/client-cognito-identity-provider@3.1156.0':
|
|
||||||
dependencies:
|
|
||||||
'@aws-sdk/core': 3.978.5
|
|
||||||
'@aws-sdk/credential-provider-node': 3.972.88
|
|
||||||
'@aws-sdk/types': 3.975.0
|
|
||||||
'@smithy/core': 3.36.2
|
|
||||||
'@smithy/fetch-http-handler': 5.8.0
|
|
||||||
'@smithy/node-http-handler': 4.12.1
|
|
||||||
'@smithy/types': 4.19.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@aws-sdk/client-dynamodb@3.1112.0':
|
'@aws-sdk/client-dynamodb@3.1112.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.977.8
|
'@aws-sdk/core': 3.977.8
|
||||||
@@ -3327,17 +3249,6 @@ snapshots:
|
|||||||
bowser: 2.14.1
|
bowser: 2.14.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/core@3.978.5':
|
|
||||||
dependencies:
|
|
||||||
'@aws-sdk/types': 3.975.0
|
|
||||||
'@aws-sdk/xml-builder': 3.972.41
|
|
||||||
'@aws/lambda-invoke-store': 0.3.0
|
|
||||||
'@smithy/core': 3.36.2
|
|
||||||
'@smithy/signature-v4': 5.7.4
|
|
||||||
'@smithy/types': 4.19.0
|
|
||||||
bowser: 2.14.1
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-env@3.972.54':
|
'@aws-sdk/credential-provider-env@3.972.54':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.978.0
|
'@aws-sdk/core': 3.978.0
|
||||||
@@ -3386,14 +3297,6 @@ snapshots:
|
|||||||
'@smithy/types': 4.19.0
|
'@smithy/types': 4.19.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-env@3.972.76':
|
|
||||||
dependencies:
|
|
||||||
'@aws-sdk/core': 3.978.5
|
|
||||||
'@aws-sdk/types': 3.975.0
|
|
||||||
'@smithy/core': 3.36.2
|
|
||||||
'@smithy/types': 4.19.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-http@3.972.56':
|
'@aws-sdk/credential-provider-http@3.972.56':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.978.0
|
'@aws-sdk/core': 3.978.0
|
||||||
@@ -3454,16 +3357,6 @@ snapshots:
|
|||||||
'@smithy/types': 4.19.0
|
'@smithy/types': 4.19.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-http@3.972.78':
|
|
||||||
dependencies:
|
|
||||||
'@aws-sdk/core': 3.978.5
|
|
||||||
'@aws-sdk/types': 3.975.0
|
|
||||||
'@smithy/core': 3.36.2
|
|
||||||
'@smithy/fetch-http-handler': 5.8.0
|
|
||||||
'@smithy/node-http-handler': 4.12.1
|
|
||||||
'@smithy/types': 4.19.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-ini@3.972.61':
|
'@aws-sdk/credential-provider-ini@3.972.61':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.978.0
|
'@aws-sdk/core': 3.978.0
|
||||||
@@ -3544,22 +3437,6 @@ snapshots:
|
|||||||
'@smithy/types': 4.19.0
|
'@smithy/types': 4.19.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-ini@3.973.21':
|
|
||||||
dependencies:
|
|
||||||
'@aws-sdk/core': 3.978.5
|
|
||||||
'@aws-sdk/credential-provider-env': 3.972.76
|
|
||||||
'@aws-sdk/credential-provider-http': 3.972.78
|
|
||||||
'@aws-sdk/credential-provider-login': 3.972.83
|
|
||||||
'@aws-sdk/credential-provider-process': 3.972.76
|
|
||||||
'@aws-sdk/credential-provider-sso': 3.973.20
|
|
||||||
'@aws-sdk/credential-provider-web-identity': 3.972.82
|
|
||||||
'@aws-sdk/nested-clients': 3.997.50
|
|
||||||
'@aws-sdk/types': 3.975.0
|
|
||||||
'@smithy/core': 3.36.2
|
|
||||||
'@smithy/credential-provider-imds': 4.5.2
|
|
||||||
'@smithy/types': 4.19.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-ini@3.973.7':
|
'@aws-sdk/credential-provider-ini@3.973.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.978.0
|
'@aws-sdk/core': 3.978.0
|
||||||
@@ -3630,15 +3507,6 @@ snapshots:
|
|||||||
'@smithy/types': 4.19.0
|
'@smithy/types': 4.19.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-login@3.972.83':
|
|
||||||
dependencies:
|
|
||||||
'@aws-sdk/core': 3.978.5
|
|
||||||
'@aws-sdk/nested-clients': 3.997.50
|
|
||||||
'@aws-sdk/types': 3.975.0
|
|
||||||
'@smithy/core': 3.36.2
|
|
||||||
'@smithy/types': 4.19.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-node@3.972.63':
|
'@aws-sdk/credential-provider-node@3.972.63':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/credential-provider-env': 3.972.54
|
'@aws-sdk/credential-provider-env': 3.972.54
|
||||||
@@ -3737,20 +3605,6 @@ snapshots:
|
|||||||
'@smithy/types': 4.19.0
|
'@smithy/types': 4.19.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-node@3.972.88':
|
|
||||||
dependencies:
|
|
||||||
'@aws-sdk/credential-provider-env': 3.972.76
|
|
||||||
'@aws-sdk/credential-provider-http': 3.972.78
|
|
||||||
'@aws-sdk/credential-provider-ini': 3.973.21
|
|
||||||
'@aws-sdk/credential-provider-process': 3.972.76
|
|
||||||
'@aws-sdk/credential-provider-sso': 3.973.20
|
|
||||||
'@aws-sdk/credential-provider-web-identity': 3.972.82
|
|
||||||
'@aws-sdk/types': 3.975.0
|
|
||||||
'@smithy/core': 3.36.2
|
|
||||||
'@smithy/credential-provider-imds': 4.5.2
|
|
||||||
'@smithy/types': 4.19.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-process@3.972.54':
|
'@aws-sdk/credential-provider-process@3.972.54':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.978.0
|
'@aws-sdk/core': 3.978.0
|
||||||
@@ -3799,14 +3653,6 @@ snapshots:
|
|||||||
'@smithy/types': 4.19.0
|
'@smithy/types': 4.19.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-process@3.972.76':
|
|
||||||
dependencies:
|
|
||||||
'@aws-sdk/core': 3.978.5
|
|
||||||
'@aws-sdk/types': 3.975.0
|
|
||||||
'@smithy/core': 3.36.2
|
|
||||||
'@smithy/types': 4.19.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-sso@3.972.60':
|
'@aws-sdk/credential-provider-sso@3.972.60':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.978.0
|
'@aws-sdk/core': 3.978.0
|
||||||
@@ -3857,16 +3703,6 @@ snapshots:
|
|||||||
'@smithy/types': 4.19.0
|
'@smithy/types': 4.19.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-sso@3.973.20':
|
|
||||||
dependencies:
|
|
||||||
'@aws-sdk/core': 3.978.5
|
|
||||||
'@aws-sdk/nested-clients': 3.997.50
|
|
||||||
'@aws-sdk/token-providers': 3.1155.0
|
|
||||||
'@aws-sdk/types': 3.975.0
|
|
||||||
'@smithy/core': 3.36.2
|
|
||||||
'@smithy/types': 4.19.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-sso@3.973.6':
|
'@aws-sdk/credential-provider-sso@3.973.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.978.0
|
'@aws-sdk/core': 3.978.0
|
||||||
@@ -3931,15 +3767,6 @@ snapshots:
|
|||||||
'@smithy/types': 4.19.0
|
'@smithy/types': 4.19.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/credential-provider-web-identity@3.972.82':
|
|
||||||
dependencies:
|
|
||||||
'@aws-sdk/core': 3.978.5
|
|
||||||
'@aws-sdk/nested-clients': 3.997.50
|
|
||||||
'@aws-sdk/types': 3.975.0
|
|
||||||
'@smithy/core': 3.36.2
|
|
||||||
'@smithy/types': 4.19.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@aws-sdk/dynamodb-codec@3.973.43':
|
'@aws-sdk/dynamodb-codec@3.973.43':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.978.0
|
'@aws-sdk/core': 3.978.0
|
||||||
@@ -4072,17 +3899,6 @@ snapshots:
|
|||||||
'@smithy/types': 4.19.0
|
'@smithy/types': 4.19.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/nested-clients@3.997.50':
|
|
||||||
dependencies:
|
|
||||||
'@aws-sdk/core': 3.978.5
|
|
||||||
'@aws-sdk/signature-v4-multi-region': 3.996.49
|
|
||||||
'@aws-sdk/types': 3.975.0
|
|
||||||
'@smithy/core': 3.36.2
|
|
||||||
'@smithy/fetch-http-handler': 5.8.0
|
|
||||||
'@smithy/node-http-handler': 4.12.1
|
|
||||||
'@smithy/types': 4.19.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@aws-sdk/signature-v4-multi-region@3.996.38':
|
'@aws-sdk/signature-v4-multi-region@3.996.38':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/types': 3.974.5
|
'@aws-sdk/types': 3.974.5
|
||||||
@@ -4104,13 +3920,6 @@ snapshots:
|
|||||||
'@smithy/types': 4.19.0
|
'@smithy/types': 4.19.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/signature-v4-multi-region@3.996.49':
|
|
||||||
dependencies:
|
|
||||||
'@aws-sdk/types': 3.975.0
|
|
||||||
'@smithy/signature-v4': 5.7.4
|
|
||||||
'@smithy/types': 4.19.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@aws-sdk/token-providers@3.1080.0':
|
'@aws-sdk/token-providers@3.1080.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/core': 3.978.1
|
'@aws-sdk/core': 3.978.1
|
||||||
@@ -4165,15 +3974,6 @@ snapshots:
|
|||||||
'@smithy/types': 4.19.0
|
'@smithy/types': 4.19.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/token-providers@3.1155.0':
|
|
||||||
dependencies:
|
|
||||||
'@aws-sdk/core': 3.978.5
|
|
||||||
'@aws-sdk/nested-clients': 3.997.50
|
|
||||||
'@aws-sdk/types': 3.975.0
|
|
||||||
'@smithy/core': 3.36.2
|
|
||||||
'@smithy/types': 4.19.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@aws-sdk/types@3.973.15':
|
'@aws-sdk/types@3.973.15':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@smithy/types': 4.17.2
|
'@smithy/types': 4.17.2
|
||||||
@@ -4199,11 +3999,6 @@ snapshots:
|
|||||||
'@smithy/types': 4.19.0
|
'@smithy/types': 4.19.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@aws-sdk/types@3.975.0':
|
|
||||||
dependencies:
|
|
||||||
'@smithy/types': 4.19.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@aws-sdk/util-dynamodb@3.996.9(@aws-sdk/client-dynamodb@3.1112.0)':
|
'@aws-sdk/util-dynamodb@3.996.9(@aws-sdk/client-dynamodb@3.1112.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@aws-sdk/client-dynamodb': 3.1112.0
|
'@aws-sdk/client-dynamodb': 3.1112.0
|
||||||
@@ -4701,11 +4496,6 @@ snapshots:
|
|||||||
'@smithy/types': 4.19.0
|
'@smithy/types': 4.19.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@smithy/core@3.36.2':
|
|
||||||
dependencies:
|
|
||||||
'@smithy/types': 4.19.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@smithy/credential-provider-imds@4.4.14':
|
'@smithy/credential-provider-imds@4.4.14':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@smithy/core': 3.33.3
|
'@smithy/core': 3.33.3
|
||||||
@@ -4825,12 +4615,6 @@ snapshots:
|
|||||||
'@smithy/types': 4.19.0
|
'@smithy/types': 4.19.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@smithy/signature-v4@5.7.4':
|
|
||||||
dependencies:
|
|
||||||
'@smithy/core': 3.36.2
|
|
||||||
'@smithy/types': 4.19.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@smithy/types@4.15.1':
|
'@smithy/types@4.15.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|||||||
Reference in New Issue
Block a user