@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,780 @@
1
+ /**
2
+ * AWS Textract Client
3
+ * Document OCR, form extraction, table extraction
4
+ * No external SDK dependencies - implements AWS Signature V4 directly
5
+ */
6
+
7
+ import { AWSClient } from './client'
8
+
9
+ // ============================================================================
10
+ // Types
11
+ // ============================================================================
12
+
13
+ export interface S3Object {
14
+ Bucket?: string
15
+ Name?: string
16
+ Version?: string
17
+ }
18
+
19
+ export interface Document {
20
+ Bytes?: Uint8Array
21
+ S3Object?: S3Object
22
+ }
23
+
24
+ export interface BoundingBox {
25
+ Width?: number
26
+ Height?: number
27
+ Left?: number
28
+ Top?: number
29
+ }
30
+
31
+ export interface Point {
32
+ X?: number
33
+ Y?: number
34
+ }
35
+
36
+ export interface Geometry {
37
+ BoundingBox?: BoundingBox
38
+ Polygon?: Point[]
39
+ }
40
+
41
+ export interface Relationship {
42
+ Type?: 'VALUE' | 'CHILD' | 'COMPLEX_FEATURES' | 'MERGED_CELL' | 'TITLE' | 'ANSWER' | 'TABLE' | 'TABLE_TITLE' | 'TABLE_FOOTER'
43
+ Ids?: string[]
44
+ }
45
+
46
+ export interface Block {
47
+ BlockType?: 'KEY_VALUE_SET' | 'PAGE' | 'LINE' | 'WORD' | 'TABLE' | 'CELL' | 'SELECTION_ELEMENT' | 'MERGED_CELL' | 'TITLE' | 'QUERY' | 'QUERY_RESULT' | 'SIGNATURE' | 'TABLE_TITLE' | 'TABLE_FOOTER' | 'LAYOUT_TEXT' | 'LAYOUT_TITLE' | 'LAYOUT_HEADER' | 'LAYOUT_FOOTER' | 'LAYOUT_SECTION_HEADER' | 'LAYOUT_PAGE_NUMBER' | 'LAYOUT_LIST' | 'LAYOUT_FIGURE' | 'LAYOUT_TABLE' | 'LAYOUT_KEY_VALUE'
48
+ Confidence?: number
49
+ Text?: string
50
+ TextType?: 'HANDWRITING' | 'PRINTED'
51
+ RowIndex?: number
52
+ ColumnIndex?: number
53
+ RowSpan?: number
54
+ ColumnSpan?: number
55
+ Geometry?: Geometry
56
+ Id?: string
57
+ Relationships?: Relationship[]
58
+ EntityTypes?: ('KEY' | 'VALUE' | 'COLUMN_HEADER' | 'TABLE_TITLE' | 'TABLE_FOOTER' | 'TABLE_SECTION_TITLE' | 'TABLE_SUMMARY' | 'STRUCTURED_TABLE' | 'SEMI_STRUCTURED_TABLE')[]
59
+ SelectionStatus?: 'SELECTED' | 'NOT_SELECTED'
60
+ Page?: number
61
+ Query?: {
62
+ Text: string
63
+ Alias?: string
64
+ Pages?: string[]
65
+ }
66
+ }
67
+
68
+ export interface DocumentMetadata {
69
+ Pages?: number
70
+ }
71
+
72
+ export interface Warning {
73
+ ErrorCode?: string
74
+ Pages?: number[]
75
+ }
76
+
77
+ export interface DetectDocumentTextCommandInput {
78
+ Document: Document
79
+ }
80
+
81
+ export interface DetectDocumentTextCommandOutput {
82
+ DocumentMetadata?: DocumentMetadata
83
+ Blocks?: Block[]
84
+ DetectDocumentTextModelVersion?: string
85
+ }
86
+
87
+ export interface AnalyzeDocumentCommandInput {
88
+ Document: Document
89
+ FeatureTypes: ('TABLES' | 'FORMS' | 'QUERIES' | 'SIGNATURES' | 'LAYOUT')[]
90
+ HumanLoopConfig?: {
91
+ HumanLoopName: string
92
+ FlowDefinitionArn: string
93
+ DataAttributes?: {
94
+ ContentClassifiers?: ('FreeOfPersonallyIdentifiableInformation' | 'FreeOfAdultContent')[]
95
+ }
96
+ }
97
+ QueriesConfig?: {
98
+ Queries: Array<{
99
+ Text: string
100
+ Alias?: string
101
+ Pages?: string[]
102
+ }>
103
+ }
104
+ AdaptersConfig?: {
105
+ Adapters: Array<{
106
+ AdapterId: string
107
+ Pages?: string[]
108
+ Version: string
109
+ }>
110
+ }
111
+ }
112
+
113
+ export interface AnalyzeDocumentCommandOutput {
114
+ DocumentMetadata?: DocumentMetadata
115
+ Blocks?: Block[]
116
+ HumanLoopActivationOutput?: {
117
+ HumanLoopArn?: string
118
+ HumanLoopActivationReasons?: string[]
119
+ HumanLoopActivationConditionsEvaluationResults?: string
120
+ }
121
+ AnalyzeDocumentModelVersion?: string
122
+ }
123
+
124
+ export interface AnalyzeExpenseCommandInput {
125
+ Document: Document
126
+ }
127
+
128
+ export interface ExpenseField {
129
+ Type?: {
130
+ Text?: string
131
+ Confidence?: number
132
+ }
133
+ LabelDetection?: {
134
+ Text?: string
135
+ Geometry?: Geometry
136
+ Confidence?: number
137
+ }
138
+ ValueDetection?: {
139
+ Text?: string
140
+ Geometry?: Geometry
141
+ Confidence?: number
142
+ }
143
+ PageNumber?: number
144
+ Currency?: {
145
+ Code?: string
146
+ Confidence?: number
147
+ }
148
+ GroupProperties?: Array<{
149
+ Types?: string[]
150
+ Id?: string
151
+ }>
152
+ }
153
+
154
+ export interface LineItemGroup {
155
+ LineItemGroupIndex?: number
156
+ LineItems?: Array<{
157
+ LineItemExpenseFields?: ExpenseField[]
158
+ }>
159
+ }
160
+
161
+ export interface ExpenseDocument {
162
+ ExpenseIndex?: number
163
+ SummaryFields?: ExpenseField[]
164
+ LineItemGroups?: LineItemGroup[]
165
+ Blocks?: Block[]
166
+ }
167
+
168
+ export interface AnalyzeExpenseCommandOutput {
169
+ DocumentMetadata?: DocumentMetadata
170
+ ExpenseDocuments?: ExpenseDocument[]
171
+ }
172
+
173
+ export interface AnalyzeIDCommandInput {
174
+ DocumentPages: Document[]
175
+ }
176
+
177
+ export interface IdentityDocument {
178
+ DocumentIndex?: number
179
+ IdentityDocumentFields?: Array<{
180
+ Type?: {
181
+ Text?: string
182
+ Confidence?: number
183
+ }
184
+ ValueDetection?: {
185
+ Text?: string
186
+ NormalizedValue?: {
187
+ Value?: string
188
+ ValueType?: 'DATE'
189
+ }
190
+ Confidence?: number
191
+ }
192
+ }>
193
+ Blocks?: Block[]
194
+ }
195
+
196
+ export interface AnalyzeIDCommandOutput {
197
+ IdentityDocuments?: IdentityDocument[]
198
+ DocumentMetadata?: DocumentMetadata
199
+ AnalyzeIDModelVersion?: string
200
+ }
201
+
202
+ export interface StartDocumentTextDetectionCommandInput {
203
+ DocumentLocation: {
204
+ S3Object?: S3Object
205
+ }
206
+ ClientRequestToken?: string
207
+ JobTag?: string
208
+ NotificationChannel?: {
209
+ SNSTopicArn: string
210
+ RoleArn: string
211
+ }
212
+ OutputConfig?: {
213
+ S3Bucket: string
214
+ S3Prefix?: string
215
+ }
216
+ KMSKeyId?: string
217
+ }
218
+
219
+ export interface StartDocumentTextDetectionCommandOutput {
220
+ JobId?: string
221
+ }
222
+
223
+ export interface GetDocumentTextDetectionCommandInput {
224
+ JobId: string
225
+ MaxResults?: number
226
+ NextToken?: string
227
+ }
228
+
229
+ export interface GetDocumentTextDetectionCommandOutput {
230
+ DocumentMetadata?: DocumentMetadata
231
+ JobStatus?: 'IN_PROGRESS' | 'SUCCEEDED' | 'FAILED' | 'PARTIAL_SUCCESS'
232
+ NextToken?: string
233
+ Blocks?: Block[]
234
+ Warnings?: Warning[]
235
+ StatusMessage?: string
236
+ DetectDocumentTextModelVersion?: string
237
+ }
238
+
239
+ export interface StartDocumentAnalysisCommandInput {
240
+ DocumentLocation: {
241
+ S3Object?: S3Object
242
+ }
243
+ FeatureTypes: ('TABLES' | 'FORMS' | 'QUERIES' | 'SIGNATURES' | 'LAYOUT')[]
244
+ ClientRequestToken?: string
245
+ JobTag?: string
246
+ NotificationChannel?: {
247
+ SNSTopicArn: string
248
+ RoleArn: string
249
+ }
250
+ OutputConfig?: {
251
+ S3Bucket: string
252
+ S3Prefix?: string
253
+ }
254
+ KMSKeyId?: string
255
+ QueriesConfig?: {
256
+ Queries: Array<{
257
+ Text: string
258
+ Alias?: string
259
+ Pages?: string[]
260
+ }>
261
+ }
262
+ AdaptersConfig?: {
263
+ Adapters: Array<{
264
+ AdapterId: string
265
+ Pages?: string[]
266
+ Version: string
267
+ }>
268
+ }
269
+ }
270
+
271
+ export interface StartDocumentAnalysisCommandOutput {
272
+ JobId?: string
273
+ }
274
+
275
+ export interface GetDocumentAnalysisCommandInput {
276
+ JobId: string
277
+ MaxResults?: number
278
+ NextToken?: string
279
+ }
280
+
281
+ export interface GetDocumentAnalysisCommandOutput {
282
+ DocumentMetadata?: DocumentMetadata
283
+ JobStatus?: 'IN_PROGRESS' | 'SUCCEEDED' | 'FAILED' | 'PARTIAL_SUCCESS'
284
+ NextToken?: string
285
+ Blocks?: Block[]
286
+ Warnings?: Warning[]
287
+ StatusMessage?: string
288
+ AnalyzeDocumentModelVersion?: string
289
+ }
290
+
291
+ export interface StartExpenseAnalysisCommandInput {
292
+ DocumentLocation: {
293
+ S3Object?: S3Object
294
+ }
295
+ ClientRequestToken?: string
296
+ JobTag?: string
297
+ NotificationChannel?: {
298
+ SNSTopicArn: string
299
+ RoleArn: string
300
+ }
301
+ OutputConfig?: {
302
+ S3Bucket: string
303
+ S3Prefix?: string
304
+ }
305
+ KMSKeyId?: string
306
+ }
307
+
308
+ export interface StartExpenseAnalysisCommandOutput {
309
+ JobId?: string
310
+ }
311
+
312
+ export interface GetExpenseAnalysisCommandInput {
313
+ JobId: string
314
+ MaxResults?: number
315
+ NextToken?: string
316
+ }
317
+
318
+ export interface GetExpenseAnalysisCommandOutput {
319
+ DocumentMetadata?: DocumentMetadata
320
+ JobStatus?: 'IN_PROGRESS' | 'SUCCEEDED' | 'FAILED' | 'PARTIAL_SUCCESS'
321
+ NextToken?: string
322
+ ExpenseDocuments?: ExpenseDocument[]
323
+ Warnings?: Warning[]
324
+ StatusMessage?: string
325
+ AnalyzeExpenseModelVersion?: string
326
+ }
327
+
328
+ export interface StartLendingAnalysisCommandInput {
329
+ DocumentLocation: {
330
+ S3Object?: S3Object
331
+ }
332
+ ClientRequestToken?: string
333
+ JobTag?: string
334
+ NotificationChannel?: {
335
+ SNSTopicArn: string
336
+ RoleArn: string
337
+ }
338
+ OutputConfig?: {
339
+ S3Bucket: string
340
+ S3Prefix?: string
341
+ }
342
+ KMSKeyId?: string
343
+ }
344
+
345
+ export interface StartLendingAnalysisCommandOutput {
346
+ JobId?: string
347
+ }
348
+
349
+ export interface GetLendingAnalysisCommandInput {
350
+ JobId: string
351
+ MaxResults?: number
352
+ NextToken?: string
353
+ }
354
+
355
+ export interface LendingDocument {
356
+ LendingFields?: Array<{
357
+ Type?: string
358
+ KeyDetection?: {
359
+ Text?: string
360
+ Geometry?: Geometry
361
+ Confidence?: number
362
+ }
363
+ ValueDetections?: Array<{
364
+ Text?: string
365
+ Geometry?: Geometry
366
+ Confidence?: number
367
+ SelectionStatus?: 'SELECTED' | 'NOT_SELECTED'
368
+ }>
369
+ }>
370
+ SignatureDetections?: Array<{
371
+ Confidence?: number
372
+ Geometry?: Geometry
373
+ }>
374
+ }
375
+
376
+ export interface LendingResult {
377
+ Page?: number
378
+ PageClassification?: {
379
+ PageType?: Array<{
380
+ Value?: string
381
+ Confidence?: number
382
+ }>
383
+ PageNumber?: Array<{
384
+ Value?: string
385
+ Confidence?: number
386
+ }>
387
+ }
388
+ Extractions?: Array<{
389
+ LendingDocument?: LendingDocument
390
+ ExpenseDocument?: ExpenseDocument
391
+ IdentityDocument?: IdentityDocument
392
+ }>
393
+ }
394
+
395
+ export interface GetLendingAnalysisCommandOutput {
396
+ DocumentMetadata?: DocumentMetadata
397
+ JobStatus?: 'IN_PROGRESS' | 'SUCCEEDED' | 'FAILED' | 'PARTIAL_SUCCESS'
398
+ NextToken?: string
399
+ Results?: LendingResult[]
400
+ Warnings?: Warning[]
401
+ StatusMessage?: string
402
+ AnalyzeLendingModelVersion?: string
403
+ }
404
+
405
+ // ============================================================================
406
+ // Textract Client
407
+ // ============================================================================
408
+
409
+ export class TextractClient {
410
+ private client: AWSClient
411
+ private region: string
412
+
413
+ constructor(region: string = 'us-east-1') {
414
+ this.region = region
415
+ this.client = new AWSClient()
416
+ }
417
+
418
+ private async request<T>(action: string, params: Record<string, unknown>): Promise<T> {
419
+ return this.client.request({
420
+ service: 'textract',
421
+ region: this.region,
422
+ method: 'POST',
423
+ path: '/',
424
+ headers: {
425
+ 'Content-Type': 'application/x-amz-json-1.1',
426
+ 'X-Amz-Target': `Textract.${action}`,
427
+ },
428
+ body: JSON.stringify(params),
429
+ })
430
+ }
431
+
432
+ // -------------------------------------------------------------------------
433
+ // Synchronous Operations
434
+ // -------------------------------------------------------------------------
435
+
436
+ /**
437
+ * Detect text in a document (OCR)
438
+ */
439
+ async detectDocumentText(params: DetectDocumentTextCommandInput): Promise<DetectDocumentTextCommandOutput> {
440
+ return this.request('DetectDocumentText', params as unknown as Record<string, unknown>)
441
+ }
442
+
443
+ /**
444
+ * Analyze a document (forms, tables, queries)
445
+ */
446
+ async analyzeDocument(params: AnalyzeDocumentCommandInput): Promise<AnalyzeDocumentCommandOutput> {
447
+ return this.request('AnalyzeDocument', params as unknown as Record<string, unknown>)
448
+ }
449
+
450
+ /**
451
+ * Analyze expense document (receipts, invoices)
452
+ */
453
+ async analyzeExpense(params: AnalyzeExpenseCommandInput): Promise<AnalyzeExpenseCommandOutput> {
454
+ return this.request('AnalyzeExpense', params as unknown as Record<string, unknown>)
455
+ }
456
+
457
+ /**
458
+ * Analyze ID document (driver's license, passport)
459
+ */
460
+ async analyzeID(params: AnalyzeIDCommandInput): Promise<AnalyzeIDCommandOutput> {
461
+ return this.request('AnalyzeID', params as unknown as Record<string, unknown>)
462
+ }
463
+
464
+ // -------------------------------------------------------------------------
465
+ // Asynchronous Operations
466
+ // -------------------------------------------------------------------------
467
+
468
+ /**
469
+ * Start async text detection job
470
+ */
471
+ async startDocumentTextDetection(params: StartDocumentTextDetectionCommandInput): Promise<StartDocumentTextDetectionCommandOutput> {
472
+ return this.request('StartDocumentTextDetection', params as unknown as Record<string, unknown>)
473
+ }
474
+
475
+ /**
476
+ * Get results of text detection job
477
+ */
478
+ async getDocumentTextDetection(params: GetDocumentTextDetectionCommandInput): Promise<GetDocumentTextDetectionCommandOutput> {
479
+ return this.request('GetDocumentTextDetection', params as unknown as Record<string, unknown>)
480
+ }
481
+
482
+ /**
483
+ * Start async document analysis job
484
+ */
485
+ async startDocumentAnalysis(params: StartDocumentAnalysisCommandInput): Promise<StartDocumentAnalysisCommandOutput> {
486
+ return this.request('StartDocumentAnalysis', params as unknown as Record<string, unknown>)
487
+ }
488
+
489
+ /**
490
+ * Get results of document analysis job
491
+ */
492
+ async getDocumentAnalysis(params: GetDocumentAnalysisCommandInput): Promise<GetDocumentAnalysisCommandOutput> {
493
+ return this.request('GetDocumentAnalysis', params as unknown as Record<string, unknown>)
494
+ }
495
+
496
+ /**
497
+ * Start async expense analysis job
498
+ */
499
+ async startExpenseAnalysis(params: StartExpenseAnalysisCommandInput): Promise<StartExpenseAnalysisCommandOutput> {
500
+ return this.request('StartExpenseAnalysis', params as unknown as Record<string, unknown>)
501
+ }
502
+
503
+ /**
504
+ * Get results of expense analysis job
505
+ */
506
+ async getExpenseAnalysis(params: GetExpenseAnalysisCommandInput): Promise<GetExpenseAnalysisCommandOutput> {
507
+ return this.request('GetExpenseAnalysis', params as unknown as Record<string, unknown>)
508
+ }
509
+
510
+ /**
511
+ * Start async lending analysis job
512
+ */
513
+ async startLendingAnalysis(params: StartLendingAnalysisCommandInput): Promise<StartLendingAnalysisCommandOutput> {
514
+ return this.request('StartLendingAnalysis', params as unknown as Record<string, unknown>)
515
+ }
516
+
517
+ /**
518
+ * Get results of lending analysis job
519
+ */
520
+ async getLendingAnalysis(params: GetLendingAnalysisCommandInput): Promise<GetLendingAnalysisCommandOutput> {
521
+ return this.request('GetLendingAnalysis', params as unknown as Record<string, unknown>)
522
+ }
523
+
524
+ // -------------------------------------------------------------------------
525
+ // Convenience Methods
526
+ // -------------------------------------------------------------------------
527
+
528
+ /**
529
+ * Extract all text from a document
530
+ */
531
+ async extractText(document: Document): Promise<string[]> {
532
+ const result = await this.detectDocumentText({ Document: document })
533
+ return result.Blocks?.filter(b => b.BlockType === 'LINE').map(b => b.Text || '') || []
534
+ }
535
+
536
+ /**
537
+ * Extract text from S3 document
538
+ */
539
+ async extractTextFromS3(bucket: string, key: string): Promise<string[]> {
540
+ return this.extractText({ S3Object: { Bucket: bucket, Name: key } })
541
+ }
542
+
543
+ /**
544
+ * Extract key-value pairs (forms) from a document
545
+ */
546
+ async extractForms(document: Document): Promise<Array<{ key: string; value: string; confidence: number }>> {
547
+ const result = await this.analyzeDocument({
548
+ Document: document,
549
+ FeatureTypes: ['FORMS'],
550
+ })
551
+
552
+ const blocks = result.Blocks || []
553
+ const blockMap = new Map<string, Block>()
554
+ blocks.forEach(b => b.Id && blockMap.set(b.Id, b))
555
+
556
+ const forms: Array<{ key: string; value: string; confidence: number }> = []
557
+
558
+ for (const block of blocks) {
559
+ if (block.BlockType === 'KEY_VALUE_SET' && block.EntityTypes?.includes('KEY')) {
560
+ const keyText = this.getBlockText(block, blockMap)
561
+ const valueBlock = block.Relationships?.find(r => r.Type === 'VALUE')
562
+ let valueText = ''
563
+ if (valueBlock?.Ids) {
564
+ for (const id of valueBlock.Ids) {
565
+ const vb = blockMap.get(id)
566
+ if (vb) valueText += this.getBlockText(vb, blockMap) + ' '
567
+ }
568
+ }
569
+ forms.push({
570
+ key: keyText.trim(),
571
+ value: valueText.trim(),
572
+ confidence: block.Confidence || 0,
573
+ })
574
+ }
575
+ }
576
+
577
+ return forms
578
+ }
579
+
580
+ /**
581
+ * Extract tables from a document
582
+ */
583
+ async extractTables(document: Document): Promise<Array<{ rows: string[][] }>> {
584
+ const result = await this.analyzeDocument({
585
+ Document: document,
586
+ FeatureTypes: ['TABLES'],
587
+ })
588
+
589
+ const blocks = result.Blocks || []
590
+ const blockMap = new Map<string, Block>()
591
+ blocks.forEach(b => b.Id && blockMap.set(b.Id, b))
592
+
593
+ const tables: Array<{ rows: string[][] }> = []
594
+
595
+ for (const block of blocks) {
596
+ if (block.BlockType === 'TABLE') {
597
+ const cellIds = block.Relationships?.find(r => r.Type === 'CHILD')?.Ids || []
598
+ const cells: Block[] = cellIds.map(id => blockMap.get(id)).filter(Boolean) as Block[]
599
+
600
+ // Find max row and column
601
+ let maxRow = 0
602
+ let maxCol = 0
603
+ for (const cell of cells) {
604
+ if (cell.RowIndex && cell.RowIndex > maxRow) maxRow = cell.RowIndex
605
+ if (cell.ColumnIndex && cell.ColumnIndex > maxCol) maxCol = cell.ColumnIndex
606
+ }
607
+
608
+ // Build 2D array
609
+ const rows: string[][] = Array.from({ length: maxRow }, () => Array.from({ length: maxCol }, () => ''))
610
+
611
+ for (const cell of cells) {
612
+ if (cell.RowIndex && cell.ColumnIndex) {
613
+ rows[cell.RowIndex - 1][cell.ColumnIndex - 1] = this.getBlockText(cell, blockMap)
614
+ }
615
+ }
616
+
617
+ tables.push({ rows })
618
+ }
619
+ }
620
+
621
+ return tables
622
+ }
623
+
624
+ /**
625
+ * Extract expense summary from receipt/invoice
626
+ */
627
+ async extractExpenseSummary(document: Document): Promise<{
628
+ vendor?: string
629
+ total?: string
630
+ date?: string
631
+ items: Array<{ description?: string; quantity?: string; price?: string }>
632
+ }> {
633
+ const result = await this.analyzeExpense({ Document: document })
634
+ const expense = result.ExpenseDocuments?.[0]
635
+
636
+ if (!expense) {
637
+ return { items: [] }
638
+ }
639
+
640
+ const summary: { vendor?: string; total?: string; date?: string } = {}
641
+
642
+ for (const field of expense.SummaryFields || []) {
643
+ const type = field.Type?.Text?.toUpperCase()
644
+ const value = field.ValueDetection?.Text
645
+
646
+ if (type === 'VENDOR_NAME') summary.vendor = value
647
+ if (type === 'TOTAL') summary.total = value
648
+ if (type === 'INVOICE_RECEIPT_DATE') summary.date = value
649
+ }
650
+
651
+ const items: Array<{ description?: string; quantity?: string; price?: string }> = []
652
+
653
+ for (const group of expense.LineItemGroups || []) {
654
+ for (const lineItem of group.LineItems || []) {
655
+ const item: { description?: string; quantity?: string; price?: string } = {}
656
+ for (const field of lineItem.LineItemExpenseFields || []) {
657
+ const type = field.Type?.Text?.toUpperCase()
658
+ const value = field.ValueDetection?.Text
659
+
660
+ if (type === 'ITEM') item.description = value
661
+ if (type === 'QUANTITY') item.quantity = value
662
+ if (type === 'PRICE') item.price = value
663
+ }
664
+ if (item.description || item.price) items.push(item)
665
+ }
666
+ }
667
+
668
+ return { ...summary, items }
669
+ }
670
+
671
+ /**
672
+ * Answer questions about a document
673
+ */
674
+ async queryDocument(document: Document, questions: string[]): Promise<Array<{ question: string; answer: string; confidence: number }>> {
675
+ const result = await this.analyzeDocument({
676
+ Document: document,
677
+ FeatureTypes: ['QUERIES'],
678
+ QueriesConfig: {
679
+ Queries: questions.map(q => ({ Text: q })),
680
+ },
681
+ })
682
+
683
+ const blocks = result.Blocks || []
684
+ const answers: Array<{ question: string; answer: string; confidence: number }> = []
685
+
686
+ for (const block of blocks) {
687
+ if (block.BlockType === 'QUERY_RESULT' && block.Text) {
688
+ // Find the corresponding query
689
+ const queryBlock = blocks.find(b =>
690
+ b.BlockType === 'QUERY' && b.Relationships?.some(r =>
691
+ r.Type === 'ANSWER' && r.Ids?.includes(block.Id || ''),
692
+ ),
693
+ )
694
+ answers.push({
695
+ question: queryBlock?.Query?.Text || '',
696
+ answer: block.Text,
697
+ confidence: block.Confidence || 0,
698
+ })
699
+ }
700
+ }
701
+
702
+ return answers
703
+ }
704
+
705
+ /**
706
+ * Wait for async job to complete
707
+ */
708
+ async waitForJob(
709
+ jobId: string,
710
+ getJob: (jobId: string) => Promise<{ JobStatus?: string }>,
711
+ options?: { maxWaitMs?: number; pollIntervalMs?: number },
712
+ ): Promise<void> {
713
+ const maxWaitMs = options?.maxWaitMs ?? 300000 // 5 minutes
714
+ const pollIntervalMs = options?.pollIntervalMs ?? 5000
715
+ const startTime = Date.now()
716
+
717
+ while (Date.now() - startTime < maxWaitMs) {
718
+ const result = await getJob(jobId)
719
+ if (result.JobStatus === 'SUCCEEDED' || result.JobStatus === 'PARTIAL_SUCCESS') {
720
+ return
721
+ }
722
+ if (result.JobStatus === 'FAILED') {
723
+ throw new Error(`Textract job ${jobId} failed`)
724
+ }
725
+ await new Promise(resolve => setTimeout(resolve, pollIntervalMs))
726
+ }
727
+
728
+ throw new Error(`Timeout waiting for Textract job ${jobId}`)
729
+ }
730
+
731
+ private getBlockText(block: Block, blockMap: Map<string, Block>): string {
732
+ if (block.Text) return block.Text
733
+
734
+ const childIds = block.Relationships?.find(r => r.Type === 'CHILD')?.Ids || []
735
+ return childIds.map(id => blockMap.get(id)?.Text || '').join(' ')
736
+ }
737
+ }
738
+
739
+ // ============================================================================
740
+ // Helper Functions
741
+ // ============================================================================
742
+
743
+ /**
744
+ * Quick text extraction from S3 document
745
+ */
746
+ export async function extractTextFromS3(
747
+ bucket: string,
748
+ key: string,
749
+ region?: string,
750
+ ): Promise<string> {
751
+ const client = new TextractClient(region || 'us-east-1')
752
+ const lines = await client.extractTextFromS3(bucket, key)
753
+ return lines.join('\n')
754
+ }
755
+
756
+ /**
757
+ * Quick form extraction from S3 document
758
+ */
759
+ export async function extractFormsFromS3(
760
+ bucket: string,
761
+ key: string,
762
+ region?: string,
763
+ ): Promise<Record<string, string>> {
764
+ const client = new TextractClient(region || 'us-east-1')
765
+ const forms = await client.extractForms({ S3Object: { Bucket: bucket, Name: key } })
766
+ return Object.fromEntries(forms.map(f => [f.key, f.value]))
767
+ }
768
+
769
+ /**
770
+ * Quick table extraction from S3 document
771
+ */
772
+ export async function extractTablesFromS3(
773
+ bucket: string,
774
+ key: string,
775
+ region?: string,
776
+ ): Promise<string[][][]> {
777
+ const client = new TextractClient(region || 'us-east-1')
778
+ const tables = await client.extractTables({ S3Object: { Bucket: bucket, Name: key } })
779
+ return tables.map(t => t.rows)
780
+ }