create-aws-project 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +118 -0
  2. package/dist/__tests__/generator/replace-tokens.spec.d.ts +1 -0
  3. package/dist/__tests__/generator/replace-tokens.spec.js +281 -0
  4. package/dist/__tests__/generator.spec.d.ts +1 -0
  5. package/dist/__tests__/generator.spec.js +162 -0
  6. package/dist/__tests__/validation/project-name.spec.d.ts +1 -0
  7. package/dist/__tests__/validation/project-name.spec.js +57 -0
  8. package/dist/__tests__/wizard.spec.d.ts +1 -0
  9. package/dist/__tests__/wizard.spec.js +232 -0
  10. package/dist/aws/iam.d.ts +75 -0
  11. package/dist/aws/iam.js +264 -0
  12. package/dist/aws/organizations.d.ts +79 -0
  13. package/dist/aws/organizations.js +168 -0
  14. package/dist/cli.d.ts +4 -0
  15. package/dist/cli.js +206 -0
  16. package/dist/commands/setup-github.d.ts +4 -0
  17. package/dist/commands/setup-github.js +185 -0
  18. package/dist/generator/copy-file.d.ts +15 -0
  19. package/dist/generator/copy-file.js +56 -0
  20. package/dist/generator/generate-project.d.ts +14 -0
  21. package/dist/generator/generate-project.js +81 -0
  22. package/dist/generator/index.d.ts +4 -0
  23. package/dist/generator/index.js +3 -0
  24. package/dist/generator/replace-tokens.d.ts +29 -0
  25. package/dist/generator/replace-tokens.js +68 -0
  26. package/dist/github/secrets.d.ts +109 -0
  27. package/dist/github/secrets.js +275 -0
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.js +6 -0
  30. package/dist/prompts/auth.d.ts +3 -0
  31. package/dist/prompts/auth.js +23 -0
  32. package/dist/prompts/aws-config.d.ts +2 -0
  33. package/dist/prompts/aws-config.js +14 -0
  34. package/dist/prompts/features.d.ts +2 -0
  35. package/dist/prompts/features.js +10 -0
  36. package/dist/prompts/github-setup.d.ts +53 -0
  37. package/dist/prompts/github-setup.js +208 -0
  38. package/dist/prompts/org-structure.d.ts +9 -0
  39. package/dist/prompts/org-structure.js +93 -0
  40. package/dist/prompts/platforms.d.ts +2 -0
  41. package/dist/prompts/platforms.js +12 -0
  42. package/dist/prompts/project-name.d.ts +2 -0
  43. package/dist/prompts/project-name.js +8 -0
  44. package/dist/prompts/theme.d.ts +2 -0
  45. package/dist/prompts/theme.js +14 -0
  46. package/dist/templates/index.d.ts +4 -0
  47. package/dist/templates/index.js +2 -0
  48. package/dist/templates/manifest.d.ts +11 -0
  49. package/dist/templates/manifest.js +99 -0
  50. package/dist/templates/tokens.d.ts +39 -0
  51. package/dist/templates/tokens.js +37 -0
  52. package/dist/templates/types.d.ts +52 -0
  53. package/dist/templates/types.js +1 -0
  54. package/dist/types.d.ts +27 -0
  55. package/dist/types.js +1 -0
  56. package/dist/validation/project-name.d.ts +1 -0
  57. package/dist/validation/project-name.js +12 -0
  58. package/dist/wizard.d.ts +2 -0
  59. package/dist/wizard.js +81 -0
  60. package/package.json +68 -0
  61. package/templates/.github/actions/build-and-test/action.yml +24 -0
  62. package/templates/.github/actions/deploy-cdk/action.yml +46 -0
  63. package/templates/.github/actions/deploy-web/action.yml +72 -0
  64. package/templates/.github/actions/setup/action.yml +29 -0
  65. package/templates/.github/pull_request_template.md +15 -0
  66. package/templates/.github/workflows/deploy-dev.yml +80 -0
  67. package/templates/.github/workflows/deploy-prod.yml +67 -0
  68. package/templates/.github/workflows/deploy-stage.yml +77 -0
  69. package/templates/.github/workflows/pull-request.yml +72 -0
  70. package/templates/.vscode/extensions.json +7 -0
  71. package/templates/.vscode/settings.json +67 -0
  72. package/templates/apps/api/.eslintrc.json +18 -0
  73. package/templates/apps/api/cdk/app.ts +93 -0
  74. package/templates/apps/api/cdk/auth/cognito-stack.ts +164 -0
  75. package/templates/apps/api/cdk/cdk.json +73 -0
  76. package/templates/apps/api/cdk/deployment-user-stack.ts +187 -0
  77. package/templates/apps/api/cdk/org-stack.ts +67 -0
  78. package/templates/apps/api/cdk/static-stack.ts +361 -0
  79. package/templates/apps/api/cdk/tsconfig.json +39 -0
  80. package/templates/apps/api/cdk/user-stack.ts +255 -0
  81. package/templates/apps/api/jest.config.ts +38 -0
  82. package/templates/apps/api/lambdas.yml +84 -0
  83. package/templates/apps/api/project.json.template +58 -0
  84. package/templates/apps/api/src/__tests__/setup.ts +10 -0
  85. package/templates/apps/api/src/handlers/users/create-user.ts +52 -0
  86. package/templates/apps/api/src/handlers/users/delete-user.ts +45 -0
  87. package/templates/apps/api/src/handlers/users/get-me.ts +72 -0
  88. package/templates/apps/api/src/handlers/users/get-user.ts +45 -0
  89. package/templates/apps/api/src/handlers/users/get-users.ts +23 -0
  90. package/templates/apps/api/src/handlers/users/index.ts +17 -0
  91. package/templates/apps/api/src/handlers/users/update-user.ts +72 -0
  92. package/templates/apps/api/src/lib/dynamo/dynamo-model.ts +504 -0
  93. package/templates/apps/api/src/lib/dynamo/index.ts +12 -0
  94. package/templates/apps/api/src/lib/dynamo/utils.ts +39 -0
  95. package/templates/apps/api/src/middleware/auth0-auth.ts +97 -0
  96. package/templates/apps/api/src/middleware/cognito-auth.ts +90 -0
  97. package/templates/apps/api/src/models/UserModel.ts +109 -0
  98. package/templates/apps/api/src/schemas/user.schema.ts +44 -0
  99. package/templates/apps/api/src/services/user-service.ts +108 -0
  100. package/templates/apps/api/src/utils/auth-context.ts +60 -0
  101. package/templates/apps/api/src/utils/common/helpers.ts +26 -0
  102. package/templates/apps/api/src/utils/lambda-handler.ts +148 -0
  103. package/templates/apps/api/src/utils/response.ts +52 -0
  104. package/templates/apps/api/src/utils/validator.ts +75 -0
  105. package/templates/apps/api/tsconfig.app.json +15 -0
  106. package/templates/apps/api/tsconfig.json +19 -0
  107. package/templates/apps/api/tsconfig.spec.json +17 -0
  108. package/templates/apps/mobile/.env.example +5 -0
  109. package/templates/apps/mobile/.eslintrc.json +33 -0
  110. package/templates/apps/mobile/app.json +33 -0
  111. package/templates/apps/mobile/assets/.gitkeep +0 -0
  112. package/templates/apps/mobile/babel.config.js +19 -0
  113. package/templates/apps/mobile/index.js +7 -0
  114. package/templates/apps/mobile/jest.config.ts +22 -0
  115. package/templates/apps/mobile/metro.config.js +35 -0
  116. package/templates/apps/mobile/package.json +22 -0
  117. package/templates/apps/mobile/project.json.template +64 -0
  118. package/templates/apps/mobile/src/App.tsx +367 -0
  119. package/templates/apps/mobile/src/__tests__/App.spec.tsx +46 -0
  120. package/templates/apps/mobile/src/__tests__/store/user-store.spec.ts +156 -0
  121. package/templates/apps/mobile/src/config/api.ts +16 -0
  122. package/templates/apps/mobile/src/store/user-store.ts +56 -0
  123. package/templates/apps/mobile/src/test-setup.ts +10 -0
  124. package/templates/apps/mobile/tsconfig.json +22 -0
  125. package/templates/apps/web/.env.example +13 -0
  126. package/templates/apps/web/.eslintrc.json +26 -0
  127. package/templates/apps/web/index.html +13 -0
  128. package/templates/apps/web/jest.config.ts +24 -0
  129. package/templates/apps/web/package.json +15 -0
  130. package/templates/apps/web/project.json.template +66 -0
  131. package/templates/apps/web/src/App.tsx +352 -0
  132. package/templates/apps/web/src/__mocks__/config/api.ts +41 -0
  133. package/templates/apps/web/src/__tests__/App.spec.tsx +240 -0
  134. package/templates/apps/web/src/__tests__/store/user-store.spec.ts +185 -0
  135. package/templates/apps/web/src/auth/auth0-provider.tsx +103 -0
  136. package/templates/apps/web/src/auth/cognito-provider.tsx +143 -0
  137. package/templates/apps/web/src/auth/index.ts +7 -0
  138. package/templates/apps/web/src/auth/use-auth.ts +16 -0
  139. package/templates/apps/web/src/config/amplify-config.ts +31 -0
  140. package/templates/apps/web/src/config/api.ts +38 -0
  141. package/templates/apps/web/src/config/auth0-config.ts +17 -0
  142. package/templates/apps/web/src/main.tsx +41 -0
  143. package/templates/apps/web/src/store/user-store.ts +56 -0
  144. package/templates/apps/web/src/styles.css +165 -0
  145. package/templates/apps/web/src/test-setup.ts +1 -0
  146. package/templates/apps/web/src/theme/index.ts +30 -0
  147. package/templates/apps/web/src/vite-env.d.ts +19 -0
  148. package/templates/apps/web/tsconfig.app.json +24 -0
  149. package/templates/apps/web/tsconfig.json +22 -0
  150. package/templates/apps/web/tsconfig.spec.json +28 -0
  151. package/templates/apps/web/vite.config.ts +87 -0
  152. package/templates/manifest.json +28 -0
  153. package/templates/packages/api-client/.eslintrc.json +18 -0
  154. package/templates/packages/api-client/jest.config.ts +13 -0
  155. package/templates/packages/api-client/package.json +8 -0
  156. package/templates/packages/api-client/project.json.template +34 -0
  157. package/templates/packages/api-client/src/__tests__/api-client.spec.ts +408 -0
  158. package/templates/packages/api-client/src/api-client.ts +201 -0
  159. package/templates/packages/api-client/src/config.ts +193 -0
  160. package/templates/packages/api-client/src/index.ts +9 -0
  161. package/templates/packages/api-client/tsconfig.json +22 -0
  162. package/templates/packages/api-client/tsconfig.lib.json +11 -0
  163. package/templates/packages/api-client/tsconfig.spec.json +14 -0
  164. package/templates/packages/common-types/.eslintrc.json +18 -0
  165. package/templates/packages/common-types/package.json +6 -0
  166. package/templates/packages/common-types/project.json.template +26 -0
  167. package/templates/packages/common-types/src/api.types.ts +24 -0
  168. package/templates/packages/common-types/src/auth.types.ts +36 -0
  169. package/templates/packages/common-types/src/common.types.ts +46 -0
  170. package/templates/packages/common-types/src/index.ts +19 -0
  171. package/templates/packages/common-types/src/lambda.types.ts +39 -0
  172. package/templates/packages/common-types/src/user.types.ts +31 -0
  173. package/templates/packages/common-types/tsconfig.json +19 -0
  174. package/templates/packages/common-types/tsconfig.lib.json +11 -0
  175. package/templates/root/.editorconfig +23 -0
  176. package/templates/root/.nvmrc +1 -0
  177. package/templates/root/eslint.config.js +61 -0
  178. package/templates/root/jest.preset.js +16 -0
  179. package/templates/root/nx.json +29 -0
  180. package/templates/root/package.json +131 -0
  181. package/templates/root/tsconfig.base.json +29 -0
