@girardmedia/bootspring 3.3.2 → 3.4.0
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.
- package/assets/agents/accessibility-auditor.md +39 -0
- package/assets/agents/api-designer.md +40 -0
- package/assets/agents/auth-implementer.md +64 -0
- package/assets/agents/bug-hunter.md +42 -0
- package/assets/agents/bundle-analyzer.md +40 -0
- package/assets/agents/cache-optimizer.md +55 -0
- package/assets/agents/changelog-writer.md +55 -0
- package/assets/agents/ci-cd-builder.md +40 -0
- package/assets/agents/code-explainer.md +39 -0
- package/assets/agents/code-reviewer.md +39 -0
- package/assets/agents/cost-optimizer.md +57 -0
- package/assets/agents/cron-scheduler.md +51 -0
- package/assets/agents/data-seeder.md +56 -0
- package/assets/agents/database-architect.md +40 -0
- package/assets/agents/dependency-updater.md +40 -0
- package/assets/agents/deploy-checker.md +40 -0
- package/assets/agents/docker-optimizer.md +40 -0
- package/assets/agents/documentation-writer.md +40 -0
- package/assets/agents/email-builder.md +55 -0
- package/assets/agents/env-setup.md +40 -0
- package/assets/agents/error-handler.md +40 -0
- package/assets/agents/eslint-fixer.md +46 -0
- package/assets/agents/feature-flagger.md +69 -0
- package/assets/agents/git-detective.md +39 -0
- package/assets/agents/graphql-builder.md +60 -0
- package/assets/agents/incident-responder.md +59 -0
- package/assets/agents/log-analyzer.md +39 -0
- package/assets/agents/migration-planner.md +41 -0
- package/assets/agents/monorepo-navigator.md +39 -0
- package/assets/agents/nextjs-expert.md +57 -0
- package/assets/agents/notification-builder.md +56 -0
- package/assets/agents/onboarding-guide.md +39 -0
- package/assets/agents/performance-profiler.md +40 -0
- package/assets/agents/prisma-expert.md +57 -0
- package/assets/agents/rate-limiter.md +58 -0
- package/assets/agents/react-expert.md +58 -0
- package/assets/agents/refactorer.md +42 -0
- package/assets/agents/regex-builder.md +46 -0
- package/assets/agents/release-manager.md +40 -0
- package/assets/agents/s3-manager.md +58 -0
- package/assets/agents/schema-validator.md +40 -0
- package/assets/agents/search-builder.md +62 -0
- package/assets/agents/security-auditor.md +39 -0
- package/assets/agents/sitemap-generator.md +53 -0
- package/assets/agents/stripe-integrator.md +59 -0
- package/assets/agents/tailwind-expert.md +55 -0
- package/assets/agents/tech-debt-tracker.md +39 -0
- package/assets/agents/test-writer.md +42 -0
- package/assets/agents/type-fixer.md +45 -0
- package/assets/agents/webhook-builder.md +54 -0
- package/assets/rules/cpp.md +53 -0
- package/assets/rules/css.md +52 -0
- package/assets/rules/go.md +50 -0
- package/assets/rules/html.md +52 -0
- package/assets/rules/java.md +51 -0
- package/assets/rules/kotlin.md +50 -0
- package/assets/rules/php.md +51 -0
- package/assets/rules/python.md +51 -0
- package/assets/rules/ruby.md +51 -0
- package/assets/rules/rust.md +49 -0
- package/assets/rules/shell.md +52 -0
- package/assets/rules/sql.md +49 -0
- package/assets/rules/swift.md +50 -0
- package/assets/rules/typescript.md +52 -0
- package/assets/rules/yaml-json.md +51 -0
- package/assets/skills/accessibility.md +210 -0
- package/assets/skills/agent-patterns.md +387 -0
- package/assets/skills/ai-integration.md +263 -0
- package/assets/skills/animation-patterns.md +224 -0
- package/assets/skills/api-design.md +218 -0
- package/assets/skills/api-gateway.md +341 -0
- package/assets/skills/api-versioning.md +226 -0
- package/assets/skills/astro-patterns.md +233 -0
- package/assets/skills/auth-patterns.md +248 -0
- package/assets/skills/aws-patterns.md +171 -0
- package/assets/skills/background-jobs.md +162 -0
- package/assets/skills/browser-extensions.md +309 -0
- package/assets/skills/caching-patterns.md +253 -0
- package/assets/skills/ci-cd.md +251 -0
- package/assets/skills/cli-development.md +296 -0
- package/assets/skills/code-review.md +185 -0
- package/assets/skills/cron-patterns.md +327 -0
- package/assets/skills/data-fetching.md +231 -0
- package/assets/skills/database-migrations.md +346 -0
- package/assets/skills/database-patterns.md +219 -0
- package/assets/skills/debugging.md +281 -0
- package/assets/skills/design-system.md +289 -0
- package/assets/skills/django-patterns.md +182 -0
- package/assets/skills/docker-patterns.md +235 -0
- package/assets/skills/e2e-testing.md +287 -0
- package/assets/skills/edge-computing.md +268 -0
- package/assets/skills/electron-patterns.md +266 -0
- package/assets/skills/email-templates.md +206 -0
- package/assets/skills/error-handling.md +265 -0
- package/assets/skills/event-driven.md +232 -0
- package/assets/skills/express-patterns.md +239 -0
- package/assets/skills/fastapi-patterns.md +198 -0
- package/assets/skills/feature-flags.md +212 -0
- package/assets/skills/figma-to-code.md +298 -0
- package/assets/skills/file-upload.md +228 -0
- package/assets/skills/forms-patterns.md +264 -0
- package/assets/skills/gcp-patterns.md +189 -0
- package/assets/skills/git-workflow.md +187 -0
- package/assets/skills/golang-patterns.md +185 -0
- package/assets/skills/graphql-patterns.md +244 -0
- package/assets/skills/i18n-patterns.md +172 -0
- package/assets/skills/image-processing.md +350 -0
- package/assets/skills/java-springboot.md +226 -0
- package/assets/skills/kotlin-patterns.md +207 -0
- package/assets/skills/kubernetes-patterns.md +326 -0
- package/assets/skills/laravel-patterns.md +261 -0
- package/assets/skills/llm-fine-tuning.md +335 -0
- package/assets/skills/load-testing.md +303 -0
- package/assets/skills/logging-observability.md +228 -0
- package/assets/skills/markdown-processing.md +318 -0
- package/assets/skills/mcp-server-patterns.md +292 -0
- package/assets/skills/microservices.md +272 -0
- package/assets/skills/migration-patterns.md +239 -0
- package/assets/skills/mongodb-patterns.md +189 -0
- package/assets/skills/monorepo-patterns.md +287 -0
- package/assets/skills/nextjs-app-router.md +237 -0
- package/assets/skills/notification-patterns.md +348 -0
- package/assets/skills/oauth-patterns.md +246 -0
- package/assets/skills/payment-integration.md +222 -0
- package/assets/skills/pdf-generation.md +307 -0
- package/assets/skills/performance-optimization.md +277 -0
- package/assets/skills/php-patterns.md +210 -0
- package/assets/skills/prisma-patterns.md +241 -0
- package/assets/skills/prompt-engineering.md +193 -0
- package/assets/skills/pwa-patterns.md +247 -0
- package/assets/skills/python-patterns.md +158 -0
- package/assets/skills/python-testing.md +172 -0
- package/assets/skills/queue-patterns.md +295 -0
- package/assets/skills/rag-patterns.md +159 -0
- package/assets/skills/rate-limiting.md +319 -0
- package/assets/skills/react-components.md +201 -0
- package/assets/skills/react-native-patterns.md +299 -0
- package/assets/skills/real-time-patterns.md +181 -0
- package/assets/skills/redis-patterns.md +188 -0
- package/assets/skills/refactoring.md +218 -0
- package/assets/skills/regex-patterns.md +191 -0
- package/assets/skills/remix-patterns.md +262 -0
- package/assets/skills/responsive-design.md +199 -0
- package/assets/skills/ruby-rails-patterns.md +178 -0
- package/assets/skills/rust-patterns.md +211 -0
- package/assets/skills/search-patterns.md +227 -0
- package/assets/skills/security-hardening.md +237 -0
- package/assets/skills/seo-patterns.md +179 -0
- package/assets/skills/serverless-patterns.md +223 -0
- package/assets/skills/sql-optimization.md +154 -0
- package/assets/skills/state-management.md +254 -0
- package/assets/skills/storybook-patterns.md +330 -0
- package/assets/skills/svelte-patterns.md +258 -0
- package/assets/skills/swift-patterns.md +227 -0
- package/assets/skills/tailwind-patterns.md +272 -0
- package/assets/skills/tdd-workflow.md +199 -0
- package/assets/skills/terraform-patterns.md +270 -0
- package/assets/skills/testing-react.md +240 -0
- package/assets/skills/testing-vitest.md +232 -0
- package/assets/skills/typescript-strict.md +159 -0
- package/assets/skills/video-processing.md +340 -0
- package/assets/skills/vue-patterns.md +247 -0
- package/assets/skills/web-workers.md +327 -0
- package/assets/skills/webhooks-patterns.md +283 -0
- package/assets/skills/websocket-patterns.md +306 -0
- package/dist/cli/index.js +941 -958
- package/dist/core/index.d.ts +341 -11
- package/dist/core.js +58 -95
- package/dist/mcp/index.d.ts +33 -1
- package/dist/mcp-server.js +177 -255
- package/package.json +4 -1
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: astro-patterns
|
|
3
|
+
description: Astro patterns for islands architecture, content collections, SSG/SSR modes, integrations, and view transitions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Astro Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Use Astro for content-heavy websites, documentation sites, blogs, marketing pages, and any project where most pages are static with selective interactivity. Astro's islands architecture ships zero JavaScript by default, hydrating only the interactive components you specify. Choose Astro when page load performance and SEO are top priorities.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Project Structure and Configuration
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// astro.config.mjs
|
|
17
|
+
import { defineConfig } from 'astro/config';
|
|
18
|
+
import react from '@astrojs/react';
|
|
19
|
+
import tailwind from '@astrojs/tailwind';
|
|
20
|
+
import mdx from '@astrojs/mdx';
|
|
21
|
+
import sitemap from '@astrojs/sitemap';
|
|
22
|
+
import vercel from '@astrojs/vercel';
|
|
23
|
+
|
|
24
|
+
export default defineConfig({
|
|
25
|
+
site: 'https://example.com',
|
|
26
|
+
output: 'hybrid', // static by default, opt-in to SSR per page
|
|
27
|
+
adapter: vercel(),
|
|
28
|
+
integrations: [
|
|
29
|
+
react(),
|
|
30
|
+
tailwind({ applyBaseStyles: false }),
|
|
31
|
+
mdx(),
|
|
32
|
+
sitemap({ filter: (page) => !page.includes('/admin/') }),
|
|
33
|
+
],
|
|
34
|
+
image: { service: { entrypoint: 'astro/assets/services/sharp' } },
|
|
35
|
+
vite: { build: { assetsInlineLimit: 4096 } },
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Islands Architecture (Partial Hydration)
|
|
40
|
+
|
|
41
|
+
```astro
|
|
42
|
+
---
|
|
43
|
+
// src/pages/index.astro
|
|
44
|
+
import Layout from '../layouts/Base.astro';
|
|
45
|
+
import Hero from '../components/Hero.astro'; // Static — no JS shipped
|
|
46
|
+
import SearchBar from '../components/SearchBar'; // React — hydrated on load
|
|
47
|
+
import Newsletter from '../components/Newsletter'; // React — hydrated on visible
|
|
48
|
+
import Analytics from '../components/Analytics'; // React — hydrated on idle
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
<Layout title="Home">
|
|
52
|
+
<Hero headline="Welcome" subline="Fast by default" />
|
|
53
|
+
|
|
54
|
+
<!-- Hydrate immediately — critical interactive component -->
|
|
55
|
+
<SearchBar client:load placeholder="Search docs..." />
|
|
56
|
+
|
|
57
|
+
<!-- Hydrate when scrolled into view — below the fold -->
|
|
58
|
+
<Newsletter client:visible />
|
|
59
|
+
|
|
60
|
+
<!-- Hydrate when browser is idle — non-critical -->
|
|
61
|
+
<Analytics client:idle />
|
|
62
|
+
|
|
63
|
+
<!-- Hydrate only on specific media query -->
|
|
64
|
+
<MobileMenu client:media="(max-width: 768px)" />
|
|
65
|
+
</Layout>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Content Collections
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// src/content/config.ts
|
|
72
|
+
import { defineCollection, z, reference } from 'astro:content';
|
|
73
|
+
|
|
74
|
+
const blog = defineCollection({
|
|
75
|
+
type: 'content',
|
|
76
|
+
schema: ({ image }) => z.object({
|
|
77
|
+
title: z.string().max(100),
|
|
78
|
+
description: z.string().max(200),
|
|
79
|
+
pubDate: z.coerce.date(),
|
|
80
|
+
updatedDate: z.coerce.date().optional(),
|
|
81
|
+
author: reference('authors'),
|
|
82
|
+
tags: z.array(z.string()).default([]),
|
|
83
|
+
cover: image().optional(),
|
|
84
|
+
draft: z.boolean().default(false),
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const authors = defineCollection({
|
|
89
|
+
type: 'data',
|
|
90
|
+
schema: z.object({
|
|
91
|
+
name: z.string(),
|
|
92
|
+
avatar: z.string().url(),
|
|
93
|
+
bio: z.string(),
|
|
94
|
+
social: z.object({
|
|
95
|
+
twitter: z.string().optional(),
|
|
96
|
+
github: z.string().optional(),
|
|
97
|
+
}),
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
export const collections = { blog, authors };
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```astro
|
|
105
|
+
---
|
|
106
|
+
// src/pages/blog/[...slug].astro
|
|
107
|
+
import { getCollection } from 'astro:content';
|
|
108
|
+
import BlogPost from '../../layouts/BlogPost.astro';
|
|
109
|
+
|
|
110
|
+
export async function getStaticPaths() {
|
|
111
|
+
const posts = await getCollection('blog', ({ data }) => !data.draft);
|
|
112
|
+
return posts.map((post) => ({
|
|
113
|
+
params: { slug: post.slug },
|
|
114
|
+
props: { post },
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { post } = Astro.props;
|
|
119
|
+
const { Content, headings } = await post.render();
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
<BlogPost frontmatter={post.data} headings={headings}>
|
|
123
|
+
<Content />
|
|
124
|
+
</BlogPost>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### SSR with Server Endpoints
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// src/pages/api/search.ts
|
|
131
|
+
import type { APIRoute } from 'astro';
|
|
132
|
+
import { getCollection } from 'astro:content';
|
|
133
|
+
|
|
134
|
+
export const prerender = false; // opt this route into SSR
|
|
135
|
+
|
|
136
|
+
export const GET: APIRoute = async ({ url }) => {
|
|
137
|
+
const query = url.searchParams.get('q')?.toLowerCase() ?? '';
|
|
138
|
+
if (query.length < 2) {
|
|
139
|
+
return new Response(JSON.stringify({ results: [] }), {
|
|
140
|
+
headers: { 'Content-Type': 'application/json' },
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const posts = await getCollection('blog');
|
|
145
|
+
const results = posts
|
|
146
|
+
.filter((p) =>
|
|
147
|
+
p.data.title.toLowerCase().includes(query) ||
|
|
148
|
+
p.data.description.toLowerCase().includes(query)
|
|
149
|
+
)
|
|
150
|
+
.slice(0, 10)
|
|
151
|
+
.map((p) => ({ title: p.data.title, slug: p.slug, description: p.data.description }));
|
|
152
|
+
|
|
153
|
+
return new Response(JSON.stringify({ results }), {
|
|
154
|
+
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=60' },
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### View Transitions
|
|
160
|
+
|
|
161
|
+
```astro
|
|
162
|
+
---
|
|
163
|
+
// src/layouts/Base.astro
|
|
164
|
+
import { ViewTransitions } from 'astro:transitions';
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
<html lang="en">
|
|
168
|
+
<head>
|
|
169
|
+
<meta charset="utf-8" />
|
|
170
|
+
<ViewTransitions />
|
|
171
|
+
</head>
|
|
172
|
+
<body>
|
|
173
|
+
<header transition:persist>
|
|
174
|
+
<nav><!-- persists across page navigations --></nav>
|
|
175
|
+
</header>
|
|
176
|
+
|
|
177
|
+
<main transition:animate="slide">
|
|
178
|
+
<slot />
|
|
179
|
+
</main>
|
|
180
|
+
</body>
|
|
181
|
+
</html>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Custom Integration
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
// integrations/reading-time.ts
|
|
188
|
+
import type { AstroIntegration } from 'astro';
|
|
189
|
+
import { toString } from 'mdast-util-to-string';
|
|
190
|
+
import getReadingTime from 'reading-time';
|
|
191
|
+
|
|
192
|
+
export function readingTime(): AstroIntegration {
|
|
193
|
+
return {
|
|
194
|
+
name: 'reading-time',
|
|
195
|
+
hooks: {
|
|
196
|
+
'astro:config:setup': ({ updateConfig }) => {
|
|
197
|
+
updateConfig({
|
|
198
|
+
markdown: {
|
|
199
|
+
remarkPlugins: [
|
|
200
|
+
() => (tree: any, file: any) => {
|
|
201
|
+
const text = toString(tree);
|
|
202
|
+
const { minutes } = getReadingTime(text);
|
|
203
|
+
file.data.astro.frontmatter.readingTime = Math.ceil(minutes);
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Examples
|
|
215
|
+
|
|
216
|
+
| Directive | When It Hydrates | Use Case |
|
|
217
|
+
|-----------|-----------------|----------|
|
|
218
|
+
| `client:load` | Immediately on page load | Search bar, auth widget |
|
|
219
|
+
| `client:idle` | After page is idle | Analytics, chat widget |
|
|
220
|
+
| `client:visible` | When scrolled into viewport | Below-fold interactive content |
|
|
221
|
+
| `client:media="(max-width: 768px)"` | On matching media query | Mobile-only components |
|
|
222
|
+
| `client:only="react"` | Client-only, never SSR | Components using browser APIs |
|
|
223
|
+
| No directive | Never (static HTML) | Headers, footers, content |
|
|
224
|
+
|
|
225
|
+
## Checklist
|
|
226
|
+
- [ ] Static components have no `client:*` directive (zero JS by default)
|
|
227
|
+
- [ ] Interactive islands use the least aggressive hydration directive needed
|
|
228
|
+
- [ ] Content collections have Zod schemas with validation in `config.ts`
|
|
229
|
+
- [ ] `getStaticPaths` filters out draft content in production
|
|
230
|
+
- [ ] SSR routes explicitly set `export const prerender = false`
|
|
231
|
+
- [ ] View transitions enabled with `transition:persist` on shared elements
|
|
232
|
+
- [ ] Images use `astro:assets` for automatic optimization
|
|
233
|
+
- [ ] Sitemap integration configured to exclude private routes
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: auth-patterns
|
|
3
|
+
description: Implement authentication and authorization — JWT, refresh tokens, RBAC, OAuth2 PKCE, and API keys.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Auth Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply these patterns whenever your application has users, API consumers, or any
|
|
11
|
+
protected resource. Authentication (who are you?) and authorization (what can
|
|
12
|
+
you do?) are distinct concerns — implement both. Getting auth wrong means data
|
|
13
|
+
breaches, account takeovers, and compliance failures.
|
|
14
|
+
|
|
15
|
+
## How It Works
|
|
16
|
+
|
|
17
|
+
### 1. JWT Access Tokens
|
|
18
|
+
|
|
19
|
+
Short-lived tokens (15 minutes) for API authentication. Stateless verification
|
|
20
|
+
with no database lookup per request.
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import jwt from 'jsonwebtoken';
|
|
24
|
+
|
|
25
|
+
interface TokenPayload {
|
|
26
|
+
sub: string; // user ID
|
|
27
|
+
role: string;
|
|
28
|
+
permissions: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function signAccessToken(user: User): string {
|
|
32
|
+
const payload: TokenPayload = {
|
|
33
|
+
sub: user.id,
|
|
34
|
+
role: user.role,
|
|
35
|
+
permissions: user.permissions,
|
|
36
|
+
};
|
|
37
|
+
return jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: '15m' });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function verifyAccessToken(token: string): TokenPayload {
|
|
41
|
+
return jwt.verify(token, process.env.JWT_SECRET!) as TokenPayload;
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Never put sensitive data (email, password hash, PII) in the JWT payload — it's
|
|
46
|
+
base64 encoded, not encrypted.
|
|
47
|
+
|
|
48
|
+
### 2. Refresh Tokens
|
|
49
|
+
|
|
50
|
+
Long-lived (7-30 days), stored in the database, rotated on each use.
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
async function refreshTokens(refreshToken: string) {
|
|
54
|
+
const stored = await db.refreshToken.findUnique({
|
|
55
|
+
where: { token: hashToken(refreshToken) },
|
|
56
|
+
include: { user: true },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!stored || stored.expiresAt < new Date()) {
|
|
60
|
+
throw new AuthError('Invalid refresh token');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Rotate: delete old, create new
|
|
64
|
+
await db.refreshToken.delete({ where: { id: stored.id } });
|
|
65
|
+
|
|
66
|
+
const newRefreshToken = generateSecureToken();
|
|
67
|
+
await db.refreshToken.create({
|
|
68
|
+
data: {
|
|
69
|
+
token: hashToken(newRefreshToken),
|
|
70
|
+
userId: stored.userId,
|
|
71
|
+
expiresAt: addDays(new Date(), 7),
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const accessToken = signAccessToken(stored.user);
|
|
76
|
+
return { accessToken, refreshToken: newRefreshToken };
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Store refresh tokens as hashed values. If the database leaks, raw tokens remain
|
|
81
|
+
unknown.
|
|
82
|
+
|
|
83
|
+
### 3. RBAC — Role-Based Access Control
|
|
84
|
+
|
|
85
|
+
Define roles and permissions, check at the middleware level.
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
const ROLE_PERMISSIONS: Record<string, string[]> = {
|
|
89
|
+
admin: ['users:read', 'users:write', 'billing:manage', 'settings:write'],
|
|
90
|
+
editor: ['users:read', 'content:write', 'content:publish'],
|
|
91
|
+
viewer: ['users:read', 'content:read'],
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
function requirePermission(...required: string[]) {
|
|
95
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
96
|
+
const userPermissions = req.user.permissions;
|
|
97
|
+
const hasAll = required.every((p) => userPermissions.includes(p));
|
|
98
|
+
|
|
99
|
+
if (!hasAll) {
|
|
100
|
+
return res.status(403).json({
|
|
101
|
+
error: { code: 'FORBIDDEN', message: 'Insufficient permissions' },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
next();
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Usage
|
|
109
|
+
router.delete('/users/:id', requirePermission('users:write'), deleteUser);
|
|
110
|
+
router.get('/users', requirePermission('users:read'), listUsers);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 4. Session Management
|
|
114
|
+
|
|
115
|
+
For server-rendered apps, use secure HTTP-only cookies.
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
app.use(session({
|
|
119
|
+
store: new RedisStore({ client: redisClient }),
|
|
120
|
+
secret: process.env.SESSION_SECRET!,
|
|
121
|
+
resave: false,
|
|
122
|
+
saveUninitialized: false,
|
|
123
|
+
cookie: {
|
|
124
|
+
httpOnly: true, // JavaScript can't read it
|
|
125
|
+
secure: true, // HTTPS only
|
|
126
|
+
sameSite: 'lax', // CSRF protection
|
|
127
|
+
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
|
128
|
+
},
|
|
129
|
+
}));
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 5. OAuth2 PKCE Flow
|
|
133
|
+
|
|
134
|
+
For SPAs and mobile apps that can't securely store client secrets.
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// Step 1: Generate code verifier and challenge
|
|
138
|
+
function generatePKCE() {
|
|
139
|
+
const verifier = crypto.randomBytes(32).toString('base64url');
|
|
140
|
+
const challenge = crypto
|
|
141
|
+
.createHash('sha256')
|
|
142
|
+
.update(verifier)
|
|
143
|
+
.digest('base64url');
|
|
144
|
+
return { verifier, challenge };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Step 2: Redirect to authorization server
|
|
148
|
+
const { verifier, challenge } = generatePKCE();
|
|
149
|
+
sessionStorage.setItem('pkce_verifier', verifier);
|
|
150
|
+
|
|
151
|
+
const authUrl = new URL('https://provider.com/authorize');
|
|
152
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
153
|
+
authUrl.searchParams.set('client_id', CLIENT_ID);
|
|
154
|
+
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
|
|
155
|
+
authUrl.searchParams.set('code_challenge', challenge);
|
|
156
|
+
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
157
|
+
authUrl.searchParams.set('scope', 'openid profile email');
|
|
158
|
+
|
|
159
|
+
// Step 3: Exchange code for tokens (callback handler)
|
|
160
|
+
async function handleCallback(code: string) {
|
|
161
|
+
const verifier = sessionStorage.getItem('pkce_verifier');
|
|
162
|
+
const response = await fetch('https://provider.com/token', {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
165
|
+
body: new URLSearchParams({
|
|
166
|
+
grant_type: 'authorization_code',
|
|
167
|
+
code,
|
|
168
|
+
redirect_uri: REDIRECT_URI,
|
|
169
|
+
client_id: CLIENT_ID,
|
|
170
|
+
code_verifier: verifier!,
|
|
171
|
+
}),
|
|
172
|
+
});
|
|
173
|
+
return response.json(); // { access_token, refresh_token, id_token }
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 6. API Keys
|
|
178
|
+
|
|
179
|
+
For server-to-server authentication. Simpler than OAuth, appropriate when the
|
|
180
|
+
caller is a trusted backend.
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// Generation
|
|
184
|
+
function generateApiKey(): { key: string; hash: string } {
|
|
185
|
+
const prefix = 'bs_live_';
|
|
186
|
+
const secret = crypto.randomBytes(24).toString('base64url');
|
|
187
|
+
const key = `${prefix}${secret}`;
|
|
188
|
+
const hash = crypto.createHash('sha256').update(key).digest('hex');
|
|
189
|
+
return { key, hash }; // show key once, store hash
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Middleware
|
|
193
|
+
function authenticateApiKey(req: Request, res: Response, next: NextFunction) {
|
|
194
|
+
const key = req.headers['x-api-key'] as string;
|
|
195
|
+
if (!key) return res.status(401).json({ error: 'API key required' });
|
|
196
|
+
|
|
197
|
+
const hash = crypto.createHash('sha256').update(key).digest('hex');
|
|
198
|
+
const record = await db.apiKey.findUnique({ where: { hash } });
|
|
199
|
+
|
|
200
|
+
if (!record || record.revokedAt) {
|
|
201
|
+
return res.status(401).json({ error: 'Invalid API key' });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
req.apiKeyOwner = record.userId;
|
|
205
|
+
next();
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### 7. Authentication Middleware
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
function authenticate(req: Request, res: Response, next: NextFunction) {
|
|
213
|
+
const header = req.headers.authorization;
|
|
214
|
+
if (!header?.startsWith('Bearer ')) {
|
|
215
|
+
return res.status(401).json({ error: { code: 'UNAUTHENTICATED' } });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const token = header.slice(7);
|
|
220
|
+
req.user = verifyAccessToken(token);
|
|
221
|
+
next();
|
|
222
|
+
} catch {
|
|
223
|
+
return res.status(401).json({ error: { code: 'TOKEN_EXPIRED' } });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Examples
|
|
229
|
+
|
|
230
|
+
| Scenario | Pattern | Token lifetime |
|
|
231
|
+
|----------|---------|---------------|
|
|
232
|
+
| SPA calling own API | JWT access + refresh | 15min / 7 days |
|
|
233
|
+
| SSR web app | HTTP-only session cookie | 24 hours |
|
|
234
|
+
| Mobile app, third-party login | OAuth2 PKCE | Provider-dependent |
|
|
235
|
+
| Backend-to-backend | API key | Until revoked |
|
|
236
|
+
| Microservice mesh | mTLS or JWT with service accounts | 1 hour |
|
|
237
|
+
|
|
238
|
+
## Checklist
|
|
239
|
+
|
|
240
|
+
- [ ] Access tokens expire in 15 minutes or less
|
|
241
|
+
- [ ] Refresh tokens are hashed in the database and rotated on use
|
|
242
|
+
- [ ] Passwords are hashed with bcrypt/argon2 (cost factor >= 10)
|
|
243
|
+
- [ ] RBAC permissions are checked in middleware, not scattered in handlers
|
|
244
|
+
- [ ] Cookies use `httpOnly`, `secure`, and `sameSite` flags
|
|
245
|
+
- [ ] OAuth2 flows use PKCE, never implicit grant
|
|
246
|
+
- [ ] API keys are prefixed (`bs_live_`, `bs_test_`) for identification
|
|
247
|
+
- [ ] Failed login attempts are rate-limited (5 per minute per IP)
|
|
248
|
+
- [ ] Token secrets and session secrets come from environment variables, never source code
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: aws-patterns
|
|
3
|
+
description: AWS patterns for S3, Lambda, SQS, DynamoDB, CloudFront, and IAM best practices.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# AWS Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Apply when architecting or operating workloads on AWS. Covers the most common building blocks: S3 for storage, Lambda for compute, SQS for decoupling, DynamoDB for NoSQL, CloudFront for CDN, and IAM for least-privilege access. Use this skill when designing new services, reviewing infrastructure, or optimizing AWS costs.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### S3 -- Object Storage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
|
|
17
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
18
|
+
|
|
19
|
+
const s3 = new S3Client({ region: "us-east-1" });
|
|
20
|
+
|
|
21
|
+
// Upload with server-side encryption
|
|
22
|
+
await s3.send(new PutObjectCommand({
|
|
23
|
+
Bucket: "my-app-uploads",
|
|
24
|
+
Key: `users/${userId}/avatar.png`,
|
|
25
|
+
Body: fileBuffer,
|
|
26
|
+
ContentType: "image/png",
|
|
27
|
+
ServerSideEncryption: "AES256",
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
// Presigned URL for client-side upload (bypass your server)
|
|
31
|
+
const presignedUrl = await getSignedUrl(s3, new PutObjectCommand({
|
|
32
|
+
Bucket: "my-app-uploads",
|
|
33
|
+
Key: `users/${userId}/avatar.png`,
|
|
34
|
+
ContentType: "image/png",
|
|
35
|
+
}), { expiresIn: 300 });
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Rules: enable versioning, lifecycle policies to Glacier after 90 days, block all public access, use presigned URLs.
|
|
39
|
+
|
|
40
|
+
### Lambda -- Stateless Functions
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { SQSEvent, Context } from "aws-lambda";
|
|
44
|
+
|
|
45
|
+
export const handler = async (event: SQSEvent, context: Context) => {
|
|
46
|
+
const failures: { itemIdentifier: string }[] = [];
|
|
47
|
+
|
|
48
|
+
for (const record of event.Records) {
|
|
49
|
+
try {
|
|
50
|
+
const order = JSON.parse(record.body);
|
|
51
|
+
await processOrder(order);
|
|
52
|
+
} catch {
|
|
53
|
+
failures.push({ itemIdentifier: record.messageId });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { batchItemFailures: failures };
|
|
57
|
+
};
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Best practices: keep handler thin, set `reservedConcurrency`, use ARM64 (Graviton) for 20% savings, set timeout to 2x p99.
|
|
61
|
+
|
|
62
|
+
### SQS -- Decouple Services
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
|
|
66
|
+
|
|
67
|
+
const sqs = new SQSClient({ region: "us-east-1" });
|
|
68
|
+
|
|
69
|
+
await sqs.send(new SendMessageCommand({
|
|
70
|
+
QueueUrl: process.env.ORDER_QUEUE_URL,
|
|
71
|
+
MessageBody: JSON.stringify({ orderId, action: "fulfill" }),
|
|
72
|
+
MessageGroupId: orderId,
|
|
73
|
+
MessageDeduplicationId: `${orderId}-fulfill`,
|
|
74
|
+
}));
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Use Standard queues for throughput, FIFO for ordering. Always configure a Dead Letter Queue with `maxReceiveCount: 3`.
|
|
78
|
+
|
|
79
|
+
### DynamoDB -- Single-Table Design
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// PK = entity type + ID, SK = relationship
|
|
83
|
+
await dynamo.put({
|
|
84
|
+
TableName: "AppTable",
|
|
85
|
+
Item: {
|
|
86
|
+
PK: `USER#${userId}`,
|
|
87
|
+
SK: `ORDER#${orderId}`,
|
|
88
|
+
GSI1PK: `ORDER#${orderId}`,
|
|
89
|
+
GSI1SK: `USER#${userId}`,
|
|
90
|
+
type: "order",
|
|
91
|
+
total: 4599,
|
|
92
|
+
status: "pending",
|
|
93
|
+
createdAt: new Date().toISOString(),
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Query all orders for a user
|
|
98
|
+
const orders = await dynamo.query({
|
|
99
|
+
TableName: "AppTable",
|
|
100
|
+
KeyConditionExpression: "PK = :pk AND begins_with(SK, :sk)",
|
|
101
|
+
ExpressionAttributeValues: { ":pk": `USER#${userId}`, ":sk": "ORDER#" },
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Design access patterns first, then the table. Use GSIs for inverse lookups. Avoid scans.
|
|
106
|
+
|
|
107
|
+
### CloudFront -- CDN and Edge
|
|
108
|
+
|
|
109
|
+
```yaml
|
|
110
|
+
Origins:
|
|
111
|
+
- Id: s3-static
|
|
112
|
+
DomainName: my-app-static.s3.amazonaws.com
|
|
113
|
+
S3OriginConfig:
|
|
114
|
+
OriginAccessIdentity: origin-access-identity/cloudfront/EXXXXX
|
|
115
|
+
- Id: api-origin
|
|
116
|
+
DomainName: api.my-app.com
|
|
117
|
+
CustomOriginConfig:
|
|
118
|
+
OriginProtocolPolicy: https-only
|
|
119
|
+
CacheBehaviors:
|
|
120
|
+
- PathPattern: /api/*
|
|
121
|
+
TargetOriginId: api-origin
|
|
122
|
+
CachePolicyId: 4135ea2d-... # CachingDisabled
|
|
123
|
+
- PathPattern: /*
|
|
124
|
+
TargetOriginId: s3-static
|
|
125
|
+
CachePolicyId: 658327ea-... # CachingOptimized
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### IAM -- Least Privilege
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"Version": "2012-10-17",
|
|
133
|
+
"Statement": [{
|
|
134
|
+
"Effect": "Allow",
|
|
135
|
+
"Action": ["s3:GetObject", "s3:PutObject"],
|
|
136
|
+
"Resource": "arn:aws:s3:::my-app-uploads/users/${aws:PrincipalTag/userId}/*"
|
|
137
|
+
}]
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Never use `*` for actions or resources in production. Use IAM Roles for Lambda/EC2/ECS, not access keys.
|
|
142
|
+
|
|
143
|
+
### Cost Optimization
|
|
144
|
+
|
|
145
|
+
| Technique | Savings | Effort |
|
|
146
|
+
|-----------|---------|--------|
|
|
147
|
+
| Graviton (ARM) Lambda/EC2 | 20-40% | Low |
|
|
148
|
+
| S3 Intelligent-Tiering | 30-70% cold data | Low |
|
|
149
|
+
| Reserved Instances / Savings Plans | 30-60% steady-state | Medium |
|
|
150
|
+
| Right-size Lambda memory | 10-50% | Low |
|
|
151
|
+
| Spot Instances for batch | 60-90% | Medium |
|
|
152
|
+
|
|
153
|
+
## Examples
|
|
154
|
+
|
|
155
|
+
| Service | Pattern | Key Consideration |
|
|
156
|
+
|---------|---------|-------------------|
|
|
157
|
+
| S3 | Presigned uploads | Never make buckets public |
|
|
158
|
+
| Lambda | SQS trigger + partial batch | Return batchItemFailures |
|
|
159
|
+
| SQS | FIFO + DLQ | Monitor oldest message age |
|
|
160
|
+
| DynamoDB | Single-table design | Access patterns first |
|
|
161
|
+
| CloudFront | Static + API origins | Disable cache for API |
|
|
162
|
+
|
|
163
|
+
## Checklist
|
|
164
|
+
- [ ] S3 buckets have versioning, encryption, public access blocked
|
|
165
|
+
- [ ] Lambda functions have reserved concurrency and DLQ
|
|
166
|
+
- [ ] SQS queues have DLQs with `maxReceiveCount` set
|
|
167
|
+
- [ ] DynamoDB access patterns documented before table design
|
|
168
|
+
- [ ] CloudFront serves static assets; API bypasses cache
|
|
169
|
+
- [ ] IAM policies follow least privilege -- no `*` actions
|
|
170
|
+
- [ ] CloudWatch alarms on Lambda errors, SQS age, DynamoDB throttles
|
|
171
|
+
- [ ] Infrastructure defined in IaC (CDK, Terraform, or SAM)
|