@stacksjs/ts-cloud 0.1.8 → 0.1.9

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 (75) hide show
  1. package/dist/bin/cli.js +1 -1
  2. package/package.json +18 -16
  3. package/src/aws/acm.ts +768 -0
  4. package/src/aws/application-autoscaling.ts +845 -0
  5. package/src/aws/bedrock.ts +4074 -0
  6. package/src/aws/client.ts +891 -0
  7. package/src/aws/cloudformation.ts +896 -0
  8. package/src/aws/cloudfront.ts +1531 -0
  9. package/src/aws/cloudwatch-logs.ts +154 -0
  10. package/src/aws/comprehend.ts +839 -0
  11. package/src/aws/connect.ts +1056 -0
  12. package/src/aws/deploy-imap.ts +384 -0
  13. package/src/aws/dynamodb.ts +340 -0
  14. package/src/aws/ec2.ts +1385 -0
  15. package/src/aws/ecr.ts +621 -0
  16. package/src/aws/ecs.ts +615 -0
  17. package/src/aws/elasticache.ts +301 -0
  18. package/src/aws/elbv2.ts +942 -0
  19. package/src/aws/email.ts +928 -0
  20. package/src/aws/eventbridge.ts +248 -0
  21. package/src/aws/iam.ts +1689 -0
  22. package/src/aws/imap-server.ts +2100 -0
  23. package/src/aws/index.ts +213 -0
  24. package/src/aws/kendra.ts +1097 -0
  25. package/src/aws/lambda.ts +786 -0
  26. package/src/aws/opensearch.ts +158 -0
  27. package/src/aws/personalize.ts +977 -0
  28. package/src/aws/polly.ts +559 -0
  29. package/src/aws/rds.ts +888 -0
  30. package/src/aws/rekognition.ts +846 -0
  31. package/src/aws/route53-domains.ts +359 -0
  32. package/src/aws/route53.ts +1046 -0
  33. package/src/aws/s3.ts +2334 -0
  34. package/src/aws/scheduler.ts +571 -0
  35. package/src/aws/secrets-manager.ts +769 -0
  36. package/src/aws/ses.ts +1081 -0
  37. package/src/aws/setup-phone.ts +104 -0
  38. package/src/aws/setup-sms.ts +580 -0
  39. package/src/aws/sms.ts +1735 -0
  40. package/src/aws/smtp-server.ts +531 -0
  41. package/src/aws/sns.ts +758 -0
  42. package/src/aws/sqs.ts +382 -0
  43. package/src/aws/ssm.ts +807 -0
  44. package/src/aws/sts.ts +92 -0
  45. package/src/aws/support.ts +391 -0
  46. package/src/aws/test-imap.ts +86 -0
  47. package/src/aws/textract.ts +780 -0
  48. package/src/aws/transcribe.ts +108 -0
  49. package/src/aws/translate.ts +641 -0
  50. package/src/aws/voice.ts +1379 -0
  51. package/src/config.ts +35 -0
  52. package/src/deploy/index.ts +7 -0
  53. package/src/deploy/static-site-external-dns.ts +945 -0
  54. package/src/deploy/static-site.ts +1175 -0
  55. package/src/dns/cloudflare.ts +548 -0
  56. package/src/dns/godaddy.ts +412 -0
  57. package/src/dns/index.ts +205 -0
  58. package/src/dns/porkbun.ts +362 -0
  59. package/src/dns/route53-adapter.ts +414 -0
  60. package/src/dns/types.ts +119 -0
  61. package/src/dns/validator.ts +369 -0
  62. package/src/generators/index.ts +5 -0
  63. package/src/generators/infrastructure.ts +1660 -0
  64. package/src/index.ts +163 -0
  65. package/src/push/apns.ts +452 -0
  66. package/src/push/fcm.ts +506 -0
  67. package/src/push/index.ts +58 -0
  68. package/src/security/pre-deploy-scanner.ts +655 -0
  69. package/src/ssl/acme-client.ts +478 -0
  70. package/src/ssl/index.ts +7 -0
  71. package/src/ssl/letsencrypt.ts +747 -0
  72. package/src/types.ts +2 -0
  73. package/src/utils/cli.ts +398 -0
  74. package/src/validation/index.ts +5 -0
  75. package/src/validation/template.ts +405 -0
