diff --git a/samples/python/README.md b/samples/python/README.md index d37a260..e3ddfeb 100644 --- a/samples/python/README.md +++ b/samples/python/README.md @@ -316,10 +316,163 @@ python convert_cli.py ../../test_files/test-batch/Analysis.docx /tmp/output.pdf ./test_all.sh ``` -## Sample Files +## Testing with Sample Files + +The repository includes test files you can use to try out the scripts. All test files are located in the `../../test_files/` directory (relative to the Python samples folder). + +### Quick Test Commands + +#### 1. Test Authentication +```bash +uv run python quickstart.py +# Expected: ✅ Authentication successful! Token: eyJ0... +``` + +#### 2. Convert Single Document +```bash +# Convert Word document to PDF +uv run python convert_cli.py ../../test_files/test-batch/Analysis.docx output/test_output.pdf pdf + +# Convert Excel to PDF +uv run python convert_cli.py ../../test_files/test-batch/Feedback.xlsx output/test_output.pdf pdf + +# Convert PowerPoint to PNG +uv run python convert_cli.py ../../test_files/SamplePPTX.pptx output/test_output.png png +``` + +#### 3. Batch Document Conversion +```bash +# Convert all Word documents in test-batch to PDF +uv run python batch_process.py \ + ../../test_files/test-batch \ + output/batch_results \ + pdf \ + "*.docx" + +# Convert all Excel files +uv run python batch_process.py \ + ../../test_files/test-batch \ + output/batch_results \ + pdf \ + "*.xlsx" +``` + +#### 4. Extract Data from PDFs +```bash +# Extract form fields from student loan application +uv run python extract_data.py \ + forms \ + "../../test_files/test-pdfs/BOB - Student-Loan-Application-Form.pdf" \ + output/forms_output.json + +# Extract tables from PDF +uv run python extract_data.py \ + tables \ + "../../test_files/test-pdfs/Sample Tables.pdf" \ + output/tables_output.json +``` + +#### 5. Smart PII Redaction +```bash +# Automatically detect and redact PII from PDFs +uv run python smart_redact_pii.py \ + ../../test_files/test-pdfs \ + output/pii_redacted +``` + +#### 6. Keyword-Based Redaction +```bash +# Redact specific keywords from resume +uv run python redact_by_keyword.py \ + ../../test_files/test-pdfs/SampleResume.pdf \ + output/redacted.pdf \ + "resume" "contact" +``` + +#### 7. Bulk Password Protection +```bash +# Password protect all PDFs in test-pdfs folder +uv run python bulk_password_protect.py \ + ../../test_files/test-pdfs \ + output/protected \ + "SecurePass123" +``` + +#### 8. Prepare PDFs for Distribution +```bash +# Convert marketing brochures to optimized PDFs with metadata removed +uv run python prepare_pdf_for_distribution.py \ + ../../test_files/pdf-distribution \ + output/distribution +``` + +#### 9. Employee Policy Onboarding (Sign API) +```bash +# Send company policies for signature to employees +uv run python employee_policy_onboarding.py \ + ../../test_files/test-sign \ + ../../test_files/test-sign/employees.csv + +# Note: This sends real signature requests to email addresses in the CSV! +# Check the CSV file first: ../../test_files/test-sign/employees.csv +``` + +### Using Task Commands + +All the above can also be run using Task commands for convenience: + +```bash +# Convert +task convert INPUT=../../test_files/test-batch/Analysis.docx OUTPUT=output/test.pdf FORMAT=pdf + +# Batch process +task batch INPUT_DIR=../../test_files/test-batch OUTPUT_DIR=output/batch FORMAT=pdf PATTERN='*.docx' + +# Extract data +task extract MODE=tables INPUT="../../test_files/test-pdfs/Sample Tables.pdf" OUTPUT=output/tables.json + +# Smart redact +task smart-redact INPUT_DIR=../../test_files/test-pdfs OUTPUT_DIR=output/redacted + +# Prepare PDFs +task prepare-distribution INPUT_DIR=../../test_files/pdf-distribution OUTPUT_DIR=output/distribution + +# Employee onboarding +task onboard-employees POLICIES_DIR=../../test_files/test-sign CSV=../../test_files/test-sign/employees.csv +``` + +### Available Test Files Sample files for testing are available in the `test_files/` folder at the repository root. +#### Batch Conversion (`test_files/test-batch/`) +- `Analysis.docx` - Word document +- `Feedback.xlsx` - Excel spreadsheet + +#### PDF Operations (`test_files/test-pdfs/`) +- `SampleResume.pdf` - Resume with PII (for redaction testing) +- `Sample Tables.pdf` - PDF with tables (for extraction) +- `BOB - Student-Loan-Application-Form.pdf` - Form with fields (for extraction) + +#### Distribution (`test_files/pdf-distribution/`) +- `Marketing_Brochure_Product_A_Rich.docx` +- `Marketing_Brochure_Product_B_Rich.docx` +- `Marketing_Brochure_Product_C_Rich.docx` + +#### Sign API (`test_files/test-sign/`) +- `company-policies.pdf` - Company policy document +- `confidentiality-agreement.pdf` - NDA template +- `sample-company-policies.pdf` - Additional policy +- `employees.csv` - Sample employee list for testing + +### Important Notes + +- **Sign API Testing**: The `employee_policy_onboarding` script sends real signature requests via email. Make sure the email addresses in `employees.csv` are valid and you have permission to send them test requests. +- **Output Folders**: All scripts automatically create output folders if they don't exist. +- **File Paths**: Use quotes around file paths with spaces (e.g., `"Sample Tables.pdf"`). +- **Large Files**: Some operations may take longer with large or complex documents. + + ## Getting Your Credentials 1. Go to [https://admin.gonitro.com](https://admin.gonitro.com) diff --git a/samples/typescript/.env.example b/samples/typescript/.env.example new file mode 100644 index 0000000..3a3d066 --- /dev/null +++ b/samples/typescript/.env.example @@ -0,0 +1,3 @@ +PLATFORM_BASE_URL=https://api.gonitro.dev +PLATFORM_CLIENT_ID=your-client-id-here +PLATFORM_CLIENT_SECRET=your-client-secret-here diff --git a/samples/typescript/.gitignore b/samples/typescript/.gitignore new file mode 100644 index 0000000..4f46eb1 --- /dev/null +++ b/samples/typescript/.gitignore @@ -0,0 +1,30 @@ +# Dependencies +node_modules/ + +# Build outputs +dist/ +*.tsbuildinfo + +# Environment files +.env +.env.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Output files +output/ +temp/ diff --git a/samples/typescript/README.md b/samples/typescript/README.md new file mode 100644 index 0000000..008fa9c --- /dev/null +++ b/samples/typescript/README.md @@ -0,0 +1,462 @@ +# TypeScript Examples + +TypeScript examples for integrating with the Nitro Platform API. + +## Prerequisites + +- Node.js 18.0.0 or higher +- npm or yarn package manager + +## Setup + +1. **Install dependencies:** + ```bash + cd samples/typescript + npm install + ``` + +2. **Configure environment variables:** + ```bash + cp .env.example .env + # Edit .env with your credentials + ``` + +3. **Get your API credentials:** + - Go to https://admin.gonitro.com + - Navigate to Settings → API + - Click "Create Application" + - Name your application and save the Client ID and Client Secret + - Add these to your `.env` file: + ``` + PLATFORM_CLIENT_ID=your-client-id-here + PLATFORM_CLIENT_SECRET=your-client-secret-here + PLATFORM_BASE_URL=https://api.gonitro.dev + ``` + +4. **Test your setup:** + ```bash + npm run quickstart + ``` + +## Development + +### Running Scripts + +All scripts can be run using npm scripts or directly with tsx: + +```bash +# Using npm scripts (recommended) +npm run quickstart +npm run convert -- input.docx output.pdf pdf +npm run batch -- ./documents ./output pdf "*.docx" + +# Using tsx directly +tsx src/scripts/quickstart.ts +tsx src/scripts/convert-cli.ts input.docx output.pdf pdf +``` + +### Building the Project + +To compile TypeScript to JavaScript: + +```bash +npm run build +``` + +This will create compiled JavaScript files in the `dist/` directory. + +### Development Mode + +For development with auto-reloading: + +```bash +npm run dev +``` + +## Architecture + +### API Client Structure + +The TypeScript SDK follows a clean, object-oriented architecture similar to the Python SDK: + +``` +src/ +├── api/ +│ ├── base-client.ts # BaseOAuthClient - Shared OAuth2 authentication +│ ├── platform-api.ts # PlatformAPIClient - Document operations +│ └── sign-api.ts # SignAPIClient - eSignature operations +├── helpers/ +│ ├── document-helpers.ts # File validation and setup utilities +│ └── sign-helpers.ts # Sign workflow orchestration helpers +└── scripts/ + ├── quickstart.ts + ├── convert-cli.ts + ├── batch-process.ts + ├── extract-data.ts + ├── smart-redact-pii.ts + ├── employee-policy-onboarding.ts + ├── bulk-password-protect.ts + ├── redact-by-keyword.ts + └── prepare-pdf-for-distribution.ts +``` + +**BaseOAuthClient**: Base class providing OAuth2 authentication for all API clients +- Automatic token management with expiry buffer +- Connection pooling via axios +- Loads credentials from environment variables or .env file + +**PlatformAPIClient**: Client for document conversions, extractions, and transformations +- Inherits authentication from BaseOAuthClient +- Methods: `convert()`, `extractText()`, `detectPii()`, `redact()`, `compress()`, etc. +- Uses FormData for file uploads + +**SignAPIClient**: Client for eSignature/envelope operations +- Inherits authentication from BaseOAuthClient +- Methods: `createEnvelope()`, `createParticipant()`, `sendForSigning()`, etc. +- Full envelope lifecycle management + +## Available Scripts + +### Platform API Tools + +#### quickstart.ts +Test authentication and API connection. +```bash +npm run quickstart +``` + +#### convert-cli.ts +Convert a single document to another format. +```bash +npm run convert -- + +# Examples: +npm run convert -- document.docx document.pdf pdf +npm run convert -- presentation.pptx slide.png png +``` + +**Supported formats:** pdf, docx, xlsx, pptx, png + +#### batch-process.ts +Batch convert multiple documents in a folder. +```bash +npm run batch -- [pattern] + +# Examples: +npm run batch -- ./documents ./output pdf "*.docx" +npm run batch -- ./spreadsheets ./pdfs pdf "*.xlsx" +``` + +#### extract-data.ts +Extract structured data (forms or tables) from PDFs. +```bash +npm run extract -- + +# Examples: +npm run extract -- forms application.pdf data.json +npm run extract -- tables invoice.pdf tables.json +``` + +**Modes:** +- `forms` - Extract form fields (name-value pairs) +- `tables` - Extract table data (rows and columns) + +#### smart-redact-pii.ts +Automatically detect and redact personally identifiable information (PII). +```bash +npm run smart-redact -- + +# Example: +npm run smart-redact -- ./documents ./redacted +``` + +Detects and redacts: +- Social Security Numbers +- Phone numbers +- Email addresses +- Physical addresses +- Other PII patterns + +#### redact-by-keyword.ts +Redact specific keywords or phrases from PDFs. +```bash +npm run redact-keyword -- [keyword2 ...] + +# Examples: +npm run redact-keyword -- contract.pdf redacted.pdf "confidential" "proprietary" +npm run redact-keyword -- report.pdf clean.pdf "Project Zeus" "Client ABC" +``` + +#### bulk-password-protect.ts +Apply password protection to multiple PDFs. +```bash +npm run bulk-password -- + +# Example: +npm run bulk-password -- ./documents ./protected "MySecureP@ss123" +``` + +**Note:** Password must be at least 6 characters long. + +#### prepare-pdf-for-distribution.ts +Prepare documents for external distribution (convert to PDF, compress, remove metadata). +```bash +npm run prepare-pdf -- + +# Example: +npm run prepare-pdf -- ./brochures ./distribution +``` + +This script: +1. Converts documents to PDF format +2. Compresses PDFs to reduce file size +3. Removes all metadata properties (author, title, etc.) + +### Sign API Tools + +#### employee-policy-onboarding.ts +Complete HR onboarding workflow: send company policies to employees for signature. + +```bash +npm run employee-policy -- + +# Example: +npm run employee-policy -- ./policies ./employees.csv +``` + +**CSV Format:** +```csv +name,email +John Doe,john.doe@company.com +Jane Smith,jane.smith@company.com +``` + +**This script:** +1. Loads employee list from CSV +2. Loads policy documents from folder +3. For each employee: + - Creates a signature envelope + - Uploads all policy documents + - Adds the employee as a signer + - Configures signature fields + - Sends the envelope via email + - Monitors signing status (up to 60 minutes) + - Downloads signed documents when complete +4. Organizes output by employee folder + +**Output Structure:** +``` +output/ +├── john-doe/ +│ ├── signed-documents/ +│ │ ├── policy-1-signed.pdf +│ │ ├── policy-2-signed.pdf +│ │ └── audit-trail.pdf +│ └── envelope-info.json +├── jane-smith/ + ├── signed-documents/ + └── envelope-info.json +``` + +## Testing with Sample Files + +The repository includes test files you can use to try out the scripts. All test files are located in the `../../test_files/` directory (relative to the TypeScript samples folder). + +### Quick Test Commands + +#### 1. Test Authentication +```bash +npm run quickstart +# Expected: ✅ Authentication successful! Token: eyJ0... +``` + +#### 2. Convert Single Document +```bash +# Convert Word document to PDF +npm run convert -- ../../test_files/test-batch/Analysis.docx output.pdf pdf + +# Convert Excel to PDF +npm run convert -- ../../test_files/test-batch/Feedback.xlsx output.pdf pdf + +# Convert PowerPoint to PNG +npm run convert -- ../../test_files/SamplePPTX.pptx output.png png +``` + +#### 3. Batch Document Conversion +```bash +# Convert all Word documents in test-batch to PDF +npm run batch -- ../../test_files/test-batch ./output pdf "*.docx" + +# Convert all Excel files +npm run batch -- ../../test_files/test-batch ./output pdf "*.xlsx" +``` + +#### 4. Extract Data from PDFs +```bash +# Extract form fields from student loan application +npm run extract -- forms "../../test_files/test-pdfs/BOB - Student-Loan-Application-Form.pdf" forms-output.json + +# Extract tables from PDF +npm run extract -- tables "../../test_files/test-pdfs/Sample Tables.pdf" tables-output.json +``` + +#### 5. Smart PII Redaction +```bash +# Automatically detect and redact PII from resume +npm run smart-redact -- ../../test_files/test-pdfs ./redacted-output +``` + +#### 6. Keyword-Based Redaction +```bash +# Redact specific keywords from resume +npm run redact-keyword -- ../../test_files/test-pdfs/SampleResume.pdf redacted.pdf "resume" "contact" +``` + +#### 7. Bulk Password Protection +```bash +# Password protect all PDFs in test-pdfs folder +npm run bulk-password -- ../../test_files/test-pdfs ./protected-output "SecurePass123" +``` + +#### 8. Prepare PDFs for Distribution +```bash +# Convert marketing brochures to optimized PDFs with metadata removed +npm run prepare-pdf -- ../../test_files/pdf-distribution ./distribution-output +``` + +#### 9. Employee Policy Onboarding (Sign API) +```bash +# Send company policies for signature to employees +npm run employee-policy -- ../../test_files/test-sign ../../test_files/test-sign/employees.csv + +# Note: This sends real signature requests to email addresses in the CSV! +# Check the CSV file first: ../../test_files/test-sign/employees.csv +``` + +### Available Test Files + +#### Batch Conversion (`test_files/test-batch/`) +- `Analysis.docx` - Word document +- `Feedback.xlsx` - Excel spreadsheet + +#### PDF Operations (`test_files/test-pdfs/`) +- `SampleResume.pdf` - Resume with PII (for redaction testing) +- `Sample Tables.pdf` - PDF with tables (for extraction) +- `BOB - Student-Loan-Application-Form.pdf` - Form with fields (for extraction) + +#### Distribution (`test_files/pdf-distribution/`) +- `Marketing_Brochure_Product_A_Rich.docx` +- `Marketing_Brochure_Product_B_Rich.docx` +- `Marketing_Brochure_Product_C_Rich.docx` + +#### Sign API (`test_files/test-sign/`) +- `company-policies.pdf` - Company policy document +- `confidentiality-agreement.pdf` - NDA template +- `sample-company-policies.pdf` - Additional policy +- `employees.csv` - Sample employee list for testing + + +### Important Notes + +- **Sign API Testing**: The `employee-policy-onboarding` script sends real signature requests via email. Make sure the email addresses in `employees.csv` are valid and you have permission to send them test requests. +- **Output Folders**: All scripts automatically create output folders if they don't exist. +- **File Paths**: Use quotes around file paths with spaces (e.g., `"Sample Tables.pdf"`). +- **Large Files**: Some operations may take longer with large or complex documents. + +## Code Style & Conventions + +### Naming Conventions +- **Files:** kebab-case (e.g., `base-client.ts`, `convert-cli.ts`) +- **Classes:** PascalCase (e.g., `BaseOAuthClient`, `PlatformAPIClient`) +- **Functions/Variables:** camelCase (e.g., `getToken`, `convertDocument`) +- **Constants:** SCREAMING_SNAKE_CASE (e.g., `TOKEN_EXPIRY_BUFFER_SECONDS`) +- **Types/Interfaces:** PascalCase (e.g., `Settings`, `TokenResponse`) + +### Module System +This project uses ES Modules (ESM). All imports must use the `.js` extension: +```typescript +import { BaseOAuthClient } from './base-client.js'; +``` + +### Error Handling +Always use try-catch blocks and provide meaningful error messages: +```typescript +try { + const result = await client.convert(filePath, OutputFormat.PDF); + console.log('✅ Success'); +} catch (error: any) { + console.error(`❌ Error: ${error.message}`); + process.exit(1); +} +``` + +## TypeScript Configuration + +The project uses strict TypeScript settings: +- Target: ES2022 +- Module: ESNext +- Strict mode enabled +- Source maps for debugging + +See `tsconfig.json` for complete configuration. + +## Differences from Python SDK + +While the TypeScript SDK maintains functional parity with the Python SDK, there are some differences: + +1. **Module System:** Uses ES Modules instead of Python imports +2. **Async/Await:** All API calls are asynchronous (using async/await) +3. **Type Safety:** Full TypeScript type checking +4. **CLI Framework:** Uses commander instead of typer +5. **File Operations:** Uses Node.js fs/promises instead of pathlib +6. **HTTP Client:** Uses axios instead of httpx + +## Dependencies + +### Production Dependencies +- **axios** - HTTP client with connection pooling +- **commander** - CLI framework +- **dotenv** - Environment variable management +- **form-data** - Multipart form data for file uploads +- **csv-parse** - CSV file parsing +- **adm-zip** - ZIP file extraction +- **mime-types** - MIME type detection + +### Development Dependencies +- **typescript** - TypeScript compiler +- **tsx** - TypeScript execution and development +- **@types/node** - Node.js type definitions + +## Troubleshooting + +### "Missing required environment variables" +Make sure you've created a `.env` file with your API credentials. Copy `.env.example` and fill in your values. + +### "ENOENT: no such file or directory" +Check that your input file paths are correct and the files exist. + +### "401 Unauthorized" +Your API credentials may be incorrect or expired. Double-check your `PLATFORM_CLIENT_ID` and `PLATFORM_CLIENT_SECRET` in the `.env` file. + +### "Network error" or timeout +Check your internet connection and verify that `PLATFORM_BASE_URL` is correct in your `.env` file. + +### TypeScript compilation errors +Make sure all dependencies are installed: +```bash +npm install +``` + +## Further Resources + +- [Nitro Platform API Documentation](https://developers.gonitro.com/docs) +- [Nitro Admin Portal](https://admin.gonitro.com) +- [TypeScript Documentation](https://www.typescriptlang.org/docs/) +- [Node.js Documentation](https://nodejs.org/docs/) + +## Support + +For questions or issues: +- Check the [Python SDK examples](../python/README.md) for additional context +- Review the [API documentation](https://developers.gonitro.com/docs) +- Contact Nitro support through the admin portal diff --git a/samples/typescript/package-lock.json b/samples/typescript/package-lock.json new file mode 100644 index 0000000..8d0b96f --- /dev/null +++ b/samples/typescript/package-lock.json @@ -0,0 +1,936 @@ +{ + "name": "@nitro/platform-samples-typescript", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@nitro/platform-samples-typescript", + "version": "0.1.0", + "dependencies": { + "adm-zip": "^0.5.10", + "axios": "^1.6.0", + "commander": "^11.0.0", + "csv-parse": "^5.5.0", + "dotenv": "^16.0.0", + "form-data": "^4.0.0", + "mime-types": "^2.1.35" + }, + "devDependencies": { + "@types/adm-zip": "^0.5.5", + "@types/mime-types": "^2.1.4", + "@types/node": "^20.0.0", + "tsx": "^4.0.0", + "typescript": "^5.3.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/adm-zip": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.7.tgz", + "integrity": "sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mime-types": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", + "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", + "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/csv-parse": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.6.0.tgz", + "integrity": "sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/samples/typescript/package.json b/samples/typescript/package.json new file mode 100644 index 0000000..d70caca --- /dev/null +++ b/samples/typescript/package.json @@ -0,0 +1,38 @@ +{ + "name": "@nitro/platform-samples-typescript", + "version": "0.1.0", + "type": "module", + "description": "TypeScript sample scripts for the Nitro Platform API", + "scripts": { + "build": "tsc", + "dev": "tsx watch", + "quickstart": "tsx src/scripts/quickstart.ts", + "convert": "tsx src/scripts/convert-cli.ts", + "batch": "tsx src/scripts/batch-process.ts", + "extract": "tsx src/scripts/extract-data.ts", + "smart-redact": "tsx src/scripts/smart-redact-pii.ts", + "employee-policy": "tsx src/scripts/employee-policy-onboarding.ts", + "bulk-password": "tsx src/scripts/bulk-password-protect.ts", + "redact-keyword": "tsx src/scripts/redact-by-keyword.ts", + "prepare-pdf": "tsx src/scripts/prepare-pdf-for-distribution.ts" + }, + "dependencies": { + "axios": "^1.6.0", + "commander": "^11.0.0", + "dotenv": "^16.0.0", + "form-data": "^4.0.0", + "csv-parse": "^5.5.0", + "adm-zip": "^0.5.10", + "mime-types": "^2.1.35" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/adm-zip": "^0.5.5", + "@types/mime-types": "^2.1.4", + "typescript": "^5.3.0", + "tsx": "^4.0.0" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/samples/typescript/src/api/base-client.ts b/samples/typescript/src/api/base-client.ts new file mode 100644 index 0000000..2c40168 --- /dev/null +++ b/samples/typescript/src/api/base-client.ts @@ -0,0 +1,104 @@ +/** + * Base client for API authentication with OAuth2. + */ + +import axios, { AxiosInstance } from 'axios'; +import dotenv from 'dotenv'; + +// Load environment variables +dotenv.config(); + +/** + * Token expiry buffer in seconds to account for clock skew and network latency. + */ +const TOKEN_EXPIRY_BUFFER_SECONDS = 60; + +/** + * OAuth2 token response model. + */ +interface TokenResponse { + accessToken: string; + expiresIn: number; +} + +/** + * Application settings for API authentication. + */ +interface Settings { + platformClientId: string; + platformClientSecret: string; + platformBaseUrl: string; +} + +/** + * Base class for API clients with OAuth2 authentication. + */ +export class BaseOAuthClient { + protected settings: Settings; + private token: string | null = null; + private tokenExpiry: number = 0; + protected client: AxiosInstance; + + /** + * Creates a new BaseOAuthClient instance. + * @param settings - Optional settings override. If not provided, loads from environment variables. + */ + constructor(settings?: Settings) { + this.settings = settings || this.loadSettingsFromEnv(); + + // Create axios instance with connection pooling + this.client = axios.create({ + timeout: 30000, + maxRedirects: 5, + }); + } + + /** + * Load settings from environment variables. + * @returns Settings object populated from environment. + * @throws Error if required environment variables are missing. + */ + private loadSettingsFromEnv(): Settings { + const clientId = process.env.PLATFORM_CLIENT_ID; + const clientSecret = process.env.PLATFORM_CLIENT_SECRET; + const baseUrl = process.env.PLATFORM_BASE_URL || 'https://api.gonitro.dev'; + + if (!clientId || !clientSecret) { + throw new Error( + 'Missing required environment variables: PLATFORM_CLIENT_ID and PLATFORM_CLIENT_SECRET must be set' + ); + } + + return { + platformClientId: clientId, + platformClientSecret: clientSecret, + platformBaseUrl: baseUrl, + }; + } + + /** + * Get or refresh OAuth2 access token. + * @returns OAuth2 access token for API authentication. + */ + protected async getToken(): Promise { + // Return cached token if still valid + if (this.token && Date.now() / 1000 < this.tokenExpiry) { + return this.token; + } + + // Request new token + const response = await this.client.post( + `${this.settings.platformBaseUrl}/oauth/token`, + { + clientID: this.settings.platformClientId, + clientSecret: this.settings.platformClientSecret, + } + ); + + const tokenData = response.data; + this.token = tokenData.accessToken; + this.tokenExpiry = Date.now() / 1000 + tokenData.expiresIn - TOKEN_EXPIRY_BUFFER_SECONDS; + + return tokenData.accessToken; + } +} diff --git a/samples/typescript/src/api/platform-api.ts b/samples/typescript/src/api/platform-api.ts new file mode 100644 index 0000000..71d2c97 --- /dev/null +++ b/samples/typescript/src/api/platform-api.ts @@ -0,0 +1,234 @@ +/** + * Platform API client for Nitro Platform integrations. + */ + +import { readFile } from 'fs/promises'; +import FormData from 'form-data'; +import axios from 'axios'; +import { BaseOAuthClient } from './base-client.js'; +import { lookup } from 'mime-types'; + +/** + * Supported output formats for document conversion. + */ +export enum OutputFormat { + PDF = 'pdf', + DOCX = 'docx', + XLSX = 'xlsx', + PPTX = 'pptx', + PNG = 'png', +} + +/** + * Type for API endpoints. + */ +type Endpoint = 'conversions' | 'extractions' | 'transformations'; + +/** + * Synchronous client for Nitro Platform API operations. + */ +export class PlatformAPIClient extends BaseOAuthClient { + /** + * Make API request with file upload. + * @param endpoint - API endpoint (conversions, extractions, transformations). + * @param method - API method to call. + * @param filePath - Path to file to upload. + * @param params - Optional parameters for the method. + * @returns Parsed JSON response. + */ + private async request( + endpoint: Endpoint, + method: string, + filePath: string, + params?: Record + ): Promise { + const token = await this.getToken(); + + // Read file and detect MIME type + const fileBuffer = await readFile(filePath); + const fileName = filePath.split('/').pop() || 'file'; + const mimeType = lookup(filePath) || 'application/octet-stream'; + + // Create form data + const form = new FormData(); + form.append('file', fileBuffer, { + filename: fileName, + contentType: mimeType, + }); + form.append('method', method); + form.append('params', JSON.stringify(params || {})); + + const response = await this.client.post( + `${this.settings.platformBaseUrl}/${endpoint}`, + form, + { + headers: { + Authorization: `Bearer ${token}`, + ...form.getHeaders(), + }, + } + ); + + return response.data; + } + + /** + * Make API request and return raw bytes from result file URL. + * @param endpoint - API endpoint. + * @param method - API method to call. + * @param filePath - Path to file to upload. + * @param params - Optional parameters for the method. + * @returns File content as Buffer. + */ + private async requestBytes( + endpoint: Endpoint, + method: string, + filePath: string, + params?: Record + ): Promise { + const result = await this.request(endpoint, method, filePath, params); + + // Download from S3 URL + const downloadUrl = result.result.file.URL; + const downloadResponse = await axios.get(downloadUrl, { + responseType: 'arraybuffer', + }); + + return Buffer.from(downloadResponse.data); + } + + /** + * Convert document to specified format. + * @param filePath - Path to input file. + * @param toFormat - Target format (pdf, docx, xlsx, pptx, png). + * @returns Converted file as Buffer. + */ + async convert(filePath: string, toFormat: OutputFormat): Promise { + return this.requestBytes('conversions', 'convert', filePath, { to: toFormat }); + } + + /** + * Extract text from document. + * @param filePath - Path to input file. + * @returns Extracted text data. + */ + async extractText(filePath: string): Promise { + return this.request('extractions', 'extract-text', filePath); + } + + /** + * Extract form data from PDF. + * @param filePath - Path to PDF file. + * @returns Extracted form data. + */ + async extractForms(filePath: string): Promise { + return this.request('extractions', 'extract-forms', filePath); + } + + /** + * Extract table data from PDF. + * @param filePath - Path to PDF file. + * @returns Extracted table data. + */ + async extractTables(filePath: string): Promise { + return this.request('extractions', 'extract-tables', filePath); + } + + /** + * Detect PII and return bounding boxes. + * @param filePath - Path to input file. + * @param language - Language code (default: 'en'). + * @returns PII detection results with bounding boxes. + */ + async detectPii(filePath: string, language: string = 'en'): Promise { + return this.request('extractions', 'extract-pii-bounding-boxes', filePath, { language }); + } + + /** + * Find bounding boxes for specified text strings. + * @param filePath - Path to input file. + * @param texts - Array of text strings to search for. + * @returns Bounding boxes for found text. + */ + async findTextBoxes(filePath: string, texts: string[]): Promise { + return this.request('extractions', 'extract-text-bounding-boxes', filePath, { texts }); + } + + /** + * Redact specified bounding boxes. + * @param filePath - Path to input file. + * @param redactions - Array of redaction specifications. + * @returns Redacted file as Buffer. + */ + async redact(filePath: string, redactions: any[]): Promise { + return this.requestBytes('transformations', 'redact', filePath, { redactions }); + } + + /** + * Add password protection to PDF. + * @param filePath - Path to PDF file. + * @param password - Password to set. + * @returns Protected PDF as Buffer. + */ + async passwordProtect(filePath: string, password: string): Promise { + return this.requestBytes('transformations', 'protect', filePath, { + ownerPassword: password, + userPassword: password, + }); + } + + /** + * Compress PDF. + * @param filePath - Path to PDF file. + * @param level - Compression level (1-3, default: 2). + * @returns Compressed PDF as Buffer. + */ + async compress(filePath: string, level: number = 2): Promise { + return this.requestBytes('transformations', 'compress', filePath, { level }); + } + + /** + * Set or clear PDF metadata properties. + * @param filePath - Path to PDF file. + * @param properties - Object with property names and values. + * @returns Modified PDF as Buffer. + */ + async setProperties(filePath: string, properties: Record): Promise { + return this.requestBytes('transformations', 'set-properties', filePath, properties); + } + + /** + * Merge multiple PDFs. + * @param filePaths - Array of paths to PDF files to merge. + * @returns Merged PDF as Buffer. + */ + async merge(filePaths: string[]): Promise { + const token = await this.getToken(); + + // Create form data with multiple files + const form = new FormData(); + + for (const filePath of filePaths) { + const fileBuffer = await readFile(filePath); + const fileName = filePath.split('/').pop() || 'file'; + form.append('file', fileBuffer, { filename: fileName }); + } + + form.append('method', 'merge'); + form.append('params', '{}'); + + const response = await this.client.post( + `${this.settings.platformBaseUrl}/transformations`, + form, + { + headers: { + Authorization: `Bearer ${token}`, + ...form.getHeaders(), + }, + responseType: 'arraybuffer', + } + ); + + return Buffer.from(response.data); + } +} diff --git a/samples/typescript/src/api/sign-api.ts b/samples/typescript/src/api/sign-api.ts new file mode 100644 index 0000000..2925e41 --- /dev/null +++ b/samples/typescript/src/api/sign-api.ts @@ -0,0 +1,280 @@ +/** + * Sign API client for Nitro Sign integrations (eSignature operations). + */ + +import { readFile } from 'fs/promises'; +import FormData from 'form-data'; +import { BaseOAuthClient } from './base-client.js'; + +/** + * Synchronous client for Nitro Sign API operations (eSignature/envelopes). + */ +export class SignAPIClient extends BaseOAuthClient { + /** + * Make authenticated API request returning JSON. + * @param method - HTTP method. + * @param endpoint - API endpoint path. + * @param jsonData - Optional JSON payload. + * @param params - Optional query parameters. + * @returns Response data. + */ + private async request( + method: string, + endpoint: string, + jsonData?: Record, + params?: Record + ): Promise { + const token = await this.getToken(); + + try { + const response = await this.client.request({ + method, + url: `${this.settings.platformBaseUrl}${endpoint}`, + headers: { + Authorization: `Bearer ${token}`, + }, + data: jsonData, + params, + }); + + return response.data; + } catch (error: any) { + // Try to get error details from response + if (error.response?.data) { + console.error(` ❌ API Error Response:`, error.response.data); + } else if (error.response?.text) { + console.error(` ❌ API Error (no JSON):`, error.response.text); + } + throw error; + } + } + + /** + * Make authenticated API request returning binary data. + * @param method - HTTP method. + * @param endpoint - API endpoint path. + * @param params - Optional query parameters. + * @returns Binary response data as Buffer. + */ + private async requestBytes( + method: string, + endpoint: string, + params?: Record + ): Promise { + const token = await this.getToken(); + + const response = await this.client.request({ + method, + url: `${this.settings.platformBaseUrl}${endpoint}`, + headers: { + Authorization: `Bearer ${token}`, + }, + params, + responseType: 'arraybuffer', + }); + + return Buffer.from(response.data); + } + + // ========== Envelope Management ========== + + /** + * List all envelopes with cursor-based pagination. + * @param pageAfter - Cursor token to get items after the last item from previous response. + * @param pageBefore - Cursor token to get items before the last item from previous response. + * @returns Object with 'items' (list of envelopes) and optional 'nextPage' (cursor token). + */ + async listEnvelopes(pageAfter?: string, pageBefore?: string): Promise { + const params: Record = {}; + if (pageAfter) { + params.pageAfter = pageAfter; + } else if (pageBefore) { + params.pageBefore = pageBefore; + } + + return this.request('GET', '/sign/envelopes', undefined, params); + } + + /** + * Create a new envelope. + * @param envelopeData - Envelope configuration including name, documents, participants, fields. + * @returns Created envelope with ID and status. + */ + async createEnvelope(envelopeData: Record): Promise { + return this.request('POST', '/sign/envelopes', envelopeData); + } + + /** + * Get envelope details by ID. + * @param envelopeId - UUID of the envelope. + * @returns Envelope details including status, participants, documents. + */ + async getEnvelope(envelopeId: string): Promise { + return this.request('GET', `/sign/envelopes/${envelopeId}`); + } + + /** + * Update an envelope. + * @param envelopeId - UUID of the envelope. + * @param updates - Fields to update (name, participants, etc.). + * @returns Updated envelope data. + */ + async updateEnvelope(envelopeId: string, updates: Record): Promise { + return this.request('PATCH', `/sign/envelopes/${envelopeId}`, updates); + } + + /** + * Delete an envelope by ID. + * @param envelopeId - UUID of the envelope. + */ + async deleteEnvelope(envelopeId: string): Promise { + const token = await this.getToken(); + + await this.client.delete(`${this.settings.platformBaseUrl}/sign/envelopes/${envelopeId}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + } + + // ========== Document Management ========== + + /** + * Upload a document to an envelope using form-data. + * @param envelopeId - ID of the envelope. + * @param filePath - Path to the PDF file to upload. + * @param documentName - Optional custom name for the document. + * @returns Created document with ID. + */ + async createDocument( + envelopeId: string, + filePath: string, + documentName?: string + ): Promise { + const token = await this.getToken(); + + if (!documentName) { + documentName = filePath.split('/').pop() || 'document.pdf'; + } + + // Read the binary content of the PDF file + const pdfBinary = await readFile(filePath); + + // Prepare metadata as JSON string + const metadata = JSON.stringify({ name: documentName }); + + // Prepare form-data with binary content + const form = new FormData(); + form.append('metadata', metadata, { + filename: 'metadata', + contentType: 'application/json', + }); + form.append('payload', pdfBinary, { + filename: filePath.split('/').pop() || 'document.pdf', + contentType: 'application/pdf', + }); + + const response = await this.client.post( + `${this.settings.platformBaseUrl}/sign/envelopes/${envelopeId}/documents`, + form, + { + headers: { + Authorization: `Bearer ${token}`, + ...form.getHeaders(), + }, + } + ); + + return response.data; + } + + // ========== Participant Management ========== + + /** + * Add a participant to an envelope. + * @param envelopeId - ID of the envelope. + * @param participantData - Participant configuration with role, email, name. + * @returns Created participant with ID. + */ + async createParticipant( + envelopeId: string, + participantData: Record + ): Promise { + return this.request( + 'POST', + `/sign/envelopes/${envelopeId}/participants`, + participantData + ); + } + + // ========== Field Management ========== + + /** + * Add a signature field to a document in an envelope. + * @param envelopeId - ID of the envelope. + * @param documentId - ID of the document. + * @param fieldData - Field configuration with boundingBox, participantID, type, page. + * @returns Created field with ID. + */ + async createField( + envelopeId: string, + documentId: string, + fieldData: Record + ): Promise { + return this.request( + 'POST', + `/sign/envelopes/${envelopeId}/documents/${documentId}/fields`, + fieldData + ); + } + + // ========== Envelope Actions ========== + + /** + * Send envelope to participants for signing. + * This transitions the envelope from 'drafted' to 'sent' status. + * @param envelopeId - UUID of the envelope. + * @returns Updated envelope with 'sent' status. + */ + async sendForSigning(envelopeId: string): Promise { + return this.request('POST', `/sign/envelopes/${envelopeId}:send-for-signing`); + } + + /** + * Cancel an envelope that was sent for signing. + * @param envelopeId - UUID of the envelope. + * @returns Envelope with 'cancelled' status. + */ + async cancelEnvelope(envelopeId: string): Promise { + return this.request('PUT', `/sign/envelopes/${envelopeId}/cancel`); + } + + /** + * Send reminder notifications to pending signers. + * @param envelopeId - UUID of the envelope. + * @returns Confirmation of reminder sent. + */ + async sendReminders(envelopeId: string): Promise { + return this.request('POST', `/sign/envelopes/${envelopeId}/reminders`); + } + + // ========== Document Downloads ========== + + /** + * Download the sealed (signed and completed) envelope. + * @param envelopeId - UUID of the envelope. + * @returns PDF bytes of the sealed document. + */ + async downloadSealedEnvelope(envelopeId: string): Promise { + return this.requestBytes('GET', `/sign/envelopes/${envelopeId}:download-sealed`); + } + + /** + * Download the original (unsigned) envelope documents. + * @param envelopeId - UUID of the envelope. + * @returns Original document bytes. + */ + async downloadOriginalEnvelope(envelopeId: string): Promise { + return this.requestBytes('GET', `/sign/envelopes/${envelopeId}:download-original`); + } +} diff --git a/samples/typescript/src/helpers/document-helpers.ts b/samples/typescript/src/helpers/document-helpers.ts new file mode 100644 index 0000000..db8d131 --- /dev/null +++ b/samples/typescript/src/helpers/document-helpers.ts @@ -0,0 +1,104 @@ +/** + * Common helper utilities for document processing scripts. + */ + +import { readdir, mkdir, stat } from 'fs/promises'; +import { join } from 'path'; + +/** + * Check if a filename matches any of the given glob patterns. + * @param filename - Name of the file to check. + * @param patterns - Array of glob patterns (e.g., ['*.docx', '*.pdf']). + * @returns True if filename matches any pattern. + */ +function matchesPattern(filename: string, patterns: string[]): boolean { + for (const pattern of patterns) { + // Convert simple glob pattern to regex + // This handles basic * wildcard matching + const regexPattern = pattern + .replace(/\./g, '\\.') + .replace(/\*/g, '.*'); + const regex = new RegExp(`^${regexPattern}$`, 'i'); + + if (regex.test(filename)) { + return true; + } + } + return false; +} + +/** + * Recursively find files matching patterns in a directory. + * @param dir - Directory to search. + * @param patterns - Array of glob patterns. + * @param files - Accumulator for found files (used internally). + * @returns Array of file paths. + */ +async function findMatchingFiles( + dir: string, + patterns: string[], + files: string[] = [] +): Promise { + const entries = await readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = join(dir, entry.name); + + if (entry.isDirectory()) { + // Recursively search subdirectories + await findMatchingFiles(fullPath, patterns, files); + } else if (entry.isFile() && matchesPattern(entry.name, patterns)) { + files.push(fullPath); + } + } + + return files; +} + +/** + * Validate input folder and setup output folder. Returns list of files to process. + * @param inputFolder - Path to the input directory containing files to process. + * @param outputFolder - Path to the output directory for processed files. + * @param filePatterns - List of glob patterns to match files (e.g., ['*.docx', '*.pdf']). + * If not provided, defaults to common Office document formats. + * @returns Array of file paths for files matching the patterns. + * @throws Error if input folder is invalid or no matching files are found. + */ +export async function validateAndSetup( + inputFolder: string, + outputFolder: string, + filePatterns?: string[] +): Promise { + // Default patterns for Office documents if none provided + const patterns = filePatterns || ['*.docx', '*.doc', '*.xlsx', '*.xls', '*.pptx', '*.ppt']; + + // Validate input folder exists + try { + const inputStat = await stat(inputFolder); + if (!inputStat.isDirectory()) { + console.error(`❌ Error: Invalid input folder: ${inputFolder}`); + process.exit(1); + } + } catch (error) { + console.error(`❌ Error: Invalid input folder: ${inputFolder}`); + process.exit(1); + } + + // Create output folder if needed + try { + await mkdir(outputFolder, { recursive: true }); + } catch (error) { + console.error(`❌ Error: Could not create output folder: ${outputFolder}`); + process.exit(1); + } + + // Find all matching files + const files = await findMatchingFiles(inputFolder, patterns); + + if (files.length === 0) { + console.error(`❌ No files matching patterns ${patterns.join(', ')} found in ${inputFolder}`); + process.exit(1); + } + + return files; +} diff --git a/samples/typescript/src/helpers/sign-helpers.ts b/samples/typescript/src/helpers/sign-helpers.ts new file mode 100644 index 0000000..b657dc9 --- /dev/null +++ b/samples/typescript/src/helpers/sign-helpers.ts @@ -0,0 +1,320 @@ +/** + * Sign API helper utilities for envelope operations. + */ + +import { readFile, readdir, mkdir, writeFile } from 'fs/promises'; +import { join } from 'path'; +import { parse } from 'csv-parse/sync'; +import AdmZip from 'adm-zip'; +import { SignAPIClient } from '../api/sign-api.js'; + +/** + * Convert employee name to folder-safe name. + * @param employeeName - Full name like "John Doe". + * @returns Folder-safe name like "john-doe". + */ +export function createEmployeeFolderName(employeeName: string): string { + return employeeName.toLowerCase().replace(/\s+/g, '-').replace(/\./g, ''); +} + +/** + * Load employee list from CSV file. + * @param csvPath - Path to CSV file with columns: name, email. + * @returns Array of employee objects with 'name' and 'email'. + * @throws Error if CSV format is invalid. + */ +export async function loadEmployeesFromCsv(csvPath: string): Promise> { + try { + const content = await readFile(csvPath, 'utf-8'); + const records = parse(content, { + columns: true, + skip_empty_lines: true, + trim: true, + }); + + const employees: Array<{ name: string; email: string }> = []; + + for (const row of records) { + const name = row.name?.trim(); + const email = row.email?.trim(); + + if (name && email && email.includes('@')) { + employees.push({ name, email }); + } + } + + if (employees.length === 0) { + throw new Error('No valid employees found in CSV'); + } + + return employees; + } catch (error: any) { + throw new Error(`Failed to load employees from CSV: ${error.message}`); + } +} + +/** + * Load all PDF files from the policies folder. + * @param policiesFolder - Path to folder containing policy PDFs. + * @returns Array of document objects with 'name', 'binary', and 'path'. + */ +export async function loadPolicyDocumentsFromFolder(policiesFolder: string): Promise> { + const files = await readdir(policiesFolder); + const pdfFiles = files.filter(f => f.toLowerCase().endsWith('.pdf')); + + if (pdfFiles.length === 0) { + throw new Error(`No PDF files found in ${policiesFolder}`); + } + + console.log(`📂 Found ${pdfFiles.length} policy document(s)\n`); + + const documents: Array<{ name: string; binary: Buffer; path: string }> = []; + + for (const filename of pdfFiles) { + const filePath = join(policiesFolder, filename); + const binary = await readFile(filePath); + + documents.push({ + name: filename, + binary, + path: filePath, + }); + } + + return documents; +} + +/** + * Upload documents to an existing envelope. + * @param signClient - Sign API client instance. + * @param envelopeId - ID of the envelope to upload to. + * @param documents - Array of document objects with 'name', 'binary', 'path'. + * @returns Array of document IDs. + */ +async function uploadDocumentsToEnvelope( + signClient: SignAPIClient, + envelopeId: string, + documents: Array<{ name: string; binary: Buffer; path: string }> +): Promise { + const documentIds: string[] = []; + + for (const doc of documents) { + // Use the createDocument method from SignAPIClient + // We need to save to a temp file since createDocument expects a file path + // For simplicity, we'll use the existing path + const document = await signClient.createDocument(envelopeId, doc.path, doc.name); + documentIds.push(document.ID); + } + + return documentIds; +} + +/** + * Create envelope and upload documents. + * @param signClient - Sign API client instance. + * @param documents - Array of document objects. + * @param employeeName - Full name of employee. + * @param employeeEmail - Email address of employee. + * @returns Tuple of [envelopeId, documentIds]. + */ +export async function createSignatureEnvelope( + signClient: SignAPIClient, + documents: Array<{ name: string; binary: Buffer; path: string }>, + employeeName: string, + employeeEmail: string +): Promise<[string, string[]]> { + // Create empty envelope + const envelopeData = { + name: `Company Policies - ${employeeName}`, + mode: 'parallel', + notification: { + subject: 'Please sign: Company Policies', + body: `Hello ${employeeName}, please review and sign the attached company policy documents.`, + }, + }; + + const envelope = await signClient.createEnvelope(envelopeData); + const envelopeId = envelope.ID; + + // Upload documents to envelope + const documentIds = await uploadDocumentsToEnvelope(signClient, envelopeId, documents); + + return [envelopeId, documentIds]; +} + +/** + * Add signature and date fields to all documents in envelope. + * @param signClient - Sign API client instance. + * @param envelopeId - ID of the envelope. + * @param documentIds - Array of document IDs to add fields to. + * @param participantId - ID of the participant who will sign. + */ +export async function addSignatureFieldsToDocuments( + signClient: SignAPIClient, + envelopeId: string, + documentIds: string[], + participantId: string +): Promise { + for (const docId of documentIds) { + // Add signature field (positioned bottom-left of page) + // Coordinates: [x, y, width, height] in points (72 points = 1 inch) + // Standard letter page: 612 x 792 points + const signatureFieldData = { + participantID: participantId, + type: 'signature', + label: 'Your Signature', + page: 1, + boundingBox: [50, 100, 200, 50], // Bottom area, safe coordinates + required: true, + }; + await signClient.createField(envelopeId, docId, signatureFieldData); + + // Add date field (positioned to the right of signature) + const dateFieldData = { + participantID: participantId, + type: 'date', + label: 'Date Signed', + page: 1, + boundingBox: [270, 100, 150, 50], // Next to signature, safe coordinates + required: true, + format: 'MM/DD/YYYY', + }; + await signClient.createField(envelopeId, docId, dateFieldData); + } +} + +/** + * Send envelope and monitor until signed or timeout. + * @param signClient - Sign API client instance. + * @param envelopeId - ID of envelope to send. + * @param email - Email address of recipient. + * @param timeoutMinutes - Maximum time to wait (default: 60). + * @returns Final status: 'sealed', 'cancelled', 'timeout', or 'error'. + */ +export async function sendAndMonitorEnvelope( + signClient: SignAPIClient, + envelopeId: string, + email: string, + timeoutMinutes: number = 60 +): Promise { + // Send envelope + await signClient.sendForSigning(envelopeId); + + // Log send time and status + const sendTime = new Date().toISOString().replace('T', ' ').substring(0, 19); + console.log(` ✅ Sent at: ${sendTime}`); + console.log(` 📧 Email sent to: ${email}`); + console.log(` 🔗 Envelope ID: ${envelopeId}`); + + // Check initial status + const envelope = await signClient.getEnvelope(envelopeId); + console.log(` 📊 Status: ${envelope.status}`); + + // Monitor for completion + console.log(' ⏳ Waiting for signature...'); + console.log(` ⏱️ Checking every 30 seconds (timeout: ${timeoutMinutes} minutes)`); + + const status = await monitorEnvelope(signClient, envelopeId, timeoutMinutes); + + // Log final status + const completionTime = new Date().toISOString().replace('T', ' ').substring(0, 19); + console.log(` 📊 Final status: ${status}`); + console.log(` 🕐 Completed at: ${completionTime}`); + + return status; +} + +/** + * Monitor envelope until signed, cancelled, or timeout. + * @param signClient - Sign API client instance. + * @param envelopeId - ID of envelope to monitor. + * @param timeoutMinutes - Maximum time to wait (default: 60). + * @returns Final status: 'sealed', 'cancelled', 'timeout', or 'error'. + */ +export async function monitorEnvelope( + signClient: SignAPIClient, + envelopeId: string, + timeoutMinutes: number = 60 +): Promise { + const checkInterval = 30; // seconds + const maxChecks = Math.floor((timeoutMinutes * 60) / checkInterval); + + for (let i = 0; i < maxChecks; i++) { + try { + const envelope = await signClient.getEnvelope(envelopeId); + const status = envelope.status; + + if (status === 'sealed') { + return 'sealed'; + } + if (['cancelled', 'rejected', 'deleted'].includes(status)) { + return 'cancelled'; + } + + if (i < maxChecks - 1) { + await new Promise(resolve => setTimeout(resolve, checkInterval * 1000)); + } + } catch (error: any) { + console.error(` ⚠️ Error checking status: ${error.message}`); + return 'error'; + } + } + + return 'timeout'; +} + +/** + * Download sealed envelope and extract signed documents. + * @param signClient - Sign API client instance. + * @param envelopeId - ID of sealed envelope. + * @param outputFolder - Employee-specific output folder. + * @param documentName - Name for the saved file (should end with .zip). + * @returns Path to extracted documents folder. + */ +export async function downloadSignedDocument( + signClient: SignAPIClient, + envelopeId: string, + outputFolder: string, + documentName: string +): Promise { + // Download sealed envelope (returns ZIP file) + const zipBytes = await signClient.downloadSealedEnvelope(envelopeId); + + // Save as temporary ZIP file + if (!documentName.endsWith('.zip')) { + documentName = documentName.replace('.pdf', '.zip'); + } + + const tempZipPath = join(outputFolder, documentName); + await writeFile(tempZipPath, zipBytes); + + // Extract the ZIP contents + const extractFolder = join(outputFolder, 'signed-documents'); + await mkdir(extractFolder, { recursive: true }); + + const zip = new AdmZip(tempZipPath); + zip.extractAllTo(extractFolder, true); + + // Delete the ZIP file after extraction + const fs = await import('fs/promises'); + await fs.unlink(tempZipPath); + + // Save envelope metadata + const envelope = await signClient.getEnvelope(envelopeId); + const jsonPath = join(outputFolder, 'envelope-info.json'); + await writeFile(jsonPath, JSON.stringify(envelope, null, 2)); + + console.log(` 💾 Extracted to: ${extractFolder}`); + + return extractFolder; +} + +/** + * Log a processing step with consistent formatting. + * @param message - The message to log. + * @param emoji - Optional emoji to prepend (default: none). + */ +export function logStep(message: string, emoji?: string): void { + const prefix = emoji ? `${emoji} ` : ' '; + console.log(`${prefix}${message}`); +} diff --git a/samples/typescript/src/scripts/batch-process.ts b/samples/typescript/src/scripts/batch-process.ts new file mode 100644 index 0000000..49351fe --- /dev/null +++ b/samples/typescript/src/scripts/batch-process.ts @@ -0,0 +1,105 @@ +#!/usr/bin/env node +/** + 📁 BATCH DOCUMENT CONVERSION +============================= + +The script exemplifies a typical workflow for document format standardization. +As a IT administrator, it's essential to convert +large collections of documents into standardized formats for archival, compliance, +or system integration purposes. Manually converting individual files through desktop +applications is time-consuming and impractical for large document sets, often leading +to inconsistent results and wasted effort. + +This workflow automates bulk document conversion. The script processes files +individually - for every document matching the specified pattern in the input folder, +it converts the file to the target format (PDF, DOCX, XLSX, PNG, JPG, etc.) using +high-fidelity conversion algorithms. Each converted file is saved to the output +folder with the same base name but the new format extension, resulting in a +complete batch of standardized documents. + +BATCH CONVERSION FEATURES: + ✓ Multiple format support (PDF, DOCX, XLSX, PNG, JPG, etc.) + ✓ Flexible file pattern matching (*.docx, *.xlsx, *.pptx, *.pdf.) + +USAGE: + npm run batch -- [pattern] + +EXAMPLES: + npm run batch -- ../../test_files/test-batch ./output pdf "*.docx" + npm run batch -- ./documents ./converted png "*" + */ + +import { program } from 'commander'; +import { writeFile } from 'fs/promises'; +import { join, parse } from 'path'; +import { PlatformAPIClient, OutputFormat } from '../api/platform-api.js'; +import { validateAndSetup } from '../helpers/document-helpers.js'; + +program + .name('batch-process') + .description('Process multiple documents in batch, converting them to a specified format') + .argument('', 'Input folder containing documents to convert') + .argument('', 'Output folder for converted documents') + .argument('', 'Target format for conversion (pdf, docx, xlsx, pptx)') + .argument('[pattern]', 'File pattern to match (e.g., "*.docx", "*.pdf", "*")', '*') + .action(async (inputFolder: string, outputFolder: string, format: string, pattern: string) => { + try { + // Validate format + const validFormats = Object.values(OutputFormat); + if (!validFormats.includes(format.toLowerCase() as OutputFormat)) { + console.error(`❌ Error: Invalid format '${format}'. Valid formats: ${validFormats.join(', ')}`); + process.exit(1); + } + + const toFormat = format.toLowerCase() as OutputFormat; + + // Validate and setup with custom pattern + const files = await validateAndSetup(inputFolder, outputFolder, [pattern]); + console.log(`📋 Found ${files.length} file(s) matching '${pattern}'\n`); + + // Initialize API client (loads credentials from .env) + const client = new PlatformAPIClient(); + + // Process each document + let successCount = 0; + let failedCount = 0; + + for (let i = 0; i < files.length; i++) { + const filePath = files[i]; + const fileName = filePath.split('/').pop() || filePath; + const fileBaseName = parse(filePath).name; + + console.log(`[${i + 1}/${files.length}] Processing: ${fileName}`); + + try { + // Convert to target format + console.log(` 🔄 Converting to ${toFormat.toUpperCase()}...`); + const converted = await client.convert(filePath, toFormat); + + // Save converted file + const outputFile = join(outputFolder, `${fileBaseName}.${toFormat}`); + await writeFile(outputFile, converted); + + console.log(` ✅ Converted: ${parse(outputFile).base}\n`); + successCount++; + } catch (error: any) { + console.log(` ❌ FAILED: ${error.message}\n`); + failedCount++; + } + } + + // Display summary + console.log('='.repeat(60)); + console.log(`✅ ${successCount} file(s) converted to ${toFormat.toUpperCase()}`); + if (failedCount > 0) { + console.log(`⚠️ ${failedCount} file(s) FAILED to convert!`); + } + console.log(`📂 Output: ${outputFolder}`); + console.log('='.repeat(60)); + } catch (error: any) { + console.error(`❌ Batch processing FAILED: ${error.message}`); + process.exit(1); + } + }); + +program.parse(); diff --git a/samples/typescript/src/scripts/bulk-password-protect.ts b/samples/typescript/src/scripts/bulk-password-protect.ts new file mode 100644 index 0000000..a4f468a --- /dev/null +++ b/samples/typescript/src/scripts/bulk-password-protect.ts @@ -0,0 +1,101 @@ +#!/usr/bin/env node +/** + 🔐 BULK PASSWORD PROTECTION +============================ + +The script exemplifies a typical workflow for securing confidential documents. +As a security professional, it's essential to protect sensitive +documents with passwords before distributing them to authorized personnel, storing +them in shared drives, or archiving them for compliance purposes. Manually setting +passwords on individual files is tedious and inconsistent, leading to weak passwords +or missed files that remain unprotected. + +This workflow automates secure document protection. The script processes each PDF +file individually - for every document in the input folder, it applies robust +password encryption using a consistent password across all files. Each protected +file is saved to the output folder with the same filename, ensuring that the entire +batch of documents maintains uniform security standards. The result is a complete +set of password-protected PDFs ready for secure distribution or storage. + +DOCUMENT SECURITY STANDARDS: + ✓ Password encryption (AES-256) + ✓ Batch processing (entire folders) + ✓ Consistent security (uniform password policy) + +USAGE: + npm run bulk-password -- + +EXAMPLE: + npm run bulk-password -- ../../test_files/test-pdfs ./output MySecureP@ss123 + */ + +import { program } from 'commander'; +import { writeFile } from 'fs/promises'; +import { join } from 'path'; +import { PlatformAPIClient } from '../api/platform-api.js'; +import { validateAndSetup } from '../helpers/document-helpers.js'; + +program + .name('bulk-password-protect') + .description('Apply password protection to all PDF files in a directory') + .argument('', 'Input folder containing PDF documents') + .argument('', 'Output folder for protected PDFs') + .argument('', 'Password for protection (min 6 characters)') + .action(async (inputFolder: string, outputFolder: string, password: string) => { + try { + // Validate password strength + if (password.length < 6) { + console.error('❌ Error: Password must be at least 6 characters long'); + process.exit(1); + } + + // Validate and setup (only process PDF files) + const files = await validateAndSetup(inputFolder, outputFolder, ['*.pdf']); + console.log(`📋 Found ${files.length} PDF document(s) to protect\n`); + + // Initialize API client (loads credentials from .env) + const client = new PlatformAPIClient(); + + // Process each document + let successCount = 0; + let failedCount = 0; + + for (let i = 0; i < files.length; i++) { + const pdfFile = files[i]; + const fileName = pdfFile.split('/').pop() || pdfFile; + + console.log(`[${i + 1}/${files.length}] Processing: ${fileName}`); + + try { + // Apply password protection + console.log(' 🔐 Applying password protection...'); + const protectedPdf = await client.passwordProtect(pdfFile, password); + + // Save protected PDF + const outputFile = join(outputFolder, fileName); + await writeFile(outputFile, protectedPdf); + + console.log(` ✅ Protected: ${fileName}\n`); + successCount++; + } catch (error: any) { + console.log(` ❌ FAILED: ${error.message}\n`); + failedCount++; + } + } + + // Display summary + console.log('='.repeat(60)); + console.log(`✅ ${successCount} document(s) password protected`); + if (failedCount > 0) { + console.log(`⚠️ ${failedCount} document(s) FAILED - remain unprotected!`); + } + console.log(`📂 Output: ${outputFolder}`); + console.log(`🔑 Password: ${'*'.repeat(password.length)} (${password.length} characters)`); + console.log('='.repeat(60)); + } catch (error: any) { + console.error(`❌ Bulk password protection FAILED: ${error.message}`); + process.exit(1); + } + }); + +program.parse(); diff --git a/samples/typescript/src/scripts/convert-cli.ts b/samples/typescript/src/scripts/convert-cli.ts new file mode 100644 index 0000000..7234cc2 --- /dev/null +++ b/samples/typescript/src/scripts/convert-cli.ts @@ -0,0 +1,89 @@ +#!/usr/bin/env node +/** +🔄 SINGLE DOCUMENT CONVERSION +============================== + +The script exemplifies a typical workflow for quick document format conversion. +As a business professional, you often need to convert individual +documents between formats for sharing, presentations, or compatibility requirements. +Whether converting a Word document to PDF for distribution, an Excel spreadsheet to +CSV for data processing, or a presentation to images for web display, manual +conversion through multiple applications is inefficient. + +This workflow provides instant document conversion. The script takes a single input +file and converts it to the specified output format using professional-grade +conversion algorithms. The result is a high-quality converted file that preserves +formatting, structure, and content fidelity, ready for immediate use. + +CONVERSION FEATURES: + ✓ Multiple format support (PDF, DOCX, XLSX, PNG, etc.) + ✓ High-fidelity conversion (preserves formatting) + +USAGE: + npm run convert -- + +EXAMPLES: + npm run convert -- document.docx document.pdf pdf + npm run convert -- presentation.pptx slide.pdf pdf + npm run convert -- spreadsheet.xlsx data.pdf pdf + */ + +import { program } from 'commander'; +import { stat, mkdir, writeFile } from 'fs/promises'; +import { dirname } from 'path'; +import { PlatformAPIClient, OutputFormat } from '../api/platform-api.js'; + +program + .name('convert-cli') + .description('Convert a document from one format to another using the Platform API') + .argument('', 'Input file to convert') + .argument('', 'Output file path') + .argument('', 'Target format for conversion (pdf, docx, xlsx, pptx, png)') + .action(async (inputFile: string, outputFile: string, format: string) => { + try { + // Validate format + const validFormats = Object.values(OutputFormat); + if (!validFormats.includes(format.toLowerCase() as OutputFormat)) { + console.error(`❌ Error: Invalid format '${format}'. Valid formats: ${validFormats.join(', ')}`); + process.exit(1); + } + + const toFormat = format.toLowerCase() as OutputFormat; + + // Validate input file exists + try { + await stat(inputFile); + } catch (error) { + console.error(`❌ Error: Input file not found: ${inputFile}`); + process.exit(1); + } + + // Create output directory if needed + await mkdir(dirname(outputFile), { recursive: true }); + + // Initialize API client (loads credentials from .env) + const client = new PlatformAPIClient(); + + // Get input file size + const inputStat = await stat(inputFile); + const inputFileName = inputFile.split('/').pop() || inputFile; + + // Convert document + console.log(`🔄 Converting ${inputFileName} to ${toFormat.toUpperCase()}...`); + const converted = await client.convert(inputFile, toFormat); + + // Save converted file + await writeFile(outputFile, converted); + + // Display success message + console.log('✅ Conversion successful!'); + console.log(`📄 Input: ${inputFileName} (${inputStat.size.toLocaleString()} bytes)`); + console.log(`📄 Output: ${outputFile.split('/').pop()} (${converted.length.toLocaleString()} bytes)`); + console.log(`📂 Saved to: ${outputFile}`); + } catch (error: any) { + console.error(`❌ Conversion FAILED: ${error.message}`); + process.exit(1); + } + }); + +program.parse(); diff --git a/samples/typescript/src/scripts/employee-policy-onboarding.ts b/samples/typescript/src/scripts/employee-policy-onboarding.ts new file mode 100644 index 0000000..fb598e0 --- /dev/null +++ b/samples/typescript/src/scripts/employee-policy-onboarding.ts @@ -0,0 +1,254 @@ +#!/usr/bin/env node +/** +📝 EMPLOYEE POLICY ONBOARDING +============================== + +This script exemplifies a typical HR onboarding workflow for new employees. +As an HR professional, it's necessary to ensure all new hires review and sign +required company policies before their start date. Manual distribution and +tracking of signatures is time-consuming and error-prone, especially when +onboarding multiple employees simultaneously. + +This workflow automates policy distribution and signature collection. The script +processes each new employee individually - for every person in the CSV file, it +creates a signature envelope containing all company policy documents, sends it +via email with signature fields pre-configured, monitors the signing status, and +automatically downloads the signed documents once completed. Each employee's +signed policies are organized in their own folder, creating an audit-ready +archive of onboarding documentation. + + +NOTE: To run this script and see the complete workflow, you must provide a CSV file +with valid employee names and email addresses. The script will send actual signature +requests to these email addresses and wait for them to be signed. + +EMPLOYEE CSV FORMAT: + name,email + John Doe,john.doe@company.com + Jane Smith,jane.smith@company.com + Bob Johnson,bob.johnson@company.com + +USAGE: + npm run employee-policy -- + + EXAMPLE: + npm run employee-policy -- ./policies ./new_hires_jan2025.csv + + OUTPUT STRUCTURE: + output/ + ├── john-doe/ + │ ├── signed-documents/ + │ ├── code-of-conduct-signed.pdf + │ ├── confidentiality-agreement-signed.pdf + │ └── audit-trail.pdf + ├── jane-smith/ + │ ├── signed-documents/ + */ + +import { program } from 'commander'; +import { stat, mkdir } from 'fs/promises'; +import { join } from 'path'; +import { SignAPIClient } from '../api/sign-api.js'; +import { + loadEmployeesFromCsv, + loadPolicyDocumentsFromFolder, + createEmployeeFolderName, + createSignatureEnvelope, + addSignatureFieldsToDocuments, + sendAndMonitorEnvelope, + downloadSignedDocument, + logStep, +} from '../helpers/sign-helpers.js'; + +/** + * Custom error for envelope not signed in time. + */ +class EnvelopeNotSignedError extends Error { + constructor(message: string) { + super(message); + this.name = 'EnvelopeNotSignedError'; + } +} + +/** + * Validate inputs and load data. + * @param policiesFolder - Path to folder containing policy PDFs. + * @param employeesCsv - Path to CSV file with employee data. + * @returns Tuple of [outputFolder, employees, documents]. + */ +async function validateAndSetupInputs( + policiesFolder: string, + employeesCsv: string +): Promise<[string, Array<{ name: string; email: string }>, Array<{ name: string; binary: Buffer; path: string }>]> { + // Validate inputs + try { + const policiesStat = await stat(policiesFolder); + if (!policiesStat.isDirectory()) { + console.error(`❌ Policies folder not found: ${policiesFolder}`); + process.exit(1); + } + } catch (error) { + console.error(`❌ Policies folder not found: ${policiesFolder}`); + process.exit(1); + } + + try { + await stat(employeesCsv); + } catch (error) { + console.error(`❌ Employees CSV not found: ${employeesCsv}`); + process.exit(1); + } + + // Display header + const outputFolder = 'output'; + console.log('='.repeat(60)); + console.log('📝 SEND POLICIES TO EMPLOYEES'); + console.log('='.repeat(60)); + console.log(`Policies: ${policiesFolder}`); + console.log(`Employees: ${employeesCsv}`); + console.log(`Output: ${outputFolder}`); + console.log('='.repeat(60)); + console.log(); + + // Load employees and documents + const employees = await loadEmployeesFromCsv(employeesCsv); + console.log(`👥 Found ${employees.length} employee(s)\n`); + + const documents = await loadPolicyDocumentsFromFolder(policiesFolder); + + // Create output folder + await mkdir(outputFolder, { recursive: true }); + + return [outputFolder, employees, documents]; +} + +/** + * Process onboarding workflow for a single employee. + * @param signClient - Sign API client instance. + * @param employee - Employee data object with 'name' and 'email'. + * @param documents - Array of policy documents to send. + * @param outputFolder - Base output folder for signed documents. + * @param employeeNum - Current employee number (for display). + * @param totalEmployees - Total number of employees (for display). + */ +async function processEmployeeOnboarding( + signClient: SignAPIClient, + employee: { name: string; email: string }, + documents: Array<{ name: string; binary: Buffer; path: string }>, + outputFolder: string, + employeeNum: number, + totalEmployees: number +): Promise { + const { name, email } = employee; + + console.log(`\n[${employeeNum}/${totalEmployees}] ${name}`); + + // Create employee-specific output folder + const employeeFolder = join(outputFolder, createEmployeeFolderName(name)); + await mkdir(employeeFolder, { recursive: true }); + + // Create envelope and upload documents + logStep('📝 Creating envelope...'); + const [envelopeId, documentIds] = await createSignatureEnvelope( + signClient, + documents, + name, + email + ); + + // Add participant (signer) + logStep('👤 Adding signer...'); + const participantResponse = await signClient.createParticipant(envelopeId, { + email, + role: 'signer', + name, + }); + const participantId = participantResponse.ID; + + // Add signature fields to all documents + logStep('✍️ Adding fields...'); + await addSignatureFieldsToDocuments(signClient, envelopeId, documentIds, participantId); + + // Send and monitor envelope + logStep('📤 Sending...'); + const status = await sendAndMonitorEnvelope(signClient, envelopeId, email, 60); + + if (status !== 'sealed') { + throw new EnvelopeNotSignedError(`Envelope not signed: ${status}`); + } + + // Download signed documents + logStep('📥 Downloading...'); + await downloadSignedDocument(signClient, envelopeId, employeeFolder, 'signed-policies.zip'); + + console.log(' ✅ Completed\n'); +} + +program + .name('employee-policy-onboarding') + .description('Send company policy documents to employees for electronic signature via Sign API') + .argument('', 'Folder containing policy PDF documents') + .argument('', 'CSV file with employee data (name,email columns)') + .action(async (policiesFolder: string, employeesCsv: string) => { + try { + // Validate inputs and load data + const [outputFolder, employees, documents] = await validateAndSetupInputs( + policiesFolder, + employeesCsv + ); + + // Initialize Sign API client + const signClient = new SignAPIClient(); + + // Process each employee + console.log('='.repeat(60)); + console.log(`📤 PROCESSING ${employees.length} EMPLOYEE(S)`); + console.log('='.repeat(60)); + + let successCount = 0; + let failedCount = 0; + + for (let i = 0; i < employees.length; i++) { + const employee = employees[i]; + + try { + await processEmployeeOnboarding( + signClient, + employee, + documents, + outputFolder, + i + 1, + employees.length + ); + successCount++; + } catch (error: any) { + console.log(` ❌ FAILED: ${error.message}\n`); + failedCount++; + } + } + + // Display summary + console.log('='.repeat(60)); + console.log(`✅ ${successCount} employee(s) completed`); + if (failedCount > 0) { + console.log(`❌ ${failedCount} failed`); + } + console.log(`📂 Output: ${outputFolder}`); + console.log('='.repeat(60)); + } catch (error: any) { + if (error.message === 'SIGINT') { + console.log('\n\n⚠️ Interrupted by user'); + process.exit(1); + } + console.error(`\n❌ Error: ${error.message}`); + process.exit(1); + } + }); + +// Handle SIGINT (Ctrl+C) +process.on('SIGINT', () => { + console.log('\n\n⚠️ Interrupted by user'); + process.exit(1); +}); + +program.parse(); diff --git a/samples/typescript/src/scripts/extract-data.ts b/samples/typescript/src/scripts/extract-data.ts new file mode 100644 index 0000000..69922f1 --- /dev/null +++ b/samples/typescript/src/scripts/extract-data.ts @@ -0,0 +1,117 @@ +#!/usr/bin/env node +/** +📊 DOCUMENT DATA EXTRACTION +============================ + +The script exemplifies a typical workflow for intelligent document data extraction. +As a data analyst, you need to extract structured +data from PDF documents - whether form fields from applications, surveys, and +questionnaires, or table data from reports, invoices, and financial statements. +Manual data entry is error-prone and time-consuming, especially when processing +hundreds of documents for analysis or database import. + +This workflow automates data extraction using AI-powered document understanding. +The script analyzes PDF documents and intelligently identifies and extracts either +form fields (with field names and values) or table structures (with rows, columns, +and cell contents). The extracted data is saved as structured JSON, ready for +immediate integration with databases, spreadsheets, or analytics pipelines. + +DATA EXTRACTION FEATURES: + ✓ AI-powered form field extraction + ✓ Intelligent table detection and extraction + ✓ Structured JSON output format + ✓ High accuracy recognition + + +USAGE: + npm run extract -- + +MODES: + forms - Extract form fields (name-value pairs) + tables - Extract table data (rows and columns) + +EXAMPLES: + npm run extract -- forms application.pdf data.json + npm run extract -- tables invoice.pdf tables.json + */ + +import { program } from 'commander'; +import { stat, mkdir, writeFile } from 'fs/promises'; +import { dirname } from 'path'; +import { PlatformAPIClient } from '../api/platform-api.js'; + +program + .name('extract-data') + .description('Extract structured data (forms or tables) from PDF documents') + .argument('', "Extraction mode: 'forms' or 'tables'") + .argument('', 'Input PDF file') + .argument('', 'Output JSON file') + .action(async (mode: string, inputPdf: string, outputJson: string) => { + try { + // Normalize mode to lowercase + mode = mode.toLowerCase(); + + // Validate mode + if (!['forms', 'tables'].includes(mode)) { + console.error("❌ Error: Mode must be 'forms' or 'tables'"); + process.exit(1); + } + + // Validate input file exists + try { + await stat(inputPdf); + } catch (error) { + console.error(`❌ Error: Input file not found: ${inputPdf}`); + process.exit(1); + } + + // Validate input is a PDF + if (!inputPdf.toLowerCase().endsWith('.pdf')) { + console.error('❌ Error: Input must be a PDF file'); + process.exit(1); + } + + // Create output directory if needed + await mkdir(dirname(outputJson), { recursive: true }); + + // Initialize API client (loads credentials from .env) + const client = new PlatformAPIClient(); + + // Extract data based on mode + let data: any; + let dataType: string; + + if (mode === 'forms') { + console.log(`📋 Extracting form fields from ${inputPdf.split('/').pop()}...`); + data = await client.extractForms(inputPdf); + dataType = 'form fields'; + } else { + // mode === 'tables' + console.log(`📊 Extracting table data from ${inputPdf.split('/').pop()}...`); + data = await client.extractTables(inputPdf); + dataType = 'tables'; + } + + // Count extracted items + const result = data.result || {}; + const itemCount = + mode === 'forms' + ? (result.fields || []).length + : (result.tables || []).length; + + // Save extracted data as JSON + await writeFile(outputJson, JSON.stringify(data, null, 2), 'utf-8'); + + // Display success message + console.log('✅ Extraction successful!'); + console.log(`📊 Extracted: ${itemCount} ${dataType}`); + console.log(`📄 Input: ${inputPdf.split('/').pop()}`); + console.log(`📄 Output: ${outputJson.split('/').pop()}`); + console.log(`📂 Saved to: ${outputJson}`); + } catch (error: any) { + console.error(`❌ Extraction FAILED: ${error.message}`); + process.exit(1); + } + }); + +program.parse(); diff --git a/samples/typescript/src/scripts/prepare-pdf-for-distribution.ts b/samples/typescript/src/scripts/prepare-pdf-for-distribution.ts new file mode 100644 index 0000000..6f9c76d --- /dev/null +++ b/samples/typescript/src/scripts/prepare-pdf-for-distribution.ts @@ -0,0 +1,123 @@ +#!/usr/bin/env node +/** +🔒 PREPARE PDF FOR DISTRIBUTION +================================ + +The script exemplifies a typical workflow of marketing brochure distribution. +As a marketing professional, it's necessary to share company brochures externally +while ensuring they comply with corporate distribution standards. Word document +properties can expose internal information such as author names, template paths, +revision history, and company file structures that should remain confidential. + +This workflow automates compliant document preparation. The script processes each +file individually - for every brochure in the input folder, it converts the Word +document into PDF format, then compresses the file to reduce size and optimize +transmission, and finally removes all metadata properties to ensure privacy and +confidentiality. Each processed file is saved to the output folder, resulting in +distribution-ready brochures. + +COMPANY DISTRIBUTION STANDARDS: + ✓ PDF format (prevents editing) + ✓ Compressed (optimized file size) + ✓ Properties removed (no metadata exposure) + ⏳ Annotations removed (feature in development) + ⏳ Accessibility enabled (feature in development) +USAGE: + npm run prepare-pdf -- + +EXAMPLE: + npm run prepare-pdf -- ../../test_files/test-batch ./output + */ + +import { program } from 'commander'; +import { writeFile, unlink } from 'fs/promises'; +import { join, parse } from 'path'; +import { PlatformAPIClient, OutputFormat } from '../api/platform-api.js'; +import { validateAndSetup } from '../helpers/document-helpers.js'; + +// Configuration: Properties to remove from PDFs +const PROPERTIES_TO_REMOVE = ['title', 'author', 'subject', 'keywords', 'creator', 'producer']; + +program + .name('prepare-pdf-for-distribution') + .description('Prepare documents for distribution by converting to PDF and removing metadata') + .argument('', 'Input folder containing documents') + .argument('', 'Output folder for prepared PDFs') + .action(async (inputFolder: string, outputFolder: string) => { + try { + // Validate and setup + const files = await validateAndSetup(inputFolder, outputFolder); + console.log(`📋 Found ${files.length} document(s) to process\n`); + + // Initialize API client (loads credentials from .env) + const client = new PlatformAPIClient(); + + // Process each document + let successCount = 0; + let failedCount = 0; + + for (let i = 0; i < files.length; i++) { + const doc = files[i]; + const docName = doc.split('/').pop() || doc; + const docBaseName = parse(doc).name; + + console.log(`[${i + 1}/${files.length}] Processing: ${docName}`); + + let tempPdf: string | null = null; + + try { + // Step 1: Convert to PDF + console.log(' 🔐 Converting to PDF...'); + const pdfBytes = await client.convert(doc, OutputFormat.PDF); + + tempPdf = join(outputFolder, `${docBaseName}_temp.pdf`); + await writeFile(tempPdf, pdfBytes); + + // Step 2: Compress PDF + console.log(' 📦 Compressing...'); + const compressedPdf = await client.compress(tempPdf, 2); + await writeFile(tempPdf, compressedPdf); + + // Step 3: Remove metadata properties + console.log(' 🔒 Removing metadata...'); + const propertiesToClear: Record = {}; + for (const prop of PROPERTIES_TO_REMOVE) { + propertiesToClear[prop] = ''; + } + const cleanPdf = await client.setProperties(tempPdf, propertiesToClear); + + // Save final PDF + const finalPdf = join(outputFolder, `${docBaseName}.pdf`); + await writeFile(finalPdf, cleanPdf); + await unlink(tempPdf); + + console.log(` ✅ Secured: ${parse(finalPdf).base}\n`); + successCount++; + } catch (error: any) { + console.log(` ❌ FAILED: ${error.message}\n`); + failedCount++; + if (tempPdf) { + try { + await unlink(tempPdf); + } catch (e) { + // Ignore cleanup errors + } + } + } + } + + // Display summary + console.log('='.repeat(60)); + console.log(`✅ ${successCount} document(s) secured`); + if (failedCount > 0) { + console.log(`⚠️ ${failedCount} document(s) FAILED - do NOT distribute!`); + } + console.log(`📂 Output: ${outputFolder}`); + console.log('='.repeat(60)); + } catch (error: any) { + console.error(`❌ PDF preparation FAILED: ${error.message}`); + process.exit(1); + } + }); + +program.parse(); diff --git a/samples/typescript/src/scripts/quickstart.ts b/samples/typescript/src/scripts/quickstart.ts new file mode 100644 index 0000000..bb112d1 --- /dev/null +++ b/samples/typescript/src/scripts/quickstart.ts @@ -0,0 +1,86 @@ +#!/usr/bin/env node +/** + * Quickstart script to test Nitro Platform API authentication and connection. + */ + +import dotenv from 'dotenv'; +import axios from 'axios'; + +dotenv.config(); + +const BASE_URL = process.env.PLATFORM_BASE_URL || 'https://api.gonitro.dev'; +const CLIENT_ID = process.env.PLATFORM_CLIENT_ID; +const CLIENT_SECRET = process.env.PLATFORM_CLIENT_SECRET; + +/** + * Get OAuth2 access token using client credentials. + * @returns Access token string. + */ +async function getAccessToken(): Promise { + const url = `${BASE_URL}/oauth/token`; + const data = { + clientID: CLIENT_ID, + clientSecret: CLIENT_SECRET, + }; + + const response = await axios.post(url, data); + return response.data.accessToken; +} + +/** + * Test API connection with a simple request. + * @param token - OAuth2 access token. + * @returns True if connection is successful. + */ +async function testConnection(token: string): Promise { + const url = `${BASE_URL}/jobs/test-job-id/status`; + const headers = { Authorization: `Bearer ${token}` }; + + try { + await axios.get(url, { headers }); + return true; + } catch (error: any) { + // 404 is expected for non-existent job, but proves auth works + if (error.response?.status === 404) { + console.log('✅ Authentication successful (404 expected for test job ID)'); + return true; + } + throw error; + } +} + +/** + * Main function to test API authentication and connection. + */ +async function main(): Promise { + if (!CLIENT_ID || !CLIENT_SECRET) { + console.log('❌ Missing credentials!'); + console.log('To get your credentials:'); + console.log('1. Go to https://admin.gonitro.com'); + console.log('2. Navigate to Settings → API'); + console.log("3. Click 'Create Application'"); + console.log('4. Name your application and save the Client ID and Client Secret'); + console.log('5. Set environment variables:'); + console.log(' export PLATFORM_CLIENT_ID='); + console.log(' export PLATFORM_CLIENT_SECRET='); + return; + } + + try { + console.log('🔐 Getting access token...'); + const token = await getAccessToken(); + console.log('✅ Token obtained successfully'); + + console.log('🧪 Testing API connection...'); + await testConnection(token); + console.log('✅ API connection successful'); + + console.log('\n🎉 Setup complete! You can now use the Platform API.'); + console.log('📖 See https://developers.gonitro.com/docs for API documentation'); + } catch (error: any) { + console.error(`❌ Error: ${error.message}`); + process.exit(1); + } +} + +main(); diff --git a/samples/typescript/src/scripts/redact-by-keyword.ts b/samples/typescript/src/scripts/redact-by-keyword.ts new file mode 100644 index 0000000..e23988a --- /dev/null +++ b/samples/typescript/src/scripts/redact-by-keyword.ts @@ -0,0 +1,114 @@ +#!/usr/bin/env node +/** +🔍 KEYWORD-BASED REDACTION +=========================== + +The script exemplifies a typical workflow for targeted content redaction. +As a compliance officer, you need to redact specific +sensitive terms from documents before external sharing or public disclosure. +Whether removing client names, project codenames, financial figures, or +proprietary terminology, manually searching through pages and applying redactions +is tedious and risks missing instances, potentially exposing confidential information. + +This workflow automates keyword-based redaction. The script searches the entire +PDF document for all specified keywords and phrases, identifies their exact +locations across all pages, then automatically applies permanent redactions to +remove them. Multiple keywords can be processed in a single pass, ensuring +comprehensive coverage. The result is a thoroughly redacted document ready for +safe distribution. + +KEYWORD REDACTION FEATURES: + ✓ Multi-keyword search (process multiple terms) + ✓ Whole document scanning (all pages) + ✓ Exact location detection + ✓ Permanent redaction (unrecoverable) + ⏳ Case-insensitive matching (feature in development) + ⏳ Regex pattern support (feature in development) + +USAGE: + npm run redact-keyword -- [keyword2 ...] + +EXAMPLES: + npm run redact-keyword -- contract.pdf redacted.pdf "confidential" "proprietary" + npm run redact-keyword -- report.pdf clean.pdf "Project Zeus" "Client ABC" + */ + +import { program } from 'commander'; +import { stat, mkdir, writeFile, copyFile } from 'fs/promises'; +import { dirname } from 'path'; +import { PlatformAPIClient } from '../api/platform-api.js'; + +program + .name('redact-by-keyword') + .description('Redact specific keywords from PDF documents using text search') + .argument('', 'Input PDF file to redact') + .argument('', 'Output PDF file with redactions') + .argument('', 'Keywords to search for and redact') + .action(async (inputPdf: string, outputPdf: string, keywords: string[]) => { + try { + // Validate input file exists + try { + await stat(inputPdf); + } catch (error) { + console.error(`❌ Error: Input file not found: ${inputPdf}`); + process.exit(1); + } + + // Validate input is a PDF + if (!inputPdf.toLowerCase().endsWith('.pdf')) { + console.error('❌ Error: Input must be a PDF file'); + process.exit(1); + } + + // Create output directory if needed + await mkdir(dirname(outputPdf), { recursive: true }); + + // Initialize API client (loads credentials from .env) + const client = new PlatformAPIClient(); + + // Step 1: Search for keywords in document + console.log(`🔍 Searching for ${keywords.length} keyword(s) in ${inputPdf.split('/').pop()}...`); + console.log(` Keywords: ${keywords.map(k => `"${k}"`).join(', ')}`); + + const bboxData = await client.findTextBoxes(inputPdf, keywords); + + // Extract text box locations from response + const textBoxes = bboxData.result?.textBoxes || []; + + if (textBoxes.length === 0) { + console.log('ℹ️ No keyword matches found - copying original file'); + // Copy original file to output if no keywords found + await copyFile(inputPdf, outputPdf); + console.log(`✅ Saved: ${outputPdf.split('/').pop()}`); + console.log(`📂 Output: ${outputPdf}`); + return; + } + + console.log(`🎯 Found ${textBoxes.length} keyword instance(s) to redact`); + + // Step 2: Prepare redaction coordinates + console.log('🔒 Applying redactions...'); + const redactions = textBoxes.map((box: any) => ({ + pageIndex: box.pageIndex, + boundingBox: box.boundingBox, + })); + + // Step 3: Apply redactions to document + const redactedPdf = await client.redact(inputPdf, redactions); + + // Save redacted PDF + await writeFile(outputPdf, redactedPdf); + + // Display success message + console.log('✅ Redaction successful!'); + console.log(`🔒 Redacted: ${textBoxes.length} instance(s)`); + console.log(`📄 Input: ${inputPdf.split('/').pop()}`); + console.log(`📄 Output: ${outputPdf.split('/').pop()}`); + console.log(`📂 Saved to: ${outputPdf}`); + } catch (error: any) { + console.error(`❌ Redaction FAILED: ${error.message}`); + process.exit(1); + } + }); + +program.parse(); diff --git a/samples/typescript/src/scripts/smart-redact-pii.ts b/samples/typescript/src/scripts/smart-redact-pii.ts new file mode 100644 index 0000000..bbff0e3 --- /dev/null +++ b/samples/typescript/src/scripts/smart-redact-pii.ts @@ -0,0 +1,123 @@ +#!/usr/bin/env node +/** +🔒 SMART PII REDACTION +====================== + +The script exemplifies a typical workflow for protecting sensitive customer information. +As a compliance officer, it's essential to review and redact +personally identifiable information (PII) from documents before sharing them with +third parties, storing them in public systems, or using them for analysis. Manual +redaction is time-consuming and error-prone, potentially missing sensitive data like +social security numbers, phone numbers, addresses, or email addresses. + +This workflow automates compliant document redaction. The script processes each PDF +file individually - for every document in the input folder, it uses AI-powered PII +detection to identify all instances of sensitive information across all pages, then +automatically applies redactions to permanently remove this data. Each processed file +is saved to the output folder with all PII securely redacted, ready for safe sharing +or archival. + +PRIVACY COMPLIANCE STANDARDS: + ✓ AI-powered PII detection (SSN, phone, email, address) + ✓ Automatic redaction (permanent removal) + ✓ Batch processing (entire folders) + +USAGE: + npm run smart-redact -- + +EXAMPLE: + npm run smart-redact -- ../../test_files/test-pdfs ./output + */ + +import { program } from 'commander'; +import { writeFile, copyFile } from 'fs/promises'; +import { join } from 'path'; +import { PlatformAPIClient } from '../api/platform-api.js'; +import { validateAndSetup } from '../helpers/document-helpers.js'; + +program + .name('smart-redact-pii') + .description('Automatically detect and redact PII (personally identifiable information) from PDFs') + .argument('', 'Input folder containing PDF documents') + .argument('', 'Output folder for redacted PDFs') + .action(async (inputFolder: string, outputFolder: string) => { + try { + // Validate and setup (only process PDF files) + const files = await validateAndSetup(inputFolder, outputFolder, ['*.pdf']); + console.log(`📋 Found ${files.length} PDF document(s) to process`); + + // Initialize API client + const client = new PlatformAPIClient(); + + // Process each document + let successCount = 0; + let failedCount = 0; + let totalPiiCount = 0; + + for (let i = 0; i < files.length; i++) { + const pdfFile = files[i]; + const fileName = pdfFile.split('/').pop() || pdfFile; + + console.log(`[${i + 1}/${files.length}] Processing: ${fileName}`); + + try { + // Step 1: Detect PII in the document + console.log(' 🔍 Detecting PII...'); + const piiData = await client.detectPii(pdfFile); + + // Extract PII bounding boxes from response + const piiBoxes = piiData.result?.PIIBoxes || []; + + if (piiBoxes.length === 0) { + console.log(' ℹ️ No PII detected - copying original file'); + + // Copy original file to output if no PII found + const outputFile = join(outputFolder, fileName); + await copyFile(pdfFile, outputFile); + + console.log(` ✅ Saved: ${fileName}`); + successCount++; + continue; + } + + console.log(` 🎯 Found ${piiBoxes.length} PII instance(s)`); + totalPiiCount += piiBoxes.length; + + // Step 2: Prepare redaction coordinates + console.log(' 🔒 Applying redactions...'); + const redactions = piiBoxes.map((box: any) => ({ + pageIndex: box.pageIndex, + boundingBox: box.boundingBox, + })); + + // Step 3: Apply redactions to document + const redactedPdf = await client.redact(pdfFile, redactions); + + // Save redacted PDF + const outputFile = join(outputFolder, fileName); + await writeFile(outputFile, redactedPdf); + + console.log(` ✅ Redacted: ${fileName}`); + successCount++; + } catch (error: any) { + console.log(` ❌ FAILED: ${error.message}`); + failedCount++; + } + } + + // Display summary + console.log('='.repeat(60)); + console.log(`✅ ${successCount} document(s) processed`); + console.log(`🔒 ${totalPiiCount} total PII instance(s) redacted`); + if (failedCount > 0) { + console.log(`⚠️ ${failedCount} document(s) FAILED - review manually!`); + } + console.log(`📂 Output: ${outputFolder}`); + console.log('='.repeat(60)); + } catch (error: any) { + console.error(`❌ Smart redaction FAILED: ${error.message}`); + process.exit(1); + } + }); + +program.parse(); diff --git a/samples/typescript/tsconfig.json b/samples/typescript/tsconfig.json new file mode 100644 index 0000000..72093ae --- /dev/null +++ b/samples/typescript/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022"], + "moduleResolution": "node", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}