@@ -0,0 +1,90 @@
1
+ import { CognitoJwtVerifier } from 'aws-jwt-verify';
2
+ import type { CognitoJwtPayload } from 'aws-jwt-verify/jwt-model';
3
+ import { errorResponse } from '../utils/response';
4
+ import { ERROR_CODES, HTTP_STATUS } from '{{PACKAGE_SCOPE}}/common-types';
5
+
6
+ /**
7
+ * Authenticated user information extracted from JWT token
8
+ */
9
+ export interface AuthUser {
10
+ /** Cognito user ID (subject claim) */
11
+ sub: string;
12
+ /** User's email address (if available in token) */
13
+ email?: string;
14
+ /** Cognito groups the user belongs to */
15
+ groups?: string[];
16
+ /** Full JWT payload for additional claims access */
17
+ tokenPayload: CognitoJwtPayload;
18
+ }
19
+
20
+ // Verifier instantiated outside handler for JWKS caching across Lambda invocations
21
+ const verifier = CognitoJwtVerifier.create({
22
+ userPoolId: process.env['COGNITO_USER_POOL_ID']!,
23
+ tokenUse: 'access',
24
+ clientId: process.env['COGNITO_CLIENT_ID']!,
25
+ });
26
+
27
+ /**
28
+ * Verify a JWT access token from the Authorization header
29
+ *
30
+ * @param authHeader - The Authorization header value (e.g., "Bearer <token>")
31
+ * @returns The authenticated user if token is valid, null otherwise
32
+ */
33
+ export async function verifyToken(authHeader: string | undefined): Promise<AuthUser | null> {
34
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
35
+ return null;
36
+ }
37
+
38
+ const token = authHeader.slice(7); // Remove 'Bearer ' prefix
39
+
40
+ try {
41
+ const payload = await verifier.verify(token);
42
+ return {
43
+ sub: payload.sub,
44
+ email: payload.email as string | undefined,
45
+ groups: payload['cognito:groups'] as string[] | undefined,
46
+ tokenPayload: payload,
47
+ };
48
+ } catch (error) {
49
+ console.error('Token verification failed:', error);
50
+ return null;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Middleware wrapper that requires authentication for a handler
56
+ *
57
+ * Wraps a handler function to automatically verify the JWT token
58
+ * and pass the authenticated user to the handler.
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * export const handler = requireAuth(async (event, user) => {
63
+ * console.log('User ID:', user.sub);
64
+ * return successResponse({ userId: user.sub });
65
+ * });
66
+ * ```
67
+ *
68
+ * @param handler - Handler function that receives the event and authenticated user
69
+ * @returns Wrapped handler that validates authentication first
70
+ */
71
+ export function requireAuth<T>(
72
+ handler: (event: T, user: AuthUser) => Promise<unknown>
73
+ ): (event: T) => Promise<unknown> {
74
+ return async (event: T) => {
75
+ const authHeader = (event as Record<string, Record<string, string>>).headers?.authorization ||
76
+ (event as Record<string, Record<string, string>>).headers?.Authorization;
77
+
78
+ const user = await verifyToken(authHeader);
79
+
80
+ if (!user) {
81
+ return errorResponse(
82
+ ERROR_CODES.UNAUTHORIZED,
83
+ 'Invalid or missing authentication token',
84
+ HTTP_STATUS.UNAUTHORIZED
85
+ );
86
+ }
87
+
88
+ return handler(event, user);
89
+ };
90
+ }
@@ -0,0 +1,109 @@
1
+ import { DynamoModel, BaseModel, generateUUID } from '../lib/dynamo';
2
+ import type { User as UserType } from '{{PACKAGE_SCOPE}}/common-types';
3
+
4
+ /**
5
+ * User interface extending BaseModel for DynamoDB
6
+ */
7
+ export interface UserModel extends BaseModel {
8
+ email: string;
9
+ name: string;
10
+ }
11
+
12
+ /**
13
+ * User Model for DynamoDB operations
14
+ * Extends DynamoModel to provide user-specific functionality
15
+ */
16
+ export class UserDynamoModel extends DynamoModel<UserModel> {
17
+ constructor(tableName?: string, region?: string) {
18
+ super(
19
+ tableName || process.env['DYNAMODB_TABLE'] || '{{PROJECT_NAME}}-table',
20
+ region || process.env['AWS_REGION'] || 'us-east-1'
21
+ );
22
+ }
23
+
24
+ /**
25
+ * Get entity name for logging
26
+ */
27
+ protected getEntityName(): string {
28
+ return 'User';
29
+ }
30
+
31
+ /**
32
+ * Generate unique ID for new users
33
+ */
34
+ protected async generateId(data: UserModel): Promise<string> {
35
+ return generateUUID();
36
+ }
37
+
38
+ /**
39
+ * Set GSI keys for user queries
40
+ * GSI1: Query users by email
41
+ * GSI2: Query users by creation date
42
+ */
43
+ protected setGSIKeys(entity: UserModel, now: string): void {
44
+ // GSI1: For email lookups
45
+ entity.pk1 = 'USER';
46
+ entity.sk1 = `EMAIL#${entity.email}`;
47
+
48
+ // GSI2: For time-based queries
49
+ entity.pk2 = 'USER';
50
+ entity.sk2 = `CREATED#${now}`;
51
+ }
52
+
53
+ /**
54
+ * Transform UserModel to User type for API responses
55
+ */
56
+ toUserType(userModel: UserModel): UserType {
57
+ return {
58
+ id: userModel.id,
59
+ email: userModel.email,
60
+ name: userModel.name,
61
+ createdAt: userModel.createdAt,
62
+ updatedAt: userModel.updatedAt,
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Get user by email
68
+ */
69
+ async getByEmail(email: string): Promise<UserModel | null> {
70
+ try {
71
+ this.logger.info(`Getting user by email: ${email}`);
72
+
73
+ const results = await this.queryByGSI(
74
+ 'GSI1',
75
+ 'pk1 = :pk AND sk1 = :sk',
76
+ {
77
+ ':pk': 'USER',
78
+ ':sk': `EMAIL#${email}`,
79
+ }
80
+ );
81
+
82
+ return results.length > 0 ? results[0] : null;
83
+ } catch (error) {
84
+ this.logger.error('Error getting user by email', { email, error });
85
+ throw error;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Get users created after a specific date
91
+ */
92
+ async getByCreatedAfter(date: string): Promise<UserModel[]> {
93
+ try {
94
+ this.logger.info(`Getting users created after: ${date}`);
95
+
96
+ return await this.queryByGSI(
97
+ 'GSI2',
98
+ 'pk2 = :pk AND sk2 > :sk',
99
+ {
100
+ ':pk': 'USER',
101
+ ':sk': `CREATED#${date}`,
102
+ }
103
+ );
104
+ } catch (error) {
105
+ this.logger.error('Error getting users by created date', { date, error });
106
+ throw error;
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,44 @@
1
+ import { JSONSchemaType } from 'ajv';
2
+ import type { CreateUserRequest, UpdateUserRequest } from '{{PACKAGE_SCOPE}}/common-types';
3
+
4
+ /**
5
+ * JSON Schema for CreateUserRequest
6
+ */
7
+ export const createUserSchema: JSONSchemaType<CreateUserRequest> = {
8
+ type: 'object',
9
+ properties: {
10
+ email: {
11
+ type: 'string',
12
+ format: 'email',
13
+ minLength: 3,
14
+ maxLength: 255,
15
+ },
16
+ name: {
17
+ type: 'string',
18
+ minLength: 1,
19
+ maxLength: 255,
20
+ pattern: '^(?!\\s*$)(?!.*\\s$)(?!^\\s).*$', // No leading/trailing whitespace, not empty
21
+ },
22
+ },
23
+ required: ['email', 'name'],
24
+ additionalProperties: false,
25
+ };
26
+
27
+ /**
28
+ * JSON Schema for UpdateUserRequest
29
+ */
30
+ export const updateUserSchema: JSONSchemaType<UpdateUserRequest> = {
31
+ type: 'object',
32
+ properties: {
33
+ name: {
34
+ type: 'string',
35
+ minLength: 1,
36
+ maxLength: 255,
37
+ pattern: '^(?!\\s*$)(?!.*\\s$)(?!^\\s).*$', // No leading/trailing whitespace, not empty
38
+ nullable: true,
39
+ },
40
+ },
41
+ required: [],
42
+ additionalProperties: false,
43
+ minProperties: 1, // At least one property must be present
44
+ };
@@ -0,0 +1,108 @@
1
+ import type { User, CreateUserRequest, UpdateUserRequest } from '{{PACKAGE_SCOPE}}/common-types';
2
+ import { UserDynamoModel } from '../models/UserModel';
3
+
4
+ /**
5
+ * User Service
6
+ * Handles all business logic for user management with DynamoDB backend
7
+ */
8
+ export class UserService {
9
+ private userModel: UserDynamoModel;
10
+
11
+ constructor() {
12
+ this.userModel = new UserDynamoModel();
13
+ }
14
+
15
+ /**
16
+ * Get all users
17
+ */
18
+ async getAllUsers(): Promise<User[]> {
19
+ const users = await this.userModel.scanAll();
20
+ return users.map(user => this.userModel.toUserType(user));
21
+ }
22
+
23
+ /**
24
+ * Get user by ID
25
+ */
26
+ async getUserById(id: string): Promise<User | null> {
27
+ const user = await this.userModel.getById(id);
28
+ return user ? this.userModel.toUserType(user) : null;
29
+ }
30
+
31
+ /**
32
+ * Get user by email
33
+ */
34
+ async getUserByEmail(email: string): Promise<User | null> {
35
+ const user = await this.userModel.getByEmail(email);
36
+ return user ? this.userModel.toUserType(user) : null;
37
+ }
38
+
39
+ /**
40
+ * Create a new user
41
+ */
42
+ async createUser(request: CreateUserRequest): Promise<User> {
43
+ // Check if user with email already exists
44
+ const existingUser = await this.userModel.getByEmail(request.email);
45
+ if (existingUser) {
46
+ throw new Error(`User with email ${request.email} already exists`);
47
+ }
48
+
49
+ const user = await this.userModel.create({
50
+ email: request.email,
51
+ name: request.name,
52
+ });
53
+
54
+ return this.userModel.toUserType(user);
55
+ }
56
+
57
+ /**
58
+ * Update an existing user
59
+ */
60
+ async updateUser(id: string, request: UpdateUserRequest): Promise<User | null> {
61
+ const updatedUser = await this.userModel.update(id, {
62
+ ...(request.name && { name: request.name }),
63
+ });
64
+
65
+ return updatedUser ? this.userModel.toUserType(updatedUser) : null;
66
+ }
67
+
68
+ /**
69
+ * Delete a user
70
+ */
71
+ async deleteUser(id: string): Promise<boolean> {
72
+ return await this.userModel.delete(id);
73
+ }
74
+
75
+ /**
76
+ * Delete multiple users
77
+ */
78
+ async batchDeleteUsers(ids: string[]): Promise<{ success: string[]; failed: Array<{ id: string; error: string }> }> {
79
+ return await this.userModel.batchDelete(ids);
80
+ }
81
+
82
+ /**
83
+ * Check if user exists
84
+ */
85
+ async userExists(id: string): Promise<boolean> {
86
+ const user = await this.userModel.getById(id);
87
+ return user !== null;
88
+ }
89
+
90
+ /**
91
+ * Get user count
92
+ */
93
+ async getUserCount(): Promise<number> {
94
+ const users = await this.userModel.scanAll();
95
+ return users.length;
96
+ }
97
+
98
+ /**
99
+ * Get users created after a specific date
100
+ */
101
+ async getUsersCreatedAfter(date: string): Promise<User[]> {
102
+ const users = await this.userModel.getByCreatedAfter(date);
103
+ return users.map(user => this.userModel.toUserType(user));
104
+ }
105
+ }
106
+
107
+ // Export singleton instance
108
+ export const userService = new UserService();
@@ -0,0 +1,60 @@
1
+ import type { APIGatewayProxyEvent } from 'aws-lambda';
2
+ import type { AuthUser } from '../middleware/cognito-auth';
3
+
4
+ /**
5
+ * Get auth user from request context
6
+ * Used when auth is handled by API Gateway authorizer instead of middleware
7
+ *
8
+ * @param event - API Gateway proxy event with authorizer claims
9
+ * @returns AuthUser if claims present in context, null otherwise
10
+ */
11
+ export function getAuthUserFromContext(event: APIGatewayProxyEvent): AuthUser | null {
12
+ const claims = event.requestContext?.authorizer?.claims;
13
+
14
+ if (!claims) {
15
+ return null;
16
+ }
17
+
18
+ return {
19
+ sub: claims.sub,
20
+ email: claims.email,
21
+ groups: claims['cognito:groups']?.split(','),
22
+ tokenPayload: claims as AuthUser['tokenPayload'],
23
+ };
24
+ }
25
+
26
+ /**
27
+ * Check if user has required group membership
28
+ *
29
+ * @param user - Authenticated user
30
+ * @param groupName - Name of the Cognito group to check
31
+ * @returns true if user belongs to the specified group
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * if (hasGroup(user, 'admins')) {
36
+ * // User is an admin
37
+ * }
38
+ * ```
39
+ */
40
+ export function hasGroup(user: AuthUser, groupName: string): boolean {
41
+ return user.groups?.includes(groupName) ?? false;
42
+ }
43
+
44
+ /**
45
+ * Check if user is the owner of a resource
46
+ *
47
+ * @param user - Authenticated user
48
+ * @param resourceOwnerId - The owner ID stored on the resource
49
+ * @returns true if the user's sub matches the resource owner ID
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * if (!isOwner(user, document.ownerId)) {
54
+ * return errorResponse(ERROR_CODES.FORBIDDEN, 'Not authorized');
55
+ * }
56
+ * ```
57
+ */
58
+ export function isOwner(user: AuthUser, resourceOwnerId: string): boolean {
59
+ return user.sub === resourceOwnerId;
60
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Common utility functions
3
+ */
4
+
5
+ /**
6
+ * Remove GSI (Global Secondary Index) fields from an entity
7
+ * Useful for cleaning data before returning to clients
8
+ */
9
+ export function removeGSIFields<T extends Record<string, any>>(entity: T): Omit<T, 'pk1' | 'sk1' | 'pk2' | 'sk2' | 'pk3' | 'sk3' | 'pk4' | 'sk4' | 'pk5' | 'sk5' | 'pk6' | 'sk6'> {
10
+ const { pk1, sk1, pk2, sk2, pk3, sk3, pk4, sk4, pk5, sk5, pk6, sk6, ...rest } = entity;
11
+ return rest as Omit<T, 'pk1' | 'sk1' | 'pk2' | 'sk2' | 'pk3' | 'sk3' | 'pk4' | 'sk4' | 'pk5' | 'sk5' | 'pk6' | 'sk6'>;
12
+ }
13
+
14
+ /**
15
+ * Generate a unique ID
16
+ */
17
+ export function generateUUID(): string {
18
+ return crypto.randomUUID();
19
+ }
20
+
21
+ /**
22
+ * Get current ISO timestamp
23
+ */
24
+ export function getCurrentTimestamp(): string {
25
+ return new Date().toISOString();
26
+ }
@@ -0,0 +1,148 @@
1
+ import type {
2
+ ApiGatewayProxyEvent,
3
+ ApiGatewayProxyResult,
4
+ } from '{{PACKAGE_SCOPE}}/common-types';
5
+ import { HTTP_STATUS, ERROR_CODES } from '{{PACKAGE_SCOPE}}/common-types';
6
+ import { errorResponse } from './response';
7
+
8
+ /**
9
+ * Parsed request data from API Gateway event
10
+ */
11
+ export interface ParsedRequest<TBody = unknown> {
12
+ pathParameters: Record<string, string>;
13
+ queryParameters: Record<string, string>;
14
+ headers: Record<string, string>;
15
+ body: TBody | null;
16
+ rawBody: string | null;
17
+ httpMethod: string;
18
+ path: string;
19
+ }
20
+
21
+ /**
22
+ * Handler function type that receives parsed request data
23
+ */
24
+ export type HandlerFunction<TBody = unknown, TResponse = unknown> = (
25
+ request: ParsedRequest<TBody>
26
+ ) => Promise<TResponse>;
27
+
28
+ /**
29
+ * Parse the API Gateway event into a more convenient format
30
+ */
31
+ export function parseRequest<TBody = unknown>(
32
+ event: ApiGatewayProxyEvent
33
+ ): ParsedRequest<TBody> {
34
+ const pathParameters = event.pathParameters || {};
35
+ const queryParameters = event.queryStringParameters || {};
36
+ const headers = event.headers || {};
37
+ const rawBody = event.body;
38
+
39
+ let body: TBody | null = null;
40
+ if (rawBody) {
41
+ try {
42
+ body = JSON.parse(rawBody) as TBody;
43
+ } catch (error) {
44
+ // Body parsing will be handled by validation in handlers
45
+ body = null;
46
+ }
47
+ }
48
+
49
+ return {
50
+ pathParameters,
51
+ queryParameters,
52
+ headers,
53
+ body,
54
+ rawBody,
55
+ httpMethod: event.httpMethod,
56
+ path: event.path,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Validate that required path parameters are present
62
+ */
63
+ export function validatePathParameters(
64
+ pathParameters: Record<string, string>,
65
+ required: string[]
66
+ ): { valid: boolean; missing?: string[] } {
67
+ const missing = required.filter(param => !pathParameters[param]);
68
+
69
+ if (missing.length > 0) {
70
+ return { valid: false, missing };
71
+ }
72
+
73
+ return { valid: true };
74
+ }
75
+
76
+ /**
77
+ * Validate that request body is present
78
+ */
79
+ export function validateBodyPresent(
80
+ body: unknown,
81
+ rawBody: string | null
82
+ ): { valid: boolean; error?: string } {
83
+ if (!rawBody) {
84
+ return { valid: false, error: 'Request body is required' };
85
+ }
86
+
87
+ if (!body) {
88
+ return { valid: false, error: 'Invalid JSON in request body' };
89
+ }
90
+
91
+ return { valid: true };
92
+ }
93
+
94
+ /**
95
+ * Wrapper for Lambda handlers that handles common request parsing and error handling
96
+ *
97
+ * @param handlerFn - The handler function to execute
98
+ * @param handlerName - Name of the handler for logging purposes
99
+ */
100
+ export function createLambdaHandler<TBody = unknown, TResponse = unknown>(
101
+ handlerFn: HandlerFunction<TBody, TResponse>,
102
+ handlerName: string
103
+ ) {
104
+ return async (event: ApiGatewayProxyEvent): Promise<ApiGatewayProxyResult> => {
105
+ console.log(`[${handlerName}] Event:`, JSON.stringify(event, null, 2));
106
+
107
+ try {
108
+ const parsedRequest = parseRequest<TBody>(event);
109
+
110
+ console.log(`[${handlerName}] Parsed request:`, {
111
+ pathParameters: parsedRequest.pathParameters,
112
+ queryParameters: parsedRequest.queryParameters,
113
+ httpMethod: parsedRequest.httpMethod,
114
+ path: parsedRequest.path,
115
+ hasBody: !!parsedRequest.body,
116
+ });
117
+
118
+ const result = await handlerFn(parsedRequest);
119
+ return result as ApiGatewayProxyResult;
120
+ } catch (error) {
121
+ console.error(`[${handlerName}] Error:`, error);
122
+
123
+ // If error is already an ApiGatewayProxyResult, return it
124
+ if (error && typeof error === 'object' && 'statusCode' in error && 'body' in error) {
125
+ return error as ApiGatewayProxyResult;
126
+ }
127
+
128
+ // Otherwise, return a generic error
129
+ return errorResponse(
130
+ ERROR_CODES.INTERNAL_ERROR,
131
+ `Failed to process request in ${handlerName}`,
132
+ HTTP_STATUS.INTERNAL_SERVER_ERROR
133
+ );
134
+ }
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Create an error result that can be thrown and caught by the handler wrapper
140
+ */
141
+ export function createErrorResult(
142
+ code: string,
143
+ message: string,
144
+ statusCode: (typeof HTTP_STATUS)[keyof typeof HTTP_STATUS],
145
+ details?: Record<string, unknown>
146
+ ): ApiGatewayProxyResult {
147
+ return errorResponse(code, message, statusCode, details);
148
+ }
@@ -0,0 +1,52 @@
1
+ import type {
2
+ ApiGatewayProxyResult,
3
+ ApiResponse,
4
+ HTTP_STATUS,
5
+ } from '{{PACKAGE_SCOPE}}/common-types';
6
+
7
+ const defaultHeaders = {
8
+ 'Content-Type': 'application/json',
9
+ 'Access-Control-Allow-Origin': process.env['ALLOWED_ORIGINS'] || '*',
10
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
11
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
12
+ };
13
+
14
+ export function successResponse<T>(
15
+ data: T,
16
+ statusCode: (typeof HTTP_STATUS)[keyof typeof HTTP_STATUS] = 200,
17
+ message?: string
18
+ ): ApiGatewayProxyResult {
19
+ const response: ApiResponse<T> = {
20
+ success: true,
21
+ data,
22
+ message,
23
+ };
24
+
25
+ return {
26
+ statusCode,
27
+ headers: defaultHeaders,
28
+ body: JSON.stringify(response),
29
+ };
30
+ }
31
+
32
+ export function errorResponse(
33
+ code: string,
34
+ message: string,
35
+ statusCode: (typeof HTTP_STATUS)[keyof typeof HTTP_STATUS] = 500,
36
+ details?: Record<string, unknown>
37
+ ): ApiGatewayProxyResult {
38
+ const response: ApiResponse = {
39
+ success: false,
40
+ error: {
41
+ code,
42
+ message,
43
+ details,
44
+ },
45
+ };
46
+
47
+ return {
48
+ statusCode,
49
+ headers: defaultHeaders,
50
+ body: JSON.stringify(response),
51
+ };
52
+ }