@@ -0,0 +1,478 @@
1
+ /**
2
+ * ACME Client for Let's Encrypt
3
+ * Implements RFC 8555 (ACME Protocol) for certificate issuance
4
+ *
5
+ * This is a pure TypeScript/Bun implementation without external dependencies.
6
+ */
7
+
8
+ import { createHash, createSign, generateKeyPairSync, randomBytes } from 'node:crypto'
9
+
10
+ // ACME Directory URLs
11
+ export const ACME_DIRECTORIES = {
12
+ production: 'https://acme-v02.api.letsencrypt.org/directory',
13
+ staging: 'https://acme-staging-v02.api.letsencrypt.org/directory',
14
+ } as const
15
+
16
+ export interface AcmeClientOptions {
17
+ /**
18
+ * Use staging server for testing
19
+ * @default false
20
+ */
21
+ staging?: boolean
22
+
23
+ /**
24
+ * Account email for Let's Encrypt notifications
25
+ */
26
+ email: string
27
+
28
+ /**
29
+ * Account key in PEM format (optional, will be generated if not provided)
30
+ */
31
+ accountKey?: string
32
+ }
33
+
34
+ export interface AcmeChallenge {
35
+ type: 'http-01' | 'dns-01'
36
+ token: string
37
+ keyAuthorization: string
38
+ /**
39
+ * For HTTP-01: URL path to serve the challenge
40
+ * For DNS-01: TXT record name
41
+ */
42
+ identifier: string
43
+ /**
44
+ * For DNS-01: The value to put in the TXT record
45
+ */
46
+ dnsValue?: string
47
+ }
48
+
49
+ export interface AcmeCertificate {
50
+ certificate: string
51
+ privateKey: string
52
+ chain: string
53
+ fullchain: string
54
+ expiresAt: Date
55
+ }
56
+
57
+ interface AcmeDirectory {
58
+ newNonce: string
59
+ newAccount: string
60
+ newOrder: string
61
+ revokeCert: string
62
+ keyChange: string
63
+ }
64
+
65
+ /**
66
+ * ACME Client for Let's Encrypt certificate management
67
+ */
68
+ export class AcmeClient {
69
+ private directoryUrl: string
70
+ private email: string
71
+ private accountKey: string
72
+ private accountUrl: string | null = null
73
+ private directory: AcmeDirectory | null = null
74
+ private nonce: string | null = null
75
+
76
+ constructor(options: AcmeClientOptions) {
77
+ this.directoryUrl = options.staging
78
+ ? ACME_DIRECTORIES.staging
79
+ : ACME_DIRECTORIES.production
80
+ this.email = options.email
81
+ this.accountKey = options.accountKey || this.generateAccountKey()
82
+ }
83
+
84
+ /**
85
+ * Generate a new account key pair
86
+ */
87
+ private generateAccountKey(): string {
88
+ const { privateKey } = generateKeyPairSync('ec', {
89
+ namedCurve: 'P-256',
90
+ })
91
+ return privateKey.export({ type: 'pkcs8', format: 'pem' }) as string
92
+ }
93
+
94
+ /**
95
+ * Get the ACME directory
96
+ */
97
+ private async getDirectory(): Promise<AcmeDirectory> {
98
+ if (this.directory) return this.directory
99
+
100
+ const response = await fetch(this.directoryUrl)
101
+ if (!response.ok) {
102
+ throw new Error(`Failed to fetch ACME directory: ${response.status}`)
103
+ }
104
+
105
+ this.directory = await response.json() as AcmeDirectory
106
+ return this.directory
107
+ }
108
+
109
+ /**
110
+ * Get a fresh nonce for requests
111
+ */
112
+ private async getNonce(): Promise<string> {
113
+ if (this.nonce) {
114
+ const nonce = this.nonce
115
+ this.nonce = null
116
+ return nonce
117
+ }
118
+
119
+ const directory = await this.getDirectory()
120
+ const response = await fetch(directory.newNonce, { method: 'HEAD' })
121
+ const nonce = response.headers.get('replay-nonce')
122
+
123
+ if (!nonce) {
124
+ throw new Error('Failed to get nonce from ACME server')
125
+ }
126
+
127
+ return nonce
128
+ }
129
+
130
+ /**
131
+ * Create JWK from account key
132
+ */
133
+ private getJwk(): Record<string, string> {
134
+ // Parse the EC private key to extract public key components
135
+ const keyLines = this.accountKey.split('\n')
136
+ .filter(line => !line.startsWith('-----'))
137
+ .join('')
138
+
139
+ // For EC P-256, we need to extract x and y coordinates
140
+ // This is a simplified version - in production you'd use a proper ASN.1 parser
141
+ const keyBuffer = Buffer.from(keyLines, 'base64')
142
+
143
+ // EC public key is the last 65 bytes (04 || x || y for uncompressed point)
144
+ // For P-256: 32 bytes for x, 32 bytes for y
145
+ const publicKeyStart = keyBuffer.length - 65
146
+ const x = keyBuffer.subarray(publicKeyStart + 1, publicKeyStart + 33)
147
+ const y = keyBuffer.subarray(publicKeyStart + 33, publicKeyStart + 65)
148
+
149
+ return {
150
+ kty: 'EC',
151
+ crv: 'P-256',
152
+ x: this.base64UrlEncode(x),
153
+ y: this.base64UrlEncode(y),
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Calculate JWK thumbprint
159
+ */
160
+ private getJwkThumbprint(): string {
161
+ const jwk = this.getJwk()
162
+ // Canonical JSON for EC key
163
+ const canonical = JSON.stringify({
164
+ crv: jwk.crv,
165
+ kty: jwk.kty,
166
+ x: jwk.x,
167
+ y: jwk.y,
168
+ })
169
+
170
+ const hash = createHash('sha256').update(canonical).digest()
171
+ return this.base64UrlEncode(hash)
172
+ }
173
+
174
+ /**
175
+ * Base64URL encode
176
+ */
177
+ private base64UrlEncode(data: Buffer | string): string {
178
+ const buffer = typeof data === 'string' ? Buffer.from(data) : data
179
+ return buffer.toString('base64')
180
+ .replace(/\+/g, '-')
181
+ .replace(/\//g, '_')
182
+ .replace(/=/g, '')
183
+ }
184
+
185
+ /**
186
+ * Sign a payload for ACME request
187
+ */
188
+ private async signPayload(url: string, payload: any): Promise<string> {
189
+ const nonce = await this.getNonce()
190
+ const jwk = this.getJwk()
191
+
192
+ const protectedHeader: Record<string, any> = {
193
+ alg: 'ES256',
194
+ nonce,
195
+ url,
196
+ }
197
+
198
+ // Use kid if we have an account URL, otherwise use jwk
199
+ if (this.accountUrl) {
200
+ protectedHeader.kid = this.accountUrl
201
+ } else {
202
+ protectedHeader.jwk = jwk
203
+ }
204
+
205
+ const protectedB64 = this.base64UrlEncode(JSON.stringify(protectedHeader))
206
+ const payloadB64 = payload === ''
207
+ ? ''
208
+ : this.base64UrlEncode(JSON.stringify(payload))
209
+
210
+ const signatureInput = `${protectedB64}.${payloadB64}`
211
+
212
+ const sign = createSign('SHA256')
213
+ sign.update(signatureInput)
214
+ const signature = sign.sign(this.accountKey)
215
+
216
+ // Convert DER signature to raw r||s format for ES256
217
+ const r = signature.subarray(4, 4 + signature[3])
218
+ const sStart = 4 + signature[3] + 2
219
+ const s = signature.subarray(sStart, sStart + signature[sStart - 1])
220
+
221
+ // Pad r and s to 32 bytes
222
+ const rPadded = Buffer.alloc(32)
223
+ const sPadded = Buffer.alloc(32)
224
+ r.copy(rPadded, 32 - r.length)
225
+ s.copy(sPadded, 32 - s.length)
226
+
227
+ const rawSignature = Buffer.concat([rPadded, sPadded])
228
+
229
+ return JSON.stringify({
230
+ protected: protectedB64,
231
+ payload: payloadB64,
232
+ signature: this.base64UrlEncode(rawSignature),
233
+ })
234
+ }
235
+
236
+ /**
237
+ * Make a signed ACME request
238
+ */
239
+ private async acmeRequest(url: string, payload: any): Promise<{ body: any; headers: Headers }> {
240
+ const body = await this.signPayload(url, payload)
241
+
242
+ const response = await fetch(url, {
243
+ method: 'POST',
244
+ headers: {
245
+ 'Content-Type': 'application/jose+json',
246
+ },
247
+ body,
248
+ })
249
+
250
+ // Store the new nonce for the next request
251
+ const newNonce = response.headers.get('replay-nonce')
252
+ if (newNonce) {
253
+ this.nonce = newNonce
254
+ }
255
+
256
+ let responseBody: any
257
+ const contentType = response.headers.get('content-type')
258
+ if (contentType?.includes('application/json') || contentType?.includes('application/problem+json')) {
259
+ responseBody = await response.json()
260
+ } else {
261
+ responseBody = await response.text()
262
+ }
263
+
264
+ if (!response.ok) {
265
+ throw new Error(`ACME request failed: ${JSON.stringify(responseBody)}`)
266
+ }
267
+
268
+ return { body: responseBody, headers: response.headers }
269
+ }
270
+
271
+ /**
272
+ * Register or get existing account
273
+ */
274
+ async registerAccount(): Promise<string> {
275
+ const directory = await this.getDirectory()
276
+
277
+ const { body, headers } = await this.acmeRequest(directory.newAccount, {
278
+ termsOfServiceAgreed: true,
279
+ contact: [`mailto:${this.email}`],
280
+ })
281
+
282
+ const location = headers.get('location')
283
+ if (!location) {
284
+ throw new Error('No account URL returned')
285
+ }
286
+
287
+ this.accountUrl = location
288
+ return location
289
+ }
290
+
291
+ /**
292
+ * Create a new certificate order
293
+ */
294
+ async createOrder(domains: string[]): Promise<{
295
+ orderUrl: string
296
+ authorizations: string[]
297
+ finalize: string
298
+ }> {
299
+ if (!this.accountUrl) {
300
+ await this.registerAccount()
301
+ }
302
+
303
+ const directory = await this.getDirectory()
304
+
305
+ const { body, headers } = await this.acmeRequest(directory.newOrder, {
306
+ identifiers: domains.map(domain => ({
307
+ type: 'dns',
308
+ value: domain,
309
+ })),
310
+ })
311
+
312
+ const orderUrl = headers.get('location')
313
+ if (!orderUrl) {
314
+ throw new Error('No order URL returned')
315
+ }
316
+
317
+ return {
318
+ orderUrl,
319
+ authorizations: body.authorizations,
320
+ finalize: body.finalize,
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Get authorization challenges
326
+ */
327
+ async getAuthorization(authUrl: string): Promise<{
328
+ domain: string
329
+ challenges: AcmeChallenge[]
330
+ }> {
331
+ const { body } = await this.acmeRequest(authUrl, '')
332
+
333
+ const domain = body.identifier.value
334
+ const thumbprint = this.getJwkThumbprint()
335
+
336
+ const challenges: AcmeChallenge[] = body.challenges
337
+ .filter((c: any) => c.type === 'http-01' || c.type === 'dns-01')
338
+ .map((c: any) => {
339
+ const keyAuthorization = `${c.token}.${thumbprint}`
340
+
341
+ if (c.type === 'http-01') {
342
+ return {
343
+ type: 'http-01' as const,
344
+ token: c.token,
345
+ keyAuthorization,
346
+ identifier: `/.well-known/acme-challenge/${c.token}`,
347
+ }
348
+ } else {
349
+ // DNS-01: TXT record value is base64url(sha256(keyAuthorization))
350
+ const dnsValue = this.base64UrlEncode(
351
+ createHash('sha256').update(keyAuthorization).digest()
352
+ )
353
+ return {
354
+ type: 'dns-01' as const,
355
+ token: c.token,
356
+ keyAuthorization,
357
+ identifier: `_acme-challenge.${domain}`,
358
+ dnsValue,
359
+ }
360
+ }
361
+ })
362
+
363
+ return { domain, challenges }
364
+ }
365
+
366
+ /**
367
+ * Respond to a challenge (tell ACME server we're ready)
368
+ */
369
+ async respondToChallenge(challengeUrl: string): Promise<void> {
370
+ await this.acmeRequest(challengeUrl, {})
371
+ }
372
+
373
+ /**
374
+ * Poll for authorization status
375
+ */
376
+ async waitForAuthorization(authUrl: string, maxAttempts = 30): Promise<void> {
377
+ for (let i = 0; i < maxAttempts; i++) {
378
+ const { body } = await this.acmeRequest(authUrl, '')
379
+
380
+ if (body.status === 'valid') {
381
+ return
382
+ }
383
+
384
+ if (body.status === 'invalid') {
385
+ throw new Error(`Authorization failed: ${JSON.stringify(body)}`)
386
+ }
387
+
388
+ await new Promise(resolve => setTimeout(resolve, 2000))
389
+ }
390
+
391
+ throw new Error('Authorization timed out')
392
+ }
393
+
394
+ /**
395
+ * Generate a CSR (Certificate Signing Request)
396
+ */
397
+ private generateCsr(domains: string[]): { csr: string; privateKey: string } {
398
+ // Generate a new key pair for the certificate
399
+ const { privateKey, publicKey } = generateKeyPairSync('rsa', {
400
+ modulusLength: 2048,
401
+ })
402
+
403
+ const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }) as string
404
+
405
+ // For a proper CSR, we'd need to use a library like node-forge
406
+ // This is a simplified placeholder - in production, use proper CSR generation
407
+ // For now, we'll create a minimal CSR structure
408
+
409
+ // The CSR needs proper ASN.1 encoding with the domains in Subject Alternative Names
410
+ // This requires either a native module or a library like node-forge
411
+
412
+ // Placeholder: In a real implementation, generate proper CSR
413
+ const csrPlaceholder = this.createSimpleCsr(domains, privateKey as any)
414
+
415
+ return {
416
+ csr: csrPlaceholder,
417
+ privateKey: privateKeyPem,
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Create a simple CSR (placeholder - would need proper implementation)
423
+ */
424
+ private createSimpleCsr(domains: string[], privateKey: any): string {
425
+ // This is a placeholder. A real implementation would:
426
+ // 1. Create proper ASN.1 structure for CSR
427
+ // 2. Include Subject Alternative Names for all domains
428
+ // 3. Sign with the private key
429
+
430
+ // For production, use a library like @peculiar/x509 or node-forge
431
+ throw new Error(
432
+ 'CSR generation requires additional implementation. ' +
433
+ 'Consider using the shell-based certbot approach instead.'
434
+ )
435
+ }
436
+
437
+ /**
438
+ * Finalize the order and get the certificate
439
+ */
440
+ async finalizeOrder(finalizeUrl: string, domains: string[]): Promise<AcmeCertificate> {
441
+ const { csr, privateKey } = this.generateCsr(domains)
442
+
443
+ const { body } = await this.acmeRequest(finalizeUrl, {
444
+ csr: this.base64UrlEncode(Buffer.from(csr, 'base64')),
445
+ })
446
+
447
+ // Poll for certificate
448
+ let certificateUrl = body.certificate
449
+ if (!certificateUrl) {
450
+ // Need to poll the order
451
+ throw new Error('Certificate not immediately available - polling not implemented')
452
+ }
453
+
454
+ // Download certificate
455
+ const certResponse = await fetch(certificateUrl)
456
+ const fullchain = await certResponse.text()
457
+
458
+ // Split fullchain into certificate and chain
459
+ const certs = fullchain.split(/(?=-----BEGIN CERTIFICATE-----)/g)
460
+ const certificate = certs[0]
461
+ const chain = certs.slice(1).join('')
462
+
463
+ return {
464
+ certificate,
465
+ privateKey,
466
+ chain,
467
+ fullchain,
468
+ expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days
469
+ }
470
+ }
471
+
472
+ /**
473
+ * Get the account key (for storage/reuse)
474
+ */
475
+ getAccountKey(): string {
476
+ return this.accountKey
477
+ }
478
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * SSL/TLS Certificate Management
3
+ * Supports Let's Encrypt (ACME) and AWS ACM
4
+ */
5
+
6
+ export * from './letsencrypt'
7
+ export * from './acme-client'